diff --git a/Cargo.lock b/Cargo.lock index ff669a9..aa6e9b2 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -120,9 +120,9 @@ dependencies = [ [[package]] name = "async-trait" -version = "0.1.80" +version = "0.1.83" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c6fa2087f2753a7da8cc1c0dbfcf89579dd57458e36769de5ac750b4671737ca" +checksum = "721cae7de5c34fbb2acd27e21e6d2cf7b886dce0c27388d46c4e6c47ea4318dd" dependencies = [ "proc-macro2", "quote", @@ -619,9 +619,10 @@ checksum = "d231dfb89cfffdbc30e7fc41579ed6066ad03abda9e567ccafae602b97ec5024" [[package]] name = "homers" -version = "0.4.2" +version = "0.5.0" dependencies = [ "anyhow", + "async-trait", "chrono", "clap", "clap-verbosity-flag", diff --git a/Cargo.toml b/Cargo.toml index 8024076..4cd8dd8 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,12 +1,13 @@ [package] name = "homers" -version = "0.4.2" +version = "0.5.0" edition = "2021" # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html [dependencies] anyhow = "1.0.79" +async-trait = "0.1.83" chrono = "0.4.34" clap = "4.5.1" clap-verbosity-flag = "2.2.0" diff --git a/Dockerfile b/Dockerfile index d68d25c..0fd32d0 100644 --- a/Dockerfile +++ b/Dockerfile @@ -6,9 +6,8 @@ RUN USER=root cargo new homers WORKDIR /usr/src/homers COPY Cargo.toml Cargo.lock ./ COPY src ./src -RUN cargo build --release -RUN cargo install --path . +RUN cargo install --locked --path . # Bundle Stage FROM debian:trixie-slim diff --git a/Taskfile.yml b/Taskfile.yml index 53c6a08..ae27d2f 100644 --- a/Taskfile.yml +++ b/Taskfile.yml @@ -11,7 +11,7 @@ vars: tasks: build: cmds: - - cmd: docker build -t mcth/homers:{{.DOCKER_TAG}} . + - cmd: docker buildx build --push --platform linux/amd64 -t mcth/homers:{{.DOCKER_TAG}} . push: cmds: - cmd: docker push mcth/homers:{{.DOCKER_TAG}} diff --git a/flake.nix b/flake.nix index 13992d3..8df8738 100644 --- a/flake.nix +++ b/flake.nix @@ -68,7 +68,7 @@ rustc cargo rustfmt - ]; + ]++ lib.optionals stdenv.isDarwin [ darwin.apple_sdk.frameworks.SystemConfiguration ]; }; }); } diff --git a/src/config.rs b/src/config.rs index 6db0eac..d9b4312 100644 --- a/src/config.rs +++ b/src/config.rs @@ -50,6 +50,8 @@ pub enum Task { TautulliLibrary(Tautulli), PlexSession(Plex), PlexLibrary(Plex), + JellyfinSession(Jellyfin), + JellyfinLibrary(Jellyfin), Default, } @@ -123,5 +125,12 @@ pub fn get_tasks(config: Config) -> anyhow::Result> { tasks.push(Task::PlexLibrary(client)); } } + if let Some(jellyfin) = config.jellyfin { + for (name, j) in jellyfin { + let client = Jellyfin::new(&name, remove_trailing_slash(&j.address), &j.api_key)?; + tasks.push(Task::JellyfinSession(client.clone())); + tasks.push(Task::JellyfinLibrary(client)); + } + } Ok(tasks) } diff --git a/src/http_server.rs b/src/http_server.rs index eb6d55b..6f42f79 100644 --- a/src/http_server.rs +++ b/src/http_server.rs @@ -3,7 +3,6 @@ use futures::future::try_join_all; use log::{error, info}; use rocket::http::{Accept, ContentType, Status}; use rocket::tokio::task; -use rocket::tokio::task::JoinSet; use rocket::{get, routes, Build, Responder, Rocket, State}; use std::collections::HashMap; use std::process::exit; @@ -113,6 +112,19 @@ async fn process_tasks(tasks: Vec) -> Result, JoinError> { let result = HashMap::from([(name.to_string(), result)]); Ok(TaskResult::PlexLibrary(result)) } + Task::JellyfinSession(jellyfin) => { + let name = &jellyfin.name; + let result = jellyfin.get_current_sessions().await; + let result = HashMap::from([(name.to_string(), result)]); + let users = jellyfin.get_users().await; + Ok(TaskResult::JellyfinSession(result, users)) + } + Task::JellyfinLibrary(jellyfin) => { + let name = &jellyfin.name; + let result = jellyfin.get_library().await; + let result = HashMap::from([(name.to_string(), result)]); + Ok(TaskResult::JellyfinLibrary(result)) + } Task::Default => Ok(TaskResult::Default), } }) @@ -142,40 +154,6 @@ async fn serve_metrics(format: Format, unscheduled_tasks: &State>) -> ) } } - //let mut join_set = JoinSet::new(); - //for task in unscheduled_tasks.iter().cloned() { - // join_set.spawn(process_task(task)); - //} - - //wait_for_metrics(format, join_set).await.map_or_else( - // |e| { - // error!("General error while fetching providers data: {e}"); - // MetricsResponse::new( - // Status::InternalServerError, - // format, - // "Error while fetching providers data. Check the logs".into(), - // ) - // }, - // |metrics| MetricsResponse::new(Status::Ok, format, metrics), - //) -} - -async fn wait_for_metrics( - _format: Format, - mut join_set: JoinSet>, -) -> anyhow::Result { - let mut tasks: Vec = Vec::new(); - while let Some(result) = join_set.join_next().await { - match result? { - Ok(tr) => { - tasks.push(tr); - } - Err(e) => { - error!("Error while fetching metrics: {e}"); - } - } - } - format_metrics(tasks) } const fn get_content_type_params(version: &str) -> [(&str, &str); 2] { diff --git a/src/main.rs.test b/src/main.rs.test index b36a753..6579f71 100644 --- a/src/main.rs.test +++ b/src/main.rs.test @@ -82,21 +82,23 @@ async fn main() -> Result<()> { .log_level() .expect("Log level cannot be not available"); let config = config::read(args.config.clone(), log_level)?; - //let jelly_conf = config.jellyfin.expect("Jellyfin config not found"); - //for (name, j) in jelly_conf { - // let jellyfin = Jellyfin::new(&name, &j.address, &j.api_key)?; - // let session = jellyfin.get_sessions().await?; - // println!("{:?}", session); - //} - - let plex = config.plex.expect("plex config not found"); - for (name, p) in plex { - let plex = providers::plex::Plex::new(&name, &p.address, &p.token)?; - let users = plex.get_users().await; - for user in users { - println!("{:?}", user); - } + let jelly_conf = config.jellyfin.expect("Jellyfin config not found"); + for (name, j) in jelly_conf { + let jellyfin = Jellyfin::new(&name, &j.address, &j.api_key)?; + let session = jellyfin.get_sessions().await?; + println!("{:?}", session); + let library_counts = jellyfin.get_library_counts().await?; + println!("{:?}", library_counts); } + + //let plex = config.plex.expect("plex config not found"); + //for (name, p) in plex { + // let plex = providers::plex::Plex::new(&name, &p.address, &p.token)?; + // let users = plex.get_users().await; + // for user in users { + // println!("{:?}", user); + // } + //} // let history = match plex.get_history().await { // Ok(history) => history, // Err(e) => { diff --git a/src/prometheus.rs b/src/prometheus.rs index ca73877..f801fb4 100644 --- a/src/prometheus.rs +++ b/src/prometheus.rs @@ -8,11 +8,12 @@ use std::collections::HashMap; use std::sync::atomic::AtomicU64; use crate::providers::overseerr::OverseerrRequest; -use crate::providers::plex::{LibraryInfos, PlexSessions, User as PlexUser}; use crate::providers::radarr::RadarrMovie; use crate::providers::sonarr::SonarrEpisode; -use crate::providers::structs::plex::BandwidthLocation; use crate::providers::structs::tautulli::Library; +use crate::providers::structs::{ + BandwidthLocation, LibraryCount, MediaType as LibraryMediaType, Session, User, +}; use crate::providers::tautulli::SessionSummary; #[derive(PartialEq, Debug, Eq, Copy, Clone)] @@ -28,18 +29,20 @@ pub enum TaskResult { TautulliLibrary(Vec), Radarr(HashMap>), Overseerr(Vec), - PlexSession(HashMap>, Vec), - PlexLibrary(HashMap>), + PlexSession(HashMap>, Vec), + PlexLibrary(HashMap>), + JellyfinSession(HashMap>, Vec), + JellyfinLibrary(HashMap>), Default, } #[derive(Clone, Hash, Eq, PartialEq, EncodeLabelSet, Debug)] -struct PlexSessionBandwidth { +struct SessionBandwidth { pub name: String, pub location: String, } #[derive(Clone, Hash, Eq, PartialEq, EncodeLabelSet, Debug)] -struct PlexSessionLabels { +struct SessionLabels { pub name: String, pub title: String, pub user: String, @@ -169,10 +172,16 @@ pub fn format_metrics(task_result: Vec) -> anyhow::Result { TaskResult::Radarr(movies) => format_radarr_metrics(movies, &mut registry), TaskResult::Overseerr(overseerr) => format_overseerr_metrics(overseerr, &mut registry), TaskResult::PlexSession(sessions, users) => { - format_plex_session_metrics(sessions, users, &mut registry) + format_session_metrics("plex", sessions, users, &mut registry) } TaskResult::PlexLibrary(libraries) => { - format_plex_library_metrics(libraries, &mut registry) + format_library_metrics("plex", libraries, &mut registry) + } + TaskResult::JellyfinSession(sessions, users) => { + format_session_metrics("jellyfin", sessions, users, &mut registry) + } + TaskResult::JellyfinLibrary(libraries) => { + format_library_metrics("jellyfin", libraries, &mut registry) } TaskResult::Default => return Err(anyhow::anyhow!("No task result")), } @@ -332,42 +341,12 @@ fn format_radarr_metrics(radarr_hash: HashMap>, registr fn format_overseerr_metrics(requests: Vec, registry: &mut Registry) { debug!("Formatting {requests:?} as Prometheus"); let overseerr_request = Family::>::default(); - //let mut registy_request = HashMap::new(); - //let mut registy_media = HashMap::new(); registry.register( "overseerr_requests", format!("overseerr requests status"), overseerr_request.clone(), ); - /* - overseerr::MediaStatus::get_all() - .into_iter() - .for_each(|status| { - registy_media.insert( - status.to_string(), - Family::>::default(), - ); - registry.register( - &format!("overseerr_requests_{}", status.to_string()), - format!("{}", status.to_description()), - registy_media.get(&status.to_string()).unwrap().clone(), - ); - }); - overseerr::RequestStatus::get_all() - .into_iter() - .for_each(|status| { - registy_request.insert( - status.to_string(), - Family::>::default(), - ); - registry.register( - &format!("overseerr_requests_{}", status.to_string()), - format!("{}", status.to_description()), - registy_request.get(&status.to_string()).unwrap().clone(), - ); - }); - */ requests.into_iter().for_each(|request| { let labels = OverseerrLabels { media_type: request.media_type.clone(), @@ -380,62 +359,67 @@ fn format_overseerr_metrics(requests: Vec, registry: &mut Regi overseerr_request .get_or_create(&labels) .set(request.status.as_f64()); - /*match request.status.into() { - overseerr::RequestStatus::Pending => { - registy_request - .get(&overseerr::RequestStatus::Pending.to_string()) - .unwrap() - .get_or_create(&OverseerrRequestsLabels { - kind: overseerr::RequestStatus::Pending.to_string(), - }) - .inc(); - } - overseerr::RequestStatus::Approved => { - registy_request - .get(&overseerr::RequestStatus::Approved.to_string()) - .unwrap() - .get_or_create(&OverseerrRequestsLabels { - kind: overseerr::RequestStatus::Approved.to_string(), - }) - .inc(); - } - overseerr::RequestStatus::Declined => { - registy_request - .get(&overseerr::RequestStatus::Declined.to_string()) - .unwrap() - .get_or_create(&OverseerrRequestsLabels { - kind: overseerr::RequestStatus::Declined.to_string(), - }) - .inc(); - } - };*/ }); } -fn format_plex_session_metrics( - sessions: HashMap>, - users: Vec, +fn format_session_metrics( + kind: &str, + sessions: HashMap>, + users: Vec, registry: &mut Registry, ) { debug!("Formatting {sessions:?} as Prometheus"); - let plex_sessions = Family::>::default(); - let plex_sessions_percentage = Family::>::default(); - let plex_session_bandwidth = Family::>::default(); - registry.register( - "plex_sessions", - format!("Plex sessions status"), - plex_sessions.clone(), - ); - registry.register( - "plex_sessions_percentage", - format!("Plex sessions percentage status"), - plex_sessions_percentage.clone(), - ); - registry.register( - "plex_session_bandwidth", - format!("Plex session bandwidth"), - plex_session_bandwidth.clone(), - ); + let sessions_labels = Family::>::default(); + let sessions_percentage = Family::>::default(); + let session_bandwidth = Family::>::default(); + match kind { + "plex" => { + registry.register( + "plex_sessions", + format!("Plex sessions status"), + sessions_labels.clone(), + ); + registry.register( + "plex_sessions_percentage", + format!("Plex sessions percentage status"), + sessions_percentage.clone(), + ); + registry.register( + "plex_session_bandwidth", + format!("Plex session bandwidth"), + session_bandwidth.clone(), + ); + } + "jellyfin" => { + registry.register( + "jellyfin_sessions", + format!("Jellyfin sessions status"), + sessions_labels.clone(), + ); + registry.register( + "jellyfin_sessions_percentage", + format!("Jellyfin sessions percentage status"), + sessions_percentage.clone(), + ); + } + _ => { + registry.register( + "sessions", + format!("Sessions status"), + sessions_labels.clone(), + ); + registry.register( + "sessions_percentage", + format!("Sessions percentage status"), + sessions_percentage.clone(), + ); + registry.register( + "session_bandwidth", + format!("Session bandwidth"), + session_bandwidth.clone(), + ); + } + } let mut wan_bandwidth = 0.0; let mut lan_bandwidth = 0.0; let mut inactive_users = users; @@ -444,9 +428,10 @@ fn format_plex_session_metrics( match session.bandwidth.location { BandwidthLocation::Wan => wan_bandwidth += session.bandwidth.bandwidth as f64, BandwidthLocation::Lan => lan_bandwidth += session.bandwidth.bandwidth as f64, + BandwidthLocation::Unknown => {} }; - inactive_users.retain(|user| user.title != session.user); - let session_labels = PlexSessionLabels { + inactive_users.retain(|user| user.name != session.user); + let session_labels = SessionLabels { name: name.clone(), title: session.title, user: session.user, @@ -467,17 +452,17 @@ fn format_plex_session_metrics( latitude: session.location.latitude, }; - plex_sessions_percentage + sessions_percentage .get_or_create(&session_labels) .set(session.progress as f64); - plex_sessions.get_or_create(&session_labels).set(1.0); + sessions_labels.get_or_create(&session_labels).set(1.0); }); inactive_users.iter().for_each(|user| { - plex_sessions - .get_or_create(&PlexSessionLabels { + sessions_labels + .get_or_create(&SessionLabels { name: name.to_string(), title: "".to_string(), - user: user.title.clone(), + user: user.name.clone(), decision: "".to_string(), state: "inactive".to_string(), platform: "".to_string(), @@ -496,61 +481,85 @@ fn format_plex_session_metrics( }) .set(0.0); }); - plex_session_bandwidth - .get_or_create(&PlexSessionBandwidth { - name: name.clone(), - location: "LAN".to_string(), - }) - .set(lan_bandwidth); - plex_session_bandwidth - .get_or_create(&PlexSessionBandwidth { - name, - location: "WAN".to_string(), - }) - .set(wan_bandwidth); + match kind { + "plex" => { + session_bandwidth + .get_or_create(&SessionBandwidth { + name: name.clone(), + location: "LAN".to_string(), + }) + .set(lan_bandwidth); + session_bandwidth + .get_or_create(&SessionBandwidth { + name, + location: "WAN".to_string(), + }) + .set(wan_bandwidth); + } + _ => {} + } }); } -fn format_plex_library_metrics( - libraries: HashMap>, +fn format_library_metrics( + kind: &str, + libraries: HashMap>, registry: &mut Registry, ) { debug!("Formatting {libraries:?} as Prometheus"); - let plex_movie_count = Family::>::default(); - let plex_show_count = Family::>::default(); - let plex_season_count = Family::>::default(); - let plex_episode_count = Family::>::default(); - let plex_show_library = Family::>::default(); - let plex_library = Family::>::default(); - registry.register( - "plex_library", - format!("Plex library status"), - plex_library.clone(), - ); - registry.register( - "plex_show_library", - format!("Plex show library status"), - plex_show_library.clone(), - ); - registry.register( - "plex_movie_count", - format!("Plex movie count"), - plex_movie_count.clone(), - ); - registry.register( - "plex_show_count", - format!("Plex show count"), - plex_show_count.clone(), - ); - registry.register( - "plex_season_count", - format!("Plex season count"), - plex_season_count.clone(), - ); - registry.register( - "plex_episode_count", - format!("Plex episode count"), - plex_episode_count.clone(), - ); + let movie_count_label = Family::>::default(); + let show_count_label = Family::>::default(); + let season_count_label = Family::>::default(); + let episode_count_label = Family::>::default(); + let show_library_label = Family::>::default(); + let library_label = Family::>::default(); + match kind { + "plex" => { + registry.register( + "plex_movie_count", + "Plex movie count", + movie_count_label.clone(), + ); + registry.register( + "plex_show_count", + "Plex show count", + show_count_label.clone(), + ); + registry.register( + "plex_season_count", + "Plex season count", + season_count_label.clone(), + ); + registry.register( + "plex_episode_count", + "Plex episode count", + episode_count_label.clone(), + ); + registry.register( + "plex_show_library", + "Plex show library", + show_library_label.clone(), + ); + registry.register("plex_library", "Plex library", library_label.clone()); + } + "jellyfin" => { + registry.register( + "jellyfin_movie_count", + "Jellyfin movie count", + movie_count_label.clone(), + ); + registry.register( + "jellyfin_show_count", + "Jellyfin show count", + show_count_label.clone(), + ); + registry.register( + "jellyfin_episode_count", + "Jellyfin episode count", + episode_count_label.clone(), + ); + } + _ => {} + } let mut movie_count = 0; let mut episode_count = 0; let mut season_count = 0; @@ -559,48 +568,48 @@ fn format_plex_library_metrics( library.into_iter().for_each(|lib| { let library_labels = PlexLibraryLabels { name: name.clone(), - library_name: lib.library_name.clone(), - library_type: lib.library_type.clone(), + library_name: lib.name.clone(), + library_type: lib.media_type.to_string(), }; - match lib.library_type.as_str() { - "movie" => { - movie_count += lib.library_size; - plex_library + match lib.media_type { + LibraryMediaType::Movie => { + movie_count += lib.count; + library_label .get_or_create(&library_labels) - .set(lib.library_size as f64); + .set(lib.count as f64); } - "show" => { - plex_show_library + LibraryMediaType::Show => { + show_library_label .get_or_create(&PlexShowLabels { name: name.clone(), - library_name: lib.library_name.clone(), - library_type: lib.library_type.clone(), - season_count: lib.library_child_size, - episode_count: lib.library_grand_child_size, + library_name: lib.name.clone(), + library_type: lib.media_type.to_string(), + season_count: lib.child_count, + episode_count: lib.grand_child_count, }) - .set(lib.library_size as f64); - episode_count += lib.library_grand_child_size.unwrap_or(0); - season_count += lib.library_child_size.unwrap_or(0); - show_count += lib.library_size + .set(lib.count as f64); + episode_count += lib.grand_child_count.unwrap_or(0); + season_count += lib.child_count.unwrap_or(0); + show_count += lib.count } _ => { - plex_library + library_label .get_or_create(&library_labels) - .set(lib.library_size as f64); + .set(lib.count as f64); } }; }); }); - plex_movie_count + movie_count_label .get_or_create(&EmptyLabel {}) .set(movie_count as f64); - plex_show_count + show_count_label .get_or_create(&EmptyLabel {}) .set(show_count as f64); - plex_season_count + season_count_label .get_or_create(&EmptyLabel {}) .set(season_count as f64); - plex_episode_count + episode_count_label .get_or_create(&EmptyLabel {}) .set(episode_count as f64); } diff --git a/src/providers/jellyfin.rs b/src/providers/jellyfin.rs index 7d082bb..0c0c084 100644 --- a/src/providers/jellyfin.rs +++ b/src/providers/jellyfin.rs @@ -1,8 +1,12 @@ +use crate::providers::structs::AsyncFrom; +use log::error; use reqwest::header; use serde::{Deserialize, Serialize}; -use serde_json::Value; -use crate::providers::structs::Session; +use crate::providers::structs::jellyfin::{ + JellyfinLibraryCounts, SessionResponse, User as JellyfinUser, +}; +use crate::providers::structs::{LibraryCount, Session, User}; use crate::providers::{Provider, ProviderError, ProviderErrorKind}; #[derive(Debug, Deserialize, Clone, Serialize)] @@ -43,7 +47,7 @@ impl Jellyfin { }) } - pub async fn get_sessions(&self) -> Result, ProviderError> { + async fn get_sessions(&self) -> Result, ProviderError> { let url = format!("{}/Sessions", self.address); let response = match self.client.get(&url).send().await { Ok(response) => response, @@ -55,7 +59,7 @@ impl Jellyfin { )); } }; - let sessions: Vec = match response.json().await { + let sessions: Vec = match response.json().await { Ok(sessions) => sessions, Err(e) => { return Err(ProviderError::new( @@ -65,6 +69,75 @@ impl Jellyfin { )); } }; - Ok(sessions) + let mut jelly_sessions: Vec = Vec::new(); + for session in sessions { + let session = Session::from_async(session).await; + jelly_sessions.push(session); + } + Ok(jelly_sessions) + } + pub async fn get_current_sessions(&self) -> Vec { + match self.get_sessions().await { + Ok(sessions) => sessions, + Err(e) => { + error!("Failed to get sessions: {}", e); + Vec::new() + } + } + } + pub async fn get_users(&self) -> Vec { + let url = format!("{}/Users", self.address); + let response = match self.client.get(&url).send().await { + Ok(response) => response, + Err(e) => { + error!("Failed to get users: {}", e); + return Vec::new(); + } + }; + let users: Vec = match response.json().await { + Ok(users) => users, + Err(e) => { + error!("Failed to parse users: {}", e); + return Vec::new(); + } + }; + users.into_iter().map(User::from).collect() + } + async fn get_library_counts(&self) -> Result { + let url = format!("{}/Items/Counts", self.address); + let response = match self.client.get(&url).send().await { + Ok(response) => response, + Err(e) => { + return Err(ProviderError::new( + Provider::Jellyfin, + ProviderErrorKind::GetError, + &format!("{:?}", e), + )); + } + }; + let library_counts: JellyfinLibraryCounts = match response.json().await { + Ok(library_counts) => library_counts, + Err(e) => { + return Err(ProviderError::new( + Provider::Jellyfin, + ProviderErrorKind::ParseError, + &format!("{:?}", e), + )); + } + }; + Ok(library_counts) + } + pub async fn get_library(&self) -> Vec { + let library_infos = match self.get_library_counts().await { + Ok(library_counts) => library_counts.into(), + Err(e) => { + error!("Failed to get library counts: {}", e); + Vec::new() + } + }; + library_infos + .into_iter() + .map(|library| library.into()) + .collect() } } diff --git a/src/providers/plex.rs b/src/providers/plex.rs index 48416b9..c629518 100644 --- a/src/providers/plex.rs +++ b/src/providers/plex.rs @@ -1,10 +1,12 @@ +use crate::providers::structs::AsyncFrom; use log::{debug, error}; use reqwest; use reqwest::header; use serde::{Deserialize, Serialize}; -pub use crate::providers::structs::plex::{MediaContainer, PlexSessions, User}; +pub use crate::providers::structs::plex::{LibraryInfos, MediaContainer}; use crate::providers::structs::plex::{Metadata, PlexResponse, StatUser}; +use crate::providers::structs::{LibraryCount, Session, User}; use crate::providers::{Provider, ProviderError, ProviderErrorKind}; #[derive(Debug, Deserialize, Clone, Serialize)] @@ -12,14 +14,6 @@ pub struct PlexViews { pub episodes_viewed: i64, pub movies_viewed: i64, } -#[derive(Debug, Deserialize, Clone, Serialize)] -pub struct LibraryInfos { - pub library_name: String, - pub library_type: String, - pub library_size: i64, - pub library_child_size: Option, - pub library_grand_child_size: Option, -} #[derive(Debug, Deserialize, Clone, Serialize)] pub struct Plex { @@ -108,7 +102,7 @@ impl Plex { Ok(library_items) } - pub async fn get_all_library_size(&self) -> Vec { + pub async fn get_all_library_size(&self) -> Vec { let libraries = match self.get_all_libraries().await { Ok(libraries) => libraries, Err(e) => { @@ -175,10 +169,10 @@ impl Plex { }), } } - library_infos + library_infos.into_iter().map(|item| item.into()).collect() } - pub async fn get_current_sessions(&self) -> Vec { + pub async fn get_current_sessions(&self) -> Vec { let sessions = match self.get_sessions().await { Ok(sessions) => sessions, Err(e) => { @@ -186,7 +180,7 @@ impl Plex { return Vec::new(); } }; - let mut current_sessions: Vec = Vec::new(); + let mut current_sessions: Vec = Vec::new(); let activity_container = match sessions.media_container { MediaContainer::ActivityContainer(activity_container) => activity_container, _ => { @@ -194,10 +188,11 @@ impl Plex { return Vec::new(); } }; - for item in activity_container.metadata.iter() { + for item in activity_container.metadata.into_iter() { match item { Metadata::SessionMetadata(meta) => { - current_sessions.push(meta.to().await); + let session = Session::from_async(meta).await; + current_sessions.push(session); } _ => { error!("Metadata received does not match session metadata"); diff --git a/src/providers/structs.rs b/src/providers/structs.rs index f88cf72..bd0799f 100644 --- a/src/providers/structs.rs +++ b/src/providers/structs.rs @@ -1,3 +1,5 @@ +use async_trait::async_trait; +use ipgeolocate::{Locator, Service}; use serde::{Deserialize, Serialize}; use std::fmt::Display; pub mod jellyfin; @@ -7,6 +9,53 @@ pub mod radarr; pub mod sonarr; pub mod tautulli; +#[async_trait] +pub trait AsyncFrom: Sized { + async fn from_async(value: T) -> Self; +} + +async fn get_ip_info(ip: &str) -> Location { + let service = Service::IpApi; + match Locator::get(ip, service).await { + Ok(location) => Location { + city: location.city, + country: location.country, + ip_address: ip.to_string(), + latitude: location.latitude, + longitude: location.longitude, + }, + Err(_) => Location { + city: "Unknown".to_string(), + country: "Unknown".to_string(), + ip_address: ip.to_string(), + latitude: "0.0".to_string(), + longitude: "0.0".to_string(), + }, + } +} + +#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)] +pub struct User { + pub name: String, +} +impl From for User { + fn from(user: plex::User) -> Self { + User { name: user.title } + } +} +impl From for User { + fn from(user: jellyfin::User) -> Self { + User { name: user.name } + } +} +impl From for User { + fn from(stat_user: plex::StatUser) -> Self { + User { + name: stat_user.name, + } + } +} + #[derive(Debug, Clone, PartialEq, Serialize, Deserialize)] pub struct Session { pub title: String, @@ -14,7 +63,7 @@ pub struct Session { pub stream_decision: StreamDecision, pub media_type: String, pub state: String, - pub progress: i64, + pub progress: f64, pub quality: String, pub season_number: Option, pub episode_number: Option, @@ -26,28 +75,48 @@ pub struct Session { pub platform: String, pub bandwidth: Bandwidth, } -impl From for Session { - fn from(session: jellyfin::SessionResponse) -> Self { +#[async_trait] +impl AsyncFrom for Session { + async fn from_async(session: jellyfin::SessionResponse) -> Self { let mut title = "".to_string(); let mut media_type = "Unknown".to_string(); + let mut quality = "".to_string(); match &session.now_playing_item { Some(item) => { title = item.name.clone(); media_type = item.type_field.clone(); + let media_stream = &item + .media_streams + .iter() + .find(|stream| stream.type_field == "Video"); + quality = match media_stream { + Some(stream) => stream.display_title.clone(), + None => "Unknown".to_string(), + } } None => (), }; - let progress = match &session.play_state.posititon_ticks { - Some(progress) => (progress / &session.now_playing_item.unwrap().run_time_ticks) * 100, - None => 0, + let progress = match &session.play_state.position_ticks { + Some(position) => match &session.now_playing_item { + Some(item) => (*position as f64 / item.run_time_ticks as f64) * 100.0, + None => 0.0, + }, + None => 0.0, }; let state = match &session.play_state.is_paused { Some(paused) => match paused { true => "Paused", - false => "Playing", + false => { + if session.now_playing_item.is_some() { + "Playing" + } else { + "Idle" + } + } }, - None => "", + None => "Idle", }; + let location = get_ip_info(&session.remote_end_point).await; let stream_decision = match &session.play_state.play_method { Some(method) => match method.as_str() { "DirectPlay" => StreamDecision::DirectPlay, @@ -63,7 +132,7 @@ impl From for Session { }, _ => StreamDecision::Transcode, }, - None => StreamDecision::Transcode, + None => StreamDecision::None, }; Session { @@ -73,28 +142,88 @@ impl From for Session { media_type, state: state.to_string(), progress, - quality: "".to_string(), + quality, season_number: None, episode_number: None, address: session.remote_end_point, - location: Location { - city: "".to_string(), - country: "".to_string(), - ip_address: "".to_string(), - latitude: "".to_string(), - longitude: "".to_string(), - }, + location, local: false, secure: false, relayed: false, platform: session.client, bandwidth: Bandwidth { - bandwidth: 0, - location: BandwidthLocation::Wan, + bandwidth: -1, + location: BandwidthLocation::Unknown, }, } } } +#[async_trait] +impl AsyncFrom for Session { + async fn from_async(session: plex::SessionMetadata) -> Self { + let media_type = session.type_field.clone(); + let user = session.user.title.clone(); + let state = session.player.state_field.clone(); + let progress = session.progress(); + let part = &session.media[0].part[0]; + let video_stream: &plex::Stream = &part.stream.iter().find(|s| s.stream_type == 1).unwrap(); + let quality = video_stream.display_title.to_string(); + let season_number = match session.parent_index { + Some(index) => Some(index.to_string()), + None => None, + }; + let episode_number = match session.index { + Some(index) => Some(index.to_string()), + None => None, + }; + let location = get_ip_info(&session.player.remote_public_address).await; + let decision = part.decision.clone(); + let video_stream_decision = match &video_stream.decision { + Some(decision) => decision.to_string(), + None => "transcode".to_string(), + }; + let stream_decision = match decision.as_str() { + "directplay" => StreamDecision::DirectPlay, + "transcode" => match video_stream_decision.as_str() { + "copy" => StreamDecision::DirectStream, + _ => StreamDecision::Transcode, + }, + _ => StreamDecision::Transcode, + }; + let address = session.player.address.clone(); + let local = session.player.local; + let secure = session.player.secure; + let relayed = session.player.relayed; + let platform = session.player.platform.clone(); + let title = match &session.grand_parent_title { + Some(parent) => parent.to_string(), + None => session.title.clone(), + }; + let bandwidth = Bandwidth { + bandwidth: session.session.bandwidth, + location: session.session.location.clone().into(), + }; + Session { + title, + user, + stream_decision, + media_type, + state, + progress: progress as f64, + quality, + season_number, + episode_number, + location, + address, + local, + secure, + relayed, + platform, + bandwidth, + } + } +} + #[derive(Debug, Clone, PartialEq, Serialize, Deserialize)] pub struct Bandwidth { pub bandwidth: i64, @@ -105,13 +234,14 @@ pub struct Bandwidth { pub enum BandwidthLocation { Wan, Lan, + Unknown, } impl From for BandwidthLocation { fn from(location: String) -> Self { match location.as_str() { "wan" => BandwidthLocation::Wan, "lan" => BandwidthLocation::Lan, - _ => BandwidthLocation::Wan, + _ => BandwidthLocation::Unknown, } } } @@ -120,6 +250,7 @@ impl Display for BandwidthLocation { match self { BandwidthLocation::Wan => write!(f, "WAN"), BandwidthLocation::Lan => write!(f, "LAN"), + BandwidthLocation::Unknown => write!(f, "Undefined"), } } } @@ -129,6 +260,7 @@ pub enum StreamDecision { DirectPlay, DirectStream, Transcode, + None, } impl From for StreamDecision { fn from(decision: String) -> Self { @@ -146,6 +278,7 @@ impl Display for StreamDecision { StreamDecision::DirectPlay => write!(f, "Direct Play"), StreamDecision::DirectStream => write!(f, "Direct Stream"), StreamDecision::Transcode => write!(f, "Transcode"), + StreamDecision::None => write!(f, "None"), } } } @@ -157,3 +290,64 @@ pub struct Location { pub latitude: String, pub longitude: String, } + +#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)] +pub enum MediaType { + Movie, + Show, + Music, + Book, + Unknown, +} +impl From for MediaType { + fn from(media_type: String) -> Self { + match media_type.as_str() { + "movie" => MediaType::Movie, + "show" | "shows" => MediaType::Show, + "music" => MediaType::Music, + "book" => MediaType::Book, + _ => MediaType::Unknown, + } + } +} +impl ToString for MediaType { + fn to_string(&self) -> String { + match self { + MediaType::Movie => "Movie".to_string(), + MediaType::Show => "Show".to_string(), + MediaType::Music => "Music".to_string(), + MediaType::Book => "Book".to_string(), + MediaType::Unknown => "Unknown".to_string(), + } + } +} +#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)] +pub struct LibraryCount { + pub name: String, + pub media_type: MediaType, + pub count: i64, + pub child_count: Option, + pub grand_child_count: Option, +} +impl From for LibraryCount { + fn from(library: plex::LibraryInfos) -> Self { + LibraryCount { + name: library.library_name, + media_type: library.library_type.into(), + count: library.library_size, + child_count: library.library_child_size, + grand_child_count: library.library_grand_child_size, + } + } +} +impl From for LibraryCount { + fn from(counts: jellyfin::LibraryInfos) -> Self { + LibraryCount { + name: counts.name, + media_type: counts.library_type.to_lowercase().into(), + count: counts.count, + child_count: counts.child_count, + grand_child_count: counts.grand_child_count, + } + } +} diff --git a/src/providers/structs/jellyfin.rs b/src/providers/structs/jellyfin.rs index 24c55c8..edc2492 100644 --- a/src/providers/structs/jellyfin.rs +++ b/src/providers/structs/jellyfin.rs @@ -15,7 +15,7 @@ pub struct SessionResponse { #[derive(Default, Debug, Clone, PartialEq, Serialize, Deserialize)] #[serde(rename_all = "PascalCase")] pub struct PlayState { - pub posititon_ticks: Option, + pub position_ticks: Option, pub is_paused: Option, pub play_method: Option, } @@ -34,4 +34,86 @@ pub struct NowPlayingItem { pub run_time_ticks: i64, #[serde(rename = "Type")] pub type_field: String, + pub media_streams: Vec, +} + +#[derive(Default, Debug, Clone, PartialEq, Serialize, Deserialize)] +#[serde(rename_all = "PascalCase")] +pub struct MediaStream { + pub codec: String, + #[serde(rename = "Type")] + pub type_field: String, + pub display_title: String, +} + +#[derive(Default, Debug, Clone, PartialEq, Serialize, Deserialize)] +#[serde(rename_all = "PascalCase")] +pub struct JellyfinLibraryCounts { + pub movie_count: i64, + pub series_count: i64, + pub episode_count: i64, + pub artist_count: i64, + pub program_count: i64, + pub trailer_count: i64, + pub song_count: i64, + pub album_count: i64, + pub music_video_count: i64, + pub box_set_count: i64, + pub book_count: i64, + pub item_count: i64, +} + +#[derive(Default, Debug, Clone, PartialEq, Serialize, Deserialize)] +#[serde(rename_all = "PascalCase")] +pub struct Users { + pub users: Vec, +} + +#[derive(Default, Debug, Clone, PartialEq, Serialize, Deserialize)] +#[serde(rename_all = "PascalCase")] +pub struct User { + pub name: String, +} + +#[derive(Default, Debug, Clone, PartialEq, Serialize, Deserialize)] +pub struct LibraryInfos { + pub name: String, + pub library_type: String, + pub count: i64, + pub child_count: Option, + pub grand_child_count: Option, +} +impl From for Vec { + fn from(counts: JellyfinLibraryCounts) -> Self { + vec![ + LibraryInfos { + name: "Movies".to_string(), + library_type: "Movie".to_string(), + count: counts.movie_count, + child_count: None, + grand_child_count: None, + }, + LibraryInfos { + name: "Shows".to_string(), + library_type: "Shows".to_string(), + count: counts.series_count, + child_count: None, + grand_child_count: Some(counts.episode_count), + }, + LibraryInfos { + name: "Music".to_string(), + library_type: "Music".to_string(), + count: counts.album_count, + child_count: Some(counts.artist_count), + grand_child_count: Some(counts.song_count), + }, + LibraryInfos { + name: "Books".to_string(), + library_type: "Book".to_string(), + count: counts.book_count, + child_count: None, + grand_child_count: None, + }, + ] + } } diff --git a/src/providers/structs/plex.rs b/src/providers/structs/plex.rs index 481239f..cb16cf7 100644 --- a/src/providers/structs/plex.rs +++ b/src/providers/structs/plex.rs @@ -1,4 +1,3 @@ -use ipgeolocate::{Locator, Service}; use serde::{Deserialize, Serialize}; use std::fmt::Display; @@ -28,28 +27,6 @@ pub enum Metadata { Default(serde_json::Value), } -#[derive(Debug, Default, Clone, PartialEq, Serialize, Deserialize)] -#[serde(rename_all = "lowercase")] -pub enum LibraryType { - Show, - Movie, - Photo, - Artist, - #[default] - Default, -} -impl Display for LibraryType { - fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - match self { - LibraryType::Show => write!(f, "Show"), - LibraryType::Movie => write!(f, "Movie"), - LibraryType::Photo => write!(f, "Photo"), - LibraryType::Artist => write!(f, "Music"), - LibraryType::Default => write!(f, "Unknown"), - } - } -} - #[derive(Default, Debug, Clone, PartialEq, Serialize, Deserialize)] #[serde(rename_all = "camelCase")] pub struct ActivityContainer { @@ -143,68 +120,6 @@ impl SessionMetadata { let progress = (offset as f64 / duration as f64) * 100.0; progress as i64 } - pub async fn to(&self) -> PlexSessions { - let media_type = self.type_field.clone(); - let user = self.user.title.clone(); - let state = self.player.state_field.clone(); - let progress = self.progress(); - let part = &self.media[0].part[0]; - let video_stream: &Stream = &part.stream.iter().find(|s| s.stream_type == 1).unwrap(); - let quality = video_stream.display_title.to_string(); - let season_number = match self.parent_index { - Some(index) => Some(index.to_string()), - None => None, - }; - let episode_number = match self.index { - Some(index) => Some(index.to_string()), - None => None, - }; - let location = get_ip_info(&self.player.remote_public_address).await; - let decision = part.decision.clone(); - let video_stream_decision = match &video_stream.decision { - Some(decision) => decision.to_string(), - None => "transcode".to_string(), - }; - let stream_decision = match decision.as_str() { - "directplay" => StreamDecision::DirectPlay, - "transcode" => match video_stream_decision.as_str() { - "copy" => StreamDecision::DirectStream, - _ => StreamDecision::Transcode, - }, - _ => StreamDecision::Transcode, - }; - let address = self.player.address.clone(); - let local = self.player.local; - let secure = self.player.secure; - let relayed = self.player.relayed; - let platform = self.player.platform.clone(); - let title = match &self.grand_parent_title { - Some(parent) => parent.to_string(), - None => self.title.clone(), - }; - let bandwidth = Bandwidth { - bandwidth: self.session.bandwidth, - location: self.session.location.clone().into(), - }; - PlexSessions { - title, - user, - stream_decision, - media_type, - state, - progress, - quality, - season_number, - episode_number, - location, - address, - local, - secure, - relayed, - platform, - bandwidth, - } - } } #[derive(Default, Debug, Clone, PartialEq, Serialize, Deserialize)] @@ -214,7 +129,6 @@ pub struct Media { pub part: Vec, pub duration: i64, } - #[derive(Default, Debug, Clone, PartialEq, Serialize, Deserialize)] #[serde(rename_all = "camelCase")] pub struct Part { @@ -278,74 +192,6 @@ pub struct Location { pub longitude: String, } -async fn get_ip_info(ip: &str) -> Location { - let service = Service::IpApi; - match Locator::get(ip, service).await { - Ok(location) => Location { - city: location.city, - country: location.country, - ip_address: ip.to_string(), - latitude: location.latitude, - longitude: location.longitude, - }, - Err(_) => Location { - city: "Unknown".to_string(), - country: "Unknown".to_string(), - ip_address: ip.to_string(), - latitude: "0.0".to_string(), - longitude: "0.0".to_string(), - }, - } -} -#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)] -pub struct PlexSessions { - pub title: String, - pub user: String, - pub stream_decision: StreamDecision, - pub media_type: String, - pub state: String, - pub progress: i64, - pub quality: String, - pub season_number: Option, - pub episode_number: Option, - pub address: String, - pub location: Location, - pub local: bool, - pub secure: bool, - pub relayed: bool, - pub platform: String, - pub bandwidth: Bandwidth, -} - -#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)] -pub struct Bandwidth { - pub bandwidth: i64, - pub location: BandwidthLocation, -} - -#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)] -pub enum BandwidthLocation { - Wan, - Lan, -} -impl From for BandwidthLocation { - fn from(location: String) -> Self { - match location.as_str() { - "wan" => BandwidthLocation::Wan, - "lan" => BandwidthLocation::Lan, - _ => BandwidthLocation::Wan, - } - } -} -impl Display for BandwidthLocation { - fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - match self { - BandwidthLocation::Wan => write!(f, "WAN"), - BandwidthLocation::Lan => write!(f, "LAN"), - } - } -} - #[derive(Debug, Clone, PartialEq, Serialize, Deserialize)] pub enum StreamDecision { DirectPlay, @@ -371,3 +217,11 @@ impl Display for StreamDecision { } } } +#[derive(Debug, Deserialize, Clone, Serialize)] +pub struct LibraryInfos { + pub library_name: String, + pub library_type: String, + pub library_size: i64, + pub library_child_size: Option, + pub library_grand_child_size: Option, +}