Compare commits

...

7 Commits

18 changed files with 333 additions and 167 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 -1
View File
@@ -32,7 +32,7 @@ jobs:
- name: Run release-plz - name: Run release-plz
uses: MarcoIeni/release-plz-action@v0.5 uses: MarcoIeni/release-plz-action@v0.5
with: with:
bump: major 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 }}
+1 -1
View File
@@ -32,7 +32,7 @@ jobs:
- name: Run release-plz - name: Run release-plz
uses: MarcoIeni/release-plz-action@v0.5 uses: MarcoIeni/release-plz-action@v0.5
with: with:
bump: minor command: release --bump-minor
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 }}
+1 -1
View File
@@ -32,7 +32,7 @@ jobs:
- name: Run release-plz - name: Run release-plz
uses: MarcoIeni/release-plz-action@v0.5 uses: MarcoIeni/release-plz-action@v0.5
with: with:
bump: patch command: release --bump-patch
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 }}
+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
+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
+6
View File
@@ -0,0 +1,6 @@
{
"name": "managarr",
"lockfileVersion": 3,
"requires": true,
"packages": {}
}
+1
View File
@@ -0,0 +1 @@
{}
+35 -5
View File
@@ -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);
+5
View File
@@ -56,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;
@@ -113,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()
} }
+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,
); );
+19 -3
View File
@@ -6,6 +6,7 @@ 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;
@@ -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;
@@ -144,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<()> {
@@ -228,7 +241,10 @@ 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 let Some(ref cert_path) = config.radarr.ssl_cert_path { if let Some(ref cert_path) = config.radarr.ssl_cert_path {
let cert = create_cert(cert_path, "Radarr"); let cert = create_cert(cert_path, "Radarr");
+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() => {
+21 -1
View File
@@ -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;
+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
@@ -320,9 +315,20 @@ fn draw_movie_cast(f: &mut Frame<'_>, app: &mut App<'_>, area: Rect) {
.constraints([Constraint::Ratio(1, 2), Constraint::Ratio(1, 2)]); .constraints([Constraint::Ratio(1, 2), Constraint::Ratio(1, 2)]);
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
@@ -360,6 +358,16 @@ fn draw_movie_crew(f: &mut Frame<'_>, app: &mut App<'_>, area: Rect) {
.footer(help_footer); .footer(help_footer);
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) {