From ed292cf3bd33e2013800454821ff8d8ae7f3e90b Mon Sep 17 00:00:00 2001 From: Luther Schallot Date: Sat, 4 Jan 2025 15:31:48 -0600 Subject: [PATCH 01/18] Code formatted via rustfmt. --- src/entities/activity_details.rs | 21 +- src/entities/device_details.rs | 21 +- src/entities/library_details.rs | 29 +- src/entities/log_details.rs | 17 +- src/entities/media_details.rs | 27 +- src/entities/mod.rs | 14 +- src/entities/movie_details.rs | 32 +- src/entities/package_details.rs | 28 +- src/entities/plugin_details.rs | 30 +- src/entities/repository_details.rs | 24 +- src/entities/server_info.rs | 8 +- src/entities/task_details.rs | 10 +- src/entities/user_details.rs | 2 +- src/main.rs | 2379 +++++++++++++++------------- src/plugin_actions.rs | 19 +- src/responder.rs | 25 +- src/system_actions.rs | 969 ++++++----- src/user_actions.rs | 614 +++---- src/utils/mod.rs | 2 +- src/utils/output_writer.rs | 2 +- src/utils/status_handler.rs | 2 +- 21 files changed, 2359 insertions(+), 1916 deletions(-) diff --git a/src/entities/activity_details.rs b/src/entities/activity_details.rs index 4b23bc6..979077b 100644 --- a/src/entities/activity_details.rs +++ b/src/entities/activity_details.rs @@ -1,7 +1,7 @@ use serde_derive::Deserialize; use serde_derive::Serialize; -use comfy_table::{ Table, ContentArrangement }; +use comfy_table::{ContentArrangement, Table}; #[derive(Default, Clone, Serialize, Deserialize)] pub struct ActivityDetails { @@ -42,7 +42,15 @@ impl ActivityDetails { let mut table = Table::new(); table .set_content_arrangement(ContentArrangement::Dynamic) - .set_header(vec!["Date", "User", "Type", "Severity", "Name", "ShortOverview", "Overview"]); + .set_header(vec![ + "Date", + "User", + "Type", + "Severity", + "Name", + "ShortOverview", + "Overview", + ]); for activity in activities.items { table.add_row(vec![ &activity.date, @@ -51,17 +59,18 @@ impl ActivityDetails { &activity.severity, &activity.name, &activity.short_overview, - &activity.overview + &activity.overview, ]); } println!("{table}"); } - pub fn print_as_csv(activities: ActivityDetails) -> String{ + pub fn print_as_csv(activities: ActivityDetails) -> String { // first print the headers let mut data: String = "Date,User,Type,Severity,Name,ShortOverview,Overview\n".to_owned(); for activity in activities.items { - let piece = format!("{},{},{},{},{},{},{}\n", + let piece = format!( + "{},{},{},{},{},{},{}\n", &activity.date, &activity.id.to_string(), &activity.type_field, @@ -74,4 +83,4 @@ impl ActivityDetails { } data } -} \ No newline at end of file +} diff --git a/src/entities/device_details.rs b/src/entities/device_details.rs index 3994a58..5bacc31 100644 --- a/src/entities/device_details.rs +++ b/src/entities/device_details.rs @@ -1,9 +1,9 @@ -use comfy_table::{ Table, ContentArrangement }; +use comfy_table::{ContentArrangement, Table}; #[derive(Serialize, Deserialize)] pub struct DeviceRootJson { #[serde(rename = "Items")] - pub items: Vec + pub items: Vec, } #[derive(Serialize, Deserialize)] @@ -15,16 +15,21 @@ pub struct DeviceDetails { #[serde(rename = "LastUserName")] pub lastusername: String, #[serde(rename = "DateLastActivity")] - pub lastactivity: String + pub lastactivity: String, } impl DeviceDetails { - pub fn new(id: String, name: String, lastusername: String, lastactivity: String) -> DeviceDetails { - DeviceDetails{ + pub fn new( + id: String, + name: String, + lastusername: String, + lastactivity: String, + ) -> DeviceDetails { + DeviceDetails { id, name, lastusername, - lastactivity + lastactivity, } } @@ -43,6 +48,4 @@ impl DeviceDetails { } println!("{table}"); } - - -} \ No newline at end of file +} diff --git a/src/entities/library_details.rs b/src/entities/library_details.rs index 83e643a..d3c470b 100644 --- a/src/entities/library_details.rs +++ b/src/entities/library_details.rs @@ -1,4 +1,4 @@ -use comfy_table::{ Table, ContentArrangement }; +use comfy_table::{ContentArrangement, Table}; pub type LibraryRootJson = Vec; @@ -15,12 +15,17 @@ pub struct LibraryDetails { } impl LibraryDetails { - pub fn new(name: String, collection_type: String, item_id: String, refresh_status: String) -> LibraryDetails { - LibraryDetails{ + pub fn new( + name: String, + collection_type: String, + item_id: String, + refresh_status: String, + ) -> LibraryDetails { + LibraryDetails { name, collection_type, item_id, - refresh_status + refresh_status, } } @@ -33,10 +38,20 @@ impl LibraryDetails { table .set_content_arrangement(ContentArrangement::Dynamic) .set_width(120) - .set_header(vec!["Library Name", "Collection Type", "Library Id", "Refresh Status"]); + .set_header(vec![ + "Library Name", + "Collection Type", + "Library Id", + "Refresh Status", + ]); for library in libraries { - table.add_row(vec![library.name, library.collection_type, library.item_id, library.refresh_status]); + table.add_row(vec![ + library.name, + library.collection_type, + library.item_id, + library.refresh_status, + ]); } println!("{table}"); } -} \ No newline at end of file +} diff --git a/src/entities/log_details.rs b/src/entities/log_details.rs index 4fabc8f..5c9844c 100644 --- a/src/entities/log_details.rs +++ b/src/entities/log_details.rs @@ -1,4 +1,4 @@ -use comfy_table::{ Table, ContentArrangement }; +use comfy_table::{ContentArrangement, Table}; #[derive(Serialize, Deserialize)] pub struct LogDetails { @@ -9,16 +9,16 @@ pub struct LogDetails { #[serde(rename = "Name")] pub name: String, #[serde(rename = "Size")] - pub size: i32 + pub size: i32, } impl LogDetails { pub fn new(date_created: String, date_modified: String, name: String, size: i32) -> LogDetails { - LogDetails{ + LogDetails { date_created, date_modified, name, - size + size, } } @@ -33,8 +33,13 @@ impl LogDetails { .set_width(120) .set_header(vec!["Log Name", "Size", "Date Created", "Last Modified"]); for log in logs { - table.add_row(vec![log.name, log.size.to_string(), log.date_created, log.date_modified]); + table.add_row(vec![ + log.name, + log.size.to_string(), + log.date_created, + log.date_modified, + ]); } println!("{table}"); } -} \ No newline at end of file +} diff --git a/src/entities/media_details.rs b/src/entities/media_details.rs index 88a8574..cbf3521 100644 --- a/src/entities/media_details.rs +++ b/src/entities/media_details.rs @@ -1,6 +1,6 @@ // THIS IS CURRENTLY NOT USEABLE -use comfy_table::{ Table, ContentArrangement }; +use comfy_table::{ContentArrangement, Table}; use serde_derive::Deserialize; use serde_derive::Serialize; @@ -25,8 +25,8 @@ pub struct MediaItem { #[serde(rename = "ServerId")] pub server_id: String, - #[serde(rename = "Id")] - pub id: String, + #[serde(rename = "Id")] + pub id: String, #[serde(rename = "Etag")] pub etag: String, #[serde(rename = "SourceType")] @@ -127,8 +127,8 @@ pub struct MediaItem { pub is_folder: bool, #[serde(rename = "ParentId")] pub parent_id: String, - #[serde(rename = "Type")] - pub type_field: String, + #[serde(rename = "Type")] + pub type_field: String, #[serde(rename = "People")] pub people: Vec, #[serde(rename = "Studios")] @@ -1053,8 +1053,7 @@ pub struct Chapter3 { #[derive(Default, Debug, Clone, PartialEq, Serialize, Deserialize)] #[serde(rename_all = "camelCase", default)] -pub struct CurrentProgram { -} +pub struct CurrentProgram {} impl MediaRoot { pub fn table_print(media: MediaRoot, table_columns: &Vec) { @@ -1063,9 +1062,7 @@ impl MediaRoot { .set_content_arrangement(ContentArrangement::Dynamic) .set_header(table_columns); for media_item in media.items { - table.add_row( - build_table_row(&media_item, table_columns) - ); + table.add_row(build_table_row(&media_item, table_columns)); } println!("{table}"); } @@ -1074,9 +1071,8 @@ impl MediaRoot { let mut wtr = csv::Writer::from_writer(std::io::stdout()); for media_item in media.items { - wtr.write_record( - build_table_row(&media_item, table_columns) - ).unwrap(); + wtr.write_record(build_table_row(&media_item, table_columns)) + .unwrap(); } } @@ -1096,10 +1092,9 @@ fn build_table_row(media_item: &MediaItem, table_columns: &Vec) -> Vec row.push(media_item.path.to_string()), "CRITICRATING" => row.push(media_item.critic_rating.to_string()), "PRODUCTIONYEAR" => row.push(media_item.production_year.to_string()), - _ => row.push("?".to_string()) + _ => row.push("?".to_string()), } } - + row } - diff --git a/src/entities/mod.rs b/src/entities/mod.rs index c82c294..1ffc656 100644 --- a/src/entities/mod.rs +++ b/src/entities/mod.rs @@ -1,13 +1,13 @@ -pub mod user_details; +pub mod activity_details; pub mod device_details; -pub mod task_details; -pub mod log_details; pub mod library_details; +pub mod log_details; +pub mod media_details; pub mod movie_details; +pub mod package_details; pub mod plugin_details; -pub mod activity_details; +pub mod repository_details; pub mod server_info; +pub mod task_details; pub mod token_details; -pub mod media_details; -pub mod repository_details; -pub mod package_details; \ No newline at end of file +pub mod user_details; diff --git a/src/entities/movie_details.rs b/src/entities/movie_details.rs index ff20420..7192b37 100644 --- a/src/entities/movie_details.rs +++ b/src/entities/movie_details.rs @@ -1,6 +1,6 @@ +use comfy_table::{ContentArrangement, Table}; use serde_derive::Deserialize; use serde_derive::Serialize; -use comfy_table::{ Table, ContentArrangement }; #[derive(Default, Debug, Clone, PartialEq, Serialize, Deserialize)] #[serde(rename_all = "camelCase")] @@ -41,10 +41,21 @@ pub struct Item { impl MovieDetails { pub fn table_print(movies: MovieDetails) { let mut table = Table::new(); - table + table .set_content_arrangement(ContentArrangement::Dynamic) - .set_header(vec!["Name", "Date Added", "Premiere Date", "Release Year", "Genres", "Parental Rating", "Community Rating", - "Runtime (in minutes)", "Resolution", "Subtitles", "Path "]); + .set_header(vec![ + "Name", + "Date Added", + "Premiere Date", + "Release Year", + "Genres", + "Parental Rating", + "Community Rating", + "Runtime (in minutes)", + "Resolution", + "Subtitles", + "Path ", + ]); for movie in movies.items { table.add_row(vec![ &movie.name, @@ -57,7 +68,7 @@ impl MovieDetails { &Self::ticks_to_minutes(&movie.run_time_ticks).to_string(), &Self::format_resolution(movie.width.to_string(), movie.height.to_string()), &movie.has_subtitles.to_string(), - &movie.path + &movie.path, ]); } println!("{table}"); @@ -66,7 +77,8 @@ impl MovieDetails { pub fn print_as_csv(movies: MovieDetails) -> String { let mut data: String = "Name,Date Added,Premiere Date,Release Year,Genres,Parental Rating,Community Rating,Runtime (in minutes),Resolution,Subtitles,Path\n".to_owned(); for movie in movies.items { - let piece = format!("{},{},{},{},{},{},{},{},{},{},{}\n", + let piece = format!( + "{},{},{},{},{},{},{},{},{},{},{}\n", movie.name, movie.date_created, movie.premiere_date, @@ -89,11 +101,15 @@ impl MovieDetails { } fn genres_to_string(movie: &Item) -> String { - let string = &movie.genres.iter().map(|x| x.to_string() + ";").collect::(); + let string = &movie + .genres + .iter() + .map(|x| x.to_string() + ";") + .collect::(); string.trim_end_matches(',').to_string() } fn format_resolution(width: String, height: String) -> String { format!("{} * {}", width, height) } -} \ No newline at end of file +} diff --git a/src/entities/package_details.rs b/src/entities/package_details.rs index fca2688..0b8020e 100644 --- a/src/entities/package_details.rs +++ b/src/entities/package_details.rs @@ -1,4 +1,4 @@ -use comfy_table::{ Table, ContentArrangement }; +use comfy_table::{ContentArrangement, Table}; pub type PackageDetailsRoot = Vec; #[derive(Default, Debug, Clone, PartialEq, Serialize, Deserialize)] @@ -38,17 +38,33 @@ impl PackageDetails { pub fn table_print(packages: Vec) { let mut table = Table::new(); table - .set_content_arrangement(ContentArrangement::Dynamic) - .set_width(120) - .set_header(vec!["Name", "Description", "Overview", "Owner", "GUID", "Category", "Versions"]); + .set_content_arrangement(ContentArrangement::Dynamic) + .set_width(120) + .set_header(vec![ + "Name", + "Description", + "Overview", + "Owner", + "GUID", + "Category", + "Versions", + ]); for package in packages { let mut version_output: String = "".to_string(); for version in package.versions { version_output.push_str(version.version.as_str()); version_output.push(' '); } - table.add_row(vec![package.name, package.description, package.overview, package.owner, package.guid, package.category, version_output]); + table.add_row(vec![ + package.name, + package.description, + package.overview, + package.owner, + package.guid, + package.category, + version_output, + ]); } println!("{table}"); } -} \ No newline at end of file +} diff --git a/src/entities/plugin_details.rs b/src/entities/plugin_details.rs index 9befc54..770eab6 100644 --- a/src/entities/plugin_details.rs +++ b/src/entities/plugin_details.rs @@ -1,4 +1,4 @@ -use comfy_table::{Table, ContentArrangement}; +use comfy_table::{ContentArrangement, Table}; pub type PluginRootJson = Vec; @@ -19,7 +19,7 @@ pub struct PluginDetails { #[serde(rename = "HasImage")] pub has_image: bool, #[serde(rename = "Status")] - pub status: String + pub status: String, } impl PluginDetails { @@ -32,10 +32,30 @@ impl PluginDetails { table .set_content_arrangement(ContentArrangement::Dynamic) .set_width(120) - .set_header(vec!["Plugin Name", "Version", "Config Filename", "Description", "Id", "Can Uninstall", "Image", "Status"]); + .set_header(vec![ + "Plugin Name", + "Version", + "Config Filename", + "Description", + "Id", + "Can Uninstall", + "Image", + "Status", + ]); for plugin in plugins { - table.add_row(vec![plugin.name, plugin.version, plugin.configuration_file_name.unwrap_or_else(|| {String::new()}), plugin.description, plugin.id, plugin.can_uninstall.to_string(), plugin.has_image.to_string(), plugin.status]); + table.add_row(vec![ + plugin.name, + plugin.version, + plugin + .configuration_file_name + .unwrap_or_else(|| String::new()), + plugin.description, + plugin.id, + plugin.can_uninstall.to_string(), + plugin.has_image.to_string(), + plugin.status, + ]); } println!("{table}"); } -} \ No newline at end of file +} diff --git a/src/entities/repository_details.rs b/src/entities/repository_details.rs index a823f13..f4ff406 100644 --- a/src/entities/repository_details.rs +++ b/src/entities/repository_details.rs @@ -1,4 +1,4 @@ -use comfy_table::{ Table, ContentArrangement }; +use comfy_table::{ContentArrangement, Table}; pub type RepositoryDetailsRoot = Vec; @@ -14,12 +14,8 @@ pub struct RepositoryDetails { } impl RepositoryDetails { - pub fn new (name: String, url: String, enabled: bool) -> RepositoryDetails { - RepositoryDetails { - name, - url, - enabled - } + pub fn new(name: String, url: String, enabled: bool) -> RepositoryDetails { + RepositoryDetails { name, url, enabled } } pub fn json_print(repos: &[RepositoryDetails]) { @@ -31,11 +27,19 @@ impl RepositoryDetails { table .set_content_arrangement(ContentArrangement::Dynamic) .set_width(120) - .set_header(vec!["Plugin Name", "Version", "Config Filename", "Description", "Id", "Can Uninstall", "Image", "Status"]); + .set_header(vec![ + "Plugin Name", + "Version", + "Config Filename", + "Description", + "Id", + "Can Uninstall", + "Image", + "Status", + ]); for repo in repos { table.add_row(vec![repo.name, repo.url, repo.enabled.to_string()]); - } println!("{table}"); } -} \ No newline at end of file +} diff --git a/src/entities/server_info.rs b/src/entities/server_info.rs index 6947517..a043ab7 100644 --- a/src/entities/server_info.rs +++ b/src/entities/server_info.rs @@ -1,13 +1,13 @@ pub struct ServerInfo { pub server_url: String, - pub api_key: String + pub api_key: String, } impl ServerInfo { pub fn new(endpoint: &str, server_url: &str, api_key: &str) -> ServerInfo { ServerInfo { - server_url: format!("{}{}",server_url, endpoint), - api_key: api_key.to_owned() + server_url: format!("{}{}", server_url, endpoint), + api_key: api_key.to_owned(), } } -} \ No newline at end of file +} diff --git a/src/entities/task_details.rs b/src/entities/task_details.rs index 1f09eff..80480a5 100644 --- a/src/entities/task_details.rs +++ b/src/entities/task_details.rs @@ -1,4 +1,4 @@ -use comfy_table::{ Table, ContentArrangement }; +use comfy_table::{ContentArrangement, Table}; #[derive(Serialize, Deserialize)] pub struct TaskDetails { @@ -10,16 +10,16 @@ pub struct TaskDetails { //pub percent_complete: Option, pub percent_complete: f32, #[serde(rename = "Id")] - pub id: String + pub id: String, } impl TaskDetails { pub fn new(name: String, state: String, percent_complete: f32, id: String) -> TaskDetails { - TaskDetails{ + TaskDetails { name, state, percent_complete, - id + id, } } @@ -43,4 +43,4 @@ impl TaskDetails { } println!("{table}"); } -} \ No newline at end of file +} diff --git a/src/entities/user_details.rs b/src/entities/user_details.rs index 5fa32a4..9a83c56 100644 --- a/src/entities/user_details.rs +++ b/src/entities/user_details.rs @@ -166,4 +166,4 @@ impl UserDetails { pub fn json_print_users(users: &[UserDetails]) { println!("{}", serde_json::to_string_pretty(&users).unwrap()); } -} \ No newline at end of file +} diff --git a/src/main.rs b/src/main.rs index c2bb8c8..1df6d21 100644 --- a/src/main.rs +++ b/src/main.rs @@ -1,1096 +1,1283 @@ -use std::fs::{File, self}; -use std::env; -use std::io::{self, BufRead, BufReader, Cursor, Read, Write}; -use std::fmt; -use clap::{Parser, Subcommand, ValueEnum}; -use image::ImageFormat; -use base64::{engine::general_purpose, Engine as _}; - -mod user_actions; -use user_actions::{UserWithPass, UserAuth, UserList}; -mod system_actions; -use system_actions::*; -mod plugin_actions; -use plugin_actions::PluginInfo; -mod responder; -mod entities; -use entities::user_details::{UserDetails, Policy}; -use entities::device_details::{DeviceDetails, DeviceRootJson}; -use entities::task_details::TaskDetails; -use entities::log_details::LogDetails; -use entities::library_details::{LibraryDetails, LibraryRootJson}; -use entities::plugin_details::{PluginDetails, PluginRootJson}; -use entities::activity_details::ActivityDetails; -use entities::movie_details::MovieDetails; -use entities::media_details::MediaRoot; -use entities::repository_details::{RepositoryDetails, RepositoryDetailsRoot}; -use entities::package_details::{PackageDetailsRoot, PackageDetails}; -use entities::server_info::ServerInfo; -mod utils; -use utils::output_writer::export_data; -use utils::status_handler::{handle_others, handle_unauthorized}; - -#[macro_use] -extern crate serde_derive; - -// -// Global variables for API endpoints -// -const USER_POLICY: &str = "/Users/{userId}/Policy"; -const USER_ID: &str = "/Users/{userId}"; -const USERS: &str = "/Users"; -const DEVICES: &str = "/Devices"; - -#[derive(Debug, Clone, Serialize, Deserialize)] -#[serde(default)] -pub struct AppConfig { - status: String, - comfy: bool, - server_url: String, - os: String, - api_key: String, - token: String -} - -impl Default for AppConfig { - fn default() -> Self { - AppConfig { - status: "not configured".to_owned(), - comfy: true, - server_url: "Unknown".to_owned(), - os: "Unknown".to_owned(), - api_key: "Unknown".to_owned(), - token: "Unknown".to_owned() - } - } -} - -#[derive(ValueEnum, Clone, Debug)] -enum OutputFormat { - Json, - Csv, - Table -} - -/// CLAP CONFIGURATION -/// CLI controller for Jellyfin -#[derive(Debug, Parser)] // requires `derive` feature -#[clap(name = "jellyroller", author, version)] -#[clap(about = "A CLI controller for managing Jellyfin", long_about = None)] -struct Cli { - #[clap(subcommand)] - command: Commands, -} - - #[derive(Debug, Subcommand)] - enum Commands { - /// Creates a new user - #[clap(arg_required_else_help = true)] - AddUser { - /// Username to create. - #[clap(required = true, value_parser)] - username: String, - /// Password for created user. - #[clap(required = true, value_parser)] - password: String, - }, - /// Deletes an existing user. - #[clap(arg_required_else_help = true)] - DeleteUser { - /// User to remove. - #[clap(required = true, value_parser)] - username: String, - }, - /// Lists the current users with basic information. - ListUsers { - /// Exports the user list information to a file - #[clap(short, long)] - export: bool, - /// Path for the file export - #[clap(short, long, default_value="")] - output: String, - /// Username to gather information about - #[clap(short, long, default_value="")] - username: String - }, - /// Resets a user's password. - #[clap(arg_required_else_help = true)] - ResetPassword { - /// User to be modified. - #[clap(required = true, value_parser)] - username: String, - /// What to reset the specified user's password to. - #[clap(required = true, value_parser)] - password: String, - }, - /// Displays the server information. - ServerInfo {}, - /// Displays the available system logs. - ListLogs{ - /// Print information as json. - #[clap(long, required = false)] - json: bool - }, - /// Displays the requested logfile. - ShowLog { - /// Name of the logfile to show. - #[clap(required = true, value_parser)] - logfile: String - }, - /// Reconfigure the connection information. - Reconfigure {}, - /// Show all devices. - GetDevices { - /// Only show devices active in the last hour - #[clap(long, required = false)] - active: bool, - /// Print information as json. - #[clap(long, required = false)] - json: bool - }, - /// Removes all devices associated with the specified user. - RemoveDeviceByUsername { - #[clap(required = true, value_parser)] - username: String - }, - /// Show all scheduled tasks and their status. - GetScheduledTasks { - /// Print information as json. - #[clap(long, required = false)] - json: bool - }, - /// Executes a scheduled task by name. - ExecuteTaskByName { - #[clap(required = true, value_parser)] - task: String - }, - /// Start a library scan. - ScanLibrary { - /// Library ID - #[clap(required = false, value_parser, default_value="all")] - library_id: String, - /// Type of scan - #[clap(required = false, default_value="all")] - scan_type: ScanType, - }, - /// Disable a user. - DisableUser { - #[clap(required = true, value_parser)] - username: String - }, - /// Enable a user. - EnableUser { - #[clap(required = true, value_parser)] - username: String - }, - /// Grants the specified user admin rights. - GrantAdmin { - #[clap(required = true, value_parser)] - username: String - }, - /// Revokes admin rights from the specified user. - RevokeAdmin { - #[clap(required = true, value_parser)] - username: String - }, - /// Restarts Jellyfin - RestartJellyfin {}, - /// Shuts down Jellyfin - ShutdownJellyfin {}, - /// Gets the libraries available to the configured user - GetLibraries { - /// Print information as json. - #[clap(long, required = false)] - json: bool - }, - /// Returns a list of installed plugins - GetPlugins { - /// Print information as json. - #[clap(long, required = false)] - json: bool - }, - /// Uses the supplied file to mass create new users. - AddUsers { - /// File that contains the user information in "username,password" lines. - #[clap(required = true, value_parser)] - inputfile: String - }, - /// Mass update users in the supplied file - UpdateUsers { - /// File that contains the user JSON information. - #[clap(required = true, value_parser)] - inputfile: String - }, - /// Creates a report of either activity or available movie items - CreateReport { - /// Type of report (activity or movie) - #[clap(required = true)] - report_type: ReportType, - /// Total number of records to return (defaults to 100) - #[clap(required = false, short, long, default_value="100")] - limit: String, - /// Output filename - #[clap(required = false, short, long, default_value="")] - filename: String - - }, - /// Executes a search of your media - SearchMedia { - /// Search term - #[clap(required = true, short, long)] - term: String, - /// Filter for media type - #[clap(required = false, short, long, default_value="all")] - mediatype: String, - #[clap(required = false, short, long, default_value="")] - parentid: String, - #[clap(short = 'o', long, value_enum, default_value = "table")] - output_format: OutputFormat, - /// By default, the server does not include file paths in the search results. Setting this - /// will tell the server to include the file path in the search results. - #[clap(short = 'f', long, required = false)] - include_filepath: bool, - /// Available columns: Name, Id, Type, Path, CriticRating, ProductionYear - #[clap(short = 'c', long, value_parser, num_args = 0.., value_delimiter = ',', default_value = "Name,ID,Type")] - table_columns: Vec - }, - /// Updates image of specified file by name - UpdateImageByName { - /// Attempt to update based on title. Requires unique search term. - #[clap(required = true, short, long)] - title: String, - /// Path to the image that will be used. - #[clap(required = true, short, long)] - path: String, - #[clap(required = true, short, long)] - imagetype: ImageType - }, - /// Updates image of specified file by id - UpdateImageById { - /// Attempt to update based on item id. - #[clap(required = true, short = 'i', long)] - id: String, - /// Path to the image that will be used. - #[clap(required = true, short, long)] - path: String, - #[clap(required = true, short = 'I', long)] - imagetype: ImageType - }, - /// Generate a report for an issue. - GenerateReport {}, - /// Updates metadata of specified id with metadata provided by specified file - UpdateMetadata { - /// ID of the file to update - #[clap(required = true, short = 'i', long)] - id: String, - /// File that contains the metadata to upload to the server - #[clap(required = true, short = 'f', long)] - filename: String - }, - /// Registers a new library. - RegisterLibrary { - /// Name of the new library - #[clap(required = true, short = 'n', long)] - name: String, - /// Collection Type of the new library - #[clap(required = true, short = 'c', long)] - collectiontype: CollectionType, - /// Path to file that contains the JSON for the library - #[clap(required = true, short = 'f', long)] - filename: String - }, - /// Registers a new Plugin Repository - RegisterRepository { - /// Name of the new repository - #[clap(required = true, short = 'n', long = "name")] - name: String, - /// URL of the new repository - #[clap(required = true, short = 'u', long = "url")] - path: String - }, - /// Lists all current repositories - GetRepositories { - /// Print information as json. - #[clap(long, required = false)] - json: bool - }, - /// Lists all available packages - GetPackages { - /// Print information as json. - #[clap(long, required = false)] - json: bool - }, - /// Installs the specified package - InstallPackage { - /// Package to install - #[clap(short = 'p', long = "package", required = true)] - package: String, - /// Version to install - #[clap(short = 'v', long = "version", required = false, default_value = "")] - version: String, - /// Repository to install from - #[clap(short = 'r', long = "repository", required = false, default_value = "")] - repository: String - } -} - -#[derive(ValueEnum, Clone, Debug, PartialEq)] -enum Detail { - User, - Server -} - -#[derive(ValueEnum, Clone, Debug, PartialEq)] -enum ScanType { - NewUpdated, - MissingMetadata, - ReplaceMetadata, - All -} - -#[derive(ValueEnum, Clone, Debug, PartialEq)] -enum ReportType { - Activity, - Movie -} - -#[derive(ValueEnum, Clone, Debug, PartialEq)] -enum ImageType { - Primary, - Art, - Backdrop, - Banner, - Logo, - Thumb, - Disc, - Box, - Screenshot, - Menu, - BoxRear, - Profile -} - -#[derive(ValueEnum, Clone, Debug, PartialEq)] -enum CollectionType { - Movies, - TVShows, - Music, - MusicVideos, - HomeVideos, - BoxSets, - Books, - Mixed -} - -fn main() -> Result<(), confy::ConfyError> { - let mut current = env::current_exe().unwrap(); - current.pop(); - current.push("jellyroller.config"); - - let cfg: AppConfig = if std::path::Path::new(current.as_path()).exists() { - confy::load_path(current.as_path())? - } else { - confy::load("jellyroller", "jellyroller")? - }; - - if cfg.status == "not configured" { - println!("Application is not configured!"); - initial_config(cfg); - std::process::exit(0); - } else if cfg.token == "Unknown" { - println!("[INFO] Username/Password detected. Reconfiguring to use API key."); - token_to_api(cfg.clone()); - } - let args = Cli::parse(); - match args.command { - - //TODO: Create a simple_post variation that allows for query params. - Commands::RegisterLibrary { name, collectiontype, filename} => { - let mut endpoint= String::from("/Library/VirtualFolders?CollectionType="); - endpoint.push_str(collectiontype.to_string().as_str()); - endpoint.push_str("&refreshLibrary=true"); - endpoint.push_str("&name="); - endpoint.push_str(name.as_str()); - let mut file = File::open(filename).expect("Unable to open file."); - let mut contents = String::new(); - file.read_to_string(&mut contents).expect("Unable to read file."); - register_library( - ServerInfo::new(endpoint.as_str(), &cfg.server_url, &cfg.api_key), - contents - ) - }, - - Commands::GenerateReport {} => { - let info = return_server_info(ServerInfo::new("/System/Info", &cfg.server_url, &cfg.api_key)); - let json: serde_json::Value = serde_json::from_str(info.as_str()).expect("failed"); - println!("\ - Please copy/paste the following information to any issue that is being opened:\n\ - JellyRoller Version: {}\n\ - JellyRoller OS: {}\n\ - Jellyfin Version: {}\n\ - Jellyfin Host OK: {}\n\ - Jellyfin Server Architecture: {}\ - ", - env!("CARGO_PKG_VERSION"), - env::consts::OS, - json.get("Version").expect("Unable to extract Jellyfin version."), - json.get("OperatingSystem").expect("Unable to extract Jellyfin OS information."), - json.get("SystemArchitecture").expect("Unable to extract Jellyfin System Architecture.") - ); - }, - - Commands::UpdateMetadata { id, filename } => { - // Read the JSON file and prepare it for upload. - let json: String = fs::read_to_string(filename).unwrap(); - update_metadata( - ServerInfo::new("/Items/{itemId}", &cfg.server_url, &cfg.api_key), - id, - json - ); - - - }, - Commands::UpdateImageByName { title, path, imagetype } => { - let search: MediaRoot = execute_search(&title, "all".to_string(), "".to_string(), false, &cfg); - if search.total_record_count > 1 { - eprintln!("Too many results found. Updating by name requires a unique search term."); - std::process::exit(1); - } - let img_base64 = image_to_base64(path); - for item in search.items { - update_image( - ServerInfo::new("/Items/{itemId}/Images/{imageType}", &cfg.server_url, &cfg.api_key), - item.id, - &imagetype, - &img_base64 - ); - } - }, - Commands::UpdateImageById { id, path, imagetype} => { - let img_base64 = image_to_base64(path); - update_image( - ServerInfo::new("/Items/{itemId}/Images/{imageType}", &cfg.server_url, &cfg.api_key), - id, - &imagetype, - &img_base64 - ); - }, - - // User based commands - Commands::AddUser { username, password } => { - add_user(&cfg, username, password); - }, - Commands::DeleteUser { username } => { - let user_id = get_user_id(&cfg, &username); - let server_path = format!("{}/Users/{}", cfg.server_url, user_id); - match UserWithPass::delete_user(UserWithPass::new(Some(username), None, None, server_path, cfg.api_key)) { - Err(_) => { - eprintln!("Unable to delete user."); - std::process::exit(1); - }, - Ok(i) => i - } - }, - Commands::ListUsers { export, mut output, username} => { - if username.is_empty() { - let users: Vec = - match UserList::list_users(UserList::new(USERS, &cfg.server_url, &cfg.api_key)) { - Err(_) => { - eprintln!("Unable to gather users."); - std::process::exit(1); - }, - Ok(i) => i - }; - if export { - println!("Exporting all user information....."); - if output.is_empty() { - "exported-user-info.json".clone_into(&mut output); - - } - let data: String = - match serde_json::to_string_pretty(&users) { - Err(_) => { - eprintln!("Unable to convert user information into JSON."); - std::process::exit(1); - }, - Ok(i) => i - }; - export_data(&data, output); - } else { - UserDetails::json_print_users(&users); - } - } else { - let user_id = UserList::get_user_id(UserList::new(USERS, &cfg.server_url, &cfg.api_key), &username); - let user = gather_user_information(&cfg, &username, &user_id); - if export { - println!("Exporting user information....."); - if output.is_empty() { - output = format!("exported-user-info-{}.json", username); - } - let data: String = - match serde_json::to_string_pretty(&user) { - Err(_) => { - eprintln!("Unable to convert user information into JSON."); - std::process::exit(1); - }, - Ok(i) => i - }; - export_data(&data, output); - } else { - UserDetails::json_print_user(&user); - } - } - }, - Commands::ResetPassword { username, password } => { - // Get usename - let user_id = UserList::get_user_id(UserList::new(USERS, &cfg.server_url, &cfg.api_key), &username); - // Setup the endpoint - let server_path = format!("{}/Users/{}/Password", &cfg.server_url, user_id); - match UserWithPass::resetpass(UserWithPass::new(None, Some(password), Some("".to_string()), server_path, cfg.api_key)) { - Err(_) => { - eprintln!("Unable to convert user information into JSON."); - std::process::exit(1); - }, - Ok(i) => i - } - }, - Commands::DisableUser { username } => { - let id = get_user_id(&cfg, &username); - let mut user_info = gather_user_information(&cfg, &username, &id); - user_info.policy.is_disabled = true; - UserList::update_user_config_bool( - UserList::new(USER_POLICY, &cfg.server_url, &cfg.api_key), - &user_info.policy, - &id, - &username) - .expect("Unable to update user."); - }, - Commands::EnableUser { username } => { - let id = get_user_id(&cfg, &username); - let mut user_info = gather_user_information(&cfg, &username, &id); - user_info.policy.is_disabled = false; - UserList::update_user_config_bool( - UserList::new(USER_POLICY, &cfg.server_url, &cfg.api_key), - &user_info.policy, - &id, - &username) - .expect("Unable to update user."); - }, - Commands::GrantAdmin { username } => { - let id = get_user_id(&cfg, &username); - let mut user_info = gather_user_information(&cfg, &username, &id); - user_info.policy.is_administrator = true; - UserList::update_user_config_bool( - UserList::new(USER_POLICY, &cfg.server_url, &cfg.api_key), - &user_info.policy, - &id, - &username) - .expect("Unable to update user."); - }, - Commands::RevokeAdmin { username } => { - let id = get_user_id(&cfg, &username); - let mut user_info = gather_user_information(&cfg, &username, &id); - user_info.policy.is_administrator = false; - UserList::update_user_config_bool( - UserList::new(USER_POLICY, &cfg.server_url, &cfg.api_key), - &user_info.policy, - &id, - &username) - .expect("Unable to update user."); - }, - Commands::AddUsers { inputfile } => { - let reader = BufReader::new(File::open(inputfile).unwrap()); - for line in reader.lines() { - match line { - Ok(l) => { - let vec: Vec<&str> = l.split(',').collect(); - add_user(&cfg, vec[0].to_owned(), vec[1].to_owned()); - - }, - Err(e) => println!("Unable to add user. {e}") - } - } - }, - Commands::UpdateUsers { inputfile } => { - let data: String = - match fs::read_to_string(inputfile) { - Err(_) => { - eprintln!("Unable to process input file."); - std::process::exit(1); - }, - Ok(i) => i - }; - if data.starts_with('[') { - let info: Vec = - match serde_json::from_str::>(&data) { - Err(_) => { - eprintln!("Unable to convert user details JSON.."); - std::process::exit(1); - }, - Ok(i) => i - }; - for item in info { - match UserList::update_user_info( - UserList::new(USER_ID, &cfg.server_url, &cfg.api_key), - &item.id, - &item - ) { - Ok(_) => {}, - Err(e) => eprintln!("Unable to update user. {e}") - }; - - } - } else { - let info: UserDetails = - match serde_json::from_str::(&data) { - Err(_) => { - eprintln!("Unable to convert user details JSON."); - std::process::exit(1); - }, - Ok(i) => i - }; - let user_id = get_user_id(&cfg, &info.name); - match UserList::update_user_info( - UserList::new(USER_ID, &cfg.server_url, &cfg.api_key), - &user_id, - &info - ) { - Ok(_) => {}, - Err(e) => { eprintln!("Unable to update user. {e}")} - } - } - } - - // Server based commands - Commands::GetPackages { json } => { - let packages = get_packages_info(ServerInfo::new("/Packages", &cfg.server_url, &cfg.api_key)).unwrap(); - if json { - PackageDetails::json_print(&packages); - } else { - PackageDetails::table_print(packages); - } - }, - - Commands::GetRepositories { json } => { - let repos = get_repo_info(ServerInfo::new("/Repositories", &cfg.server_url, &cfg.api_key)).unwrap(); - if json { - RepositoryDetails::json_print(&repos); - } else { - RepositoryDetails::table_print(repos); - } - }, - - Commands::RegisterRepository { name, path} => { - let mut repos = get_repo_info(ServerInfo::new("/Repositories", &cfg.server_url, &cfg.api_key)).unwrap(); - repos.push(RepositoryDetails::new(name, path, true)); - set_repo_info(ServerInfo::new("/Repositories", &cfg.server_url, &cfg.api_key), repos); - }, - - Commands::InstallPackage { package, version, repository } => { - // Check if package name has spaces and replace them as needed - let encoded = package.replace(" ","%20"); - install_package(ServerInfo::new("/Packages/Installed/{package}", &cfg.server_url, &cfg.api_key), &encoded, &version, &repository); - }, - - Commands::ServerInfo {} => { - get_server_info(ServerInfo::new("/System/Info", &cfg.server_url, &cfg.api_key)) - .expect("Unable to gather server information."); - }, - Commands::ListLogs { json } => { - let logs = - match get_log_filenames(ServerInfo::new("/System/Logs", &cfg.server_url, &cfg.api_key)) { - Err(_) => { - eprintln!("Unable to get get log filenames."); - std::process::exit(1); - }, - Ok(i) => i - }; - if json { - LogDetails::json_print(&logs); - } else { - LogDetails::table_print(logs); - } - }, - Commands::ShowLog { logfile } => { - LogFile::get_logfile( - LogFile::new( - ServerInfo::new("/System/Logs/Log", &cfg.server_url, &cfg.api_key), - logfile - )) - .expect("Unable to retrieve the specified logfile."); - }, - Commands::Reconfigure {} => { - initial_config(cfg); - }, - Commands::GetDevices { active, json } => { - let devices: Vec = - match get_devices(ServerInfo::new(DEVICES, &cfg.server_url, &cfg.api_key), active) { - Err(e) => { - eprintln!("Unable to get devices, {e}"); - std::process::exit(1); - }, - Ok(i) => i - }; - if json { - DeviceDetails::json_print(&devices); - } else { - DeviceDetails::table_print(&devices); - } - }, - Commands::GetLibraries { json } => { - let libraries: Vec = - match get_libraries(ServerInfo::new("/Library/VirtualFolders", &cfg.server_url, &cfg.api_key)) { - Err(_) => { - eprintln!("Unable to get libraries."); - std::process::exit(1); - }, - Ok(i) => i - }; - if json { - LibraryDetails::json_print(&libraries); - } else { - LibraryDetails::table_print(libraries); - } - }, - Commands::GetScheduledTasks { json } => { - let tasks: Vec = - match get_scheduled_tasks(ServerInfo::new("/ScheduledTasks", &cfg.server_url, &cfg.api_key)) { - Err(e) => { - eprintln!("Unable to get scheduled tasks, {e}"); - std::process::exit(1); - }, - Ok(i) => i - }; - - if json { - TaskDetails::json_print(&tasks); - } else { - TaskDetails::table_print(tasks); - } - }, - Commands::ExecuteTaskByName { task } => { - let taskid: String = - match get_taskid_by_taskname(ServerInfo::new("/ScheduledTasks", &cfg.server_url, &cfg.api_key), &task) { - Err(e) => { - eprintln!("Unable to get task id by taskname, {e}"); - std::process::exit(1); - }, - Ok(i) => i - }; - execute_task_by_id(ServerInfo::new("/ScheduledTasks/Running/{taskId}", &cfg.server_url, &cfg.api_key), &task, &taskid); - } - Commands::ScanLibrary {library_id, scan_type} => { - if library_id == "all" { - scan_library_all(ServerInfo::new("/Library/Refresh", &cfg.server_url, &cfg.api_key)); - } else { - let query_info = match scan_type { - ScanType::NewUpdated => { - vec![ - ("Recursive", "true"), - ("ImageRefreshMode", "Default"), - ("MetadataRefreshMode", "Default"), - ("ReplaceAllImages", "false"), - ("RegenerateTrickplay", "false"), - ("ReplaceAllMetadata", "false") - ] - }, - ScanType::MissingMetadata => { - vec![ - ("Recursive", "true"), - ("ImageRefreshMode", "FullRefresh"), - ("MetadataRefreshMode", "FullRefresh"), - ("ReplaceAllImages", "false"), - ("RegenerateTrickplay", "false"), - ("ReplaceAllMetadata", "false") - ] - }, - ScanType::ReplaceMetadata => { - vec![ - ("Recursive", "true"), - ("ImageRefreshMode", "FullRefresh"), - ("MetadataRefreshMode", "FullRefresh"), - ("ReplaceAllImages", "false"), - ("RegenerateTrickplay", "false"), - ("ReplaceAllMetadata", "true") - ] - }, - _ => std::process::exit(1) - }; - scan_library(ServerInfo::new("/Items/{library_id}/Refresh", &cfg.server_url, &cfg.api_key), query_info, library_id); - } - }, - Commands::RemoveDeviceByUsername { username } => { - let filtered: Vec = - match get_deviceid_by_username(ServerInfo::new(DEVICES, &cfg.server_url, &cfg.api_key), &username) { - Err(_) => { - eprintln!("Unable to get device id by username."); - std::process::exit(1); - }, - Ok(i) => i - }; - for item in filtered { - remove_device(ServerInfo::new(DEVICES, &cfg.server_url, &cfg.api_key), &item) - .expect("Unable to delete specified id."); - } - }, - Commands::RestartJellyfin {} => { - restart_or_shutdown(ServerInfo::new("/System/Restart", &cfg.server_url, &cfg.api_key)); - }, - Commands::ShutdownJellyfin {} => { - restart_or_shutdown(ServerInfo::new("/System/Shutdown", &cfg.server_url, &cfg.api_key)); - }, - Commands::GetPlugins { json } => { - let plugins: Vec = - match PluginInfo::get_plugins(PluginInfo::new("/Plugins", &cfg.server_url, cfg.api_key)) { - Err(_) => { - eprintln!("Unable to get plugin information."); - std::process::exit(1); - }, - Ok(i) => i - }; - if json { - PluginDetails::json_print(&plugins); - } else { - PluginDetails::table_print(plugins); - } - }, - Commands::CreateReport { report_type, limit, filename} => { - match report_type { - ReportType::Activity => { - println!("Gathering Activity information....."); - let activities: ActivityDetails = - match get_activity(ServerInfo::new("/System/ActivityLog/Entries", &cfg.server_url, &cfg.api_key), &limit) { - Err(e) => { - eprintln!("Unable to gather activity log entries, {e}"); - std::process::exit(1); - }, - Ok(i) => i - }; - if !filename.is_empty() { - println!("Exporting Activity information to {}.....", &filename); - let csv = ActivityDetails::print_as_csv(activities); - export_data(&csv, filename); - println!("Export complete."); - } else { - ActivityDetails::table_print(activities); - } - }, - ReportType::Movie => { - let user_id: String = - match UserList::get_current_user_information(UserList::new("/Users/Me", &cfg.server_url, &cfg.api_key)) { - Err(e) => { - eprintln!("Unable to gather information about current user, {e}"); - std::process::exit(1); - }, - Ok(i) => i.id - }; - let movies: MovieDetails = - match export_library(ServerInfo::new("/Users/{userId}/Items", &cfg.server_url, &cfg.api_key), &user_id) { - Err(e) => { - eprintln!("Unable to export library, {e}"); - std::process::exit(1); - }, - Ok(i) => i - }; - if !filename.is_empty() { - println!("Exporting Movie information to {}.....", &filename); - let csv = MovieDetails::print_as_csv(movies); - export_data(&csv, filename); - println!("Export complete."); - } else { - MovieDetails::table_print(movies); - } - } - } - }, - Commands::SearchMedia { term, mediatype, parentid, include_filepath, output_format, table_columns } => { - let search_result = execute_search(&term, mediatype, parentid, include_filepath, &cfg); - - let mut used_table_columns = table_columns.clone(); - - if include_filepath { - used_table_columns.push("Path".to_string()); - } - - match output_format { - OutputFormat::Json => { - MediaRoot::json_print(search_result); - }, - OutputFormat::Csv => { - MediaRoot::csv_print(search_result, &used_table_columns); - }, - _ => { - MediaRoot::table_print(search_result, &used_table_columns); - } - } - } - } - - Ok(()) -} - -/// -/// Executes a search with the passed parameters. -/// -fn execute_search(term: &str, mediatype: String, parentid: String, include_filepath: bool, cfg: &AppConfig) -> MediaRoot { - let mut query = - vec![ - ("SortBy", "SortName,ProductionYear"), - ("Recursive", "true"), - ("searchTerm", term) - ]; - if mediatype != "all" { - query.push(("IncludeItemTypes", &mediatype)); - } - - if include_filepath { - query.push(("fields", "Path")); - } - - if !parentid.is_empty() { - query.push(("parentId", &parentid)); - } - - match get_search_results(ServerInfo::new("/Items", &cfg.server_url, &cfg.api_key), query) { - Err(e) => { - eprintln!("Unable to execute search, {e}"); - std::process::exit(1); - }, - Ok(i) => i - } -} - -/// -/// Retrieve the id for the specified user. Most API calls require the id of the user rather than the username. -/// -fn get_user_id(cfg: &AppConfig, username: &String) -> String { - UserList::get_user_id(UserList::new("/Users", &cfg.server_url, &cfg.api_key), username) -} - -/// -/// Gathers user information. -/// -fn gather_user_information(cfg: &AppConfig, username: &String, id: &str) -> UserDetails { - match UserList::get_user_information(UserList::new(USER_ID, &cfg.server_url, &cfg.api_key), id) { - Err(_) => { - println!("Unable to get user id for {}", username); - std::process::exit(1); - }, - Ok(ul) => ul, - } -} - -/// -/// Helper function to standardize the call for adding a user with a password. -/// -fn add_user(cfg: &AppConfig, username: String, password: String) { - let server_path = format!("{}/Users/New", cfg.server_url); - match UserWithPass::create_user(UserWithPass::new(Some(username), Some(password), None, server_path, cfg.api_key.clone())) { - Err(_) => { - println!("Unable to create user"); - std::process::exit(1); - }, - Ok(i) => i - } -} - -/// -/// Executed on initial run or when user wants to redo configuration. Will attempt to auto-configure -/// the application prior to allowing customization by -/// the user. -/// -fn initial_config(mut cfg: AppConfig) { - println!("[INFO] Attempting to determine Jellyfin information....."); - env::consts::OS.clone_into(&mut cfg.os); - println!("[INFO] OS detected as {}.", cfg.os); - - print!("[INPUT] Please enter your Jellyfin URL: "); - io::stdout().flush().expect("Unable to get Jellyfin URL."); - let mut server_url_input = String::new(); - io::stdin().read_line(&mut server_url_input) - .expect("Could not read server url information"); - server_url_input.trim().clone_into(&mut cfg.server_url); - - print!("[INPUT] Please enter your Jellyfin username: "); - io::stdout().flush().expect("Unable to get username."); - let mut username = String::new(); - io::stdin().read_line(&mut username) - .expect("[ERROR] Could not read Jellyfin username"); - let password = rpassword::prompt_password("Please enter your Jellyfin password: ").unwrap(); - println!("[INFO] Attempting to authenticate user."); - cfg.api_key = UserAuth::auth_user(UserAuth::new(&cfg.server_url, username.trim(), password)) - .expect("Unable to generate user auth token. Please assure your configuration information was input correctly\n"); - - "configured".clone_into(&mut cfg.status); - token_to_api(cfg); -} - -/// -/// Due to an issue with api key processing in Jellyfin, JellyRoller was initially relied on using auto tokens to communicate. -/// Now that the issue has been fixed, the auto tokens need to be converted to an API key. The single purpose of this function -/// is to handle the conversion with no input required from the user. -/// -fn token_to_api(mut cfg: AppConfig) { - println!("[INFO] Attempting to auto convert user auth token to API key....."); - // Check if api key already exists - if UserWithPass::retrieve_api_token(UserWithPass::new(None, None, None, format!("{}/Auth/Keys", cfg.server_url), cfg.api_key.clone())).unwrap().is_empty() { - UserWithPass::create_api_token(UserWithPass::new(None, None, None, format!("{}/Auth/Keys", cfg.server_url), cfg.api_key.clone())); - } - cfg.api_key = UserWithPass::retrieve_api_token(UserWithPass::new(None, None, None, format!("{}/Auth/Keys", cfg.server_url), cfg.api_key)).unwrap(); - cfg.token = "apiKey".to_string(); - confy::store("jellyroller", "jellyroller", cfg) - .expect("[ERROR] Unable to store updated configuration."); - println!("[INFO] Auth token successfully converted to API key."); - -} - -/// -/// Function that converts an image into a base64 png image. -/// -fn image_to_base64(path: String) -> String { - let base_img = image::open(path).unwrap(); - let mut image_data: Vec = Vec::new(); - base_img.write_to(&mut Cursor::new(&mut image_data), ImageFormat::Png).unwrap(); - general_purpose::STANDARD.encode(image_data) -} - -/// -/// Custom implementation to convert the ImageType enum into Strings -/// for easy comparison. -/// -impl fmt::Display for ImageType { - fn fmt (&self, f: &mut fmt::Formatter) -> fmt::Result { - match self { - ImageType::Primary => write!(f, "Primary"), - ImageType::Art => write!(f, "Art"), - ImageType::Backdrop => write!(f, "Backdrop"), - ImageType::Banner => write!(f, "Banner"), - ImageType::Logo => write!(f, "Logo"), - ImageType::Thumb => write!(f, "Thumb"), - ImageType::Disc => write!(f, "Disc"), - ImageType::Box => write!(f, "Box"), - ImageType::Screenshot => write!(f, "Screenshot"), - ImageType::Menu => write!(f, "Menu"), - ImageType::BoxRear => write!(f, "BoxRear"), - ImageType::Profile => write!(f, "Profile"), - } - } -} - -/// -/// Custom implementation to convert collectiontype enum into Strings -/// -impl fmt::Display for CollectionType { - fn fmt (&self, f: &mut fmt::Formatter) -> fmt::Result { - match self { - CollectionType::Movies => write!(f, "movies"), - CollectionType::TVShows => write!(f, "tvshows"), - CollectionType::Music => write!(f, "music"), - CollectionType::MusicVideos => write!(f, "musicvideos"), - CollectionType::HomeVideos => write!(f, "homevideos"), - CollectionType::BoxSets => write!(f, "boxsets"), - CollectionType::Books => write!(f, "books"), - CollectionType::Mixed => write!(f, "mixed") - } - } -} +use base64::{engine::general_purpose, Engine as _}; +use clap::{Parser, Subcommand, ValueEnum}; +use image::ImageFormat; +use std::env; +use std::fmt; +use std::fs::{self, File}; +use std::io::{self, BufRead, BufReader, Cursor, Read, Write}; + +mod user_actions; +use user_actions::{UserAuth, UserList, UserWithPass}; +mod system_actions; +use system_actions::*; +mod plugin_actions; +use plugin_actions::PluginInfo; +mod entities; +mod responder; +use entities::activity_details::ActivityDetails; +use entities::device_details::{DeviceDetails, DeviceRootJson}; +use entities::library_details::{LibraryDetails, LibraryRootJson}; +use entities::log_details::LogDetails; +use entities::media_details::MediaRoot; +use entities::movie_details::MovieDetails; +use entities::package_details::{PackageDetails, PackageDetailsRoot}; +use entities::plugin_details::{PluginDetails, PluginRootJson}; +use entities::repository_details::{RepositoryDetails, RepositoryDetailsRoot}; +use entities::server_info::ServerInfo; +use entities::task_details::TaskDetails; +use entities::user_details::{Policy, UserDetails}; +mod utils; +use utils::output_writer::export_data; +use utils::status_handler::{handle_others, handle_unauthorized}; + +#[macro_use] +extern crate serde_derive; + +// +// Global variables for API endpoints +// +const USER_POLICY: &str = "/Users/{userId}/Policy"; +const USER_ID: &str = "/Users/{userId}"; +const USERS: &str = "/Users"; +const DEVICES: &str = "/Devices"; + +#[derive(Debug, Clone, Serialize, Deserialize)] +#[serde(default)] +pub struct AppConfig { + status: String, + comfy: bool, + server_url: String, + os: String, + api_key: String, + token: String, +} + +impl Default for AppConfig { + fn default() -> Self { + AppConfig { + status: "not configured".to_owned(), + comfy: true, + server_url: "Unknown".to_owned(), + os: "Unknown".to_owned(), + api_key: "Unknown".to_owned(), + token: "Unknown".to_owned(), + } + } +} + +#[derive(ValueEnum, Clone, Debug)] +enum OutputFormat { + Json, + Csv, + Table, +} + +/// CLAP CONFIGURATION +/// CLI controller for Jellyfin +#[derive(Debug, Parser)] // requires `derive` feature +#[clap(name = "jellyroller", author, version)] +#[clap(about = "A CLI controller for managing Jellyfin", long_about = None)] +struct Cli { + #[clap(subcommand)] + command: Commands, +} + +#[derive(Debug, Subcommand)] +enum Commands { + /// Creates a new user + #[clap(arg_required_else_help = true)] + AddUser { + /// Username to create. + #[clap(required = true, value_parser)] + username: String, + /// Password for created user. + #[clap(required = true, value_parser)] + password: String, + }, + /// Deletes an existing user. + #[clap(arg_required_else_help = true)] + DeleteUser { + /// User to remove. + #[clap(required = true, value_parser)] + username: String, + }, + /// Lists the current users with basic information. + ListUsers { + /// Exports the user list information to a file + #[clap(short, long)] + export: bool, + /// Path for the file export + #[clap(short, long, default_value = "")] + output: String, + /// Username to gather information about + #[clap(short, long, default_value = "")] + username: String, + }, + /// Resets a user's password. + #[clap(arg_required_else_help = true)] + ResetPassword { + /// User to be modified. + #[clap(required = true, value_parser)] + username: String, + /// What to reset the specified user's password to. + #[clap(required = true, value_parser)] + password: String, + }, + /// Displays the server information. + ServerInfo {}, + /// Displays the available system logs. + ListLogs { + /// Print information as json. + #[clap(long, required = false)] + json: bool, + }, + /// Displays the requested logfile. + ShowLog { + /// Name of the logfile to show. + #[clap(required = true, value_parser)] + logfile: String, + }, + /// Reconfigure the connection information. + Reconfigure {}, + /// Show all devices. + GetDevices { + /// Only show devices active in the last hour + #[clap(long, required = false)] + active: bool, + /// Print information as json. + #[clap(long, required = false)] + json: bool, + }, + /// Removes all devices associated with the specified user. + RemoveDeviceByUsername { + #[clap(required = true, value_parser)] + username: String, + }, + /// Show all scheduled tasks and their status. + GetScheduledTasks { + /// Print information as json. + #[clap(long, required = false)] + json: bool, + }, + /// Executes a scheduled task by name. + ExecuteTaskByName { + #[clap(required = true, value_parser)] + task: String, + }, + /// Start a library scan. + ScanLibrary { + /// Library ID + #[clap(required = false, value_parser, default_value = "all")] + library_id: String, + /// Type of scan + #[clap(required = false, default_value = "all")] + scan_type: ScanType, + }, + /// Disable a user. + DisableUser { + #[clap(required = true, value_parser)] + username: String, + }, + /// Enable a user. + EnableUser { + #[clap(required = true, value_parser)] + username: String, + }, + /// Grants the specified user admin rights. + GrantAdmin { + #[clap(required = true, value_parser)] + username: String, + }, + /// Revokes admin rights from the specified user. + RevokeAdmin { + #[clap(required = true, value_parser)] + username: String, + }, + /// Restarts Jellyfin + RestartJellyfin {}, + /// Shuts down Jellyfin + ShutdownJellyfin {}, + /// Gets the libraries available to the configured user + GetLibraries { + /// Print information as json. + #[clap(long, required = false)] + json: bool, + }, + /// Returns a list of installed plugins + GetPlugins { + /// Print information as json. + #[clap(long, required = false)] + json: bool, + }, + /// Uses the supplied file to mass create new users. + AddUsers { + /// File that contains the user information in "username,password" lines. + #[clap(required = true, value_parser)] + inputfile: String, + }, + /// Mass update users in the supplied file + UpdateUsers { + /// File that contains the user JSON information. + #[clap(required = true, value_parser)] + inputfile: String, + }, + /// Creates a report of either activity or available movie items + CreateReport { + /// Type of report (activity or movie) + #[clap(required = true)] + report_type: ReportType, + /// Total number of records to return (defaults to 100) + #[clap(required = false, short, long, default_value = "100")] + limit: String, + /// Output filename + #[clap(required = false, short, long, default_value = "")] + filename: String, + }, + /// Executes a search of your media + SearchMedia { + /// Search term + #[clap(required = true, short, long)] + term: String, + /// Filter for media type + #[clap(required = false, short, long, default_value = "all")] + mediatype: String, + #[clap(required = false, short, long, default_value = "")] + parentid: String, + #[clap(short = 'o', long, value_enum, default_value = "table")] + output_format: OutputFormat, + /// By default, the server does not include file paths in the search results. Setting this + /// will tell the server to include the file path in the search results. + #[clap(short = 'f', long, required = false)] + include_filepath: bool, + /// Available columns: Name, Id, Type, Path, CriticRating, ProductionYear + #[clap(short = 'c', long, value_parser, num_args = 0.., value_delimiter = ',', default_value = "Name,ID,Type")] + table_columns: Vec, + }, + /// Updates image of specified file by name + UpdateImageByName { + /// Attempt to update based on title. Requires unique search term. + #[clap(required = true, short, long)] + title: String, + /// Path to the image that will be used. + #[clap(required = true, short, long)] + path: String, + #[clap(required = true, short, long)] + imagetype: ImageType, + }, + /// Updates image of specified file by id + UpdateImageById { + /// Attempt to update based on item id. + #[clap(required = true, short = 'i', long)] + id: String, + /// Path to the image that will be used. + #[clap(required = true, short, long)] + path: String, + #[clap(required = true, short = 'I', long)] + imagetype: ImageType, + }, + /// Generate a report for an issue. + GenerateReport {}, + /// Updates metadata of specified id with metadata provided by specified file + UpdateMetadata { + /// ID of the file to update + #[clap(required = true, short = 'i', long)] + id: String, + /// File that contains the metadata to upload to the server + #[clap(required = true, short = 'f', long)] + filename: String, + }, + /// Registers a new library. + RegisterLibrary { + /// Name of the new library + #[clap(required = true, short = 'n', long)] + name: String, + /// Collection Type of the new library + #[clap(required = true, short = 'c', long)] + collectiontype: CollectionType, + /// Path to file that contains the JSON for the library + #[clap(required = true, short = 'f', long)] + filename: String, + }, + /// Registers a new Plugin Repository + RegisterRepository { + /// Name of the new repository + #[clap(required = true, short = 'n', long = "name")] + name: String, + /// URL of the new repository + #[clap(required = true, short = 'u', long = "url")] + path: String, + }, + /// Lists all current repositories + GetRepositories { + /// Print information as json. + #[clap(long, required = false)] + json: bool, + }, + /// Lists all available packages + GetPackages { + /// Print information as json. + #[clap(long, required = false)] + json: bool, + }, + /// Installs the specified package + InstallPackage { + /// Package to install + #[clap(short = 'p', long = "package", required = true)] + package: String, + /// Version to install + #[clap(short = 'v', long = "version", required = false, default_value = "")] + version: String, + /// Repository to install from + #[clap(short = 'r', long = "repository", required = false, default_value = "")] + repository: String, + }, +} + +#[derive(ValueEnum, Clone, Debug, PartialEq)] +enum Detail { + User, + Server, +} + +#[derive(ValueEnum, Clone, Debug, PartialEq)] +enum ScanType { + NewUpdated, + MissingMetadata, + ReplaceMetadata, + All, +} + +#[derive(ValueEnum, Clone, Debug, PartialEq)] +enum ReportType { + Activity, + Movie, +} + +#[derive(ValueEnum, Clone, Debug, PartialEq)] +enum ImageType { + Primary, + Art, + Backdrop, + Banner, + Logo, + Thumb, + Disc, + Box, + Screenshot, + Menu, + BoxRear, + Profile, +} + +#[derive(ValueEnum, Clone, Debug, PartialEq)] +enum CollectionType { + Movies, + TVShows, + Music, + MusicVideos, + HomeVideos, + BoxSets, + Books, + Mixed, +} + +fn main() -> Result<(), confy::ConfyError> { + let mut current = env::current_exe().unwrap(); + current.pop(); + current.push("jellyroller.config"); + + let cfg: AppConfig = if std::path::Path::new(current.as_path()).exists() { + confy::load_path(current.as_path())? + } else { + confy::load("jellyroller", "jellyroller")? + }; + + if cfg.status == "not configured" { + println!("Application is not configured!"); + initial_config(cfg); + std::process::exit(0); + } else if cfg.token == "Unknown" { + println!("[INFO] Username/Password detected. Reconfiguring to use API key."); + token_to_api(cfg.clone()); + } + let args = Cli::parse(); + match args.command { + //TODO: Create a simple_post variation that allows for query params. + Commands::RegisterLibrary { + name, + collectiontype, + filename, + } => { + let mut endpoint = String::from("/Library/VirtualFolders?CollectionType="); + endpoint.push_str(collectiontype.to_string().as_str()); + endpoint.push_str("&refreshLibrary=true"); + endpoint.push_str("&name="); + endpoint.push_str(name.as_str()); + let mut file = File::open(filename).expect("Unable to open file."); + let mut contents = String::new(); + file.read_to_string(&mut contents) + .expect("Unable to read file."); + register_library( + ServerInfo::new(endpoint.as_str(), &cfg.server_url, &cfg.api_key), + contents, + ) + } + + Commands::GenerateReport {} => { + let info = return_server_info(ServerInfo::new( + "/System/Info", + &cfg.server_url, + &cfg.api_key, + )); + let json: serde_json::Value = serde_json::from_str(info.as_str()).expect("failed"); + println!( + "\ + Please copy/paste the following information to any issue that is being opened:\n\ + JellyRoller Version: {}\n\ + JellyRoller OS: {}\n\ + Jellyfin Version: {}\n\ + Jellyfin Host OK: {}\n\ + Jellyfin Server Architecture: {}\ + ", + env!("CARGO_PKG_VERSION"), + env::consts::OS, + json.get("Version") + .expect("Unable to extract Jellyfin version."), + json.get("OperatingSystem") + .expect("Unable to extract Jellyfin OS information."), + json.get("SystemArchitecture") + .expect("Unable to extract Jellyfin System Architecture.") + ); + } + + Commands::UpdateMetadata { id, filename } => { + // Read the JSON file and prepare it for upload. + let json: String = fs::read_to_string(filename).unwrap(); + update_metadata( + ServerInfo::new("/Items/{itemId}", &cfg.server_url, &cfg.api_key), + id, + json, + ); + } + Commands::UpdateImageByName { + title, + path, + imagetype, + } => { + let search: MediaRoot = + execute_search(&title, "all".to_string(), "".to_string(), false, &cfg); + if search.total_record_count > 1 { + eprintln!( + "Too many results found. Updating by name requires a unique search term." + ); + std::process::exit(1); + } + let img_base64 = image_to_base64(path); + for item in search.items { + update_image( + ServerInfo::new( + "/Items/{itemId}/Images/{imageType}", + &cfg.server_url, + &cfg.api_key, + ), + item.id, + &imagetype, + &img_base64, + ); + } + } + Commands::UpdateImageById { + id, + path, + imagetype, + } => { + let img_base64 = image_to_base64(path); + update_image( + ServerInfo::new( + "/Items/{itemId}/Images/{imageType}", + &cfg.server_url, + &cfg.api_key, + ), + id, + &imagetype, + &img_base64, + ); + } + + // User based commands + Commands::AddUser { username, password } => { + add_user(&cfg, username, password); + } + Commands::DeleteUser { username } => { + let user_id = get_user_id(&cfg, &username); + let server_path = format!("{}/Users/{}", cfg.server_url, user_id); + match UserWithPass::delete_user(UserWithPass::new( + Some(username), + None, + None, + server_path, + cfg.api_key, + )) { + Err(_) => { + eprintln!("Unable to delete user."); + std::process::exit(1); + } + Ok(i) => i, + } + } + Commands::ListUsers { + export, + mut output, + username, + } => { + if username.is_empty() { + let users: Vec = + match UserList::list_users(UserList::new(USERS, &cfg.server_url, &cfg.api_key)) + { + Err(_) => { + eprintln!("Unable to gather users."); + std::process::exit(1); + } + Ok(i) => i, + }; + if export { + println!("Exporting all user information....."); + if output.is_empty() { + "exported-user-info.json".clone_into(&mut output); + } + let data: String = match serde_json::to_string_pretty(&users) { + Err(_) => { + eprintln!("Unable to convert user information into JSON."); + std::process::exit(1); + } + Ok(i) => i, + }; + export_data(&data, output); + } else { + UserDetails::json_print_users(&users); + } + } else { + let user_id = UserList::get_user_id( + UserList::new(USERS, &cfg.server_url, &cfg.api_key), + &username, + ); + let user = gather_user_information(&cfg, &username, &user_id); + if export { + println!("Exporting user information....."); + if output.is_empty() { + output = format!("exported-user-info-{}.json", username); + } + let data: String = match serde_json::to_string_pretty(&user) { + Err(_) => { + eprintln!("Unable to convert user information into JSON."); + std::process::exit(1); + } + Ok(i) => i, + }; + export_data(&data, output); + } else { + UserDetails::json_print_user(&user); + } + } + } + Commands::ResetPassword { username, password } => { + // Get usename + let user_id = UserList::get_user_id( + UserList::new(USERS, &cfg.server_url, &cfg.api_key), + &username, + ); + // Setup the endpoint + let server_path = format!("{}/Users/{}/Password", &cfg.server_url, user_id); + match UserWithPass::resetpass(UserWithPass::new( + None, + Some(password), + Some("".to_string()), + server_path, + cfg.api_key, + )) { + Err(_) => { + eprintln!("Unable to convert user information into JSON."); + std::process::exit(1); + } + Ok(i) => i, + } + } + Commands::DisableUser { username } => { + let id = get_user_id(&cfg, &username); + let mut user_info = gather_user_information(&cfg, &username, &id); + user_info.policy.is_disabled = true; + UserList::update_user_config_bool( + UserList::new(USER_POLICY, &cfg.server_url, &cfg.api_key), + &user_info.policy, + &id, + &username, + ) + .expect("Unable to update user."); + } + Commands::EnableUser { username } => { + let id = get_user_id(&cfg, &username); + let mut user_info = gather_user_information(&cfg, &username, &id); + user_info.policy.is_disabled = false; + UserList::update_user_config_bool( + UserList::new(USER_POLICY, &cfg.server_url, &cfg.api_key), + &user_info.policy, + &id, + &username, + ) + .expect("Unable to update user."); + } + Commands::GrantAdmin { username } => { + let id = get_user_id(&cfg, &username); + let mut user_info = gather_user_information(&cfg, &username, &id); + user_info.policy.is_administrator = true; + UserList::update_user_config_bool( + UserList::new(USER_POLICY, &cfg.server_url, &cfg.api_key), + &user_info.policy, + &id, + &username, + ) + .expect("Unable to update user."); + } + Commands::RevokeAdmin { username } => { + let id = get_user_id(&cfg, &username); + let mut user_info = gather_user_information(&cfg, &username, &id); + user_info.policy.is_administrator = false; + UserList::update_user_config_bool( + UserList::new(USER_POLICY, &cfg.server_url, &cfg.api_key), + &user_info.policy, + &id, + &username, + ) + .expect("Unable to update user."); + } + Commands::AddUsers { inputfile } => { + let reader = BufReader::new(File::open(inputfile).unwrap()); + for line in reader.lines() { + match line { + Ok(l) => { + let vec: Vec<&str> = l.split(',').collect(); + add_user(&cfg, vec[0].to_owned(), vec[1].to_owned()); + } + Err(e) => println!("Unable to add user. {e}"), + } + } + } + Commands::UpdateUsers { inputfile } => { + let data: String = match fs::read_to_string(inputfile) { + Err(_) => { + eprintln!("Unable to process input file."); + std::process::exit(1); + } + Ok(i) => i, + }; + if data.starts_with('[') { + let info: Vec = match serde_json::from_str::>(&data) { + Err(_) => { + eprintln!("Unable to convert user details JSON.."); + std::process::exit(1); + } + Ok(i) => i, + }; + for item in info { + match UserList::update_user_info( + UserList::new(USER_ID, &cfg.server_url, &cfg.api_key), + &item.id, + &item, + ) { + Ok(_) => {} + Err(e) => eprintln!("Unable to update user. {e}"), + }; + } + } else { + let info: UserDetails = match serde_json::from_str::(&data) { + Err(_) => { + eprintln!("Unable to convert user details JSON."); + std::process::exit(1); + } + Ok(i) => i, + }; + let user_id = get_user_id(&cfg, &info.name); + match UserList::update_user_info( + UserList::new(USER_ID, &cfg.server_url, &cfg.api_key), + &user_id, + &info, + ) { + Ok(_) => {} + Err(e) => { + eprintln!("Unable to update user. {e}") + } + } + } + } + + // Server based commands + Commands::GetPackages { json } => { + let packages = + get_packages_info(ServerInfo::new("/Packages", &cfg.server_url, &cfg.api_key)) + .unwrap(); + if json { + PackageDetails::json_print(&packages); + } else { + PackageDetails::table_print(packages); + } + } + + Commands::GetRepositories { json } => { + let repos = get_repo_info(ServerInfo::new( + "/Repositories", + &cfg.server_url, + &cfg.api_key, + )) + .unwrap(); + if json { + RepositoryDetails::json_print(&repos); + } else { + RepositoryDetails::table_print(repos); + } + } + + Commands::RegisterRepository { name, path } => { + let mut repos = get_repo_info(ServerInfo::new( + "/Repositories", + &cfg.server_url, + &cfg.api_key, + )) + .unwrap(); + repos.push(RepositoryDetails::new(name, path, true)); + set_repo_info( + ServerInfo::new("/Repositories", &cfg.server_url, &cfg.api_key), + repos, + ); + } + + Commands::InstallPackage { + package, + version, + repository, + } => { + // Check if package name has spaces and replace them as needed + let encoded = package.replace(" ", "%20"); + install_package( + ServerInfo::new( + "/Packages/Installed/{package}", + &cfg.server_url, + &cfg.api_key, + ), + &encoded, + &version, + &repository, + ); + } + + Commands::ServerInfo {} => { + get_server_info(ServerInfo::new( + "/System/Info", + &cfg.server_url, + &cfg.api_key, + )) + .expect("Unable to gather server information."); + } + Commands::ListLogs { json } => { + let logs = match get_log_filenames(ServerInfo::new( + "/System/Logs", + &cfg.server_url, + &cfg.api_key, + )) { + Err(_) => { + eprintln!("Unable to get get log filenames."); + std::process::exit(1); + } + Ok(i) => i, + }; + if json { + LogDetails::json_print(&logs); + } else { + LogDetails::table_print(logs); + } + } + Commands::ShowLog { logfile } => { + LogFile::get_logfile(LogFile::new( + ServerInfo::new("/System/Logs/Log", &cfg.server_url, &cfg.api_key), + logfile, + )) + .expect("Unable to retrieve the specified logfile."); + } + Commands::Reconfigure {} => { + initial_config(cfg); + } + Commands::GetDevices { active, json } => { + let devices: Vec = match get_devices( + ServerInfo::new(DEVICES, &cfg.server_url, &cfg.api_key), + active, + ) { + Err(e) => { + eprintln!("Unable to get devices, {e}"); + std::process::exit(1); + } + Ok(i) => i, + }; + if json { + DeviceDetails::json_print(&devices); + } else { + DeviceDetails::table_print(&devices); + } + } + Commands::GetLibraries { json } => { + let libraries: Vec = match get_libraries(ServerInfo::new( + "/Library/VirtualFolders", + &cfg.server_url, + &cfg.api_key, + )) { + Err(_) => { + eprintln!("Unable to get libraries."); + std::process::exit(1); + } + Ok(i) => i, + }; + if json { + LibraryDetails::json_print(&libraries); + } else { + LibraryDetails::table_print(libraries); + } + } + Commands::GetScheduledTasks { json } => { + let tasks: Vec = match get_scheduled_tasks(ServerInfo::new( + "/ScheduledTasks", + &cfg.server_url, + &cfg.api_key, + )) { + Err(e) => { + eprintln!("Unable to get scheduled tasks, {e}"); + std::process::exit(1); + } + Ok(i) => i, + }; + + if json { + TaskDetails::json_print(&tasks); + } else { + TaskDetails::table_print(tasks); + } + } + Commands::ExecuteTaskByName { task } => { + let taskid: String = match get_taskid_by_taskname( + ServerInfo::new("/ScheduledTasks", &cfg.server_url, &cfg.api_key), + &task, + ) { + Err(e) => { + eprintln!("Unable to get task id by taskname, {e}"); + std::process::exit(1); + } + Ok(i) => i, + }; + execute_task_by_id( + ServerInfo::new( + "/ScheduledTasks/Running/{taskId}", + &cfg.server_url, + &cfg.api_key, + ), + &task, + &taskid, + ); + } + Commands::ScanLibrary { + library_id, + scan_type, + } => { + if library_id == "all" { + scan_library_all(ServerInfo::new( + "/Library/Refresh", + &cfg.server_url, + &cfg.api_key, + )); + } else { + let query_info = match scan_type { + ScanType::NewUpdated => { + vec![ + ("Recursive", "true"), + ("ImageRefreshMode", "Default"), + ("MetadataRefreshMode", "Default"), + ("ReplaceAllImages", "false"), + ("RegenerateTrickplay", "false"), + ("ReplaceAllMetadata", "false"), + ] + } + ScanType::MissingMetadata => { + vec![ + ("Recursive", "true"), + ("ImageRefreshMode", "FullRefresh"), + ("MetadataRefreshMode", "FullRefresh"), + ("ReplaceAllImages", "false"), + ("RegenerateTrickplay", "false"), + ("ReplaceAllMetadata", "false"), + ] + } + ScanType::ReplaceMetadata => { + vec![ + ("Recursive", "true"), + ("ImageRefreshMode", "FullRefresh"), + ("MetadataRefreshMode", "FullRefresh"), + ("ReplaceAllImages", "false"), + ("RegenerateTrickplay", "false"), + ("ReplaceAllMetadata", "true"), + ] + } + _ => std::process::exit(1), + }; + scan_library( + ServerInfo::new("/Items/{library_id}/Refresh", &cfg.server_url, &cfg.api_key), + query_info, + library_id, + ); + } + } + Commands::RemoveDeviceByUsername { username } => { + let filtered: Vec = match get_deviceid_by_username( + ServerInfo::new(DEVICES, &cfg.server_url, &cfg.api_key), + &username, + ) { + Err(_) => { + eprintln!("Unable to get device id by username."); + std::process::exit(1); + } + Ok(i) => i, + }; + for item in filtered { + remove_device( + ServerInfo::new(DEVICES, &cfg.server_url, &cfg.api_key), + &item, + ) + .expect("Unable to delete specified id."); + } + } + Commands::RestartJellyfin {} => { + restart_or_shutdown(ServerInfo::new( + "/System/Restart", + &cfg.server_url, + &cfg.api_key, + )); + } + Commands::ShutdownJellyfin {} => { + restart_or_shutdown(ServerInfo::new( + "/System/Shutdown", + &cfg.server_url, + &cfg.api_key, + )); + } + Commands::GetPlugins { json } => { + let plugins: Vec = match PluginInfo::get_plugins(PluginInfo::new( + "/Plugins", + &cfg.server_url, + cfg.api_key, + )) { + Err(_) => { + eprintln!("Unable to get plugin information."); + std::process::exit(1); + } + Ok(i) => i, + }; + if json { + PluginDetails::json_print(&plugins); + } else { + PluginDetails::table_print(plugins); + } + } + Commands::CreateReport { + report_type, + limit, + filename, + } => match report_type { + ReportType::Activity => { + println!("Gathering Activity information....."); + let activities: ActivityDetails = match get_activity( + ServerInfo::new("/System/ActivityLog/Entries", &cfg.server_url, &cfg.api_key), + &limit, + ) { + Err(e) => { + eprintln!("Unable to gather activity log entries, {e}"); + std::process::exit(1); + } + Ok(i) => i, + }; + if !filename.is_empty() { + println!("Exporting Activity information to {}.....", &filename); + let csv = ActivityDetails::print_as_csv(activities); + export_data(&csv, filename); + println!("Export complete."); + } else { + ActivityDetails::table_print(activities); + } + } + ReportType::Movie => { + let user_id: String = match UserList::get_current_user_information(UserList::new( + "/Users/Me", + &cfg.server_url, + &cfg.api_key, + )) { + Err(e) => { + eprintln!("Unable to gather information about current user, {e}"); + std::process::exit(1); + } + Ok(i) => i.id, + }; + let movies: MovieDetails = match export_library( + ServerInfo::new("/Users/{userId}/Items", &cfg.server_url, &cfg.api_key), + &user_id, + ) { + Err(e) => { + eprintln!("Unable to export library, {e}"); + std::process::exit(1); + } + Ok(i) => i, + }; + if !filename.is_empty() { + println!("Exporting Movie information to {}.....", &filename); + let csv = MovieDetails::print_as_csv(movies); + export_data(&csv, filename); + println!("Export complete."); + } else { + MovieDetails::table_print(movies); + } + } + }, + Commands::SearchMedia { + term, + mediatype, + parentid, + include_filepath, + output_format, + table_columns, + } => { + let search_result = execute_search(&term, mediatype, parentid, include_filepath, &cfg); + + let mut used_table_columns = table_columns.clone(); + + if include_filepath { + used_table_columns.push("Path".to_string()); + } + + match output_format { + OutputFormat::Json => { + MediaRoot::json_print(search_result); + } + OutputFormat::Csv => { + MediaRoot::csv_print(search_result, &used_table_columns); + } + _ => { + MediaRoot::table_print(search_result, &used_table_columns); + } + } + } + } + + Ok(()) +} + +/// +/// Executes a search with the passed parameters. +/// +fn execute_search( + term: &str, + mediatype: String, + parentid: String, + include_filepath: bool, + cfg: &AppConfig, +) -> MediaRoot { + let mut query = vec![ + ("SortBy", "SortName,ProductionYear"), + ("Recursive", "true"), + ("searchTerm", term), + ]; + if mediatype != "all" { + query.push(("IncludeItemTypes", &mediatype)); + } + + if include_filepath { + query.push(("fields", "Path")); + } + + if !parentid.is_empty() { + query.push(("parentId", &parentid)); + } + + match get_search_results( + ServerInfo::new("/Items", &cfg.server_url, &cfg.api_key), + query, + ) { + Err(e) => { + eprintln!("Unable to execute search, {e}"); + std::process::exit(1); + } + Ok(i) => i, + } +} + +/// +/// Retrieve the id for the specified user. Most API calls require the id of the user rather than the username. +/// +fn get_user_id(cfg: &AppConfig, username: &String) -> String { + UserList::get_user_id( + UserList::new("/Users", &cfg.server_url, &cfg.api_key), + username, + ) +} + +/// +/// Gathers user information. +/// +fn gather_user_information(cfg: &AppConfig, username: &String, id: &str) -> UserDetails { + match UserList::get_user_information(UserList::new(USER_ID, &cfg.server_url, &cfg.api_key), id) + { + Err(_) => { + println!("Unable to get user id for {}", username); + std::process::exit(1); + } + Ok(ul) => ul, + } +} + +/// +/// Helper function to standardize the call for adding a user with a password. +/// +fn add_user(cfg: &AppConfig, username: String, password: String) { + let server_path = format!("{}/Users/New", cfg.server_url); + match UserWithPass::create_user(UserWithPass::new( + Some(username), + Some(password), + None, + server_path, + cfg.api_key.clone(), + )) { + Err(_) => { + println!("Unable to create user"); + std::process::exit(1); + } + Ok(i) => i, + } +} + +/// +/// Executed on initial run or when user wants to redo configuration. Will attempt to auto-configure +/// the application prior to allowing customization by +/// the user. +/// +fn initial_config(mut cfg: AppConfig) { + println!("[INFO] Attempting to determine Jellyfin information....."); + env::consts::OS.clone_into(&mut cfg.os); + println!("[INFO] OS detected as {}.", cfg.os); + + print!("[INPUT] Please enter your Jellyfin URL: "); + io::stdout().flush().expect("Unable to get Jellyfin URL."); + let mut server_url_input = String::new(); + io::stdin() + .read_line(&mut server_url_input) + .expect("Could not read server url information"); + server_url_input.trim().clone_into(&mut cfg.server_url); + + print!("[INPUT] Please enter your Jellyfin username: "); + io::stdout().flush().expect("Unable to get username."); + let mut username = String::new(); + io::stdin() + .read_line(&mut username) + .expect("[ERROR] Could not read Jellyfin username"); + let password = rpassword::prompt_password("Please enter your Jellyfin password: ").unwrap(); + println!("[INFO] Attempting to authenticate user."); + cfg.api_key = UserAuth::auth_user(UserAuth::new(&cfg.server_url, username.trim(), password)) + .expect("Unable to generate user auth token. Please assure your configuration information was input correctly\n"); + + "configured".clone_into(&mut cfg.status); + token_to_api(cfg); +} + +/// +/// Due to an issue with api key processing in Jellyfin, JellyRoller was initially relied on using auto tokens to communicate. +/// Now that the issue has been fixed, the auto tokens need to be converted to an API key. The single purpose of this function +/// is to handle the conversion with no input required from the user. +/// +fn token_to_api(mut cfg: AppConfig) { + println!("[INFO] Attempting to auto convert user auth token to API key....."); + // Check if api key already exists + if UserWithPass::retrieve_api_token(UserWithPass::new( + None, + None, + None, + format!("{}/Auth/Keys", cfg.server_url), + cfg.api_key.clone(), + )) + .unwrap() + .is_empty() + { + UserWithPass::create_api_token(UserWithPass::new( + None, + None, + None, + format!("{}/Auth/Keys", cfg.server_url), + cfg.api_key.clone(), + )); + } + cfg.api_key = UserWithPass::retrieve_api_token(UserWithPass::new( + None, + None, + None, + format!("{}/Auth/Keys", cfg.server_url), + cfg.api_key, + )) + .unwrap(); + cfg.token = "apiKey".to_string(); + confy::store("jellyroller", "jellyroller", cfg) + .expect("[ERROR] Unable to store updated configuration."); + println!("[INFO] Auth token successfully converted to API key."); +} + +/// +/// Function that converts an image into a base64 png image. +/// +fn image_to_base64(path: String) -> String { + let base_img = image::open(path).unwrap(); + let mut image_data: Vec = Vec::new(); + base_img + .write_to(&mut Cursor::new(&mut image_data), ImageFormat::Png) + .unwrap(); + general_purpose::STANDARD.encode(image_data) +} + +/// +/// Custom implementation to convert the ImageType enum into Strings +/// for easy comparison. +/// +impl fmt::Display for ImageType { + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + match self { + ImageType::Primary => write!(f, "Primary"), + ImageType::Art => write!(f, "Art"), + ImageType::Backdrop => write!(f, "Backdrop"), + ImageType::Banner => write!(f, "Banner"), + ImageType::Logo => write!(f, "Logo"), + ImageType::Thumb => write!(f, "Thumb"), + ImageType::Disc => write!(f, "Disc"), + ImageType::Box => write!(f, "Box"), + ImageType::Screenshot => write!(f, "Screenshot"), + ImageType::Menu => write!(f, "Menu"), + ImageType::BoxRear => write!(f, "BoxRear"), + ImageType::Profile => write!(f, "Profile"), + } + } +} + +/// +/// Custom implementation to convert collectiontype enum into Strings +/// +impl fmt::Display for CollectionType { + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + match self { + CollectionType::Movies => write!(f, "movies"), + CollectionType::TVShows => write!(f, "tvshows"), + CollectionType::Music => write!(f, "music"), + CollectionType::MusicVideos => write!(f, "musicvideos"), + CollectionType::HomeVideos => write!(f, "homevideos"), + CollectionType::BoxSets => write!(f, "boxsets"), + CollectionType::Books => write!(f, "books"), + CollectionType::Mixed => write!(f, "mixed"), + } + } +} diff --git a/src/plugin_actions.rs b/src/plugin_actions.rs index fc58676..4ae1549 100644 --- a/src/plugin_actions.rs +++ b/src/plugin_actions.rs @@ -1,17 +1,19 @@ -use super::{ PluginRootJson, PluginDetails, responder::simple_get, handle_others, handle_unauthorized }; +use super::{ + handle_others, handle_unauthorized, responder::simple_get, PluginDetails, PluginRootJson, +}; use reqwest::StatusCode; #[derive(Clone)] pub struct PluginInfo { server_url: String, - api_key: String + api_key: String, } impl PluginInfo { pub fn new(endpoint: &str, server_url: &str, api_key: String) -> PluginInfo { PluginInfo { server_url: format!("{}{}", server_url, endpoint), - api_key + api_key, } } @@ -21,15 +23,16 @@ impl PluginInfo { StatusCode::OK => { let json = response.text()?; let plugins = serde_json::from_str::(&json)?; - return Ok(plugins) - } StatusCode::UNAUTHORIZED => { + return Ok(plugins); + } + StatusCode::UNAUTHORIZED => { handle_unauthorized(); - } _ => { + } + _ => { handle_others(response); - } } Ok(Vec::new()) } -} \ No newline at end of file +} diff --git a/src/responder.rs b/src/responder.rs index 2173e47..eaa028b 100644 --- a/src/responder.rs +++ b/src/responder.rs @@ -7,7 +7,9 @@ pub fn simple_get(server_url: String, api_key: String, query: Vec<(&str, &str)>) .header("Authorization", format!("MediaBrowser Token=\"{api_key}\"")) .query(&query) .send(); - if let Ok(resp) = response { resp } else { + if let Ok(resp) = response { + resp + } else { println!("Get response error."); std::process::exit(1); } @@ -21,13 +23,20 @@ pub fn simple_post(server_url: String, api_key: String, body: String) -> Respons .header("Authorization", format!("MediaBrowser Token=\"{api_key}\"")) .body(body) .send(); - if let Ok(resp) = response { resp } else { + if let Ok(resp) = response { + resp + } else { println!("Post response error."); std::process::exit(1); } } -pub fn simple_post_with_query(server_url: String, api_key: String, body: String, query: Vec<(&str, &str)>) -> Response { +pub fn simple_post_with_query( + server_url: String, + api_key: String, + body: String, + query: Vec<(&str, &str)>, +) -> Response { let client = Client::new(); let response = client .post(server_url) @@ -36,7 +45,9 @@ pub fn simple_post_with_query(server_url: String, api_key: String, body: String, .body(body) .query(&query) .send(); - if let Ok(resp) = response { resp } else { + if let Ok(resp) = response { + resp + } else { println!("Post with query response error."); std::process::exit(1); } @@ -50,8 +61,10 @@ pub fn simple_post_image(server_url: String, api_key: String, body: String) -> R .header("Authorization", format!("MediaBrowser Token=\"{api_key}\"")) .body(body) .send(); - if let Ok(resp) = response { resp } else { + if let Ok(resp) = response { + resp + } else { println!("Post image response error."); std::process::exit(1); } -} \ No newline at end of file +} diff --git a/src/system_actions.rs b/src/system_actions.rs index 9004d49..b1ef951 100644 --- a/src/system_actions.rs +++ b/src/system_actions.rs @@ -1,429 +1,540 @@ -use crate:: entities::{activity_details::ActivityDetails, media_details::MediaRoot, repository_details::RepositoryDetails, task_details::TaskDetails }; - -use super::{ ServerInfo, DeviceDetails, DeviceRootJson, LibraryDetails, LibraryRootJson, LogDetails, MovieDetails, RepositoryDetailsRoot, PackageDetailsRoot, PackageDetails, ImageType, responder::{ simple_get, simple_post, simple_post_with_query, simple_post_image }, handle_unauthorized, handle_others }; -use reqwest::{blocking::Client, StatusCode}; -use serde_json::Value; -use chrono::{ DateTime, Duration }; - -pub type LogFileVec = Vec; -pub type ScheduledTasksVec = Vec; - -// Currently used for server-info, restart-jellyfin, shutdown-jellyfin -pub fn get_server_info(server_info: ServerInfo) -> Result<(), Box> { - let response = simple_get(server_info.server_url, server_info.api_key, Vec::new()); - match response.status() { - StatusCode::OK => { - let body: Value = response.json()?; - println!("{:#}", body); - } StatusCode::UNAUTHORIZED => { - handle_unauthorized(); - } _ => { - handle_others(response); - } - } - - Ok(()) -} - -pub fn get_repo_info(server_info: ServerInfo) -> Result, Box> { - let response = simple_get(server_info.server_url, server_info.api_key, Vec::new()); - let mut repos = Vec::new(); - match response.status() { - StatusCode::OK => { - repos = response.json::()?; - } _ => { - handle_others(response) - } - } - Ok(repos) -} - -pub fn set_repo_info(server_info: ServerInfo, repos: Vec) { - simple_post(server_info.server_url, server_info.api_key, serde_json::to_string(&repos).unwrap()); -} - -pub fn get_packages_info(server_info: ServerInfo) -> Result, Box> { - let response = simple_get(server_info.server_url, server_info.api_key, Vec::new()); - let mut packages = Vec::new(); - match response.status() { - StatusCode::OK => { - packages = response.json::()?; - } _ => { - handle_others(response) - } - } - Ok(packages) -} - -pub fn install_package(server_info: ServerInfo, package: &str, version: &str, repository: &str) { - let query = - vec![ - ("version", version), - ("repository", repository) - ]; - let response = simple_post_with_query(server_info.server_url.replace("{package}", package),server_info.api_key, String::new(), query); - match response.status() { - StatusCode::NO_CONTENT => { - println!("Package successfully installed."); - } StatusCode::UNAUTHORIZED => { - handle_unauthorized(); - } _ => { - handle_others(response); - } - } -} - -pub fn return_server_info(server_info: ServerInfo) -> String { - let response = simple_get(server_info.server_url, server_info.api_key, Vec::new()); - match response.status() { - StatusCode::OK => { - let body: Value = response.json().unwrap(); - body.to_string() - } _ => { - handle_others(response); - "".to_string() - } - } -} - -pub fn restart_or_shutdown(server_info: ServerInfo) { - let response = simple_post(server_info.server_url, server_info.api_key, String::new()); - match response.status() { - StatusCode::NO_CONTENT => { - println!("Command successful."); - } StatusCode::UNAUTHORIZED => { - handle_unauthorized(); - } _ => { - handle_others(response); - } - } -} - -pub fn get_log_filenames(server_info: ServerInfo) -> Result, Box> { - let response = simple_get(server_info.server_url, server_info.api_key, Vec::new()); - let mut details = Vec::new(); - match response.status() { - StatusCode::OK => { - let logs = response.json::()?; - for log in logs { - details.push(LogDetails::new(log.date_created, log.date_modified, log.name, log.size/1024)); - } - } StatusCode::UNAUTHORIZED => { - handle_unauthorized(); - } _ => { - handle_others(response); - } - } - - Ok(details) -} - -pub fn get_devices(server_info: ServerInfo, active: bool) -> Result, Box> { - let response = simple_get(server_info.server_url, server_info.api_key, Vec::new()); - let mut details = Vec::new(); - match response.status() { - StatusCode::OK => { - let json = response.text()?; - let devices = serde_json::from_str::(&json)?; - let cutofftime = chrono::offset::Utc::now() - Duration::seconds(960); - for device in devices.items { - let datetime = DateTime::parse_from_rfc3339(&device.lastactivity).unwrap(); - if active { - if cutofftime < datetime { - details.push(DeviceDetails::new(device.id, device.name, device.lastusername, device.lastactivity)); - } - } else { - details.push(DeviceDetails::new(device.id, device.name, device.lastusername, device.lastactivity)); - } - } - } StatusCode::UNAUTHORIZED => { - handle_unauthorized(); - } _ => { - handle_others(response); - } - } - - Ok(details) -} - -pub fn get_libraries(server_info: ServerInfo) -> Result, Box> { - let response = simple_get(server_info.server_url, server_info.api_key, Vec::new()); - let mut details = Vec::new(); - match response.status() { - StatusCode::OK => { - let json = response.text()?; - let libraries = serde_json::from_str::(&json)?; - for library in libraries { - details.push(LibraryDetails::new(library.name, library.collection_type, library.item_id, library.refresh_status)); - } - } StatusCode::UNAUTHORIZED => { - handle_unauthorized(); - } _ => { - handle_others(response); - } - } - Ok(details) -} - -pub fn export_library(server_info: ServerInfo, user_id: &str) -> Result> { - let query = - vec![ - ("SortBy", "SortName,ProductionYear"), - ("IncludeItemTypes", "Movie"), - ("Recursive", "true"), - ("fields", "Genres,DateCreated,Width,Height,Path") - ]; - let response = simple_get(server_info.server_url.replace("{userId}", user_id), server_info.api_key, query); - match response.status() { - StatusCode::OK => { - let details = response.json::()?; - Ok(details) - } _ => { - handle_others(response); - std::process::exit(1) - } - } -} - -pub fn get_activity(server_info: ServerInfo, limit: &str) -> Result> { - let query = vec![("limit", limit)]; - let response = simple_get(server_info.server_url, server_info.api_key, query); - match response.status() { - StatusCode::OK => { - let activities = response.json::()?; - Ok(activities) - } _ => { - handle_others(response); - std::process::exit(1); - } - } -} - -pub fn get_taskid_by_taskname(server_info: ServerInfo, taskname: &str) -> Result> { - let response = simple_get(server_info.server_url, server_info.api_key, Vec::new()); - match response.status() { - StatusCode::OK => { - let tasks = response.json::()?; - for task in tasks { - if task.name.to_lowercase() == taskname.to_lowercase() { - return Ok(task.id); - } - } - } StatusCode::UNAUTHORIZED => { - handle_unauthorized(); - } _ => { - handle_others(response); - } - } - Ok(String::new()) -} - -pub fn execute_task_by_id(server_info: ServerInfo, taskname: &str, taskid: &str) { - let response = simple_post(server_info.server_url.replace("{taskId}", taskid), server_info.api_key, String::new()); - match response.status() { - StatusCode::NO_CONTENT => { - println!("Task \"{}\" initiated.", taskname); - } StatusCode::UNAUTHORIZED => { - handle_unauthorized(); - } _ => { - handle_others(response); - } - } -} - -pub fn get_deviceid_by_username(server_info: ServerInfo, username: &str) -> Result, Box> { - let response = simple_get(server_info.server_url, server_info.api_key, Vec::new()); - let mut filtered = Vec::new(); - match response.status() { - StatusCode::OK => { - let json = response.text()?; - let devices = serde_json::from_str::(&json)?; - for device in devices.items { - if device.lastusername == username { - filtered.push(device.id); - } - } - } StatusCode::UNAUTHORIZED => { - handle_unauthorized(); - } _ => { - handle_others(response); - } - } - - Ok(filtered) -} - -pub fn remove_device(server_info: ServerInfo, id: &str) -> Result<(), reqwest::Error> { - let client = Client::new(); - let apikey = server_info.api_key; - let response = client - .delete(server_info.server_url) - .header("Authorization", format!("MediaBrowser Token=\"{apikey}\"")) - .query(&[("id", &id)]) - .send()?; - match response.status() { - StatusCode::NO_CONTENT => { - println!("\t Removes device with id = {}.", id); - } StatusCode::UNAUTHORIZED => { - handle_unauthorized(); - } _ => { - handle_others(response); - } - } - Ok(()) -} - -pub fn get_scheduled_tasks(server_info: ServerInfo) -> Result, reqwest::Error> { - let response = simple_get(server_info.server_url, server_info.api_key, Vec::new()); - let mut details = Vec::new(); - match response.status() { - StatusCode::OK => { - let scheduled_tasks = response.json::()?; - for task in scheduled_tasks { - details.push(TaskDetails::new(task.name, task.state, task.percent_complete, task.id)); - } - } StatusCode::UNAUTHORIZED => { - handle_unauthorized(); - } _ => { - handle_others(response); - } - } - - Ok(details) -} - -pub fn scan_library(server_info: ServerInfo, scan_options: Vec<(&str, &str)>, library_id: String) { - let response = simple_post_with_query( - server_info.server_url.replace("{library_id}", library_id.as_str()), - server_info.api_key, - String::new(), - scan_options); - match response.status() { - StatusCode::NO_CONTENT => { - println!("Library scan initiated."); - } StatusCode::UNAUTHORIZED => { - handle_unauthorized(); - } _ => { - handle_others(response); - } - } -} - -pub fn scan_library_all(server_info: ServerInfo) { - let response = simple_post( - server_info.server_url, - server_info.api_key, - String::new()); - match response.status() { - StatusCode::NO_CONTENT => { - println!("Library scan initiated."); - } StatusCode::UNAUTHORIZED => { - handle_unauthorized(); - } _ => { - handle_others(response); - } - } -} - -pub fn register_library(server_info: ServerInfo, json_contents: String) { - let response = simple_post( - server_info.server_url, - server_info.api_key, - json_contents - ); - match response.status() { - StatusCode::NO_CONTENT => { - println!("Library successfully added."); - } StatusCode::UNAUTHORIZED => { - handle_unauthorized(); - } _ => { - handle_others(response); - } - } -} - -pub fn update_image(server_info: ServerInfo, id: String, imagetype: &ImageType, img_base64: &String) { - let response = simple_post_image( - server_info.server_url.replace("{itemId}", id.as_str()).replace("{imageType}", imagetype.to_string().as_str()), - server_info.api_key, - img_base64.to_string()); - match response.status() { - StatusCode::NO_CONTENT => { - println!("Image successfully updated."); - } StatusCode::UNAUTHORIZED => { - handle_unauthorized(); - } _ => { - handle_others(response); - } - } -} - -pub fn update_metadata(server_info: ServerInfo, id: String, json: String) { - let response = simple_post( - server_info.server_url.replace("{itemId}", id.as_str()), - server_info.api_key, - json); - match response.status() { - StatusCode::NO_CONTENT => { - println!("Metadata successfully updated."); - } StatusCode::UNAUTHORIZED => { - handle_unauthorized(); - } _ => { - handle_others(response); - } - } -} - -pub fn get_search_results(server_info: ServerInfo, query: Vec<(&str, &str)>) -> Result> { - let response = simple_get( - server_info.server_url, - server_info.api_key, - query - ); - match response.status() { - StatusCode::OK => { - let media = response.json::()?; - Ok(media) - } StatusCode::UNAUTHORIZED => { - handle_unauthorized(); - std::process::exit(1); - } _ => { - handle_others(response); - std::process::exit(1); - } - } -} -pub struct LogFile { - server_info: ServerInfo, - logname: String -} - -impl LogFile { - pub fn new(server_info: ServerInfo, logname: String) -> LogFile { - LogFile { - server_info, - logname - } - } - - pub fn get_logfile(self) -> Result<(), reqwest::Error> { - let client = Client::new(); - let apikey = self.server_info.api_key; - let response = client - .get(self.server_info.server_url) - .query(&[("name", self.logname)]) - .header("Authorization", format!("MediaBrowser Token=\"{apikey}\"")) - .send()?; - match response.status() { - StatusCode::OK => { - let body = response.text(); - println!("{:#}", body?); - } StatusCode::UNAUTHORIZED => { - handle_unauthorized(); - } _ => { - handle_others(response); - } - } - Ok(()) - } -} \ No newline at end of file +use crate::entities::{ + activity_details::ActivityDetails, media_details::MediaRoot, + repository_details::RepositoryDetails, task_details::TaskDetails, +}; + +use super::{ + handle_others, handle_unauthorized, + responder::{simple_get, simple_post, simple_post_image, simple_post_with_query}, + DeviceDetails, DeviceRootJson, ImageType, LibraryDetails, LibraryRootJson, LogDetails, + MovieDetails, PackageDetails, PackageDetailsRoot, RepositoryDetailsRoot, ServerInfo, +}; +use chrono::{DateTime, Duration}; +use reqwest::{blocking::Client, StatusCode}; +use serde_json::Value; + +pub type LogFileVec = Vec; +pub type ScheduledTasksVec = Vec; + +// Currently used for server-info, restart-jellyfin, shutdown-jellyfin +pub fn get_server_info(server_info: ServerInfo) -> Result<(), Box> { + let response = simple_get(server_info.server_url, server_info.api_key, Vec::new()); + match response.status() { + StatusCode::OK => { + let body: Value = response.json()?; + println!("{:#}", body); + } + StatusCode::UNAUTHORIZED => { + handle_unauthorized(); + } + _ => { + handle_others(response); + } + } + + Ok(()) +} + +pub fn get_repo_info( + server_info: ServerInfo, +) -> Result, Box> { + let response = simple_get(server_info.server_url, server_info.api_key, Vec::new()); + let mut repos = Vec::new(); + match response.status() { + StatusCode::OK => { + repos = response.json::()?; + } + _ => handle_others(response), + } + Ok(repos) +} + +pub fn set_repo_info(server_info: ServerInfo, repos: Vec) { + simple_post( + server_info.server_url, + server_info.api_key, + serde_json::to_string(&repos).unwrap(), + ); +} + +pub fn get_packages_info( + server_info: ServerInfo, +) -> Result, Box> { + let response = simple_get(server_info.server_url, server_info.api_key, Vec::new()); + let mut packages = Vec::new(); + match response.status() { + StatusCode::OK => { + packages = response.json::()?; + } + _ => handle_others(response), + } + Ok(packages) +} + +pub fn install_package(server_info: ServerInfo, package: &str, version: &str, repository: &str) { + let query = vec![("version", version), ("repository", repository)]; + let response = simple_post_with_query( + server_info.server_url.replace("{package}", package), + server_info.api_key, + String::new(), + query, + ); + match response.status() { + StatusCode::NO_CONTENT => { + println!("Package successfully installed."); + } + StatusCode::UNAUTHORIZED => { + handle_unauthorized(); + } + _ => { + handle_others(response); + } + } +} + +pub fn return_server_info(server_info: ServerInfo) -> String { + let response = simple_get(server_info.server_url, server_info.api_key, Vec::new()); + match response.status() { + StatusCode::OK => { + let body: Value = response.json().unwrap(); + body.to_string() + } + _ => { + handle_others(response); + "".to_string() + } + } +} + +pub fn restart_or_shutdown(server_info: ServerInfo) { + let response = simple_post(server_info.server_url, server_info.api_key, String::new()); + match response.status() { + StatusCode::NO_CONTENT => { + println!("Command successful."); + } + StatusCode::UNAUTHORIZED => { + handle_unauthorized(); + } + _ => { + handle_others(response); + } + } +} + +pub fn get_log_filenames( + server_info: ServerInfo, +) -> Result, Box> { + let response = simple_get(server_info.server_url, server_info.api_key, Vec::new()); + let mut details = Vec::new(); + match response.status() { + StatusCode::OK => { + let logs = response.json::()?; + for log in logs { + details.push(LogDetails::new( + log.date_created, + log.date_modified, + log.name, + log.size / 1024, + )); + } + } + StatusCode::UNAUTHORIZED => { + handle_unauthorized(); + } + _ => { + handle_others(response); + } + } + + Ok(details) +} + +pub fn get_devices( + server_info: ServerInfo, + active: bool, +) -> Result, Box> { + let response = simple_get(server_info.server_url, server_info.api_key, Vec::new()); + let mut details = Vec::new(); + match response.status() { + StatusCode::OK => { + let json = response.text()?; + let devices = serde_json::from_str::(&json)?; + let cutofftime = chrono::offset::Utc::now() - Duration::seconds(960); + for device in devices.items { + let datetime = DateTime::parse_from_rfc3339(&device.lastactivity).unwrap(); + if active { + if cutofftime < datetime { + details.push(DeviceDetails::new( + device.id, + device.name, + device.lastusername, + device.lastactivity, + )); + } + } else { + details.push(DeviceDetails::new( + device.id, + device.name, + device.lastusername, + device.lastactivity, + )); + } + } + } + StatusCode::UNAUTHORIZED => { + handle_unauthorized(); + } + _ => { + handle_others(response); + } + } + + Ok(details) +} + +pub fn get_libraries( + server_info: ServerInfo, +) -> Result, Box> { + let response = simple_get(server_info.server_url, server_info.api_key, Vec::new()); + let mut details = Vec::new(); + match response.status() { + StatusCode::OK => { + let json = response.text()?; + let libraries = serde_json::from_str::(&json)?; + for library in libraries { + details.push(LibraryDetails::new( + library.name, + library.collection_type, + library.item_id, + library.refresh_status, + )); + } + } + StatusCode::UNAUTHORIZED => { + handle_unauthorized(); + } + _ => { + handle_others(response); + } + } + Ok(details) +} + +pub fn export_library( + server_info: ServerInfo, + user_id: &str, +) -> Result> { + let query = vec![ + ("SortBy", "SortName,ProductionYear"), + ("IncludeItemTypes", "Movie"), + ("Recursive", "true"), + ("fields", "Genres,DateCreated,Width,Height,Path"), + ]; + let response = simple_get( + server_info.server_url.replace("{userId}", user_id), + server_info.api_key, + query, + ); + match response.status() { + StatusCode::OK => { + let details = response.json::()?; + Ok(details) + } + _ => { + handle_others(response); + std::process::exit(1) + } + } +} + +pub fn get_activity( + server_info: ServerInfo, + limit: &str, +) -> Result> { + let query = vec![("limit", limit)]; + let response = simple_get(server_info.server_url, server_info.api_key, query); + match response.status() { + StatusCode::OK => { + let activities = response.json::()?; + Ok(activities) + } + _ => { + handle_others(response); + std::process::exit(1); + } + } +} + +pub fn get_taskid_by_taskname( + server_info: ServerInfo, + taskname: &str, +) -> Result> { + let response = simple_get(server_info.server_url, server_info.api_key, Vec::new()); + match response.status() { + StatusCode::OK => { + let tasks = response.json::()?; + for task in tasks { + if task.name.to_lowercase() == taskname.to_lowercase() { + return Ok(task.id); + } + } + } + StatusCode::UNAUTHORIZED => { + handle_unauthorized(); + } + _ => { + handle_others(response); + } + } + Ok(String::new()) +} + +pub fn execute_task_by_id(server_info: ServerInfo, taskname: &str, taskid: &str) { + let response = simple_post( + server_info.server_url.replace("{taskId}", taskid), + server_info.api_key, + String::new(), + ); + match response.status() { + StatusCode::NO_CONTENT => { + println!("Task \"{}\" initiated.", taskname); + } + StatusCode::UNAUTHORIZED => { + handle_unauthorized(); + } + _ => { + handle_others(response); + } + } +} + +pub fn get_deviceid_by_username( + server_info: ServerInfo, + username: &str, +) -> Result, Box> { + let response = simple_get(server_info.server_url, server_info.api_key, Vec::new()); + let mut filtered = Vec::new(); + match response.status() { + StatusCode::OK => { + let json = response.text()?; + let devices = serde_json::from_str::(&json)?; + for device in devices.items { + if device.lastusername == username { + filtered.push(device.id); + } + } + } + StatusCode::UNAUTHORIZED => { + handle_unauthorized(); + } + _ => { + handle_others(response); + } + } + + Ok(filtered) +} + +pub fn remove_device(server_info: ServerInfo, id: &str) -> Result<(), reqwest::Error> { + let client = Client::new(); + let apikey = server_info.api_key; + let response = client + .delete(server_info.server_url) + .header("Authorization", format!("MediaBrowser Token=\"{apikey}\"")) + .query(&[("id", &id)]) + .send()?; + match response.status() { + StatusCode::NO_CONTENT => { + println!("\t Removes device with id = {}.", id); + } + StatusCode::UNAUTHORIZED => { + handle_unauthorized(); + } + _ => { + handle_others(response); + } + } + Ok(()) +} + +pub fn get_scheduled_tasks(server_info: ServerInfo) -> Result, reqwest::Error> { + let response = simple_get(server_info.server_url, server_info.api_key, Vec::new()); + let mut details = Vec::new(); + match response.status() { + StatusCode::OK => { + let scheduled_tasks = response.json::()?; + for task in scheduled_tasks { + details.push(TaskDetails::new( + task.name, + task.state, + task.percent_complete, + task.id, + )); + } + } + StatusCode::UNAUTHORIZED => { + handle_unauthorized(); + } + _ => { + handle_others(response); + } + } + + Ok(details) +} + +pub fn scan_library(server_info: ServerInfo, scan_options: Vec<(&str, &str)>, library_id: String) { + let response = simple_post_with_query( + server_info + .server_url + .replace("{library_id}", library_id.as_str()), + server_info.api_key, + String::new(), + scan_options, + ); + match response.status() { + StatusCode::NO_CONTENT => { + println!("Library scan initiated."); + } + StatusCode::UNAUTHORIZED => { + handle_unauthorized(); + } + _ => { + handle_others(response); + } + } +} + +pub fn scan_library_all(server_info: ServerInfo) { + let response = simple_post(server_info.server_url, server_info.api_key, String::new()); + match response.status() { + StatusCode::NO_CONTENT => { + println!("Library scan initiated."); + } + StatusCode::UNAUTHORIZED => { + handle_unauthorized(); + } + _ => { + handle_others(response); + } + } +} + +pub fn register_library(server_info: ServerInfo, json_contents: String) { + let response = simple_post(server_info.server_url, server_info.api_key, json_contents); + match response.status() { + StatusCode::NO_CONTENT => { + println!("Library successfully added."); + } + StatusCode::UNAUTHORIZED => { + handle_unauthorized(); + } + _ => { + handle_others(response); + } + } +} + +pub fn update_image( + server_info: ServerInfo, + id: String, + imagetype: &ImageType, + img_base64: &String, +) { + let response = simple_post_image( + server_info + .server_url + .replace("{itemId}", id.as_str()) + .replace("{imageType}", imagetype.to_string().as_str()), + server_info.api_key, + img_base64.to_string(), + ); + match response.status() { + StatusCode::NO_CONTENT => { + println!("Image successfully updated."); + } + StatusCode::UNAUTHORIZED => { + handle_unauthorized(); + } + _ => { + handle_others(response); + } + } +} + +pub fn update_metadata(server_info: ServerInfo, id: String, json: String) { + let response = simple_post( + server_info.server_url.replace("{itemId}", id.as_str()), + server_info.api_key, + json, + ); + match response.status() { + StatusCode::NO_CONTENT => { + println!("Metadata successfully updated."); + } + StatusCode::UNAUTHORIZED => { + handle_unauthorized(); + } + _ => { + handle_others(response); + } + } +} + +pub fn get_search_results( + server_info: ServerInfo, + query: Vec<(&str, &str)>, +) -> Result> { + let response = simple_get(server_info.server_url, server_info.api_key, query); + match response.status() { + StatusCode::OK => { + let media = response.json::()?; + Ok(media) + } + StatusCode::UNAUTHORIZED => { + handle_unauthorized(); + std::process::exit(1); + } + _ => { + handle_others(response); + std::process::exit(1); + } + } +} +pub struct LogFile { + server_info: ServerInfo, + logname: String, +} + +impl LogFile { + pub fn new(server_info: ServerInfo, logname: String) -> LogFile { + LogFile { + server_info, + logname, + } + } + + pub fn get_logfile(self) -> Result<(), reqwest::Error> { + + let client = Client::new(); + let apikey = self.server_info.api_key; + let response = client + .get(self.server_info.server_url) + .query(&[("name", self.logname)]) + .header("Authorization", format!("MediaBrowser Token=\"{apikey}\"")) + .send()?; + match response.status() { + StatusCode::OK => { + let body = response.text(); + println!("{:#}", body?); + } + StatusCode::UNAUTHORIZED => { + handle_unauthorized(); + } + _ => { + handle_others(response); + } + } + Ok(()) + } +} diff --git a/src/user_actions.rs b/src/user_actions.rs index 7dce393..d20b90f 100644 --- a/src/user_actions.rs +++ b/src/user_actions.rs @@ -1,284 +1,330 @@ -use crate::entities::token_details::TokenDetails; - -use super::{ UserDetails, Policy, responder::{simple_get, simple_post}, handle_others, handle_unauthorized } ; -use reqwest::{StatusCode, blocking::Client, header::{CONTENT_TYPE, CONTENT_LENGTH}}; - -#[derive(Serialize, Deserialize)] -pub struct UserWithPass { - #[serde(rename = "Name")] - username: Option, - #[serde(rename = "NewPw")] - pass: Option, - #[serde(rename = "CurrentPw")] - currentpwd: Option, - server_url: String, - auth_key: String -} - -impl UserWithPass { - pub fn new(username: Option, pass: Option, currentpwd: Option, server_url: String, auth_key: String) -> UserWithPass { - UserWithPass{ - username: Some(username.unwrap_or_else(|| {String::new()})), - pass: Some(pass.unwrap_or_else(|| {String::new()})), - currentpwd: Some(currentpwd.unwrap_or_else(|| {String::new()})), - server_url, - auth_key - } - } - - pub fn resetpass(self) -> Result<(), Box> { - let response = simple_post( - self.server_url.clone(), - self.auth_key.clone(), - serde_json::to_string_pretty(&self)?); - match response.status() { - StatusCode::NO_CONTENT => { - println!("Password updated successfully."); - } StatusCode::UNAUTHORIZED => { - handle_unauthorized(); - } _ => { - println!("{}", response.status() ); - handle_others(response); - } - } - - Ok(()) - } - - pub fn create_user(self) -> Result<(), Box< dyn std::error::Error>> { - let response = simple_post( - self.server_url.clone(), - self.auth_key.clone(), - serde_json::to_string_pretty(&self)?); - match response.status() { - StatusCode::OK => { - println!("User \"{}\" successfully created.", &self.username.unwrap()); - } StatusCode::UNAUTHORIZED => { - handle_unauthorized(); - } _ => { - handle_others(response); - } - } - - - Ok(()) - } - - pub fn delete_user(self) -> Result<(), Box> { - let client = reqwest::blocking::Client::new(); - let apikey = self.auth_key; - let response = client - .delete(self.server_url) - .header("Authorization", format!("MediaBrowser Token=\"{apikey}\"")) - .header(CONTENT_TYPE, "application/json") - .send()?; - match response.status() { - StatusCode::NO_CONTENT => { - println!("User \"{}\" successfully removed.", &self.username.unwrap()); - } StatusCode::UNAUTHORIZED => { - handle_unauthorized(); - } _ => { - handle_others(response); - } - } - - Ok(()) - } - - pub fn create_api_token(self) { - let client = Client::new(); - let apikey = self.auth_key; - let response = client - .post(self.server_url) - .header("Authorization", format!("MediaBrowser Token=\"{apikey}\"")) - .header(CONTENT_LENGTH, 0) - .query(&[("app", "JellyRoller")]) - .send() - .unwrap(); - - match response.status() { - StatusCode::NO_CONTENT => { - println!("API key created."); - } _ => { - handle_others(response); - } - - } - } - - pub fn retrieve_api_token(self) -> Result> { - let response = simple_get(self.server_url, self.auth_key, Vec::new()); - match response.status() { - StatusCode::OK => { - let tokens = serde_json::from_str::(&response.text()?)?; - for token in tokens.items { - if token.app_name == "JellyRoller" { - return Ok(token.access_token); - } - } - } StatusCode::UNAUTHORIZED => { - handle_unauthorized(); - } _ => { - handle_others(response); - } - } - Ok(String::new()) - } -} - -#[derive(Serialize, Deserialize)] -#[serde(rename_all = "camelCase")] -pub struct UserAuthJson { - #[serde(rename = "AccessToken")] - pub access_token: String, - #[serde(rename = "ServerId")] - pub server_id: String, -} - -pub type UserInfoVec = Vec; - -#[derive(Serialize, Deserialize)] -pub struct UserAuth { - server_url: String, - username: String, - pw: String -} - -impl UserAuth { - pub fn new(server_url: &str, username: &str, password: String) -> UserAuth{ - UserAuth{ - server_url: format!("{}/Users/authenticatebyname",server_url), - username: username.to_owned(), - pw: password - } - } - - pub fn auth_user(self) -> Result> { - let client = Client::new(); - let response = client - .post(self.server_url.clone()) - .header(CONTENT_TYPE, "application/json") - .header("Authorization", "MediaBrowser Client=\"JellyRoller\", Device=\"jellyroller\", DeviceId=\"1\", Version=\"0.0.1\"") - .body(serde_json::to_string_pretty(&self)?) - .send()?; - match response.status() { - StatusCode::OK => { - let result = response.json::()?; - println!("[INFO] User authenticated successfully."); - Ok(result.access_token) - } _ => { - // Panic since the application requires an authenticated user - handle_others(response); - panic!("[ERROR] Unable to authenticate user. Please assure your configuration information is correct.\n"); - } - } - } - - -} - -#[derive(Clone)] -pub struct UserList { - server_url: String, - api_key: String -} - -impl UserList { - pub fn new(endpoint: &str, server_url: &str, api_key: &str) -> UserList{ - UserList{ - server_url: format!("{}{}",server_url, endpoint), - api_key: api_key.to_string() - } - } - - pub fn list_users(self) -> Result, Box> { - let response = simple_get(self.server_url, self.api_key, Vec::new()); - let mut users = Vec::new(); - match response.status() { - StatusCode::OK => { - users = response.json::()?; - } StatusCode::UNAUTHORIZED => { - handle_unauthorized(); - } _ => { - handle_others(response); - } - } - - Ok(users) - } - - // TODO: Standardize the GET request? - pub fn get_user_id(self, username: &String) -> String { - let response = simple_get(self.server_url, self.api_key, Vec::new()); - let users = response.json::().unwrap(); - for user in users { - if user.name == *username { - return user.id; - } - } - - // Supplied username could not be found. Panic. - panic!("Could not find user {}.", username); - } - - pub fn get_user_information(self, id: &str) -> Result> { - let response = simple_get(self.server_url.replace("{userId}", id), self.api_key, Vec::new()); - Ok(serde_json::from_str(response.text()?.as_str())?) - } - - pub fn get_current_user_information(self) -> Result> { - let response = simple_get(self.server_url, self.api_key, Vec::new()); - Ok(response.json::()?) - } - - pub fn update_user_config_bool(self, user_info: &Policy, id: &str, username: &str) -> Result<(), Box> { - let body = serde_json::to_string_pretty(user_info)?; - let response = simple_post( - self.server_url.replace("{userId}", id), - self.api_key.clone(), - body); - if response.status() == StatusCode::NO_CONTENT { - println!("User {} successfully updated.", username); - } else { - println!("Unable to update user policy information."); - println!("Status Code: {}", response.status()); - println!("{}", response.text()?); - } - Ok(()) - } - - // - // I really hate this function but it works for now. - // - pub fn update_user_info(self, id: &str, info: &UserDetails) -> Result<(), Box> { - let body = serde_json::to_string_pretty(&info)?; - // So we have to update the Policy and the user info separate even though they are the same JSON object :/ - - // First we will update the Policy - let policy_url = format!("{}/Policy",self.server_url); - let user_response = simple_post(self.server_url.replace("{userId}", id), self.api_key.clone(), body); - if user_response.status() == StatusCode::NO_CONTENT {} else { - println!("Unable to update user information."); - println!("Status Code: {}", user_response.status()); - match user_response.text() { - Ok(t) => println!("{}", t), - Err(_) => eprintln!("Could not get response text from user information update.") - } - } - - let response = simple_post(policy_url.replace("{userId}", id), self.api_key, serde_json::to_string_pretty(&info.policy)?); - if response.status() == StatusCode::NO_CONTENT { - println!("{} successfully updated.", info.name); - } else { - println!("Unable to update user information."); - println!("Status Code: {}", response.status()); - match response.text() { - Ok(t) => println!("{}", t), - Err(_) => eprintln!("Could not get response text from user policy update.") - } - } - - Ok(()) - - } -} \ No newline at end of file +use crate::entities::token_details::TokenDetails; + +use super::{ + handle_others, handle_unauthorized, + responder::{simple_get, simple_post}, + Policy, UserDetails, +}; +use reqwest::{ + blocking::Client, + header::{CONTENT_LENGTH, CONTENT_TYPE}, + StatusCode, +}; + +#[derive(Serialize, Deserialize)] +pub struct UserWithPass { + #[serde(rename = "Name")] + username: Option, + #[serde(rename = "NewPw")] + pass: Option, + #[serde(rename = "CurrentPw")] + currentpwd: Option, + server_url: String, + auth_key: String, +} + +impl UserWithPass { + pub fn new( + username: Option, + pass: Option, + currentpwd: Option, + server_url: String, + auth_key: String, + ) -> UserWithPass { + UserWithPass { + username: Some(username.unwrap_or_else(|| String::new())), + pass: Some(pass.unwrap_or_else(|| String::new())), + currentpwd: Some(currentpwd.unwrap_or_else(|| String::new())), + server_url, + auth_key, + } + } + + pub fn resetpass(self) -> Result<(), Box> { + let response = simple_post( + self.server_url.clone(), + self.auth_key.clone(), + serde_json::to_string_pretty(&self)?, + ); + match response.status() { + StatusCode::NO_CONTENT => { + println!("Password updated successfully."); + } + StatusCode::UNAUTHORIZED => { + handle_unauthorized(); + } + _ => { + println!("{}", response.status()); + handle_others(response); + } + } + + Ok(()) + } + + pub fn create_user(self) -> Result<(), Box> { + let response = simple_post( + self.server_url.clone(), + self.auth_key.clone(), + serde_json::to_string_pretty(&self)?, + ); + match response.status() { + StatusCode::OK => { + println!("User \"{}\" successfully created.", &self.username.unwrap()); + } + StatusCode::UNAUTHORIZED => { + handle_unauthorized(); + } + _ => { + handle_others(response); + } + } + + Ok(()) + } + + pub fn delete_user(self) -> Result<(), Box> { + let client = reqwest::blocking::Client::new(); + let apikey = self.auth_key; + let response = client + .delete(self.server_url) + .header("Authorization", format!("MediaBrowser Token=\"{apikey}\"")) + .header(CONTENT_TYPE, "application/json") + .send()?; + match response.status() { + StatusCode::NO_CONTENT => { + println!("User \"{}\" successfully removed.", &self.username.unwrap()); + } + StatusCode::UNAUTHORIZED => { + handle_unauthorized(); + } + _ => { + handle_others(response); + } + } + + Ok(()) + } + + pub fn create_api_token(self) { + let client = Client::new(); + let apikey = self.auth_key; + let response = client + .post(self.server_url) + .header("Authorization", format!("MediaBrowser Token=\"{apikey}\"")) + .header(CONTENT_LENGTH, 0) + .query(&[("app", "JellyRoller")]) + .send() + .unwrap(); + + match response.status() { + StatusCode::NO_CONTENT => { + println!("API key created."); + } + _ => { + handle_others(response); + } + } + } + + pub fn retrieve_api_token(self) -> Result> { + let response = simple_get(self.server_url, self.auth_key, Vec::new()); + match response.status() { + StatusCode::OK => { + let tokens = serde_json::from_str::(&response.text()?)?; + for token in tokens.items { + if token.app_name == "JellyRoller" { + return Ok(token.access_token); + } + } + } + StatusCode::UNAUTHORIZED => { + handle_unauthorized(); + } + _ => { + handle_others(response); + } + } + Ok(String::new()) + } +} + +#[derive(Serialize, Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct UserAuthJson { + #[serde(rename = "AccessToken")] + pub access_token: String, + #[serde(rename = "ServerId")] + pub server_id: String, +} + +pub type UserInfoVec = Vec; + +#[derive(Serialize, Deserialize)] +pub struct UserAuth { + server_url: String, + username: String, + pw: String, +} + +impl UserAuth { + pub fn new(server_url: &str, username: &str, password: String) -> UserAuth { + UserAuth { + server_url: format!("{}/Users/authenticatebyname", server_url), + username: username.to_owned(), + pw: password, + } + } + + pub fn auth_user(self) -> Result> { + let client = Client::new(); + let response = client + .post(self.server_url.clone()) + .header(CONTENT_TYPE, "application/json") + .header("Authorization", "MediaBrowser Client=\"JellyRoller\", Device=\"jellyroller\", DeviceId=\"1\", Version=\"0.0.1\"") + .body(serde_json::to_string_pretty(&self)?) + .send()?; + match response.status() { + StatusCode::OK => { + let result = response.json::()?; + println!("[INFO] User authenticated successfully."); + Ok(result.access_token) + } + _ => { + // Panic since the application requires an authenticated user + handle_others(response); + panic!("[ERROR] Unable to authenticate user. Please assure your configuration information is correct.\n"); + } + } + } +} + +#[derive(Clone)] +pub struct UserList { + server_url: String, + api_key: String, +} + +impl UserList { + pub fn new(endpoint: &str, server_url: &str, api_key: &str) -> UserList { + UserList { + server_url: format!("{}{}", server_url, endpoint), + api_key: api_key.to_string(), + } + } + + pub fn list_users(self) -> Result, Box> { + let response = simple_get(self.server_url, self.api_key, Vec::new()); + let mut users = Vec::new(); + match response.status() { + StatusCode::OK => { + users = response.json::()?; + } + StatusCode::UNAUTHORIZED => { + handle_unauthorized(); + } + _ => { + handle_others(response); + } + } + + Ok(users) + } + + // TODO: Standardize the GET request? + pub fn get_user_id(self, username: &String) -> String { + let response = simple_get(self.server_url, self.api_key, Vec::new()); + let users = response.json::().unwrap(); + for user in users { + if user.name == *username { + return user.id; + } + } + + // Supplied username could not be found. Panic. + panic!("Could not find user {}.", username); + } + + pub fn get_user_information(self, id: &str) -> Result> { + let response = simple_get( + self.server_url.replace("{userId}", id), + self.api_key, + Vec::new(), + ); + Ok(serde_json::from_str(response.text()?.as_str())?) + } + + pub fn get_current_user_information(self) -> Result> { + let response = simple_get(self.server_url, self.api_key, Vec::new()); + Ok(response.json::()?) + } + + pub fn update_user_config_bool( + self, + user_info: &Policy, + id: &str, + username: &str, + ) -> Result<(), Box> { + let body = serde_json::to_string_pretty(user_info)?; + let response = simple_post( + self.server_url.replace("{userId}", id), + self.api_key.clone(), + body, + ); + if response.status() == StatusCode::NO_CONTENT { + println!("User {} successfully updated.", username); + } else { + println!("Unable to update user policy information."); + println!("Status Code: {}", response.status()); + println!("{}", response.text()?); + } + Ok(()) + } + + // + // I really hate this function but it works for now. + // + pub fn update_user_info( + self, + id: &str, + info: &UserDetails, + ) -> Result<(), Box> { + let body = serde_json::to_string_pretty(&info)?; + // So we have to update the Policy and the user info separate even though they are the same JSON object :/ + + // First we will update the Policy + let policy_url = format!("{}/Policy", self.server_url); + let user_response = simple_post( + self.server_url.replace("{userId}", id), + self.api_key.clone(), + body, + ); + if user_response.status() == StatusCode::NO_CONTENT { + } else { + println!("Unable to update user information."); + println!("Status Code: {}", user_response.status()); + match user_response.text() { + Ok(t) => println!("{}", t), + Err(_) => eprintln!("Could not get response text from user information update."), + } + } + + let response = simple_post( + policy_url.replace("{userId}", id), + self.api_key, + serde_json::to_string_pretty(&info.policy)?, + ); + if response.status() == StatusCode::NO_CONTENT { + println!("{} successfully updated.", info.name); + } else { + println!("Unable to update user information."); + println!("Status Code: {}", response.status()); + match response.text() { + Ok(t) => println!("{}", t), + Err(_) => eprintln!("Could not get response text from user policy update."), + } + } + + Ok(()) + } +} diff --git a/src/utils/mod.rs b/src/utils/mod.rs index a3e5195..7145787 100644 --- a/src/utils/mod.rs +++ b/src/utils/mod.rs @@ -1,2 +1,2 @@ pub mod output_writer; -pub mod status_handler; \ No newline at end of file +pub mod status_handler; diff --git a/src/utils/output_writer.rs b/src/utils/output_writer.rs index 157ba48..61d3de7 100644 --- a/src/utils/output_writer.rs +++ b/src/utils/output_writer.rs @@ -5,4 +5,4 @@ pub fn export_data(data: &str, path: String) { let f = File::create(path).expect("Unable to create file"); let mut f = BufWriter::new(f); f.write_all(data.as_bytes()).expect("Unable to write data"); -} \ No newline at end of file +} diff --git a/src/utils/status_handler.rs b/src/utils/status_handler.rs index d3a16d7..db52a9e 100644 --- a/src/utils/status_handler.rs +++ b/src/utils/status_handler.rs @@ -8,4 +8,4 @@ pub fn handle_unauthorized() { pub fn handle_others(response: Response) { println!("Status Code: {}", response.status()); std::process::exit(1); -} \ No newline at end of file +} From 92840df03ac0bfd38b159c7314adbfc6bee8b636 Mon Sep 17 00:00:00 2001 From: Luther Schallot Date: Sat, 4 Jan 2025 15:42:48 -0600 Subject: [PATCH 02/18] Decided to change the output order of the help menu. --- src/main.rs | 292 ++++++++++++++++++++++++++-------------------------- 1 file changed, 146 insertions(+), 146 deletions(-) diff --git a/src/main.rs b/src/main.rs index 1df6d21..38b3bdd 100644 --- a/src/main.rs +++ b/src/main.rs @@ -94,6 +94,24 @@ enum Commands { #[clap(required = true, value_parser)] password: String, }, + /// Uses the supplied file to mass create new users. + AddUsers { + /// File that contains the user information in "username,password" lines. + #[clap(required = true, value_parser)] + inputfile: String, + }, + /// Creates a report of either activity or available movie items + CreateReport { + /// Type of report (activity or movie) + #[clap(required = true)] + report_type: ReportType, + /// Total number of records to return (defaults to 100) + #[clap(required = false, short, long, default_value = "100")] + limit: String, + /// Output filename + #[clap(required = false, short, long, default_value = "")] + filename: String, + }, /// Deletes an existing user. #[clap(arg_required_else_help = true)] DeleteUser { @@ -101,44 +119,23 @@ enum Commands { #[clap(required = true, value_parser)] username: String, }, - /// Lists the current users with basic information. - ListUsers { - /// Exports the user list information to a file - #[clap(short, long)] - export: bool, - /// Path for the file export - #[clap(short, long, default_value = "")] - output: String, - /// Username to gather information about - #[clap(short, long, default_value = "")] + /// Disable a user. + DisableUser { + #[clap(required = true, value_parser)] username: String, }, - /// Resets a user's password. - #[clap(arg_required_else_help = true)] - ResetPassword { - /// User to be modified. + /// Enable a user. + EnableUser { #[clap(required = true, value_parser)] username: String, - /// What to reset the specified user's password to. - #[clap(required = true, value_parser)] - password: String, - }, - /// Displays the server information. - ServerInfo {}, - /// Displays the available system logs. - ListLogs { - /// Print information as json. - #[clap(long, required = false)] - json: bool, }, - /// Displays the requested logfile. - ShowLog { - /// Name of the logfile to show. + /// Executes a scheduled task by name. + ExecuteTaskByName { #[clap(required = true, value_parser)] - logfile: String, + task: String, }, - /// Reconfigure the connection information. - Reconfigure {}, + /// Generate a report for an issue. + GenerateReport {}, /// Show all devices. GetDevices { /// Only show devices active in the last hour @@ -148,10 +145,29 @@ enum Commands { #[clap(long, required = false)] json: bool, }, - /// Removes all devices associated with the specified user. - RemoveDeviceByUsername { - #[clap(required = true, value_parser)] - username: String, + /// Gets the libraries available to the configured user + GetLibraries { + /// Print information as json. + #[clap(long, required = false)] + json: bool, + }, + /// Lists all available packages + GetPackages { + /// Print information as json. + #[clap(long, required = false)] + json: bool, + }, + /// Returns a list of installed plugins + GetPlugins { + /// Print information as json. + #[clap(long, required = false)] + json: bool, + }, + /// Lists all current repositories + GetRepositories { + /// Print information as json. + #[clap(long, required = false)] + json: bool, }, /// Show all scheduled tasks and their status. GetScheduledTasks { @@ -159,34 +175,78 @@ enum Commands { #[clap(long, required = false)] json: bool, }, - /// Executes a scheduled task by name. - ExecuteTaskByName { + /// Grants the specified user admin rights. + GrantAdmin { #[clap(required = true, value_parser)] - task: String, + username: String, }, - /// Start a library scan. - ScanLibrary { - /// Library ID - #[clap(required = false, value_parser, default_value = "all")] - library_id: String, - /// Type of scan - #[clap(required = false, default_value = "all")] - scan_type: ScanType, + /// Installs the specified package + InstallPackage { + /// Package to install + #[clap(short = 'p', long = "package", required = true)] + package: String, + /// Version to install + #[clap(short = 'v', long = "version", required = false, default_value = "")] + version: String, + /// Repository to install from + #[clap(short = 'r', long = "repository", required = false, default_value = "")] + repository: String, }, - /// Disable a user. - DisableUser { - #[clap(required = true, value_parser)] + /// Displays the available system logs. + ListLogs { + /// Print information as json. + #[clap(long, required = false)] + json: bool, + }, + /// Lists the current users with basic information. + ListUsers { + /// Exports the user list information to a file + #[clap(short, long)] + export: bool, + /// Path for the file export + #[clap(short, long, default_value = "")] + output: String, + /// Username to gather information about + #[clap(short, long, default_value = "")] username: String, }, - /// Enable a user. - EnableUser { + /// Reconfigure the connection information. + Reconfigure {}, + /// Registers a new library. + RegisterLibrary { + /// Name of the new library + #[clap(required = true, short = 'n', long)] + name: String, + /// Collection Type of the new library + #[clap(required = true, short = 'c', long)] + collectiontype: CollectionType, + /// Path to file that contains the JSON for the library + #[clap(required = true, short = 'f', long)] + filename: String, + }, + /// Registers a new Plugin Repository + RegisterRepository { + /// Name of the new repository + #[clap(required = true, short = 'n', long = "name")] + name: String, + /// URL of the new repository + #[clap(required = true, short = 'u', long = "url")] + path: String, + }, + /// Removes all devices associated with the specified user. + RemoveDeviceByUsername { #[clap(required = true, value_parser)] username: String, }, - /// Grants the specified user admin rights. - GrantAdmin { + /// Resets a user's password. + #[clap(arg_required_else_help = true)] + ResetPassword { + /// User to be modified. #[clap(required = true, value_parser)] username: String, + /// What to reset the specified user's password to. + #[clap(required = true, value_parser)] + password: String, }, /// Revokes admin rights from the specified user. RevokeAdmin { @@ -195,43 +255,14 @@ enum Commands { }, /// Restarts Jellyfin RestartJellyfin {}, - /// Shuts down Jellyfin - ShutdownJellyfin {}, - /// Gets the libraries available to the configured user - GetLibraries { - /// Print information as json. - #[clap(long, required = false)] - json: bool, - }, - /// Returns a list of installed plugins - GetPlugins { - /// Print information as json. - #[clap(long, required = false)] - json: bool, - }, - /// Uses the supplied file to mass create new users. - AddUsers { - /// File that contains the user information in "username,password" lines. - #[clap(required = true, value_parser)] - inputfile: String, - }, - /// Mass update users in the supplied file - UpdateUsers { - /// File that contains the user JSON information. - #[clap(required = true, value_parser)] - inputfile: String, - }, - /// Creates a report of either activity or available movie items - CreateReport { - /// Type of report (activity or movie) - #[clap(required = true)] - report_type: ReportType, - /// Total number of records to return (defaults to 100) - #[clap(required = false, short, long, default_value = "100")] - limit: String, - /// Output filename - #[clap(required = false, short, long, default_value = "")] - filename: String, + /// Start a library scan. + ScanLibrary { + /// Library ID + #[clap(required = false, value_parser, default_value = "all")] + library_id: String, + /// Type of scan + #[clap(required = false, default_value = "all")] + scan_type: ScanType, }, /// Executes a search of your media SearchMedia { @@ -253,17 +284,16 @@ enum Commands { #[clap(short = 'c', long, value_parser, num_args = 0.., value_delimiter = ',', default_value = "Name,ID,Type")] table_columns: Vec, }, - /// Updates image of specified file by name - UpdateImageByName { - /// Attempt to update based on title. Requires unique search term. - #[clap(required = true, short, long)] - title: String, - /// Path to the image that will be used. - #[clap(required = true, short, long)] - path: String, - #[clap(required = true, short, long)] - imagetype: ImageType, + /// Displays the server information. + ServerInfo {}, + /// Displays the requested logfile. + ShowLog { + /// Name of the logfile to show. + #[clap(required = true, value_parser)] + logfile: String, }, + /// Shuts down Jellyfin + ShutdownJellyfin {}, /// Updates image of specified file by id UpdateImageById { /// Attempt to update based on item id. @@ -275,8 +305,17 @@ enum Commands { #[clap(required = true, short = 'I', long)] imagetype: ImageType, }, - /// Generate a report for an issue. - GenerateReport {}, + /// Updates image of specified file by name + UpdateImageByName { + /// Attempt to update based on title. Requires unique search term. + #[clap(required = true, short, long)] + title: String, + /// Path to the image that will be used. + #[clap(required = true, short, long)] + path: String, + #[clap(required = true, short, long)] + imagetype: ImageType, + }, /// Updates metadata of specified id with metadata provided by specified file UpdateMetadata { /// ID of the file to update @@ -286,50 +325,11 @@ enum Commands { #[clap(required = true, short = 'f', long)] filename: String, }, - /// Registers a new library. - RegisterLibrary { - /// Name of the new library - #[clap(required = true, short = 'n', long)] - name: String, - /// Collection Type of the new library - #[clap(required = true, short = 'c', long)] - collectiontype: CollectionType, - /// Path to file that contains the JSON for the library - #[clap(required = true, short = 'f', long)] - filename: String, - }, - /// Registers a new Plugin Repository - RegisterRepository { - /// Name of the new repository - #[clap(required = true, short = 'n', long = "name")] - name: String, - /// URL of the new repository - #[clap(required = true, short = 'u', long = "url")] - path: String, - }, - /// Lists all current repositories - GetRepositories { - /// Print information as json. - #[clap(long, required = false)] - json: bool, - }, - /// Lists all available packages - GetPackages { - /// Print information as json. - #[clap(long, required = false)] - json: bool, - }, - /// Installs the specified package - InstallPackage { - /// Package to install - #[clap(short = 'p', long = "package", required = true)] - package: String, - /// Version to install - #[clap(short = 'v', long = "version", required = false, default_value = "")] - version: String, - /// Repository to install from - #[clap(short = 'r', long = "repository", required = false, default_value = "")] - repository: String, + /// Mass update users in the supplied file + UpdateUsers { + /// File that contains the user JSON information. + #[clap(required = true, value_parser)] + inputfile: String, }, } From ab6e0c753283ca25895fdfe6a68d4fdffdda0e73 Mon Sep 17 00:00:00 2001 From: Luther Schallot Date: Sun, 5 Jan 2025 22:05:01 -0600 Subject: [PATCH 03/18] Updating README.md to mirror help output. --- README.md | 48 ++++++++++++++++++++++++------------------------ 1 file changed, 24 insertions(+), 24 deletions(-) diff --git a/README.md b/README.md index 85a7387..71fd903 100644 --- a/README.md +++ b/README.md @@ -17,39 +17,39 @@ Usage: jellyroller.exe Commands: add-user Creates a new user + add-users Uses the supplied file to mass create new users + create-report Creates a report of either activity or available movie items delete-user Deletes an existing user - list-users Lists the current users with basic information - reset-password Resets a user's password - server-info Displays the server information - list-logs Displays the available system logs - show-log Displays the requested logfile - reconfigure Reconfigure the connection information - get-devices Show all devices - remove-device-by-username Removes all devices associated with the specified user - get-scheduled-tasks Show all scheduled tasks and their status - execute-task-by-name Executes a scheduled task by name - scan-library Start a library scan disable-user Disable a user enable-user Enable a user + execute-task-by-name Executes a scheduled task by name + generate-report Generate a report for an issue + get-devices Show all devices + get-libraries Gets the libraries available to the configured user + get-packages Lists all available packages + get-plugins Returns a list of installed plugins + get-repositories Lists all current repositories + get-scheduled-tasks Show all scheduled tasks and their status grant-admin Grants the specified user admin rights + install-package Installs the specified package + list-logs Displays the available system logs + list-users Lists the current users with basic information + reconfigure Reconfigure the connection information + register-library Registers a new library + register-repository Registers a new Plugin Repository + remove-device-by-username Removes all devices associated with the specified user + reset-password Resets a user's password revoke-admin Revokes admin rights from the specified user restart-jellyfin Restarts Jellyfin - shutdown-jellyfin Shuts down Jellyfin - get-libraries Gets the libraries available to the configured user - get-plugins Returns a list of installed plugins - add-users Uses the supplied file to mass create new users - update-users Mass update users in the supplied file - create-report Creates a report of either activity or available movie items + scan-library Start a library scan search-media Executes a search of your media - update-image-by-name Updates image of specified file by name + server-info Displays the server information + show-log Displays the requested logfile + shutdown-jellyfin Shuts down Jellyfin update-image-by-id Updates image of specified file by id - generate-report Generate a report for an issue + update-image-by-name Updates image of specified file by name update-metadata Updates metadata of specified id with metadata provided by specified file - register-library Registers a new library - register-repository Registers a new Plugin Repository - get-repositories Lists all current repositories - get-packages Lists all available packages - install-package Installs the specified package + update-users Mass update users in the supplied file help Print this message or the help of the given subcommand(s) Options: From 5ded13732b6151f5335f06cb9a9ce6fab165fbec Mon Sep 17 00:00:00 2001 From: Luther Schallot Date: Mon, 6 Jan 2025 21:44:33 -0600 Subject: [PATCH 04/18] Starting process of deprecating the --json flag in lieu of output_format. Added csv output to TaskDetails. Organized ENUMS in main. --- src/entities/task_details.rs | 9 ++++ src/main.rs | 96 +++++++++++++++++++++++------------- 2 files changed, 71 insertions(+), 34 deletions(-) diff --git a/src/entities/task_details.rs b/src/entities/task_details.rs index 80480a5..c660b48 100644 --- a/src/entities/task_details.rs +++ b/src/entities/task_details.rs @@ -27,6 +27,15 @@ impl TaskDetails { println!("{}", serde_json::to_string_pretty(&tasks).unwrap()); } + pub fn csv_print(tasks: &[TaskDetails]) { + for task in tasks { + let mut per_comp: String = "".to_string(); + if task.percent_complete > 0.0 { + per_comp = task.percent_complete.to_string(); + } + println!("{}, {}, {}, {}", task.name, task.state, per_comp, task.id); + } + } pub fn table_print(tasks: Vec) { let mut table = Table::new(); table diff --git a/src/main.rs b/src/main.rs index 38b3bdd..e799f27 100644 --- a/src/main.rs +++ b/src/main.rs @@ -5,6 +5,7 @@ use std::env; use std::fmt; use std::fs::{self, File}; use std::io::{self, BufRead, BufReader, Cursor, Read, Write}; +use std::{thread, time}; mod user_actions; use user_actions::{UserAuth, UserList, UserWithPass}; @@ -65,13 +66,6 @@ impl Default for AppConfig { } } -#[derive(ValueEnum, Clone, Debug)] -enum OutputFormat { - Json, - Csv, - Table, -} - /// CLAP CONFIGURATION /// CLI controller for Jellyfin #[derive(Debug, Parser)] // requires `derive` feature @@ -171,9 +165,12 @@ enum Commands { }, /// Show all scheduled tasks and their status. GetScheduledTasks { - /// Print information as json. + /// Print information as json (DEPRECATED). #[clap(long, required = false)] json: bool, + /// Specify the output format + #[clap(short = 'o', long, value_enum, default_value = "table")] + output_format: OutputFormat, }, /// Grants the specified user admin rights. GrantAdmin { @@ -334,23 +331,21 @@ enum Commands { } #[derive(ValueEnum, Clone, Debug, PartialEq)] -enum Detail { - User, - Server, -} - -#[derive(ValueEnum, Clone, Debug, PartialEq)] -enum ScanType { - NewUpdated, - MissingMetadata, - ReplaceMetadata, - All, +enum CollectionType { + Movies, + TVShows, + Music, + MusicVideos, + HomeVideos, + BoxSets, + Books, + Mixed, } #[derive(ValueEnum, Clone, Debug, PartialEq)] -enum ReportType { - Activity, - Movie, +enum Detail { + User, + Server, } #[derive(ValueEnum, Clone, Debug, PartialEq)] @@ -369,16 +364,25 @@ enum ImageType { Profile, } +#[derive(ValueEnum, Clone, Debug)] +enum OutputFormat { + Json, + Csv, + Table, +} + #[derive(ValueEnum, Clone, Debug, PartialEq)] -enum CollectionType { - Movies, - TVShows, - Music, - MusicVideos, - HomeVideos, - BoxSets, - Books, - Mixed, +enum ReportType { + Activity, + Movie, +} + +#[derive(ValueEnum, Clone, Debug, PartialEq)] +enum ScanType { + NewUpdated, + MissingMetadata, + ReplaceMetadata, + All, } fn main() -> Result<(), confy::ConfyError> { @@ -840,7 +844,7 @@ fn main() -> Result<(), confy::ConfyError> { LibraryDetails::table_print(libraries); } } - Commands::GetScheduledTasks { json } => { + Commands::GetScheduledTasks { json , output_format} => { let tasks: Vec = match get_scheduled_tasks(ServerInfo::new( "/ScheduledTasks", &cfg.server_url, @@ -854,9 +858,21 @@ fn main() -> Result<(), confy::ConfyError> { }; if json { + json_deprecation(); TaskDetails::json_print(&tasks); - } else { - TaskDetails::table_print(tasks); + std::process::exit(0); + } + + match output_format { + OutputFormat::Json => { + TaskDetails::json_print(&tasks); + } + OutputFormat::Csv => { + TaskDetails::csv_print(&tasks); + } + _ => { + TaskDetails::table_print(tasks); + } } } Commands::ExecuteTaskByName { task } => { @@ -1073,6 +1089,18 @@ fn main() -> Result<(), confy::ConfyError> { Ok(()) } +/// +/// JSON flag deprecation message. +/// +fn json_deprecation() { + println!("|========= DEPRECATION WARNING ============|"); + println!(" The \"--json\" flag has been deprecated."); + println!(" Please consider migrating to the"); + println!(" \"output_format\" argument"); + println!("|==========================================|"); + thread::sleep(time::Duration::from_millis(5000)); +} + /// /// Executes a search with the passed parameters. /// From b985a1982ea69cc535f35cdb804e3aa816e019b0 Mon Sep 17 00:00:00 2001 From: xeoneox Date: Tue, 7 Jan 2025 12:51:58 -0500 Subject: [PATCH 05/18] Update README.md installation instructions --- README.md | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/README.md b/README.md index 85a7387..436917c 100644 --- a/README.md +++ b/README.md @@ -62,10 +62,21 @@ Options: **Note:** All installation instructions assume the end-user can handle adding the application to their user's PATH. +### Mac / Linux (Homebrew) +``` +brew tap LSchallot/JellyRoller https://github.com/LSchallot/JellyRoller +brew install --build-from-source jellyroller +``` +### Windows (Scoop) + ### Building From Source Currently built with rustc 1.83.0. If building on a Linux machine, you may need to install openssl-devel. +``` +cargo install --git https://github.com/LSchallot/JellyRoller +``` + ``` git clone cd jellyroller From 3b2052063307fceba7874233fa4f49c1d77f34f8 Mon Sep 17 00:00:00 2001 From: xeoneox Date: Tue, 7 Jan 2025 14:05:24 -0500 Subject: [PATCH 06/18] Create auto-pr.ps1 --- bin/auto-pr.ps1 | 10 ++++++++++ 1 file changed, 10 insertions(+) create mode 100644 bin/auto-pr.ps1 diff --git a/bin/auto-pr.ps1 b/bin/auto-pr.ps1 new file mode 100644 index 0000000..ba2ecb2 --- /dev/null +++ b/bin/auto-pr.ps1 @@ -0,0 +1,10 @@ +#Requires -Version 5.1 +param( + # overwrite upstream param + [String]$upstream = 'lschallot/jellyroller:master' +) + +if (!$env:SCOOP_HOME) { $env:SCOOP_HOME = Resolve-Path (scoop prefix scoop) } +$autopr = "$env:SCOOP_HOME/bin/auto-pr.ps1" +$dir = "$psscriptroot/../bucket" # checks the parent dir +Invoke-Expression -Command "$autopr -dir $dir -upstream $upstream $($args | ForEach-Object { "$_ " })" From d80439534fe5722a141dc5c37593b20b9dbcd5a1 Mon Sep 17 00:00:00 2001 From: xeoneox Date: Tue, 7 Jan 2025 14:06:18 -0500 Subject: [PATCH 07/18] Create checkhashes.ps1 --- bin/checkhashes.ps1 | 5 +++++ 1 file changed, 5 insertions(+) create mode 100644 bin/checkhashes.ps1 diff --git a/bin/checkhashes.ps1 b/bin/checkhashes.ps1 new file mode 100644 index 0000000..52645a4 --- /dev/null +++ b/bin/checkhashes.ps1 @@ -0,0 +1,5 @@ +#Requires -Version 5.1 +if (!$env:SCOOP_HOME) { $env:SCOOP_HOME = Resolve-Path (scoop prefix scoop) } +$checkhashes = "$env:SCOOP_HOME\bin\checkhashes.ps1" +$dir = "$psscriptroot\..\bucket" # checks the parent dir +Invoke-Expression -Command "$checkhashes -dir $dir $($args | ForEach-Object { "$_ " })" From 18214c43a4fb407c8639de2e0fd414d91f58676f Mon Sep 17 00:00:00 2001 From: xeoneox Date: Tue, 7 Jan 2025 14:07:03 -0500 Subject: [PATCH 08/18] Create checkurls.ps1 --- bin/checkurls.ps1 | 5 +++++ 1 file changed, 5 insertions(+) create mode 100644 bin/checkurls.ps1 diff --git a/bin/checkurls.ps1 b/bin/checkurls.ps1 new file mode 100644 index 0000000..2400cf7 --- /dev/null +++ b/bin/checkurls.ps1 @@ -0,0 +1,5 @@ +#Requires -Version 5.1 +if (!$env:SCOOP_HOME) { $env:SCOOP_HOME = Resolve-Path (scoop prefix scoop) } +$checkurls = "$env:SCOOP_HOME\bin\checkurls.ps1" +$dir = "$psscriptroot\..\bucket" # checks the parent dir +Invoke-Expression -Command "$checkurls -dir $dir $($args | ForEach-Object { "$_ " })" From e56922b794ec1b2e75b15e547f0a8339564018d9 Mon Sep 17 00:00:00 2001 From: xeoneox Date: Tue, 7 Jan 2025 14:07:53 -0500 Subject: [PATCH 09/18] Create checkver.ps1 --- bin/checkver.ps1 | 12 ++++++++++++ 1 file changed, 12 insertions(+) create mode 100644 bin/checkver.ps1 diff --git a/bin/checkver.ps1 b/bin/checkver.ps1 new file mode 100644 index 0000000..fa22c38 --- /dev/null +++ b/bin/checkver.ps1 @@ -0,0 +1,12 @@ +#Requires -Version 5.1 +param( + [String] $dir = "$PSScriptRoot\..\bucket", + [Parameter(ValueFromRemainingArguments = $true)] + [String[]] $remainArgs = @() +) + +if (!$env:SCOOP_HOME) { $env:SCOOP_HOME = Resolve-Path (scoop prefix scoop) } +$checkver = "$env:SCOOP_HOME\bin\checkver.ps1" +$remainArgs = ($remainArgs | Select-Object -Unique) -join ' ' + +Invoke-Expression -Command "$checkver -dir $dir $remainArgs" From db8129a79e792de0a658e72000d570443446490b Mon Sep 17 00:00:00 2001 From: xeoneox Date: Tue, 7 Jan 2025 14:08:37 -0500 Subject: [PATCH 10/18] Create formatjson.ps1 --- bin/formatjson.ps1 | 5 +++++ 1 file changed, 5 insertions(+) create mode 100644 bin/formatjson.ps1 diff --git a/bin/formatjson.ps1 b/bin/formatjson.ps1 new file mode 100644 index 0000000..b6dd10c --- /dev/null +++ b/bin/formatjson.ps1 @@ -0,0 +1,5 @@ +#Requires -Version 5.1 +if (!$env:SCOOP_HOME) { $env:SCOOP_HOME = Resolve-Path (scoop prefix scoop) } +$formatjson = "$env:SCOOP_HOME\bin\formatjson.ps1" +$dir = "$psscriptroot\..\bucket" # checks the parent dir +Invoke-Expression -Command "$formatjson -dir $dir $($args | ForEach-Object { "$_ " })" From 68ec62a1d3ae75f8b173f36545b95efd10752c07 Mon Sep 17 00:00:00 2001 From: xeoneox Date: Tue, 7 Jan 2025 14:11:50 -0500 Subject: [PATCH 11/18] Create missing-checkver.ps1 --- bin/missing-checkver.ps1 | 6 ++++++ 1 file changed, 6 insertions(+) create mode 100644 bin/missing-checkver.ps1 diff --git a/bin/missing-checkver.ps1 b/bin/missing-checkver.ps1 new file mode 100644 index 0000000..6644327 --- /dev/null +++ b/bin/missing-checkver.ps1 @@ -0,0 +1,6 @@ +#Requires -Version 5.1 + +if (!$env:SCOOP_HOME) { $env:SCOOP_HOME = Resolve-Path (scoop prefix scoop) } +$missing_checkver = "$env:SCOOP_HOME\bin\missing-checkver.ps1" +$dir = "$psscriptroot\..\bucket" # checks the parent dir +Invoke-Expression -Command "$missing_checkver -dir $dir $($args | ForEach-Object { "$_ " })" From 4fc35dd923f6eb4dd6b89e239eda9cc227fe6f17 Mon Sep 17 00:00:00 2001 From: xeoneox Date: Tue, 7 Jan 2025 14:25:17 -0500 Subject: [PATCH 12/18] Create jellyroller.json --- bucket/jellyroller.json | 27 +++++++++++++++++++++++++++ 1 file changed, 27 insertions(+) create mode 100644 bucket/jellyroller.json diff --git a/bucket/jellyroller.json b/bucket/jellyroller.json new file mode 100644 index 0000000..4eb07b0 --- /dev/null +++ b/bucket/jellyroller.json @@ -0,0 +1,27 @@ +{ + "version": "0.6.0", + "description": "Jellyroller is a CLI configuration tool for Jellyfin", + "homepage": "https://github.com/lschallot/jellyroller", + "license": "GPL-2", + "architecture": { + "64bit": { + "url": "https://github.com/lschallot/jellyroller/releases/download/v0.6.0/jellyroller-x86_64-windows.tar.gz", + "hash": "3db3bb63461d9906058d600c181f75e80294a6a0a018437a53a8e0261e3b31d9" + } + }, + "bin": "jellyroller.exe", + "checkver": { + "url": "https://api.github.com/repos/lschallot/jellyroller/releases/latest", + "regex": "/v([\\w-.]+)" + }, + "autoupdate": { + "architecture": { + "64bit": { + "url": "https://github.com/lschallot/jellyroller/releases/download/v$version/jellyroller-x86_64-windows.tar.gz" + } + }, + "hash": { + "url": "$url.sha256" + } + } +} From 88d69aeb5eb3ec5e797f8032505a781745064632 Mon Sep 17 00:00:00 2001 From: xeoneox Date: Tue, 7 Jan 2025 14:27:53 -0500 Subject: [PATCH 13/18] Update README.md --- README.md | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/README.md b/README.md index 436917c..e5b81bb 100644 --- a/README.md +++ b/README.md @@ -68,6 +68,11 @@ brew tap LSchallot/JellyRoller https://github.com/LSchallot/JellyRoller brew install --build-from-source jellyroller ``` ### Windows (Scoop) +``` +scoop add bucket jellyroller https://github.com/lschallot/jellyroller.git +scoop update +scoop install jellyroller +``` ### Building From Source From bfd9fd0661d195d88420c30434cf06f31c4b63f2 Mon Sep 17 00:00:00 2001 From: xeoneox Date: Tue, 7 Jan 2025 14:28:50 -0500 Subject: [PATCH 14/18] Update README.md Add installation instructions --- README.md | 6 ------ 1 file changed, 6 deletions(-) diff --git a/README.md b/README.md index e5b81bb..df48775 100644 --- a/README.md +++ b/README.md @@ -82,12 +82,6 @@ Currently built with rustc 1.83.0. If building on a Linux machine, you may need cargo install --git https://github.com/LSchallot/JellyRoller ``` -``` -git clone -cd jellyroller -cargo build -``` - ### Initial Configuration When running JellyRoller for the first time, you will be prompted to configure against your Jellyfin instance. You will be prompted for various items which are described below. From 36828f850263ddddb851822d92a5261cfc1f314d Mon Sep 17 00:00:00 2001 From: Luther Schallot Date: Wed, 8 Jan 2025 18:21:27 -0600 Subject: [PATCH 15/18] Implemented output_format for all json commands. Added deprecation notice to existing json commands. Cleaned up various pieces of code. --- src/entities/device_details.rs | 8 +- src/entities/library_details.rs | 6 ++ src/entities/log_details.rs | 11 +++ src/entities/package_details.rs | 19 ++++ src/entities/plugin_details.rs | 17 ++++ src/entities/repository_details.rs | 15 ++-- src/main.rs | 136 +++++++++++++++++++++++++---- 7 files changed, 187 insertions(+), 25 deletions(-) diff --git a/src/entities/device_details.rs b/src/entities/device_details.rs index 5bacc31..3ced045 100644 --- a/src/entities/device_details.rs +++ b/src/entities/device_details.rs @@ -33,11 +33,17 @@ impl DeviceDetails { } } + pub fn csv_print(devices: &[DeviceDetails]) { + for device in devices { + println!("{}, {}, {}", &device.id, &device.name, &device.lastusername); + } + } + pub fn json_print(devices: &[DeviceDetails]) { println!("{}", serde_json::to_string_pretty(&devices).unwrap()); } - pub fn table_print(devices: &[DeviceDetails]) { + pub fn table_print(devices: Vec) { let mut table = Table::new(); table .set_content_arrangement(ContentArrangement::Dynamic) diff --git a/src/entities/library_details.rs b/src/entities/library_details.rs index d3c470b..e3c84f4 100644 --- a/src/entities/library_details.rs +++ b/src/entities/library_details.rs @@ -29,6 +29,12 @@ impl LibraryDetails { } } + pub fn csv_print(libraries: Vec) { + for library in libraries { + println!("{}, {}, {}, {}", library.name, library.collection_type, library.item_id, library.refresh_status); + } + } + pub fn json_print(libraries: &[LibraryDetails]) { println!("{}", serde_json::to_string_pretty(&libraries).unwrap()); } diff --git a/src/entities/log_details.rs b/src/entities/log_details.rs index 5c9844c..db540ef 100644 --- a/src/entities/log_details.rs +++ b/src/entities/log_details.rs @@ -22,6 +22,17 @@ impl LogDetails { } } + pub fn csv_print(logs: Vec) { + for log in logs { + println!("{}, {}, {}, {}", + log.name, + log.size.to_string(), + log.date_created, + log.date_modified, + ) + } + } + pub fn json_print(logs: &[LogDetails]) { println!("{}", serde_json::to_string_pretty(&logs).unwrap()); } diff --git a/src/entities/package_details.rs b/src/entities/package_details.rs index 0b8020e..29e813e 100644 --- a/src/entities/package_details.rs +++ b/src/entities/package_details.rs @@ -31,6 +31,25 @@ pub struct Version { } impl PackageDetails { + pub fn csv_print(packages: Vec) { + for package in packages { + let mut version_output: String = "".to_string(); + for version in package.versions { + version_output.push_str(version.version.as_str()); + version_output.push(' '); + } + println!("{}, {}, {}, {}, {}, {}, {}", + package.name, + package.description, + package.overview, + package.owner, + package.guid, + package.category, + version_output, + ); + } + } + pub fn json_print(packages: &[PackageDetails]) { println!("{}", serde_json::to_string_pretty(&packages).unwrap()); } diff --git a/src/entities/plugin_details.rs b/src/entities/plugin_details.rs index 770eab6..120ee4b 100644 --- a/src/entities/plugin_details.rs +++ b/src/entities/plugin_details.rs @@ -23,6 +23,23 @@ pub struct PluginDetails { } impl PluginDetails { + pub fn csv_print(plugins: Vec) { + for plugin in plugins { + println!("{}, {}, {}, {}, {}, {}, {}, {}", + plugin.name, + plugin.version, + plugin + .configuration_file_name + .unwrap_or_else(|| String::new()), + plugin.description, + plugin.id, + plugin.can_uninstall.to_string(), + plugin.has_image.to_string(), + plugin.status, + ) + } + } + pub fn json_print(plugins: &[PluginDetails]) { println!("{}", serde_json::to_string_pretty(&plugins).unwrap()); } diff --git a/src/entities/repository_details.rs b/src/entities/repository_details.rs index f4ff406..c255a78 100644 --- a/src/entities/repository_details.rs +++ b/src/entities/repository_details.rs @@ -18,6 +18,16 @@ impl RepositoryDetails { RepositoryDetails { name, url, enabled } } + pub fn csv_print(repos: Vec) { + for repo in repos { + println!("{}, {}, {}", + repo.name, + repo.url, + repo.enabled.to_string(), + ) + } + } + pub fn json_print(repos: &[RepositoryDetails]) { println!("{}", serde_json::to_string_pretty(&repos).unwrap()); } @@ -31,11 +41,6 @@ impl RepositoryDetails { "Plugin Name", "Version", "Config Filename", - "Description", - "Id", - "Can Uninstall", - "Image", - "Status", ]); for repo in repos { table.add_row(vec![repo.name, repo.url, repo.enabled.to_string()]); diff --git a/src/main.rs b/src/main.rs index e799f27..3d39909 100644 --- a/src/main.rs +++ b/src/main.rs @@ -138,30 +138,45 @@ enum Commands { /// Print information as json. #[clap(long, required = false)] json: bool, + /// Specify the output format + #[clap(short = 'o', long, value_enum, default_value = "table")] + output_format: OutputFormat, }, /// Gets the libraries available to the configured user GetLibraries { /// Print information as json. #[clap(long, required = false)] json: bool, + /// Specify the output format + #[clap(short = 'o', long, value_enum, default_value = "table")] + output_format: OutputFormat, }, /// Lists all available packages GetPackages { /// Print information as json. #[clap(long, required = false)] json: bool, + /// Specify the output format + #[clap(short = 'o', long, value_enum, default_value = "table")] + output_format: OutputFormat, }, /// Returns a list of installed plugins GetPlugins { /// Print information as json. #[clap(long, required = false)] json: bool, + /// Specify the output format + #[clap(short = 'o', long, value_enum, default_value = "table")] + output_format: OutputFormat, }, /// Lists all current repositories GetRepositories { /// Print information as json. #[clap(long, required = false)] json: bool, + /// Specify the output format + #[clap(short = 'o', long, value_enum, default_value = "table")] + output_format: OutputFormat, }, /// Show all scheduled tasks and their status. GetScheduledTasks { @@ -194,6 +209,9 @@ enum Commands { /// Print information as json. #[clap(long, required = false)] json: bool, + /// Specify the output format + #[clap(short = 'o', long, value_enum, default_value = "table")] + output_format: OutputFormat, }, /// Lists the current users with basic information. ListUsers { @@ -396,6 +414,8 @@ fn main() -> Result<(), confy::ConfyError> { confy::load("jellyroller", "jellyroller")? }; + let args = Cli::parse(); + if cfg.status == "not configured" { println!("Application is not configured!"); initial_config(cfg); @@ -404,7 +424,7 @@ fn main() -> Result<(), confy::ConfyError> { println!("[INFO] Username/Password detected. Reconfiguring to use API key."); token_to_api(cfg.clone()); } - let args = Cli::parse(); + match args.command { //TODO: Create a simple_post variation that allows for query params. Commands::RegisterLibrary { @@ -715,28 +735,54 @@ fn main() -> Result<(), confy::ConfyError> { } // Server based commands - Commands::GetPackages { json } => { + Commands::GetPackages { json, output_format } => { let packages = get_packages_info(ServerInfo::new("/Packages", &cfg.server_url, &cfg.api_key)) .unwrap(); + if json { + json_deprecation(); PackageDetails::json_print(&packages); - } else { - PackageDetails::table_print(packages); + std::process::exit(0) + } + + match output_format { + OutputFormat::Json => { + PackageDetails::json_print(&packages); + } + OutputFormat::Csv => { + PackageDetails::csv_print(packages); + } + _ => { + PackageDetails::table_print(packages); + } } } - Commands::GetRepositories { json } => { + Commands::GetRepositories { json, output_format } => { let repos = get_repo_info(ServerInfo::new( "/Repositories", &cfg.server_url, &cfg.api_key, )) .unwrap(); + if json { + json_deprecation(); RepositoryDetails::json_print(&repos); - } else { - RepositoryDetails::table_print(repos); + std::process::exit(0) + } + + match output_format { + OutputFormat::Json => { + RepositoryDetails::json_print(&repos); + } + OutputFormat::Csv => { + RepositoryDetails::csv_print(repos); + } + _ => { + RepositoryDetails::table_print(repos); + } } } @@ -781,7 +827,7 @@ fn main() -> Result<(), confy::ConfyError> { )) .expect("Unable to gather server information."); } - Commands::ListLogs { json } => { + Commands::ListLogs { json, output_format } => { let logs = match get_log_filenames(ServerInfo::new( "/System/Logs", &cfg.server_url, @@ -793,10 +839,23 @@ fn main() -> Result<(), confy::ConfyError> { } Ok(i) => i, }; + if json { + json_deprecation(); LogDetails::json_print(&logs); - } else { - LogDetails::table_print(logs); + std::process::exit(0) + } + + match output_format { + OutputFormat::Json => { + LogDetails::json_print(&logs); + } + OutputFormat::Csv => { + LogDetails::csv_print(logs); + } + _ => { + LogDetails::table_print(logs); + } } } Commands::ShowLog { logfile } => { @@ -809,7 +868,7 @@ fn main() -> Result<(), confy::ConfyError> { Commands::Reconfigure {} => { initial_config(cfg); } - Commands::GetDevices { active, json } => { + Commands::GetDevices { active, json, output_format } => { let devices: Vec = match get_devices( ServerInfo::new(DEVICES, &cfg.server_url, &cfg.api_key), active, @@ -820,13 +879,26 @@ fn main() -> Result<(), confy::ConfyError> { } Ok(i) => i, }; + if json { + json_deprecation(); DeviceDetails::json_print(&devices); - } else { - DeviceDetails::table_print(&devices); + std::process::exit(0) + } + + match output_format { + OutputFormat::Json => { + DeviceDetails::json_print(&devices); + } + OutputFormat::Csv => { + DeviceDetails::csv_print(&devices); + } + _ => { + DeviceDetails::table_print(devices); + } } } - Commands::GetLibraries { json } => { + Commands::GetLibraries { json, output_format } => { let libraries: Vec = match get_libraries(ServerInfo::new( "/Library/VirtualFolders", &cfg.server_url, @@ -838,10 +910,23 @@ fn main() -> Result<(), confy::ConfyError> { } Ok(i) => i, }; + if json { + json_deprecation(); LibraryDetails::json_print(&libraries); - } else { - LibraryDetails::table_print(libraries); + std::process::exit(0) + } + + match output_format { + OutputFormat::Json => { + LibraryDetails::json_print(&libraries); + } + OutputFormat::Csv => { + LibraryDetails::csv_print(libraries); + } + _ => { + LibraryDetails::table_print(libraries); + } } } Commands::GetScheduledTasks { json , output_format} => { @@ -980,7 +1065,7 @@ fn main() -> Result<(), confy::ConfyError> { &cfg.api_key, )); } - Commands::GetPlugins { json } => { + Commands::GetPlugins { json, output_format } => { let plugins: Vec = match PluginInfo::get_plugins(PluginInfo::new( "/Plugins", &cfg.server_url, @@ -992,10 +1077,23 @@ fn main() -> Result<(), confy::ConfyError> { } Ok(i) => i, }; + if json { + json_deprecation(); PluginDetails::json_print(&plugins); - } else { - PluginDetails::table_print(plugins); + std::process::exit(0) + } + + match output_format { + OutputFormat::Json => { + PluginDetails::json_print(&plugins); + } + OutputFormat::Csv => { + PluginDetails::csv_print(plugins); + } + _ => { + PluginDetails::table_print(plugins); + } } } Commands::CreateReport { From 77cf3e12f6b258c96ba4d834998bd4881b1f1a07 Mon Sep 17 00:00:00 2001 From: Luther Schallot Date: Sat, 11 Jan 2025 13:15:59 -0600 Subject: [PATCH 16/18] Updating main to allow for help text to be printed before JellyRoller configuration. --- src/main.rs | 22 +++++++++++++--------- 1 file changed, 13 insertions(+), 9 deletions(-) diff --git a/src/main.rs b/src/main.rs index 3d39909..9c9911f 100644 --- a/src/main.rs +++ b/src/main.rs @@ -414,17 +414,21 @@ fn main() -> Result<(), confy::ConfyError> { confy::load("jellyroller", "jellyroller")? }; - let args = Cli::parse(); - - if cfg.status == "not configured" { - println!("Application is not configured!"); - initial_config(cfg); - std::process::exit(0); - } else if cfg.token == "Unknown" { - println!("[INFO] Username/Password detected. Reconfiguring to use API key."); - token_to_api(cfg.clone()); + // Due to an oddity with confy and clap, manually check for help flag. + let args: Vec = env::args().collect(); + if !(args.contains(&"-h".to_string()) || args.contains(&"--help".to_string())) { + if cfg.status == "not configured" { + println!("Application is not configured!"); + initial_config(cfg); + std::process::exit(0); + } else if cfg.token == "Unknown" { + println!("[INFO] Username/Password detected. Reconfiguring to use API key."); + token_to_api(cfg.clone()); + } } + let args = Cli::parse(); + match args.command { //TODO: Create a simple_post variation that allows for query params. Commands::RegisterLibrary { From 95cdb5614a949143a788169c54cbf495c22a7719 Mon Sep 17 00:00:00 2001 From: Luther Schallot Date: Sat, 11 Jan 2025 21:58:40 -0600 Subject: [PATCH 17/18] Clippy cleanup. --- src/entities/log_details.rs | 2 +- src/entities/plugin_details.rs | 4 ++-- src/entities/repository_details.rs | 2 +- 3 files changed, 4 insertions(+), 4 deletions(-) diff --git a/src/entities/log_details.rs b/src/entities/log_details.rs index db540ef..e085cc6 100644 --- a/src/entities/log_details.rs +++ b/src/entities/log_details.rs @@ -26,7 +26,7 @@ impl LogDetails { for log in logs { println!("{}, {}, {}, {}", log.name, - log.size.to_string(), + log.size, log.date_created, log.date_modified, ) diff --git a/src/entities/plugin_details.rs b/src/entities/plugin_details.rs index 120ee4b..c82ce3c 100644 --- a/src/entities/plugin_details.rs +++ b/src/entities/plugin_details.rs @@ -33,8 +33,8 @@ impl PluginDetails { .unwrap_or_else(|| String::new()), plugin.description, plugin.id, - plugin.can_uninstall.to_string(), - plugin.has_image.to_string(), + plugin.can_uninstall, + plugin.has_image, plugin.status, ) } diff --git a/src/entities/repository_details.rs b/src/entities/repository_details.rs index c255a78..9858dce 100644 --- a/src/entities/repository_details.rs +++ b/src/entities/repository_details.rs @@ -23,7 +23,7 @@ impl RepositoryDetails { println!("{}, {}, {}", repo.name, repo.url, - repo.enabled.to_string(), + repo.enabled, ) } } From bed5cb302ece8e7e61dc6afc4a0da02455c1c0c8 Mon Sep 17 00:00:00 2001 From: Luther Schallot Date: Sun, 12 Jan 2025 14:29:06 -0600 Subject: [PATCH 18/18] Prepping for 0.7.0 release. --- Cargo.lock | 2 +- Cargo.toml | 3 ++- README.md | 2 +- 3 files changed, 4 insertions(+), 3 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 3e35d6e..5f9de6d 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -989,7 +989,7 @@ checksum = "d75a2a4b1b190afb6f5425f10f6a8f959d2ea0b9c2b1d79553551850539e4674" [[package]] name = "jellyroller" -version = "0.6.0" +version = "0.7.0" dependencies = [ "base64", "chrono", diff --git a/Cargo.toml b/Cargo.toml index 4ad3bac..ddf19ab 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "jellyroller" -version = "0.6.0" +version = "0.7.0" edition = "2021" # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html @@ -19,3 +19,4 @@ comfy-table = "7.0.1" image = "0.25.1" base64 = "0.22.1" csv = "1.3.1" + \ No newline at end of file diff --git a/README.md b/README.md index 10de563..044d5a9 100644 --- a/README.md +++ b/README.md @@ -10,7 +10,7 @@ Any previous user auth tokens will be converted to an API key upon next executio ## Usage Information ``` -jellyroller 0.6.0 +jellyroller 0.7.0 A CLI controller for managing Jellyfin Usage: jellyroller.exe