From 10003912a9a8d5efc15ae07747b10e1bd4fe9ee8 Mon Sep 17 00:00:00 2001 From: Thomas Date: Tue, 12 Nov 2024 12:55:13 +0100 Subject: [PATCH 1/3] Working plex library --- src/prometheus.rs | 4 ++++ src/providers/plex.rs | 42 ++++++++++++++++++++++++++++++----- src/providers/structs/plex.rs | 35 ++++++++++++++--------------- 3 files changed, 57 insertions(+), 24 deletions(-) diff --git a/src/prometheus.rs b/src/prometheus.rs index bd1ead2..3509109 100644 --- a/src/prometheus.rs +++ b/src/prometheus.rs @@ -80,6 +80,8 @@ struct PlexLibraryLabels { pub name: String, pub library_name: String, pub library_type: String, + pub season_count: Option, + pub episode_count: Option, } #[derive(Clone, Hash, Eq, PartialEq, EncodeLabelSet, Debug)] @@ -504,6 +506,8 @@ fn format_plex_library_metrics( 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, }) .set(lib.library_size as f64); }); diff --git a/src/providers/plex.rs b/src/providers/plex.rs index 22ff2e6..ce66534 100644 --- a/src/providers/plex.rs +++ b/src/providers/plex.rs @@ -4,7 +4,7 @@ use reqwest::header; use serde::{Deserialize, Serialize}; use crate::providers::structs::plex::{ - LibraryContainer, LibraryItemsContainer, Metadata, PlexResponse, + LibraryContainer, LibraryItemsContainer, LibraryType, Metadata, PlexResponse, }; pub use crate::providers::structs::plex::{MediaContainer, PlexSessions}; use crate::providers::{Provider, ProviderError, ProviderErrorKind}; @@ -19,6 +19,8 @@ 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)] @@ -141,11 +143,39 @@ impl Plex { return Vec::new(); } }; - library_infos.push(LibraryInfos { - library_name: item.title.to_string(), - library_type: item.type_field.to_string(), - library_size: library_items_container.size, - }); + match &item.type_field[..] { + "show" => { + let (child_sum, leaf_sum) = library_items_container.metadata.iter().fold( + (0, 0), + |(mut child_acc, mut leaf_acc), child| { + match child { + Metadata::LibraryMetadata(meta) => { + child_acc += meta.child_count.unwrap_or(0); + leaf_acc += meta.leaf_count.unwrap_or(0); + } + _ => { + error!("Metadata received does not match library metadata"); + } + } + (child_acc, leaf_acc) + }, + ); + library_infos.push(LibraryInfos { + library_name: item.title.to_string(), + library_type: item.type_field.to_string(), + library_size: library_items_container.size, + library_child_size: Some(child_sum), + library_grand_child_size: Some(leaf_sum), + }); + } + _ => library_infos.push(LibraryInfos { + library_name: item.title.to_string(), + library_type: item.type_field.to_string(), + library_size: library_items_container.size, + library_child_size: None, + library_grand_child_size: None, + }), + } } library_infos } diff --git a/src/providers/structs/plex.rs b/src/providers/structs/plex.rs index 6d1efce..95330f3 100644 --- a/src/providers/structs/plex.rs +++ b/src/providers/structs/plex.rs @@ -7,6 +7,7 @@ use std::fmt::Display; pub enum Metadata { SessionMetadata(SessionMetadata), HistoryMetadata(HistoryMetadata), + LibraryMetadata(LibraryMetadata), Default(serde_json::Value), } @@ -74,6 +75,8 @@ pub struct LibraryItemsContainer { pub library_section_title: String, #[serde(rename = "librarySectionUUID")] pub library_section_uuid: String, + #[serde(rename = "Metadata")] + pub metadata: Vec, } #[derive(Default, Debug, Clone, PartialEq, Serialize, Deserialize)] @@ -85,6 +88,16 @@ pub struct Directory { pub type_field: String, } +#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct LibraryMetadata { + #[serde(rename = "type")] + pub type_field: String, + pub title: String, + pub leaf_count: Option, + pub child_count: Option, +} + #[derive(Debug, Clone, PartialEq, Serialize, Deserialize)] #[serde(rename_all = "camelCase")] pub struct HistoryMetadata { @@ -142,7 +155,10 @@ impl SessionMetadata { let secure = self.player.secure; let relayed = self.player.relayed; let platform = self.player.platform.clone(); - let title = self.original_title.clone().unwrap_or(self.title.clone()); + let title = match &self.grand_parent_title { + Some(parent) => parent.to_string(), + None => self.title.clone(), + }; match &media_type[..] { "episode" => PlexSessions { title, @@ -196,23 +212,6 @@ impl SessionMetadata { platform, }, } - //PlexSessions { - // title, - // user, - // stream_decision, - // media_type, - // state, - // progress, - // quality, - // season_number, - // episode_number, - // location, - // address, - // local, - // secure, - // relayed, - // platform, - //} } } From 86251c8939ae807d85326545acb53fe43a1441f9 Mon Sep 17 00:00:00 2001 From: Thomas Date: Tue, 12 Nov 2024 20:46:05 +0100 Subject: [PATCH 2/3] Adding some new instruction --- README.md | 42 +++++++++++++++++++++++++++++------------- 1 file changed, 29 insertions(+), 13 deletions(-) diff --git a/README.md b/README.md index ebdad0e..e86e648 100644 --- a/README.md +++ b/README.md @@ -1,9 +1,12 @@ # HOMERS +![dev build](https://github.com/github/tcheronneau/homers/workflows/build.yml/badge.svg?branch=dev) +![latest build](https://github.com/tcheronneau/homers/actions/workflows/build-latest.yml/badge.svg?branch=main) + This project has the purposed to be a replacement for [Varken](https://github.com/Boerderij/Varken). Since InfluxDB is not a good option for me, I decided to use prometheus and to build an exporter. -It's not ready yet, but some features are already there. +The project is still in early stage and a lot can still change. ![image](https://github.com/user-attachments/assets/9a0c2fb0-52f3-439d-b590-9c6698994d10) @@ -20,7 +23,7 @@ You can either use configuration file or environment variables. Each config key has a correspondent environment variable. Example: `config.toml`: ```toml -[server] +[http] port=8000 address="0.0.0.0" [sonarr.main] @@ -44,6 +47,18 @@ api_key="" address="http://localhost:5055" api_key="" requests=200 + +[plex.main] +address="http://localhost:32400" +token="" + +``` + +Example: `environement`: +```bash +HOMERS_HTTP_ADDRESS: "0.0.0.0" +HOMERS_SONARR_MAIN_ADDRESS: "http://localhost:8989" +HOMERS_SONARR_MAIN_APIKEY: "" ``` For overseerr you can customize the number of requests you want to pull. Default is 20. @@ -61,7 +76,7 @@ Then you can run `cargo build --release`. Alternatively you can also use nix. To build the project using nix, you can run `nix build .#`. -And for the docker image : +And for the docker image (not used anymore for the current build): ``` nix build .#docker docker load < ./result @@ -71,20 +86,21 @@ docker load < ./result ## Advancement So far it's not doing much. -[X] Retrieve Sonarr today's calendar -[X] Retrieve Tautulli activity -[X] Retrieve Tautulli library information -[X] Retrieve Overseerr requests -[X] Retrieve missing episodes from sonarr -[ ] Retrieve watch information from tautulli -[ ] Connect to ombi (I'm not using it but if required could do) -[ ] Other +[X] Retrieve Sonarr today's calendar +[X] Retrieve Tautulli activity +[X] Retrieve Tautulli library information +[X] Retrieve Overseerr requests +[X] Retrieve missing episodes from sonarr +[X] Retrieve watch information from tautulli +[ ] Retrieve watch information from plex (in progress available in dev tag) +[ ] Retrieve watch information from jellyfin +[ ] Connect to ombi (I'm not using it but if required could do) +[ ] Other ## Roadmap The point is to at least support what Varken was doing. -There will also be a Grafana dashboard. -Grafana dashboard is now live at [Grafana](https://grafana.com/grafana/dashboards/20744). +Grafana dashboard example can be found at [Grafana](https://grafana.com/grafana/dashboards/20744). ## Acknowledgments From 5887af80d3ac388d8b3147aa3611e2740509bfbd Mon Sep 17 00:00:00 2001 From: Thomas Date: Tue, 12 Nov 2024 20:46:24 +0100 Subject: [PATCH 3/3] Polishing plex and improve overseerr perf --- Cargo.lock | 37 +++++++------ Cargo.toml | 1 + src/prometheus.rs | 17 +++--- src/providers/overseerr.rs | 109 +++++++++++++++++++++---------------- src/providers/plex.rs | 4 +- 5 files changed, 92 insertions(+), 76 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 73c356a..ff669a9 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -457,9 +457,9 @@ dependencies = [ [[package]] name = "futures" -version = "0.3.30" +version = "0.3.31" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "645c6916888f6cb6350d2550b80fb63e734897a8498abe35cfb732b6487804b0" +checksum = "65bc07b1a8bc7c85c5f2e110c476c7389b4554ba72af57d8445ea63a576b0876" dependencies = [ "futures-channel", "futures-core", @@ -472,9 +472,9 @@ dependencies = [ [[package]] name = "futures-channel" -version = "0.3.30" +version = "0.3.31" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "eac8f7d7865dcb88bd4373ab671c8cf4508703796caa2b1985a9ca867b3fcb78" +checksum = "2dff15bf788c671c1934e366d07e30c1814a8ef514e1af724a602e8a2fbe1b10" dependencies = [ "futures-core", "futures-sink", @@ -482,15 +482,15 @@ dependencies = [ [[package]] name = "futures-core" -version = "0.3.30" +version = "0.3.31" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "dfc6580bb841c5a68e9ef15c77ccc837b40a7504914d52e47b8b0e9bbda25a1d" +checksum = "05f29059c0c2090612e8d742178b0580d2dc940c837851ad723096f87af6663e" [[package]] name = "futures-executor" -version = "0.3.30" +version = "0.3.31" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a576fc72ae164fca6b9db127eaa9a9dda0d61316034f33a0a0d4eda41f02b01d" +checksum = "1e28d1d997f585e54aebc3f97d39e72338912123a67330d723fdbb564d646c9f" dependencies = [ "futures-core", "futures-task", @@ -499,15 +499,15 @@ dependencies = [ [[package]] name = "futures-io" -version = "0.3.30" +version = "0.3.31" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a44623e20b9681a318efdd71c299b6b222ed6f231972bfe2f224ebad6311f0c1" +checksum = "9e5c1b78ca4aae1ac06c48a526a655760685149f0d465d21f37abfe57ce075c6" [[package]] name = "futures-macro" -version = "0.3.30" +version = "0.3.31" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "87750cf4b7a4c0625b1529e4c543c2182106e4dedc60a2a6455e00d212c489ac" +checksum = "162ee34ebcb7c64a8abebc059ce0fee27c2262618d7b60ed8faf72fef13c3650" dependencies = [ "proc-macro2", "quote", @@ -516,21 +516,21 @@ dependencies = [ [[package]] name = "futures-sink" -version = "0.3.30" +version = "0.3.31" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9fb8e00e87438d937621c1c6269e53f536c14d3fbd6a042bb24879e57d474fb5" +checksum = "e575fab7d1e0dcb8d0c7bcf9a63ee213816ab51902e6d244a95819acacf1d4f7" [[package]] name = "futures-task" -version = "0.3.30" +version = "0.3.31" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "38d84fa142264698cdce1a9f9172cf383a0c82de1bddcf3092901442c4097004" +checksum = "f90f7dce0722e95104fcb095585910c0977252f286e354b5e3bd38902cd99988" [[package]] name = "futures-util" -version = "0.3.30" +version = "0.3.31" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3d6401deb83407ab3da39eba7e33987a73c3df0c82b4bb5813ee871c19c41d48" +checksum = "9fa08315bb612088cc391249efdc3bc77536f16c91f6cf495e6fbe85b20a4a81" dependencies = [ "futures-channel", "futures-core", @@ -626,6 +626,7 @@ dependencies = [ "clap", "clap-verbosity-flag", "figment", + "futures", "ipgeolocate", "json", "lazy_static", diff --git a/Cargo.toml b/Cargo.toml index 9d4430c..8024076 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -11,6 +11,7 @@ chrono = "0.4.34" clap = "4.5.1" clap-verbosity-flag = "2.2.0" figment = { version = "0.10.14", features = ["toml", "env"] } +futures = "0.3.31" ipgeolocate = "0.3.6" json = "0.12.4" lazy_static = "1.4.0" diff --git a/src/prometheus.rs b/src/prometheus.rs index 3509109..ba7e453 100644 --- a/src/prometheus.rs +++ b/src/prometheus.rs @@ -5,10 +5,9 @@ use prometheus_client::metrics::family::Family; use prometheus_client::metrics::gauge::Gauge; use prometheus_client::registry::Registry; use std::collections::HashMap; -use std::fmt::Write; use std::sync::atomic::AtomicU64; -use crate::providers::overseerr::{self, OverseerrRequest}; +use crate::providers::overseerr::OverseerrRequest; use crate::providers::plex::{LibraryInfos, PlexSessions}; use crate::providers::radarr::RadarrMovie; use crate::providers::sonarr::SonarrEpisode; @@ -145,6 +144,7 @@ struct OverseerrLabels { pub media_type: String, pub requested_by: String, pub request_status: String, + pub media_status: String, pub media_title: String, pub requested_at: String, } @@ -343,14 +343,15 @@ 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(); + //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| { @@ -377,18 +378,20 @@ fn format_overseerr_metrics(requests: Vec, registry: &mut Regi registy_request.get(&status.to_string()).unwrap().clone(), ); }); + */ requests.into_iter().for_each(|request| { let labels = OverseerrLabels { media_type: request.media_type.clone(), requested_by: request.requested_by.to_string(), request_status: request.status.to_string(), + media_status: request.media_status.to_string(), media_title: request.media_title, requested_at: request.requested_at, }; overseerr_request .get_or_create(&labels) - .set(request.media_status as f64); - match request.status.into() { + .set(request.status.as_f64()); + /*match request.status.into() { overseerr::RequestStatus::Pending => { registy_request .get(&overseerr::RequestStatus::Pending.to_string()) @@ -416,7 +419,7 @@ fn format_overseerr_metrics(requests: Vec, registry: &mut Regi }) .inc(); } - }; + };*/ }); } diff --git a/src/providers/overseerr.rs b/src/providers/overseerr.rs index bc92a6b..5a9b767 100644 --- a/src/providers/overseerr.rs +++ b/src/providers/overseerr.rs @@ -10,9 +10,9 @@ use crate::providers::{Provider, ProviderError, ProviderErrorKind}; pub struct OverseerrRequest { pub media_type: String, pub media_id: i64, - pub status: i64, + pub status: RequestStatus, pub requested_by: String, - pub media_status: i64, + pub media_status: MediaStatus, pub media_title: String, pub requested_at: String, } @@ -27,6 +27,7 @@ pub struct Overseerr { client: reqwest::Client, } +#[derive(Debug, Deserialize, Clone, Serialize)] pub enum RequestStatus { Pending, Approved, @@ -43,6 +44,13 @@ impl From for RequestStatus { } } impl RequestStatus { + pub fn as_f64(&self) -> f64 { + match self { + RequestStatus::Pending => 1.0, + RequestStatus::Approved => 2.0, + RequestStatus::Declined => 3.0, + } + } pub fn to_string(&self) -> String { match self { RequestStatus::Pending => "pending_approval".to_string(), @@ -50,7 +58,7 @@ impl RequestStatus { RequestStatus::Declined => "declined".to_string(), } } - pub fn to_description(&self) -> String { + pub fn _to_description(&self) -> String { match self { RequestStatus::Pending => "Overseerr request pending approval".to_string(), RequestStatus::Approved => "Overseerr request approved".to_string(), @@ -58,7 +66,7 @@ impl RequestStatus { } } - pub fn get_all() -> Vec { + pub fn _get_all() -> Vec { vec![ RequestStatus::Pending, RequestStatus::Approved, @@ -67,6 +75,7 @@ impl RequestStatus { } } +#[derive(Debug, Deserialize, Clone, Serialize)] pub enum MediaStatus { Unknown, Pending, @@ -74,6 +83,18 @@ pub enum MediaStatus { PartiallyAvailable, Available, } +impl From for MediaStatus { + fn from(status: i64) -> Self { + match status { + 1 => MediaStatus::Unknown, + 2 => MediaStatus::Pending, + 3 => MediaStatus::Processing, + 4 => MediaStatus::PartiallyAvailable, + 5 => MediaStatus::Available, + _ => MediaStatus::Unknown, + } + } +} impl MediaStatus { pub fn to_string(&self) -> String { match self { @@ -84,7 +105,7 @@ impl MediaStatus { MediaStatus::Available => "available".to_string(), } } - pub fn to_description(&self) -> String { + pub fn _to_description(&self) -> String { match self { MediaStatus::Unknown => "Overseerr media status unknown".to_string(), MediaStatus::Pending => "Overseerr media status pending".to_string(), @@ -96,17 +117,7 @@ impl MediaStatus { } } - pub fn from_i64(status: i64) -> MediaStatus { - match status { - 1 => MediaStatus::Unknown, - 2 => MediaStatus::Pending, - 3 => MediaStatus::Processing, - 4 => MediaStatus::PartiallyAvailable, - 5 => MediaStatus::Available, - _ => MediaStatus::Unknown, - } - } - pub fn get_all() -> Vec { + pub fn _get_all() -> Vec { vec![ MediaStatus::Unknown, MediaStatus::Pending, @@ -177,43 +188,45 @@ impl Overseerr { Vec::new() } }; - let mut overseerr_requests = Vec::new(); - for request in requests { - let media_title = match self - .get_media_title(&request.media.media_type, request.media.tmdb_id) - .await - { - Ok(title) => title, - Err(e) => { - error!("Failed to get media title: {:?}", e); - "Unknown".to_string() - } - }; - let overseerr_request = OverseerrRequest { - media_type: request.media.media_type.clone(), - media_id: request.media.id, - status: request.status, - requested_by: match self.get_username(request.clone()) { - Ok(username) => username, + let futures_requests = requests.into_iter().map(|request| { + let self_ref = self.clone(); // Assuming `self` implements `Clone`, so we can move it into the future. + async move { + // Fetch media title asynchronously + let media_title = match self_ref + .get_media_title(&request.media.media_type, request.media.tmdb_id) + .await + { + Ok(title) => title, Err(e) => { - error!("Failed to get username: {:?}", e); + error!("Failed to get media title: {:?}", e); "Unknown".to_string() } - }, - media_status: request.media.status, - media_title, - requested_at: request.created_at, - }; - overseerr_requests.push(overseerr_request); - } + }; + + // Construct the OverseerrRequest + OverseerrRequest { + media_type: request.media.media_type.clone(), + media_id: request.media.id, + status: request.status.into(), + requested_by: self_ref.get_username(&request).to_string(), + media_status: request.media.status.into(), + media_title, + requested_at: request.created_at, + } + } + }); + let overseerr_requests: Vec = futures::future::join_all(futures_requests) + .await + .into_iter() + .collect(); overseerr_requests } - fn get_username(&self, request: overseerr::Result) -> anyhow::Result { - match request.requested_by.username { - Some(username) => Ok(username), - None => match request.requested_by.plex_username { - Some(username) => Ok(username), - None => Ok("Unknown".to_string()), + fn get_username<'a>(&self, request: &'a overseerr::Result) -> &'a str { + match &request.requested_by.username { + Some(username) => username, + None => match &request.requested_by.plex_username { + Some(username) => &username, + None => "Unknown", }, } } diff --git a/src/providers/plex.rs b/src/providers/plex.rs index ce66534..b63268f 100644 --- a/src/providers/plex.rs +++ b/src/providers/plex.rs @@ -3,10 +3,8 @@ use reqwest; use reqwest::header; use serde::{Deserialize, Serialize}; -use crate::providers::structs::plex::{ - LibraryContainer, LibraryItemsContainer, LibraryType, Metadata, PlexResponse, -}; pub use crate::providers::structs::plex::{MediaContainer, PlexSessions}; +use crate::providers::structs::plex::{Metadata, PlexResponse}; use crate::providers::{Provider, ProviderError, ProviderErrorKind}; #[derive(Debug, Deserialize, Clone, Serialize)]