Compare commits

...

15 Commits

Author SHA1 Message Date
5b42129f55 fix: Yet another typo in the release workflow [skip ci] 2024-11-06 17:15:17 -07:00
4f06b2b947 fix: Fixed typo in release name [skip ci] 2024-11-06 17:14:33 -07:00
0f98050a12 fix: Removed the release-patch workflow [skip ci] 2024-11-06 17:14:01 -07:00
14839642dc fix: Fixed a typo in the new release workflow name [skip ci] 2024-11-06 17:13:17 -07:00
eccc1a2df1 fix: Release-plz to perform the release and to use Commitizen for bumping and generating the CHANGELOG [skip ci] 2024-11-06 17:12:39 -07:00
9df929a8e3 fix: Updated the Commitizen config to also always commit the Cargo.lock when doing the version bump 2024-11-06 17:07:43 -07:00
1e008f9778 bump: version 0.2.1 → 0.2.2 2024-11-06 17:03:46 -07:00
Alex Clarke
fa811da5c2 Merge pull request #16 from Dark-Alex-17/commitizen-config
Added Commitizen to enforce commit styles
2024-11-06 16:53:40 -07:00
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
21 changed files with 350 additions and 245 deletions
+10
View File
@@ -0,0 +1,10 @@
[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
[tool.commitizen.hooks]
pre-commit = "git add Cargo.toml Cargo.lock"
-38
View File
@@ -1,38 +0,0 @@
# 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:
bump: minor
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
CARGO_REGISTRY_TOKEN: ${{ secrets.CARGO_REGISTRY_TOKEN }}
-38
View File
@@ -1,38 +0,0 @@
# 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:
bump: patch
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
CARGO_REGISTRY_TOKEN: ${{ secrets.CARGO_REGISTRY_TOKEN }}
@@ -1,7 +1,7 @@
# 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 major release name: Create release
permissions: permissions:
pull-requests: write pull-requests: write
@@ -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
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
+13 -1
View File
@@ -5,7 +5,19 @@ All notable changes to this project will be documented in this file.
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
## [Unreleased] ## v0.2.2 (2024-11-06)
### Fix
- **handler**: Fixed a bug in the movie details handler that would allow key events to be processed before the data was finished loading
- **ui**: Fixed a bug that would freeze all user input while background network requests were running
- **radarr_ui**: Fixed a race condition bug in the movie details UI that would panic if the user changes tabs too quickly
### 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
## v0.2.1 (2024-11-06)
## [0.2.1](https://github.com/Dark-Alex-17/managarr/compare/v0.2.0...v0.2.1) - 2024-11-06 ## [0.2.1](https://github.com/Dark-Alex-17/managarr/compare/v0.2.0...v0.2.1) - 2024-11-06
+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.1" version = "0.2.2"
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.1" version = "0.2.2"
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"]
+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);
+6 -1
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:?}");
self.is_loading = true; if !self.should_refresh {
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()
} }
+12 -19
View File
@@ -142,35 +142,22 @@ 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.refresh_metadata().await;
} }
self.dispatch_by_radarr_block(&active_radarr_block).await;
self.refresh_metadata().await;
} }
if self.tick_count % self.tick_until_poll == 0 { if self.tick_count % self.tick_until_poll == 0 {
@@ -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 { match self.active_radarr_block {
!movie_details_modal.movie_details.is_empty() ActiveRadarrBlock::MovieDetails => {
|| !movie_details_modal.movie_history.is_empty() !self.app.is_loading && !movie_details_modal.movie_details.is_empty()
|| !movie_details_modal.movie_cast.is_empty() }
|| !movie_details_modal.movie_crew.is_empty() ActiveRadarrBlock::MovieHistory => {
|| !movie_details_modal.movie_releases.is_empty() !self.app.is_loading && !movie_details_modal.movie_history.is_empty()
} else { }
false ActiveRadarrBlock::Cast => {
}; !self.app.is_loading && !movie_details_modal.movie_cast.is_empty()
}
!self.app.is_loading && movie_details_modal_is_ready 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 {
false
}
} }
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,
); );
+21 -5
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,9 +146,20 @@ 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 {
if let Err(e) = network.handle_network_event(network_event).await { select! {
error!("Encountered an error handling network event: {e:?}"); Some(network_event) = network_rx.recv() => {
if let Err(e) = network.handle_network_event(network_event).await {
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;
}
} }
} }
} }
@@ -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;
+77 -69
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,81 +286,88 @@ 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) {
let cast_row_mapping = |cast_member: &Credit| { match app.data.radarr_data.movie_details_modal.as_mut() {
let Credit { Some(movie_details_modal) if !app.is_loading => {
person_name, let cast_row_mapping = |cast_member: &Credit| {
character, let Credit {
.. person_name,
} = cast_member; character,
..
} = cast_member;
Row::new(vec![ Row::new(vec![
Cell::from(person_name.to_owned()), Cell::from(person_name.to_owned()),
Cell::from(character.clone().unwrap_or_default()), Cell::from(character.clone().unwrap_or_default()),
]) ])
.success() .success()
}; };
let content = Some( let content = Some(&mut movie_details_modal.movie_cast);
&mut app let help_footer = app
.data .data
.radarr_data .radarr_data
.movie_details_modal .movie_info_tabs
.as_mut() .get_active_tab_contextual_help();
.unwrap() let cast_table = ManagarrTable::new(content, cast_row_mapping)
.movie_cast, .block(layout_block_top_border())
); .footer(help_footer)
let help_footer = app .loading(app.is_loading)
.data .headers(["Cast Member", "Character"])
.radarr_data .constraints([Constraint::Ratio(1, 2), Constraint::Ratio(1, 2)]);
.movie_info_tabs
.get_active_tab_contextual_help();
let cast_table = ManagarrTable::new(content, cast_row_mapping)
.block(layout_block_top_border())
.footer(help_footer)
.loading(app.is_loading)
.headers(["Cast Member", "Character"])
.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) {
let crew_row_mapping = |crew_member: &Credit| { match app.data.radarr_data.movie_details_modal.as_mut() {
let Credit { Some(movie_details_modal) if !app.is_loading => {
person_name, let crew_row_mapping = |crew_member: &Credit| {
job, let Credit {
department, person_name,
.. job,
} = crew_member; department,
..
} = crew_member;
Row::new(vec![ Row::new(vec![
Cell::from(person_name.to_owned()), Cell::from(person_name.to_owned()),
Cell::from(job.clone().unwrap_or_default()), Cell::from(job.clone().unwrap_or_default()),
Cell::from(department.clone().unwrap_or_default()), Cell::from(department.clone().unwrap_or_default()),
]) ])
.success() .success()
}; };
let content = Some( let content = Some(&mut movie_details_modal.movie_crew);
&mut app let help_footer = app
.data .data
.radarr_data .radarr_data
.movie_details_modal .movie_info_tabs
.as_mut() .get_active_tab_contextual_help();
.unwrap() let crew_table = ManagarrTable::new(content, crew_row_mapping)
.movie_crew, .block(layout_block_top_border())
); .loading(app.is_loading)
let help_footer = app .headers(["Crew Member", "Job", "Department"])
.data .constraints(iter::repeat(Constraint::Ratio(1, 3)).take(3))
.radarr_data .footer(help_footer);
.movie_info_tabs
.get_active_tab_contextual_help();
let crew_table = ManagarrTable::new(content, crew_row_mapping)
.block(layout_block_top_border())
.loading(app.is_loading)
.headers(["Crew Member", "Job", "Department"])
.constraints(iter::repeat(Constraint::Ratio(1, 3)).take(3))
.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) {