Compare commits
18 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 48ad17c6f1 | |||
| 3cd15f34cd | |||
| 53ca14e64d | |||
| 0d8803d35d | |||
| 8c90221a81 | |||
| a708f71d57 | |||
| 2a13f74a2b | |||
| 2a97c49a8e | |||
| 8c155ce656 | |||
|
|
5245ba6d98 | ||
|
|
f9789ecc9b | ||
| 9936ce1ab5 | |||
| 650c9783a6 | |||
| b253a389eb | |||
| 5023fbd3d1 | |||
| fdb08fbd34 | |||
| b125d3341a | |||
|
|
f73e3a4817 |
@@ -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 }}
|
||||||
@@ -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 }}
|
||||||
@@ -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 }}
|
||||||
@@ -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
|
||||||
@@ -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
|
||||||
|
|||||||
@@ -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
@@ -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
@@ -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
@@ -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" ]
|
||||||
|
|||||||
@@ -2,13 +2,11 @@
|
|||||||
|
|
||||||

|

|
||||||

|

|
||||||

|
|
||||||

|

|
||||||

|

|
||||||
[](https://crates.io/crates/managarr)
|
[](https://crates.io/crates/managarr)
|
||||||

|

|
||||||
[](https://codecov.io/gh/Dark-Alex-17/managarr)
|
[](https://codecov.io/gh/Dark-Alex-17/managarr)
|
||||||
[](https://github.com/Dark-Alex-17/managarr/releases)
|
|
||||||

|

|
||||||
|
|
||||||
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
|
||||||
|
|||||||
Generated
+6
@@ -0,0 +1,6 @@
|
|||||||
|
{
|
||||||
|
"name": "managarr",
|
||||||
|
"lockfileVersion": 3,
|
||||||
|
"requires": true,
|
||||||
|
"packages": {}
|
||||||
|
}
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
{}
|
||||||
+38
-8
@@ -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);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
+37
-6
@@ -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:?}");
|
||||||
|
|
||||||
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;
|
||||||
@@ -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());
|
||||||
|
}
|
||||||
|
|||||||
+12
-19
@@ -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) {
|
||||||
|
|||||||
@@ -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,
|
||||||
);
|
);
|
||||||
|
|
||||||
|
|||||||
+41
-31
@@ -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,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;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -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(_) => {
|
||||||
"Unable to read the specified {} SSL certificate",
|
log_and_print_error(format!(
|
||||||
servarr_name
|
"Unable to read the specified {} SSL certificate",
|
||||||
)),
|
servarr_name
|
||||||
|
));
|
||||||
|
process::exit(1);
|
||||||
|
}
|
||||||
},
|
},
|
||||||
Err(_) => err(format!(
|
Err(_) => {
|
||||||
"Unable to open specified {} SSL certificate",
|
log_and_print_error(format!(
|
||||||
servarr_name
|
"Unable to open specified {} SSL certificate",
|
||||||
)),
|
servarr_name
|
||||||
|
));
|
||||||
|
process::exit(1);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
+5
-4
@@ -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() => {
|
||||||
|
|||||||
@@ -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;
|
||||||
|
|||||||
@@ -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}")
|
||||||
"{protocol}://{host}:{}/api/v3{resource}",
|
} else {
|
||||||
port.unwrap_or(7878)
|
let protocol = if ssl_cert_path.is_some() {
|
||||||
);
|
"https"
|
||||||
|
} else {
|
||||||
|
"http"
|
||||||
|
};
|
||||||
|
let host = host.as_ref().unwrap();
|
||||||
|
format!(
|
||||||
|
"{protocol}://{host}:{}/api/v3{resource}",
|
||||||
|
port.unwrap_or(7878)
|
||||||
|
)
|
||||||
|
};
|
||||||
|
|
||||||
RequestProps {
|
RequestProps {
|
||||||
uri,
|
uri,
|
||||||
|
|||||||
@@ -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));
|
||||||
|
|||||||
@@ -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![
|
||||||
|
|||||||
@@ -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) {
|
||||||
|
|||||||
Reference in New Issue
Block a user