Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
003f80098a
|
||
|
|
ad8a6743ab
|
||
|
|
c61404dc71
|
||
|
|
a5b95c165a
|
||
|
|
8ec4ec018b
|
||
|
|
642a5c0a87
|
||
|
|
9128e68d0a
|
||
|
|
ddc18ae21a
|
||
|
|
4b8b9cde57
|
||
|
|
238348f6ec
|
||
|
|
37f697d1b4
|
No files matched your search
@@ -1,11 +0,0 @@
|
||||
### AI assistance (if any):
|
||||
- List tools here and files touched by them
|
||||
|
||||
### Authorship & Understanding
|
||||
|
||||
- [ ] I wrote or heavily modified this code myself
|
||||
- [ ] I understand how it works end-to-end
|
||||
- [ ] I can maintain this code in the future
|
||||
- [ ] No undisclosed AI-generated code was used
|
||||
- [ ] If AI assistance was used, it is documented below
|
||||
|
||||
@@ -0,0 +1,18 @@
|
||||
### Description
|
||||
<!-- What problem is this PR solving? -->
|
||||
|
||||
### Alternatives Considered (Optional)
|
||||
<!-- What alternatives did you consider? Why did you choose this approach? -->
|
||||
|
||||
### AI assistance (if any)
|
||||
<!-- Required if AI assistance was used to write or modify code in this PR. -->
|
||||
<!-- - List tools here -->
|
||||
|
||||
### Authorship & Understanding
|
||||
|
||||
- [ ] I wrote or heavily modified this code myself. If I didn't, I gave proper attribution to AI assistance above.
|
||||
- [ ] I understand how it works end-to-end
|
||||
- [ ] I can maintain this code in the future
|
||||
- [ ] No undisclosed AI-generated code was used
|
||||
- [ ] If AI assistance was used, it is documented above
|
||||
|
||||
@@ -93,8 +93,8 @@ act -W .github/workflows/release.yml --input_type bump=minor
|
||||
|
||||
## Authorship Policy
|
||||
|
||||
All code in this repository is written and reviewed by humans. AI-generated code (e.g., Copilot, ChatGPT,
|
||||
Claude, etc.) is not permitted unless explicitly disclosed and approved.
|
||||
All code in this repository is written and reviewed by humans, or AI-generated where explicitly disclosed. AI-generated code (e.g., Copilot, ChatGPT,
|
||||
Claude, etc.) is permitted only when explicitly disclosed and approved. For example: disclosing the use of AI-generated code in a PR description.
|
||||
|
||||
Submissions must certify that the contributor understands and can maintain the code they submit.
|
||||
|
||||
|
||||
@@ -26,6 +26,11 @@ Managarr is a TUI and CLI to help you manage your HTPC (Home Theater PC). Built
|
||||
- [ ]  [Bazarr](https://www.bazarr.media/)
|
||||
- [ ]  [Tautulli](https://tautulli.com/)
|
||||
|
||||
> [!NOTE]
|
||||
> The official Readarr repository has been archived, but Managarr's Readarr support is compatible with any Readarr forks
|
||||
> that maintain compatibility with the official Readarr v1 API. See [Tested Readarr Forks](#tested-readarr-forks) below
|
||||
> for forks that have been explicitly tested with Managarr, and any oddities in configuring them to work with Managarr.
|
||||
|
||||
## Try Out the Demo
|
||||
To try out Managarr before linking it to your HTPC, you can use the purpose built [managarr-demo](https://github.com/Dark-Alex-17/managarr-demo) repository.
|
||||
Simply run the following command to start a demo:
|
||||
@@ -134,8 +139,8 @@ Key:
|
||||
|
||||
| Symbol | Status |
|
||||
|--------|-----------|
|
||||
| ✅ | Supported |
|
||||
| ❌ | Missing |
|
||||
| ✅ | Supported |
|
||||
| ❌ | Missing |
|
||||
| 🕒 | Planned |
|
||||
| 🚫 | Won't Add |
|
||||
|
||||
@@ -143,82 +148,82 @@ Key:
|
||||
|
||||
| TUI | CLI | Feature |
|
||||
|-----|-----|----------------------------------------------------------------------------------------------------------------|
|
||||
| ✅ | ✅ | View your library, downloads, collections, and blocklist |
|
||||
| ✅ | ✅ | View details of a specific movie including description, history, downloaded file info, or the credits |
|
||||
| ✅ | ✅ | View details of any collection and the movies in them |
|
||||
| 🚫 | ✅ | View your host and security configs from the CLI to programmatically fetch the API token, among other settings |
|
||||
| ✅ | ✅ | Search your library or collections |
|
||||
| ✅ | ✅ | Add movies to your library |
|
||||
| ✅ | ✅ | Delete movies, downloads, and indexers |
|
||||
| ✅ | ✅ | Trigger automatic searches for movies |
|
||||
| ✅ | ✅ | Trigger refresh and disk scan for movies, downloads, and collections |
|
||||
| ✅ | ✅ | Manually search for movies |
|
||||
| ✅ | ✅ | Edit your movies, collections, and indexers |
|
||||
| ✅ | ✅ | Manage your tags |
|
||||
| ✅ | ✅ | Manage your root folders |
|
||||
| ✅ | ✅ | Manage your blocklist |
|
||||
| ✅ | ✅ | View and browse logs, tasks, events queues, and updates |
|
||||
| ✅ | ✅ | Manually trigger scheduled tasks |
|
||||
| ✅ | ✅ | View your library, downloads, collections, and blocklist |
|
||||
| ✅ | ✅ | View details of a specific movie including description, history, downloaded file info, or the credits |
|
||||
| ✅ | ✅ | View details of any collection and the movies in them |
|
||||
| 🚫 | ✅ | View your host and security configs from the CLI to programmatically fetch the API token, among other settings |
|
||||
| ✅ | ✅ | Search your library or collections |
|
||||
| ✅ | ✅ | Add movies to your library |
|
||||
| ✅ | ✅ | Delete movies, downloads, and indexers |
|
||||
| ✅ | ✅ | Trigger automatic searches for movies |
|
||||
| ✅ | ✅ | Trigger refresh and disk scan for movies, downloads, and collections |
|
||||
| ✅ | ✅ | Manually search for movies |
|
||||
| ✅ | ✅ | Edit your movies, collections, and indexers |
|
||||
| ✅ | ✅ | Manage your tags |
|
||||
| ✅ | ✅ | Manage your root folders |
|
||||
| ✅ | ✅ | Manage your blocklist |
|
||||
| ✅ | ✅ | View and browse logs, tasks, events queues, and updates |
|
||||
| ✅ | ✅ | Manually trigger scheduled tasks |
|
||||
|
||||
### Sonarr
|
||||
|
||||
| TUI | CLI | Feature |
|
||||
|-----|-----|--------------------------------------------------------------------------------------------------------------------|
|
||||
| ✅ | ✅ | View your library, downloads, blocklist, episodes |
|
||||
| ✅ | ✅ | View details of a specific series, or episode including description, history, downloaded file info, or the credits |
|
||||
| 🚫 | ✅ | View your host and security configs from the CLI to programmatically fetch the API token, among other settings |
|
||||
| ✅ | ✅ | Search your library |
|
||||
| ✅ | ✅ | Add series to your library |
|
||||
| ✅ | ✅ | Delete series, downloads, indexers, root folders, and episode files |
|
||||
| ✅ | ✅ | Trigger automatic searches for series, seasons, or episodes |
|
||||
| ✅ | ✅ | Trigger refresh and disk scan for series and downloads |
|
||||
| ✅ | ✅ | Manually search for series, seasons, or episodes |
|
||||
| ✅ | ✅ | Edit your series and indexers |
|
||||
| ✅ | ✅ | Manage your tags |
|
||||
| ✅ | ✅ | Manage your root folders |
|
||||
| ✅ | ✅ | Manage your blocklist |
|
||||
| ✅ | ✅ | View and browse logs, tasks, events queues, and updates |
|
||||
| ✅ | ✅ | Manually trigger scheduled tasks |
|
||||
| ✅ | ✅ | View your library, downloads, blocklist, episodes |
|
||||
| ✅ | ✅ | View details of a specific series, or episode including description, history, downloaded file info, or the credits |
|
||||
| 🚫 | ✅ | View your host and security configs from the CLI to programmatically fetch the API token, among other settings |
|
||||
| ✅ | ✅ | Search your library |
|
||||
| ✅ | ✅ | Add series to your library |
|
||||
| ✅ | ✅ | Delete series, downloads, indexers, root folders, and episode files |
|
||||
| ✅ | ✅ | Trigger automatic searches for series, seasons, or episodes |
|
||||
| ✅ | ✅ | Trigger refresh and disk scan for series and downloads |
|
||||
| ✅ | ✅ | Manually search for series, seasons, or episodes |
|
||||
| ✅ | ✅ | Edit your series and indexers |
|
||||
| ✅ | ✅ | Manage your tags |
|
||||
| ✅ | ✅ | Manage your root folders |
|
||||
| ✅ | ✅ | Manage your blocklist |
|
||||
| ✅ | ✅ | View and browse logs, tasks, events queues, and updates |
|
||||
| ✅ | ✅ | Manually trigger scheduled tasks |
|
||||
|
||||
### Lidarr
|
||||
|
||||
| TUI | CLI | Feature |
|
||||
|-----|-----|----------------------------------------------------------------------------------------------------------------|
|
||||
| ✅ | ✅ | View your library, downloads, blocklist, tracks |
|
||||
| ✅ | ✅ | View details of a specific artists, albums, or tracks including description, history, downloaded file info |
|
||||
| 🚫 | ✅ | View your host and security configs from the CLI to programmatically fetch the API token, among other settings |
|
||||
| ✅ | ✅ | Search your library |
|
||||
| ✅ | ✅ | Add artists to your library |
|
||||
| ✅ | ✅ | Delete artists, downloads, indexers, root folders, and track files |
|
||||
| ✅ | ✅ | Trigger automatic searches for artists or albums |
|
||||
| ✅ | ✅ | Trigger refresh and disk scan for artists and downloads |
|
||||
| ✅ | ✅ | Manually search for full artist discographies or albums |
|
||||
| ✅ | ✅ | Edit your artists and indexers |
|
||||
| ✅ | ✅ | Manage your tags |
|
||||
| ✅ | ✅ | Manage your root folders |
|
||||
| ✅ | ✅ | Manage your blocklist |
|
||||
| ✅ | ✅ | View and browse logs, tasks, events queues, and updates |
|
||||
| ✅ | ✅ | Manually trigger scheduled tasks |
|
||||
| ✅ | ✅ | View your library, downloads, blocklist, tracks |
|
||||
| ✅ | ✅ | View details of a specific artists, albums, or tracks including description, history, downloaded file info |
|
||||
| 🚫 | ✅ | View your host and security configs from the CLI to programmatically fetch the API token, among other settings |
|
||||
| ✅ | ✅ | Search your library |
|
||||
| ✅ | ✅ | Add artists to your library |
|
||||
| ✅ | ✅ | Delete artists, downloads, indexers, root folders, and track files |
|
||||
| ✅ | ✅ | Trigger automatic searches for artists or albums |
|
||||
| ✅ | ✅ | Trigger refresh and disk scan for artists and downloads |
|
||||
| ✅ | ✅ | Manually search for full artist discographies or albums |
|
||||
| ✅ | ✅ | Edit your artists and indexers |
|
||||
| ✅ | ✅ | Manage your tags |
|
||||
| ✅ | ✅ | Manage your root folders |
|
||||
| ✅ | ✅ | Manage your blocklist |
|
||||
| ✅ | ✅ | View and browse logs, tasks, events queues, and updates |
|
||||
| ✅ | ✅ | Manually trigger scheduled tasks |
|
||||
|
||||
### Readarr
|
||||
|
||||
| TUI | CLI | Feature |
|
||||
|-----|-----|----------------------------------------------------------------------------------------------------------------|
|
||||
| ✅ | ✅ | View your library, downloads, blocklist, editions |
|
||||
| ✅ | ✅ | View details of a specific author, book, or edition including description, history, downloaded file info |
|
||||
| 🚫 | ✅ | View your host and security configs from the CLI to programmatically fetch the API token, among other settings |
|
||||
| ✅ | ✅ | Search your library |
|
||||
| ✅ | ✅ | Add authors to your library |
|
||||
| ✅ | ✅ | Delete authors, books, downloads, indexers, root folders, and book files |
|
||||
| ✅ | ✅ | Trigger automatic searches for authors or books |
|
||||
| ✅ | ✅ | Trigger refresh and disk scan for authors and downloads |
|
||||
| ✅ | ✅ | Manually search for author releases or books |
|
||||
| ✅ | ✅ | Edit your authors and indexers |
|
||||
| ✅ | ✅ | Manage your tags |
|
||||
| ✅ | ✅ | Manage your root folders |
|
||||
| ✅ | ✅ | Manage your blocklist |
|
||||
| ✅ | ✅ | View and browse logs, tasks, events queues, and updates |
|
||||
| ✅ | ✅ | Manually trigger scheduled tasks |
|
||||
| ✅ | ✅ | View your library, downloads, blocklist, editions |
|
||||
| ✅ | ✅ | View details of a specific author, book, or edition including description, history, downloaded file info |
|
||||
| 🚫 | ✅ | View your host and security configs from the CLI to programmatically fetch the API token, among other settings |
|
||||
| ✅ | ✅ | Search your library |
|
||||
| ✅ | ✅ | Add authors to your library |
|
||||
| ✅ | ✅ | Delete authors, books, downloads, indexers, root folders, and book files |
|
||||
| ✅ | ✅ | Trigger automatic searches for authors or books |
|
||||
| ✅ | ✅ | Trigger refresh and disk scan for authors and downloads |
|
||||
| ✅ | ✅ | Manually search for author releases or books |
|
||||
| ✅ | ✅ | Edit your authors and indexers |
|
||||
| ✅ | ✅ | Manage your tags |
|
||||
| ✅ | ✅ | Manage your root folders |
|
||||
| ✅ | ✅ | Manage your blocklist |
|
||||
| ✅ | ✅ | View and browse logs, tasks, events queues, and updates |
|
||||
| ✅ | ✅ | Manually trigger scheduled tasks |
|
||||
|
||||
### Whisparr
|
||||
|
||||
@@ -261,7 +266,7 @@ To see all available commands, simply run `managarr --help`:
|
||||
|
||||
```shell
|
||||
$ managarr --help
|
||||
managarr 0.7.0
|
||||
managarr 0.8.0
|
||||
Alex Clarke <alex.j.tusa@gmail.com>
|
||||
|
||||
A TUI and CLI to manage your Servarrs
|
||||
@@ -459,6 +464,11 @@ lidarr:
|
||||
custom_headers: # Example of adding custom headers to all requests to the Servarr instance
|
||||
traefik-auth-bypass-key: someBypassKey1234567890
|
||||
SOME-OTHER-CUSTOM-HEADER: ${MY_CUSTOM_HEADER_VALUE}
|
||||
|
||||
readarr:
|
||||
- name: Chaptarr (Ebooks) # Example of how to use the Readarr support with a Readarr fork like Chaptarr
|
||||
uri: http://192.168.0.87:8789/readarr/gr/ebook
|
||||
api_token: someApiToken1234567890
|
||||
```
|
||||
|
||||
### SSL Configuration
|
||||
@@ -568,6 +578,46 @@ Managarr supports using environment variables on startup so you don't have to al
|
||||
| `MANAGARR_CONFIG_FILE` | Set the path to the config file | `--config` |
|
||||
| `MANAGARR_DISABLE_SPINNER` | Disable the CLI spinner (this can be useful when scripting and parsing output) | `--disable-spinner` |
|
||||
|
||||
## Tested Readarr Forks
|
||||
|
||||
The following Readarr forks have been explicitly tested with Managarr:
|
||||
|
||||
* [Bookshelf](https://github.com/pennydreadful/bookshelf)
|
||||
* [Readarr-Resurrected](https://github.com/ricetim/readarr-rresurrected)
|
||||
* [Librarr](https://github.com/Rorqualx/Librarr)
|
||||
* [Chaptarr](https://github.com/Chaptarr/Chaptarr)
|
||||
|
||||
> [!IMPORTANT]
|
||||
> Chaptarr houses a dedicated Readarr facade middleware that activates on a path prefix, meaning a bare config doesn't
|
||||
> work. Instead, users need to configure both Audiobooks and Ebooks separately, and based on their desired metadata
|
||||
> fetcher (i.e. Goodreads and Hardcover IDs). Because of this, Chaptarr-based configurations must use the `uri` key
|
||||
> rather than the `host` and `port` keys. The below configuration file demonstrates all the supported variations for
|
||||
> connecting Managarr to Chaptarr.
|
||||
|
||||
### Chaptarr Example Configuration
|
||||
```yaml
|
||||
readarr:
|
||||
# Because Chaptarr supports both eBooks and audiobooks, the same instance must be provisioned twice if you're using
|
||||
# both so Managarr can differentiate between the two. Thus, for Chaptarr, you need an eBook server configured and an
|
||||
# audiobook server configured like below.
|
||||
|
||||
- name: eBooks (Goodreads) # Readarr originally used Goodreads as its metadata provider
|
||||
uri: http://192.168.0.87:8789/readarr/gr/ebook # Note the `gr` in the path prefix indicating "GoodReads"
|
||||
api_token: someApiToken1234567890
|
||||
|
||||
- name: Chaptarr Audiobooks (Goodreads)
|
||||
uri: http://192.168.0.105:8789/readarr/gr/audiobook
|
||||
api_token: someApiToken1234567890
|
||||
|
||||
- name: Chaptarr eBooks (Hardcover)
|
||||
uri: http://192.168.0.105:8789/readarr/hc/ebook # Note the `hc` in the path prefix indicating "HardCover"
|
||||
api_token: someApiToken1234567890
|
||||
|
||||
- name: Chaptarr Audiobooks (Hardcover)
|
||||
uri: http://192.168.0.105:8789/readarr/hc/audiobook
|
||||
api_token: someApiToken1234567890
|
||||
```
|
||||
|
||||
## Screenshots
|
||||
|
||||
### Radarr
|
||||
@@ -590,6 +640,13 @@ Managarr supports using environment variables on startup so you don't have to al
|
||||

|
||||

|
||||
|
||||
### Readarr
|
||||

|
||||

|
||||

|
||||

|
||||

|
||||
|
||||
### General
|
||||

|
||||

|
||||
|
||||
|
Before Width: | Height: | Size: 187 KiB After Width: | Height: | Size: 194 KiB |
|
After Width: | Height: | Size: 139 KiB |
|
After Width: | Height: | Size: 120 KiB |
|
After Width: | Height: | Size: 258 KiB |
|
After Width: | Height: | Size: 164 KiB |
|
After Width: | Height: | Size: 186 KiB |
@@ -9,7 +9,9 @@ mod tests {
|
||||
use tokio::sync::Mutex;
|
||||
|
||||
use crate::cli::lidarr::LidarrCommand;
|
||||
use crate::cli::readarr::ReadarrCommand;
|
||||
use crate::network::lidarr_network::LidarrEvent;
|
||||
use crate::network::readarr_network::ReadarrEvent;
|
||||
use crate::{
|
||||
Cli,
|
||||
app::App,
|
||||
@@ -24,6 +26,10 @@ mod tests {
|
||||
BlocklistItem as RadarrBlocklistItem, BlocklistResponse as RadarrBlocklistResponse,
|
||||
RadarrSerdeable,
|
||||
},
|
||||
readarr_models::{
|
||||
BlocklistItem as ReadarrBlocklistItem, BlocklistResponse as ReadarrBlocklistResponse,
|
||||
ReadarrSerdeable,
|
||||
},
|
||||
sonarr_models::{
|
||||
BlocklistItem as SonarrBlocklistItem, BlocklistResponse as SonarrBlocklistResponse,
|
||||
SonarrSerdeable,
|
||||
@@ -68,6 +74,13 @@ mod tests {
|
||||
assert_ok!(&result);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_readarr_subcommand_delegates_to_readarr() {
|
||||
let result = Cli::command().try_get_matches_from(["managarr", "readarr", "list", "authors"]);
|
||||
|
||||
assert_ok!(&result);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_completions_requires_argument() {
|
||||
let result = Cli::command().try_get_matches_from(["managarr", "completions"]);
|
||||
@@ -218,4 +231,35 @@ mod tests {
|
||||
|
||||
assert_ok!(&result);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_cli_handler_delegates_readarr_commands_to_the_readarr_cli_handler() {
|
||||
let mut mock_network = MockNetworkTrait::new();
|
||||
mock_network
|
||||
.expect_handle_network_event()
|
||||
.with(eq::<NetworkEvent>(ReadarrEvent::GetBlocklist.into()))
|
||||
.times(1)
|
||||
.returning(|_| {
|
||||
Ok(Serdeable::Readarr(ReadarrSerdeable::BlocklistResponse(
|
||||
ReadarrBlocklistResponse {
|
||||
records: vec![ReadarrBlocklistItem::default()],
|
||||
},
|
||||
)))
|
||||
});
|
||||
mock_network
|
||||
.expect_handle_network_event()
|
||||
.with(eq::<NetworkEvent>(ReadarrEvent::ClearBlocklist.into()))
|
||||
.times(1)
|
||||
.returning(|_| {
|
||||
Ok(Serdeable::Readarr(ReadarrSerdeable::Value(
|
||||
json!({"testResponse": "response"}),
|
||||
)))
|
||||
});
|
||||
let app_arc = Arc::new(Mutex::new(App::test_default()));
|
||||
let clear_blocklist_command = ReadarrCommand::ClearBlocklist.into();
|
||||
|
||||
let result = handle_command(&app_arc, clear_blocklist_command, &mut mock_network).await;
|
||||
|
||||
assert_ok!(&result);
|
||||
}
|
||||
}
|
||||
@@ -171,6 +171,7 @@ impl<'a, 'b> CliCommandHandler<'a, 'b, ReadarrAddCommand> for ReadarrAddCommandH
|
||||
quality_profile_id,
|
||||
metadata_profile_id,
|
||||
tags,
|
||||
images: Vec::new(),
|
||||
tag_input_string: None,
|
||||
add_options: AddAuthorOptions {
|
||||
monitor,
|
||||
|
||||
@@ -447,6 +447,7 @@ mod tests {
|
||||
quality_profile_id: 1,
|
||||
metadata_profile_id: 1,
|
||||
tags: vec![1, 2],
|
||||
images: Vec::new(),
|
||||
tag_input_string: None,
|
||||
add_options: AddAuthorOptions {
|
||||
monitor: MonitorType::All,
|
||||
|
||||
@@ -1,5 +1,20 @@
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use crate::cli::{
|
||||
Command,
|
||||
readarr::{ReadarrCommand, list_command_handler::ReadarrListCommand},
|
||||
};
|
||||
use pretty_assertions::assert_eq;
|
||||
|
||||
#[test]
|
||||
fn test_readarr_command_from() {
|
||||
let command = ReadarrCommand::List(ReadarrListCommand::Authors);
|
||||
|
||||
let result = Command::from(command.clone());
|
||||
|
||||
assert_eq!(result, Command::Readarr(command));
|
||||
}
|
||||
|
||||
mod cli {
|
||||
use clap::CommandFactory;
|
||||
use clap::error::ErrorKind;
|
||||
@@ -243,20 +258,246 @@ mod tests {
|
||||
use serde_json::json;
|
||||
use tokio::sync::Mutex;
|
||||
|
||||
use crate::cli::readarr::add_command_handler::ReadarrAddCommand;
|
||||
use crate::cli::readarr::edit_command_handler::ReadarrEditCommand;
|
||||
use crate::cli::readarr::get_command_handler::ReadarrGetCommand;
|
||||
use crate::cli::readarr::refresh_command_handler::ReadarrRefreshCommand;
|
||||
use crate::cli::readarr::trigger_automatic_search_command_handler::ReadarrTriggerAutomaticSearchCommand;
|
||||
use crate::models::readarr_models::{
|
||||
BlocklistItem, BlocklistResponse, ReadarrSerdeable, ReadarrTaskName,
|
||||
Author, BlocklistItem, BlocklistResponse, DeleteParams, ReadarrSerdeable, ReadarrTaskName,
|
||||
};
|
||||
use crate::models::servarr_models::ReleaseDownloadBody;
|
||||
use crate::models::servarr_models::{IndexerSettings, ReleaseDownloadBody};
|
||||
use crate::{
|
||||
app::App,
|
||||
cli::{
|
||||
CliCommandHandler,
|
||||
readarr::{ReadarrCliHandler, ReadarrCommand},
|
||||
readarr::{
|
||||
ReadarrCliHandler, ReadarrCommand, delete_command_handler::ReadarrDeleteCommand,
|
||||
list_command_handler::ReadarrListCommand,
|
||||
},
|
||||
},
|
||||
models::Serdeable,
|
||||
network::{MockNetworkTrait, NetworkEvent, readarr_network::ReadarrEvent},
|
||||
};
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_readarr_cli_handler_delegates_add_commands_to_the_add_command_handler() {
|
||||
let expected_tag_name = "test".to_owned();
|
||||
let mut mock_network = MockNetworkTrait::new();
|
||||
mock_network
|
||||
.expect_handle_network_event()
|
||||
.with(eq::<NetworkEvent>(
|
||||
ReadarrEvent::AddTag(expected_tag_name.clone()).into(),
|
||||
))
|
||||
.times(1)
|
||||
.returning(|_| {
|
||||
Ok(Serdeable::Readarr(ReadarrSerdeable::Value(
|
||||
json!({"testResponse": "response"}),
|
||||
)))
|
||||
});
|
||||
let app_arc = Arc::new(Mutex::new(App::test_default()));
|
||||
let add_tag_command = ReadarrCommand::Add(ReadarrAddCommand::Tag {
|
||||
name: expected_tag_name,
|
||||
});
|
||||
|
||||
let result = ReadarrCliHandler::with(&app_arc, add_tag_command, &mut mock_network)
|
||||
.handle()
|
||||
.await;
|
||||
|
||||
assert_ok!(&result);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_readarr_cli_handler_delegates_get_commands_to_the_get_command_handler() {
|
||||
let mut mock_network = MockNetworkTrait::new();
|
||||
mock_network
|
||||
.expect_handle_network_event()
|
||||
.with(eq::<NetworkEvent>(ReadarrEvent::GetStatus.into()))
|
||||
.times(1)
|
||||
.returning(|_| {
|
||||
Ok(Serdeable::Readarr(ReadarrSerdeable::Value(
|
||||
json!({"testResponse": "response"}),
|
||||
)))
|
||||
});
|
||||
let app_arc = Arc::new(Mutex::new(App::test_default()));
|
||||
let get_system_status_command = ReadarrCommand::Get(ReadarrGetCommand::SystemStatus);
|
||||
|
||||
let result = ReadarrCliHandler::with(&app_arc, get_system_status_command, &mut mock_network)
|
||||
.handle()
|
||||
.await;
|
||||
|
||||
assert_ok!(&result);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_readarr_cli_handler_delegates_delete_commands_to_the_delete_command_handler() {
|
||||
let expected_delete_author_params = DeleteParams {
|
||||
id: 7,
|
||||
delete_files: true,
|
||||
add_import_list_exclusion: false,
|
||||
};
|
||||
let mut mock_network = MockNetworkTrait::new();
|
||||
mock_network
|
||||
.expect_handle_network_event()
|
||||
.with(eq::<NetworkEvent>(
|
||||
ReadarrEvent::DeleteAuthor(expected_delete_author_params).into(),
|
||||
))
|
||||
.times(1)
|
||||
.returning(|_| {
|
||||
Ok(Serdeable::Readarr(ReadarrSerdeable::Value(
|
||||
json!({"testResponse": "response"}),
|
||||
)))
|
||||
});
|
||||
let app_arc = Arc::new(Mutex::new(App::test_default()));
|
||||
let delete_author_command = ReadarrCommand::Delete(ReadarrDeleteCommand::Author {
|
||||
author_id: 7,
|
||||
delete_files_from_disk: true,
|
||||
add_list_exclusion: false,
|
||||
});
|
||||
|
||||
let result = ReadarrCliHandler::with(&app_arc, delete_author_command, &mut mock_network)
|
||||
.handle()
|
||||
.await;
|
||||
|
||||
assert_ok!(&result);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_readarr_cli_handler_delegates_edit_commands_to_the_edit_command_handler() {
|
||||
let expected_edit_all_indexer_settings = IndexerSettings {
|
||||
id: 1,
|
||||
maximum_size: 26500,
|
||||
minimum_age: 17,
|
||||
retention: 43,
|
||||
rss_sync_interval: 35,
|
||||
};
|
||||
let mut mock_network = MockNetworkTrait::new();
|
||||
mock_network
|
||||
.expect_handle_network_event()
|
||||
.with(eq::<NetworkEvent>(
|
||||
ReadarrEvent::GetAllIndexerSettings.into(),
|
||||
))
|
||||
.times(1)
|
||||
.returning(|_| {
|
||||
Ok(Serdeable::Readarr(ReadarrSerdeable::IndexerSettings(
|
||||
IndexerSettings {
|
||||
id: 9,
|
||||
maximum_size: 31200,
|
||||
minimum_age: 22,
|
||||
retention: 58,
|
||||
rss_sync_interval: 90,
|
||||
},
|
||||
)))
|
||||
});
|
||||
mock_network
|
||||
.expect_handle_network_event()
|
||||
.with(eq::<NetworkEvent>(
|
||||
ReadarrEvent::EditAllIndexerSettings(expected_edit_all_indexer_settings).into(),
|
||||
))
|
||||
.times(1)
|
||||
.returning(|_| {
|
||||
Ok(Serdeable::Readarr(ReadarrSerdeable::Value(
|
||||
json!({"testResponse": "response"}),
|
||||
)))
|
||||
});
|
||||
let app_arc = Arc::new(Mutex::new(App::test_default()));
|
||||
let edit_all_indexer_settings_command =
|
||||
ReadarrCommand::Edit(ReadarrEditCommand::AllIndexerSettings {
|
||||
maximum_size: Some(26500),
|
||||
minimum_age: Some(17),
|
||||
retention: Some(43),
|
||||
rss_sync_interval: Some(35),
|
||||
});
|
||||
|
||||
let result = ReadarrCliHandler::with(
|
||||
&app_arc,
|
||||
edit_all_indexer_settings_command,
|
||||
&mut mock_network,
|
||||
)
|
||||
.handle()
|
||||
.await;
|
||||
|
||||
assert_ok!(&result);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_readarr_cli_handler_delegates_list_commands_to_the_list_command_handler() {
|
||||
let mut mock_network = MockNetworkTrait::new();
|
||||
mock_network
|
||||
.expect_handle_network_event()
|
||||
.with(eq::<NetworkEvent>(ReadarrEvent::ListAuthors.into()))
|
||||
.times(1)
|
||||
.returning(|_| {
|
||||
Ok(Serdeable::Readarr(ReadarrSerdeable::Authors(vec![
|
||||
Author::default(),
|
||||
])))
|
||||
});
|
||||
let app_arc = Arc::new(Mutex::new(App::test_default()));
|
||||
let list_authors_command = ReadarrCommand::List(ReadarrListCommand::Authors);
|
||||
|
||||
let result = ReadarrCliHandler::with(&app_arc, list_authors_command, &mut mock_network)
|
||||
.handle()
|
||||
.await;
|
||||
|
||||
assert_ok!(&result);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_readarr_cli_handler_delegates_refresh_commands_to_the_refresh_command_handler() {
|
||||
let mut mock_network = MockNetworkTrait::new();
|
||||
mock_network
|
||||
.expect_handle_network_event()
|
||||
.with(eq::<NetworkEvent>(ReadarrEvent::UpdateAllAuthors.into()))
|
||||
.times(1)
|
||||
.returning(|_| {
|
||||
Ok(Serdeable::Readarr(ReadarrSerdeable::Value(
|
||||
json!({"testResponse": "response"}),
|
||||
)))
|
||||
});
|
||||
let app_arc = Arc::new(Mutex::new(App::test_default()));
|
||||
let refresh_all_authors_command = ReadarrCommand::Refresh(ReadarrRefreshCommand::AllAuthors);
|
||||
|
||||
let result =
|
||||
ReadarrCliHandler::with(&app_arc, refresh_all_authors_command, &mut mock_network)
|
||||
.handle()
|
||||
.await;
|
||||
|
||||
assert_ok!(&result);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_readarr_cli_handler_delegates_trigger_automatic_search_commands_to_the_trigger_automatic_search_command_handler()
|
||||
{
|
||||
let expected_author_id = 7;
|
||||
let mut mock_network = MockNetworkTrait::new();
|
||||
mock_network
|
||||
.expect_handle_network_event()
|
||||
.with(eq::<NetworkEvent>(
|
||||
ReadarrEvent::TriggerAutomaticAuthorSearch(expected_author_id).into(),
|
||||
))
|
||||
.times(1)
|
||||
.returning(|_| {
|
||||
Ok(Serdeable::Readarr(ReadarrSerdeable::Value(
|
||||
json!({"testResponse": "response"}),
|
||||
)))
|
||||
});
|
||||
let app_arc = Arc::new(Mutex::new(App::test_default()));
|
||||
let trigger_automatic_search_command =
|
||||
ReadarrCommand::TriggerAutomaticSearch(ReadarrTriggerAutomaticSearchCommand::Author {
|
||||
author_id: expected_author_id,
|
||||
});
|
||||
|
||||
let result = ReadarrCliHandler::with(
|
||||
&app_arc,
|
||||
trigger_automatic_search_command,
|
||||
&mut mock_network,
|
||||
)
|
||||
.handle()
|
||||
.await;
|
||||
|
||||
assert_ok!(&result);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_mark_history_item_as_failed_command() {
|
||||
let expected_history_item_id = 1234i64;
|
||||
|
||||
@@ -88,6 +88,7 @@ impl AddAuthorHandler<'_, '_> {
|
||||
quality_profile_id,
|
||||
metadata_profile_id,
|
||||
tags: Vec::new(),
|
||||
images: Vec::new(),
|
||||
tag_input_string: Some(tags),
|
||||
add_options: AddAuthorOptions {
|
||||
monitor,
|
||||
|
||||
@@ -1237,6 +1237,7 @@ mod tests {
|
||||
quality_profile_id: 2222,
|
||||
metadata_profile_id: 4444,
|
||||
tags: Vec::default(),
|
||||
images: Vec::new(),
|
||||
tag_input_string: Some("usenet, testing".to_owned()),
|
||||
add_options: AddAuthorOptions {
|
||||
monitor: MonitorType::Future,
|
||||
@@ -1628,6 +1629,7 @@ mod tests {
|
||||
quality_profile_id: 2222,
|
||||
metadata_profile_id: 4444,
|
||||
tags: Vec::default(),
|
||||
images: Vec::new(),
|
||||
tag_input_string: Some("usenet, testing".to_owned()),
|
||||
add_options: AddAuthorOptions {
|
||||
monitor: MonitorType::Future,
|
||||
@@ -1795,6 +1797,7 @@ mod tests {
|
||||
quality_profile_id: 2222,
|
||||
metadata_profile_id: 4444,
|
||||
tags: Vec::default(),
|
||||
images: Vec::new(),
|
||||
tag_input_string: Some("usenet, testing".to_owned()),
|
||||
add_options: AddAuthorOptions {
|
||||
monitor: MonitorType::Future,
|
||||
|
||||
@@ -222,6 +222,8 @@ pub struct AddAuthorBody {
|
||||
pub quality_profile_id: i64,
|
||||
pub metadata_profile_id: i64,
|
||||
pub tags: Vec<i64>,
|
||||
// Readarr-Resurrected persists the add body directly and rejects it when the images list is absent
|
||||
pub images: Vec<Value>,
|
||||
#[serde(skip_serializing, skip_deserializing)]
|
||||
pub tag_input_string: Option<String>,
|
||||
pub add_options: AddAuthorOptions,
|
||||
@@ -280,6 +282,7 @@ pub struct Book {
|
||||
pub ratings: Option<Ratings>,
|
||||
pub release_date: Option<DateTime<Utc>>,
|
||||
pub statistics: Option<BookStatistics>,
|
||||
#[serde(default)]
|
||||
pub grabbed: bool,
|
||||
}
|
||||
|
||||
@@ -454,6 +457,29 @@ pub struct ReadarrRelease {
|
||||
pub quality: QualityWrapper,
|
||||
}
|
||||
|
||||
#[derive(Deserialize, Clone, Debug, PartialEq, Eq)]
|
||||
#[serde(untagged)]
|
||||
pub enum ReadarrReleaseResponse {
|
||||
Releases(Vec<ReadarrRelease>),
|
||||
// Chaptarr wraps the release list in a filter summary envelope, even behind its Readarr facade
|
||||
Envelope { releases: Vec<ReadarrRelease> },
|
||||
}
|
||||
|
||||
impl Default for ReadarrReleaseResponse {
|
||||
fn default() -> Self {
|
||||
ReadarrReleaseResponse::Releases(Vec::new())
|
||||
}
|
||||
}
|
||||
|
||||
impl ReadarrReleaseResponse {
|
||||
pub fn into_releases(self) -> Vec<ReadarrRelease> {
|
||||
match self {
|
||||
ReadarrReleaseResponse::Releases(releases)
|
||||
| ReadarrReleaseResponse::Envelope { releases } => releases,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Default, Serialize, Deserialize, Debug, Clone, PartialEq, Eq)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct BlocklistItem {
|
||||
|
||||
@@ -9,7 +9,7 @@ mod tests {
|
||||
AddAuthorSearchResult, BlocklistItem, BlocklistResponse, Book, BookFile, DownloadRecord,
|
||||
DownloadsResponse, Edition, MediaInfo, MonitorType, NewItemMonitorType,
|
||||
ReadarrHistoryEventType, ReadarrHistoryItem, ReadarrHistoryWrapper, ReadarrRelease,
|
||||
ReadarrTask, ReadarrTaskName,
|
||||
ReadarrReleaseResponse, ReadarrTask, ReadarrTaskName,
|
||||
};
|
||||
use crate::models::servarr_models::{
|
||||
DiskSpace, DownloadStatus, HostConfig, Indexer, IndexerSettings, IndexerTestResult, Log,
|
||||
@@ -497,6 +497,29 @@ mod tests {
|
||||
assert_eq!(readarr_serdeable, ReadarrSerdeable::Releases(releases));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_readarr_release_response_default_is_an_empty_release_list() {
|
||||
assert_eq!(
|
||||
ReadarrReleaseResponse::default(),
|
||||
ReadarrReleaseResponse::Releases(Vec::new())
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_readarr_release_response_into_releases() {
|
||||
let releases = vec![ReadarrRelease {
|
||||
guid: "test".to_owned(),
|
||||
..ReadarrRelease::default()
|
||||
}];
|
||||
let list = ReadarrReleaseResponse::Releases(releases.clone());
|
||||
let envelope = ReadarrReleaseResponse::Envelope {
|
||||
releases: releases.clone(),
|
||||
};
|
||||
|
||||
assert_eq!(list.into_releases(), releases);
|
||||
assert_eq!(envelope.into_releases(), releases);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_readarr_serdeable_from_security_config() {
|
||||
let security_config = SecurityConfig {
|
||||
@@ -939,6 +962,22 @@ mod tests {
|
||||
assert_none!(&book.statistics);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_book_deserialization_defaults_grabbed_when_absent() {
|
||||
let book_json = json!({
|
||||
"id": 1,
|
||||
"title": "The Name of the Wind",
|
||||
"authorId": 3,
|
||||
"foreignBookId": "test-foreign-book-id",
|
||||
"monitored": true
|
||||
});
|
||||
|
||||
let book: Book = serde_json::from_value(book_json).unwrap();
|
||||
|
||||
assert!(!book.grabbed);
|
||||
assert!(book.monitored);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_edition_deserialization() {
|
||||
let edition_json = json!({
|
||||
|
||||
@@ -5,7 +5,7 @@ use serde_json::{Value, json};
|
||||
use crate::models::Route;
|
||||
use crate::models::readarr_models::{
|
||||
AddAuthorBody, AddAuthorSearchResult, Author, DeleteParams, EditAuthorParams, ReadarrCommandBody,
|
||||
ReadarrHistoryItem, ReadarrRelease,
|
||||
ReadarrHistoryItem, ReadarrRelease, ReadarrReleaseResponse,
|
||||
};
|
||||
use crate::models::servarr_data::readarr::readarr_data::ActiveReadarrBlock;
|
||||
use crate::models::stateful_table::StatefulTable;
|
||||
@@ -235,10 +235,15 @@ impl Network<'_, '_> {
|
||||
.await;
|
||||
|
||||
self
|
||||
.handle_request::<(), Vec<ReadarrRelease>>(request_props, |release_vec, mut app| {
|
||||
app.data.readarr_data.author_releases.set_items(release_vec);
|
||||
.handle_request::<(), ReadarrReleaseResponse>(request_props, |response, mut app| {
|
||||
app
|
||||
.data
|
||||
.readarr_data
|
||||
.author_releases
|
||||
.set_items(response.into_releases());
|
||||
})
|
||||
.await
|
||||
.map(ReadarrReleaseResponse::into_releases)
|
||||
}
|
||||
|
||||
pub(in crate::network::readarr_network) async fn get_readarr_author_history(
|
||||
|
||||
@@ -133,6 +133,48 @@ mod tests {
|
||||
);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_handle_get_author_releases_event_unwraps_the_release_envelope() {
|
||||
let expected_releases = vec![torrent_release()];
|
||||
let releases_json = json!({
|
||||
"releases": [serde_json::from_str::<Value>(RELEASE_JSON).unwrap()],
|
||||
"hiddenReleases": [],
|
||||
"filterSummary": { "totalResults": 1, "displayedCount": 1 },
|
||||
"siblingToggleEnabled": false
|
||||
});
|
||||
let (mock, app, _server) = MockServarrApi::get()
|
||||
.returns(releases_json)
|
||||
.query("authorId=3")
|
||||
.build_for(ReadarrEvent::GetAuthorReleases(3))
|
||||
.await;
|
||||
{
|
||||
let mut app = app.lock().await;
|
||||
app.server_tabs.set_index(3);
|
||||
app
|
||||
.data
|
||||
.readarr_data
|
||||
.author_releases
|
||||
.set_items(vec![stale_release()]);
|
||||
}
|
||||
let mut network = test_network(&app);
|
||||
|
||||
let result = network
|
||||
.handle_readarr_event(ReadarrEvent::GetAuthorReleases(3))
|
||||
.await;
|
||||
|
||||
mock.assert_async().await;
|
||||
|
||||
let ReadarrSerdeable::Releases(releases) = result.unwrap() else {
|
||||
panic!("Expected Releases")
|
||||
};
|
||||
|
||||
assert_eq!(releases, expected_releases);
|
||||
assert_eq!(
|
||||
app.lock().await.data.readarr_data.author_releases.items,
|
||||
expected_releases
|
||||
);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_handle_get_author_releases_event_failure() {
|
||||
let stale_releases = vec![stale_release()];
|
||||
@@ -638,6 +680,7 @@ mod tests {
|
||||
"qualityProfileId": 1,
|
||||
"metadataProfileId": 1,
|
||||
"tags": [1, 2],
|
||||
"images": [],
|
||||
"addOptions": {
|
||||
"monitor": "all",
|
||||
"monitorNewItems": "all",
|
||||
@@ -680,6 +723,7 @@ mod tests {
|
||||
"qualityProfileId": 1,
|
||||
"metadataProfileId": 1,
|
||||
"tags": [1, 2],
|
||||
"images": [],
|
||||
"addOptions": {
|
||||
"monitor": "all",
|
||||
"monitorNewItems": "all",
|
||||
|
||||
@@ -5,6 +5,7 @@ use serde_json::{Value, json};
|
||||
use crate::models::Route;
|
||||
use crate::models::readarr_models::{
|
||||
Book, BookFile, DeleteParams, Edition, ReadarrCommandBody, ReadarrHistoryItem, ReadarrRelease,
|
||||
ReadarrReleaseResponse,
|
||||
};
|
||||
use crate::models::servarr_data::readarr::readarr_data::ActiveReadarrBlock;
|
||||
use crate::network::readarr_network::ReadarrEvent;
|
||||
@@ -170,16 +171,19 @@ impl Network<'_, '_> {
|
||||
.await;
|
||||
|
||||
self
|
||||
.handle_request::<(), Vec<ReadarrRelease>>(request_props, |release_vec, mut app| {
|
||||
.handle_request::<(), ReadarrReleaseResponse>(request_props, |response, mut app| {
|
||||
let book_details_modal = app
|
||||
.data
|
||||
.readarr_data
|
||||
.book_details_modal
|
||||
.get_or_insert_default();
|
||||
|
||||
book_details_modal.book_releases.set_items(release_vec);
|
||||
book_details_modal
|
||||
.book_releases
|
||||
.set_items(response.into_releases());
|
||||
})
|
||||
.await
|
||||
.map(ReadarrReleaseResponse::into_releases)
|
||||
}
|
||||
|
||||
pub(in crate::network::readarr_network) async fn get_books(
|
||||
|
||||
@@ -532,6 +532,57 @@ mod tests {
|
||||
);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_handle_get_book_releases_event_unwraps_the_release_envelope() {
|
||||
let expected_releases = vec![torrent_release()];
|
||||
let releases_json = json!({
|
||||
"releases": [serde_json::from_str::<Value>(RELEASE_JSON).unwrap()],
|
||||
"hiddenReleases": [],
|
||||
"filterSummary": { "totalResults": 1, "displayedCount": 1 },
|
||||
"siblingToggleEnabled": false
|
||||
});
|
||||
let (mock, app, _server) = MockServarrApi::get()
|
||||
.returns(releases_json)
|
||||
.query("bookId=7")
|
||||
.build_for(ReadarrEvent::GetBookReleases(7))
|
||||
.await;
|
||||
{
|
||||
let mut app = app.lock().await;
|
||||
app.server_tabs.set_index(3);
|
||||
let mut book_details_modal = BookDetailsModal::default();
|
||||
book_details_modal
|
||||
.book_releases
|
||||
.set_items(vec![stale_release()]);
|
||||
app.data.readarr_data.book_details_modal = Some(book_details_modal);
|
||||
}
|
||||
let mut network = test_network(&app);
|
||||
|
||||
let result = network
|
||||
.handle_readarr_event(ReadarrEvent::GetBookReleases(7))
|
||||
.await;
|
||||
|
||||
mock.assert_async().await;
|
||||
|
||||
let ReadarrSerdeable::Releases(releases) = result.unwrap() else {
|
||||
panic!("Expected Releases")
|
||||
};
|
||||
|
||||
assert_eq!(releases, expected_releases);
|
||||
assert_eq!(
|
||||
app
|
||||
.lock()
|
||||
.await
|
||||
.data
|
||||
.readarr_data
|
||||
.book_details_modal
|
||||
.as_ref()
|
||||
.unwrap()
|
||||
.book_releases
|
||||
.items,
|
||||
expected_releases
|
||||
);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_handle_get_book_releases_event_empty_book_details_modal() {
|
||||
let expected_releases = vec![torrent_release()];
|
||||
|
||||
@@ -394,6 +394,7 @@ pub mod test_utils {
|
||||
quality_profile_id: 1,
|
||||
metadata_profile_id: 1,
|
||||
tags: Vec::default(),
|
||||
images: Vec::new(),
|
||||
tag_input_string: Some("usenet, testing".to_owned()),
|
||||
add_options: AddAuthorOptions {
|
||||
monitor: MonitorType::All,
|
||||
|
||||
@@ -46,7 +46,7 @@ impl DrawUi for CollectionDetailsUi {
|
||||
draw_popup(f, app, draw_collection_details, Size::Large);
|
||||
|
||||
if context_option.unwrap_or(active_radarr_block) == ActiveRadarrBlock::ViewMovieOverview {
|
||||
draw_popup(f, app, draw_movie_overview, Size::Large);
|
||||
draw_popup(f, app, draw_movie_overview, Size::Medium);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -8,40 +8,40 @@ expression: output
|
||||
|
||||
|
||||
|
||||
╭ Overview ──────────────────────────────────────────────────────────────────────────────────────────────────────────────╮
|
||||
│Collection movie blah blah blah │
|
||||
│ │
|
||||
│It was shot in Madison, Wisconsin: a city the cast never really left. │
|
||||
│ │
|
||||
│The sequel took seven years to finish. │
|
||||
│ │
|
||||
│ │
|
||||
│ │
|
||||
│ │
|
||||
│ │
|
||||
│ │
|
||||
│ │
|
||||
│ │
|
||||
│ │
|
||||
│ │
|
||||
│ │
|
||||
│ │
|
||||
│ │
|
||||
│ │
|
||||
│ │
|
||||
│ │
|
||||
│ │
|
||||
│ │
|
||||
│ │
|
||||
│ │
|
||||
│ │
|
||||
│ │
|
||||
│ │
|
||||
│ │
|
||||
│ │
|
||||
│ │
|
||||
│ │
|
||||
│ │
|
||||
╭ Test Collection ───────────────────────────────────────────────────────────────────────────────────────────────────────╮
|
||||
│Overview: Collection blah blah blah │
|
||||
│Root Folder Path: /nfs/movies │
|
||||
│Quality Profile: HD - 1080p │
|
||||
│Minimum Avai╭ Overview ─────────────────────────────────────────────────────────────────────────────────────╮ │
|
||||
│Monitored: Y│Collection movie blah blah blah │ │
|
||||
│Search on Ad│ │ │
|
||||
│ │It was shot in Madison, Wisconsin: a city the cast never really left. │ │
|
||||
│ │ │ │
|
||||
│ │The sequel took seven years to finish. │ │
|
||||
│ Movies ──│ │───────────│
|
||||
│ ✔ Title │ │ │
|
||||
│=> ✔ Test │ │ │
|
||||
│ │ │ │
|
||||
│ │ │ │
|
||||
│ │ │ │
|
||||
│ │ │ │
|
||||
│ │ │ │
|
||||
│ │ │ │
|
||||
│ │ │ │
|
||||
│ │ │ │
|
||||
│ │ │ │
|
||||
│ │ │ │
|
||||
│ │ │ │
|
||||
│ │ │ │
|
||||
│ │ │ │
|
||||
│ │ │ │
|
||||
│ │ │ │
|
||||
│ │ │ │
|
||||
│ │ │ │
|
||||
│ │ │ │
|
||||
│ │ │ │
|
||||
│ │ │ │
|
||||
│ ╰─────────────────────────────────────────────────────────────────────────────────────────────────╯ │
|
||||
│ │
|
||||
│ │
|
||||
│ │
|
||||
|
||||
@@ -8,40 +8,40 @@ expression: output
|
||||
|
||||
|
||||
|
||||
╭ Overview ──────────────────────────────────────────────────────────────────────────────────────────────────────────────╮
|
||||
│ │
|
||||
│ │
|
||||
│ Loading ... │
|
||||
│ │
|
||||
│ │
|
||||
│ │
|
||||
│ │
|
||||
│ │
|
||||
│ │
|
||||
│ │
|
||||
│ │
|
||||
│ │
|
||||
│ │
|
||||
│ │
|
||||
│ │
|
||||
│ │
|
||||
│ │
|
||||
│ │
|
||||
│ │
|
||||
│ │
|
||||
│ │
|
||||
│ │
|
||||
│ │
|
||||
│ │
|
||||
│ │
|
||||
│ │
|
||||
│ │
|
||||
│ │
|
||||
│ │
|
||||
│ │
|
||||
│ │
|
||||
│ │
|
||||
│ │
|
||||
╭ Test Collection ───────────────────────────────────────────────────────────────────────────────────────────────────────╮
|
||||
│Overview: Collection blah blah blah │
|
||||
│Root Folder Path: /nfs/movies │
|
||||
│Quality Profile: HD - 1080p │
|
||||
│Minimum Avai╭ Overview ─────────────────────────────────────────────────────────────────────────────────────╮ │
|
||||
│Monitored: Y│ │ │
|
||||
│Search on Ad│ │ │
|
||||
│ │ Loading ... │ │
|
||||
│ │ │ │
|
||||
│ │ │ │
|
||||
│ Movies ──│ │───────────│
|
||||
│ │ │ │
|
||||
│ │ │ │
|
||||
│ Loading ...│ │ │
|
||||
│ │ │ │
|
||||
│ │ │ │
|
||||
│ │ │ │
|
||||
│ │ │ │
|
||||
│ │ │ │
|
||||
│ │ │ │
|
||||
│ │ │ │
|
||||
│ │ │ │
|
||||
│ │ │ │
|
||||
│ │ │ │
|
||||
│ │ │ │
|
||||
│ │ │ │
|
||||
│ │ │ │
|
||||
│ │ │ │
|
||||
│ │ │ │
|
||||
│ │ │ │
|
||||
│ │ │ │
|
||||
│ │ │ │
|
||||
│ │ │ │
|
||||
│ ╰─────────────────────────────────────────────────────────────────────────────────────────────────╯ │
|
||||
│ │
|
||||
│ │
|
||||
│ │
|
||||
|
||||
@@ -8,40 +8,40 @@ expression: output
|
||||
|
||||
|
||||
|
||||
╭ Overview ──────────────────────────────────────────────────────────────────────────────────────────────────────────────╮
|
||||
│ │
|
||||
│ │
|
||||
│ Loading ... │
|
||||
│ │
|
||||
│ │
|
||||
│ │
|
||||
│ │
|
||||
│ │
|
||||
│ │
|
||||
│ │
|
||||
│ │
|
||||
│ │
|
||||
│ │
|
||||
│ │
|
||||
│ │
|
||||
│ │
|
||||
│ │
|
||||
│ │
|
||||
│ │
|
||||
│ │
|
||||
│ │
|
||||
│ │
|
||||
│ │
|
||||
│ │
|
||||
│ │
|
||||
│ │
|
||||
│ │
|
||||
│ │
|
||||
│ │
|
||||
│ │
|
||||
│ │
|
||||
│ │
|
||||
│ │
|
||||
╭ Test Collection ───────────────────────────────────────────────────────────────────────────────────────────────────────╮
|
||||
│Overview: Collection blah blah blah │
|
||||
│Root Folder Path: /nfs/movies │
|
||||
│Quality Profile: HD - 1080p │
|
||||
│Minimum Avai╭ Overview ─────────────────────────────────────────────────────────────────────────────────────╮ │
|
||||
│Monitored: Y│ │ │
|
||||
│Search on Ad│ │ │
|
||||
│ │ Loading ... │ │
|
||||
│ │ │ │
|
||||
│ │ │ │
|
||||
│ Movies ──│ │───────────│
|
||||
│ ✔ Title │ │ │
|
||||
│=> ✔ Test │ │ │
|
||||
│ │ │ │
|
||||
│ │ │ │
|
||||
│ │ │ │
|
||||
│ │ │ │
|
||||
│ │ │ │
|
||||
│ │ │ │
|
||||
│ │ │ │
|
||||
│ │ │ │
|
||||
│ │ │ │
|
||||
│ │ │ │
|
||||
│ │ │ │
|
||||
│ │ │ │
|
||||
│ │ │ │
|
||||
│ │ │ │
|
||||
│ │ │ │
|
||||
│ │ │ │
|
||||
│ │ │ │
|
||||
│ │ │ │
|
||||
│ │ │ │
|
||||
│ │ │ │
|
||||
│ ╰─────────────────────────────────────────────────────────────────────────────────────────────────╯ │
|
||||
│ │
|
||||
│ │
|
||||
│ │
|
||||
|
||||
@@ -8,40 +8,40 @@ expression: output
|
||||
|
||||
|
||||
|
||||
╭ Overview ──────────────────────────────────────────────────────────────────────────────────────────────────────────────╮
|
||||
│It was shot in Madison, Wisconsin: a city the cast never really left. │
|
||||
│ │
|
||||
│The sequel took seven years to finish. │
|
||||
│ │
|
||||
│ │
|
||||
│ │
|
||||
│ │
|
||||
│ │
|
||||
│ │
|
||||
│ │
|
||||
│ │
|
||||
│ │
|
||||
│ │
|
||||
│ │
|
||||
│ │
|
||||
│ │
|
||||
│ │
|
||||
│ │
|
||||
│ │
|
||||
│ │
|
||||
│ │
|
||||
│ │
|
||||
│ │
|
||||
│ │
|
||||
│ │
|
||||
│ │
|
||||
│ │
|
||||
│ │
|
||||
│ │
|
||||
│ │
|
||||
│ │
|
||||
│ │
|
||||
│ │
|
||||
╭ Test Collection ───────────────────────────────────────────────────────────────────────────────────────────────────────╮
|
||||
│Overview: Collection blah blah blah │
|
||||
│Root Folder Path: /nfs/movies │
|
||||
│Quality Profile: HD - 1080p │
|
||||
│Minimum Avai╭ Overview ─────────────────────────────────────────────────────────────────────────────────────╮ │
|
||||
│Monitored: Y│It was shot in Madison, Wisconsin: a city the cast never really left. │ │
|
||||
│Search on Ad│ │ │
|
||||
│ │The sequel took seven years to finish. │ │
|
||||
│ │ │ │
|
||||
│ │ │ │
|
||||
│ Movies ──│ │───────────│
|
||||
│ ✔ Title │ │ │
|
||||
│=> ✔ Test │ │ │
|
||||
│ │ │ │
|
||||
│ │ │ │
|
||||
│ │ │ │
|
||||
│ │ │ │
|
||||
│ │ │ │
|
||||
│ │ │ │
|
||||
│ │ │ │
|
||||
│ │ │ │
|
||||
│ │ │ │
|
||||
│ │ │ │
|
||||
│ │ │ │
|
||||
│ │ │ │
|
||||
│ │ │ │
|
||||
│ │ │ │
|
||||
│ │ │ │
|
||||
│ │ │ │
|
||||
│ │ │ │
|
||||
│ │ │ │
|
||||
│ │ │ │
|
||||
│ │ │ │
|
||||
│ ╰─────────────────────────────────────────────────────────────────────────────────────────────────╯ │
|
||||
│ │
|
||||
│ │
|
||||
│ │
|
||||
|
||||