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
}
}