feat: Downloads tab support in Lidarr

This commit is contained in:
2026-01-13 13:40:18 -07:00
parent e1a25bfaf2
commit c68cd75015
32 changed files with 1551 additions and 165 deletions
+24
View File
@@ -12,6 +12,7 @@ mod tests {
async fn test_dispatch_by_lidarr_block_artists() {
let (tx, mut rx) = mpsc::channel::<NetworkEvent>(500);
let mut app = App::test_default();
app.data.lidarr_data.prompt_confirm = true;
app.network_tx = Some(tx);
app
@@ -37,6 +38,7 @@ mod tests {
async fn test_dispatch_by_lidarr_block_artist_details() {
let (tx, mut rx) = mpsc::channel::<NetworkEvent>(500);
let mut app = App::test_default();
app.data.lidarr_data.prompt_confirm = true;
app.data.lidarr_data.artists.set_items(vec![artist()]);
app.network_tx = Some(tx);
@@ -50,10 +52,31 @@ mod tests {
assert_eq!(app.tick_count, 0);
}
#[tokio::test]
async fn test_dispatch_by_downloads_block() {
let (tx, mut rx) = mpsc::channel::<NetworkEvent>(500);
let mut app = App::test_default();
app.data.lidarr_data.prompt_confirm = true;
app.network_tx = Some(tx);
app
.dispatch_by_lidarr_block(&ActiveLidarrBlock::Downloads)
.await;
assert!(app.is_loading);
assert_eq!(
rx.recv().await.unwrap(),
LidarrEvent::GetDownloads(500).into()
);
assert!(!app.data.lidarr_data.prompt_confirm);
assert_eq!(app.tick_count, 0);
}
#[tokio::test]
async fn test_dispatch_by_lidarr_block_add_artist_search_results() {
let (tx, mut rx) = mpsc::channel::<NetworkEvent>(500);
let mut app = App::test_default();
app.data.lidarr_data.prompt_confirm = true;
app.network_tx = Some(tx);
app.data.lidarr_data.add_artist_search = Some("test artist".into());
@@ -74,6 +97,7 @@ mod tests {
async fn test_dispatch_by_history_block() {
let (tx, mut rx) = mpsc::channel::<NetworkEvent>(500);
let mut app = App::test_default();
app.data.lidarr_data.prompt_confirm = true;
app.network_tx = Some(tx);
app
+5
View File
@@ -28,6 +28,11 @@ impl App<'_> {
.dispatch_network_event(LidarrEvent::ListArtists.into())
.await;
}
ActiveLidarrBlock::Downloads => {
self
.dispatch_network_event(LidarrEvent::GetDownloads(500).into())
.await;
}
ActiveLidarrBlock::ArtistDetails => {
self
.dispatch_network_event(LidarrEvent::GetAlbums(self.extract_artist_id().await).into())
+12
View File
@@ -37,6 +37,11 @@ pub enum LidarrDeleteCommand {
#[arg(long, help = "Add a list exclusion for this artist")]
add_list_exclusion: bool,
},
#[command(about = "Delete the specified download")]
Download {
#[arg(long, help = "The ID of the download to delete", required = true)]
download_id: i64,
},
#[command(about = "Delete the tag with the specified ID")]
Tag {
#[arg(long, help = "The ID of the tag to delete", required = true)]
@@ -103,6 +108,13 @@ impl<'a, 'b> CliCommandHandler<'a, 'b, LidarrDeleteCommand> for LidarrDeleteComm
.await?;
serde_json::to_string_pretty(&resp)?
}
LidarrDeleteCommand::Download { download_id } => {
let resp = self
.network
.handle_network_event(LidarrEvent::DeleteDownload(download_id).into())
.await?;
serde_json::to_string_pretty(&resp)?
}
LidarrDeleteCommand::Tag { tag_id } => {
let resp = self
.network
@@ -145,6 +145,40 @@ mod tests {
assert_eq!(delete_command, expected_args);
}
#[test]
fn test_delete_download_requires_arguments() {
let result =
Cli::command().try_get_matches_from(["managarr", "lidarr", "delete", "download"]);
assert_err!(&result);
assert_eq!(
result.unwrap_err().kind(),
ErrorKind::MissingRequiredArgument
);
}
#[test]
fn test_delete_download_success() {
let expected_args = LidarrDeleteCommand::Download { download_id: 1 };
let result = Cli::try_parse_from([
"managarr",
"lidarr",
"delete",
"download",
"--download-id",
"1",
]);
assert_ok!(&result);
let Some(Command::Lidarr(LidarrCommand::Delete(delete_command))) = result.unwrap().command
else {
panic!("Unexpected command type");
};
assert_eq!(delete_command, expected_args);
}
#[test]
fn test_delete_tag_requires_arguments() {
let result = Cli::command().try_get_matches_from(["managarr", "lidarr", "delete", "tag"]);
@@ -260,6 +294,32 @@ mod tests {
assert_ok!(&result);
}
#[tokio::test]
async fn test_handle_delete_download_command() {
let expected_download_id = 1;
let mut mock_network = MockNetworkTrait::new();
mock_network
.expect_handle_network_event()
.with(eq::<NetworkEvent>(
LidarrEvent::DeleteDownload(expected_download_id).into(),
))
.times(1)
.returning(|_| {
Ok(Serdeable::Lidarr(LidarrSerdeable::Value(
json!({"testResponse": "response"}),
)))
});
let app_arc = Arc::new(Mutex::new(App::test_default()));
let delete_download_command = LidarrDeleteCommand::Download { download_id: 1 };
let result =
LidarrDeleteCommandHandler::with(&app_arc, delete_download_command, &mut mock_network)
.handle()
.await;
assert_ok!(&result);
}
#[tokio::test]
async fn test_handle_delete_tag_command() {
let expected_tag_id = 1;
+12
View File
@@ -29,6 +29,11 @@ pub enum LidarrListCommand {
},
#[command(about = "List all artists in your Lidarr library")]
Artists,
#[command(about = "List all active downloads in Lidarr")]
Downloads {
#[arg(long, help = "How many downloads to fetch", default_value_t = 500)]
count: u64,
},
#[command(about = "Fetch all Lidarr history events")]
History {
#[arg(long, help = "How many history events to fetch", default_value_t = 500)]
@@ -83,6 +88,13 @@ impl<'a, 'b> CliCommandHandler<'a, 'b, LidarrListCommand> for LidarrListCommandH
.await?;
serde_json::to_string_pretty(&resp)?
}
LidarrListCommand::Downloads { count } => {
let resp = self
.network
.handle_network_event(LidarrEvent::GetDownloads(count).into())
.await?;
serde_json::to_string_pretty(&resp)?
}
LidarrListCommand::History { events: items } => {
let resp = self
.network
@@ -58,6 +58,29 @@ mod tests {
assert_eq!(album_command, expected_args);
}
#[test]
fn test_list_downloads_count_flag_requires_arguments() {
let result =
Cli::command().try_get_matches_from(["managarr", "lidarr", "list", "downloads", "--count"]);
assert_err!(&result);
assert_eq!(result.unwrap_err().kind(), ErrorKind::InvalidValue);
}
#[test]
fn test_list_downloads_default_values() {
let expected_args = LidarrListCommand::Downloads { count: 500 };
let result = Cli::try_parse_from(["managarr", "lidarr", "list", "downloads"]);
assert_ok!(&result);
let Some(Command::Lidarr(LidarrCommand::List(downloads_command))) = result.unwrap().command
else {
panic!("Unexpected command type");
};
assert_eq!(downloads_command, expected_args);
}
#[test]
fn test_list_history_events_flag_requires_arguments() {
let result =
@@ -151,6 +174,32 @@ mod tests {
assert_ok!(&result);
}
#[tokio::test]
async fn test_handle_list_downloads_command() {
let expected_count = 1000;
let mut mock_network = MockNetworkTrait::new();
mock_network
.expect_handle_network_event()
.with(eq::<NetworkEvent>(
LidarrEvent::GetDownloads(expected_count).into(),
))
.times(1)
.returning(|_| {
Ok(Serdeable::Lidarr(LidarrSerdeable::Value(
json!({"testResponse": "response"}),
)))
});
let app_arc = Arc::new(Mutex::new(App::test_default()));
let list_downloads_command = LidarrListCommand::Downloads { count: 1000 };
let result =
LidarrListCommandHandler::with(&app_arc, list_downloads_command, &mut mock_network)
.handle()
.await;
assert_ok!(&result);
}
#[tokio::test]
async fn test_handle_list_history_command() {
let expected_events = 1000;
@@ -28,6 +28,8 @@ pub enum LidarrRefreshCommand {
)]
artist_id: i64,
},
#[command(about = "Refresh all downloads in Lidarr")]
Downloads,
}
impl From<LidarrRefreshCommand> for Command {
@@ -73,6 +75,13 @@ impl<'a, 'b> CliCommandHandler<'a, 'b, LidarrRefreshCommand>
.await?;
serde_json::to_string_pretty(&resp)?
}
LidarrRefreshCommand::Downloads => {
let resp = self
.network
.handle_network_event(LidarrEvent::UpdateDownloads.into())
.await?;
serde_json::to_string_pretty(&resp)?
}
};
Ok(result)
@@ -22,11 +22,14 @@ mod tests {
use super::*;
use clap::{Parser, error::ErrorKind};
use pretty_assertions::assert_eq;
use rstest::rstest;
#[test]
fn test_refresh_all_artists_has_no_arg_requirements() {
#[rstest]
fn test_refresh_commands_have_no_arg_requirements(
#[values("all-artists", "downloads")] subcommand: &str,
) {
let result =
Cli::command().try_get_matches_from(["managarr", "lidarr", "refresh", "all-artists"]);
Cli::command().try_get_matches_from(["managarr", "lidarr", "refresh", subcommand]);
assert_ok!(&result);
}
@@ -67,6 +70,7 @@ mod tests {
use std::sync::Arc;
use mockall::predicate::eq;
use rstest::rstest;
use serde_json::json;
use tokio::sync::Mutex;
@@ -80,12 +84,18 @@ mod tests {
network::{MockNetworkTrait, NetworkEvent},
};
#[rstest]
#[case(LidarrRefreshCommand::AllArtists, LidarrEvent::UpdateAllArtists)]
#[case(LidarrRefreshCommand::Downloads, LidarrEvent::UpdateDownloads)]
#[tokio::test]
async fn test_handle_refresh_all_artists_command() {
async fn test_handle_refresh_command(
#[case] refresh_command: LidarrRefreshCommand,
#[case] expected_sonarr_event: LidarrEvent,
) {
let mut mock_network = MockNetworkTrait::new();
mock_network
.expect_handle_network_event()
.with(eq::<NetworkEvent>(LidarrEvent::UpdateAllArtists.into()))
.with(eq::<NetworkEvent>(expected_sonarr_event.into()))
.times(1)
.returning(|_| {
Ok(Serdeable::Lidarr(LidarrSerdeable::Value(
@@ -93,7 +103,6 @@ mod tests {
)))
});
let app_arc = Arc::new(Mutex::new(App::test_default()));
let refresh_command = LidarrRefreshCommand::AllArtists;
let result = LidarrRefreshCommandHandler::with(&app_arc, refresh_command, &mut mock_network)
.handle()
+1 -87
View File
@@ -330,93 +330,7 @@ mod test_utils {
#[macro_export]
macro_rules! test_handler_delegation {
($handler:ident, $base:expr, $active_block:expr) => {
let mut app = App::test_default();
app.data.sonarr_data.history.set_items(vec![
$crate::models::sonarr_models::SonarrHistoryItem::default(),
]);
app
.data
.sonarr_data
.root_folders
.set_items(vec![$crate::models::servarr_models::RootFolder::default()]);
app
.data
.sonarr_data
.indexers
.set_items(vec![$crate::models::servarr_models::Indexer::default()]);
app
.data
.sonarr_data
.blocklist
.set_items(vec![$crate::models::sonarr_models::BlocklistItem::default()]);
app.data.sonarr_data.add_searched_series =
Some($crate::models::stateful_table::StatefulTable::default());
app
.data
.radarr_data
.movies
.set_items(vec![$crate::models::radarr_models::Movie::default()]);
app.data.radarr_data.history.set_items(vec![
$crate::models::radarr_models::RadarrHistoryItem::default(),
]);
app
.data
.radarr_data
.collections
.set_items(vec![$crate::models::radarr_models::Collection::default()]);
app.data.radarr_data.collection_movies.set_items(vec![
$crate::models::radarr_models::CollectionMovie::default(),
]);
app
.data
.radarr_data
.indexers
.set_items(vec![$crate::models::servarr_models::Indexer::default()]);
app
.data
.radarr_data
.root_folders
.set_items(vec![$crate::models::servarr_models::RootFolder::default()]);
app
.data
.radarr_data
.blocklist
.set_items(vec![$crate::models::radarr_models::BlocklistItem::default()]);
app.data.radarr_data.add_searched_movies =
Some($crate::models::stateful_table::StatefulTable::default());
let mut movie_details_modal =
$crate::models::servarr_data::radarr::modals::MovieDetailsModal::default();
movie_details_modal.movie_history.set_items(vec![
$crate::models::radarr_models::MovieHistoryItem::default(),
]);
movie_details_modal
.movie_cast
.set_items(vec![$crate::models::radarr_models::Credit::default()]);
movie_details_modal
.movie_crew
.set_items(vec![$crate::models::radarr_models::Credit::default()]);
movie_details_modal
.movie_releases
.set_items(vec![$crate::models::radarr_models::RadarrRelease::default()]);
app.data.radarr_data.movie_details_modal = Some(movie_details_modal);
let mut season_details_modal =
$crate::models::servarr_data::sonarr::modals::SeasonDetailsModal::default();
season_details_modal.season_history.set_items(vec![
$crate::models::sonarr_models::SonarrHistoryItem::default(),
]);
season_details_modal.episode_details_modal =
Some($crate::models::servarr_data::sonarr::modals::EpisodeDetailsModal::default());
app.data.sonarr_data.season_details_modal = Some(season_details_modal);
let mut series_history = $crate::models::stateful_table::StatefulTable::default();
series_history.set_items(vec![
$crate::models::sonarr_models::SonarrHistoryItem::default(),
]);
app.data.sonarr_data.series_history = Some(series_history);
app
.data
.sonarr_data
.series
.set_items(vec![$crate::models::sonarr_models::Series::default()]);
let mut app = App::test_default_fully_populated();
app.push_navigation_stack($base.into());
app.push_navigation_stack($active_block.into());
@@ -0,0 +1,481 @@
#[cfg(test)]
mod tests {
use pretty_assertions::assert_eq;
use rstest::rstest;
use strum::IntoEnumIterator;
use crate::app::App;
use crate::app::key_binding::DEFAULT_KEYBINDINGS;
use crate::assert_navigation_pushed;
use crate::event::Key;
use crate::handlers::KeyEventHandler;
use crate::handlers::lidarr_handlers::downloads::DownloadsHandler;
use crate::models::lidarr_models::DownloadRecord;
use crate::models::servarr_data::lidarr::lidarr_data::{ActiveLidarrBlock, DOWNLOADS_BLOCKS};
use crate::network::lidarr_network::lidarr_network_test_utils::test_utils::download_record;
mod test_handle_delete {
use pretty_assertions::assert_eq;
use super::*;
const DELETE_KEY: Key = DEFAULT_KEYBINDINGS.delete.key;
#[test]
fn test_delete_download_prompt() {
let mut app = App::test_default();
app.push_navigation_stack(ActiveLidarrBlock::Downloads.into());
app
.data
.lidarr_data
.downloads
.set_items(vec![DownloadRecord::default()]);
DownloadsHandler::new(DELETE_KEY, &mut app, ActiveLidarrBlock::Downloads, None).handle();
assert_navigation_pushed!(app, ActiveLidarrBlock::DeleteDownloadPrompt.into());
}
#[test]
fn test_delete_download_prompt_no_op_when_not_ready() {
let mut app = App::test_default();
app.is_loading = true;
app.push_navigation_stack(ActiveLidarrBlock::Downloads.into());
app
.data
.lidarr_data
.downloads
.set_items(vec![DownloadRecord::default()]);
DownloadsHandler::new(DELETE_KEY, &mut app, ActiveLidarrBlock::Downloads, None).handle();
assert_eq!(app.get_current_route(), ActiveLidarrBlock::Downloads.into());
}
}
mod test_handle_left_right_action {
use pretty_assertions::assert_eq;
use rstest::rstest;
use super::*;
use crate::assert_navigation_pushed;
#[rstest]
fn test_downloads_tab_left(#[values(true, false)] is_ready: bool) {
let mut app = App::test_default();
app.push_navigation_stack(ActiveLidarrBlock::Downloads.into());
app.is_loading = is_ready;
app.data.lidarr_data.main_tabs.set_index(1);
DownloadsHandler::new(
DEFAULT_KEYBINDINGS.left.key,
&mut app,
ActiveLidarrBlock::Downloads,
None,
)
.handle();
assert_eq!(
app.data.lidarr_data.main_tabs.get_active_route(),
ActiveLidarrBlock::Artists.into()
);
assert_navigation_pushed!(app, ActiveLidarrBlock::Artists.into());
}
#[rstest]
fn test_downloads_tab_right(#[values(true, false)] is_ready: bool) {
let mut app = App::test_default();
app.push_navigation_stack(ActiveLidarrBlock::Downloads.into());
app.is_loading = is_ready;
app.data.lidarr_data.main_tabs.set_index(1);
DownloadsHandler::new(
DEFAULT_KEYBINDINGS.right.key,
&mut app,
ActiveLidarrBlock::Downloads,
None,
)
.handle();
assert_eq!(
app.data.lidarr_data.main_tabs.get_active_route(),
ActiveLidarrBlock::History.into()
);
assert_navigation_pushed!(app, ActiveLidarrBlock::History.into());
}
#[rstest]
fn test_downloads_left_right_prompt_toggle(
#[values(
ActiveLidarrBlock::DeleteDownloadPrompt,
ActiveLidarrBlock::UpdateDownloadsPrompt
)]
active_lidarr_block: ActiveLidarrBlock,
#[values(DEFAULT_KEYBINDINGS.left.key, DEFAULT_KEYBINDINGS.right.key)] key: Key,
) {
let mut app = App::test_default();
app.push_navigation_stack(ActiveLidarrBlock::Downloads.into());
DownloadsHandler::new(key, &mut app, active_lidarr_block, None).handle();
assert!(app.data.lidarr_data.prompt_confirm);
DownloadsHandler::new(key, &mut app, active_lidarr_block, None).handle();
assert!(!app.data.lidarr_data.prompt_confirm);
}
}
mod test_handle_submit {
use rstest::rstest;
use crate::network::lidarr_network::LidarrEvent;
use super::*;
use crate::assert_navigation_popped;
use crate::network::lidarr_network::lidarr_network_test_utils::test_utils::download_record;
const SUBMIT_KEY: Key = DEFAULT_KEYBINDINGS.submit.key;
#[rstest]
#[case(
ActiveLidarrBlock::Downloads,
ActiveLidarrBlock::DeleteDownloadPrompt,
LidarrEvent::DeleteDownload(1)
)]
#[case(
ActiveLidarrBlock::Downloads,
ActiveLidarrBlock::UpdateDownloadsPrompt,
LidarrEvent::UpdateDownloads
)]
fn test_downloads_prompt_confirm_submit(
#[case] base_route: ActiveLidarrBlock,
#[case] prompt_block: ActiveLidarrBlock,
#[case] expected_action: LidarrEvent,
) {
let mut app = App::test_default();
app
.data
.lidarr_data
.downloads
.set_items(vec![download_record()]);
app.data.lidarr_data.prompt_confirm = true;
app.push_navigation_stack(base_route.into());
app.push_navigation_stack(prompt_block.into());
DownloadsHandler::new(SUBMIT_KEY, &mut app, prompt_block, None).handle();
assert!(app.data.lidarr_data.prompt_confirm);
assert_some_eq_x!(
&app.data.lidarr_data.prompt_confirm_action,
&expected_action
);
assert_navigation_popped!(app, base_route.into());
}
#[rstest]
#[case(ActiveLidarrBlock::Downloads, ActiveLidarrBlock::DeleteDownloadPrompt)]
#[case(ActiveLidarrBlock::Downloads, ActiveLidarrBlock::UpdateDownloadsPrompt)]
fn test_downloads_prompt_decline_submit(
#[case] base_route: ActiveLidarrBlock,
#[case] prompt_block: ActiveLidarrBlock,
) {
let mut app = App::test_default();
app
.data
.lidarr_data
.downloads
.set_items(vec![DownloadRecord::default()]);
app.push_navigation_stack(base_route.into());
app.push_navigation_stack(prompt_block.into());
DownloadsHandler::new(SUBMIT_KEY, &mut app, prompt_block, None).handle();
assert!(!app.data.lidarr_data.prompt_confirm);
assert_none!(app.data.lidarr_data.prompt_confirm_action);
assert_navigation_popped!(app, base_route.into());
}
}
mod test_handle_esc {
use rstest::rstest;
use super::*;
use crate::assert_navigation_popped;
const ESC_KEY: Key = DEFAULT_KEYBINDINGS.esc.key;
#[rstest]
#[case(ActiveLidarrBlock::Downloads, ActiveLidarrBlock::DeleteDownloadPrompt)]
#[case(ActiveLidarrBlock::Downloads, ActiveLidarrBlock::UpdateDownloadsPrompt)]
fn test_downloads_prompt_blocks_esc(
#[case] base_block: ActiveLidarrBlock,
#[case] prompt_block: ActiveLidarrBlock,
) {
let mut app = App::test_default();
app.push_navigation_stack(base_block.into());
app.push_navigation_stack(prompt_block.into());
app.data.lidarr_data.prompt_confirm = true;
DownloadsHandler::new(ESC_KEY, &mut app, prompt_block, None).handle();
assert_navigation_popped!(app, base_block.into());
assert!(!app.data.lidarr_data.prompt_confirm);
}
#[rstest]
fn test_default_esc(#[values(true, false)] is_ready: bool) {
let mut app = App::test_default();
app.is_loading = is_ready;
app.error = "test error".to_owned().into();
app.push_navigation_stack(ActiveLidarrBlock::Downloads.into());
app.push_navigation_stack(ActiveLidarrBlock::Downloads.into());
DownloadsHandler::new(ESC_KEY, &mut app, ActiveLidarrBlock::Downloads, None).handle();
assert_navigation_popped!(app, ActiveLidarrBlock::Downloads.into());
assert_is_empty!(app.error.text);
}
}
mod test_handle_key_char {
use super::*;
use crate::assert_navigation_popped;
use crate::network::lidarr_network::LidarrEvent;
use crate::network::lidarr_network::lidarr_network_test_utils::test_utils::download_record;
use pretty_assertions::assert_eq;
use rstest::rstest;
#[test]
fn test_update_downloads_key() {
let mut app = App::test_default();
app.push_navigation_stack(ActiveLidarrBlock::Downloads.into());
app
.data
.lidarr_data
.downloads
.set_items(vec![DownloadRecord::default()]);
DownloadsHandler::new(
DEFAULT_KEYBINDINGS.update.key,
&mut app,
ActiveLidarrBlock::Downloads,
None,
)
.handle();
assert_navigation_pushed!(app, ActiveLidarrBlock::UpdateDownloadsPrompt.into());
}
#[test]
fn test_update_downloads_key_no_op_when_not_ready() {
let mut app = App::test_default();
app.is_loading = true;
app.push_navigation_stack(ActiveLidarrBlock::Downloads.into());
app
.data
.lidarr_data
.downloads
.set_items(vec![DownloadRecord::default()]);
DownloadsHandler::new(
DEFAULT_KEYBINDINGS.update.key,
&mut app,
ActiveLidarrBlock::Downloads,
None,
)
.handle();
assert_eq!(app.get_current_route(), ActiveLidarrBlock::Downloads.into());
}
#[test]
fn test_refresh_downloads_key() {
let mut app = App::test_default();
app
.data
.lidarr_data
.downloads
.set_items(vec![DownloadRecord::default()]);
app.push_navigation_stack(ActiveLidarrBlock::Downloads.into());
DownloadsHandler::new(
DEFAULT_KEYBINDINGS.refresh.key,
&mut app,
ActiveLidarrBlock::Downloads,
None,
)
.handle();
assert_navigation_pushed!(app, ActiveLidarrBlock::Downloads.into());
assert!(app.should_refresh);
}
#[test]
fn test_refresh_downloads_key_no_op_when_not_ready() {
let mut app = App::test_default();
app.is_loading = true;
app.push_navigation_stack(ActiveLidarrBlock::Downloads.into());
app
.data
.lidarr_data
.downloads
.set_items(vec![DownloadRecord::default()]);
DownloadsHandler::new(
DEFAULT_KEYBINDINGS.refresh.key,
&mut app,
ActiveLidarrBlock::Downloads,
None,
)
.handle();
assert_eq!(app.get_current_route(), ActiveLidarrBlock::Downloads.into());
assert!(!app.should_refresh);
}
#[rstest]
#[case(
ActiveLidarrBlock::Downloads,
ActiveLidarrBlock::DeleteDownloadPrompt,
LidarrEvent::DeleteDownload(1)
)]
#[case(
ActiveLidarrBlock::Downloads,
ActiveLidarrBlock::UpdateDownloadsPrompt,
LidarrEvent::UpdateDownloads
)]
fn test_downloads_prompt_confirm_submit(
#[case] base_route: ActiveLidarrBlock,
#[case] prompt_block: ActiveLidarrBlock,
#[case] expected_action: LidarrEvent,
) {
let mut app = App::test_default();
app
.data
.lidarr_data
.downloads
.set_items(vec![download_record()]);
app.push_navigation_stack(base_route.into());
app.push_navigation_stack(prompt_block.into());
DownloadsHandler::new(
DEFAULT_KEYBINDINGS.confirm.key,
&mut app,
prompt_block,
None,
)
.handle();
assert!(app.data.lidarr_data.prompt_confirm);
assert_some_eq_x!(
&app.data.lidarr_data.prompt_confirm_action,
&expected_action
);
assert_navigation_popped!(app, base_route.into());
}
}
#[test]
fn test_downloads_handler_accepts() {
ActiveLidarrBlock::iter().for_each(|active_lidarr_block| {
if DOWNLOADS_BLOCKS.contains(&active_lidarr_block) {
assert!(DownloadsHandler::accepts(active_lidarr_block));
} else {
assert!(!DownloadsHandler::accepts(active_lidarr_block));
}
})
}
#[rstest]
fn test_downloads_handler_ignore_special_keys(
#[values(true, false)] ignore_special_keys_for_textbox_input: bool,
) {
let mut app = App::test_default();
app.ignore_special_keys_for_textbox_input = ignore_special_keys_for_textbox_input;
let handler = DownloadsHandler::new(
DEFAULT_KEYBINDINGS.esc.key,
&mut app,
ActiveLidarrBlock::default(),
None,
);
assert_eq!(
handler.ignore_special_keys(),
ignore_special_keys_for_textbox_input
);
}
#[test]
fn test_extract_download_id() {
let mut app = App::test_default();
app
.data
.lidarr_data
.downloads
.set_items(vec![download_record()]);
let download_id = DownloadsHandler::new(
DEFAULT_KEYBINDINGS.esc.key,
&mut app,
ActiveLidarrBlock::Downloads,
None,
)
.extract_download_id();
assert_eq!(download_id, 1);
}
#[test]
fn test_downloads_handler_not_ready_when_loading() {
let mut app = App::test_default();
app.push_navigation_stack(ActiveLidarrBlock::Downloads.into());
app.is_loading = true;
let handler = DownloadsHandler::new(
DEFAULT_KEYBINDINGS.esc.key,
&mut app,
ActiveLidarrBlock::Downloads,
None,
);
assert!(!handler.is_ready());
}
#[test]
fn test_downloads_handler_not_ready_when_downloads_is_empty() {
let mut app = App::test_default();
app.push_navigation_stack(ActiveLidarrBlock::Downloads.into());
app.is_loading = false;
let handler = DownloadsHandler::new(
DEFAULT_KEYBINDINGS.esc.key,
&mut app,
ActiveLidarrBlock::Downloads,
None,
);
assert!(!handler.is_ready());
}
#[test]
fn test_downloads_handler_ready_when_not_loading_and_downloads_is_not_empty() {
let mut app = App::test_default();
app.push_navigation_stack(ActiveLidarrBlock::Downloads.into());
app.is_loading = false;
app
.data
.lidarr_data
.downloads
.set_items(vec![DownloadRecord::default()]);
let handler = DownloadsHandler::new(
DEFAULT_KEYBINDINGS.esc.key,
&mut app,
ActiveLidarrBlock::Downloads,
None,
);
assert!(handler.is_ready());
}
}
@@ -0,0 +1,171 @@
use crate::app::App;
use crate::event::Key;
use crate::handlers::lidarr_handlers::handle_change_tab_left_right_keys;
use crate::handlers::table_handler::{TableHandlingConfig, handle_table};
use crate::handlers::{KeyEventHandler, handle_clear_errors, handle_prompt_toggle};
use crate::matches_key;
use crate::models::Route;
use crate::models::servarr_data::lidarr::lidarr_data::{ActiveLidarrBlock, DOWNLOADS_BLOCKS};
use crate::network::lidarr_network::LidarrEvent;
#[cfg(test)]
#[path = "downloads_handler_tests.rs"]
mod downloads_handler_tests;
pub(super) struct DownloadsHandler<'a, 'b> {
key: Key,
app: &'a mut App<'b>,
active_lidarr_block: ActiveLidarrBlock,
_context: Option<ActiveLidarrBlock>,
}
impl DownloadsHandler<'_, '_> {
fn extract_download_id(&self) -> i64 {
self.app.data.lidarr_data.downloads.current_selection().id
}
}
impl<'a, 'b> KeyEventHandler<'a, 'b, ActiveLidarrBlock> for DownloadsHandler<'a, 'b> {
fn handle(&mut self) {
let download_table_handling_config =
TableHandlingConfig::new(ActiveLidarrBlock::Downloads.into());
if !handle_table(
self,
|app| &mut app.data.lidarr_data.downloads,
download_table_handling_config,
) {
self.handle_key_event();
}
}
fn accepts(active_block: ActiveLidarrBlock) -> bool {
DOWNLOADS_BLOCKS.contains(&active_block)
}
fn ignore_special_keys(&self) -> bool {
self.app.ignore_special_keys_for_textbox_input
}
fn new(
key: Key,
app: &'a mut App<'b>,
active_block: ActiveLidarrBlock,
_context: Option<ActiveLidarrBlock>,
) -> DownloadsHandler<'a, 'b> {
DownloadsHandler {
key,
app,
active_lidarr_block: active_block,
_context,
}
}
fn get_key(&self) -> Key {
self.key
}
fn is_ready(&self) -> bool {
!self.app.is_loading && !self.app.data.lidarr_data.downloads.is_empty()
}
fn handle_scroll_up(&mut self) {}
fn handle_scroll_down(&mut self) {}
fn handle_home(&mut self) {}
fn handle_end(&mut self) {}
fn handle_delete(&mut self) {
if self.active_lidarr_block == ActiveLidarrBlock::Downloads {
self
.app
.push_navigation_stack(ActiveLidarrBlock::DeleteDownloadPrompt.into())
}
}
fn handle_left_right_action(&mut self) {
match self.active_lidarr_block {
ActiveLidarrBlock::Downloads => handle_change_tab_left_right_keys(self.app, self.key),
ActiveLidarrBlock::DeleteDownloadPrompt | ActiveLidarrBlock::UpdateDownloadsPrompt => {
handle_prompt_toggle(self.app, self.key)
}
_ => (),
}
}
fn handle_submit(&mut self) {
match self.active_lidarr_block {
ActiveLidarrBlock::DeleteDownloadPrompt => {
if self.app.data.lidarr_data.prompt_confirm {
self.app.data.lidarr_data.prompt_confirm_action =
Some(LidarrEvent::DeleteDownload(self.extract_download_id()));
}
self.app.pop_navigation_stack();
}
ActiveLidarrBlock::UpdateDownloadsPrompt => {
if self.app.data.lidarr_data.prompt_confirm {
self.app.data.lidarr_data.prompt_confirm_action = Some(LidarrEvent::UpdateDownloads);
}
self.app.pop_navigation_stack();
}
_ => (),
}
}
fn handle_esc(&mut self) {
match self.active_lidarr_block {
ActiveLidarrBlock::DeleteDownloadPrompt | ActiveLidarrBlock::UpdateDownloadsPrompt => {
self.app.pop_navigation_stack();
self.app.data.lidarr_data.prompt_confirm = false;
}
_ => handle_clear_errors(self.app),
}
}
fn handle_char_key_event(&mut self) {
let key = self.key;
match self.active_lidarr_block {
ActiveLidarrBlock::Downloads => match self.key {
_ if matches_key!(update, key) => {
self
.app
.push_navigation_stack(ActiveLidarrBlock::UpdateDownloadsPrompt.into());
}
_ if matches_key!(refresh, key) => {
self.app.should_refresh = true;
}
_ => (),
},
ActiveLidarrBlock::DeleteDownloadPrompt => {
if matches_key!(confirm, key) {
self.app.data.lidarr_data.prompt_confirm = true;
self.app.data.lidarr_data.prompt_confirm_action =
Some(LidarrEvent::DeleteDownload(self.extract_download_id()));
self.app.pop_navigation_stack();
}
}
ActiveLidarrBlock::UpdateDownloadsPrompt => {
if matches_key!(confirm, key) {
self.app.data.lidarr_data.prompt_confirm = true;
self.app.data.lidarr_data.prompt_confirm_action = Some(LidarrEvent::UpdateDownloads);
self.app.pop_navigation_stack();
}
}
_ => (),
}
}
fn app_mut(&mut self) -> &mut App<'b> {
self.app
}
fn current_route(&self) -> Route {
self.app.get_current_route()
}
}
@@ -29,7 +29,7 @@ mod tests {
let mut app = App::test_default();
app.push_navigation_stack(ActiveLidarrBlock::History.into());
app.is_loading = is_ready;
app.data.lidarr_data.main_tabs.set_index(1);
app.data.lidarr_data.main_tabs.set_index(2);
HistoryHandler::new(
DEFAULT_KEYBINDINGS.left.key,
@@ -41,9 +41,9 @@ mod tests {
assert_eq!(
app.data.lidarr_data.main_tabs.get_active_route(),
ActiveLidarrBlock::Artists.into()
ActiveLidarrBlock::Downloads.into()
);
assert_navigation_pushed!(app, ActiveLidarrBlock::Artists.into());
assert_navigation_pushed!(app, ActiveLidarrBlock::Downloads.into());
}
#[rstest]
@@ -51,7 +51,7 @@ mod tests {
let mut app = App::test_default();
app.push_navigation_stack(ActiveLidarrBlock::History.into());
app.is_loading = is_ready;
app.data.lidarr_data.main_tabs.set_index(1);
app.data.lidarr_data.main_tabs.set_index(2);
HistoryHandler::new(
DEFAULT_KEYBINDINGS.right.key,
@@ -2,13 +2,10 @@
mod tests {
use crate::app::App;
use crate::app::key_binding::DEFAULT_KEYBINDINGS;
use crate::assert_navigation_pushed;
use crate::handlers::KeyEventHandler;
use crate::handlers::lidarr_handlers::{LidarrHandler, handle_change_tab_left_right_keys};
use crate::models::lidarr_models::Artist;
use crate::models::lidarr_models::LidarrHistoryItem;
use crate::models::servarr_data::lidarr::lidarr_data::ActiveLidarrBlock;
use crate::models::servarr_data::lidarr::modals::EditArtistModal;
use crate::{assert_navigation_pushed, test_handler_delegation};
use pretty_assertions::assert_eq;
use rstest::rstest;
use strum::IntoEnumIterator;
@@ -55,8 +52,9 @@ mod tests {
}
#[rstest]
#[case(0, ActiveLidarrBlock::History, ActiveLidarrBlock::History)]
#[case(1, ActiveLidarrBlock::Artists, ActiveLidarrBlock::Artists)]
#[case(0, ActiveLidarrBlock::History, ActiveLidarrBlock::Downloads)]
#[case(1, ActiveLidarrBlock::Artists, ActiveLidarrBlock::History)]
#[case(2, ActiveLidarrBlock::Downloads, ActiveLidarrBlock::Artists)]
fn test_lidarr_handler_change_tab_left_right_keys(
#[case] index: usize,
#[case] left_block: ActiveLidarrBlock,
@@ -85,8 +83,9 @@ mod tests {
}
#[rstest]
#[case(0, ActiveLidarrBlock::History, ActiveLidarrBlock::History)]
#[case(1, ActiveLidarrBlock::Artists, ActiveLidarrBlock::Artists)]
#[case(0, ActiveLidarrBlock::History, ActiveLidarrBlock::Downloads)]
#[case(1, ActiveLidarrBlock::Artists, ActiveLidarrBlock::History)]
#[case(2, ActiveLidarrBlock::Downloads, ActiveLidarrBlock::Artists)]
fn test_lidarr_handler_change_tab_left_right_keys_alt_navigation(
#[case] index: usize,
#[case] left_block: ActiveLidarrBlock,
@@ -116,7 +115,8 @@ mod tests {
#[rstest]
#[case(0, ActiveLidarrBlock::Artists)]
#[case(1, ActiveLidarrBlock::History)]
#[case(1, ActiveLidarrBlock::Downloads)]
#[case(2, ActiveLidarrBlock::History)]
fn test_lidarr_handler_change_tab_left_right_keys_alt_navigation_no_op_when_ignoring_quit_key(
#[case] index: usize,
#[case] block: ActiveLidarrBlock,
@@ -165,25 +165,27 @@ mod tests {
)]
active_lidarr_block: ActiveLidarrBlock,
) {
let mut app = App::test_default();
app
.data
.lidarr_data
.artists
.set_items(vec![Artist::default()]);
app.data.lidarr_data.edit_artist_modal = Some(EditArtistModal::default());
app.push_navigation_stack(ActiveLidarrBlock::Artists.into());
app.push_navigation_stack(active_lidarr_block.into());
test_handler_delegation!(
LidarrHandler,
ActiveLidarrBlock::Artists,
active_lidarr_block
);
}
LidarrHandler::new(
DEFAULT_KEYBINDINGS.esc.key,
&mut app,
active_lidarr_block,
None,
)
.handle();
assert_eq!(app.get_current_route(), ActiveLidarrBlock::Artists.into());
#[rstest]
fn test_delegates_downloads_blocks_to_downloads_handler(
#[values(
ActiveLidarrBlock::Downloads,
ActiveLidarrBlock::DeleteDownloadPrompt,
ActiveLidarrBlock::UpdateDownloadsPrompt
)]
active_lidarr_block: ActiveLidarrBlock,
) {
test_handler_delegation!(
LidarrHandler,
ActiveLidarrBlock::Downloads,
active_lidarr_block
);
}
#[rstest]
@@ -199,23 +201,10 @@ mod tests {
)]
active_lidarr_block: ActiveLidarrBlock,
) {
let mut app = App::test_default();
app
.data
.lidarr_data
.history
.set_items(vec![LidarrHistoryItem::default()]);
app.push_navigation_stack(ActiveLidarrBlock::History.into());
app.push_navigation_stack(active_lidarr_block.into());
LidarrHandler::new(
DEFAULT_KEYBINDINGS.esc.key,
&mut app,
active_lidarr_block,
None,
)
.handle();
assert_eq!(app.get_current_route(), ActiveLidarrBlock::History.into());
test_handler_delegation!(
LidarrHandler,
ActiveLidarrBlock::History,
active_lidarr_block
);
}
}
+5
View File
@@ -2,6 +2,7 @@ use history::HistoryHandler;
use library::LibraryHandler;
use super::KeyEventHandler;
use crate::handlers::lidarr_handlers::downloads::DownloadsHandler;
use crate::models::Route;
use crate::{
app::App, event::Key, matches_key, models::servarr_data::lidarr::lidarr_data::ActiveLidarrBlock,
@@ -10,6 +11,7 @@ use crate::{
mod history;
mod library;
mod downloads;
#[cfg(test)]
#[path = "lidarr_handler_tests.rs"]
mod lidarr_handler_tests;
@@ -27,6 +29,9 @@ impl<'a, 'b> KeyEventHandler<'a, 'b, ActiveLidarrBlock> for LidarrHandler<'a, 'b
_ if LibraryHandler::accepts(self.active_lidarr_block) => {
LibraryHandler::new(self.key, self.app, self.active_lidarr_block, self.context).handle();
}
_ if DownloadsHandler::accepts(self.active_lidarr_block) => {
DownloadsHandler::new(self.key, self.app, self.active_lidarr_block, self.context).handle();
}
_ if HistoryHandler::accepts(self.active_lidarr_block) => {
HistoryHandler::new(self.key, self.app, self.active_lidarr_block, self.context).handle();
}
+16 -1
View File
@@ -1,7 +1,7 @@
use serde_json::Number;
use super::modals::{AddArtistModal, EditArtistModal};
use crate::app::context_clues::HISTORY_CONTEXT_CLUES;
use crate::app::context_clues::{DOWNLOADS_CONTEXT_CLUES, HISTORY_CONTEXT_CLUES};
use crate::app::lidarr::lidarr_context_clues::{
ARTIST_DETAILS_CONTEXT_CLUES, ARTISTS_CONTEXT_CLUES,
};
@@ -131,6 +131,12 @@ impl<'a> Default for LidarrData<'a> {
contextual_help: Some(&ARTISTS_CONTEXT_CLUES),
config: None,
},
TabRoute {
title: "Downloads".to_string(),
route: ActiveLidarrBlock::Downloads.into(),
contextual_help: Some(&DOWNLOADS_CONTEXT_CLUES),
config: None,
},
TabRoute {
title: "History".to_string(),
route: ActiveLidarrBlock::History.into(),
@@ -252,6 +258,8 @@ pub enum ActiveLidarrBlock {
DeleteArtistConfirmPrompt,
DeleteArtistToggleDeleteFile,
DeleteArtistToggleAddListExclusion,
DeleteDownloadPrompt,
Downloads,
EditArtistPrompt,
EditArtistConfirmPrompt,
EditArtistPathInput,
@@ -275,6 +283,7 @@ pub enum ActiveLidarrBlock {
SearchHistoryError,
UpdateAllArtistsPrompt,
UpdateAndScanArtistPrompt,
UpdateDownloadsPrompt,
}
pub static LIBRARY_BLOCKS: [ActiveLidarrBlock; 7] = [
@@ -295,6 +304,12 @@ pub static ARTIST_DETAILS_BLOCKS: [ActiveLidarrBlock; 5] = [
ActiveLidarrBlock::UpdateAndScanArtistPrompt,
];
pub static DOWNLOADS_BLOCKS: [ActiveLidarrBlock; 3] = [
ActiveLidarrBlock::Downloads,
ActiveLidarrBlock::DeleteDownloadPrompt,
ActiveLidarrBlock::UpdateDownloadsPrompt,
];
pub static HISTORY_BLOCKS: [ActiveLidarrBlock; 7] = [
ActiveLidarrBlock::History,
ActiveLidarrBlock::HistoryItemDetails,
@@ -1,6 +1,6 @@
#[cfg(test)]
mod tests {
use crate::app::context_clues::HISTORY_CONTEXT_CLUES;
use crate::app::context_clues::{DOWNLOADS_CONTEXT_CLUES, HISTORY_CONTEXT_CLUES};
use crate::app::lidarr::lidarr_context_clues::{
ARTIST_DETAILS_CONTEXT_CLUES, ARTISTS_CONTEXT_CLUES,
};
@@ -8,7 +8,7 @@ mod tests {
use crate::models::servarr_data::lidarr::lidarr_data::{
ADD_ARTIST_BLOCKS, ADD_ARTIST_SELECTION_BLOCKS, ARTIST_DETAILS_BLOCKS, DELETE_ALBUM_BLOCKS,
DELETE_ALBUM_SELECTION_BLOCKS, DELETE_ARTIST_BLOCKS, DELETE_ARTIST_SELECTION_BLOCKS,
EDIT_ARTIST_BLOCKS, EDIT_ARTIST_SELECTION_BLOCKS, HISTORY_BLOCKS,
DOWNLOADS_BLOCKS, EDIT_ARTIST_BLOCKS, EDIT_ARTIST_SELECTION_BLOCKS, HISTORY_BLOCKS,
};
use crate::models::{
BlockSelectionState, Route,
@@ -146,7 +146,7 @@ mod tests {
assert_is_empty!(lidarr_data.tags_map);
assert_is_empty!(lidarr_data.version);
assert_eq!(lidarr_data.main_tabs.tabs.len(), 2);
assert_eq!(lidarr_data.main_tabs.tabs.len(), 3);
assert_str_eq!(lidarr_data.main_tabs.tabs[0].title, "Library");
assert_eq!(
@@ -159,17 +159,28 @@ mod tests {
);
assert_none!(lidarr_data.main_tabs.tabs[0].config);
assert_str_eq!(lidarr_data.main_tabs.tabs[1].title, "History");
assert_str_eq!(lidarr_data.main_tabs.tabs[1].title, "Downloads");
assert_eq!(
lidarr_data.main_tabs.tabs[1].route,
ActiveLidarrBlock::History.into()
ActiveLidarrBlock::Downloads.into()
);
assert_some_eq_x!(
&lidarr_data.main_tabs.tabs[1].contextual_help,
&HISTORY_CONTEXT_CLUES
&DOWNLOADS_CONTEXT_CLUES
);
assert_none!(lidarr_data.main_tabs.tabs[1].config);
assert_str_eq!(lidarr_data.main_tabs.tabs[2].title, "History");
assert_eq!(
lidarr_data.main_tabs.tabs[2].route,
ActiveLidarrBlock::History.into()
);
assert_some_eq_x!(
&lidarr_data.main_tabs.tabs[2].contextual_help,
&HISTORY_CONTEXT_CLUES
);
assert_none!(lidarr_data.main_tabs.tabs[2].config);
assert_eq!(lidarr_data.artist_info_tabs.tabs.len(), 1);
assert_str_eq!(lidarr_data.artist_info_tabs.tabs[0].title, "Albums");
assert_eq!(
@@ -205,6 +216,14 @@ mod tests {
assert!(ARTIST_DETAILS_BLOCKS.contains(&ActiveLidarrBlock::UpdateAndScanArtistPrompt));
}
#[test]
fn test_downloads_blocks_contains_expected_blocks() {
assert_eq!(DOWNLOADS_BLOCKS.len(), 3);
assert!(DOWNLOADS_BLOCKS.contains(&ActiveLidarrBlock::Downloads));
assert!(DOWNLOADS_BLOCKS.contains(&ActiveLidarrBlock::DeleteDownloadPrompt));
assert!(DOWNLOADS_BLOCKS.contains(&ActiveLidarrBlock::UpdateDownloadsPrompt));
}
#[test]
fn test_history_blocks_contains_expected_blocks() {
assert_eq!(HISTORY_BLOCKS.len(), 7);
@@ -2,10 +2,37 @@
mod tests {
use crate::models::lidarr_models::{DownloadsResponse, LidarrSerdeable};
use crate::network::lidarr_network::LidarrEvent;
use crate::network::lidarr_network::lidarr_network_test_utils::test_utils::download_record;
use crate::network::network_tests::test_utils::{MockServarrApi, test_network};
use pretty_assertions::assert_eq;
use serde_json::json;
#[tokio::test]
async fn test_handle_delete_lidarr_download_event() {
let (mock, app, _server) = MockServarrApi::delete()
.path("/1")
.build_for(LidarrEvent::DeleteDownload(1))
.await;
app
.lock()
.await
.data
.lidarr_data
.downloads
.set_items(vec![download_record()]);
app.lock().await.server_tabs.set_index(2);
let mut network = test_network(&app);
assert!(
network
.handle_lidarr_event(LidarrEvent::DeleteDownload(1))
.await
.is_ok()
);
mock.assert_async().await;
}
#[tokio::test]
async fn test_handle_get_downloads_event() {
let downloads_json = json!({
@@ -40,4 +67,26 @@ mod tests {
assert_eq!(downloads_response, response);
assert!(!app.lock().await.data.lidarr_data.downloads.is_empty());
}
#[tokio::test]
async fn test_handle_update_lidarr_downloads_event() {
let (mock, app, _server) = MockServarrApi::post()
.with_request_body(json!({
"name": "RefreshMonitoredDownloads"
}))
.returns(json!({}))
.build_for(LidarrEvent::UpdateDownloads)
.await;
app.lock().await.server_tabs.set_index(2);
let mut network = test_network(&app);
assert!(
network
.handle_lidarr_event(LidarrEvent::UpdateDownloads)
.await
.is_ok()
);
mock.assert_async().await;
}
}
+44 -3
View File
@@ -1,15 +1,38 @@
use anyhow::Result;
use log::info;
use crate::models::lidarr_models::DownloadsResponse;
use crate::models::servarr_models::CommandBody;
use crate::network::lidarr_network::LidarrEvent;
use crate::network::{Network, RequestMethod};
use anyhow::Result;
use log::info;
use serde_json::Value;
#[cfg(test)]
#[path = "lidarr_downloads_network_tests.rs"]
mod lidarr_downloads_network_tests;
impl Network<'_, '_> {
pub(in crate::network::lidarr_network) async fn delete_lidarr_download(
&mut self,
download_id: i64,
) -> Result<()> {
let event = LidarrEvent::DeleteDownload(download_id);
info!("Deleting Lidarr 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
}
pub(in crate::network::lidarr_network) async fn get_lidarr_downloads(
&mut self,
count: u64,
@@ -37,4 +60,22 @@ impl Network<'_, '_> {
})
.await
}
pub(in crate::network::lidarr_network) async fn update_lidarr_downloads(
&mut self,
) -> Result<Value> {
info!("Updating Lidarr downloads");
let event = LidarrEvent::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
}
}
@@ -30,6 +30,13 @@ mod tests {
assert_str_eq!(event.resource(), "/artist");
}
#[rstest]
fn test_resource_downloads(
#[values(LidarrEvent::GetDownloads(0), LidarrEvent::DeleteDownload(0))] event: LidarrEvent,
) {
assert_str_eq!(event.resource(), "/queue");
}
#[rstest]
fn test_resource_history(#[values(LidarrEvent::GetHistory(0))] event: LidarrEvent) {
assert_str_eq!(event.resource(), "/history");
@@ -59,7 +66,8 @@ mod tests {
#[values(
LidarrEvent::UpdateAllArtists,
LidarrEvent::TriggerAutomaticArtistSearch(0),
LidarrEvent::UpdateAndScanArtist(0)
LidarrEvent::UpdateAndScanArtist(0),
LidarrEvent::UpdateDownloads
)]
event: LidarrEvent,
) {
@@ -81,7 +89,6 @@ mod tests {
#[rstest]
#[case(LidarrEvent::GetDiskSpace, "/diskspace")]
#[case(LidarrEvent::GetDownloads(500), "/queue")]
#[case(LidarrEvent::GetMetadataProfiles, "/metadataprofile")]
#[case(LidarrEvent::GetQualityProfiles, "/qualityprofile")]
#[case(LidarrEvent::GetRootFolders, "/rootfolder")]
+13 -2
View File
@@ -28,6 +28,7 @@ pub enum LidarrEvent {
AddTag(String),
DeleteAlbum(DeleteParams),
DeleteArtist(DeleteParams),
DeleteDownload(i64),
DeleteTag(i64),
EditArtist(EditArtistParams),
GetAlbums(i64),
@@ -52,6 +53,7 @@ pub enum LidarrEvent {
TriggerAutomaticArtistSearch(i64),
UpdateAllArtists,
UpdateAndScanArtist(i64),
UpdateDownloads,
}
impl NetworkResource for LidarrEvent {
@@ -69,13 +71,14 @@ impl NetworkResource for LidarrEvent {
| LidarrEvent::GetAlbumDetails(_)
| LidarrEvent::DeleteAlbum(_) => "/album",
LidarrEvent::GetDiskSpace => "/diskspace",
LidarrEvent::GetDownloads(_) => "/queue",
LidarrEvent::GetDownloads(_) | LidarrEvent::DeleteDownload(_) => "/queue",
LidarrEvent::GetHistory(_) => "/history",
LidarrEvent::MarkHistoryItemAsFailed(_) => "/history/failed",
LidarrEvent::GetHostConfig | LidarrEvent::GetSecurityConfig => "/config/host",
LidarrEvent::TriggerAutomaticArtistSearch(_)
| LidarrEvent::UpdateAllArtists
| LidarrEvent::UpdateAndScanArtist(_) => "/command",
| LidarrEvent::UpdateAndScanArtist(_)
| LidarrEvent::UpdateDownloads => "/command",
LidarrEvent::GetMetadataProfiles => "/metadataprofile",
LidarrEvent::GetQualityProfiles => "/qualityprofile",
LidarrEvent::GetRootFolders => "/rootfolder",
@@ -105,6 +108,10 @@ impl Network<'_, '_> {
LidarrEvent::DeleteArtist(params) => {
self.delete_artist(params).await.map(LidarrSerdeable::from)
}
LidarrEvent::DeleteDownload(download_id) => self
.delete_lidarr_download(download_id)
.await
.map(LidarrSerdeable::from),
LidarrEvent::DeleteTag(tag_id) => self
.delete_lidarr_tag(tag_id)
.await
@@ -182,6 +189,10 @@ impl Network<'_, '_> {
.map(LidarrSerdeable::from),
LidarrEvent::EditArtist(params) => self.edit_artist(params).await.map(LidarrSerdeable::from),
LidarrEvent::AddArtist(body) => self.add_artist(body).await.map(LidarrSerdeable::from),
LidarrEvent::UpdateDownloads => self
.update_lidarr_downloads()
.await
.map(LidarrSerdeable::from),
}
}
@@ -0,0 +1,72 @@
#[cfg(test)]
mod tests {
use strum::IntoEnumIterator;
use crate::app::App;
use crate::models::servarr_data::lidarr::lidarr_data::{ActiveLidarrBlock, DOWNLOADS_BLOCKS};
use crate::ui::DrawUi;
use crate::ui::lidarr_ui::downloads::DownloadsUi;
use crate::ui::ui_test_utils::test_utils::render_to_string_with_app;
#[test]
fn test_downloads_ui_accepts() {
ActiveLidarrBlock::iter().for_each(|active_lidarr_block| {
if DOWNLOADS_BLOCKS.contains(&active_lidarr_block) {
assert!(DownloadsUi::accepts(active_lidarr_block.into()));
} else {
assert!(!DownloadsUi::accepts(active_lidarr_block.into()));
}
});
}
mod snapshot_tests {
use crate::ui::ui_test_utils::test_utils::TerminalSize;
use rstest::rstest;
use super::*;
#[test]
fn test_downloads_ui_renders_loading() {
let mut app = App::test_default();
app.is_loading = true;
app.push_navigation_stack(ActiveLidarrBlock::Downloads.into());
let output = render_to_string_with_app(TerminalSize::Large, &mut app, |f, app| {
DownloadsUi::draw(f, app, f.area());
});
insta::assert_snapshot!(output);
}
#[test]
fn test_downloads_ui_renders_empty_downloads() {
let mut app = App::test_default();
app.push_navigation_stack(ActiveLidarrBlock::Downloads.into());
let output = render_to_string_with_app(TerminalSize::Large, &mut app, |f, app| {
DownloadsUi::draw(f, app, f.area());
});
insta::assert_snapshot!(output);
}
#[rstest]
fn test_downloads_ui_renders(
#[values(
ActiveLidarrBlock::Downloads,
ActiveLidarrBlock::DeleteDownloadPrompt,
ActiveLidarrBlock::UpdateDownloadsPrompt
)]
active_lidarr_block: ActiveLidarrBlock,
) {
let mut app = App::test_default_fully_populated();
app.push_navigation_stack(active_lidarr_block.into());
let output = render_to_string_with_app(TerminalSize::Large, &mut app, |f, app| {
DownloadsUi::draw(f, app, f.area());
});
insta::assert_snapshot!(format!("downloads_ui_{active_lidarr_block}"), output);
}
}
}
+146
View File
@@ -0,0 +1,146 @@
use ratatui::Frame;
use ratatui::layout::{Constraint, Rect};
use ratatui::widgets::{Cell, Row};
use crate::app::App;
use crate::models::lidarr_models::DownloadRecord;
use crate::models::servarr_data::lidarr::lidarr_data::{ActiveLidarrBlock, DOWNLOADS_BLOCKS};
use crate::models::{HorizontallyScrollableText, Route};
use crate::ui::DrawUi;
use crate::ui::styles::ManagarrStyle;
use crate::ui::utils::{get_width_from_percentage, layout_block_top_border};
use crate::ui::widgets::confirmation_prompt::ConfirmationPrompt;
use crate::ui::widgets::managarr_table::ManagarrTable;
use crate::ui::widgets::popup::{Popup, Size};
use crate::utils::convert_f64_to_gb;
#[cfg(test)]
#[path = "downloads_ui_tests.rs"]
mod downloads_ui_tests;
pub(super) struct DownloadsUi;
impl DrawUi for DownloadsUi {
fn accepts(route: Route) -> bool {
if let Route::Lidarr(active_lidarr_block, _) = route {
return DOWNLOADS_BLOCKS.contains(&active_lidarr_block);
}
false
}
fn draw(f: &mut Frame<'_>, app: &mut App<'_>, area: Rect) {
if let Route::Lidarr(active_lidarr_block, _) = app.get_current_route() {
draw_downloads(f, app, area);
match active_lidarr_block {
ActiveLidarrBlock::DeleteDownloadPrompt => {
let prompt = format!(
"Do you really want to delete this download: \n{}?",
app.data.lidarr_data.downloads.current_selection().title
);
let confirmation_prompt = ConfirmationPrompt::new()
.title("Cancel Download")
.prompt(&prompt)
.yes_no_value(app.data.lidarr_data.prompt_confirm);
f.render_widget(
Popup::new(confirmation_prompt).size(Size::MediumPrompt),
f.area(),
);
}
ActiveLidarrBlock::UpdateDownloadsPrompt => {
let confirmation_prompt = ConfirmationPrompt::new()
.title("Update Downloads")
.prompt("Do you want to update your downloads?")
.yes_no_value(app.data.lidarr_data.prompt_confirm);
f.render_widget(
Popup::new(confirmation_prompt).size(Size::MediumPrompt),
f.area(),
);
}
_ => (),
}
}
}
}
fn draw_downloads(f: &mut Frame<'_>, app: &mut App<'_>, area: Rect) {
let current_selection = if app.data.lidarr_data.downloads.items.is_empty() {
DownloadRecord::default()
} else {
app.data.lidarr_data.downloads.current_selection().clone()
};
let downloads_row_mapping = |download_record: &DownloadRecord| {
let DownloadRecord {
title,
size,
sizeleft,
download_client,
indexer,
output_path,
..
} = download_record;
if output_path.is_some() {
output_path.as_ref().unwrap().scroll_left_or_reset(
get_width_from_percentage(area, 18),
current_selection == *download_record,
app.ui_scroll_tick_count == 0,
);
}
let percent = if *size == 0.0 {
0.0
} else {
1f64 - (*sizeleft / *size)
};
let file_size: f64 = convert_f64_to_gb(*size);
Row::new(vec![
Cell::from(title.to_owned()),
Cell::from(format!("{:.0}%", percent * 100.0)),
Cell::from(format!("{file_size:.2} GB")),
Cell::from(
output_path
.as_ref()
.unwrap_or(&HorizontallyScrollableText::default())
.to_string(),
),
Cell::from(indexer.to_owned()),
Cell::from(
download_client
.as_ref()
.unwrap_or(&String::new())
.to_owned(),
),
])
.primary()
};
let downloads_table = ManagarrTable::new(
Some(&mut app.data.lidarr_data.downloads),
downloads_row_mapping,
)
.block(layout_block_top_border())
.loading(app.is_loading)
.headers([
"Title",
"Percent Complete",
"Size",
"Output Path",
"Indexer",
"Download Client",
])
.constraints([
Constraint::Percentage(30),
Constraint::Percentage(11),
Constraint::Percentage(11),
Constraint::Percentage(18),
Constraint::Percentage(17),
Constraint::Percentage(13),
]);
f.render_widget(downloads_table, area);
}
@@ -0,0 +1,38 @@
---
source: src/ui/lidarr_ui/downloads/downloads_ui_tests.rs
expression: output
---
─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────
Title Percent Complete Size Output Path Indexer Download Client
=> Test download title 50% 3.30 GB /nfs/music/alex/album kickass torrents transmission
╭──────────────────── Cancel Download ────────────────────╮
│ Do you really want to delete this download: │
│ Test download title? │
│ │
│ │
│ │
│ │
│ │
│ │
│ │
│ │
│ │
│ │
│ │
│╭────────────────────────────╮╭───────────────────────────╮│
││ Yes ││ No ││
│╰────────────────────────────╯╰───────────────────────────╯│
╰───────────────────────────────────────────────────────────╯
@@ -0,0 +1,7 @@
---
source: src/ui/lidarr_ui/downloads/downloads_ui_tests.rs
expression: output
---
─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────
Title Percent Complete Size Output Path Indexer Download Client
=> Test download title 50% 3.30 GB /nfs/music/alex/album kickass torrents transmission
@@ -0,0 +1,38 @@
---
source: src/ui/lidarr_ui/downloads/downloads_ui_tests.rs
expression: output
---
─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────
Title Percent Complete Size Output Path Indexer Download Client
=> Test download title 50% 3.30 GB /nfs/music/alex/album kickass torrents transmission
╭─────────────────── Update Downloads ────────────────────╮
│ Do you want to update your downloads? │
│ │
│ │
│ │
│ │
│ │
│ │
│ │
│ │
│ │
│ │
│ │
│ │
│╭────────────────────────────╮╭───────────────────────────╮│
││ Yes ││ No ││
│╰────────────────────────────╯╰───────────────────────────╯│
╰───────────────────────────────────────────────────────────╯
@@ -0,0 +1,5 @@
---
source: src/ui/lidarr_ui/downloads/downloads_ui_tests.rs
expression: output
---
─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────
@@ -0,0 +1,8 @@
---
source: src/ui/lidarr_ui/downloads/downloads_ui_tests.rs
expression: output
---
─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────
Loading ...
+26
View File
@@ -13,4 +13,30 @@ mod tests {
assert!(LidarrUi::accepts(Route::Lidarr(lidarr_block, None)));
}
}
mod snapshot_tests {
use super::*;
use crate::app::App;
use crate::ui::ui_test_utils::test_utils::{TerminalSize, render_to_string_with_app};
use rstest::rstest;
#[rstest]
#[case(ActiveLidarrBlock::Artists, 0)]
#[case(ActiveLidarrBlock::Downloads, 1)]
#[case(ActiveLidarrBlock::History, 2)]
fn test_lidarr_ui_renders_lidarr_tabs(
#[case] active_lidarr_block: ActiveLidarrBlock,
#[case] index: usize,
) {
let mut app = App::test_default_fully_populated();
app.push_navigation_stack(active_lidarr_block.into());
app.data.lidarr_data.main_tabs.set_index(index);
let output = render_to_string_with_app(TerminalSize::Large, &mut app, |f, app| {
LidarrUi::draw(f, app, f.area());
});
insta::assert_snapshot!(format!("lidarr_tabs_{active_lidarr_block}"), output);
}
}
}
+11 -9
View File
@@ -15,6 +15,15 @@ use ratatui::{
widgets::Paragraph,
};
use super::{
DrawUi, draw_tabs,
styles::ManagarrStyle,
utils::{
borderless_block, layout_block, line_gauge_with_label, line_gauge_with_title, title_block,
},
widgets::loading_block::LoadingBlock,
};
use crate::ui::lidarr_ui::downloads::DownloadsUi;
use crate::{
app::App,
logos::LIDARR_LOGO,
@@ -27,15 +36,7 @@ use crate::{
utils::convert_to_gb,
};
use super::{
DrawUi, draw_tabs,
styles::ManagarrStyle,
utils::{
borderless_block, layout_block, line_gauge_with_label, line_gauge_with_title, title_block,
},
widgets::loading_block::LoadingBlock,
};
mod downloads;
mod history;
mod library;
mod lidarr_ui_utils;
@@ -57,6 +58,7 @@ impl DrawUi for LidarrUi {
match route {
_ if LibraryUi::accepts(route) => LibraryUi::draw(f, app, content_area),
_ if DownloadsUi::accepts(route) => DownloadsUi::draw(f, app, content_area),
_ if HistoryUi::accepts(route) => HistoryUi::draw(f, app, content_area),
_ => (),
}
@@ -0,0 +1,54 @@
---
source: src/ui/lidarr_ui/lidarr_ui_tests.rs
expression: output
---
╭ Artists ────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╮
│ Library │ Downloads │ History │
│───────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────│
│ Name ▼ Type Status Quality Profile Metadata Profile Albums Tracks Size Monitored Tags │
│=> Alex Person Continuing Lossless Standard 1 15/15 0.00 GB 🏷 alex │
│ │
│ │
│ │
│ │
│ │
│ │
│ │
│ │
│ │
│ │
│ │
│ │
│ │
│ │
│ │
│ │
│ │
│ │
│ │
│ │
│ │
│ │
│ │
│ │
│ │
│ │
│ │
│ │
│ │
│ │
│ │
│ │
│ │
│ │
│ │
│ │
│ │
│ │
│ │
│ │
│ │
│ │
│ │
│ │
╰───────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯
@@ -0,0 +1,54 @@
---
source: src/ui/lidarr_ui/lidarr_ui_tests.rs
expression: output
---
╭ Artists ────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╮
│ Library │ Downloads │ History │
│───────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────│
│ Title Percent Complete Size Output Path Indexer Download Client │
│=> Test download title 50% 3.30 GB /nfs/music/alex/album kickass torrents transmission │
│ │
│ │
│ │
│ │
│ │
│ │
│ │
│ │
│ │
│ │
│ │
│ │
│ │
│ │
│ │
│ │
│ │
│ │
│ │
│ │
│ │
│ │
│ │
│ │
│ │
│ │
│ │
│ │
│ │
│ │
│ │
│ │
│ │
│ │
│ │
│ │
│ │
│ │
│ │
│ │
│ │
│ │
│ │
│ │
╰───────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯
@@ -0,0 +1,54 @@
---
source: src/ui/lidarr_ui/lidarr_ui_tests.rs
expression: output
---
╭ Artists ────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╮
│ Library │ Downloads │ History │
│───────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────│
│ Source Title ▼ Event Type Quality Date │
│=> Test source title grabbed Lossless 2023-01-01 00:00:00 UTC │
│ │
│ │
│ │
│ │
│ │
│ │
│ │
│ │
│ │
│ │
│ │
│ │
│ │
│ │
│ │
│ │
│ │
│ │
│ │
│ │
│ │
│ │
│ │
│ │
│ │
│ │
│ │
│ │
│ │
│ │
│ │
│ │
│ │
│ │
│ │
│ │
│ │
│ │
│ │
│ │
│ │
│ │
│ │
│ │
╰───────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯