diff --git a/Cargo.lock b/Cargo.lock index 131aff0..99a624f 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -471,23 +471,7 @@ checksum = "af150ab688ff2122fcef229be89cb50dd66af9e01a4ff320cc137eecc9bacc38" [[package]] name = "jellyfin-rpc" -version = "0.1.4" -dependencies = [ - "async-recursion", - "colored", - "discord-rich-presence", - "reqwest", - "retry", - "serde", - "serde_json", - "tokio", -] - -[[package]] -name = "jellyfin-rpc" -version = "0.1.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "856f3991e7242adf39b63489d431f3186749579df17f0b4fedd0916de46ae6e9" +version = "0.1.5" dependencies = [ "async-recursion", "colored", @@ -501,12 +485,12 @@ dependencies = [ [[package]] name = "jellyfin-rpc-cli" -version = "0.15.4" +version = "0.15.5" dependencies = [ "clap", "colored", "discord-rich-presence", - "jellyfin-rpc 0.1.4 (registry+https://github.com/rust-lang/crates.io-index)", + "jellyfin-rpc", "reqwest", "retry", "tokio", diff --git a/example.json b/example.json index 9c65f0c..d166fab 100644 --- a/example.json +++ b/example.json @@ -7,6 +7,7 @@ "display": "genres", "separator": "-" }, + "self_signed_cert": false, "_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"], diff --git a/jellyfin-rpc-cli/Cargo.toml b/jellyfin-rpc-cli/Cargo.toml index 0f52fab..fd66f5e 100644 --- a/jellyfin-rpc-cli/Cargo.toml +++ b/jellyfin-rpc-cli/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "jellyfin-rpc-cli" -version = "0.15.4" +version = "0.15.5" edition = "2021" description = "Displays the content you're currently watching on Discord!" license = "GPL-3.0-or-later" @@ -27,8 +27,8 @@ retry = "2.0" [dependencies.jellyfin-rpc] features = ["imgur", "cli"] -version = "0.1.4" -#path = "../jellyfin-rpc" +#version = "0.1.4" +path = "../jellyfin-rpc" [dependencies.clap] features = ["derive"] diff --git a/jellyfin-rpc-cli/src/main.rs b/jellyfin-rpc-cli/src/main.rs index 899d939..a6a8b46 100644 --- a/jellyfin-rpc-cli/src/main.rs +++ b/jellyfin-rpc-cli/src/main.rs @@ -1,8 +1,8 @@ use clap::Parser; use colored::Colorize; use discord_rich_presence::{activity, DiscordIpc, DiscordIpcClient}; -pub use jellyfin_rpc::services::imgur::*; pub use jellyfin_rpc::prelude::*; +pub use jellyfin_rpc::services::imgur::*; use retry::retry_with_index; #[cfg(feature = "updates")] mod updates; @@ -26,6 +26,13 @@ struct Args { help = "Path to image urls file for imgur" )] image_urls: Option, + #[arg( + short = 's', + long = "suppress-warnings", + help = "Stops warnings from showing on startup", + default_value_t = false + )] + suppress_warnings: bool, } #[tokio::main] @@ -68,11 +75,22 @@ async fn main() -> Result<(), Box> { "Jellyfin-RPC".bright_blue() ); - if config - .clone() - .images - .and_then(|images| images.enable_images) - .unwrap_or(false) + if !args.suppress_warnings && config.jellyfin.self_signed_cert.is_some_and(|val| val) { + eprintln!( + "{}\n{}", + "------------------------------------------------------------------".bold(), + "WARNING: Self-signed certificates are enabled!" + .bold() + .red() + ); + } + + if !args.suppress_warnings + && config + .clone() + .images + .and_then(|images| images.enable_images) + .unwrap_or(false) && !config .clone() .images @@ -82,7 +100,7 @@ async fn main() -> Result<(), Box> { eprintln!( "{}\n{}", "------------------------------------------------------------------".bold(), - "Images without Imgur requires port forwarding!" + "WARNING: Images without Imgur requires port forwarding!" .bold() .red() ) @@ -174,6 +192,7 @@ async fn main() -> Result<(), Box> { &config.jellyfin.api_key, &content.item_id, library, + config.jellyfin.self_signed_cert.unwrap_or(false), ) .await?; } @@ -207,6 +226,7 @@ async fn main() -> Result<(), Box> { .and_then(|imgur| imgur.client_id) .expect("Imgur client ID cant be loaded."), args.image_urls.clone(), + config.jellyfin.self_signed_cert.unwrap_or(false), ) .await .unwrap_or_else(|e| { diff --git a/jellyfin-rpc/Cargo.toml b/jellyfin-rpc/Cargo.toml index f9edb64..fe3e8f8 100644 --- a/jellyfin-rpc/Cargo.toml +++ b/jellyfin-rpc/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "jellyfin-rpc" -version = "0.1.4" +version = "0.1.5" edition = "2021" description = "Backend for the Jellyfin-RPC-cli and Jellyfin-RPC-Iced projects" license = "GPL-3.0-or-later" diff --git a/jellyfin-rpc/example.json b/jellyfin-rpc/example.json index 9c65f0c..d166fab 100644 --- a/jellyfin-rpc/example.json +++ b/jellyfin-rpc/example.json @@ -7,6 +7,7 @@ "display": "genres", "separator": "-" }, + "self_signed_cert": false, "_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"], diff --git a/jellyfin-rpc/src/core/config.rs b/jellyfin-rpc/src/core/config.rs index 7439db4..19fbf43 100644 --- a/jellyfin-rpc/src/core/config.rs +++ b/jellyfin-rpc/src/core/config.rs @@ -5,13 +5,13 @@ use serde_json; use std::env; /// Main struct containing every other struct in the file. -/// +/// /// The config file is parsed into this struct. #[derive(Debug, Serialize, Deserialize, Clone, PartialEq)] #[serde(rename_all = "lowercase")] pub struct Config { /// Jellyfin configuration. - /// + /// /// Has every required part of the config, hence why its not an `Option`. pub jellyfin: Jellyfin, /// Discord configuration. @@ -35,6 +35,8 @@ pub struct Jellyfin { pub music: Option, /// Blacklist configuration. pub blacklist: Option, + /// Self signed certificate option + pub self_signed_cert: Option, } /// Username of the person that info should be gathered from. @@ -51,7 +53,7 @@ pub enum Username { #[derive(Debug, Serialize, Deserialize, Clone, PartialEq)] pub struct Music { /// Display is where you tell the program what should be displayed. - /// + /// /// Example: `vec![String::from("genres"), String::from("year")]` pub display: Option, /// Separator is what should be between the artist(s) and the `display` options. @@ -59,7 +61,7 @@ pub struct Music { } /// Display is where you tell the program what should be displayed. -/// +/// /// Example: `vec![String::from("genres"), String::from("year")]` #[derive(Debug, Serialize, Deserialize, Clone, PartialEq)] #[serde(untagged)] @@ -89,7 +91,7 @@ pub struct Discord { } /// Button struct -/// +/// /// Contains information about buttons #[derive(Debug, Serialize, Deserialize, Clone, PartialEq)] pub struct Button { @@ -116,9 +118,9 @@ pub struct Images { } /// Find config.json in filesystem. -/// +/// /// This is to avoid the user having to specify a filepath on launch. -/// +/// /// Default config path depends on OS /// Windows: `%appdata%\jellyfin-rpc\config.json` /// Linux/macOS: `~/.config/jellyfin-rpc/config.json` @@ -154,6 +156,7 @@ impl Default for Config { api_key: "".to_string(), music: None, blacklist: None, + self_signed_cert: None, }, discord: None, imgur: None, diff --git a/jellyfin-rpc/src/core/rpc.rs b/jellyfin-rpc/src/core/rpc.rs index 17e404c..79e6a4f 100644 --- a/jellyfin-rpc/src/core/rpc.rs +++ b/jellyfin-rpc/src/core/rpc.rs @@ -2,7 +2,7 @@ use crate::prelude::MediaType; use discord_rich_presence::activity; /// Used to set the activity on Discord. -/// +/// /// This has checks to do different things for different mediatypes and replaces images with default ones if they are needed. pub fn setactivity<'a>( state_message: &'a str, diff --git a/jellyfin-rpc/src/lib.rs b/jellyfin-rpc/src/lib.rs index 53b6641..89e35b9 100644 --- a/jellyfin-rpc/src/lib.rs +++ b/jellyfin-rpc/src/lib.rs @@ -3,16 +3,16 @@ /// Main module pub mod core; /// Useful imports -/// +/// /// Contains imports that most programs will be using. pub mod prelude; /// External connections pub mod services; pub use crate::core::error; +pub use core::rpc::setactivity; use discord_rich_presence::DiscordIpc; use discord_rich_presence::DiscordIpcClient; use retry::retry_with_index; -pub use core::rpc::setactivity; #[cfg(test)] mod tests; @@ -61,3 +61,16 @@ pub fn connect(rich_presence_client: &mut DiscordIpcClient) { ) .unwrap(); } + +/// Built in reqwest::get() function, has an extra field to specify if the self signed cert should be accepted. +pub async fn get( + url: U, + self_signed_cert: bool, +) -> Result { + reqwest::Client::builder() + .danger_accept_invalid_certs(self_signed_cert) + .build()? + .get(url) + .send() + .await +} diff --git a/jellyfin-rpc/src/services/imgur.rs b/jellyfin-rpc/src/services/imgur.rs index 1af6e2f..45ed676 100644 --- a/jellyfin-rpc/src/services/imgur.rs +++ b/jellyfin-rpc/src/services/imgur.rs @@ -10,9 +10,9 @@ pub struct Imgur { } /// Find urls.json in filesystem, used to store images that were already previously uploaded to imgur. -/// +/// /// This is to avoid the user having to specify a filepath on launch. -/// +/// /// Default urls.json path depends on OS /// Windows: `%appdata%\jellyfin-rpc\urls.json` /// Linux/macOS: `~/.config/jellyfin-rpc/urls.json` @@ -32,13 +32,14 @@ pub fn get_urls_path() -> Result { impl Imgur { /// Queries the urls.json file for an imgur url with the same item ID attached. - /// + /// /// If there's no imgur URL in the file, it will upload the image to imgur, store it in the file and then hand the URL over in a result. pub async fn get( image_url: &str, item_id: &str, client_id: &str, image_urls_file: Option, + self_signed_cert: bool, ) -> Result { let file = match image_urls_file { Some(file) => file, @@ -53,7 +54,15 @@ impl Imgur { } Ok(Self { - url: Imgur::write_file(file, image_url, item_id, client_id, &mut json).await?, + url: Imgur::write_file( + file, + image_url, + item_id, + client_id, + self_signed_cert, + &mut json, + ) + .await?, }) } @@ -87,12 +96,13 @@ impl Imgur { image_url: &str, item_id: &str, client_id: &str, + self_signed_cert: bool, json: &mut Value, ) -> Result { // Create a new map that's used for adding data to the "urls.json" file let mut new_data = serde_json::Map::new(); // Upload the content's image to imgur - let imgur_url = Imgur::upload(image_url, client_id).await?; + let imgur_url = Imgur::upload(image_url, client_id, self_signed_cert).await?; // Insert the item_id and the new image url into the map we created earlier new_data.insert(item_id.to_string(), json!(imgur_url)); @@ -105,8 +115,15 @@ impl Imgur { Ok(imgur_url) } - async fn upload(image_url: &str, client_id: &str) -> Result { - let img = reqwest::get(image_url).await?.bytes().await?; + async fn upload( + image_url: &str, + client_id: &str, + self_signed_cert: bool, + ) -> Result { + let img = crate::get(image_url, self_signed_cert) + .await? + .bytes() + .await?; let client = reqwest::Client::new(); let response = client .post("https://api.imgur.com/3/image") diff --git a/jellyfin-rpc/src/services/jellyfin.rs b/jellyfin-rpc/src/services/jellyfin.rs index 741ba00..706f84f 100644 --- a/jellyfin-rpc/src/services/jellyfin.rs +++ b/jellyfin-rpc/src/services/jellyfin.rs @@ -65,7 +65,7 @@ impl ContentBuilder { #[derive(Default)] pub struct Content { /// What type of content is currently playing. - /// + /// /// Example: MediaType::Movie pub media_type: MediaType, /// The title of the content @@ -75,23 +75,23 @@ pub struct Content { /// When the content will end, current UNIX epoch + time left pub endtime: Option, /// Image URL supplied by Jellyfin, this is different from the Imgur URL - /// + /// /// This has to be passed to the Imgur::get() function to upload images to imgur pub image_url: String, - /// Item ID of the content currently playing, + /// Item ID of the content currently playing, /// used to store Imgur URLs so that they can be reused instead of reuploading to Imgur every time. pub item_id: String, /// External services to display as buttons. - /// + /// /// Example: IMDb, Trakt, etc. pub external_services: Vec, } impl Content { /// Calls the Content::get() function recursively until it returns a Content struct. - /// + /// /// It waits (attempt * 5) seconds before retrying. - /// + /// /// The max time it will wait is 30 seconds. #[async_recursion] pub async fn try_get(config: &Config, attempt: u64) -> Self { @@ -118,11 +118,14 @@ impl Content { /// Returns a Content struct with the updated information from jellyfin pub async fn get(config: &Config) -> Result { let sessions: Vec = serde_json::from_str( - &reqwest::get(format!( - "{}/Sessions?api_key={}", - config.jellyfin.url.trim_end_matches('/'), - config.jellyfin.api_key - )) + &crate::get( + format!( + "{}/Sessions?api_key={}", + config.jellyfin.url.trim_end_matches('/'), + config.jellyfin.api_key + ), + config.jellyfin.self_signed_cert.unwrap_or(false), + ) .await? .text() .await?, @@ -175,9 +178,13 @@ impl Content { .and_then(|images| images.enable_images) .unwrap_or(false) { - image_url = Content::image(&config.jellyfin.url, content.item_id.clone()) - .await - .unwrap_or(String::from("")); + image_url = Content::image( + &config.jellyfin.url, + content.item_id.clone(), + config.jellyfin.self_signed_cert.unwrap_or(false), + ) + .await + .unwrap_or(String::from("")); } content.external_services(ExternalServices::get(now_playing_item).await); @@ -395,14 +402,18 @@ impl Content { state } - async fn image(url: &str, item_id: String) -> Result { + async fn image( + url: &str, + item_id: String, + self_signed_cert: bool, + ) -> Result { let img = format!( "{}/Items/{}/Images/Primary", url.trim_end_matches('/'), item_id ); - if reqwest::get(&img) + if crate::get(&img, self_signed_cert) .await? .text() .await @@ -439,11 +450,11 @@ impl Content { #[derive(Debug, Clone)] pub struct ExternalServices { /// Name of the service - /// + /// /// Example: IMDb, Trakt pub name: String, /// URL pointing to the specific Show/Movie etc. on the external service. - /// + /// /// Example: , pub url: String, } @@ -616,14 +627,18 @@ pub async fn library_check( api_key: &str, item_id: &str, library: &str, + self_signed_cert: bool, ) -> Result> { let parents: Vec = serde_json::from_str( - &reqwest::get(format!( - "{}/Items/{}/Ancestors?api_key={}", - url.trim_end_matches('/'), - item_id, - api_key - )) + &crate::get( + format!( + "{}/Items/{}/Ancestors?api_key={}", + url.trim_end_matches('/'), + item_id, + api_key + ), + self_signed_cert, + ) .await? .text() .await?, diff --git a/jellyfin-rpc/src/tests.rs b/jellyfin-rpc/src/tests.rs index 8f82088..ca7e65a 100644 --- a/jellyfin-rpc/src/tests.rs +++ b/jellyfin-rpc/src/tests.rs @@ -1,5 +1,5 @@ +use crate::prelude::{config::*, Content, MediaType}; use std::env; -use crate::prelude::{Content, MediaType, config::*}; #[test] fn load_example_config() { @@ -10,22 +10,31 @@ fn load_example_config() { username: Username::String("your_username_here".to_string()), music: Some(Music { display: Some(Display::String("genres".to_string())), - separator: Some('-') + separator: Some('-'), }), blacklist: Some(Blacklist { - media_types: Some(vec![MediaType::Music, MediaType::Movie, MediaType::Episode, MediaType::LiveTv]), - libraries: Some(vec!["Anime".to_string(), "Anime Movies".to_string()]) - }) + media_types: Some(vec![ + MediaType::Music, + MediaType::Movie, + MediaType::Episode, + MediaType::LiveTv, + ]), + libraries: Some(vec!["Anime".to_string(), "Anime Movies".to_string()]), + }), + self_signed_cert: Some(false), }, discord: Some(Discord { application_id: Some("1053747938519679018".to_string()), - buttons: Some(vec![Button { - name: "dynamic".to_string(), - url: "dynamic".to_string() - }, Button { - name: "dynamic".to_string(), - url: "dynamic".to_string() - }]) + buttons: Some(vec![ + Button { + name: "dynamic".to_string(), + url: "dynamic".to_string(), + }, + Button { + name: "dynamic".to_string(), + url: "dynamic".to_string(), + }, + ]), }), imgur: Some(Imgur { client_id: Some("asdjdjdg394209fdjs093".to_string()), @@ -33,10 +42,11 @@ fn load_example_config() { images: Some(Images { enable_images: Some(true), imgur_images: Some(true), - }) + }), }; - let config = Config::load(&(env::var("CARGO_MANIFEST_DIR").unwrap() + "/example.json")).unwrap(); + let config = + Config::load(&(env::var("CARGO_MANIFEST_DIR").unwrap() + "/example.json")).unwrap(); assert_eq!(example, config); } @@ -50,21 +60,20 @@ fn try_get_content() { api_key: "sadasodsapasdskd".to_string(), username: Username::String("your_username_here".to_string()), music: None, - blacklist: None + blacklist: None, + self_signed_cert: None, }, discord: None, imgur: None, - images: None + images: None, }; let rt = tokio::runtime::Builder::new_current_thread() - .enable_all() - .build() - .unwrap(); + .enable_all() + .build() + .unwrap(); - rt.block_on( - Content::get(&config) - ).unwrap(); + rt.block_on(Content::get(&config)).unwrap(); } #[cfg(feature = "imgur")] @@ -77,9 +86,14 @@ fn try_imgur() { .build() .unwrap(); - rt.block_on( - imgur::Imgur::get("", "", "", Some(env::var("CARGO_MANIFEST_DIR").unwrap() + "/example.json")) - ).unwrap(); + rt.block_on(imgur::Imgur::get( + "", + "", + "", + Some(env::var("CARGO_MANIFEST_DIR").unwrap() + "/example.json"), + false, + )) + .unwrap(); } #[test] @@ -87,5 +101,8 @@ fn media_type_is_none() { let media_type_1 = MediaType::Movie; let media_type_2 = MediaType::None; - assert_eq!(media_type_1.is_none() == false, media_type_2.is_none() == true) + assert_eq!( + media_type_1.is_none() == false, + media_type_2.is_none() == true + ) }