Files
managarr/src/network/radarr_network.rs

1955 lines
60 KiB
Rust

use anyhow::{anyhow, Result};
use std::fmt::Debug;
use indoc::formatdoc;
use log::{debug, info, warn};
use serde_json::{json, Value};
use urlencoding::encode;
use crate::models::radarr_models::{
AddMovieBody, AddMovieSearchResult, BlocklistResponse, Collection,
Credit, CreditType, DeleteMovieParams, DownloadRecord, DownloadsResponse,
EditCollectionParams, EditMovieParams, IndexerSettings, IndexerTestResult, Movie,
MovieCommandBody, MovieHistoryItem, RadarrRelease, RadarrReleaseDownloadBody, RadarrSerdeable,
RadarrTask, RadarrTaskName, SystemStatus,
};
use crate::models::servarr_data::modals::IndexerTestResultModalItem;
use crate::models::servarr_data::radarr::modals::MovieDetailsModal;
use crate::models::servarr_data::radarr::radarr_data::ActiveRadarrBlock;
use crate::models::servarr_models::{
AddRootFolderBody, CommandBody, DiskSpace, EditIndexerParams, HostConfig, Indexer, LogResponse,
QualityProfile, QueueEvent, RootFolder, SecurityConfig, Tag, Update,
};
use crate::models::stateful_table::StatefulTable;
use crate::models::{HorizontallyScrollableText, Route, Scrollable, ScrollableText};
use crate::network::{Network, NetworkEvent, RequestMethod};
use crate::utils::{convert_runtime, convert_to_gb};
use super::NetworkResource;
#[cfg(test)]
#[path = "radarr_network_tests.rs"]
mod radarr_network_tests;
#[derive(Debug, Eq, PartialEq, Clone)]
pub enum RadarrEvent {
AddMovie(AddMovieBody),
AddRootFolder(AddRootFolderBody),
AddTag(String),
ClearBlocklist,
DeleteBlocklistItem(i64),
DeleteDownload(i64),
DeleteIndexer(i64),
DeleteMovie(DeleteMovieParams),
DeleteRootFolder(i64),
DeleteTag(i64),
DownloadRelease(RadarrReleaseDownloadBody),
EditAllIndexerSettings(IndexerSettings),
EditCollection(EditCollectionParams),
EditIndexer(EditIndexerParams),
EditMovie(EditMovieParams),
GetBlocklist,
GetCollections,
GetDownloads,
GetHostConfig,
GetIndexers,
GetAllIndexerSettings,
GetLogs(u64),
GetMovieCredits(i64),
GetMovieDetails(Option<i64>),
GetMovieHistory(Option<i64>),
GetMovies,
GetDiskSpace,
GetQualityProfiles,
GetQueuedEvents,
GetReleases(Option<i64>),
GetRootFolders,
GetSecurityConfig,
GetStatus,
GetTags,
GetTasks,
GetUpdates,
HealthCheck,
SearchNewMovie(Option<String>),
StartTask(Option<RadarrTaskName>),
TestIndexer(Option<i64>),
TestAllIndexers,
TriggerAutomaticSearch(Option<i64>),
UpdateAllMovies,
UpdateAndScan(Option<i64>),
UpdateCollections,
UpdateDownloads,
}
impl NetworkResource for RadarrEvent {
fn resource(&self) -> &'static str {
match &self {
RadarrEvent::ClearBlocklist => "/blocklist/bulk",
RadarrEvent::DeleteBlocklistItem(_) => "/blocklist",
RadarrEvent::GetBlocklist => "/blocklist?page=1&pageSize=10000",
RadarrEvent::GetCollections | RadarrEvent::EditCollection(_) => "/collection",
RadarrEvent::GetDownloads | RadarrEvent::DeleteDownload(_) => "/queue",
RadarrEvent::GetHostConfig | RadarrEvent::GetSecurityConfig => "/config/host",
RadarrEvent::GetIndexers | RadarrEvent::EditIndexer(_) | RadarrEvent::DeleteIndexer(_) => {
"/indexer"
}
RadarrEvent::GetAllIndexerSettings | RadarrEvent::EditAllIndexerSettings(_) => {
"/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::GetDiskSpace => "/diskspace",
RadarrEvent::GetQualityProfiles => "/qualityprofile",
RadarrEvent::GetReleases(_) | RadarrEvent::DownloadRelease(_) => "/release",
RadarrEvent::AddRootFolder(_)
| RadarrEvent::GetRootFolders
| RadarrEvent::DeleteRootFolder(_) => "/rootfolder",
RadarrEvent::GetStatus => "/system/status",
RadarrEvent::GetTags | RadarrEvent::AddTag(_) | RadarrEvent::DeleteTag(_) => "/tag",
RadarrEvent::GetTasks => "/system/task",
RadarrEvent::GetUpdates => "/update",
RadarrEvent::TestIndexer(_) => "/indexer/test",
RadarrEvent::TestAllIndexers => "/indexer/testall",
RadarrEvent::StartTask(_)
| RadarrEvent::GetQueuedEvents
| RadarrEvent::TriggerAutomaticSearch(_)
| RadarrEvent::UpdateAndScan(_)
| RadarrEvent::UpdateAllMovies
| RadarrEvent::UpdateDownloads
| RadarrEvent::UpdateCollections => "/command",
RadarrEvent::HealthCheck => "/health",
}
}
}
impl From<RadarrEvent> 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,
) -> Result<RadarrSerdeable> {
match radarr_event {
RadarrEvent::AddMovie(body) => self.add_movie(body).await.map(RadarrSerdeable::from),
RadarrEvent::AddRootFolder(path) => self
.add_radarr_root_folder(path)
.await
.map(RadarrSerdeable::from),
RadarrEvent::AddTag(tag) => self.add_radarr_tag(tag).await.map(RadarrSerdeable::from),
RadarrEvent::ClearBlocklist => self
.clear_radarr_blocklist()
.await
.map(RadarrSerdeable::from),
RadarrEvent::DeleteBlocklistItem(blocklist_item_id) => self
.delete_radarr_blocklist_item(blocklist_item_id)
.await
.map(RadarrSerdeable::from),
RadarrEvent::DeleteDownload(download_id) => self
.delete_radarr_download(download_id)
.await
.map(RadarrSerdeable::from),
RadarrEvent::DeleteIndexer(indexer_id) => self
.delete_radarr_indexer(indexer_id)
.await
.map(RadarrSerdeable::from),
RadarrEvent::DeleteMovie(params) => {
self.delete_movie(params).await.map(RadarrSerdeable::from)
}
RadarrEvent::DeleteRootFolder(root_folder_id) => self
.delete_radarr_root_folder(root_folder_id)
.await
.map(RadarrSerdeable::from),
RadarrEvent::DeleteTag(tag_id) => self
.delete_radarr_tag(tag_id)
.await
.map(RadarrSerdeable::from),
RadarrEvent::DownloadRelease(params) => self
.download_radarr_release(params)
.await
.map(RadarrSerdeable::from),
RadarrEvent::EditAllIndexerSettings(params) => self
.edit_all_radarr_indexer_settings(params)
.await
.map(RadarrSerdeable::from),
RadarrEvent::EditCollection(params) => self
.edit_collection(params)
.await
.map(RadarrSerdeable::from),
RadarrEvent::EditIndexer(params) => self
.edit_radarr_indexer(params)
.await
.map(RadarrSerdeable::from),
RadarrEvent::EditMovie(params) => self.edit_movie(params).await.map(RadarrSerdeable::from),
RadarrEvent::GetAllIndexerSettings => self
.get_all_radarr_indexer_settings()
.await
.map(RadarrSerdeable::from),
RadarrEvent::GetBlocklist => self.get_radarr_blocklist().await.map(RadarrSerdeable::from),
RadarrEvent::GetCollections => self.get_collections().await.map(RadarrSerdeable::from),
RadarrEvent::GetDownloads => self.get_radarr_downloads().await.map(RadarrSerdeable::from),
RadarrEvent::GetHostConfig => self
.get_radarr_host_config()
.await
.map(RadarrSerdeable::from),
RadarrEvent::GetIndexers => self.get_radarr_indexers().await.map(RadarrSerdeable::from),
RadarrEvent::GetLogs(events) => self
.get_radarr_logs(events)
.await
.map(RadarrSerdeable::from),
RadarrEvent::GetMovieCredits(movie_id) => {
self.get_credits(movie_id).await.map(RadarrSerdeable::from)
}
RadarrEvent::GetMovieDetails(movie_id) => self
.get_movie_details(movie_id)
.await
.map(RadarrSerdeable::from),
RadarrEvent::GetMovieHistory(movie_id) => self
.get_movie_history(movie_id)
.await
.map(RadarrSerdeable::from),
RadarrEvent::GetMovies => self.get_movies().await.map(RadarrSerdeable::from),
RadarrEvent::GetDiskSpace => self.get_radarr_diskspace().await.map(RadarrSerdeable::from),
RadarrEvent::GetQualityProfiles => self
.get_radarr_quality_profiles()
.await
.map(RadarrSerdeable::from),
RadarrEvent::GetQueuedEvents => self
.get_queued_radarr_events()
.await
.map(RadarrSerdeable::from),
RadarrEvent::GetReleases(movie_id) => self
.get_movie_releases(movie_id)
.await
.map(RadarrSerdeable::from),
RadarrEvent::GetRootFolders => self
.get_radarr_root_folders()
.await
.map(RadarrSerdeable::from),
RadarrEvent::GetSecurityConfig => self
.get_radarr_security_config()
.await
.map(RadarrSerdeable::from),
RadarrEvent::GetStatus => self.get_radarr_status().await.map(RadarrSerdeable::from),
RadarrEvent::GetTags => self.get_radarr_tags().await.map(RadarrSerdeable::from),
RadarrEvent::GetTasks => self.get_radarr_tasks().await.map(RadarrSerdeable::from),
RadarrEvent::GetUpdates => self.get_radarr_updates().await.map(RadarrSerdeable::from),
RadarrEvent::HealthCheck => self
.get_radarr_healthcheck()
.await
.map(RadarrSerdeable::from),
RadarrEvent::SearchNewMovie(query) => {
self.search_movie(query).await.map(RadarrSerdeable::from)
}
RadarrEvent::StartTask(task_name) => self
.start_radarr_task(task_name)
.await
.map(RadarrSerdeable::from),
RadarrEvent::TestIndexer(indexer_id) => self
.test_radarr_indexer(indexer_id)
.await
.map(RadarrSerdeable::from),
RadarrEvent::TestAllIndexers => self
.test_all_radarr_indexers()
.await
.map(RadarrSerdeable::from),
RadarrEvent::TriggerAutomaticSearch(movie_id) => self
.trigger_automatic_movie_search(movie_id)
.await
.map(RadarrSerdeable::from),
RadarrEvent::UpdateAllMovies => self.update_all_movies().await.map(RadarrSerdeable::from),
RadarrEvent::UpdateAndScan(movie_id) => self
.update_and_scan_movie(movie_id)
.await
.map(RadarrSerdeable::from),
RadarrEvent::UpdateCollections => self.update_collections().await.map(RadarrSerdeable::from),
RadarrEvent::UpdateDownloads => self
.update_radarr_downloads()
.await
.map(RadarrSerdeable::from),
}
}
async fn add_movie(&mut self, mut add_movie_body: AddMovieBody) -> Result<Value> {
info!("Adding new movie to Radarr");
let event = RadarrEvent::AddMovie(add_movie_body.clone());
if let Some(tag_input_string) = add_movie_body.tag_input_string.as_ref() {
let tag_ids_vec = self
.extract_and_add_radarr_tag_ids_vec(tag_input_string.clone())
.await;
add_movie_body.tags = tag_ids_vec;
}
debug!("Add movie body: {add_movie_body:?}");
let request_props = self
.request_props_from(event, RequestMethod::Post, Some(add_movie_body), None, None)
.await;
self
.handle_request::<AddMovieBody, Value>(request_props, |_, _| ())
.await
}
async fn add_radarr_root_folder(&mut self, add_root_folder_body: AddRootFolderBody) -> Result<Value> {
info!("Adding new root folder to Radarr");
let event = RadarrEvent::AddRootFolder(add_root_folder_body.clone());
debug!("Add root folder body: {add_root_folder_body:?}");
let request_props = self
.request_props_from(event, RequestMethod::Post, Some(add_root_folder_body), None, None)
.await;
self
.handle_request::<AddRootFolderBody, Value>(request_props, |_, _| ())
.await
}
async fn add_radarr_tag(&mut self, tag: String) -> Result<Tag> {
info!("Adding a new Radarr tag");
let event = RadarrEvent::AddTag(String::new());
let request_props = self
.request_props_from(
event,
RequestMethod::Post,
Some(json!({ "label": tag })),
None,
None,
)
.await;
self
.handle_request::<Value, Tag>(request_props, |tag, mut app| {
app.data.radarr_data.tags_map.insert(tag.id, tag.label);
})
.await
}
async fn delete_radarr_tag(&mut self, id: i64) -> Result<()> {
info!("Deleting Radarr tag with id: {id}");
let event = RadarrEvent::DeleteTag(id);
let request_props = self
.request_props_from(
event,
RequestMethod::Delete,
None::<()>,
Some(format!("/{id}")),
None,
)
.await;
self
.handle_request::<(), ()>(request_props, |_, _| ())
.await
}
async fn clear_radarr_blocklist(&mut self) -> Result<()> {
info!("Clearing Radarr blocklist");
let event = RadarrEvent::ClearBlocklist;
let ids = self
.app
.lock()
.await
.data
.radarr_data
.blocklist
.items
.iter()
.map(|item| item.id)
.collect::<Vec<i64>>();
let request_props = self
.request_props_from(
event,
RequestMethod::Delete,
Some(json!({"ids": ids})),
None,
None,
)
.await;
self
.handle_request::<Value, ()>(request_props, |_, _| ())
.await
}
async fn delete_radarr_blocklist_item(&mut self, blocklist_item_id: i64) -> Result<()> {
let event = RadarrEvent::DeleteBlocklistItem(blocklist_item_id);
info!("Deleting Radarr blocklist item for item with id: {blocklist_item_id}");
let request_props = self
.request_props_from(
event,
RequestMethod::Delete,
None::<()>,
Some(format!("/{blocklist_item_id}")),
None,
)
.await;
self
.handle_request::<(), ()>(request_props, |_, _| ())
.await
}
async fn delete_radarr_download(&mut self, download_id: i64) -> Result<()> {
let event = RadarrEvent::DeleteDownload(download_id);
info!("Deleting Radarr download for download with id: {download_id}");
let request_props = self
.request_props_from(
event,
RequestMethod::Delete,
None::<()>,
Some(format!("/{download_id}")),
None,
)
.await;
self
.handle_request::<(), ()>(request_props, |_, _| ())
.await
}
async fn delete_radarr_indexer(&mut self, indexer_id: i64) -> Result<()> {
let event = RadarrEvent::DeleteIndexer(indexer_id);
info!("Deleting Radarr indexer for indexer with id: {indexer_id}");
let request_props = self
.request_props_from(
event,
RequestMethod::Delete,
None::<()>,
Some(format!("/{indexer_id}")),
None,
)
.await;
self
.handle_request::<(), ()>(request_props, |_, _| ())
.await
}
async fn delete_movie(&mut self, delete_movie_params: DeleteMovieParams) -> Result<()> {
let event = RadarrEvent::DeleteMovie(delete_movie_params.clone());
let DeleteMovieParams { id, delete_movie_files, add_list_exclusion } = delete_movie_params;
info!("Deleting Radarr movie with ID: {id} with deleteFiles={delete_movie_files} and addImportExclusion={add_list_exclusion}");
let request_props = self
.request_props_from(
event,
RequestMethod::Delete,
None::<()>,
Some(format!("/{id}")),
Some(format!(
"deleteFiles={delete_movie_files}&addImportExclusion={add_list_exclusion}"
)),
)
.await;
self
.handle_request::<(), ()>(request_props, |_, _| ())
.await
}
async fn delete_radarr_root_folder(&mut self, root_folder_id: i64) -> Result<()> {
let event = RadarrEvent::DeleteRootFolder(root_folder_id);
info!("Deleting Radarr root folder for folder with id: {root_folder_id}");
let request_props = self
.request_props_from(
event,
RequestMethod::Delete,
None::<()>,
Some(format!("/{root_folder_id}")),
None,
)
.await;
self
.handle_request::<(), ()>(request_props, |_, _| ())
.await
}
async fn download_radarr_release(
&mut self,
params: RadarrReleaseDownloadBody,
) -> Result<Value> {
let event = RadarrEvent::DownloadRelease(params.clone());
info!("Downloading Radarr release with params: {params:?}");
let request_props = self
.request_props_from(event, RequestMethod::Post, Some(params), None, None)
.await;
self
.handle_request::<RadarrReleaseDownloadBody, Value>(request_props, |_, _| ())
.await
}
async fn edit_all_radarr_indexer_settings(
&mut self,
params: IndexerSettings,
) -> Result<Value> {
info!("Updating Radarr indexer settings");
let event = RadarrEvent::EditAllIndexerSettings(params.clone());
debug!("Indexer settings body: {params:?}");
let request_props = self
.request_props_from(event, RequestMethod::Put, Some(params), None, None)
.await;
self
.handle_request::<IndexerSettings, Value>(request_props, |_, _| {})
.await
}
async fn edit_collection(
&mut self,
edit_collection_params: EditCollectionParams,
) -> Result<()> {
info!("Editing Radarr collection");
let detail_event = RadarrEvent::GetCollections;
let event = RadarrEvent::EditCollection(edit_collection_params.clone());
info!("Fetching collection details");
let collection_id = edit_collection_params.collection_id;
let request_props = self
.request_props_from(
detail_event,
RequestMethod::Get,
None::<()>,
Some(format!("/{collection_id}")),
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 mut detailed_collection_body: Value = serde_json::from_str(&response)?;
let (monitored, minimum_availability, quality_profile_id, root_folder_path, search_on_add) = {
let monitored = edit_collection_params.monitored.unwrap_or_else(|| {
detailed_collection_body["monitored"]
.as_bool()
.expect("Unable to deserialize 'monitored' bool")
});
let minimum_availability = edit_collection_params
.minimum_availability
.unwrap_or_else(|| {
serde_json::from_value(detailed_collection_body["minimumAvailability"].clone())
.expect("Unable to deserialize 'minimumAvailability'")
})
.to_string();
let quality_profile_id = edit_collection_params.quality_profile_id.unwrap_or_else(|| {
detailed_collection_body["qualityProfileId"]
.as_i64()
.expect("Unable to deserialize 'qualityProfileId'")
});
let root_folder_path = edit_collection_params.root_folder_path.unwrap_or_else(|| {
detailed_collection_body["rootFolderPath"]
.as_str()
.expect("Unable to deserialize 'rootFolderPath'")
.to_owned()
});
let search_on_add = edit_collection_params.search_on_add.unwrap_or_else(|| {
detailed_collection_body["searchOnAdd"]
.as_bool()
.expect("Unable to deserialize 'searchOnAdd'")
});
(
monitored,
minimum_availability,
quality_profile_id,
root_folder_path,
search_on_add,
)
};
* 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);
debug!("Edit collection body: {detailed_collection_body:?}");
let request_props = self
.request_props_from(
event,
RequestMethod::Put,
Some(detailed_collection_body),
Some(format!("/{collection_id}")),
None,
)
.await;
self
.handle_request::<Value, ()>(request_props, |_, _| ())
.await
}
async fn edit_radarr_indexer(
&mut self,
mut edit_indexer_params: EditIndexerParams,
) -> Result<()> {
let detail_event = RadarrEvent::GetIndexers;
let event = RadarrEvent::EditIndexer(edit_indexer_params.clone());
let id = edit_indexer_params.indexer_id;
if let Some(tag_input_string) = edit_indexer_params.tag_input_string.as_ref() {
let tag_ids_vec = self
.extract_and_add_radarr_tag_ids_vec(tag_input_string.clone())
.await;
edit_indexer_params.tags = Some(tag_ids_vec);
}
info!("Updating Radarr indexer with ID: {id}");
info!("Fetching indexer details for indexer with ID: {id}");
let request_props = self
.request_props_from(
detail_event,
RequestMethod::Get,
None::<()>,
Some(format!("/{id}")),
None,
)
.await;
let mut response = String::new();
self
.handle_request::<(), Value>(request_props, |detailed_indexer_body, _| {
response = detailed_indexer_body.to_string()
})
.await?;
info!("Constructing edit indexer body");
let mut detailed_indexer_body: Value = serde_json::from_str(&response)?;
let (
name,
enable_rss,
enable_automatic_search,
enable_interactive_search,
url,
api_key,
seed_ratio,
tags,
priority,
) = {
let priority = detailed_indexer_body["priority"]
.as_i64()
.expect("Unable to deserialize 'priority'");
let seed_ratio_field_option = detailed_indexer_body["fields"]
.as_array()
.unwrap()
.iter()
.find(|field| field["name"] == "seedCriteria.seedRatio");
let name = edit_indexer_params.name.unwrap_or(
detailed_indexer_body["name"]
.as_str()
.expect("Unable to deserialize 'name'")
.to_owned(),
);
let enable_rss = edit_indexer_params.enable_rss.unwrap_or(
detailed_indexer_body["enableRss"]
.as_bool()
.expect("Unable to deserialize 'enableRss'"),
);
let enable_automatic_search = edit_indexer_params.enable_automatic_search.unwrap_or(
detailed_indexer_body["enableAutomaticSearch"]
.as_bool()
.expect("Unable to deserialize 'enableAutomaticSearch"),
);
let enable_interactive_search = edit_indexer_params.enable_interactive_search.unwrap_or(
detailed_indexer_body["enableInteractiveSearch"]
.as_bool()
.expect("Unable to deserialize 'enableInteractiveSearch'"),
);
let url = edit_indexer_params.url.unwrap_or(
detailed_indexer_body["fields"]
.as_array()
.expect("Unable to deserialize 'fields'")
.iter()
.find(|field| field["name"] == "baseUrl")
.expect("Field 'baseUrl' was not found in the 'fields' array")
.get("value")
.unwrap_or(&json!(""))
.as_str()
.expect("Unable to deserialize 'baseUrl value'")
.to_owned(),
);
let api_key = edit_indexer_params.api_key.unwrap_or(
detailed_indexer_body["fields"]
.as_array()
.expect("Unable to deserialize 'fields'")
.iter()
.find(|field| field["name"] == "apiKey")
.expect("Field 'apiKey' was not found in the 'fields' array")
.get("value")
.unwrap_or(&json!(""))
.as_str()
.expect("Unable to deserialize 'apiKey value'")
.to_owned(),
);
let seed_ratio = edit_indexer_params.seed_ratio.unwrap_or_else(|| {
if let Some(seed_ratio_field) = seed_ratio_field_option {
return seed_ratio_field
.get("value")
.unwrap_or(&json!(""))
.as_str()
.expect("Unable to deserialize 'seedCriteria.seedRatio value'")
.to_owned();
}
String::new()
});
let tags = if edit_indexer_params.clear_tags {
vec![]
} else {
edit_indexer_params.tags.unwrap_or(
detailed_indexer_body["tags"]
.as_array()
.expect("Unable to deserialize 'tags'")
.iter()
.map(|item| item.as_i64().expect("Unable to deserialize tag ID"))
.collect(),
)
};
let priority = edit_indexer_params.priority.unwrap_or(priority);
(
name,
enable_rss,
enable_automatic_search,
enable_interactive_search,
url,
api_key,
seed_ratio,
tags,
priority,
)
};
*detailed_indexer_body.get_mut("name").unwrap() = json!(name);
*detailed_indexer_body.get_mut("priority").unwrap() = json!(priority);
*detailed_indexer_body.get_mut("enableRss").unwrap() = json!(enable_rss);
*detailed_indexer_body
.get_mut("enableAutomaticSearch")
.unwrap() = json!(enable_automatic_search);
*detailed_indexer_body
.get_mut("enableInteractiveSearch")
.unwrap() = json!(enable_interactive_search);
*detailed_indexer_body
.get_mut("fields")
.unwrap()
.as_array_mut()
.unwrap()
.iter_mut()
.find(|field| field["name"] == "baseUrl")
.unwrap()
.get_mut("value")
.unwrap() = json!(url);
*detailed_indexer_body
.get_mut("fields")
.unwrap()
.as_array_mut()
.unwrap()
.iter_mut()
.find(|field| field["name"] == "apiKey")
.unwrap()
.get_mut("value")
.unwrap() = json!(api_key);
*detailed_indexer_body.get_mut("tags").unwrap() = json!(tags);
let seed_ratio_field_option = detailed_indexer_body
.get_mut("fields")
.unwrap()
.as_array_mut()
.unwrap()
.iter_mut()
.find(|field| field["name"] == "seedCriteria.seedRatio");
if let Some(seed_ratio_field) = seed_ratio_field_option {
seed_ratio_field
.as_object_mut()
.unwrap()
.insert("value".to_string(), json!(seed_ratio));
}
debug!("Edit indexer body: {detailed_indexer_body:?}");
let request_props = self
.request_props_from(
event,
RequestMethod::Put,
Some(detailed_indexer_body),
Some(format!("/{id}")),
Some("forceSave=true".to_owned()),
)
.await;
self
.handle_request::<Value, ()>(request_props, |_, _| ())
.await
}
async fn edit_movie(&mut self, mut edit_movie_params: EditMovieParams) -> Result<()> {
info!("Editing Radarr movie");
let detail_event = RadarrEvent::GetMovieDetails(None);
let event = RadarrEvent::EditMovie(edit_movie_params.clone());
let movie_id = edit_movie_params.movie_id;
if let Some(tag_input_string) = edit_movie_params.tag_input_string.as_ref() {
let tag_ids_vec = self
.extract_and_add_radarr_tag_ids_vec(tag_input_string.clone())
.await;
edit_movie_params.tags = Some(tag_ids_vec);
}
info!("Fetching movie details for movie with ID: {movie_id}");
let request_props = self
.request_props_from(
detail_event,
RequestMethod::Get,
None::<()>,
Some(format!("/{movie_id}")),
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 mut detailed_movie_body: Value = serde_json::from_str(&response)?;
let (monitored, minimum_availability, quality_profile_id, root_folder_path, tags) =
{
let monitored = edit_movie_params.monitored.unwrap_or(
detailed_movie_body["monitored"]
.as_bool()
.expect("Unable to deserialize 'monitored'"),
);
let minimum_availability = edit_movie_params
.minimum_availability
.unwrap_or_else(|| {
serde_json::from_value(detailed_movie_body["minimumAvailability"].clone())
.expect("Unable to deserialize 'minimumAvailability'")
})
.to_string();
let quality_profile_id = edit_movie_params.quality_profile_id.unwrap_or_else(|| {
detailed_movie_body["qualityProfileId"]
.as_i64()
.expect("Unable to deserialize 'qualityProfileId'")
});
let root_folder_path = edit_movie_params.root_folder_path.unwrap_or_else(|| {
detailed_movie_body["path"]
.as_str()
.expect("Unable to deserialize 'path'")
.to_owned()
});
let tags = if edit_movie_params.clear_tags {
vec![]
} else {
edit_movie_params.tags.unwrap_or(
detailed_movie_body["tags"]
.as_array()
.expect("Unable to deserialize 'tags'")
.iter()
.map(|item| item.as_i64().expect("Unable to deserialize tag ID"))
.collect(),
)
};
(
monitored,
minimum_availability,
quality_profile_id,
root_folder_path,
tags,
)
};
*detailed_movie_body.get_mut("monitored").unwrap() = json!(monitored);
*detailed_movie_body.get_mut("minimumAvailability").unwrap() = json!(minimum_availability);
*detailed_movie_body.get_mut("qualityProfileId").unwrap() = json!(quality_profile_id);
*detailed_movie_body.get_mut("path").unwrap() = json!(root_folder_path);
*detailed_movie_body.get_mut("tags").unwrap() = json!(tags);
debug!("Edit movie body: {detailed_movie_body:?}");
let request_props = self
.request_props_from(
event,
RequestMethod::Put,
Some(detailed_movie_body),
Some(format!("/{movie_id}")),
None,
)
.await;
self
.handle_request::<Value, ()>(request_props, |_, _| ())
.await
}
async fn get_radarr_blocklist(&mut self) -> Result<BlocklistResponse> {
info!("Fetching Radarr blocklist");
let event = RadarrEvent::GetBlocklist;
let request_props = self
.request_props_from(event, RequestMethod::Get, None::<()>, None, None)
.await;
self
.handle_request::<(), BlocklistResponse>(request_props, |blocklist_resp, mut app| {
if !matches!(
app.get_current_route(),
Route::Radarr(ActiveRadarrBlock::BlocklistSortPrompt, _)
) {
let mut blocklist_vec = blocklist_resp.records;
blocklist_vec.sort_by(|a, b| a.id.cmp(&b.id));
app.data.radarr_data.blocklist.set_items(blocklist_vec);
app.data.radarr_data.blocklist.apply_sorting_toggle(false);
}
})
.await
}
async fn get_collections(&mut self) -> Result<Vec<Collection>> {
info!("Fetching Radarr collections");
let event = RadarrEvent::GetCollections;
let request_props = self
.request_props_from(event, RequestMethod::Get, None::<()>, None, None)
.await;
self
.handle_request::<(), Vec<Collection>>(request_props, |mut collections_vec, mut app| {
if !matches!(
app.get_current_route(),
Route::Radarr(ActiveRadarrBlock::CollectionsSortPrompt, _)
) {
collections_vec.sort_by(|a, b| a.id.cmp(&b.id));
app.data.radarr_data.collections.set_items(collections_vec);
app.data.radarr_data.collections.apply_sorting_toggle(false);
}
})
.await
}
async fn get_credits(&mut self, movie_id: i64) -> Result<Vec<Credit>> {
info!("Fetching Radarr movie credits");
let event = RadarrEvent::GetMovieCredits(movie_id);
let request_props = self
.request_props_from(
event,
RequestMethod::Get,
None::<()>,
None,
Some(format!("movieId={movie_id}")),
)
.await;
self
.handle_request::<(), Vec<Credit>>(request_props, |credit_vec, mut app| {
let cast_vec: Vec<Credit> = credit_vec
.iter()
.filter(|&credit| credit.credit_type == CreditType::Cast)
.cloned()
.collect();
let crew_vec: Vec<Credit> = credit_vec
.iter()
.filter(|&credit| credit.credit_type == CreditType::Crew)
.cloned()
.collect();
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_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_radarr_diskspace(&mut self) -> Result<Vec<DiskSpace>> {
info!("Fetching Radarr disk space");
let event = RadarrEvent::GetDiskSpace;
let request_props = self
.request_props_from(event, RequestMethod::Get, None::<()>, None, None)
.await;
self
.handle_request::<(), Vec<DiskSpace>>(request_props, |disk_space_vec, mut app| {
app.data.radarr_data.disk_space_vec = disk_space_vec;
})
.await
}
async fn get_radarr_downloads(&mut self) -> Result<DownloadsResponse> {
info!("Fetching Radarr downloads");
let event = RadarrEvent::GetDownloads;
let request_props = self
.request_props_from(event, RequestMethod::Get, None::<()>, None, 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_radarr_host_config(&mut self) -> Result<HostConfig> {
info!("Fetching Radarr host config");
let event = RadarrEvent::GetHostConfig;
let request_props = self
.request_props_from(event, RequestMethod::Get, None::<()>, None, None)
.await;
self
.handle_request::<(), HostConfig>(request_props, |_, _| ())
.await
}
async fn get_radarr_indexers(&mut self) -> Result<Vec<Indexer>> {
info!("Fetching Radarr indexers");
let event = RadarrEvent::GetIndexers;
let request_props = self
.request_props_from(event, RequestMethod::Get, None::<()>, None, None)
.await;
self
.handle_request::<(), Vec<Indexer>>(request_props, |indexers, mut app| {
app.data.radarr_data.indexers.set_items(indexers);
})
.await
}
async fn get_all_radarr_indexer_settings(&mut self) -> Result<IndexerSettings> {
info!("Fetching Radarr indexer settings");
let event = RadarrEvent::GetAllIndexerSettings;
let request_props = self
.request_props_from(event, RequestMethod::Get, None::<()>, None, 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_radarr_healthcheck(&mut self) -> Result<()> {
info!("Performing Radarr health check");
let event = RadarrEvent::HealthCheck;
let request_props = self
.request_props_from(event, RequestMethod::Get, None::<()>, None, None)
.await;
self
.handle_request::<(), ()>(request_props, |_, _| ())
.await
}
async fn get_radarr_logs(&mut self, events: u64) -> Result<LogResponse> {
info!("Fetching Radarr logs");
let event = RadarrEvent::GetLogs(events);
let params = format!(
"pageSize={}&sortDirection=descending&sortKey=time",
events
);
let request_props = self
.request_props_from(event, RequestMethod::Get, None::<()>, None, Some(params))
.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, movie_id: Option<i64>) -> Result<Movie> {
info!("Fetching Radarr movie details");
let event = RadarrEvent::GetMovieDetails(None);
let (id, _) = self.extract_movie_id(movie_id).await;
info!("Fetching movie details for movie with ID: {id}");
let request_props = self
.request_props_from(
event,
RequestMethod::Get,
None::<()>,
Some(format!("/{id}")),
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_or(&"".to_owned())
.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
.as_ref()
.unwrap_or(&String::new())
.to_owned(),
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, movie_id: Option<i64>) -> Result<Vec<MovieHistoryItem>> {
info!("Fetching Radarr movie history");
let event = RadarrEvent::GetMovieHistory(None);
let (_, movie_id_param) = self.extract_movie_id(movie_id).await;
let request_props = self
.request_props_from(
event,
RequestMethod::Get,
None::<()>,
None,
Some(movie_id_param),
)
.await;
self
.handle_request::<(), Vec<MovieHistoryItem>>(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) -> Result<Vec<Movie>> {
info!("Fetching Radarr library");
let event = RadarrEvent::GetMovies;
let request_props = self
.request_props_from(event, RequestMethod::Get, None::<()>, None, None)
.await;
self
.handle_request::<(), Vec<Movie>>(request_props, |mut movie_vec, mut app| {
if !matches!(
app.get_current_route(),
Route::Radarr(ActiveRadarrBlock::MoviesSortPrompt, _)
) {
movie_vec.sort_by(|a, b| a.id.cmp(&b.id));
app.data.radarr_data.movies.set_items(movie_vec);
app.data.radarr_data.movies.apply_sorting_toggle(false);
}
})
.await
}
async fn get_radarr_quality_profiles(&mut self) -> Result<Vec<QualityProfile>> {
info!("Fetching Radarr quality profiles");
let event = RadarrEvent::GetQualityProfiles;
let request_props = self
.request_props_from(event, RequestMethod::Get, None::<()>, None, None)
.await;
self
.handle_request::<(), Vec<QualityProfile>>(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_radarr_events(&mut self) -> Result<Vec<QueueEvent>> {
info!("Fetching Radarr queued events");
let event = RadarrEvent::GetQueuedEvents;
let request_props = self
.request_props_from(event, RequestMethod::Get, None::<()>, None, None)
.await;
self
.handle_request::<(), Vec<QueueEvent>>(request_props, |queued_events_vec, mut app| {
app
.data
.radarr_data
.queued_events
.set_items(queued_events_vec);
})
.await
}
async fn get_movie_releases(&mut self, movie_id: Option<i64>) -> Result<Vec<RadarrRelease>> {
let (id, movie_id_param) = self.extract_movie_id(movie_id).await;
info!("Fetching releases for movie with ID: {id}");
let event = RadarrEvent::GetReleases(None);
let request_props = self
.request_props_from(
event,
RequestMethod::Get,
None::<()>,
None,
Some(movie_id_param),
)
.await;
self
.handle_request::<(), Vec<RadarrRelease>>(request_props, |release_vec, mut app| {
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_releases
.set_items(release_vec);
})
.await
}
async fn get_radarr_root_folders(&mut self) -> Result<Vec<RootFolder>> {
info!("Fetching Radarr root folders");
let event = RadarrEvent::GetRootFolders;
let request_props = self
.request_props_from(event, RequestMethod::Get, None::<()>, None, None)
.await;
self
.handle_request::<(), Vec<RootFolder>>(request_props, |root_folders, mut app| {
app.data.radarr_data.root_folders.set_items(root_folders);
})
.await
}
async fn get_radarr_security_config(&mut self) -> Result<SecurityConfig> {
info!("Fetching Radarr security config");
let event = RadarrEvent::GetSecurityConfig;
let request_props = self
.request_props_from(event, RequestMethod::Get, None::<()>, None, None)
.await;
self
.handle_request::<(), SecurityConfig>(request_props, |_, _| ())
.await
}
async fn get_radarr_status(&mut self) -> Result<SystemStatus> {
info!("Fetching Radarr system status");
let event = RadarrEvent::GetStatus;
let request_props = self
.request_props_from(event, RequestMethod::Get, None::<()>, None, 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_radarr_tags(&mut self) -> Result<Vec<Tag>> {
info!("Fetching Radarr tags");
let event = RadarrEvent::GetTags;
let request_props = self
.request_props_from(event, RequestMethod::Get, None::<()>, None, None)
.await;
self
.handle_request::<(), Vec<Tag>>(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_radarr_tasks(&mut self) -> Result<Vec<RadarrTask>> {
info!("Fetching Radarr tasks");
let event = RadarrEvent::GetTasks;
let request_props = self
.request_props_from(event, RequestMethod::Get, None::<()>, None, None)
.await;
self
.handle_request::<(), Vec<RadarrTask>>(request_props, |tasks_vec, mut app| {
app.data.radarr_data.tasks.set_items(tasks_vec);
})
.await
}
async fn get_radarr_updates(&mut self) -> Result<Vec<Update>> {
info!("Fetching Radarr updates");
let event = RadarrEvent::GetUpdates;
let request_props = self
.request_props_from(event, RequestMethod::Get, None::<()>, None, None)
.await;
self
.handle_request::<(), Vec<Update>>(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<String>| {
vec
.iter()
.map(|change| format!(" * {change}"))
.collect::<Vec<String>>()
.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, query: Option<String>) -> Result<Vec<AddMovieSearchResult>> {
info!("Searching for specific Radarr movie");
let event = RadarrEvent::SearchNewMovie(None);
let search = if let Some(search_query) = query {
Ok(search_query.into())
} else {
self
.app
.lock()
.await
.data
.radarr_data
.add_movie_search
.clone()
.ok_or(anyhow!("Encountered a race condition"))
};
match search {
Ok(search_string) => {
let request_props = self
.request_props_from(
event,
RequestMethod::Get,
None::<()>,
None,
Some(format!("term={}", encode(&search_string.text))),
)
.await;
self
.handle_request::<(), Vec<AddMovieSearchResult>>(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."
);
Ok(Vec::default())
}
}
}
async fn start_radarr_task(&mut self, task: Option<RadarrTaskName>) -> Result<Value> {
let event = RadarrEvent::StartTask(None);
let task_name = if let Some(t_name) = task {
t_name
} else {
self
.app
.lock()
.await
.data
.radarr_data
.tasks
.current_selection()
.task_name
}
.to_string();
info!("Starting Radarr task: {task_name}");
let body = CommandBody { name: task_name };
let request_props = self
.request_props_from(event, RequestMethod::Post, Some(body), None, None)
.await;
self
.handle_request::<CommandBody, Value>(request_props, |_, _| ())
.await
}
async fn test_radarr_indexer(&mut self, indexer_id: Option<i64>) -> Result<Value> {
let detail_event = RadarrEvent::GetIndexers;
let event = RadarrEvent::TestIndexer(None);
let id = if let Some(i_id) = indexer_id {
i_id
} else {
self
.app
.lock()
.await
.data
.radarr_data
.indexers
.current_selection()
.id
};
info!("Testing Radarr indexer with ID: {id}");
info!("Fetching indexer details for indexer with ID: {id}");
let request_props = self
.request_props_from(
detail_event,
RequestMethod::Get,
None::<()>,
Some(format!("/{id}")),
None,
)
.await;
let mut test_body: Value = Value::default();
self
.handle_request::<(), Value>(request_props, |detailed_indexer_body, _| {
test_body = detailed_indexer_body;
})
.await?;
info!("Testing indexer");
let mut request_props = self
.request_props_from(event, RequestMethod::Post, Some(test_body), None, None)
.await;
request_props.ignore_status_code = true;
self
.handle_request::<Value, Value>(request_props, |test_results, mut app| {
if test_results.as_object().is_none() {
app.data.radarr_data.indexer_test_errors = Some(
test_results.as_array().unwrap()[0]
.get("errorMessage")
.unwrap()
.to_string(),
);
} else {
app.data.radarr_data.indexer_test_errors = Some(String::new());
};
})
.await
}
async fn test_all_radarr_indexers(&mut self) -> Result<Vec<IndexerTestResult>> {
info!("Testing all Radarr indexers");
let event = RadarrEvent::TestAllIndexers;
let mut request_props = self
.request_props_from(event, RequestMethod::Post, None, None, None)
.await;
request_props.ignore_status_code = true;
self
.handle_request::<(), Vec<IndexerTestResult>>(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::<Vec<String>>()
.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_movie_search(&mut self, movie_id: Option<i64>) -> Result<Value> {
let event = RadarrEvent::TriggerAutomaticSearch(movie_id);
let (id, _) = self.extract_movie_id(movie_id).await;
info!("Searching indexers for movie with ID: {id}");
let body = MovieCommandBody {
name: "MoviesSearch".to_owned(),
movie_ids: vec![id],
};
let request_props = self
.request_props_from(event, RequestMethod::Post, Some(body), None, None)
.await;
self
.handle_request::<MovieCommandBody, Value>(request_props, |_, _| ())
.await
}
async fn update_all_movies(&mut self) -> Result<Value> {
info!("Updating all movies");
let event = RadarrEvent::UpdateAllMovies;
let body = MovieCommandBody {
name: "RefreshMovie".to_owned(),
movie_ids: Vec::new(),
};
let request_props = self
.request_props_from(event, RequestMethod::Post, Some(body), None, None)
.await;
self
.handle_request::<MovieCommandBody, Value>(request_props, |_, _| ())
.await
}
async fn update_and_scan_movie(&mut self, movie_id: Option<i64>) -> Result<Value> {
let (id, _) = self.extract_movie_id(movie_id).await;
let event = RadarrEvent::UpdateAndScan(None);
info!("Updating and scanning movie with ID: {id}");
let body = MovieCommandBody {
name: "RefreshMovie".to_owned(),
movie_ids: vec![id],
};
let request_props = self
.request_props_from(event, RequestMethod::Post, Some(body), None, None)
.await;
self
.handle_request::<MovieCommandBody, Value>(request_props, |_, _| ())
.await
}
async fn update_collections(&mut self) -> Result<Value> {
info!("Updating collections");
let event = RadarrEvent::UpdateCollections;
let body = CommandBody {
name: "RefreshCollections".to_owned(),
};
let request_props = self
.request_props_from(event, RequestMethod::Post, Some(body), None, None)
.await;
self
.handle_request::<CommandBody, Value>(request_props, |_, _| ())
.await
}
async fn update_radarr_downloads(&mut self) -> Result<Value> {
info!("Updating Radarr downloads");
let event = RadarrEvent::UpdateDownloads;
let body = CommandBody {
name: "RefreshMonitoredDownloads".to_owned(),
};
let request_props = self
.request_props_from(event, RequestMethod::Post, Some(body), None, None)
.await;
self
.handle_request::<CommandBody, Value>(request_props, |_, _| ())
.await
}
async fn extract_and_add_radarr_tag_ids_vec(&mut self, edit_tags: String) -> Vec<i64> {
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.to_lowercase().trim()).is_none())
.collect::<Vec<&str>>();
for tag in missing_tags_vec {
self
.add_radarr_tag(tag.trim().to_owned())
.await
.expect("Unable to add tag");
}
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.to_lowercase().trim())
.unwrap()
})
.collect()
}
async fn extract_movie_id(&mut self, movie_id: Option<i64>) -> (i64, String) {
let movie_id = if let Some(id) = movie_id {
id
} else {
self
.app
.lock()
.await
.data
.radarr_data
.movies
.current_selection()
.id
};
(movie_id, format!("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()
}