feat: Blocklist support in Lidarr in both the CLI and TUI

This commit is contained in:
2026-01-19 16:13:11 -07:00
parent eff1a901eb
commit 89f5ff6bc7
48 changed files with 2211 additions and 66 deletions
@@ -0,0 +1,74 @@
#[cfg(test)]
mod tests {
use strum::IntoEnumIterator;
use crate::app::App;
use crate::models::servarr_data::lidarr::lidarr_data::{ActiveLidarrBlock, BLOCKLIST_BLOCKS};
use crate::ui::DrawUi;
use crate::ui::lidarr_ui::blocklist::BlocklistUi;
use crate::ui::ui_test_utils::test_utils::render_to_string_with_app;
#[test]
fn test_blocklist_ui_accepts() {
ActiveLidarrBlock::iter().for_each(|active_lidarr_block| {
if BLOCKLIST_BLOCKS.contains(&active_lidarr_block) {
assert!(BlocklistUi::accepts(active_lidarr_block.into()));
} else {
assert!(!BlocklistUi::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_blocklist_ui_renders_loading() {
let mut app = App::test_default();
app.is_loading = true;
app.push_navigation_stack(ActiveLidarrBlock::Blocklist.into());
let output = render_to_string_with_app(TerminalSize::Large, &mut app, |f, app| {
BlocklistUi::draw(f, app, f.area());
});
insta::assert_snapshot!(output);
}
#[test]
fn test_blocklist_ui_renders_empty() {
let mut app = App::test_default();
app.push_navigation_stack(ActiveLidarrBlock::Blocklist.into());
let output = render_to_string_with_app(TerminalSize::Large, &mut app, |f, app| {
BlocklistUi::draw(f, app, f.area());
});
insta::assert_snapshot!(output);
}
#[rstest]
fn test_blocklist_ui_renders(
#[values(
ActiveLidarrBlock::Blocklist,
ActiveLidarrBlock::BlocklistItemDetails,
ActiveLidarrBlock::DeleteBlocklistItemPrompt,
ActiveLidarrBlock::BlocklistClearAllItemsPrompt,
ActiveLidarrBlock::BlocklistSortPrompt
)]
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| {
BlocklistUi::draw(f, app, f.area());
});
insta::assert_snapshot!(format!("blocklist_tab_{active_lidarr_block}"), output);
}
}
}
+156
View File
@@ -0,0 +1,156 @@
use crate::app::App;
use crate::models::Route;
use crate::models::lidarr_models::BlocklistItem;
use crate::models::servarr_data::lidarr::lidarr_data::{ActiveLidarrBlock, BLOCKLIST_BLOCKS};
use crate::ui::DrawUi;
use crate::ui::styles::{ManagarrStyle, secondary_style};
use crate::ui::utils::layout_block_top_border;
use crate::ui::widgets::confirmation_prompt::ConfirmationPrompt;
use crate::ui::widgets::managarr_table::ManagarrTable;
use crate::ui::widgets::message::Message;
use crate::ui::widgets::popup::{Popup, Size};
use ratatui::Frame;
use ratatui::layout::{Alignment, Constraint, Rect};
use ratatui::style::Stylize;
use ratatui::text::{Line, Text};
use ratatui::widgets::{Cell, Row};
#[cfg(test)]
#[path = "blocklist_ui_tests.rs"]
mod blocklist_ui_tests;
pub(super) struct BlocklistUi;
impl DrawUi for BlocklistUi {
fn accepts(route: Route) -> bool {
if let Route::Lidarr(active_lidarr_block, _) = route {
return BLOCKLIST_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_blocklist_table(f, app, area);
match active_lidarr_block {
ActiveLidarrBlock::BlocklistItemDetails => {
draw_blocklist_item_details_popup(f, app);
}
ActiveLidarrBlock::DeleteBlocklistItemPrompt => {
let prompt = format!(
"Do you want to remove this item from your blocklist: \n{}?",
app
.data
.lidarr_data
.blocklist
.current_selection()
.source_title
);
let confirmation_prompt = ConfirmationPrompt::new()
.title("Remove Item from Blocklist")
.prompt(&prompt)
.yes_no_value(app.data.lidarr_data.prompt_confirm);
f.render_widget(
Popup::new(confirmation_prompt).size(Size::MediumPrompt),
f.area(),
);
}
ActiveLidarrBlock::BlocklistClearAllItemsPrompt => {
let confirmation_prompt = ConfirmationPrompt::new()
.title("Clear Blocklist")
.prompt("Do you want to clear your blocklist?")
.yes_no_value(app.data.lidarr_data.prompt_confirm);
f.render_widget(
Popup::new(confirmation_prompt).size(Size::SmallPrompt),
f.area(),
);
}
_ => (),
}
}
}
}
fn draw_blocklist_table(f: &mut Frame<'_>, app: &mut App<'_>, area: Rect) {
if let Route::Lidarr(active_lidarr_block, _) = app.get_current_route() {
let blocklist_row_mapping = |blocklist_item: &BlocklistItem| {
let BlocklistItem {
source_title,
artist,
quality,
date,
..
} = blocklist_item;
let title = artist.artist_name.text.to_owned();
Row::new(vec![
Cell::from(title),
Cell::from(source_title.to_owned()),
Cell::from(quality.quality.name.to_owned()),
Cell::from(date.to_string()),
])
.primary()
};
let blocklist_table = ManagarrTable::new(
Some(&mut app.data.lidarr_data.blocklist),
blocklist_row_mapping,
)
.block(layout_block_top_border())
.loading(app.is_loading)
.sorting(active_lidarr_block == ActiveLidarrBlock::BlocklistSortPrompt)
.headers(["Artist Name", "Source Title", "Quality", "Date"])
.constraints([
Constraint::Percentage(27),
Constraint::Percentage(43),
Constraint::Percentage(13),
Constraint::Percentage(17),
]);
f.render_widget(blocklist_table, area);
}
}
fn draw_blocklist_item_details_popup(f: &mut Frame<'_>, app: &mut App<'_>) {
let current_selection = if app.data.lidarr_data.blocklist.items.is_empty() {
BlocklistItem::default()
} else {
app.data.lidarr_data.blocklist.current_selection().clone()
};
let BlocklistItem {
source_title,
protocol,
indexer,
message,
..
} = current_selection;
let text = Text::from(vec![
Line::from(vec![
"Name: ".bold().secondary(),
source_title.to_owned().secondary(),
]),
Line::from(vec![
"Protocol: ".bold().secondary(),
protocol.to_owned().secondary(),
]),
Line::from(vec![
"Indexer: ".bold().secondary(),
indexer.to_owned().secondary(),
]),
Line::from(vec![
"Message: ".bold().secondary(),
message.to_owned().secondary(),
]),
]);
let message = Message::new(text)
.title("Details")
.style(secondary_style())
.alignment(Alignment::Left);
f.render_widget(Popup::new(message).size(Size::NarrowMessage), f.area());
}
@@ -0,0 +1,7 @@
---
source: src/ui/lidarr_ui/blocklist/blocklist_ui_tests.rs
expression: output
---
─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────
Artist Name ▼ Source Title Quality Date
=> Alex Alex - Something Lossless 2023-05-20 21:29:16 UTC
@@ -0,0 +1,34 @@
---
source: src/ui/lidarr_ui/blocklist/blocklist_ui_tests.rs
expression: output
---
─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────
Artist Name ▼ Source Title Quality Date
=> Alex Alex - Something Lossless 2023-05-20 21:29:16 UTC
╭────── Clear Blocklist ──────╮
│ Do you want to clear your │
│ blocklist? │
│ │
│ │
│ │
│╭──────────────╮╭─────────────╮│
││ Yes ││ No ││
│╰──────────────╯╰─────────────╯│
╰───────────────────────────────╯
@@ -0,0 +1,34 @@
---
source: src/ui/lidarr_ui/blocklist/blocklist_ui_tests.rs
expression: output
---
─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────
Artist Name ▼ Source Title Quality Date
=> Alex Alex - Something Lossless 2023-05-20 21:29:16 UTC
╭─────────────────────────────────── Details ───────────────────────────────────╮
│Name: Alex - Something │
│Protocol: usenet │
│Indexer: NZBgeek (Prowlarr) │
│Message: test message │
│ │
│ │
│ │
│ │
╰─────────────────────────────────────────────────────────────────────────────────╯
@@ -0,0 +1,42 @@
---
source: src/ui/lidarr_ui/blocklist/blocklist_ui_tests.rs
expression: output
---
─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────
Artist Name Source Title Quality Date
=> Alex Alex - Something Lossless 2023-05-20 21:29:16 UTC
╭───────────────────────────────╮
│Something │
│ │
│ │
│ │
│ │
│ │
│ │
│ │
│ │
│ │
│ │
│ │
│ │
│ │
│ │
│ │
│ │
│ │
│ │
│ │
│ │
│ │
│ │
╰───────────────────────────────╯
@@ -0,0 +1,38 @@
---
source: src/ui/lidarr_ui/blocklist/blocklist_ui_tests.rs
expression: output
---
─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────
Artist Name ▼ Source Title Quality Date
=> Alex Alex - Something Lossless 2023-05-20 21:29:16 UTC
╭────────────── Remove Item from Blocklist ───────────────╮
│ Do you want to remove this item from your blocklist: │
│ Alex - Something? │
│ │
│ │
│ │
│ │
│ │
│ │
│ │
│ │
│ │
│ │
│ │
│╭────────────────────────────╮╭───────────────────────────╮│
││ Yes ││ No ││
│╰────────────────────────────╯╰───────────────────────────╯│
╰───────────────────────────────────────────────────────────╯
@@ -0,0 +1,5 @@
---
source: src/ui/lidarr_ui/blocklist/blocklist_ui_tests.rs
expression: output
---
─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────
@@ -0,0 +1,8 @@
---
source: src/ui/lidarr_ui/blocklist/blocklist_ui_tests.rs
expression: output
---
─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────
Loading ...
+5 -3
View File
@@ -23,9 +23,11 @@ mod tests {
#[rstest]
#[case(ActiveLidarrBlock::Artists, 0)]
#[case(ActiveLidarrBlock::Downloads, 1)]
#[case(ActiveLidarrBlock::History, 2)]
#[case(ActiveLidarrBlock::RootFolders, 3)]
#[case(ActiveLidarrBlock::Indexers, 4)]
#[case(ActiveLidarrBlock::Blocklist, 2)]
#[case(ActiveLidarrBlock::History, 3)]
#[case(ActiveLidarrBlock::RootFolders, 4)]
#[case(ActiveLidarrBlock::Indexers, 5)]
#[case(ActiveLidarrBlock::System, 6)]
fn test_lidarr_ui_renders_lidarr_tabs(
#[case] active_lidarr_block: ActiveLidarrBlock,
#[case] index: usize,
+3
View File
@@ -23,6 +23,7 @@ use super::{
},
widgets::loading_block::LoadingBlock,
};
use crate::ui::lidarr_ui::blocklist::BlocklistUi;
use crate::ui::lidarr_ui::downloads::DownloadsUi;
use crate::ui::lidarr_ui::indexers::IndexersUi;
use crate::ui::lidarr_ui::root_folders::RootFoldersUi;
@@ -39,6 +40,7 @@ use crate::{
utils::convert_to_gb,
};
mod blocklist;
mod downloads;
mod history;
mod indexers;
@@ -65,6 +67,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 BlocklistUi::accepts(route) => BlocklistUi::draw(f, app, content_area),
_ if HistoryUi::accepts(route) => HistoryUi::draw(f, app, content_area),
_ if RootFoldersUi::accepts(route) => RootFoldersUi::draw(f, app, content_area),
_ if IndexersUi::accepts(route) => IndexersUi::draw(f, app, content_area),
@@ -3,7 +3,7 @@ source: src/ui/lidarr_ui/lidarr_ui_tests.rs
expression: output
---
╭ Artists ────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╮
│ Library │ Downloads │ History │ Root Folders │ Indexers │ System
│ Library │ Downloads │ Blocklist │ History │ Root Folders │ Indexers │ System │
│───────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────│
│ 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 │ Blocklist │ History │ Root Folders │ Indexers │ System │
│───────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────│
│ Artist Name ▼ Source Title Quality Date │
│=> Alex Alex - Something Lossless 2023-05-20 21:29:16 UTC │
│ │
│ │
│ │
│ │
│ │
│ │
│ │
│ │
│ │
│ │
│ │
│ │
│ │
│ │
│ │
│ │
│ │
│ │
│ │
│ │
│ │
│ │
│ │
│ │
│ │
│ │
│ │
│ │
│ │
│ │
│ │
│ │
│ │
│ │
│ │
│ │
│ │
│ │
│ │
│ │
│ │
│ │
│ │
│ │
╰───────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯
@@ -3,7 +3,7 @@ source: src/ui/lidarr_ui/lidarr_ui_tests.rs
expression: output
---
╭ Artists ────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╮
│ Library │ Downloads │ History │ Root Folders │ Indexers │ System
│ Library │ Downloads │ Blocklist │ History │ Root Folders │ Indexers │ System │
│───────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────│
│ Title Percent Complete Size Output Path Indexer Download Client │
│=> Test download title 50% 3.30 GB /nfs/music/alex/album kickass torrents transmission │
@@ -3,7 +3,7 @@ source: src/ui/lidarr_ui/lidarr_ui_tests.rs
expression: output
---
╭ Artists ────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╮
│ Library │ Downloads │ History │ Root Folders │ Indexers │ System
│ Library │ Downloads │ Blocklist │ History │ Root Folders │ Indexers │ System │
│───────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────│
│ Source Title ▼ Event Type Quality Date │
│=> Test source title grabbed Lossless 2023-01-01 00:00:00 UTC │
@@ -3,7 +3,7 @@ source: src/ui/lidarr_ui/lidarr_ui_tests.rs
expression: output
---
╭ Artists ────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╮
│ Library │ Downloads │ History │ Root Folders │ Indexers │ System
│ Library │ Downloads │ Blocklist │ History │ Root Folders │ Indexers │ System │
│───────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────│
│ Indexer RSS Automatic Search Interactive Search Priority Tags │
│=> Test Indexer Enabled Enabled Enabled 25 alex │
@@ -3,7 +3,7 @@ source: src/ui/lidarr_ui/lidarr_ui_tests.rs
expression: output
---
╭ Artists ────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╮
│ Library │ Downloads │ History │ Root Folders │ Indexers │ System
│ Library │ Downloads │ Blocklist │ History │ Root Folders │ Indexers │ System │
│───────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────│
│ Path Free Space Unmapped Folders │
│=> /nfs 204800.00 GB 0 │
@@ -0,0 +1,54 @@
---
source: src/ui/lidarr_ui/lidarr_ui_tests.rs
expression: output
---
╭ Artists ────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╮
│ Library │ Downloads │ Blocklist │ History │ Root Folders │ Indexers │ System │
│╭ Tasks ───────────────────────────────────────────────────────────────────────╮╭ Queued Events ──────────────────────────────────────────────────────────────╮│
││Name Interval Last Execution Next Execution ││Trigger Status Name Queued Started Duration ││
││Backup 1 hour now 59 minutes ││manual completed Refresh Monitored 4 minutes ago 4 minutes a 00:03:03 ││
││ ││ ││
││ ││ ││
││ ││ ││
││ ││ ││
││ ││ ││
││ ││ ││
││ ││ ││
││ ││ ││
││ ││ ││
││ ││ ││
││ ││ ││
││ ││ ││
││ ││ ││
││ ││ ││
││ ││ ││
││ ││ ││
││ ││ ││
││ ││ ││
││ ││ ││
││ ││ ││
│╰────────────────────────────────────────────────────────────────────────────────╯╰───────────────────────────────────────────────────────────────────────────────╯│
│╭ Logs ─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╮│
││2025-12-16 16:40:59 UTC|INFO|ImportListSyncService|No list items to process ││
││ ││
││ ││
││ ││
││ ││
││ ││
││ ││
││ ││
││ ││
││ ││
││ ││
││ ││
││ ││
││ ││
││ ││
││ ││
││ ││
││ ││
││ ││
││ ││
││ ││
│╰─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯│
╰───────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯
@@ -16,7 +16,7 @@ expression: output
│ ││ ││ ⠀⠀⠀⠉⠻⠿⢿⡆⡾⠿⠟⠉⠀⠀⠀ │
╰───────────────────────────────────────────────────────────────────────╯╰──────────────────────────────────────────────────────────────────────╯╰──────────────────╯
╭ Artists ────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╮
│ Library │ Downloads │ History │ Root Folders │ Indexers │ System
│ Library │ Downloads │ Blocklist │ History │ Root Folders │ Indexers │ System │
│───────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────│
│ Name ▼ Type Status Quality Profile Metadata Profile Albums Tracks Size Monitored Tags │
│=> Alex Person Continuing Lossless Standard 1 15/15 0.00 GB 🏷 alex │
@@ -16,7 +16,7 @@ expression: output
│ │ s search │ ││ ⠀⠀⠀⠉⠻⠿⢿⡆⡾⠿⠟⠉⠀⠀⠀ │
╰───────────────────────────────────│ f filter │─────────────────╯╰──────────────────╯
╭ Artists ────────────────────────│ ctrl-r refresh │─────────────────────────────────────╮
│ Library │ Downloads │ HistoryRo│ u update all │ │
│ Library │ Downloads │ Blocklist │ │ u update all │ │
│───────────────────────────────────│ enter details │─────────────────────────────────────│
│ Name ▼ Typ│ esc cancel filter │e Monitored Tags │
│=> Alex Per│ ↑ k scroll up │0 GB 🏷 alex │
@@ -19,7 +19,7 @@ expression: output
│ ││ ││ ⠀⠀⠀⠉⠻⠿⢿⡆⡾⠿⠟⠉⠀⠀⠀ │
╰───────────────────────────────────────────────────────────────────────╯╰──────────────────────────────────────────────────────────────────────╯╰──────────────────╯
╭ Artists ────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╮
│ Library │ Downloads │ History │ Root Folders │ Indexers │ System
│ Library │ Downloads │ Blocklist │ History │ Root Folders │ Indexers │ System │
│───────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────│
│ Name ▼ Type Status Quality Profile Metadata Profile Albums Tracks Size Monitored Tags │
│=> Alex Person Continuing Lossless Standard 1 15/15 0.00 GB 🏷 alex │