Compare commits

...

18 Commits

Author SHA1 Message Date
48ad17c6f1 style: Updated the contributing doc to also explain how to install commitizen 2024-11-06 16:48:27 -07:00
3cd15f34cd style: Test install for commitizen 2024-11-06 16:39:26 -07:00
53ca14e64d fix(handler): Fixed a bug in the movie details handler that would allow key events to be processed before the data was finished loading 2024-11-06 16:17:23 -07:00
0d8803d35d fix(ui): Fixed a bug that would freeze all user input while background network requests were running 2024-11-06 15:50:47 -07:00
8c90221a81 perf(network): Improved performance and reactiveness of the UI by speeding up network requests and clearing the channel whenever a request is cancelled/the UI is routing 2024-11-06 14:52:48 -07:00
a708f71d57 fix(radarr_ui): Fixed a race condition bug in the movie details UI that would panic if the user changes tabs too quickly 2024-11-06 11:29:49 -07:00
2a13f74a2b Updated the release workflows to use the correct flags and commands 2024-11-05 18:33:17 -07:00
2a97c49a8e Updated workflows to allow manual releases [skip ci] 2024-11-05 18:29:45 -07:00
8c155ce656 Updated release actions to only be executable by the repository owner [skip ci] 2024-11-05 18:25:26 -07:00
Alex Clarke
5245ba6d98 Merge pull request #14 from Dark-Alex-17/release-plz-2024-11-06T01-20-32Z
chore: release v0.2.1
2024-11-05 18:24:50 -07:00
github-actions[bot]
f9789ecc9b chore: release v0.2.1 2024-11-06 01:20:33 +00:00
9936ce1ab5 Removed the need for use_ssl to indicate SSL usage; instead just use the ssl_cert_path
Added the ability to specify either host/port, or uri for configuring Radarr
2024-11-05 18:16:01 -07:00
650c9783a6 Applied bug fix to the downloads tab as well as the context [skip ci] 2024-11-04 14:21:34 -07:00
b253a389eb Updated the README to not include the GitHub downloads badge since all binary releases are on crates.io [skip ci] 2024-11-04 10:12:45 -07:00
5023fbd3d1 Set all releases as manually triggered instead of automatic [skip ci] 2024-11-04 10:10:36 -07:00
fdb08fbd34 Added additional workflows for releasing minor and major releases, in addition to just patches so I can manually trigger them and update the Changelog dynamically. [skip ci] 2024-11-03 16:37:52 -07:00
b125d3341a Updated dockerfile to no longer use the --disable-terminal-size-checks flag [skip ci] 2024-11-03 16:20:32 -07:00
Alex Clarke
f73e3a4817 Merge pull request #11 from Dark-Alex-17/rc/v0.2.0-address-community-comments
Rc/v0.2.0 address community comments
2024-11-03 16:12:29 -07:00
26 changed files with 548 additions and 247 deletions
+7
View File
@@ -0,0 +1,7 @@
[tool.commitizen]
name = "cz_conventional_commits"
tag_format = "v$version"
version_scheme = "semver"
version_provider = "cargo"
update_changelog_on_bump = true
major_version_zero = true
@@ -1,16 +1,14 @@
# Adapted from https://github.com/joshka/github-workflows/blob/main/.github/workflows/rust-release-plz.yml # Adapted from https://github.com/joshka/github-workflows/blob/main/.github/workflows/rust-release-plz.yml
# Thanks to joshka for permission to use this template! # Thanks to joshka for permission to use this template!
name: Create Release PR and Publish Release name: Create major release
permissions: permissions:
pull-requests: write pull-requests: write
contents: write contents: write
on: on:
push: workflow_dispatch:
branches:
- main
jobs: jobs:
release-plz: release-plz:
@@ -19,6 +17,12 @@ jobs:
name: Release-plz name: Release-plz
runs-on: ubuntu-latest runs-on: ubuntu-latest
steps: steps:
- name: Check if actor is repository owner
if: ${{ github.actor != github.repository_owner }}
run: |
echo "You are not authorized to run this workflow."
exit 1
- name: Checkout - name: Checkout
uses: actions/checkout@v4 uses: actions/checkout@v4
with: with:
@@ -27,6 +31,8 @@ jobs:
uses: dtolnay/rust-toolchain@stable uses: dtolnay/rust-toolchain@stable
- name: Run release-plz - name: Run release-plz
uses: MarcoIeni/release-plz-action@v0.5 uses: MarcoIeni/release-plz-action@v0.5
with:
command: release --bump-major
env: env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
CARGO_REGISTRY_TOKEN: ${{ secrets.CARGO_REGISTRY_TOKEN }} CARGO_REGISTRY_TOKEN: ${{ secrets.CARGO_REGISTRY_TOKEN }}
+38
View File
@@ -0,0 +1,38 @@
# Adapted from https://github.com/joshka/github-workflows/blob/main/.github/workflows/rust-release-plz.yml
# Thanks to joshka for permission to use this template!
name: Create minor release
permissions:
pull-requests: write
contents: write
on:
workflow_dispatch:
jobs:
release-plz:
# see https://release-plz.ieni.dev/docs/github
# for more information
name: Release-plz
runs-on: ubuntu-latest
steps:
- name: Check if actor is repository owner
if: ${{ github.actor != github.repository_owner }}
run: |
echo "You are not authorized to run this workflow."
exit 1
- name: Checkout
uses: actions/checkout@v4
with:
fetch-depth: 0
- name: Install Rust stable
uses: dtolnay/rust-toolchain@stable
- name: Run release-plz
uses: MarcoIeni/release-plz-action@v0.5
with:
command: release --bump-minor
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
CARGO_REGISTRY_TOKEN: ${{ secrets.CARGO_REGISTRY_TOKEN }}
+38
View File
@@ -0,0 +1,38 @@
# Adapted from https://github.com/joshka/github-workflows/blob/main/.github/workflows/rust-release-plz.yml
# Thanks to joshka for permission to use this template!
name: Create patch release
permissions:
pull-requests: write
contents: write
on:
workflow_dispatch:
jobs:
release-plz:
# see https://release-plz.ieni.dev/docs/github
# for more information
name: Release-plz
runs-on: ubuntu-latest
steps:
- name: Check if actor is repository owner
if: ${{ github.actor != github.repository_owner }}
run: |
echo "You are not authorized to run this workflow."
exit 1
- name: Checkout
uses: actions/checkout@v4
with:
fetch-depth: 0
- name: Install Rust stable
uses: dtolnay/rust-toolchain@stable
- name: Run release-plz
uses: MarcoIeni/release-plz-action@v0.5
with:
command: release --bump-patch
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
CARGO_REGISTRY_TOKEN: ${{ secrets.CARGO_REGISTRY_TOKEN }}
+8
View File
@@ -0,0 +1,8 @@
repos:
- hooks:
- id: commitizen
- id: commitizen-branch
stages:
- pre-push
repo: https://github.com/commitizen-tools/commitizen
rev: v3.30.0
+10
View File
@@ -7,6 +7,16 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
## [Unreleased] ## [Unreleased]
## [0.2.1](https://github.com/Dark-Alex-17/managarr/compare/v0.2.0...v0.2.1) - 2024-11-06
### Other
- Removed the need for use_ssl to indicate SSL usage; instead just use the ssl_cert_path
- Applied bug fix to the downloads tab as well as the context [skip ci]
- Updated the README to not include the GitHub downloads badge since all binary releases are on crates.io [skip ci]
- Set all releases as manually triggered instead of automatic [skip ci]
- Updated dockerfile to no longer use the --disable-terminal-size-checks flag [skip ci]
## [0.1.5](https://github.com/Dark-Alex-17/managarr/compare/v0.1.4...v0.1.5) - 2024-11-03 ## [0.1.5](https://github.com/Dark-Alex-17/managarr/compare/v0.1.4...v0.1.5) - 2024-11-03
### Other ### Other
+32
View File
@@ -1,6 +1,7 @@
# Contributing # Contributing
Contributors are very welcome! **No contribution is too small and all contributions are valued.** Contributors are very welcome! **No contribution is too small and all contributions are valued.**
## Rust
You'll need to have the stable Rust toolchain installed in order to develop Managarr. You'll need to have the stable Rust toolchain installed in order to develop Managarr.
The Rust toolchain (stable) can be installed via rustup using the following command: The Rust toolchain (stable) can be installed via rustup using the following command:
@@ -11,6 +12,37 @@ curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh
This will install `rustup`, `rustc` and `cargo`. For more information, refer to the [official Rust installation documentation](https://www.rust-lang.org/tools/install). This will install `rustup`, `rustc` and `cargo`. For more information, refer to the [official Rust installation documentation](https://www.rust-lang.org/tools/install).
## Commitizen
[Commitizen](https://github.com/commitizen-tools/commitizen?tab=readme-ov-file) is a nifty tool that helps us write better commit messages. It ensures that our
commits have a consistent style and makes it easier to generate CHANGELOGS. Additionally,
Commitizen is used to run pre-commit checks to enforce style constraints.
To install `commitizen` and the `pre-commit` prerequisite, run the following command:
```shell
python3 -m pip install commitizen pre-commit
```
### Commitizen Quick Guide
To see an example commit to get an idea for the Commitizen style, run:
```shell
cz example
```
To see the allowed types of commits and their descriptions, run:
```shell
cz info
```
If you'd like to create a commit using Commitizen with an interactive prompt to help you get
comfortable with the style, use:
```shell
cz commit
```
## Setup workspace ## Setup workspace
1. Clone this repo 1. Clone this repo
Generated
+1 -1
View File
@@ -1148,7 +1148,7 @@ dependencies = [
[[package]] [[package]]
name = "managarr" name = "managarr"
version = "0.2.0" version = "0.2.1"
dependencies = [ dependencies = [
"anyhow", "anyhow",
"assert_cmd", "assert_cmd",
+1 -1
View File
@@ -1,6 +1,6 @@
[package] [package]
name = "managarr" name = "managarr"
version = "0.2.0" version = "0.2.1"
authors = ["Alex Clarke <alex.j.tusa@gmail.com>"] authors = ["Alex Clarke <alex.j.tusa@gmail.com>"]
description = "A TUI and CLI to manage your Servarrs" description = "A TUI and CLI to manage your Servarrs"
keywords = ["managarr", "ratatui", "dashboard", "servarr", "tui"] keywords = ["managarr", "ratatui", "dashboard", "servarr", "tui"]
+1 -1
View File
@@ -23,4 +23,4 @@ FROM debian:stable-slim
# Copy the compiled binary from the builder container # Copy the compiled binary from the builder container
COPY --from=builder --chown=nonroot:nonroot /usr/src/managarr-temp/managarr /usr/local/bin COPY --from=builder --chown=nonroot:nonroot /usr/src/managarr-temp/managarr /usr/local/bin
ENTRYPOINT [ "/usr/local/bin/managarr", "--disable-terminal-size-checks" ] ENTRYPOINT [ "/usr/local/bin/managarr" ]
+11 -20
View File
@@ -2,13 +2,11 @@
![check](https://github.com/Dark-Alex-17/managarr/actions/workflows/check.yml/badge.svg) ![check](https://github.com/Dark-Alex-17/managarr/actions/workflows/check.yml/badge.svg)
![test](https://github.com/Dark-Alex-17/managarr/actions/workflows/test.yml/badge.svg) ![test](https://github.com/Dark-Alex-17/managarr/actions/workflows/test.yml/badge.svg)
![test](https://github.com/Dark-Alex-17/managarr/actions/workflows/release.yml/badge.svg)
![License](https://img.shields.io/badge/license-MIT-blueviolet.svg) ![License](https://img.shields.io/badge/license-MIT-blueviolet.svg)
![LOC](https://tokei.rs/b1/github/Dark-Alex-17/managarr?category=code) ![LOC](https://tokei.rs/b1/github/Dark-Alex-17/managarr?category=code)
[![crates.io link](https://img.shields.io/crates/v/managarr.svg)](https://crates.io/crates/managarr) [![crates.io link](https://img.shields.io/crates/v/managarr.svg)](https://crates.io/crates/managarr)
![Release](https://img.shields.io/github/v/release/Dark-Alex-17/managarr?color=%23c694ff) ![Release](https://img.shields.io/github/v/release/Dark-Alex-17/managarr?color=%23c694ff)
[![codecov](https://codecov.io/gh/Dark-Alex-17/managarr/graph/badge.svg?token=33G179TW67)](https://codecov.io/gh/Dark-Alex-17/managarr) [![codecov](https://codecov.io/gh/Dark-Alex-17/managarr/graph/badge.svg?token=33G179TW67)](https://codecov.io/gh/Dark-Alex-17/managarr)
[![GitHub Downloads](https://img.shields.io/github/downloads/Dark-Alex-17/managarr/total.svg?label=GitHub%20downloads)](https://github.com/Dark-Alex-17/managarr/releases)
![Crate.io downloads](https://img.shields.io/crates/d/managarr?label=Crate%20downloads) ![Crate.io downloads](https://img.shields.io/crates/d/managarr?label=Crate%20downloads)
Managarr is a TUI and CLI to help you manage your HTPC (Home Theater PC). Built with 🤎 in Rust! Managarr is a TUI and CLI to help you manage your HTPC (Home Theater PC). Built with 🤎 in Rust!
@@ -113,7 +111,7 @@ To see all available commands, simply run `managarr --help`:
```shell ```shell
$ managarr --help $ managarr --help
managarr 0.1.5 managarr 0.2.1
Alex Clarke <alex.j.tusa@gmail.com> Alex Clarke <alex.j.tusa@gmail.com>
A TUI and CLI to manage your Servarrs A TUI and CLI to manage your Servarrs
@@ -201,45 +199,38 @@ managarr --config /path/to/config.yml
### Example Configuration: ### Example Configuration:
```yaml ```yaml
radarr: radarr:
host: 127.0.0.1 host: 192.168.0.78
port: 7878 port: 7878
api_token: someApiToken1234567890 api_token: someApiToken1234567890
use_ssl: true ssl_cert_path: /path/to/radarr.crt # Required to enable SSL
ssl_cert_path: /path/to/radarr.crt
sonarr: sonarr:
host: 127.0.0.1 uri: http://htpc.local/sonarr # Example of using the 'uri' key instead of 'host' and 'port'
port: 8989
api_token: someApiToken1234567890 api_token: someApiToken1234567890
readarr: readarr:
host: 127.0.0.1 host: 192.168.0.87
port: 8787 port: 8787
api_token: someApiToken1234567890 api_token: someApiToken1234567890
use_ssl: false
lidarr: lidarr:
host: 127.0.0.1 host: 192.168.0.86
port: 8686 port: 8686
api_token: someApiToken1234567890 api_token: someApiToken1234567890
use_ssl: false
whisparr: whisparr:
host: 127.0.0.1 host: 192.168.0.69
port: 6969 port: 6969
api_token: someApiToken1234567890 api_token: someApiToken1234567890
use_ssl: false ssl_cert_path: /path/to/whisparr.crt
bazarr: bazarr:
host: 127.0.0.1 host: 192.168.0.67
port: 6767 port: 6767
api_token: someApiToken1234567890 api_token: someApiToken1234567890
use_ssl: false
prowlarr: prowlarr:
host: 127.0.0.1 host: 192.168.0.96
port: 9696 port: 9696
api_token: someApiToken1234567890 api_token: someApiToken1234567890
use_ssl: false
tautulli: tautulli:
host: 127.0.0.1 host: 192.168.0.81
port: 8181 port: 8181
api_token: someApiToken1234567890 api_token: someApiToken1234567890
use_ssl: false
``` ```
## Environment Variables ## Environment Variables
+6
View File
@@ -0,0 +1,6 @@
{
"name": "managarr",
"lockfileVersion": 3,
"requires": true,
"packages": {}
}
+1
View File
@@ -0,0 +1 @@
{}
+38 -8
View File
@@ -1,7 +1,7 @@
#[cfg(test)] #[cfg(test)]
mod tests { mod tests {
use anyhow::anyhow; use anyhow::anyhow;
use pretty_assertions::{assert_eq, assert_str_eq}; use pretty_assertions::assert_eq;
use tokio::sync::mpsc; use tokio::sync::mpsc;
use crate::app::context_clues::{build_context_clue_string, SERVARR_CONTEXT_CLUES}; use crate::app::context_clues::{build_context_clue_string, SERVARR_CONTEXT_CLUES};
@@ -87,7 +87,11 @@ mod tests {
#[test] #[test]
fn test_reset_cancellation_token() { fn test_reset_cancellation_token() {
let mut app = App::default(); let mut app = App {
is_loading: true,
should_refresh: false,
..App::default()
};
app.cancellation_token.cancel(); app.cancellation_token.cancel();
assert!(app.cancellation_token.is_cancelled()); assert!(app.cancellation_token.is_cancelled());
@@ -96,6 +100,8 @@ mod tests {
assert!(!app.cancellation_token.is_cancelled()); assert!(!app.cancellation_token.is_cancelled());
assert!(!new_token.is_cancelled()); assert!(!new_token.is_cancelled());
assert!(!app.is_loading);
assert!(app.should_refresh);
} }
#[test] #[test]
@@ -145,6 +151,29 @@ mod tests {
assert_eq!(app.error.text, test_string); assert_eq!(app.error.text, test_string);
} }
#[tokio::test]
async fn test_dispatch_network_event() {
let (sync_network_tx, mut sync_network_rx) = mpsc::channel::<NetworkEvent>(500);
let mut app = App {
tick_until_poll: 2,
network_tx: Some(sync_network_tx),
..App::default()
};
assert_eq!(app.tick_count, 0);
app
.dispatch_network_event(RadarrEvent::GetStatus.into())
.await;
assert_eq!(
sync_network_rx.recv().await.unwrap(),
RadarrEvent::GetStatus.into()
);
assert_eq!(app.tick_count, 0);
}
#[tokio::test] #[tokio::test]
async fn test_on_tick_first_render() { async fn test_on_tick_first_render() {
let (sync_network_tx, mut sync_network_rx) = mpsc::channel::<NetworkEvent>(500); let (sync_network_tx, mut sync_network_rx) = mpsc::channel::<NetworkEvent>(500);
@@ -158,6 +187,7 @@ mod tests {
assert_eq!(app.tick_count, 0); assert_eq!(app.tick_count, 0);
app.on_tick(true).await; app.on_tick(true).await;
assert_eq!( assert_eq!(
sync_network_rx.recv().await.unwrap(), sync_network_rx.recv().await.unwrap(),
RadarrEvent::GetQualityProfiles.into() RadarrEvent::GetQualityProfiles.into()
@@ -170,6 +200,10 @@ mod tests {
sync_network_rx.recv().await.unwrap(), sync_network_rx.recv().await.unwrap(),
RadarrEvent::GetRootFolders.into() RadarrEvent::GetRootFolders.into()
); );
assert_eq!(
sync_network_rx.recv().await.unwrap(),
RadarrEvent::GetDownloads.into()
);
assert_eq!( assert_eq!(
sync_network_rx.recv().await.unwrap(), sync_network_rx.recv().await.unwrap(),
RadarrEvent::GetOverview.into() RadarrEvent::GetOverview.into()
@@ -182,10 +216,6 @@ mod tests {
sync_network_rx.recv().await.unwrap(), sync_network_rx.recv().await.unwrap(),
RadarrEvent::GetMovies.into() RadarrEvent::GetMovies.into()
); );
assert_eq!(
sync_network_rx.recv().await.unwrap(),
RadarrEvent::GetDownloads.into()
);
assert!(!app.is_routing); assert!(!app.is_routing);
assert!(!app.should_refresh); assert!(!app.should_refresh);
assert_eq!(app.tick_count, 1); assert_eq!(app.tick_count, 1);
@@ -221,10 +251,10 @@ mod tests {
fn test_radarr_config_default() { fn test_radarr_config_default() {
let radarr_config = RadarrConfig::default(); let radarr_config = RadarrConfig::default();
assert_str_eq!(radarr_config.host, "localhost"); assert_eq!(radarr_config.host, Some("localhost".to_string()));
assert_eq!(radarr_config.port, Some(7878)); assert_eq!(radarr_config.port, Some(7878));
assert_eq!(radarr_config.uri, None);
assert!(radarr_config.api_token.is_empty()); assert!(radarr_config.api_token.is_empty());
assert!(!radarr_config.use_ssl);
assert_eq!(radarr_config.ssl_cert_path, None); assert_eq!(radarr_config.ssl_cert_path, None);
} }
} }
+36 -5
View File
@@ -1,4 +1,7 @@
use std::process;
use anyhow::anyhow; use anyhow::anyhow;
use colored::Colorize;
use log::{debug, error}; use log::{debug, error};
use serde::{Deserialize, Serialize}; use serde::{Deserialize, Serialize};
use tokio::sync::mpsc::Sender; use tokio::sync::mpsc::Sender;
@@ -53,7 +56,10 @@ impl<'a> App<'a> {
pub async fn dispatch_network_event(&mut self, action: NetworkEvent) { pub async fn dispatch_network_event(&mut self, action: NetworkEvent) {
debug!("Dispatching network event: {action:?}"); debug!("Dispatching network event: {action:?}");
if !self.should_refresh {
self.is_loading = true; self.is_loading = true;
}
if let Some(network_tx) = &self.network_tx { if let Some(network_tx) = &self.network_tx {
if let Err(e) = network_tx.send(action).await { if let Err(e) = network_tx.send(action).await {
self.is_loading = false; self.is_loading = false;
@@ -110,6 +116,8 @@ impl<'a> App<'a> {
pub fn reset_cancellation_token(&mut self) -> CancellationToken { pub fn reset_cancellation_token(&mut self) -> CancellationToken {
self.cancellation_token = CancellationToken::new(); self.cancellation_token = CancellationToken::new();
self.should_refresh = true;
self.is_loading = false;
self.cancellation_token.clone() self.cancellation_token.clone()
} }
@@ -166,29 +174,52 @@ pub struct Data<'a> {
pub radarr_data: RadarrData<'a>, pub radarr_data: RadarrData<'a>,
} }
pub trait ServarrConfig {
fn validate(&self);
}
#[derive(Debug, Deserialize, Serialize, Default)] #[derive(Debug, Deserialize, Serialize, Default)]
pub struct AppConfig { pub struct AppConfig {
pub radarr: RadarrConfig, pub radarr: RadarrConfig,
} }
impl ServarrConfig for AppConfig {
fn validate(&self) {
self.radarr.validate();
}
}
#[derive(Debug, Deserialize, Serialize)] #[derive(Debug, Deserialize, Serialize)]
pub struct RadarrConfig { pub struct RadarrConfig {
pub host: String, pub host: Option<String>,
pub port: Option<u16>, pub port: Option<u16>,
pub uri: Option<String>,
pub api_token: String, pub api_token: String,
#[serde(default)]
pub use_ssl: bool,
pub ssl_cert_path: Option<String>, pub ssl_cert_path: Option<String>,
} }
impl ServarrConfig for RadarrConfig {
fn validate(&self) {
if self.host.is_none() && self.uri.is_none() {
log_and_print_error("'host' or 'uri' is required for Radarr configuration".to_owned());
process::exit(1);
}
}
}
impl Default for RadarrConfig { impl Default for RadarrConfig {
fn default() -> Self { fn default() -> Self {
RadarrConfig { RadarrConfig {
host: "localhost".to_string(), host: Some("localhost".to_string()),
port: Some(7878), port: Some(7878),
uri: None,
api_token: "".to_string(), api_token: "".to_string(),
use_ssl: false,
ssl_cert_path: None, ssl_cert_path: None,
} }
} }
} }
pub fn log_and_print_error(error: String) {
error!("{}", error);
eprintln!("error: {}", error.red());
}
+11 -18
View File
@@ -142,36 +142,23 @@ impl<'a> App<'a> {
is_first_render: bool, is_first_render: bool,
) { ) {
if is_first_render { if is_first_render {
self self.refresh_metadata().await;
.dispatch_network_event(RadarrEvent::GetQualityProfiles.into())
.await;
self
.dispatch_network_event(RadarrEvent::GetTags.into())
.await;
self
.dispatch_network_event(RadarrEvent::GetRootFolders.into())
.await;
self
.dispatch_network_event(RadarrEvent::GetOverview.into())
.await;
self
.dispatch_network_event(RadarrEvent::GetStatus.into())
.await;
self.dispatch_by_radarr_block(&active_radarr_block).await; self.dispatch_by_radarr_block(&active_radarr_block).await;
} }
if self.should_refresh { if self.should_refresh {
self.dispatch_by_radarr_block(&active_radarr_block).await; self.dispatch_by_radarr_block(&active_radarr_block).await;
self.refresh_metadata().await;
} }
if self.is_routing { if self.is_routing {
if self.is_loading && !self.should_refresh { if !self.should_refresh {
self.cancellation_token.cancel(); self.cancellation_token.cancel();
} } else {
self.dispatch_by_radarr_block(&active_radarr_block).await; self.dispatch_by_radarr_block(&active_radarr_block).await;
self.refresh_metadata().await; self.refresh_metadata().await;
} }
}
if self.tick_count % self.tick_until_poll == 0 { if self.tick_count % self.tick_until_poll == 0 {
self.refresh_metadata().await; self.refresh_metadata().await;
@@ -191,6 +178,12 @@ impl<'a> App<'a> {
self self
.dispatch_network_event(RadarrEvent::GetDownloads.into()) .dispatch_network_event(RadarrEvent::GetDownloads.into())
.await; .await;
self
.dispatch_network_event(RadarrEvent::GetOverview.into())
.await;
self
.dispatch_network_event(RadarrEvent::GetStatus.into())
.await;
} }
async fn populate_movie_collection_table(&mut self) { async fn populate_movie_collection_table(&mut self) {
+14 -30
View File
@@ -508,6 +508,14 @@ mod tests {
sync_network_rx.recv().await.unwrap(), sync_network_rx.recv().await.unwrap(),
RadarrEvent::GetDownloads.into() RadarrEvent::GetDownloads.into()
); );
assert_eq!(
sync_network_rx.recv().await.unwrap(),
RadarrEvent::GetOverview.into()
);
assert_eq!(
sync_network_rx.recv().await.unwrap(),
RadarrEvent::GetStatus.into()
);
assert!(app.is_loading); assert!(app.is_loading);
} }
@@ -529,6 +537,10 @@ mod tests {
sync_network_rx.recv().await.unwrap(), sync_network_rx.recv().await.unwrap(),
RadarrEvent::GetRootFolders.into() RadarrEvent::GetRootFolders.into()
); );
assert_eq!(
sync_network_rx.recv().await.unwrap(),
RadarrEvent::GetDownloads.into()
);
assert_eq!( assert_eq!(
sync_network_rx.recv().await.unwrap(), sync_network_rx.recv().await.unwrap(),
RadarrEvent::GetOverview.into() RadarrEvent::GetOverview.into()
@@ -537,10 +549,6 @@ mod tests {
sync_network_rx.recv().await.unwrap(), sync_network_rx.recv().await.unwrap(),
RadarrEvent::GetStatus.into() RadarrEvent::GetStatus.into()
); );
assert_eq!(
sync_network_rx.recv().await.unwrap(),
RadarrEvent::GetDownloads.into()
);
assert!(app.is_loading); assert!(app.is_loading);
assert!(!app.data.radarr_data.prompt_confirm); assert!(!app.data.radarr_data.prompt_confirm);
} }
@@ -549,6 +557,7 @@ mod tests {
async fn test_radarr_on_tick_routing() { async fn test_radarr_on_tick_routing() {
let (mut app, mut sync_network_rx) = construct_app_unit(); let (mut app, mut sync_network_rx) = construct_app_unit();
app.is_routing = true; app.is_routing = true;
app.should_refresh = true;
app app
.radarr_on_tick(ActiveRadarrBlock::Downloads, false) .radarr_on_tick(ActiveRadarrBlock::Downloads, false)
@@ -574,43 +583,19 @@ mod tests {
sync_network_rx.recv().await.unwrap(), sync_network_rx.recv().await.unwrap(),
RadarrEvent::GetDownloads.into() RadarrEvent::GetDownloads.into()
); );
assert!(app.is_loading);
assert!(!app.data.radarr_data.prompt_confirm); assert!(!app.data.radarr_data.prompt_confirm);
} }
#[tokio::test] #[tokio::test]
async fn test_radarr_on_tick_routing_while_long_request_is_running_should_cancel_request() { async fn test_radarr_on_tick_routing_while_long_request_is_running_should_cancel_request() {
let (mut app, mut sync_network_rx) = construct_app_unit(); let (mut app, _) = construct_app_unit();
app.is_routing = true; app.is_routing = true;
app.is_loading = true;
app.should_refresh = false; app.should_refresh = false;
app app
.radarr_on_tick(ActiveRadarrBlock::Downloads, false) .radarr_on_tick(ActiveRadarrBlock::Downloads, false)
.await; .await;
assert_eq!(
sync_network_rx.recv().await.unwrap(),
RadarrEvent::GetDownloads.into()
);
assert_eq!(
sync_network_rx.recv().await.unwrap(),
RadarrEvent::GetQualityProfiles.into()
);
assert_eq!(
sync_network_rx.recv().await.unwrap(),
RadarrEvent::GetTags.into()
);
assert_eq!(
sync_network_rx.recv().await.unwrap(),
RadarrEvent::GetRootFolders.into()
);
assert_eq!(
sync_network_rx.recv().await.unwrap(),
RadarrEvent::GetDownloads.into()
);
assert!(app.is_loading);
assert!(!app.data.radarr_data.prompt_confirm);
assert!(app.cancellation_token.is_cancelled()); assert!(app.cancellation_token.is_cancelled());
} }
@@ -627,7 +612,6 @@ mod tests {
sync_network_rx.recv().await.unwrap(), sync_network_rx.recv().await.unwrap(),
RadarrEvent::GetDownloads.into() RadarrEvent::GetDownloads.into()
); );
assert!(app.is_loading);
assert!(app.should_refresh); assert!(app.should_refresh);
assert!(!app.data.radarr_data.prompt_confirm); assert!(!app.data.radarr_data.prompt_confirm);
} }
@@ -47,18 +47,28 @@ impl<'a, 'b> KeyEventHandler<'a, 'b, ActiveRadarrBlock> for MovieDetailsHandler<
} }
fn is_ready(&self) -> bool { fn is_ready(&self) -> bool {
let movie_details_modal_is_ready =
if let Some(movie_details_modal) = &self.app.data.radarr_data.movie_details_modal { if let Some(movie_details_modal) = &self.app.data.radarr_data.movie_details_modal {
!movie_details_modal.movie_details.is_empty() match self.active_radarr_block {
|| !movie_details_modal.movie_history.is_empty() ActiveRadarrBlock::MovieDetails => {
|| !movie_details_modal.movie_cast.is_empty() !self.app.is_loading && !movie_details_modal.movie_details.is_empty()
|| !movie_details_modal.movie_crew.is_empty() }
|| !movie_details_modal.movie_releases.is_empty() ActiveRadarrBlock::MovieHistory => {
!self.app.is_loading && !movie_details_modal.movie_history.is_empty()
}
ActiveRadarrBlock::Cast => {
!self.app.is_loading && !movie_details_modal.movie_cast.is_empty()
}
ActiveRadarrBlock::Crew => {
!self.app.is_loading && !movie_details_modal.movie_crew.is_empty()
}
ActiveRadarrBlock::ManualSearch => {
!self.app.is_loading && !movie_details_modal.movie_releases.is_empty()
}
_ => !self.app.is_loading,
}
} else { } else {
false false
}; }
!self.app.is_loading && movie_details_modal_is_ready
} }
fn handle_scroll_up(&mut self) { fn handle_scroll_up(&mut self) {
@@ -3,6 +3,7 @@ mod tests {
use std::cmp::Ordering; use std::cmp::Ordering;
use pretty_assertions::assert_str_eq; use pretty_assertions::assert_str_eq;
use rstest::rstest;
use serde_json::Number; use serde_json::Number;
use strum::IntoEnumIterator; use strum::IntoEnumIterator;
@@ -1245,10 +1246,12 @@ mod tests {
#[test] #[test]
fn test_manual_search_submit() { fn test_manual_search_submit() {
let mut app = App::default(); let mut app = App::default();
app.data.radarr_data.movie_details_modal = Some(MovieDetailsModal { let mut modal = MovieDetailsModal {
movie_details: ScrollableText::with_string("test".to_owned()), movie_details: ScrollableText::with_string("test".to_owned()),
..MovieDetailsModal::default() ..MovieDetailsModal::default()
}); };
modal.movie_releases.set_items(vec![Release::default()]);
app.data.radarr_data.movie_details_modal = Some(modal);
app.push_navigation_stack(ActiveRadarrBlock::ManualSearch.into()); app.push_navigation_stack(ActiveRadarrBlock::ManualSearch.into());
MovieDetailsHandler::with( MovieDetailsHandler::with(
@@ -1486,10 +1489,17 @@ mod tests {
active_radarr_block: ActiveRadarrBlock, active_radarr_block: ActiveRadarrBlock,
) { ) {
let mut app = App::default(); let mut app = App::default();
app.data.radarr_data.movie_details_modal = Some(MovieDetailsModal { let mut modal = MovieDetailsModal {
movie_details: ScrollableText::with_string("test".to_owned()), movie_details: ScrollableText::with_string("Test".to_owned()),
..MovieDetailsModal::default() ..MovieDetailsModal::default()
}); };
modal
.movie_history
.set_items(vec![MovieHistoryItem::default()]);
modal.movie_cast.set_items(vec![Credit::default()]);
modal.movie_crew.set_items(vec![Credit::default()]);
modal.movie_releases.set_items(vec![Release::default()]);
app.data.radarr_data.movie_details_modal = Some(modal);
MovieDetailsHandler::with( MovieDetailsHandler::with(
&DEFAULT_KEYBINDINGS.search.key, &DEFAULT_KEYBINDINGS.search.key,
@@ -1539,10 +1549,9 @@ mod tests {
#[test] #[test]
fn test_sort_key() { fn test_sort_key() {
let mut app = App::default(); let mut app = App::default();
app.data.radarr_data.movie_details_modal = Some(MovieDetailsModal { let mut modal = MovieDetailsModal::default();
movie_details: ScrollableText::with_string("test".to_owned()), modal.movie_releases.set_items(release_vec());
..MovieDetailsModal::default() app.data.radarr_data.movie_details_modal = Some(modal);
});
MovieDetailsHandler::with( MovieDetailsHandler::with(
&DEFAULT_KEYBINDINGS.sort.key, &DEFAULT_KEYBINDINGS.sort.key,
@@ -1670,10 +1679,17 @@ mod tests {
active_radarr_block: ActiveRadarrBlock, active_radarr_block: ActiveRadarrBlock,
) { ) {
let mut app = App::default(); let mut app = App::default();
app.data.radarr_data.movie_details_modal = Some(MovieDetailsModal { let mut modal = MovieDetailsModal {
movie_details: ScrollableText::with_string("test".to_owned()), movie_details: ScrollableText::with_string("Test".to_owned()),
..MovieDetailsModal::default() ..MovieDetailsModal::default()
}); };
modal
.movie_history
.set_items(vec![MovieHistoryItem::default()]);
modal.movie_cast.set_items(vec![Credit::default()]);
modal.movie_crew.set_items(vec![Credit::default()]);
modal.movie_releases.set_items(vec![Release::default()]);
app.data.radarr_data.movie_details_modal = Some(modal);
MovieDetailsHandler::with( MovieDetailsHandler::with(
&DEFAULT_KEYBINDINGS.update.key, &DEFAULT_KEYBINDINGS.update.key,
@@ -1733,10 +1749,17 @@ mod tests {
active_radarr_block: ActiveRadarrBlock, active_radarr_block: ActiveRadarrBlock,
) { ) {
let mut app = App::default(); let mut app = App::default();
app.data.radarr_data.movie_details_modal = Some(MovieDetailsModal { let mut modal = MovieDetailsModal {
movie_details: ScrollableText::with_string("test".to_owned()), movie_details: ScrollableText::with_string("Test".to_owned()),
..MovieDetailsModal::default() ..MovieDetailsModal::default()
}); };
modal
.movie_history
.set_items(vec![MovieHistoryItem::default()]);
modal.movie_cast.set_items(vec![Credit::default()]);
modal.movie_crew.set_items(vec![Credit::default()]);
modal.movie_releases.set_items(vec![Release::default()]);
app.data.radarr_data.movie_details_modal = Some(modal);
MovieDetailsHandler::with( MovieDetailsHandler::with(
&DEFAULT_KEYBINDINGS.refresh.key, &DEFAULT_KEYBINDINGS.refresh.key,
@@ -1994,15 +2017,37 @@ mod tests {
}); });
} }
#[test] #[rstest]
fn test_movie_details_handler_is_not_ready_when_loading() { fn test_movie_details_handler_is_not_ready_when_loading(
#[values(
ActiveRadarrBlock::MovieDetails,
ActiveRadarrBlock::MovieHistory,
ActiveRadarrBlock::FileInfo,
ActiveRadarrBlock::Cast,
ActiveRadarrBlock::Crew,
ActiveRadarrBlock::ManualSearch,
ActiveRadarrBlock::ManualSearch
)]
movie_details_block: ActiveRadarrBlock,
) {
let mut app = App::default(); let mut app = App::default();
app.is_loading = true; app.is_loading = true;
let mut modal = MovieDetailsModal {
movie_details: ScrollableText::with_string("Test".to_owned()),
..MovieDetailsModal::default()
};
modal
.movie_history
.set_items(vec![MovieHistoryItem::default()]);
modal.movie_cast.set_items(vec![Credit::default()]);
modal.movie_crew.set_items(vec![Credit::default()]);
modal.movie_releases.set_items(vec![Release::default()]);
app.data.radarr_data.movie_details_modal = Some(modal);
let handler = MovieDetailsHandler::with( let handler = MovieDetailsHandler::with(
&DEFAULT_KEYBINDINGS.esc.key, &DEFAULT_KEYBINDINGS.esc.key,
&mut app, &mut app,
&ActiveRadarrBlock::MovieDetails, &movie_details_block,
&None, &None,
); );
+35 -25
View File
@@ -6,11 +6,12 @@ use std::panic::PanicHookInfo;
use std::path::PathBuf; use std::path::PathBuf;
use std::sync::atomic::{AtomicBool, Ordering}; use std::sync::atomic::{AtomicBool, Ordering};
use std::sync::Arc; use std::sync::Arc;
use std::time::Duration;
use std::{io, panic, process}; use std::{io, panic, process};
use anyhow::anyhow; use anyhow::anyhow;
use anyhow::Result; use anyhow::Result;
use app::AppConfig; use app::{log_and_print_error, AppConfig, ServarrConfig};
use clap::{ use clap::{
command, crate_authors, crate_description, crate_name, crate_version, CommandFactory, Parser, command, crate_authors, crate_description, crate_name, crate_version, CommandFactory, Parser,
}; };
@@ -20,11 +21,12 @@ use crossterm::execute;
use crossterm::terminal::{ use crossterm::terminal::{
disable_raw_mode, enable_raw_mode, EnterAlternateScreen, LeaveAlternateScreen, disable_raw_mode, enable_raw_mode, EnterAlternateScreen, LeaveAlternateScreen,
}; };
use log::error; use log::{error, warn};
use network::NetworkTrait; use network::NetworkTrait;
use ratatui::backend::CrosstermBackend; use ratatui::backend::CrosstermBackend;
use ratatui::Terminal; use ratatui::Terminal;
use reqwest::{Certificate, Client}; use reqwest::{Certificate, Client};
use tokio::select;
use tokio::sync::mpsc::Receiver; use tokio::sync::mpsc::Receiver;
use tokio::sync::{mpsc, Mutex}; use tokio::sync::{mpsc, Mutex};
use tokio_util::sync::CancellationToken; use tokio_util::sync::CancellationToken;
@@ -88,6 +90,7 @@ async fn main() -> Result<()> {
} else { } else {
confy::load("managarr", "config")? confy::load("managarr", "config")?
}; };
config.validate();
let reqwest_client = build_network_client(&config); let reqwest_client = build_network_client(&config);
let (sync_network_tx, sync_network_rx) = mpsc::channel(500); let (sync_network_tx, sync_network_rx) = mpsc::channel(500);
let cancellation_token = CancellationToken::new(); let cancellation_token = CancellationToken::new();
@@ -143,11 +146,22 @@ async fn start_networking(
) { ) {
let mut network = Network::new(app, cancellation_token, client); let mut network = Network::new(app, cancellation_token, client);
while let Some(network_event) = network_rx.recv().await { loop {
select! {
Some(network_event) = network_rx.recv() => {
if let Err(e) = network.handle_network_event(network_event).await { if let Err(e) = network.handle_network_event(network_event).await {
error!("Encountered an error handling network event: {e:?}"); error!("Encountered an error handling network event: {e:?}");
} }
} }
_ = network.cancellation_token.cancelled() => {
warn!("Clearing network channel");
while network_rx.try_recv().is_ok() {
// Discard the message
}
network.reset_cancellation_token().await;
}
}
}
} }
async fn start_ui(app: &Arc<Mutex<App<'_>>>) -> Result<()> { async fn start_ui(app: &Arc<Mutex<App<'_>>>) -> Result<()> {
@@ -227,10 +241,13 @@ fn load_config(path: &str) -> Result<AppConfig> {
} }
fn build_network_client(config: &AppConfig) -> Client { fn build_network_client(config: &AppConfig) -> Client {
let mut client_builder = Client::builder(); let mut client_builder = Client::builder()
.pool_max_idle_per_host(10)
.http2_keep_alive_interval(Duration::from_secs(5))
.tcp_keepalive(Duration::from_secs(5));
if config.radarr.use_ssl { if let Some(ref cert_path) = config.radarr.ssl_cert_path {
let cert = create_cert(config.radarr.ssl_cert_path.clone(), "Radarr"); let cert = create_cert(cert_path, "Radarr");
client_builder = client_builder.add_root_certificate(cert); client_builder = client_builder.add_root_certificate(cert);
} }
@@ -244,32 +261,25 @@ fn build_network_client(config: &AppConfig) -> Client {
} }
} }
fn create_cert(cert_path: Option<String>, servarr_name: &str) -> Certificate { fn create_cert(cert_path: &String, servarr_name: &str) -> Certificate {
let err = |error: String| { match fs::read(cert_path) {
error!("{}", error);
eprintln!("error: {}", error.red());
process::exit(1);
};
if cert_path.is_none() {
err(format!(
"A {} cert path is required when 'use_ssl' is 'true'",
servarr_name
));
}
match fs::read(cert_path.unwrap()) {
Ok(cert) => match Certificate::from_pem(&cert) { Ok(cert) => match Certificate::from_pem(&cert) {
Ok(certificate) => certificate, Ok(certificate) => certificate,
Err(_) => err(format!( Err(_) => {
log_and_print_error(format!(
"Unable to read the specified {} SSL certificate", "Unable to read the specified {} SSL certificate",
servarr_name servarr_name
)), ));
process::exit(1);
}
}, },
Err(_) => err(format!( Err(_) => {
log_and_print_error(format!(
"Unable to open specified {} SSL certificate", "Unable to open specified {} SSL certificate",
servarr_name servarr_name
)), ));
process::exit(1);
}
} }
} }
+5 -4
View File
@@ -40,7 +40,7 @@ pub trait NetworkTrait {
#[derive(Clone)] #[derive(Clone)]
pub struct Network<'a, 'b> { pub struct Network<'a, 'b> {
client: Client, client: Client,
cancellation_token: CancellationToken, pub cancellation_token: CancellationToken,
pub app: &'a Arc<Mutex<App<'b>>>, pub app: &'a Arc<Mutex<App<'b>>>,
} }
@@ -74,6 +74,10 @@ impl<'a, 'b> Network<'a, 'b> {
} }
} }
pub(super) async fn reset_cancellation_token(&mut self) {
self.cancellation_token = self.app.lock().await.reset_cancellation_token();
}
async fn handle_request<B, R>( async fn handle_request<B, R>(
&mut self, &mut self,
request_props: RequestProps<B>, request_props: RequestProps<B>,
@@ -89,9 +93,6 @@ impl<'a, 'b> Network<'a, 'b> {
select! { select! {
_ = self.cancellation_token.cancelled() => { _ = self.cancellation_token.cancelled() => {
warn!("Received Cancel request. Cancelling request to: {request_uri}"); warn!("Received Cancel request. Cancelling request to: {request_uri}");
let mut app = self.app.lock().await;
self.cancellation_token = app.reset_cancellation_token();
app.is_loading = false;
Ok(R::default()) Ok(R::default())
} }
resp = self.call_api(request_props).await.send() => { resp = self.call_api(request_props).await.send() => {
+23 -3
View File
@@ -26,7 +26,7 @@ mod tests {
.with_body("{}") .with_body("{}")
.create_async() .create_async()
.await; .await;
let host = server.host_with_port().split(':').collect::<Vec<&str>>()[0].to_owned(); let host = Some(server.host_with_port().split(':').collect::<Vec<&str>>()[0].to_owned());
let port = Some( let port = Some(
server.host_with_port().split(':').collect::<Vec<&str>>()[1] server.host_with_port().split(':').collect::<Vec<&str>>()[1]
.parse() .parse()
@@ -38,8 +38,8 @@ mod tests {
host, host,
api_token: String::new(), api_token: String::new(),
port, port,
use_ssl: false,
ssl_cert_path: None, ssl_cert_path: None,
..RadarrConfig::default()
}; };
app.config.radarr = radarr_config; app.config.radarr = radarr_config;
let app_arc = Arc::new(Mutex::new(app)); let app_arc = Arc::new(Mutex::new(app));
@@ -181,11 +181,31 @@ mod tests {
assert!(!async_server.matched_async().await); assert!(!async_server.matched_async().await);
assert!(app_arc.lock().await.error.text.is_empty()); assert!(app_arc.lock().await.error.text.is_empty());
assert!(!network.cancellation_token.is_cancelled());
assert!(resp.is_ok()); assert!(resp.is_ok());
assert_eq!(resp.unwrap(), Test::default()); assert_eq!(resp.unwrap(), Test::default());
} }
#[tokio::test]
async fn test_reset_cancellation_token() {
let cancellation_token = CancellationToken::new();
let (tx, _) = mpsc::channel::<NetworkEvent>(500);
let app_arc = Arc::new(Mutex::new(App::new(
tx,
AppConfig::default(),
cancellation_token.clone(),
)));
app_arc.lock().await.should_refresh = false;
app_arc.lock().await.is_loading = true;
let mut network = Network::new(&app_arc, cancellation_token, Client::new());
network.cancellation_token.cancel();
network.reset_cancellation_token().await;
assert!(!network.cancellation_token.is_cancelled());
assert!(app_arc.lock().await.should_refresh);
assert!(!app_arc.lock().await.is_loading);
}
#[tokio::test] #[tokio::test]
async fn test_handle_request_get_invalid_body() { async fn test_handle_request_get_invalid_body() {
let mut server = Server::new_async().await; let mut server = Server::new_async().await;
+14 -5
View File
@@ -2261,15 +2261,24 @@ impl<'a, 'b> Network<'a, 'b> {
let RadarrConfig { let RadarrConfig {
host, host,
port, port,
uri,
api_token, api_token,
use_ssl, ssl_cert_path,
..
} = &app.config.radarr; } = &app.config.radarr;
let protocol = if *use_ssl { "https" } else { "http" }; let uri = if let Some(radarr_uri) = uri {
let uri = format!( format!("{radarr_uri}/api/v3{resource}")
} else {
let protocol = if ssl_cert_path.is_some() {
"https"
} else {
"http"
};
let host = host.as_ref().unwrap();
format!(
"{protocol}://{host}:{}/api/v3{resource}", "{protocol}://{host}:{}/api/v3{resource}",
port.unwrap_or(7878) port.unwrap_or(7878)
); )
};
RequestProps { RequestProps {
uri, uri,
+31 -12
View File
@@ -794,7 +794,7 @@ mod test {
.match_header("X-Api-Key", "test1234"); .match_header("X-Api-Key", "test1234");
async_server = async_server.expect_at_most(0).create_async().await; async_server = async_server.expect_at_most(0).create_async().await;
let host = server.host_with_port().split(':').collect::<Vec<&str>>()[0].to_owned(); let host = Some(server.host_with_port().split(':').collect::<Vec<&str>>()[0].to_owned());
let port = Some( let port = Some(
server.host_with_port().split(':').collect::<Vec<&str>>()[1] server.host_with_port().split(':').collect::<Vec<&str>>()[1]
.parse() .parse()
@@ -805,8 +805,7 @@ mod test {
host, host,
port, port,
api_token: "test1234".to_owned(), api_token: "test1234".to_owned(),
use_ssl: false, ..RadarrConfig::default()
ssl_cert_path: None,
}; };
app.config.radarr = radarr_config; app.config.radarr = radarr_config;
let app_arc = Arc::new(Mutex::new(app)); let app_arc = Arc::new(Mutex::new(app));
@@ -4876,11 +4875,10 @@ mod test {
assert!(request_props.api_token.is_empty()); assert!(request_props.api_token.is_empty());
app_arc.lock().await.config.radarr = RadarrConfig { app_arc.lock().await.config.radarr = RadarrConfig {
host: "192.168.0.123".to_owned(), host: Some("192.168.0.123".to_owned()),
port: Some(8080), port: Some(8080),
api_token: "testToken1234".to_owned(), api_token: "testToken1234".to_owned(),
use_ssl: false, ..RadarrConfig::default()
ssl_cert_path: None,
}; };
} }
@@ -4889,11 +4887,33 @@ mod test {
let api_token = "testToken1234".to_owned(); let api_token = "testToken1234".to_owned();
let app_arc = Arc::new(Mutex::new(App::default())); let app_arc = Arc::new(Mutex::new(App::default()));
app_arc.lock().await.config.radarr = RadarrConfig { app_arc.lock().await.config.radarr = RadarrConfig {
host: "192.168.0.123".to_owned(), host: Some("192.168.0.123".to_owned()),
port: Some(8080), port: Some(8080),
api_token: api_token.clone(), api_token: api_token.clone(),
use_ssl: true, ssl_cert_path: Some("/test/cert.crt".to_owned()),
ssl_cert_path: None, ..RadarrConfig::default()
};
let network = Network::new(&app_arc, CancellationToken::new(), Client::new());
let request_props = network
.radarr_request_props_from("/test", RequestMethod::Get, None::<()>)
.await;
assert_str_eq!(request_props.uri, "https://192.168.0.123:8080/api/v3/test");
assert_eq!(request_props.method, RequestMethod::Get);
assert_eq!(request_props.body, None);
assert_str_eq!(request_props.api_token, api_token);
}
#[tokio::test]
async fn test_radarr_request_props_from_custom_radarr_config_using_uri_instead_of_host_and_port()
{
let api_token = "testToken1234".to_owned();
let app_arc = Arc::new(Mutex::new(App::default()));
app_arc.lock().await.config.radarr = RadarrConfig {
uri: Some("https://192.168.0.123:8080".to_owned()),
api_token: api_token.clone(),
..RadarrConfig::default()
}; };
let network = Network::new(&app_arc, CancellationToken::new(), Client::new()); let network = Network::new(&app_arc, CancellationToken::new(), Client::new());
@@ -4986,7 +5006,7 @@ mod test {
async_server = async_server.create_async().await; async_server = async_server.create_async().await;
let host = server.host_with_port().split(':').collect::<Vec<&str>>()[0].to_owned(); let host = Some(server.host_with_port().split(':').collect::<Vec<&str>>()[0].to_owned());
let port = Some( let port = Some(
server.host_with_port().split(':').collect::<Vec<&str>>()[1] server.host_with_port().split(':').collect::<Vec<&str>>()[1]
.parse() .parse()
@@ -4997,8 +5017,7 @@ mod test {
host, host,
port, port,
api_token: "test1234".to_owned(), api_token: "test1234".to_owned(),
use_ssl: false, ..RadarrConfig::default()
ssl_cert_path: None,
}; };
app.config.radarr = radarr_config; app.config.radarr = radarr_config;
let app_arc = Arc::new(Mutex::new(app)); let app_arc = Arc::new(Mutex::new(app));
+5 -1
View File
@@ -92,7 +92,11 @@ fn draw_downloads(f: &mut Frame<'_>, app: &mut App<'_>, area: Rect) {
); );
} }
let percent = 1f64 - (*sizeleft as f64 / *size as f64); let percent = if *size == 0 {
0.0
} else {
1f64 - (*sizeleft as f64 / *size as f64)
};
let file_size: f64 = convert_to_gb(*size); let file_size: f64 = convert_to_gb(*size);
Row::new(vec![ Row::new(vec![
+27 -19
View File
@@ -166,6 +166,7 @@ fn draw_file_info(f: &mut Frame<'_>, app: &App<'_>, area: Rect) {
fn draw_movie_details(f: &mut Frame<'_>, app: &App<'_>, area: Rect) { fn draw_movie_details(f: &mut Frame<'_>, app: &App<'_>, area: Rect) {
let block = layout_block_top_border(); let block = layout_block_top_border();
let unknown_download_status = "Status: Unknown".to_owned();
match app.data.radarr_data.movie_details_modal.as_ref() { match app.data.radarr_data.movie_details_modal.as_ref() {
Some(movie_details_modal) if !app.is_loading => { Some(movie_details_modal) if !app.is_loading => {
@@ -182,7 +183,7 @@ fn draw_movie_details(f: &mut Frame<'_>, app: &App<'_>, area: Rect) {
.items .items
.iter() .iter()
.find(|&line| line.starts_with("Status: ")) .find(|&line| line.starts_with("Status: "))
.unwrap() .unwrap_or(&unknown_download_status)
.split(": ") .split(": ")
.collect::<Vec<&str>>()[1]; .collect::<Vec<&str>>()[1];
let text = Text::from( let text = Text::from(
@@ -285,6 +286,8 @@ fn draw_movie_history(f: &mut Frame<'_>, app: &mut App<'_>, area: Rect) {
} }
fn draw_movie_cast(f: &mut Frame<'_>, app: &mut App<'_>, area: Rect) { fn draw_movie_cast(f: &mut Frame<'_>, app: &mut App<'_>, area: Rect) {
match app.data.radarr_data.movie_details_modal.as_mut() {
Some(movie_details_modal) if !app.is_loading => {
let cast_row_mapping = |cast_member: &Credit| { let cast_row_mapping = |cast_member: &Credit| {
let Credit { let Credit {
person_name, person_name,
@@ -298,15 +301,7 @@ fn draw_movie_cast(f: &mut Frame<'_>, app: &mut App<'_>, area: Rect) {
]) ])
.success() .success()
}; };
let content = Some( let content = Some(&mut movie_details_modal.movie_cast);
&mut app
.data
.radarr_data
.movie_details_modal
.as_mut()
.unwrap()
.movie_cast,
);
let help_footer = app let help_footer = app
.data .data
.radarr_data .radarr_data
@@ -321,8 +316,19 @@ fn draw_movie_cast(f: &mut Frame<'_>, app: &mut App<'_>, area: Rect) {
f.render_widget(cast_table, area); f.render_widget(cast_table, area);
} }
_ => f.render_widget(
LoadingBlock::new(
app.is_loading || app.data.radarr_data.movie_details_modal.is_none(),
layout_block_top_border(),
),
area,
),
}
}
fn draw_movie_crew(f: &mut Frame<'_>, app: &mut App<'_>, area: Rect) { fn draw_movie_crew(f: &mut Frame<'_>, app: &mut App<'_>, area: Rect) {
match app.data.radarr_data.movie_details_modal.as_mut() {
Some(movie_details_modal) if !app.is_loading => {
let crew_row_mapping = |crew_member: &Credit| { let crew_row_mapping = |crew_member: &Credit| {
let Credit { let Credit {
person_name, person_name,
@@ -338,15 +344,7 @@ fn draw_movie_crew(f: &mut Frame<'_>, app: &mut App<'_>, area: Rect) {
]) ])
.success() .success()
}; };
let content = Some( let content = Some(&mut movie_details_modal.movie_crew);
&mut app
.data
.radarr_data
.movie_details_modal
.as_mut()
.unwrap()
.movie_crew,
);
let help_footer = app let help_footer = app
.data .data
.radarr_data .radarr_data
@@ -362,6 +360,16 @@ fn draw_movie_crew(f: &mut Frame<'_>, app: &mut App<'_>, area: Rect) {
f.render_widget(crew_table, area); f.render_widget(crew_table, area);
} }
_ => f.render_widget(
LoadingBlock::new(
app.is_loading || app.data.radarr_data.movie_details_modal.is_none(),
layout_block_top_border(),
),
area,
),
}
}
fn draw_movie_releases(f: &mut Frame<'_>, app: &mut App<'_>, area: Rect) { fn draw_movie_releases(f: &mut Frame<'_>, app: &mut App<'_>, area: Rect) {
if let Route::Radarr(active_radarr_block, _) = *app.get_current_route() { if let Route::Radarr(active_radarr_block, _) = *app.get_current_route() {
let (current_selection, is_empty) = match app.data.radarr_data.movie_details_modal.as_ref() { let (current_selection, is_empty) = match app.data.radarr_data.movie_details_modal.as_ref() {