Implemented the ability to view indexers

This commit is contained in:
2023-08-08 10:50:07 -06:00
parent d32f2b538d
commit 72194fe668
9 changed files with 1362 additions and 1003 deletions
+15 -1
View File
@@ -4,7 +4,7 @@ use strum::IntoEnumIterator;
use crate::app::{App, Route};
use crate::models::radarr_models::{
AddMovieSearchResult, Collection, CollectionMovie, Credit, DiskSpace, DownloadRecord,
AddMovieSearchResult, Collection, CollectionMovie, Credit, DiskSpace, DownloadRecord, Indexer,
MinimumAvailability, Monitor, Movie, MovieHistoryItem, QueueEvent, Release, ReleaseField,
RootFolder, Task,
};
@@ -36,6 +36,7 @@ pub struct RadarrData<'a> {
pub root_folder_list: StatefulList<RootFolder>,
pub selected_block: BlockSelectionState<'a, ActiveRadarrBlock>,
pub downloads: StatefulTable<DownloadRecord>,
pub indexers: StatefulTable<Indexer>,
pub quality_profile_map: BiMap<u64, String>,
pub tags_map: BiMap<u64, String>,
pub movie_details: ScrollableText,
@@ -261,6 +262,7 @@ impl<'a> Default for RadarrData<'a> {
selected_block: BlockSelectionState::default(),
filtered_movies: StatefulTable::default(),
downloads: StatefulTable::default(),
indexers: StatefulTable::default(),
quality_profile_map: BiMap::default(),
tags_map: BiMap::default(),
file_details: String::default(),
@@ -318,6 +320,12 @@ impl<'a> Default for RadarrData<'a> {
help: "",
contextual_help: Some("<a> add | <del> delete | <r> refresh"),
},
TabRoute {
title: "Indexers",
route: ActiveRadarrBlock::Indexers.into(),
help: "",
contextual_help: Some("<r> refresh"),
},
TabRoute {
title: "System",
route: ActiveRadarrBlock::System.into(),
@@ -410,6 +418,7 @@ pub enum ActiveRadarrBlock {
FileInfo,
FilterCollections,
FilterMovies,
Indexers,
ManualSearch,
ManualSearchSortPrompt,
ManualSearchConfirmPrompt,
@@ -573,6 +582,11 @@ impl<'a> App<'a> {
.dispatch_network_event(RadarrEvent::GetDownloads.into())
.await;
}
ActiveRadarrBlock::Indexers => {
self
.dispatch_network_event(RadarrEvent::GetIndexers.into())
.await;
}
ActiveRadarrBlock::System => {
self
.dispatch_network_event(RadarrEvent::GetTasks.into())
+32 -3
View File
@@ -273,6 +273,7 @@ mod tests {
assert_eq!(radarr_data.selected_block, BlockSelectionState::default());
assert!(radarr_data.filtered_movies.items.is_empty());
assert!(radarr_data.downloads.items.is_empty());
assert!(radarr_data.indexers.items.is_empty());
assert!(radarr_data.quality_profile_map.is_empty());
assert!(radarr_data.tags_map.is_empty());
assert!(radarr_data.file_details.is_empty());
@@ -306,7 +307,7 @@ mod tests {
assert!(!radarr_data.delete_movie_files);
assert!(!radarr_data.add_list_exclusion);
assert_eq!(radarr_data.main_tabs.tabs.len(), 5);
assert_eq!(radarr_data.main_tabs.tabs.len(), 6);
assert_str_eq!(radarr_data.main_tabs.tabs[0].title, "Library");
assert_eq!(
@@ -348,14 +349,25 @@ mod tests {
Some("<a> add | <del> delete | <r> refresh")
);
assert_str_eq!(radarr_data.main_tabs.tabs[4].title, "System");
assert_str_eq!(radarr_data.main_tabs.tabs[4].title, "Indexers");
assert_eq!(
radarr_data.main_tabs.tabs[4].route,
ActiveRadarrBlock::System.into()
ActiveRadarrBlock::Indexers.into()
);
assert!(radarr_data.main_tabs.tabs[4].help.is_empty());
assert_eq!(
radarr_data.main_tabs.tabs[4].contextual_help,
Some("<r> refresh")
);
assert_str_eq!(radarr_data.main_tabs.tabs[5].title, "System");
assert_eq!(
radarr_data.main_tabs.tabs[5].route,
ActiveRadarrBlock::System.into()
);
assert!(radarr_data.main_tabs.tabs[5].help.is_empty());
assert_eq!(
radarr_data.main_tabs.tabs[5].contextual_help,
Some("<t> open tasks | <z> open queue | <l> open logs | <u> open updates | <r> refresh")
);
@@ -684,6 +696,23 @@ mod tests {
assert_eq!(app.tick_count, 0);
}
#[tokio::test]
async fn test_dispatch_by_indexers_block() {
let (mut app, mut sync_network_rx) = construct_app_unit();
app
.dispatch_by_radarr_block(&ActiveRadarrBlock::Indexers)
.await;
assert!(app.is_loading);
assert_eq!(
sync_network_rx.recv().await.unwrap(),
RadarrEvent::GetIndexers.into()
);
assert!(!app.data.radarr_data.prompt_confirm);
assert_eq!(app.tick_count, 0);
}
#[tokio::test]
async fn test_dispatch_by_system_block() {
let (mut app, mut sync_network_rx) = construct_app_unit();
+11
View File
@@ -116,6 +116,7 @@ impl<'a, 'b> KeyEventHandler<'a, 'b, ActiveRadarrBlock> for RadarrHandler<'a, 'b
}
}
ActiveRadarrBlock::Downloads => self.app.data.radarr_data.downloads.scroll_up(),
ActiveRadarrBlock::Indexers => self.app.data.radarr_data.indexers.scroll_up(),
ActiveRadarrBlock::RootFolders => self.app.data.radarr_data.root_folders.scroll_up(),
_ => (),
}
@@ -145,6 +146,7 @@ impl<'a, 'b> KeyEventHandler<'a, 'b, ActiveRadarrBlock> for RadarrHandler<'a, 'b
}
}
ActiveRadarrBlock::Downloads => self.app.data.radarr_data.downloads.scroll_down(),
ActiveRadarrBlock::Indexers => self.app.data.radarr_data.indexers.scroll_down(),
ActiveRadarrBlock::RootFolders => self.app.data.radarr_data.root_folders.scroll_down(),
_ => (),
}
@@ -179,6 +181,7 @@ impl<'a, 'b> KeyEventHandler<'a, 'b, ActiveRadarrBlock> for RadarrHandler<'a, 'b
}
}
ActiveRadarrBlock::Downloads => self.app.data.radarr_data.downloads.scroll_to_top(),
ActiveRadarrBlock::Indexers => self.app.data.radarr_data.indexers.scroll_to_top(),
ActiveRadarrBlock::RootFolders => self.app.data.radarr_data.root_folders.scroll_to_top(),
ActiveRadarrBlock::SearchMovie | ActiveRadarrBlock::SearchCollection => {
self.app.data.radarr_data.search.scroll_home()
@@ -220,6 +223,7 @@ impl<'a, 'b> KeyEventHandler<'a, 'b, ActiveRadarrBlock> for RadarrHandler<'a, 'b
}
}
ActiveRadarrBlock::Downloads => self.app.data.radarr_data.downloads.scroll_to_bottom(),
ActiveRadarrBlock::Indexers => self.app.data.radarr_data.indexers.scroll_to_bottom(),
ActiveRadarrBlock::RootFolders => self.app.data.radarr_data.root_folders.scroll_to_bottom(),
ActiveRadarrBlock::SearchMovie | ActiveRadarrBlock::SearchCollection => {
self.app.data.radarr_data.search.reset_offset()
@@ -257,6 +261,7 @@ impl<'a, 'b> KeyEventHandler<'a, 'b, ActiveRadarrBlock> for RadarrHandler<'a, 'b
| ActiveRadarrBlock::Downloads
| ActiveRadarrBlock::Collections
| ActiveRadarrBlock::RootFolders
| ActiveRadarrBlock::Indexers
| ActiveRadarrBlock::System => match self.key {
_ if *self.key == DEFAULT_KEYBINDINGS.left.key => {
self.app.data.radarr_data.main_tabs.previous();
@@ -521,6 +526,12 @@ impl<'a, 'b> KeyEventHandler<'a, 'b, ActiveRadarrBlock> for RadarrHandler<'a, 'b
}
_ => (),
},
ActiveRadarrBlock::Indexers => match self.key {
_ if *key == DEFAULT_KEYBINDINGS.refresh.key => {
self.app.should_refresh = true;
}
_ => (),
},
ActiveRadarrBlock::Collections => match self.key {
_ if *key == DEFAULT_KEYBINDINGS.search.key => {
self
@@ -16,7 +16,7 @@ mod tests {
mod test_handle_scroll_up_and_down {
use rstest::rstest;
use crate::models::radarr_models::{DownloadRecord, RootFolder};
use crate::models::radarr_models::{DownloadRecord, Indexer, RootFolder};
use crate::{simple_stateful_iterable_vec, test_iterable_scroll};
use super::*;
@@ -75,6 +75,16 @@ mod tests {
title
);
test_iterable_scroll!(
test_indexers_scroll,
RadarrHandler,
indexers,
simple_stateful_iterable_vec!(Indexer, String, protocol),
ActiveRadarrBlock::Indexers,
None,
protocol
);
test_iterable_scroll!(
test_root_folders_scroll,
RadarrHandler,
@@ -89,7 +99,7 @@ mod tests {
mod test_handle_home_end {
use pretty_assertions::assert_eq;
use crate::models::radarr_models::{DownloadRecord, RootFolder};
use crate::models::radarr_models::{DownloadRecord, Indexer, RootFolder};
use crate::{
extended_stateful_iterable_vec, test_iterable_home_and_end, test_text_box_home_end_keys,
};
@@ -150,6 +160,16 @@ mod tests {
title
);
test_iterable_home_and_end!(
test_indexers_home_end,
RadarrHandler,
indexers,
extended_stateful_iterable_vec!(Indexer, String, protocol),
ActiveRadarrBlock::Indexers,
None,
protocol
);
test_iterable_home_and_end!(
test_root_folders_home_end,
RadarrHandler,
@@ -250,7 +270,8 @@ mod tests {
#[rstest]
#[case(ActiveRadarrBlock::Movies, 0, ActiveRadarrBlock::System)]
#[case(ActiveRadarrBlock::System, 4, ActiveRadarrBlock::RootFolders)]
#[case(ActiveRadarrBlock::System, 5, ActiveRadarrBlock::Indexers)]
#[case(ActiveRadarrBlock::Indexers, 4, ActiveRadarrBlock::RootFolders)]
#[case(ActiveRadarrBlock::RootFolders, 3, ActiveRadarrBlock::Collections)]
#[case(ActiveRadarrBlock::Collections, 2, ActiveRadarrBlock::Downloads)]
#[case(ActiveRadarrBlock::Downloads, 1, ActiveRadarrBlock::Movies)]
@@ -281,8 +302,9 @@ mod tests {
#[case(ActiveRadarrBlock::Movies, 0, ActiveRadarrBlock::Downloads)]
#[case(ActiveRadarrBlock::Downloads, 1, ActiveRadarrBlock::Collections)]
#[case(ActiveRadarrBlock::Collections, 2, ActiveRadarrBlock::RootFolders)]
#[case(ActiveRadarrBlock::RootFolders, 3, ActiveRadarrBlock::System)]
#[case(ActiveRadarrBlock::System, 4, ActiveRadarrBlock::Movies)]
#[case(ActiveRadarrBlock::RootFolders, 3, ActiveRadarrBlock::Indexers)]
#[case(ActiveRadarrBlock::Indexers, 4, ActiveRadarrBlock::System)]
#[case(ActiveRadarrBlock::System, 5, ActiveRadarrBlock::Movies)]
fn test_radarr_tab_right(
#[case] active_radarr_block: ActiveRadarrBlock,
#[case] index: usize,
@@ -963,6 +985,7 @@ mod tests {
ActiveRadarrBlock::Movies,
ActiveRadarrBlock::Collections,
ActiveRadarrBlock::Downloads,
ActiveRadarrBlock::Indexers,
ActiveRadarrBlock::RootFolders,
ActiveRadarrBlock::System
)]
+337 -289
View File
@@ -3,7 +3,7 @@ use std::fmt::{Display, Formatter};
use chrono::{DateTime, Utc};
use derivative::Derivative;
use serde::{Deserialize, Serialize};
use serde_json::Number;
use serde_json::{Number, Value};
use strum_macros::{Display, EnumIter};
use crate::models::HorizontallyScrollableText;
@@ -12,86 +12,47 @@ use crate::models::HorizontallyScrollableText;
#[path = "radarr_models_tests.rs"]
mod radarr_models_tests;
#[derive(Deserialize, Debug, Clone, Eq, PartialEq)]
#[derive(Default, Serialize, Debug)]
#[serde(rename_all = "camelCase")]
pub struct DiskSpace {
pub free_space: Number,
pub total_space: Number,
}
#[derive(Deserialize, Debug)]
#[serde(rename_all = "camelCase")]
pub struct SystemStatus {
pub version: String,
pub start_time: DateTime<Utc>,
}
#[derive(Derivative, Deserialize, Debug, Clone, Eq, PartialEq)]
#[derivative(Default)]
#[serde(rename_all = "camelCase")]
pub struct RootFolder {
#[derivative(Default(value = "Number::from(0)"))]
pub id: Number,
pub path: String,
pub accessible: bool,
#[derivative(Default(value = "Number::from(0)"))]
pub free_space: Number,
pub unmapped_folders: Option<Vec<UnmappedFolder>>,
}
#[derive(Deserialize, Default, Debug, Clone, Eq, PartialEq)]
pub struct UnmappedFolder {
pub name: String,
pub path: String,
pub struct AddMovieBody {
pub tmdb_id: u64,
pub title: String,
pub root_folder_path: String,
pub quality_profile_id: u64,
pub minimum_availability: String,
pub monitored: bool,
pub tags: Vec<u64>,
pub add_options: AddOptions,
}
#[derive(Derivative, Deserialize, Debug, Clone, PartialEq, Eq)]
#[derivative(Default)]
#[serde(rename_all = "camelCase")]
pub struct Movie {
pub struct AddMovieSearchResult {
#[derivative(Default(value = "Number::from(0)"))]
pub id: Number,
pub tmdb_id: Number,
pub title: HorizontallyScrollableText,
pub original_language: Language,
#[derivative(Default(value = "Number::from(0)"))]
pub size_on_disk: Number,
pub status: String,
pub overview: String,
pub path: String,
pub studio: String,
pub genres: Vec<String>,
#[derivative(Default(value = "Number::from(0)"))]
pub year: Number,
pub monitored: bool,
pub has_file: bool,
#[derivative(Default(value = "Number::from(0)"))]
pub runtime: Number,
#[derivative(Default(value = "Number::from(0)"))]
pub tmdb_id: Number,
#[derivative(Default(value = "Number::from(0)"))]
pub quality_profile_id: Number,
pub minimum_availability: MinimumAvailability,
pub certification: Option<String>,
pub tags: Vec<Number>,
pub ratings: RatingsList,
pub movie_file: Option<MovieFile>,
pub collection: Option<Collection>,
}
#[derive(Derivative, Deserialize, Debug, Clone, PartialEq, Eq)]
#[derivative(Default)]
#[derive(Default, Serialize, Debug, PartialEq, Eq)]
#[serde(rename_all = "camelCase")]
pub struct CollectionMovie {
pub title: HorizontallyScrollableText,
pub overview: String,
#[derivative(Default(value = "Number::from(0)"))]
pub year: Number,
#[derivative(Default(value = "Number::from(0)"))]
pub runtime: Number,
#[derivative(Default(value = "Number::from(0)"))]
pub tmdb_id: Number,
pub genres: Vec<String>,
pub ratings: RatingsList,
pub struct AddOptions {
pub monitor: String,
pub search_for_movie: bool,
}
#[derive(Default, Serialize, Debug)]
pub struct AddRootFolderBody {
pub path: String,
}
#[derive(Deserialize, Derivative, Clone, Debug, PartialEq, Eq)]
@@ -112,14 +73,148 @@ pub struct Collection {
pub movies: Option<Vec<CollectionMovie>>,
}
#[derive(Deserialize, Derivative, Debug, Clone, PartialEq, Eq)]
#[derive(Derivative, Deserialize, Debug, Clone, PartialEq, Eq)]
#[derivative(Default)]
#[serde(rename_all = "camelCase")]
pub struct MovieFile {
pub relative_path: String,
pub path: String,
pub date_added: DateTime<Utc>,
pub media_info: Option<MediaInfo>,
pub struct CollectionMovie {
pub title: HorizontallyScrollableText,
pub overview: String,
#[derivative(Default(value = "Number::from(0)"))]
pub year: Number,
#[derivative(Default(value = "Number::from(0)"))]
pub runtime: Number,
#[derivative(Default(value = "Number::from(0)"))]
pub tmdb_id: Number,
pub genres: Vec<String>,
pub ratings: RatingsList,
}
#[derive(Default, Derivative, Serialize, Debug)]
#[serde(rename_all = "camelCase")]
pub struct CommandBody {
pub name: String,
}
#[derive(Deserialize, Default, Clone, Debug, PartialEq, Eq)]
#[serde(rename_all = "camelCase")]
pub struct Credit {
pub person_name: String,
pub character: Option<String>,
pub department: Option<String>,
pub job: Option<String>,
#[serde(rename(deserialize = "type"))]
pub credit_type: CreditType,
}
#[derive(Deserialize, Default, PartialEq, Eq, Clone, Debug)]
#[serde(rename_all = "lowercase")]
pub enum CreditType {
#[default]
Cast,
Crew,
}
#[derive(Deserialize, Debug, Clone, Eq, PartialEq)]
#[serde(rename_all = "camelCase")]
pub struct DiskSpace {
pub free_space: Number,
pub total_space: Number,
}
#[derive(Derivative, Deserialize, Debug, Clone, PartialEq, Eq)]
#[derivative(Default)]
#[serde(rename_all = "camelCase")]
pub struct DownloadRecord {
pub title: String,
pub status: String,
#[derivative(Default(value = "Number::from(0)"))]
pub id: Number,
#[derivative(Default(value = "Number::from(0)"))]
pub movie_id: Number,
#[derivative(Default(value = "Number::from(0)"))]
pub size: Number,
#[derivative(Default(value = "Number::from(0)"))]
pub sizeleft: Number,
pub output_path: Option<HorizontallyScrollableText>,
pub indexer: String,
pub download_client: String,
}
#[derive(Derivative, Deserialize, Debug)]
#[derivative(Default)]
#[serde(rename_all = "camelCase")]
pub struct DownloadsResponse {
pub records: Vec<DownloadRecord>,
}
#[derive(Derivative, Deserialize, Serialize, Debug, Clone, Eq, PartialEq)]
#[derivative(Default)]
#[serde(rename_all = "camelCase")]
pub struct Indexer {
#[derivative(Default(value = "Number::from(0)"))]
pub id: Number,
pub name: Option<String>,
pub implementation: Option<String>,
pub implementation_name: Option<String>,
pub config_contract: Option<String>,
pub supports_rss: bool,
pub supports_search: bool,
pub fields: Option<Vec<IndexerField>>,
pub enable_rss: bool,
pub enable_automatic_search: bool,
pub enable_interactive_search: bool,
pub protocol: String,
#[derivative(Default(value = "Number::from(0)"))]
pub priority: Number,
#[derivative(Default(value = "Number::from(0)"))]
pub download_client_id: Number,
pub tags: Option<Vec<String>>,
}
#[derive(Derivative, Deserialize, Serialize, Debug, Clone, Eq, PartialEq)]
#[derivative(Default)]
#[serde(rename_all = "camelCase")]
pub struct IndexerField {
#[derivative(Default(value = "Number::from(0)"))]
pub order: Number,
pub name: Option<String>,
pub label: Option<String>,
pub value: Option<Value>,
pub advanced: bool,
pub select_options: Option<Vec<IndexerSelectOption>>,
}
#[derive(Derivative, Deserialize, Serialize, Debug, Clone, Eq, PartialEq)]
#[derivative(Default)]
#[serde(rename_all = "camelCase")]
pub struct IndexerSelectOption {
#[derivative(Default(value = "Number::from(0)"))]
pub value: Number,
pub name: Option<String>,
#[derivative(Default(value = "Number::from(0)"))]
pub order: Number,
}
#[derive(Deserialize, Default, Debug, Clone, PartialEq, Eq, Ord, PartialOrd)]
pub struct Language {
pub name: String,
}
#[derive(Default, Deserialize, Clone, Debug, Eq, PartialEq)]
#[serde(rename_all = "camelCase")]
pub struct Log {
pub time: DateTime<Utc>,
pub exception: Option<String>,
pub exception_type: Option<String>,
pub level: String,
pub logger: Option<String>,
pub message: Option<String>,
pub method: Option<String>,
}
#[derive(Default, Deserialize, Debug, Eq, PartialEq)]
pub struct LogResponse {
pub records: Vec<Log>,
}
#[derive(Deserialize, Derivative, Debug, Clone, PartialEq, Eq)]
@@ -146,206 +241,6 @@ pub struct MediaInfo {
pub scan_type: String,
}
#[derive(Default, Deserialize, Debug, Clone, PartialEq, Eq)]
#[serde(rename_all = "camelCase")]
pub struct RatingsList {
pub imdb: Option<Rating>,
pub tmdb: Option<Rating>,
pub rotten_tomatoes: Option<Rating>,
}
#[derive(Derivative, Deserialize, Debug, Clone, PartialEq, Eq)]
#[derivative(Default)]
pub struct Rating {
#[derivative(Default(value = "Number::from(0)"))]
pub value: Number,
}
#[derive(Derivative, Deserialize, Debug)]
#[derivative(Default)]
#[serde(rename_all = "camelCase")]
pub struct DownloadsResponse {
pub records: Vec<DownloadRecord>,
}
#[derive(Derivative, Deserialize, Debug, Clone, PartialEq, Eq)]
#[derivative(Default)]
#[serde(rename_all = "camelCase")]
pub struct DownloadRecord {
pub title: String,
pub status: String,
#[derivative(Default(value = "Number::from(0)"))]
pub id: Number,
#[derivative(Default(value = "Number::from(0)"))]
pub movie_id: Number,
#[derivative(Default(value = "Number::from(0)"))]
pub size: Number,
#[derivative(Default(value = "Number::from(0)"))]
pub sizeleft: Number,
pub output_path: Option<HorizontallyScrollableText>,
pub indexer: String,
pub download_client: String,
}
#[derive(Derivative, Deserialize, Debug)]
#[derivative(Default)]
pub struct QualityProfile {
#[derivative(Default(value = "Number::from(0)"))]
pub id: Number,
pub name: String,
}
#[derive(Derivative, Deserialize, Debug)]
#[derivative(Default)]
pub struct Tag {
#[derivative(Default(value = "Number::from(0)"))]
pub id: Number,
pub label: String,
}
#[derive(Deserialize, Default, Debug, Clone, PartialEq, Eq)]
#[serde(rename_all = "camelCase")]
pub struct MovieHistoryItem {
pub source_title: HorizontallyScrollableText,
pub quality: QualityWrapper,
pub languages: Vec<Language>,
pub date: DateTime<Utc>,
pub event_type: String,
}
#[derive(Deserialize, Default, Debug, Clone, PartialEq, Eq, Ord, PartialOrd)]
pub struct Language {
pub name: String,
}
#[derive(Deserialize, Default, Debug, Clone, PartialEq, Eq, Ord, PartialOrd)]
pub struct Quality {
pub name: String,
}
#[derive(Deserialize, Default, Debug, Clone, PartialEq, Eq, Ord, PartialOrd)]
pub struct QualityWrapper {
pub quality: Quality,
}
#[derive(Deserialize, Default, PartialEq, Eq, Clone, Debug)]
#[serde(rename_all = "lowercase")]
pub enum CreditType {
#[default]
Cast,
Crew,
}
#[derive(Deserialize, Default, Clone, Debug, PartialEq, Eq)]
#[serde(rename_all = "camelCase")]
pub struct Credit {
pub person_name: String,
pub character: Option<String>,
pub department: Option<String>,
pub job: Option<String>,
#[serde(rename(deserialize = "type"))]
pub credit_type: CreditType,
}
#[derive(Deserialize, Derivative, Clone, Debug, PartialEq, Eq)]
#[derivative(Default)]
#[serde(rename_all = "camelCase")]
pub struct Release {
pub guid: String,
pub protocol: String,
#[derivative(Default(value = "Number::from(0)"))]
pub age: Number,
pub title: HorizontallyScrollableText,
pub indexer: String,
#[derivative(Default(value = "Number::from(0)"))]
pub indexer_id: Number,
#[derivative(Default(value = "Number::from(0)"))]
pub size: Number,
pub rejected: bool,
pub rejections: Option<Vec<String>>,
pub seeders: Option<Number>,
pub leechers: Option<Number>,
pub languages: Option<Vec<Language>>,
pub quality: QualityWrapper,
}
#[derive(Default, PartialEq, Eq, Clone, Copy, Debug, EnumIter, Display)]
pub enum ReleaseField {
#[default]
Source,
Age,
Rejected,
Title,
Indexer,
Size,
Peers,
Language,
Quality,
}
#[derive(Default, Serialize, Debug)]
#[serde(rename_all = "camelCase")]
pub struct AddMovieBody {
pub tmdb_id: u64,
pub title: String,
pub root_folder_path: String,
pub quality_profile_id: u64,
pub minimum_availability: String,
pub monitored: bool,
pub tags: Vec<u64>,
pub add_options: AddOptions,
}
#[derive(Default, Serialize, Debug, PartialEq, Eq)]
#[serde(rename_all = "camelCase")]
pub struct AddOptions {
pub monitor: String,
pub search_for_movie: bool,
}
#[derive(Default, Serialize, Debug)]
#[serde(rename_all = "camelCase")]
pub struct ReleaseDownloadBody {
pub guid: String,
pub indexer_id: u64,
}
#[derive(Derivative, Deserialize, Debug, Clone, PartialEq, Eq)]
#[derivative(Default)]
#[serde(rename_all = "camelCase")]
pub struct AddMovieSearchResult {
#[derivative(Default(value = "Number::from(0)"))]
pub tmdb_id: Number,
pub title: HorizontallyScrollableText,
pub original_language: Language,
pub status: String,
pub overview: String,
pub genres: Vec<String>,
#[derivative(Default(value = "Number::from(0)"))]
pub year: Number,
#[derivative(Default(value = "Number::from(0)"))]
pub runtime: Number,
pub ratings: RatingsList,
}
#[derive(Default, Serialize, Debug)]
pub struct AddRootFolderBody {
pub path: String,
}
#[derive(Default, Derivative, Serialize, Debug)]
#[serde(rename_all = "camelCase")]
pub struct MovieCommandBody {
pub name: String,
pub movie_ids: Vec<u64>,
}
#[derive(Default, Derivative, Serialize, Debug)]
#[serde(rename_all = "camelCase")]
pub struct CommandBody {
pub name: String,
}
#[derive(Serialize, Deserialize, Default, PartialEq, Eq, Clone, Copy, Debug, EnumIter)]
#[serde(rename_all = "camelCase")]
pub enum MinimumAvailability {
@@ -408,21 +303,181 @@ impl Monitor {
}
}
#[derive(Default, Deserialize, Clone, Debug, Eq, PartialEq)]
#[derive(Derivative, Deserialize, Debug, Clone, PartialEq, Eq)]
#[derivative(Default)]
#[serde(rename_all = "camelCase")]
pub struct Log {
pub time: DateTime<Utc>,
pub exception: Option<String>,
pub exception_type: Option<String>,
pub level: String,
pub logger: Option<String>,
pub message: Option<String>,
pub method: Option<String>,
pub struct Movie {
#[derivative(Default(value = "Number::from(0)"))]
pub id: Number,
pub title: HorizontallyScrollableText,
pub original_language: Language,
#[derivative(Default(value = "Number::from(0)"))]
pub size_on_disk: Number,
pub status: String,
pub overview: String,
pub path: String,
pub studio: String,
pub genres: Vec<String>,
#[derivative(Default(value = "Number::from(0)"))]
pub year: Number,
pub monitored: bool,
pub has_file: bool,
#[derivative(Default(value = "Number::from(0)"))]
pub runtime: Number,
#[derivative(Default(value = "Number::from(0)"))]
pub tmdb_id: Number,
#[derivative(Default(value = "Number::from(0)"))]
pub quality_profile_id: Number,
pub minimum_availability: MinimumAvailability,
pub certification: Option<String>,
pub tags: Vec<Number>,
pub ratings: RatingsList,
pub movie_file: Option<MovieFile>,
pub collection: Option<Collection>,
}
#[derive(Default, Deserialize, Debug, Eq, PartialEq)]
pub struct LogResponse {
pub records: Vec<Log>,
#[derive(Default, Derivative, Serialize, Debug)]
#[serde(rename_all = "camelCase")]
pub struct MovieCommandBody {
pub name: String,
pub movie_ids: Vec<u64>,
}
#[derive(Deserialize, Derivative, Debug, Clone, PartialEq, Eq)]
#[derivative(Default)]
#[serde(rename_all = "camelCase")]
pub struct MovieFile {
pub relative_path: String,
pub path: String,
pub date_added: DateTime<Utc>,
pub media_info: Option<MediaInfo>,
}
#[derive(Deserialize, Default, Debug, Clone, PartialEq, Eq)]
#[serde(rename_all = "camelCase")]
pub struct MovieHistoryItem {
pub source_title: HorizontallyScrollableText,
pub quality: QualityWrapper,
pub languages: Vec<Language>,
pub date: DateTime<Utc>,
pub event_type: String,
}
#[derive(Deserialize, Default, Debug, Clone, PartialEq, Eq, Ord, PartialOrd)]
pub struct Quality {
pub name: String,
}
#[derive(Derivative, Deserialize, Debug)]
#[derivative(Default)]
pub struct QualityProfile {
#[derivative(Default(value = "Number::from(0)"))]
pub id: Number,
pub name: String,
}
#[derive(Deserialize, Default, Debug, Clone, PartialEq, Eq, Ord, PartialOrd)]
pub struct QualityWrapper {
pub quality: Quality,
}
#[derive(Default, Deserialize, Debug, Clone, PartialEq, Eq)]
#[serde(rename_all = "camelCase")]
pub struct QueueEvent {
pub trigger: String,
pub name: String,
pub command_name: String,
pub status: String,
pub queued: DateTime<Utc>,
pub started: Option<DateTime<Utc>>,
pub ended: Option<DateTime<Utc>>,
pub duration: Option<String>,
}
#[derive(Derivative, Deserialize, Debug, Clone, PartialEq, Eq)]
#[derivative(Default)]
pub struct Rating {
#[derivative(Default(value = "Number::from(0)"))]
pub value: Number,
}
#[derive(Default, Deserialize, Debug, Clone, PartialEq, Eq)]
#[serde(rename_all = "camelCase")]
pub struct RatingsList {
pub imdb: Option<Rating>,
pub tmdb: Option<Rating>,
pub rotten_tomatoes: Option<Rating>,
}
#[derive(Deserialize, Derivative, Clone, Debug, PartialEq, Eq)]
#[derivative(Default)]
#[serde(rename_all = "camelCase")]
pub struct Release {
pub guid: String,
pub protocol: String,
#[derivative(Default(value = "Number::from(0)"))]
pub age: Number,
pub title: HorizontallyScrollableText,
pub indexer: String,
#[derivative(Default(value = "Number::from(0)"))]
pub indexer_id: Number,
#[derivative(Default(value = "Number::from(0)"))]
pub size: Number,
pub rejected: bool,
pub rejections: Option<Vec<String>>,
pub seeders: Option<Number>,
pub leechers: Option<Number>,
pub languages: Option<Vec<Language>>,
pub quality: QualityWrapper,
}
#[derive(Default, Serialize, Debug)]
#[serde(rename_all = "camelCase")]
pub struct ReleaseDownloadBody {
pub guid: String,
pub indexer_id: u64,
}
#[derive(Default, PartialEq, Eq, Clone, Copy, Debug, EnumIter, Display)]
pub enum ReleaseField {
#[default]
Source,
Age,
Rejected,
Title,
Indexer,
Size,
Peers,
Language,
Quality,
}
#[derive(Derivative, Deserialize, Debug, Clone, Eq, PartialEq)]
#[derivative(Default)]
#[serde(rename_all = "camelCase")]
pub struct RootFolder {
#[derivative(Default(value = "Number::from(0)"))]
pub id: Number,
pub path: String,
pub accessible: bool,
#[derivative(Default(value = "Number::from(0)"))]
pub free_space: Number,
pub unmapped_folders: Option<Vec<UnmappedFolder>>,
}
#[derive(Deserialize, Debug)]
#[serde(rename_all = "camelCase")]
pub struct SystemStatus {
pub version: String,
pub start_time: DateTime<Utc>,
}
#[derive(Derivative, Deserialize, Debug)]
#[derivative(Default)]
pub struct Tag {
#[derivative(Default(value = "Number::from(0)"))]
pub id: Number,
pub label: String,
}
#[derive(Derivative, Deserialize, Debug, Clone, PartialEq, Eq)]
@@ -438,17 +493,10 @@ pub struct Task {
pub next_execution: DateTime<Utc>,
}
#[derive(Default, Deserialize, Debug, Clone, PartialEq, Eq)]
#[serde(rename_all = "camelCase")]
pub struct QueueEvent {
pub trigger: String,
#[derive(Deserialize, Default, Debug, Clone, Eq, PartialEq)]
pub struct UnmappedFolder {
pub name: String,
pub command_name: String,
pub status: String,
pub queued: DateTime<Utc>,
pub started: Option<DateTime<Utc>>,
pub ended: Option<DateTime<Utc>>,
pub duration: Option<String>,
pub path: String,
}
#[derive(Default, Deserialize, Debug, Clone, PartialEq, Eq)]
File diff suppressed because it is too large Load Diff
+116 -2
View File
@@ -13,8 +13,8 @@ mod test {
use crate::app::radarr::ActiveRadarrBlock;
use crate::models::radarr_models::{
CollectionMovie, Language, MediaInfo, MinimumAvailability, Monitor, MovieFile, Quality,
QualityWrapper, Rating, RatingsList,
CollectionMovie, IndexerField, IndexerSelectOption, Language, MediaInfo, MinimumAvailability,
Monitor, MovieFile, Quality, QualityWrapper, Rating, RatingsList,
};
use crate::models::HorizontallyScrollableText;
use crate::App;
@@ -854,6 +854,71 @@ mod test {
);
}
#[tokio::test]
async fn test_handle_get_indexers_event() {
let indexers_response_json = json!([{
"enableRss": true,
"enableAutomaticSearch": true,
"enableInteractiveSearch": true,
"supportsRss": true,
"supportsSearch": true,
"protocol": "torrent",
"priority": 25,
"downloadClientId": 0,
"name": "Test Indexer",
"fields": [
{
"order": 0,
"name": "valueIsString",
"label": "Value Is String",
"value": "hello",
"advanced": false
},
{
"order": 1,
"name": "emptyValueWithSelectOptions",
"label": "Empty Value With Select Options",
"advanced": true,
"selectOptions": [
{
"value": -2,
"name": "Original",
"order": 0,
}
]
},
{
"order": 2,
"name": "valueIsAnArray",
"label": "Value is an array",
"value": [1, 2],
"advanced": false,
},
],
"implementationName": "Torznab",
"implementation": "Torznab",
"configContract": "TorznabSettings",
"tags": ["test_tag"],
"id": 1
}]);
let (async_server, app_arc, _server) = mock_radarr_api(
RequestMethod::Get,
None,
Some(indexers_response_json),
RadarrEvent::GetIndexers.resource(),
)
.await;
let network = Network::new(reqwest::Client::new(), &app_arc);
network.handle_radarr_event(RadarrEvent::GetIndexers).await;
async_server.assert_async().await;
assert_eq!(
app_arc.lock().await.data.radarr_data.indexers.items,
vec![indexer()]
);
}
#[tokio::test]
async fn test_handle_get_queued_events_event() {
let queued_events_json = json!([{
@@ -2125,4 +2190,53 @@ mod test {
credit_type: CreditType::Crew,
}
}
fn indexer() -> Indexer {
Indexer {
enable_rss: true,
enable_automatic_search: true,
enable_interactive_search: true,
supports_rss: true,
supports_search: true,
protocol: "torrent".to_owned(),
priority: Number::from(25),
download_client_id: Number::from(0),
name: Some("Test Indexer".to_owned()),
implementation_name: Some("Torznab".to_owned()),
implementation: Some("Torznab".to_owned()),
config_contract: Some("TorznabSettings".to_owned()),
tags: Some(vec!["test_tag".to_owned()]),
id: Number::from(1),
fields: Some(vec![
IndexerField {
order: Number::from(0),
name: Some("valueIsString".to_owned()),
label: Some("Value Is String".to_owned()),
value: Some(json!("hello")),
advanced: false,
select_options: None,
},
IndexerField {
order: Number::from(1),
name: Some("emptyValueWithSelectOptions".to_owned()),
label: Some("Empty Value With Select Options".to_owned()),
value: None,
advanced: true,
select_options: Some(vec![IndexerSelectOption {
value: Number::from(-2),
name: Some("Original".to_owned()),
order: Number::from(0),
}]),
},
IndexerField {
order: Number::from(2),
name: Some("valueIsAnArray".to_owned()),
label: Some("Value is an array".to_owned()),
value: Some(json!([1, 2])),
advanced: false,
select_options: None,
},
]),
}
}
}
+96
View File
@@ -0,0 +1,96 @@
use tui::backend::Backend;
use tui::layout::{Constraint, Rect};
use tui::text::Text;
use tui::widgets::{Cell, Row};
use tui::Frame;
use crate::app::radarr::ActiveRadarrBlock;
use crate::app::App;
use crate::models::radarr_models::Indexer;
use crate::models::Route;
use crate::ui::utils::{layout_block_top_border, style_failure, style_primary, style_success};
use crate::ui::{draw_table, DrawUi, TableProps};
pub(super) struct IndexersUi {}
impl DrawUi for IndexersUi {
fn draw<B: Backend>(f: &mut Frame<'_, B>, app: &mut App<'_>, content_rect: Rect) {
if matches!(
*app.get_current_route(),
Route::Radarr(ActiveRadarrBlock::Indexers, _)
) {
draw_indexers(f, app, content_rect);
}
}
}
fn draw_indexers<B: Backend>(f: &mut Frame<'_, B>, app: &mut App<'_>, area: Rect) {
draw_table(
f,
area,
layout_block_top_border(),
TableProps {
content: &mut app.data.radarr_data.indexers,
table_headers: vec![
"Indexer",
"RSS",
"Automatic Search",
"Interactive Search",
"Priority",
],
constraints: vec![
Constraint::Ratio(1, 5),
Constraint::Ratio(1, 5),
Constraint::Ratio(1, 5),
Constraint::Ratio(1, 5),
Constraint::Ratio(1, 5),
],
help: app
.data
.radarr_data
.main_tabs
.get_active_tab_contextual_help(),
},
|indexer: &'_ Indexer| {
let Indexer {
name,
enable_rss,
enable_automatic_search,
enable_interactive_search,
priority,
..
} = indexer;
let bool_to_text = |flag: bool| {
if flag {
return ("Enabled", style_success());
}
("Disabled", style_failure())
};
let (rss_text, rss_style) = bool_to_text(*enable_rss);
let mut rss = Text::from(rss_text);
rss.patch_style(rss_style);
let (auto_search_text, auto_search_style) = bool_to_text(*enable_automatic_search);
let mut automatic_search = Text::from(auto_search_text);
automatic_search.patch_style(auto_search_style);
let (interactive_search_text, interactive_search_style) =
bool_to_text(*enable_interactive_search);
let mut interactive_search = Text::from(interactive_search_text);
interactive_search.patch_style(interactive_search_style);
Row::new(vec![
Cell::from(name.clone().unwrap_or_default()),
Cell::from(rss),
Cell::from(automatic_search),
Cell::from(interactive_search),
Cell::from(priority.as_u64().unwrap().to_string()),
])
.style(style_primary())
},
app.is_loading,
true,
)
}
+3
View File
@@ -21,6 +21,7 @@ use crate::models::Route;
use crate::ui::draw_selectable_list;
use crate::ui::draw_tabs;
use crate::ui::loading;
use crate::ui::radarr_ui::indexers_ui::IndexersUi;
use crate::ui::radarr_ui::system_details_ui::SystemDetailsUi;
use crate::ui::radarr_ui::system_ui::SystemUi;
use crate::ui::radarr_ui::{
@@ -44,6 +45,7 @@ mod delete_movie_ui;
mod downloads_ui;
mod edit_collection_ui;
mod edit_movie_ui;
mod indexers_ui;
mod library_ui;
mod movie_details_ui;
mod radarr_ui_utils;
@@ -72,6 +74,7 @@ impl DrawUi for RadarrUi {
ActiveRadarrBlock::Downloads
| ActiveRadarrBlock::DeleteDownloadPrompt
| ActiveRadarrBlock::UpdateDownloadsPrompt => DownloadsUi::draw(f, app, content_rect),
ActiveRadarrBlock::Indexers => IndexersUi::draw(f, app, content_rect),
ActiveRadarrBlock::RootFolders
| ActiveRadarrBlock::AddRootFolderPrompt
| ActiveRadarrBlock::DeleteRootFolderPrompt => RootFoldersUi::draw(f, app, content_rect),