diff --git a/Cargo.toml b/Cargo.toml index 90612ec..be12825 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "jellyfin-rpc" -version = "0.11.2" +version = "0.12.0" edition = "2021" # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html diff --git a/README.md b/README.md index 9efa3a8..6cbe7fc 100644 --- a/README.md +++ b/README.md @@ -31,8 +31,8 @@ Terminal Output: ## Setup - Installers - - Windows - - macOS + - Windows + - macOS Make a `main.json` file with the following items in `$XDG_CONFIG_HOME/jellyfin-rpc` on Linux/macOS and `%APPDATA%\jellyfin-rpc\main.json` on Windows. @@ -40,23 +40,29 @@ If you're unsure about the directory then run jellyfin-rpc and it will tell you ``` { - "Jellyfin": { - "URL": "https://example.com", - "API_KEY": "sadasodsapasdskd", - "USERNAME": "your_username_here", - "_comment": "the 2 lines below and this line arent needed and should be removed, by default nothing will display if these are present" - "TYPE_BLACKLIST": ["music", "movie", "episode", "livetv"] - "LIBRARY_BLACKLIST": ["Anime", "Anime Movies"] + "jellyfin": { + "url": "https://example.com", + "api_key": "sadasodsapasdskd", + "username": "your_username_here", + "music": { + "display": "genres", + "separator": "-" + }, + "_comment": "the 4 lines below and this line arent needed and should be removed, by default nothing will display if these are present", + "blacklist": { + "media_types": ["music", "movie", "episode", "livetv"], + "libraries": ["Anime", "Anime Movies"] + } }, - "Discord": { - "APPLICATION_ID": "1053747938519679018" + "discord": { + "application_id": "1053747938519679018" }, - "Imgur": { - "CLIENT_ID": "asdjdjdg394209fdjs093" + "imgur": { + "client_id": "asdjdjdg394209fdjs093" }, - "Images": { - "ENABLE_IMAGES": true, - "IMGUR_IMAGES": true + "images": { + "enable_images": true, + "imgur_images": true } } ``` diff --git a/example.json b/example.json index b077bf8..ece5f77 100644 --- a/example.json +++ b/example.json @@ -1,20 +1,26 @@ { - "Jellyfin": { - "URL": "https://example.com", - "API_KEY": "sadasodsapasdskd", - "USERNAME": "your_username_here", - "_comment": "the 2 lines below and this line arent needed and should be removed, by default nothing will display if these are present" - "TYPE_BLACKLIST": ["music", "movie", "episode", "livetv"] - "LIBRARY_BLACKLIST": ["Anime", "Anime Movies"] + "jellyfin": { + "url": "https://example.com", + "api_key": "sadasodsapasdskd", + "username": "your_username_here", + "music": { + "display": "genres", + "separator": "-" + }, + "_comment": "the 4 lines below and this line arent needed and should be removed, by default nothing will display if these are present", + "blacklist": { + "media_types": ["music", "movie", "episode", "livetv"], + "libraries": ["Anime", "Anime Movies"] + } }, - "Discord": { - "APPLICATION_ID": "1053747938519679018" + "discord": { + "application_id": "1053747938519679018" }, - "Imgur": { - "CLIENT_ID": "asdjdjdg394209fdjs093" + "imgur": { + "client_id": "asdjdjdg394209fdjs093" }, - "Images": { - "ENABLE_IMAGES": true, - "IMGUR_IMAGES": true + "images": { + "enable_images": true, + "imgur_images": true } } diff --git a/scripts/Auto-install-macos.sh b/scripts/install-macos.sh similarity index 90% rename from scripts/Auto-install-macos.sh rename to scripts/install-macos.sh index 23b56f4..d7ef5fe 100755 --- a/scripts/Auto-install-macos.sh +++ b/scripts/install-macos.sh @@ -8,6 +8,9 @@ vared -p "Jellyfin API Key (you can find this at ${jellyfinurl}/web/#!/apikeys.h vared -p "Jellyfin Username: " -c jellyfinuser echo "" +echo "The blacklist creation in this script is currently broken" +echo "Check the README for details on how to set it up in the config " + # Prompt the user for what libraries should be included/blocked responses=() @@ -95,38 +98,38 @@ fi configFileContents="" configFileContents+="$(cat < main.json -echo "Jellyfin": { >> main.json -echo "URL": "%JELLYFIN_URL%", >> main.json -echo "API_KEY": "%JELLYFIN_API_KEY%", >> main.json -echo "USERNAME": "%JELLYFIN_USERNAME%" >> main.json +echo "jellyfin": { >> main.json +echo "url": "%JELLYFIN_URL%", >> main.json +echo "api_key": "%JELLYFIN_API_KEY%", >> main.json +echo "username": "%JELLYFIN_USERNAME%" >> main.json echo }, >> main.json -echo "Discord": { >> main.json -echo "APPLICATION_ID": "%DISCORD_APPLICATION_ID%" >> main.json +echo "discord": { >> main.json +echo "application_id": "%DISCORD_APPLICATION_ID%" >> main.json echo }, >> main.json -echo "Imgur": { >> main.json -echo "CLIENT_ID": "%IMGUR_CLIENT_ID%" >> main.json +echo "imgur": { >> main.json +echo "client_id": "%IMGUR_CLIENT_ID%" >> main.json echo }, >> main.json -echo "Images": { >> main.json -echo "ENABLE_IMAGES": %IMAGES_ENABLE_IMAGES%, >> main.json -echo "IMGUR_IMAGES": %IMAGES_IMGUR_IMAGES% >> main.json +echo "images": { >> main.json +echo "enable_images": %IMAGES_ENABLE_IMAGES%, >> main.json +echo "imgur_images": %IMAGES_IMGUR_IMAGES% >> main.json echo } >> main.json echo } >> main.json diff --git a/src/config.rs b/src/config.rs deleted file mode 100644 index b1ee432..0000000 --- a/src/config.rs +++ /dev/null @@ -1,185 +0,0 @@ -use crate::services::jellyfin::MediaType; -use colored::Colorize; -use std::env; - -/* - TODO: Comments -*/ - -pub struct Config { - pub url: String, - pub api_key: String, - pub username: String, - pub blacklist: Blacklist, - pub rpc_client_id: String, - pub imgur_client_id: String, - pub images: Images, -} - -pub struct Blacklist { - pub types: Vec, - pub libraries: Vec, -} - -pub struct Images { - pub enabled: bool, - pub imgur: bool, -} - -#[derive(Debug)] -pub enum ConfigError { - MissingConfig(String), - Io(String), - Json(String), - VarError(String), -} - -impl From<&'static str> for ConfigError { - fn from(value: &'static str) -> Self { - Self::MissingConfig(value.to_string()) - } -} - -impl From for ConfigError { - fn from(value: std::io::Error) -> Self { - Self::Io(format!("Unable to open file: {}", value)) - } -} - -impl From for ConfigError { - fn from(value: serde_json::Error) -> Self { - Self::Json(format!("Unable to parse config: {}", value)) - } -} - -impl From for ConfigError { - fn from(value: env::VarError) -> Self { - Self::VarError(format!("Unable to get environment variables: {}", value)) - } -} - -pub fn get_config_path() -> Result { - if cfg!(not(windows)) { - let user = env::var("USER")?; - if user != "root" { - let xdg_config_home = env::var("XDG_CONFIG_HOME") - .unwrap_or_else(|_| env::var("HOME").unwrap() + "/.config"); - Ok(xdg_config_home + ("/jellyfin-rpc/main.json")) - } else { - Ok("/etc/jellyfin-rpc/main.json".to_string()) - } - } else { - let app_data = env::var("APPDATA")?; - Ok(app_data + r"\jellyfin-rpc\main.json") - } -} - -impl Config { - pub fn load_config(path: String) -> Result { - let data = std::fs::read_to_string(path)?; - let res: serde_json::Value = serde_json::from_str(&data)?; - - let jellyfin: serde_json::Value = res["Jellyfin"].clone(); - let discord: serde_json::Value = res["Discord"].clone(); - let imgur: serde_json::Value = res["Imgur"].clone(); - let images: serde_json::Value = res["Images"].clone(); - - let url = jellyfin["URL"].as_str().unwrap_or("").to_string(); - let api_key = jellyfin["API_KEY"].as_str().unwrap_or("").to_string(); - let username = jellyfin["USERNAME"].as_str().unwrap_or("").to_string(); - let mut type_blacklist: Vec = vec![MediaType::None]; - if !Option::is_none(&jellyfin["TYPE_BLACKLIST"].get(0)) { - type_blacklist.pop(); - jellyfin["TYPE_BLACKLIST"] - .as_array() - .unwrap() - .iter() - .for_each(|val| { - if val != "music" && val != "movie" && val != "episode" && val != "livetv" { - eprintln!("{} is invalid, valid media types to blacklist include: \"music\", \"movie\", \"episode\" and \"livetv\"", val); - std::process::exit(2) - } - type_blacklist.push( - MediaType::from(val - .as_str() - .expect("Media types to blacklist need to be in quotes \"music\"") - .to_string())) - }); - } - let mut library_blacklist: Vec = vec!["".to_string()]; - if !Option::is_none(&jellyfin["LIBRARY_BLACKLIST"].get(0)) { - library_blacklist.pop(); - jellyfin["LIBRARY_BLACKLIST"] - .as_array() - .unwrap() - .iter() - .for_each(|val| { - library_blacklist.push( - val.as_str() - .expect("Libraries to blacklist need to be in quotes \"music\"") - .to_lowercase(), - ) - }); - } - let rpc_client_id = discord["APPLICATION_ID"] - .as_str() - .unwrap_or("1053747938519679018") - .to_string(); - - let imgur_client_id = imgur["CLIENT_ID"].as_str().unwrap_or("").to_string(); - - let enable_images = images["ENABLE_IMAGES"].as_bool().unwrap_or_else(|| { - eprintln!( - "{}\n{} {} {} {}", - "ENABLE_IMAGES has to be a bool...".red().bold(), - "EXAMPLE:".bold(), - "true".bright_green().bold(), - "not".bold(), - "'true'".red().bold() - ); - std::process::exit(2) - }); - let imgur_images = images["IMGUR_IMAGES"].as_bool().unwrap_or_else(|| { - eprintln!( - "{}\n{} {} {} {}", - "IMGUR_IMAGES has to be a bool...".red().bold(), - "EXAMPLE:".bold(), - "true".bright_green().bold(), - "not".bold(), - "'true'".red().bold() - ); - std::process::exit(2) - }); - - match ( - url.is_empty(), - api_key.is_empty(), - username.is_empty(), - rpc_client_id.is_empty(), - (imgur_images, imgur_client_id.is_empty()), - ) { - (true, _, _, _, _) => Err(ConfigError::from("Jellyfin URL is empty!")), - (_, true, _, _, _) => Err(ConfigError::from("Jellyfin API key is empty!")), - (_, _, true, _, _) => Err(ConfigError::from("Jellyfin Username is empty!")), - (_, _, _, true, _) => Err(ConfigError::from("Discord Application ID is empty!")), - (_, _, _, _, (true, true)) => Err(ConfigError::from( - "Imgur Client ID is empty but Imgur images are enabled!", - )), - (false, false, false, false, _) => Ok(Config { - url, - api_key, - username, - blacklist: Blacklist { - types: type_blacklist, - libraries: library_blacklist, - }, - rpc_client_id, - imgur_client_id, - images: Images { - enabled: enable_images, - imgur: imgur_images, - }, - }), - } - } -} diff --git a/src/core/config.rs b/src/core/config.rs new file mode 100644 index 0000000..eca0530 --- /dev/null +++ b/src/core/config.rs @@ -0,0 +1,271 @@ +use crate::core::error::ConfigError; +use crate::services::jellyfin::MediaType; +use colored::Colorize; +use std::env; + +/* + TODO: Comments +*/ + +#[derive(Default)] +struct ConfigBuilder { + url: String, + api_key: String, + username: Vec, + blacklist: Blacklist, + music: Music, + rpc_client_id: String, + imgur_client_id: String, + images: Images, +} + +impl ConfigBuilder { + fn new() -> Self { + Self::default() + } + + fn url(&mut self, url: String) { + self.url = url; + } + + fn api_key(&mut self, api_key: String) { + self.api_key = api_key; + } + + fn username(&mut self, username: Vec) { + self.username = username; + } + + fn blacklist(&mut self, types: Vec, libraries: Vec) { + self.blacklist = Blacklist { types, libraries }; + } + + fn music_display(&mut self, display: Vec) { + self.music.display = display + } + + fn music_seperator(&mut self, separator: Option) { + self.music.separator = separator + } + + fn rpc_client_id(&mut self, rpc_client_id: String) { + self.rpc_client_id = rpc_client_id; + } + + fn imgur_client_id(&mut self, imgur_client_id: String) { + self.imgur_client_id = imgur_client_id; + } + + fn images(&mut self, enabled: bool, imgur: bool) { + self.images = Images { enabled, imgur }; + } + + fn build(self) -> Result { + match ( + self.url.is_empty(), + self.api_key.is_empty(), + self.username.is_empty(), + self.rpc_client_id.is_empty(), + (self.images.imgur, self.imgur_client_id.is_empty()), + ) { + (true, _, _, _, _) => Err(ConfigError::from("Jellyfin URL is empty!")), + (_, true, _, _, _) => Err(ConfigError::from("Jellyfin API key is empty!")), + (_, _, true, _, _) => Err(ConfigError::from("Jellyfin Username is empty!")), + (_, _, _, true, _) => Err(ConfigError::from("Discord Application ID is empty!")), + (_, _, _, _, (true, true)) => Err(ConfigError::from( + "Imgur Client ID is empty but Imgur images are enabled!", + )), + (false, false, false, false, _) => Ok(Config { + url: self.url, + api_key: self.api_key, + username: self.username, + blacklist: self.blacklist, + music: Music { + display: self.music.display, + separator: self.music.separator + }, + rpc_client_id: self.rpc_client_id, + imgur_client_id: self.imgur_client_id, + images: self.images + }, + ), + } + } +} + +pub struct Config { + pub url: String, + pub api_key: String, + pub username: Vec, + pub blacklist: Blacklist, + pub music: Music, + pub rpc_client_id: String, + pub imgur_client_id: String, + pub images: Images, +} + +#[derive(Default)] +pub struct Blacklist { + pub types: Vec, + pub libraries: Vec, +} + +#[derive(Default)] +pub struct Images { + pub enabled: bool, + pub imgur: bool, +} + +#[derive(Default)] +pub struct Music { + pub display: Vec, + pub separator: Option +} + +pub fn get_config_path() -> Result { + if cfg!(not(windows)) { + let user = env::var("USER")?; + if user != "root" { + let xdg_config_home = env::var("XDG_CONFIG_HOME") + .unwrap_or_else(|_| env::var("HOME").unwrap() + "/.config"); + Ok(xdg_config_home + ("/jellyfin-rpc/main.json")) + } else { + Ok("/etc/jellyfin-rpc/main.json".to_string()) + } + } else { + let app_data = env::var("APPDATA")?; + Ok(app_data + r"\jellyfin-rpc\main.json") + } +} + +impl Config { + pub fn load_config(path: String) -> Result { + let mut config = ConfigBuilder::new(); + let data = std::fs::read_to_string(path)?; + let res: serde_json::Value = serde_json::from_str(&data)?; + + let jellyfin: serde_json::Value = res["jellyfin"].clone(); + let music: serde_json::Value = jellyfin["music"].clone(); + let blacklist: serde_json::Value = jellyfin["blacklist"].clone(); + + let discord: serde_json::Value = res["discord"].clone(); + let imgur: serde_json::Value = res["imgur"].clone(); + let images: serde_json::Value = res["images"].clone(); + + config.url(jellyfin["url"].as_str().unwrap_or("").to_string()); + config.api_key(jellyfin["api_key"].as_str().unwrap_or("").to_string()); + if jellyfin["username"].is_string() { + config.username(vec![ + jellyfin["username"].as_str().unwrap_or("").to_string() + ]); + } else { + config.username( + jellyfin["username"].as_array() + .unwrap() + .iter() + .map(|username| username.as_str().unwrap().to_string()) + .collect::>() + ); + } + let mut library_blacklist: Vec = vec!["".to_string()]; + let mut type_blacklist: Vec = vec![MediaType::None]; + if blacklist.is_object() { + if blacklist["media_types"].get(0).is_some() { + type_blacklist.pop(); + blacklist["media_types"] + .as_array() + .unwrap() + .iter() + .for_each(|val| { + if val != "music" && val != "movie" && val != "episode" && val != "livetv" { + eprintln!("{} is invalid, valid media types to blacklist include: \"music\", \"movie\", \"episode\" and \"livetv\"", val); + std::process::exit(2) + } + type_blacklist.push( + MediaType::from(val + .as_str() + .expect("Media types to blacklist need to be in quotes \"music\"") + .to_string())) + }); + } + + if blacklist["libraries"].get(0).is_some() { + library_blacklist.pop(); + blacklist["libraries"] + .as_array() + .unwrap() + .iter() + .for_each(|val| { + library_blacklist.push( + val.as_str() + .expect("Libraries to blacklist need to be in quotes \"music\"") + .to_lowercase(), + ) + }); + } + } + config.blacklist(type_blacklist, library_blacklist); + + if music["display"].is_string() { + config.music_display( + music["display"] + .as_str() + .unwrap() + .split(',') + .map(|username| + username.trim().to_string() + ) + .collect::>() + ) + } else if music["display"].is_array() { + config.music_display( + music["display"] + .as_array() + .unwrap() + .iter() + .map(|username| + username.as_str().unwrap().trim().to_string() + ) + .collect::>() + ) + } else { + config.music_display(vec![String::from("genres")]) + } + + config.music_seperator(music["separator"].as_str().unwrap_or("-").chars().next()); + + config.rpc_client_id(discord["application_id"] + .as_str() + .unwrap_or("1053747938519679018") + .to_string()); + + config.imgur_client_id(imgur["client_id"].as_str().unwrap_or("").to_string()); + + config.images( + images["enable_images"].as_bool().unwrap_or_else(|| { + eprintln!( + "{}\n{} {} {} {}", + "enable_images has to be a bool...".red().bold(), + "EXAMPLE:".bold(), + "true".bright_green().bold(), + "not".bold(), + "'true'".red().bold() + ); + std::process::exit(2) + }), + images["imgur_images"].as_bool().unwrap_or_else(|| { + eprintln!( + "{}\n{} {} {} {}", + "imgur_images has to be a bool...".red().bold(), + "EXAMPLE:".bold(), + "true".bright_green().bold(), + "not".bold(), + "'true'".red().bold() + ); + std::process::exit(2) + }) + ); + + config.build() + } +} diff --git a/src/core/error.rs b/src/core/error.rs new file mode 100644 index 0000000..4ea581f --- /dev/null +++ b/src/core/error.rs @@ -0,0 +1,65 @@ +use std::env; + +#[derive(Debug)] +pub enum ConfigError { + MissingConfig(String), + Io(String), + Json(String), + VarError(String), +} + +impl From<&'static str> for ConfigError { + fn from(value: &'static str) -> Self { + Self::MissingConfig(value.to_string()) + } +} + +impl From for ConfigError { + fn from(value: std::io::Error) -> Self { + Self::Io(format!("Unable to open file: {}", value)) + } +} + +impl From for ConfigError { + fn from(value: serde_json::Error) -> Self { + Self::Json(format!("Unable to parse config: {}", value)) + } +} + +impl From for ConfigError { + fn from(value: env::VarError) -> Self { + Self::VarError(format!("Unable to get environment variables: {}", value)) + } +} + +#[derive(Debug)] +pub enum ImgurError { + Reqwest(String), + Io(String), + Json(String), + VarError(String), +} + +impl From for ImgurError { + fn from(value: reqwest::Error) -> Self { + Self::Reqwest(format!("Error uploading image: {}", value)) + } +} + +impl From for ImgurError { + fn from(value: std::io::Error) -> Self { + Self::Io(format!("Unable to open file: {}", value)) + } +} + +impl From for ImgurError { + fn from(value: serde_json::Error) -> Self { + Self::Json(format!("Unable to parse urls: {}", value)) + } +} + +impl From for ImgurError { + fn from(value: env::VarError) -> Self { + Self::VarError(format!("Unable to get environment variables: {}", value)) + } +} diff --git a/src/core/mod.rs b/src/core/mod.rs new file mode 100644 index 0000000..00ccafc --- /dev/null +++ b/src/core/mod.rs @@ -0,0 +1,3 @@ +pub mod config; +pub mod error; +pub mod updates; diff --git a/src/core/updates.rs b/src/core/updates.rs new file mode 100644 index 0000000..7c3ec4d --- /dev/null +++ b/src/core/updates.rs @@ -0,0 +1,31 @@ +use colored::Colorize; + +use crate::VERSION; +pub async fn checker() { + let current = VERSION.unwrap_or("0.0.0").to_string(); + let latest = get_latest_github() + .await + .unwrap_or( + current.clone() + ); + if latest != current { + eprintln!("{} (Current: v{}, Latest: v{})\n{}\n{}\n{}", + "You are not running the latest version of Jellyfin-RPC".red().bold(), + current, + latest, + "A newer version can be found at".red().bold(), + "https://github.com/Radiicall/jellyfin-rpc/releases/latest".green().bold(), + "This can be safely ignored if you are running a prerelease version".bold()); + std::thread::sleep(std::time::Duration::from_secs(1)); + } +} + +async fn get_latest_github() -> Result { + let url = reqwest::get("https://github.com/Radiicall/jellyfin-rpc/releases/latest") + .await? + .url() + .as_str() + .trim_start_matches("https://github.com/Radiicall/jellyfin-rpc/releases/tag/") + .to_string(); + Ok(url) +} diff --git a/src/main.rs b/src/main.rs index fd76346..2368818 100644 --- a/src/main.rs +++ b/src/main.rs @@ -1,8 +1,9 @@ pub mod services; +use crate::core::updates; pub use crate::services::imgur::*; pub use crate::services::jellyfin::*; -pub mod config; -pub use crate::config::*; +pub mod core; +pub use crate::core::config::*; use clap::Parser; use colored::Colorize; use discord_rich_presence::{activity, DiscordIpc, DiscordIpcClient}; @@ -31,6 +32,7 @@ struct Args { #[tokio::main] async fn main() -> Result<(), Box> { + updates::checker().await; let args = Args::parse(); let config_path = args.config.unwrap_or_else(|| { get_config_path().unwrap_or_else(|err| { @@ -38,7 +40,7 @@ async fn main() -> Result<(), Box> { std::process::exit(1) }) }); - + std::fs::create_dir_all(std::path::Path::new(&config_path).parent().unwrap()).ok(); let config = Config::load_config(config_path.clone()).unwrap_or_else(|e| { @@ -114,10 +116,7 @@ async fn main() -> Result<(), Box> { // Start loop loop { let mut content = Content::get( - &config.url, - &config.api_key, - &config.username, - &config.images.enabled, + &config ) .await?; diff --git a/src/services/imgur.rs b/src/services/imgur.rs index 4ca5067..b1409fb 100644 --- a/src/services/imgur.rs +++ b/src/services/imgur.rs @@ -1,3 +1,4 @@ +use crate::core::error::ImgurError; use serde_json::Value; use std::env; use std::io::Write; @@ -17,38 +18,6 @@ pub struct Imgur { pub url: String, } -#[derive(Debug)] -pub enum ImgurError { - Reqwest(String), - Io(String), - Json(String), - VarError(String), -} - -impl From for ImgurError { - fn from(value: reqwest::Error) -> Self { - Self::Reqwest(format!("Error uploading image: {}", value)) - } -} - -impl From for ImgurError { - fn from(value: std::io::Error) -> Self { - Self::Io(format!("Unable to open file: {}", value)) - } -} - -impl From for ImgurError { - fn from(value: serde_json::Error) -> Self { - Self::Json(format!("Unable to parse urls: {}", value)) - } -} - -impl From for ImgurError { - fn from(value: env::VarError) -> Self { - Self::VarError(format!("Unable to get environment variables: {}", value)) - } -} - pub fn get_urls_path() -> Result { if cfg!(not(windows)) { let user = env::var("USER")?; @@ -67,9 +36,9 @@ pub fn get_urls_path() -> Result { impl Imgur { pub async fn get( - image_url: &String, - item_id: &String, - client_id: &String, + image_url: &str, + item_id: &str, + client_id: &str, image_urls_file: Option, ) -> Result { let file = image_urls_file.unwrap_or_else(|| get_urls_path().unwrap()); @@ -102,9 +71,9 @@ impl Imgur { async fn write_file( file: String, - image_url: &String, - item_id: &String, - client_id: &String, + image_url: &str, + item_id: &str, + client_id: &str, json: &mut Value, ) -> Result { let mut new_data = serde_json::Map::new(); @@ -118,7 +87,7 @@ impl Imgur { Ok(imgur_url) } - async fn upload(image_url: &String, client_id: &String) -> Result { + async fn upload(image_url: &str, client_id: &str) -> Result { let img = reqwest::get(image_url).await?.bytes().await?; let client = reqwest::Client::new(); let response = client diff --git a/src/services/jellyfin.rs b/src/services/jellyfin.rs index 7e8ef73..5dce8d3 100644 --- a/src/services/jellyfin.rs +++ b/src/services/jellyfin.rs @@ -1,9 +1,68 @@ use serde_json::Value; +use crate::core::config::Config; + /* TODO: Comments */ +#[derive(Default, Clone)] +struct ContentBuilder { + media_type: MediaType, + details: String, + state_message: String, + endtime: Option, + image_url: String, + item_id: String, + external_services: Vec +} + +impl ContentBuilder { + fn new() -> Self { + Self::default() + } + + fn media_type(&mut self, media_type: MediaType) { + self.media_type = media_type; + } + + fn details(&mut self, details: String) { + self.details = details; + } + + fn state_message(&mut self, state_message: String) { + self.state_message = state_message; + } + + fn endtime(&mut self, endtime: Option) { + self.endtime = endtime; + } + + fn image_url(&mut self, image_url: String) { + self.image_url = image_url; + } + + fn item_id(&mut self, item_id: String) { + self.item_id = item_id; + } + + fn external_services(&mut self, external_services: Vec) { + self.external_services = external_services; + } + + pub fn build(self) -> Content { + Content { + media_type: self.media_type, + details: self.details, + state_message: self.state_message, + endtime: self.endtime, + image_url: self.image_url, + item_id: self.item_id, + external_services: self.external_services, + } + } +} + #[derive(Default)] pub struct Content { pub media_type: MediaType, @@ -17,16 +76,13 @@ pub struct Content { impl Content { pub async fn get( - url: &str, - api_key: &String, - username: &String, - enable_images: &bool, + config: &Config ) -> Result { let sessions: Vec = serde_json::from_str( &reqwest::get(format!( "{}/Sessions?api_key={}", - url.trim_end_matches('/'), - api_key + config.url.trim_end_matches('/'), + config.api_key )) .await? .text() @@ -35,45 +91,42 @@ impl Content { .unwrap_or_else(|_| { panic!( "Can't unwrap URL, check if JELLYFIN_URL is correct. Current URL: {}", - url + config.url ) }); for session in sessions { - if Option::is_none(&session.get("UserName")) { + if session.get("UserName").is_none() { continue; } - if session["UserName"].as_str().unwrap() != username { + + if config.username.iter().all(|username| session["UserName"].as_str().unwrap() != username) { continue; } - if Option::is_none(&session.get("NowPlayingItem")) { + + if session.get("NowPlayingItem").is_none() { continue; } - let now_playing_item = &session["NowPlayingItem"]; + let mut content = ContentBuilder::new(); - let external_services = ExternalServices::get(now_playing_item).await; + let now_playing_item = &session["NowPlayingItem"]; - let main = Content::watching(now_playing_item).await; + Content::watching(&mut content, now_playing_item, config).await; let mut image_url: String = "".to_string(); - if enable_images == &true { - image_url = Content::image(url, main[3].clone()).await; + if config.images.enabled { + image_url = Content::image(&config.url, content.item_id.clone()).await; } + content.external_services(ExternalServices::get(now_playing_item).await); + content.endtime(Content::time_left(now_playing_item, &session).await); + content.image_url(image_url); - return Ok(Self { - media_type: main[0].clone().into(), - details: main[1].clone(), - state_message: main[2].clone(), - endtime: Content::time_left(now_playing_item, &session).await, - image_url, - item_id: main[3].clone(), - external_services, - }); + return Ok(content.build()); } Ok(Self::default()) } - async fn watching(now_playing_item: &Value) -> Vec { + async fn watching(content: &mut ContentBuilder, now_playing_item: &Value, config: &Config) { /* FIXME: Update this explanation/remove it. @@ -87,28 +140,22 @@ impl Content { Then we send it off as a Vec with the external urls and the end timer to the main loop. */ let name = now_playing_item["Name"].as_str().unwrap(); - let item_type: String; - let item_id: String; let mut genres = "".to_string(); if now_playing_item["Type"].as_str().unwrap() == "Episode" { - item_type = "episode".to_owned(); - let series_name = now_playing_item["SeriesName"].as_str().unwrap().to_string(); - item_id = now_playing_item["SeriesId"].as_str().unwrap().to_string(); - let season = now_playing_item["ParentIndexNumber"].to_string(); let first_episode_number = now_playing_item["IndexNumber"].to_string(); - let mut msg = "S".to_owned() + &season + "E" + &first_episode_number; + let mut state = "S".to_owned() + &season + "E" + &first_episode_number; - if !Option::is_none(&now_playing_item.get("IndexNumberEnd")) { - msg += &("-".to_string() + &now_playing_item["IndexNumberEnd"].to_string()); + if now_playing_item.get("IndexNumberEnd").is_some() { + state += &("-".to_string() + &now_playing_item["IndexNumberEnd"].to_string()); } - msg += &(" ".to_string() + name); - - vec![item_type, series_name, msg, item_id] + state += &(" ".to_string() + name); + content.media_type(MediaType::Episode); + content.details(now_playing_item["SeriesName"].as_str().unwrap().to_string()); + content.state_message(state); + content.item_id(now_playing_item["SeriesId"].as_str().unwrap().to_string()); } else if now_playing_item["Type"].as_str().unwrap() == "Movie" { - item_type = "movie".to_owned(); - item_id = now_playing_item["Id"].as_str().unwrap().to_string(); match now_playing_item.get("Genres") { None => (), genre_array => { @@ -123,43 +170,63 @@ impl Content { } }; - vec![item_type, name.to_string(), genres, item_id] + content.media_type(MediaType::Movie); + content.details(name.into()); + content.state_message(genres); + content.item_id(now_playing_item["Id"].as_str().unwrap().to_string()); } else if now_playing_item["Type"].as_str().unwrap() == "Audio" { - item_type = "music".to_owned(); - item_id = now_playing_item["AlbumId"].as_str().unwrap().to_string(); - let artist = now_playing_item["AlbumArtist"].as_str().unwrap(); - match now_playing_item.get("Genres") { - None => (), - genre_array => { - genres.push_str(" - "); - genres += &genre_array - .unwrap() - .as_array() - .unwrap() - .iter() - .map(|x| x.as_str().unwrap().to_string()) - .collect::>() - .join(", "); + let artist = now_playing_item["AlbumArtist"].as_str().unwrap().to_string(); + let mut state = format!("By {} - ", artist); + let mut index = 0; + config.music.display.iter().for_each(|data| { + index += 1; + let data = data.as_str(); + let old_state = state.clone(); + match data { + "genres" => match now_playing_item.get("Genres") { + None => (), + genre_array => { + state.push_str( + &genre_array + .unwrap() + .as_array() + .unwrap() + .iter() + .map(|genre| genre.as_str().unwrap().to_string()) + .collect::>() + .join(", ") + ) + } + }, + "album" => state.push_str(now_playing_item["Album"].as_str().unwrap_or("")), + "year" => { + let mut year = now_playing_item["ProductionYear"].as_u64().unwrap_or(0).to_string(); + if year == "0" { + year = String::from(""); + } + state.push_str(&year) + }, + _ => state = format!("By {}", artist), } - }; - - let msg = format!("By {}{}", artist, genres); + + if state != old_state && config.music.display.len() != index { + if config.music.separator.is_some() { + state.push_str(&format!(" {} ", config.music.separator.unwrap())) + } else { + state.push(' ') + } + } + }); - vec![item_type, name.to_string(), msg, item_id] + content.media_type(MediaType::Music); + content.details(name.into()); + content.state_message(state); + content.item_id(now_playing_item["AlbumId"].as_str().unwrap().to_string()); } else if now_playing_item["Type"].as_str().unwrap() == "TvChannel" { - item_type = "livetv".to_owned(); - item_id = now_playing_item["Id"].as_str().unwrap().to_string(); - let msg = "Live TV".to_string(); - - vec![item_type, name.to_string(), msg, item_id] - } else { - // Return 4 empty strings to make vector equal length - vec![ - "".to_string(), - "".to_string(), - "".to_string(), - "".to_string(), - ] + content.media_type(MediaType::LiveTv); + content.details(name.into()); + content.state_message("Live TV".into()); + content.item_id(now_playing_item["Id"].as_str().unwrap().to_string()); } } @@ -194,7 +261,7 @@ impl Content { } } -#[derive(Debug)] +#[derive(Debug, Clone)] pub struct ExternalServices { pub name: String, pub url: String, @@ -216,8 +283,8 @@ impl ExternalServices { i.get("Url").and_then(Value::as_str), ) { external_services.push(Self { - name: name.to_string(), - url: url.to_string(), + name: name.into(), + url: url.into(), }); if external_services.len() == 2 { break; @@ -259,14 +326,7 @@ impl Default for MediaType { impl MediaType { pub fn is_none(&self) -> bool { - if self == &MediaType::None { - return true; - } - false - } - - pub fn equal_to(&self, value: String) -> bool { - self == &MediaType::from(value) + self == &Self::None } }