use anyhow::anyhow; use std::fmt::Debug; use indoc::formatdoc; use log::{debug, info, warn}; use serde::Serialize; use serde_json::{json, Value}; use urlencoding::encode; use crate::app::RadarrConfig; use crate::models::radarr_models::{ AddMovieBody, AddMovieSearchResult, AddOptions, AddRootFolderBody, Collection, CollectionMovie, CommandBody, Credit, CreditType, DiskSpace, DownloadRecord, DownloadsResponse, Indexer, IndexerSettings, IndexerTestResult, LogResponse, Movie, MovieCommandBody, MovieHistoryItem, QualityProfile, QueueEvent, Release, ReleaseDownloadBody, RootFolder, SystemStatus, Tag, Task, Update, }; use crate::models::servarr_data::radarr::modals::{ AddMovieModal, EditCollectionModal, EditMovieModal, IndexerTestResultModalItem, MovieDetailsModal, }; use crate::models::servarr_data::radarr::radarr_data::ActiveRadarrBlock; use crate::models::{HorizontallyScrollableText, Route, Scrollable, ScrollableText, StatefulTable}; use crate::network::{Network, NetworkEvent, RequestMethod, RequestProps}; use crate::utils::{convert_runtime, convert_to_gb}; #[cfg(test)] #[path = "radarr_network_tests.rs"] mod radarr_network_tests; #[derive(Debug, Eq, PartialEq, Clone, Copy)] pub enum RadarrEvent { AddMovie, AddRootFolder, DeleteDownload, DeleteIndexer, DeleteMovie, DeleteRootFolder, DownloadRelease, EditCollection, EditMovie, GetCollections, GetDownloads, GetIndexers, GetIndexerSettings, GetLogs, GetMovieCredits, GetMovieDetails, GetMovieHistory, GetMovies, GetOverview, GetQualityProfiles, GetQueuedEvents, GetReleases, GetRootFolders, GetStatus, GetTags, GetTasks, GetUpdates, HealthCheck, SearchNewMovie, StartTask, TestAllIndexers, TriggerAutomaticSearch, UpdateAllMovies, UpdateAndScan, UpdateCollections, UpdateDownloads, UpdateIndexerSettings, } impl RadarrEvent { const fn resource(self) -> &'static str { match self { RadarrEvent::GetCollections | RadarrEvent::EditCollection => "/collection", RadarrEvent::GetDownloads | RadarrEvent::DeleteDownload => "/queue", RadarrEvent::GetIndexers | RadarrEvent::DeleteIndexer => "/indexer", RadarrEvent::GetIndexerSettings | RadarrEvent::UpdateIndexerSettings => "/config/indexer", RadarrEvent::GetLogs => "/log", RadarrEvent::AddMovie | RadarrEvent::EditMovie | RadarrEvent::GetMovies | RadarrEvent::GetMovieDetails | RadarrEvent::DeleteMovie => "/movie", RadarrEvent::SearchNewMovie => "/movie/lookup", RadarrEvent::GetMovieCredits => "/credit", RadarrEvent::GetMovieHistory => "/history/movie", RadarrEvent::GetOverview => "/diskspace", RadarrEvent::GetQualityProfiles => "/qualityprofile", RadarrEvent::GetReleases | RadarrEvent::DownloadRelease => "/release", RadarrEvent::AddRootFolder | RadarrEvent::GetRootFolders | RadarrEvent::DeleteRootFolder => { "/rootfolder" } RadarrEvent::GetStatus => "/system/status", RadarrEvent::GetTags => "/tag", RadarrEvent::GetTasks => "/system/task", RadarrEvent::GetUpdates => "/update", RadarrEvent::TestAllIndexers => "/indexer/testall", RadarrEvent::StartTask | RadarrEvent::GetQueuedEvents | RadarrEvent::TriggerAutomaticSearch | RadarrEvent::UpdateAndScan | RadarrEvent::UpdateAllMovies | RadarrEvent::UpdateDownloads | RadarrEvent::UpdateCollections => "/command", RadarrEvent::HealthCheck => "/health", } } } impl From for NetworkEvent { fn from(radarr_event: RadarrEvent) -> Self { NetworkEvent::Radarr(radarr_event) } } impl<'a, 'b> Network<'a, 'b> { pub async fn handle_radarr_event(&mut self, radarr_event: RadarrEvent) { match radarr_event { RadarrEvent::AddMovie => self.add_movie().await, RadarrEvent::AddRootFolder => self.add_root_folder().await, RadarrEvent::DeleteDownload => self.delete_download().await, RadarrEvent::DeleteIndexer => self.delete_indexer().await, RadarrEvent::DeleteMovie => self.delete_movie().await, RadarrEvent::DeleteRootFolder => self.delete_root_folder().await, RadarrEvent::DownloadRelease => self.download_release().await, RadarrEvent::EditCollection => self.edit_collection().await, RadarrEvent::EditMovie => self.edit_movie().await, RadarrEvent::GetCollections => self.get_collections().await, RadarrEvent::GetDownloads => self.get_downloads().await, RadarrEvent::GetIndexers => self.get_indexers().await, RadarrEvent::GetIndexerSettings => self.get_indexer_settings().await, RadarrEvent::GetLogs => self.get_logs().await, RadarrEvent::GetMovieCredits => self.get_credits().await, RadarrEvent::GetMovieDetails => self.get_movie_details().await, RadarrEvent::GetMovieHistory => self.get_movie_history().await, RadarrEvent::GetMovies => self.get_movies().await, RadarrEvent::GetOverview => self.get_diskspace().await, RadarrEvent::GetQualityProfiles => self.get_quality_profiles().await, RadarrEvent::GetQueuedEvents => self.get_queued_events().await, RadarrEvent::GetReleases => self.get_releases().await, RadarrEvent::GetRootFolders => self.get_root_folders().await, RadarrEvent::GetStatus => self.get_status().await, RadarrEvent::GetTags => self.get_tags().await, RadarrEvent::GetTasks => self.get_tasks().await, RadarrEvent::GetUpdates => self.get_updates().await, RadarrEvent::HealthCheck => self.get_healthcheck().await, RadarrEvent::SearchNewMovie => self.search_movie().await, RadarrEvent::StartTask => self.start_task().await, RadarrEvent::TestAllIndexers => self.test_all_indexers().await, RadarrEvent::TriggerAutomaticSearch => self.trigger_automatic_search().await, RadarrEvent::UpdateAllMovies => self.update_all_movies().await, RadarrEvent::UpdateAndScan => self.update_and_scan().await, RadarrEvent::UpdateCollections => self.update_collections().await, RadarrEvent::UpdateDownloads => self.update_downloads().await, RadarrEvent::UpdateIndexerSettings => self.update_indexer_settings().await, } } async fn add_movie(&mut self) { info!("Adding new movie to Radarr"); let body = { let tags = self .app .lock() .await .data .radarr_data .add_movie_modal .as_ref() .unwrap() .tags .text .clone(); let tag_ids_vec = self.extract_and_add_tag_ids_vec(tags).await; let mut app = self.app.lock().await; let AddMovieModal { root_folder_list, monitor_list, minimum_availability_list, quality_profile_list, .. } = app.data.radarr_data.add_movie_modal.as_ref().unwrap(); let (tmdb_id, title) = if let Route::Radarr(active_radarr_block, _) = *app.get_current_route() { if active_radarr_block == ActiveRadarrBlock::CollectionDetails { let CollectionMovie { tmdb_id, title, .. } = app .data .radarr_data .collection_movies .current_selection() .clone(); (tmdb_id, title.text) } else { let AddMovieSearchResult { tmdb_id, title, .. } = app .data .radarr_data .add_searched_movies .as_ref() .unwrap() .current_selection() .clone(); (tmdb_id, title.text) } } else { let AddMovieSearchResult { tmdb_id, title, .. } = app .data .radarr_data .add_searched_movies .as_ref() .unwrap() .current_selection() .clone(); (tmdb_id, title.text) }; let quality_profile = quality_profile_list.current_selection(); let quality_profile_id = *app .data .radarr_data .quality_profile_map .iter() .filter(|(_, value)| *value == quality_profile) .map(|(key, _)| key) .next() .unwrap(); let path = root_folder_list.current_selection().path.clone(); let monitor = monitor_list.current_selection().to_string(); let minimum_availability = minimum_availability_list.current_selection().to_string(); app.data.radarr_data.add_movie_modal = None; AddMovieBody { tmdb_id, title, root_folder_path: path, minimum_availability, monitored: true, quality_profile_id, tags: tag_ids_vec, add_options: AddOptions { monitor, search_for_movie: true, }, } }; debug!("Add movie body: {body:?}"); let request_props = self .radarr_request_props_from( RadarrEvent::AddMovie.resource(), RequestMethod::Post, Some(body), ) .await; self .handle_request::(request_props, |_, _| ()) .await; } async fn add_root_folder(&mut self) { info!("Adding new root folder to Radarr"); let body = { let mut app = self.app.lock().await; let path = app .data .radarr_data .edit_root_folder .as_ref() .unwrap() .text .clone(); app.data.radarr_data.edit_root_folder = None; AddRootFolderBody { path } }; debug!("Add root folder body: {body:?}"); let request_props = self .radarr_request_props_from( RadarrEvent::AddRootFolder.resource(), RequestMethod::Post, Some(body), ) .await; self .handle_request::(request_props, |_, _| ()) .await; } async fn add_tag(&mut self, tag: String) { info!("Adding a new Radarr tag"); let request_props = self .radarr_request_props_from( RadarrEvent::GetTags.resource(), RequestMethod::Post, Some(json!({ "label": tag })), ) .await; self .handle_request::(request_props, |tag, mut app| { app.data.radarr_data.tags_map.insert(tag.id, tag.label); }) .await; } async fn delete_download(&mut self) { let download_id = self .app .lock() .await .data .radarr_data .downloads .current_selection() .id; info!("Deleting Radarr download for download with id: {download_id}"); let request_props = self .radarr_request_props_from( format!("{}/{download_id}", RadarrEvent::DeleteDownload.resource()).as_str(), RequestMethod::Delete, None::<()>, ) .await; self .handle_request::<(), ()>(request_props, |_, _| ()) .await; } async fn delete_indexer(&mut self) { let indexer_id = self .app .lock() .await .data .radarr_data .indexers .current_selection() .id; info!("Deleting Radarr indexer for indexer with id: {indexer_id}"); let request_props = self .radarr_request_props_from( format!("{}/{indexer_id}", RadarrEvent::DeleteIndexer.resource()).as_str(), RequestMethod::Delete, None::<()>, ) .await; self .handle_request::<(), ()>(request_props, |_, _| ()) .await; } async fn delete_movie(&mut self) { let (movie_id, tmdb_id) = self.extract_movie_id().await; let delete_files = self.app.lock().await.data.radarr_data.delete_movie_files; let add_import_exclusion = self.app.lock().await.data.radarr_data.add_list_exclusion; info!("Deleting Radarr movie with tmdb_id {tmdb_id} and Radarr id: {movie_id} with deleteFiles={delete_files} and addImportExclusion={add_import_exclusion}"); let request_props = self .radarr_request_props_from( format!( "{}/{movie_id}?deleteFiles={delete_files}&addImportExclusion={add_import_exclusion}", RadarrEvent::DeleteMovie.resource() ) .as_str(), RequestMethod::Delete, None::<()>, ) .await; self .handle_request::<(), ()>(request_props, |_, _| ()) .await; self .app .lock() .await .data .radarr_data .reset_delete_movie_preferences(); } async fn delete_root_folder(&mut self) { let root_folder_id = self .app .lock() .await .data .radarr_data .root_folders .current_selection() .id; info!("Deleting Radarr root folder for folder with id: {root_folder_id}"); let request_props = self .radarr_request_props_from( format!( "{}/{root_folder_id}", RadarrEvent::DeleteRootFolder.resource() ) .as_str(), RequestMethod::Delete, None::<()>, ) .await; self .handle_request::<(), ()>(request_props, |_, _| ()) .await; } async fn download_release(&mut self) { let (movie_id, _) = self.extract_movie_id().await; let (guid, title, indexer_id) = { let app = self.app.lock().await; let Release { guid, title, indexer_id, .. } = app .data .radarr_data .movie_details_modal .as_ref() .unwrap() .movie_releases .current_selection(); (guid.clone(), title.clone(), *indexer_id) }; info!("Downloading release: {title}"); let download_release_body = ReleaseDownloadBody { guid, indexer_id, movie_id, }; let request_props = self .radarr_request_props_from( RadarrEvent::DownloadRelease.resource(), RequestMethod::Post, Some(download_release_body), ) .await; self .handle_request::(request_props, |_, _| ()) .await; } async fn edit_collection(&mut self) { info!("Editing Radarr collection"); info!("Fetching collection details"); let collection_id = self.extract_collection_id().await; let request_props = self .radarr_request_props_from( format!("{}/{collection_id}", RadarrEvent::GetCollections.resource()).as_str(), RequestMethod::Get, None::<()>, ) .await; let mut response = String::new(); self .handle_request::<(), Value>(request_props, |detailed_collection_body, _| { response = detailed_collection_body.to_string() }) .await; info!("Constructing edit collection body"); let body = { let mut app = self.app.lock().await; let mut detailed_collection_body: Value = serde_json::from_str(&response).unwrap(); let EditCollectionModal { path, search_on_add, minimum_availability_list, monitored, quality_profile_list, } = app.data.radarr_data.edit_collection_modal.as_ref().unwrap(); let quality_profile = quality_profile_list.current_selection(); let quality_profile_id = *app .data .radarr_data .quality_profile_map .iter() .filter(|(_, value)| *value == quality_profile) .map(|(key, _)| key) .next() .unwrap(); let root_folder_path: String = path.text.clone(); let monitored = monitored.unwrap_or_default(); let search_on_add = search_on_add.unwrap_or_default(); let minimum_availability = minimum_availability_list.current_selection().to_string(); *detailed_collection_body.get_mut("monitored").unwrap() = json!(monitored); *detailed_collection_body .get_mut("minimumAvailability") .unwrap() = json!(minimum_availability); *detailed_collection_body .get_mut("qualityProfileId") .unwrap() = json!(quality_profile_id); *detailed_collection_body.get_mut("rootFolderPath").unwrap() = json!(root_folder_path); *detailed_collection_body.get_mut("searchOnAdd").unwrap() = json!(search_on_add); app.data.radarr_data.edit_collection_modal = None; detailed_collection_body }; debug!("Edit collection body: {body:?}"); let request_props = self .radarr_request_props_from( format!("{}/{collection_id}", RadarrEvent::EditCollection.resource()).as_str(), RequestMethod::Put, Some(body), ) .await; self .handle_request::(request_props, |_, _| ()) .await; } async fn edit_movie(&mut self) { info!("Editing Radarr movie"); let (movie_id, tmdb_id) = self.extract_movie_id().await; info!("Fetching movie details for movie with TMDB ID: {tmdb_id}"); let request_props = self .radarr_request_props_from( format!("{}/{movie_id}", RadarrEvent::GetMovieDetails.resource()).as_str(), RequestMethod::Get, None::<()>, ) .await; let mut response = String::new(); self .handle_request::<(), Value>(request_props, |detailed_movie_body, _| { response = detailed_movie_body.to_string() }) .await; info!("Constructing edit movie body"); let body = { let tags = self .app .lock() .await .data .radarr_data .edit_movie_modal .as_ref() .unwrap() .tags .text .clone(); let tag_ids_vec = self.extract_and_add_tag_ids_vec(tags).await; let mut app = self.app.lock().await; let mut detailed_movie_body: Value = serde_json::from_str(&response).unwrap(); let EditMovieModal { monitored, path, minimum_availability_list, quality_profile_list, .. } = app.data.radarr_data.edit_movie_modal.as_ref().unwrap(); let quality_profile = quality_profile_list.current_selection(); let quality_profile_id = *app .data .radarr_data .quality_profile_map .iter() .filter(|(_, value)| *value == quality_profile) .map(|(key, _)| key) .next() .unwrap(); *detailed_movie_body.get_mut("monitored").unwrap() = json!(monitored.unwrap_or_default()); *detailed_movie_body.get_mut("minimumAvailability").unwrap() = json!(minimum_availability_list.current_selection().to_string()); *detailed_movie_body.get_mut("qualityProfileId").unwrap() = json!(quality_profile_id); *detailed_movie_body.get_mut("path").unwrap() = json!(path.text.clone()); *detailed_movie_body.get_mut("tags").unwrap() = json!(tag_ids_vec); app.data.radarr_data.edit_movie_modal = None; detailed_movie_body }; debug!("Edit movie body: {body:?}"); let request_props = self .radarr_request_props_from( format!("{}/{movie_id}", RadarrEvent::EditMovie.resource()).as_str(), RequestMethod::Put, Some(body), ) .await; self .handle_request::(request_props, |_, _| ()) .await; } async fn get_collections(&mut self) { info!("Fetching Radarr collections"); let request_props = self .radarr_request_props_from( RadarrEvent::GetCollections.resource(), RequestMethod::Get, None::<()>, ) .await; self .handle_request::<(), Vec>(request_props, |collections_vec, mut app| { app.data.radarr_data.collections.set_items(collections_vec); }) .await; } async fn get_credits(&mut self) { info!("Fetching Radarr movie credits"); let request_uri = self .append_movie_id_param(RadarrEvent::GetMovieCredits.resource()) .await; let request_props = self .radarr_request_props_from(request_uri.as_str(), RequestMethod::Get, None::<()>) .await; self .handle_request::<(), Vec>(request_props, |credit_vec, mut app| { let cast_vec: Vec = credit_vec .iter() .cloned() .filter(|credit| credit.credit_type == CreditType::Cast) .collect(); let crew_vec: Vec = credit_vec .iter() .cloned() .filter(|credit| credit.credit_type == CreditType::Crew) .collect(); debug!("Assuming the movie_details_modal is already a Some and was created by the get_movie_details request"); app.data.radarr_data.movie_details_modal.as_mut().unwrap().movie_cast.set_items(cast_vec); app.data.radarr_data.movie_details_modal.as_mut().unwrap().movie_crew.set_items(crew_vec); }) .await; } async fn get_diskspace(&mut self) { info!("Fetching Radarr disk space"); let request_props = self .radarr_request_props_from( RadarrEvent::GetOverview.resource(), RequestMethod::Get, None::<()>, ) .await; self .handle_request::<(), Vec>(request_props, |disk_space_vec, mut app| { app.data.radarr_data.disk_space_vec = disk_space_vec; }) .await; } async fn get_downloads(&mut self) { info!("Fetching Radarr downloads"); let request_props = self .radarr_request_props_from( RadarrEvent::GetDownloads.resource(), RequestMethod::Get, None::<()>, ) .await; self .handle_request::<(), DownloadsResponse>(request_props, |queue_response, mut app| { app .data .radarr_data .downloads .set_items(queue_response.records); }) .await } async fn get_indexers(&mut self) { info!("Fetching Radarr indexers"); let request_props = self .radarr_request_props_from( RadarrEvent::GetIndexers.resource(), RequestMethod::Get, None::<()>, ) .await; self .handle_request::<(), Vec>(request_props, |indexers, mut app| { app.data.radarr_data.indexers.set_items(indexers); }) .await } async fn get_indexer_settings(&mut self) { info!("Fetching Radarr indexer settings"); let request_props = self .radarr_request_props_from( RadarrEvent::GetIndexerSettings.resource(), RequestMethod::Get, None::<()>, ) .await; self .handle_request::<(), IndexerSettings>(request_props, |indexer_settings, mut app| { if app.data.radarr_data.indexer_settings.is_none() { app.data.radarr_data.indexer_settings = Some(indexer_settings); } else { debug!("Indexer Settings are being modified. Ignoring update..."); } }) .await; } async fn get_healthcheck(&mut self) { info!("Performing Radarr health check"); let request_props = self .radarr_request_props_from( RadarrEvent::HealthCheck.resource(), RequestMethod::Get, None::<()>, ) .await; self .handle_request::<(), ()>(request_props, |_, _| ()) .await; } async fn get_logs(&mut self) { info!("Fetching Radarr logs"); let resource = format!( "{}?pageSize=500&sortDirection=descending&sortKey=time", RadarrEvent::GetLogs.resource() ); let request_props = self .radarr_request_props_from(&resource, RequestMethod::Get, None::<()>) .await; self .handle_request::<(), LogResponse>(request_props, |log_response, mut app| { let mut logs = log_response.records; logs.reverse(); let log_lines = logs .into_iter() .map(|log| { if log.exception.is_some() { HorizontallyScrollableText::from(format!( "{}|{}|{}|{}|{}", log.time, log.level.to_uppercase(), log.logger.as_ref().unwrap(), log.exception_type.as_ref().unwrap(), log.exception.as_ref().unwrap() )) } else { HorizontallyScrollableText::from(format!( "{}|{}|{}|{}", log.time, log.level.to_uppercase(), log.logger.as_ref().unwrap(), log.message.as_ref().unwrap() )) } }) .collect(); app.data.radarr_data.logs.set_items(log_lines); app.data.radarr_data.logs.scroll_to_bottom(); }) .await; } async fn get_movie_details(&mut self) { info!("Fetching Radarr movie details"); let (movie_id, tmdb_id) = self.extract_movie_id().await; info!("Fetching movie details for movie with TMDB ID: {tmdb_id}"); let request_props = self .radarr_request_props_from( format!("{}/{movie_id}", RadarrEvent::GetMovieDetails.resource()).as_str(), RequestMethod::Get, None::<()>, ) .await; self .handle_request::<(), Movie>(request_props, |movie_response, mut app| { let Movie { id, title, year, overview, path, studio, has_file, quality_profile_id, size_on_disk, genres, runtime, certification, ratings, movie_file, collection, .. } = movie_response; let (hours, minutes) = convert_runtime(runtime); let size = convert_to_gb(size_on_disk); let quality_profile = app .data .radarr_data .quality_profile_map .get_by_left(&quality_profile_id) .unwrap() .to_owned(); let imdb_rating = if let Some(rating) = ratings.imdb { if let Some(value) = rating.value.as_f64() { format!("{:.1}", value) } else { String::new() } } else { String::new() }; let tmdb_rating = if let Some(rating) = ratings.tmdb { if let Some(value) = rating.value.as_f64() { format!("{}%", (value * 10f64).ceil()) } else { String::new() } } else { String::new() }; let rotten_tomatoes_rating = if let Some(rating) = ratings.rotten_tomatoes { if let Some(value) = rating.value.as_u64() { format!("{}%", value) } else { String::new() } } else { String::new() }; let status = get_movie_status(has_file, &app.data.radarr_data.downloads.items, id); let collection = collection.unwrap_or_default(); let mut movie_details_modal = MovieDetailsModal { movie_details: ScrollableText::with_string(formatdoc!( "Title: {title} Year: {year} Runtime: {hours}h {minutes}m Rating: {} Collection: {} Status: {status} Description: {overview} TMDB: {tmdb_rating} IMDB: {imdb_rating} Rotten Tomatoes: {rotten_tomatoes_rating} Quality Profile: {quality_profile} Size: {size:.2} GB Path: {path} Studio: {studio} Genres: {}", certification.unwrap_or_default(), collection.title, genres.join(", ") )), ..MovieDetailsModal::default() }; if let Some(file) = movie_file { movie_details_modal.file_details = formatdoc!( "Relative Path: {} Absolute Path: {} Size: {size:.2} GB Date Added: {}", file.relative_path, file.path, file.date_added ); if let Some(media_info) = file.media_info { movie_details_modal.audio_details = formatdoc!( "Bitrate: {} Channels: {:.1} Codec: {} Languages: {} Stream Count: {}", media_info.audio_bitrate, media_info.audio_channels.as_f64().unwrap(), media_info.audio_codec.unwrap_or_default(), media_info.audio_languages.unwrap_or_default(), media_info.audio_stream_count ); movie_details_modal.video_details = formatdoc!( "Bit Depth: {} Bitrate: {} Codec: {} FPS: {} Resolution: {} Scan Type: {} Runtime: {}", media_info.video_bit_depth, media_info.video_bitrate, media_info.video_codec, media_info.video_fps.as_f64().unwrap(), media_info.resolution, media_info.scan_type, media_info.run_time ); } } app.data.radarr_data.movie_details_modal = Some(movie_details_modal); }) .await; } async fn get_movie_history(&mut self) { info!("Fetching Radarr movie history"); let request_uri = self .append_movie_id_param(RadarrEvent::GetMovieHistory.resource()) .await; let request_props = self .radarr_request_props_from(request_uri.as_str(), RequestMethod::Get, None::<()>) .await; self .handle_request::<(), Vec>(request_props, |movie_history_vec, mut app| { let mut reversed_movie_history_vec = movie_history_vec.to_vec(); reversed_movie_history_vec.reverse(); if app.data.radarr_data.movie_details_modal.is_none() { app.data.radarr_data.movie_details_modal = Some(MovieDetailsModal::default()) } app .data .radarr_data .movie_details_modal .as_mut() .unwrap() .movie_history .set_items(reversed_movie_history_vec) }) .await; } async fn get_movies(&mut self) { info!("Fetching Radarr library"); let request_props = self .radarr_request_props_from( RadarrEvent::GetMovies.resource(), RequestMethod::Get, None::<()>, ) .await; self .handle_request::<(), Vec>(request_props, |movie_vec, mut app| { app.data.radarr_data.movies.set_items(movie_vec) }) .await; } async fn get_quality_profiles(&mut self) { info!("Fetching Radarr quality profiles"); let request_props = self .radarr_request_props_from( RadarrEvent::GetQualityProfiles.resource(), RequestMethod::Get, None::<()>, ) .await; self .handle_request::<(), Vec>(request_props, |quality_profiles, mut app| { app.data.radarr_data.quality_profile_map = quality_profiles .into_iter() .map(|profile| (profile.id, profile.name)) .collect(); }) .await; } async fn get_queued_events(&mut self) { info!("Fetching Radarr queued events"); let request_props = self .radarr_request_props_from( RadarrEvent::GetQueuedEvents.resource(), RequestMethod::Get, None::<()>, ) .await; self .handle_request::<(), Vec>(request_props, |queued_events_vec, mut app| { app .data .radarr_data .queued_events .set_items(queued_events_vec); }) .await; } async fn get_releases(&mut self) { let (movie_id, tmdb_id) = self.extract_movie_id().await; info!("Fetching releases for movie with TMDB id {tmdb_id} and with Radarr id: {movie_id}"); let request_props = self .radarr_request_props_from( format!("{}?movieId={movie_id}", RadarrEvent::GetReleases.resource()).as_str(), RequestMethod::Get, None::<()>, ) .await; self .handle_request::<(), Vec>(request_props, |release_vec, mut app| { debug!("Assuming the movie_details_modal is already a Some and was created by the get_movie_details request"); app.data.radarr_data.movie_details_modal.as_mut().unwrap().movie_releases.set_items(release_vec); }) .await; } async fn get_root_folders(&mut self) { info!("Fetching Radarr root folders"); let request_props = self .radarr_request_props_from( RadarrEvent::GetRootFolders.resource(), RequestMethod::Get, None::<()>, ) .await; self .handle_request::<(), Vec>(request_props, |root_folders, mut app| { app.data.radarr_data.root_folders.set_items(root_folders); }) .await; } async fn get_status(&mut self) { info!("Fetching Radarr system status"); let request_props = self .radarr_request_props_from( RadarrEvent::GetStatus.resource(), RequestMethod::Get, None::<()>, ) .await; self .handle_request::<(), SystemStatus>(request_props, |system_status, mut app| { app.data.radarr_data.version = system_status.version; app.data.radarr_data.start_time = system_status.start_time; }) .await; } async fn get_tags(&mut self) { info!("Fetching Radarr tags"); let request_props = self .radarr_request_props_from( RadarrEvent::GetTags.resource(), RequestMethod::Get, None::<()>, ) .await; self .handle_request::<(), Vec>(request_props, |tags_vec, mut app| { app.data.radarr_data.tags_map = tags_vec .into_iter() .map(|tag| (tag.id, tag.label)) .collect(); }) .await; } async fn get_tasks(&mut self) { info!("Fetching Radarr tasks"); let request_props = self .radarr_request_props_from( RadarrEvent::GetTasks.resource(), RequestMethod::Get, None::<()>, ) .await; self .handle_request::<(), Vec>(request_props, |tasks_vec, mut app| { app.data.radarr_data.tasks.set_items(tasks_vec); }) .await; } async fn get_updates(&mut self) { info!("Fetching Radarr updates"); let request_props = self .radarr_request_props_from( RadarrEvent::GetUpdates.resource(), RequestMethod::Get, None::<()>, ) .await; self .handle_request::<(), Vec>(request_props, |updates_vec, mut app| { let latest_installed = if updates_vec .iter() .any(|update| update.latest && update.installed_on.is_some()) { "already".to_owned() } else { "not".to_owned() }; let updates = updates_vec .into_iter() .map(|update| { let install_status = if update.installed_on.is_some() { if update.installed { "(Currently Installed)".to_owned() } else { "(Previously Installed)".to_owned() } } else { String::new() }; let vec_to_bullet_points = |vec: Vec| { vec .iter() .map(|change| format!(" * {change}")) .collect::>() .join("\n") }; let mut update_info = formatdoc!( "{} - {} {install_status} {}", update.version, update.release_date, "-".repeat(200) ); if let Some(new_changes) = update.changes.new { let changes = vec_to_bullet_points(new_changes); update_info = formatdoc!( "{update_info} New: {changes}" ) } if let Some(fixes) = update.changes.fixed { let fixes = vec_to_bullet_points(fixes); update_info = formatdoc!( "{update_info} Fixed: {fixes}" ); } update_info }) .reduce(|version_1, version_2| format!("{version_1}\n\n\n{version_2}")) .unwrap(); app.data.radarr_data.updates = ScrollableText::with_string(formatdoc!( "The latest version of Radarr is {latest_installed} installed {updates}" )); }) .await; } async fn search_movie(&mut self) { info!("Searching for specific Radarr movie"); let search = self .app .lock() .await .data .radarr_data .search .clone() .ok_or(anyhow!("Encountered a race condition")); match search { Ok(search_string) => { let request_props = self .radarr_request_props_from( format!( "{}?term={}", RadarrEvent::SearchNewMovie.resource(), encode(&search_string.text) ) .as_str(), RequestMethod::Get, None::<()>, ) .await; self .handle_request::<(), Vec>(request_props, |movie_vec, mut app| { if movie_vec.is_empty() { app.pop_and_push_navigation_stack( ActiveRadarrBlock::AddMovieEmptySearchResults.into(), ); } else if let Some(add_searched_movies) = app.data.radarr_data.add_searched_movies.as_mut() { add_searched_movies.set_items(movie_vec); } else { let mut add_searched_movies = StatefulTable::default(); add_searched_movies.set_items(movie_vec); app.data.radarr_data.add_searched_movies = Some(add_searched_movies); } }) .await; } Err(e) => { warn!( "Encountered a race condition: {e}\n \ This is most likely caused by the user trying to navigate between modals rapidly. \ Ignoring search request." ); } } } async fn start_task(&mut self) { let task_name = self .app .lock() .await .data .radarr_data .tasks .current_selection() .task_name .clone(); info!("Starting Radarr task: {task_name}"); let body = CommandBody { name: task_name }; let request_props = self .radarr_request_props_from( RadarrEvent::StartTask.resource(), RequestMethod::Post, Some(body), ) .await; self .handle_request::(request_props, |_, _| ()) .await; } async fn test_all_indexers(&mut self) { info!("Testing all indexers"); let mut request_props = self .radarr_request_props_from( RadarrEvent::TestAllIndexers.resource(), RequestMethod::Post, None, ) .await; request_props.ignore_status_code = true; self .handle_request::<(), Vec>(request_props, |test_results, mut app| { let mut test_all_indexer_results = StatefulTable::default(); let indexers = app.data.radarr_data.indexers.items.clone(); let modal_test_results = test_results .iter() .map(|result| { let name = indexers .iter() .filter(|&indexer| indexer.id == result.id) .map(|indexer| indexer.name.clone()) .nth(0) .unwrap_or_default(); let validation_failures = result .validation_failures .iter() .map(|failure| { format!( "Failure for field '{}': {}", failure.property_name, failure.error_message ) }) .collect::>() .join(", "); IndexerTestResultModalItem { name: name.unwrap_or_default(), is_valid: result.is_valid, validation_failures: validation_failures.into(), } }) .collect(); test_all_indexer_results.set_items(modal_test_results); app.data.radarr_data.indexer_test_all_results = Some(test_all_indexer_results); }) .await; } async fn trigger_automatic_search(&mut self) { let (movie_id, tmdb_id) = self.extract_movie_id().await; info!("Searching indexers for movie with TMDB id {tmdb_id} and with Radarr id: {movie_id}"); let body = MovieCommandBody { name: "MoviesSearch".to_owned(), movie_ids: vec![movie_id], }; let request_props = self .radarr_request_props_from( RadarrEvent::TriggerAutomaticSearch.resource(), RequestMethod::Post, Some(body), ) .await; self .handle_request::(request_props, |_, _| ()) .await; } async fn update_all_movies(&mut self) { info!("Updating all movies"); let body = MovieCommandBody { name: "RefreshMovie".to_owned(), movie_ids: Vec::new(), }; let request_props = self .radarr_request_props_from( RadarrEvent::UpdateAllMovies.resource(), RequestMethod::Post, Some(body), ) .await; self .handle_request::(request_props, |_, _| ()) .await; } async fn update_and_scan(&mut self) { let (movie_id, tmdb_id) = self.extract_movie_id().await; info!("Updating and scanning movie with TMDB id {tmdb_id} and with Radarr id: {movie_id}"); let body = MovieCommandBody { name: "RefreshMovie".to_owned(), movie_ids: vec![movie_id], }; let request_props = self .radarr_request_props_from( RadarrEvent::UpdateAndScan.resource(), RequestMethod::Post, Some(body), ) .await; self .handle_request::(request_props, |_, _| ()) .await; } async fn update_collections(&mut self) { info!("Updating collections"); let body = CommandBody { name: "RefreshCollections".to_owned(), }; let request_props = self .radarr_request_props_from( RadarrEvent::UpdateCollections.resource(), RequestMethod::Post, Some(body), ) .await; self .handle_request::(request_props, |_, _| ()) .await; } async fn update_downloads(&mut self) { info!("Updating downloads"); let body = CommandBody { name: "RefreshMonitoredDownloads".to_owned(), }; let request_props = self .radarr_request_props_from( RadarrEvent::UpdateDownloads.resource(), RequestMethod::Post, Some(body), ) .await; self .handle_request::(request_props, |_, _| ()) .await; } async fn update_indexer_settings(&mut self) { info!("Updating Radarr indexer settings"); let body = self .app .lock() .await .data .radarr_data .indexer_settings .as_ref() .unwrap() .clone(); debug!("Indexer settings body: {body:?}"); let request_props = self .radarr_request_props_from( RadarrEvent::UpdateIndexerSettings.resource(), RequestMethod::Put, Some(body), ) .await; self .handle_request::(request_props, |_, _| {}) .await; self.app.lock().await.data.radarr_data.indexer_settings = None; } async fn radarr_request_props_from( &self, resource: &str, method: RequestMethod, body: Option, ) -> RequestProps { let app = self.app.lock().await; let RadarrConfig { host, port, api_token, } = &app.config.radarr; let uri = format!("http://{host}:{}/api/v3{resource}", port.unwrap_or(7878)); RequestProps { uri, method, body, api_token: api_token.to_owned(), ignore_status_code: false, } } async fn extract_and_add_tag_ids_vec(&mut self, edit_tags: String) -> Vec { let tags_map = self.app.lock().await.data.radarr_data.tags_map.clone(); let tags = edit_tags.clone(); let missing_tags_vec = edit_tags .split(',') .filter(|&tag| !tag.is_empty() && tags_map.get_by_right(tag.trim()).is_none()) .collect::>(); for tag in missing_tags_vec { self.add_tag(tag.trim().to_owned()).await; } let app = self.app.lock().await; tags .split(',') .filter(|tag| !tag.is_empty()) .map(|tag| { *app .data .radarr_data .tags_map .get_by_right(tag.trim()) .unwrap() }) .collect() } async fn extract_movie_id(&mut self) -> (i64, i64) { let app = self.app.lock().await; if app.data.radarr_data.filtered_movies.is_some() { ( app .data .radarr_data .filtered_movies .as_ref() .unwrap() .current_selection() .id, app .data .radarr_data .filtered_movies .as_ref() .unwrap() .current_selection() .tmdb_id, ) } else { ( app.data.radarr_data.movies.current_selection().id, app.data.radarr_data.movies.current_selection().tmdb_id, ) } } async fn extract_collection_id(&mut self) -> i64 { if self .app .lock() .await .data .radarr_data .filtered_collections .is_some() { self .app .lock() .await .data .radarr_data .filtered_collections .as_ref() .unwrap() .current_selection() .id } else { self .app .lock() .await .data .radarr_data .collections .current_selection() .id } } async fn append_movie_id_param(&mut self, resource: &str) -> String { let (movie_id, _) = self.extract_movie_id().await; format!("{resource}?movieId={movie_id}") } } fn get_movie_status(has_file: bool, downloads_vec: &[DownloadRecord], movie_id: i64) -> String { if !has_file { if let Some(download) = downloads_vec .iter() .find(|&download| download.movie_id == movie_id) { if download.status == "downloading" { return "Downloading".to_owned(); } if download.status == "completed" { return "Awaiting Import".to_owned(); } } return "Missing".to_owned(); } "Downloaded".to_owned() }