Compare commits

...

13 Commits

Author SHA1 Message Date
github-actions[bot] a26444006b chore: bump Cargo.toml to 0.7.3
Check / stable / fmt (push) Failing after 27s
Check / beta / clippy (push) Failing after 1m13s
Check / stable / clippy (push) Failing after 1m15s
Check / nightly / doc (push) Successful in 1m3s
Check / 1.95.0 / check (push) Successful in 1m9s
Test Suite / ubuntu / beta (push) Successful in 1m47s
Test Suite / ubuntu / stable (push) Successful in 1m47s
Test Suite / ubuntu / stable / coverage (push) Failing after 2m24s
Test Suite / macos-latest / stable (push) Has been cancelled
Test Suite / windows-latest / stable (push) Has been cancelled
2026-06-25 20:58:11 +00:00
github-actions[bot] ccf3e28323 bump: version 0.7.2 → 0.7.3 [skip ci] 2026-06-25 20:58:10 +00:00
Alex Clarke 6eea6b92fb Merge pull request #62 from Dark-Alex-17/develop
Security fixes, rolling logs, and Rust version upgrade
2026-06-25 14:37:16 -06:00
Dark-Alex-17 6df68b8a66 tests: Addressed additional CR comments and added tests for tail-logs 2026-06-25 14:21:57 -06:00
Dark-Alex-17 cbca6bd916 fix: addressed code review comments 2026-06-25 14:11:18 -06:00
Dark-Alex-17 92187c5f16 fix: tail-logs subcommand follows log rollovers and sleeps to minimize idle CPU loops 2026-06-25 13:51:32 -06:00
Dark-Alex-17 366809d8c6 fmt: applied formatting 2026-06-25 13:32:31 -06:00
Dark-Alex-17 dd93fe117d feat: Implemented log rolling so the log file doesn't just grow exponentially [#60] 2026-06-25 13:29:56 -06:00
Dark-Alex-17 10e18af1bf build: upgraded to openssl 0.10.79 to fix security vulnerabilities 2026-06-25 13:20:14 -06:00
Dark-Alex-17 a4d93692a9 build: Upgraded to Rust v1.95.0 2026-06-25 13:19:27 -06:00
Dark-Alex-17 03d134bec9 refactor: Refactored several usages of sort_by_key and match guards to utilize newer Rust version APIs 2026-06-25 13:18:43 -06:00
Dark-Alex-17 4cad9e1755 docs: Improved the README to demonstrate what a truly minimal configuration looks like for each service and explain the "reasonable defaults" [#61] 2026-06-25 12:35:51 -06:00
github-actions[bot] adb76bb603 bump: version 0.7.1 → 0.7.2 [skip ci] 2026-04-20 22:37:15 +00:00
49 changed files with 755 additions and 805 deletions
+4 -4
View File
@@ -76,15 +76,15 @@ jobs:
RUSTDOCFLAGS: --cfg docsrs RUSTDOCFLAGS: --cfg docsrs
msrv: msrv:
# check that we can build using the minimal rust version that is specified by this crate # check that we can build using the minimal rust version that is specified by this crate
name: 1.89.0 / check name: 1.95.0 / check
runs-on: ubuntu-latest runs-on: ubuntu-latest
steps: steps:
- uses: actions/checkout@v4 - uses: actions/checkout@v4
- name: Install 1.89.0 - name: Install 1.95.0
uses: dtolnay/rust-toolchain@master uses: dtolnay/rust-toolchain@master
with: with:
toolchain: 1.89.0 toolchain: 1.95.0
- name: cargo +1.89.0 check - name: cargo +1.95.0 check
run: cargo check run: cargo check
+21
View File
@@ -5,6 +5,27 @@ 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).
## v0.7.3 (2026-06-25)
### Feat
- Implemented log rolling so the log file doesn't just grow exponentially [#60]
### Fix
- addressed code review comments
- tail-logs subcommand follows log rollovers and sleeps to minimize idle CPU loops
### Refactor
- Refactored several usages of sort_by_key and match guards to utilize newer Rust version APIs
## v0.7.2 (2026-04-20)
### Feat
- Created a separate 'ssl' property for the config so users don't have to specify an ssl_cert_path to use SSL or use the uri workaround for HTTPS API access
## v0.7.1 (2026-02-04) ## v0.7.1 (2026-02-04)
### Feat ### Feat
Generated
+364 -527
View File
File diff suppressed because it is too large Load Diff
+4 -4
View File
@@ -1,6 +1,6 @@
[package] [package]
name = "managarr" name = "managarr"
version = "0.7.1" version = "0.7.3"
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"]
@@ -10,7 +10,7 @@ homepage = "https://github.com/Dark-Alex-17/managarr"
readme = "README.md" readme = "README.md"
edition = "2024" edition = "2024"
license = "MIT" license = "MIT"
rust-version = "1.89.0" rust-version = "1.95.0"
exclude = [".github", "CONTRIBUTING.md", "*.log", "tags"] exclude = [".github", "CONTRIBUTING.md", "*.log", "tags"]
[workspace] [workspace]
@@ -32,7 +32,7 @@ derivative = "2.2.0"
human-panic = "2.0.6" human-panic = "2.0.6"
indoc = "2.0.7" indoc = "2.0.7"
log = "0.4.29" log = "0.4.29"
log4rs = { version = "1.4.0", features = ["file_appender"] } log4rs = { version = "1.4.0", features = ["rolling_file_appender", "compound_policy", "size_trigger", "fixed_window_roller"] }
regex = "1.12.2" regex = "1.12.2"
reqwest = { version = "0.13.2", features = ["json"] } reqwest = { version = "0.13.2", features = ["json"] }
serde_yaml = "0.9.34" serde_yaml = "0.9.34"
@@ -63,7 +63,7 @@ managarr-tree-widget = "0.25.0"
indicatif = "0.17.11" indicatif = "0.17.11"
derive_setters = "0.1.9" derive_setters = "0.1.9"
deunicode = "1.6.2" deunicode = "1.6.2"
openssl = { version = "0.10.75", features = ["vendored"] } openssl = { version = "0.10.79", features = ["vendored"] }
veil = "0.2.0" veil = "0.2.0"
validate_theme_derive = "0.1.0" validate_theme_derive = "0.1.0"
enum_display_style_derive = "0.1.0" enum_display_style_derive = "0.1.0"
+32 -1
View File
@@ -338,7 +338,38 @@ $ managarr radarr list movies | jq '.[] | select(.title == "Ad Astra") | .id'
# Configuration # Configuration
Managarr assumes reasonable defaults to connect to each service (i.e. Radarr is on localhost:7878), Managarr assumes reasonable defaults to connect to each service (i.e. Radarr is on localhost:7878),
but all servers will require you to input the API token. but all servers will require you to input the API token. This means that for each Servarr you configure,
if you define only the `api_token`, Managarr will assume the Servarr is running on `localhost` and on the
default port for that respective service. That is:
| Servarr | Default Host | Default Port |
|---------|--------------|--------------|
| Radarr | `localhost` | 7878 |
| Sonarr | `localhost` | 8989 |
| Lidarr | `localhost` | 8686 |
> [!TIP]
> In general, all Servarrs store their API tokens under Settings -> General -> Security -> API Key in their web UIs.
## Minimum Configuration Requirements
The following configuration file will connect to each Servarr running on localhost with their default ports. The only
requirement for each is the specification of an API token.
```yaml
radarr:
# Connect to Radarr running on localhost:7878
- api_token: <your-radarr-api-token-here>
sonarr:
# Connect to sonarr running on localhost:8989
- api_token: <your-sonarr-api-token-here>
lidarr:
# Connect to lidarr running on localhost:8686
- api_token: <your-lidarr-api-token-here>
```
## Configuration File Location
The configuration file is located somewhere different for each OS, so run the following command to print out the default The configuration file is located somewhere different for each OS, so run the following command to print out the default
location of the `managarr` configuration file for your system: location of the `managarr` configuration file for your system:
+4 -4
View File
@@ -231,8 +231,8 @@ mod tests {
let expected_keybinding_items = Vec::from(SERVARR_CONTEXT_CLUES) let expected_keybinding_items = Vec::from(SERVARR_CONTEXT_CLUES)
.iter() .iter()
.map(|(key, desc)| { .map(|(key, desc)| {
let (key, alt_key) = if key.alt.is_some() { let (key, alt_key) = if let Some(key1) = key.alt {
(key.key.to_string(), key.alt.as_ref().unwrap().to_string()) (key.key.to_string(), key1.to_string())
} else { } else {
(key.key.to_string(), String::new()) (key.key.to_string(), String::new())
}; };
@@ -338,8 +338,8 @@ mod tests {
} }
fn context_clue_to_keybinding_item(key: &KeyBinding, desc: &&str) -> KeybindingItem { fn context_clue_to_keybinding_item(key: &KeyBinding, desc: &&str) -> KeybindingItem {
let (key, alt_key) = if key.alt.is_some() { let (key, alt_key) = if let Some(key1) = key.alt {
(key.key.to_string(), key.alt.as_ref().unwrap().to_string()) (key.key.to_string(), key1.to_string())
} else { } else {
(key.key.to_string(), String::new()) (key.key.to_string(), String::new())
}; };
@@ -506,18 +506,17 @@ impl<'a, 'b> KeyEventHandler<'a, 'b, ActiveLidarrBlock> for EditIndexerHandler<'
.tags .tags
); );
} }
ActiveLidarrBlock::EditIndexerPrompt => { ActiveLidarrBlock::EditIndexerPrompt
if self.app.data.lidarr_data.selected_block.get_active_block() if self.app.data.lidarr_data.selected_block.get_active_block()
== ActiveLidarrBlock::EditIndexerConfirmPrompt == ActiveLidarrBlock::EditIndexerConfirmPrompt
&& matches_key!(confirm, self.key) && matches_key!(confirm, self.key) =>
{ {
self.app.data.lidarr_data.prompt_confirm = true; self.app.data.lidarr_data.prompt_confirm = true;
self.app.data.lidarr_data.prompt_confirm_action = self.app.data.lidarr_data.prompt_confirm_action =
Some(LidarrEvent::EditIndexer(self.build_edit_indexer_params())); Some(LidarrEvent::EditIndexer(self.build_edit_indexer_params()));
self.app.should_refresh = true; self.app.should_refresh = true;
self.app.pop_navigation_stack(); self.app.pop_navigation_stack();
}
} }
_ => (), _ => (),
} }
@@ -106,10 +106,10 @@ impl<'a, 'b> KeyEventHandler<'a, 'b, ActiveLidarrBlock> for IndexerSettingsHandl
indexer_settings.maximum_size -= 1; indexer_settings.maximum_size -= 1;
} }
} }
ActiveLidarrBlock::IndexerSettingsRssSyncIntervalInput => { ActiveLidarrBlock::IndexerSettingsRssSyncIntervalInput
if indexer_settings.rss_sync_interval > 0 { if indexer_settings.rss_sync_interval > 0 =>
indexer_settings.rss_sync_interval -= 1; {
} indexer_settings.rss_sync_interval -= 1;
} }
_ => (), _ => (),
} }
@@ -591,16 +591,15 @@ impl<'a, 'b> KeyEventHandler<'a, 'b, ActiveLidarrBlock> for AddArtistHandler<'a,
.tags .tags
) )
} }
ActiveLidarrBlock::AddArtistPrompt => { ActiveLidarrBlock::AddArtistPrompt
if self.app.data.lidarr_data.selected_block.get_active_block() if self.app.data.lidarr_data.selected_block.get_active_block()
== ActiveLidarrBlock::AddArtistConfirmPrompt == ActiveLidarrBlock::AddArtistConfirmPrompt
&& matches_key!(confirm, key) && matches_key!(confirm, key) =>
{ {
self.app.data.lidarr_data.prompt_confirm = true; self.app.data.lidarr_data.prompt_confirm = true;
self.app.data.lidarr_data.prompt_confirm_action = self.app.data.lidarr_data.prompt_confirm_action =
Some(LidarrEvent::AddArtist(self.build_add_artist_body())); Some(LidarrEvent::AddArtist(self.build_add_artist_body()));
self.app.pop_navigation_stack(); self.app.pop_navigation_stack();
}
} }
_ => (), _ => (),
} }
@@ -293,16 +293,16 @@ impl<'a, 'b> KeyEventHandler<'a, 'b, ActiveLidarrBlock> for ArtistDetailsHandler
self.app.data.lidarr_data.selected_block = self.app.data.lidarr_data.selected_block =
BlockSelectionState::new(EDIT_ARTIST_SELECTION_BLOCKS); BlockSelectionState::new(EDIT_ARTIST_SELECTION_BLOCKS);
} }
_ if matches_key!(toggle_monitoring, key) => { _ if matches_key!(toggle_monitoring, key)
if !self.app.data.lidarr_data.albums.is_empty() { && !self.app.data.lidarr_data.albums.is_empty() =>
self.app.data.lidarr_data.prompt_confirm = true; {
self.app.data.lidarr_data.prompt_confirm_action = self.app.data.lidarr_data.prompt_confirm = true;
Some(LidarrEvent::ToggleAlbumMonitoring(self.extract_album_id())); self.app.data.lidarr_data.prompt_confirm_action =
Some(LidarrEvent::ToggleAlbumMonitoring(self.extract_album_id()));
self self
.app .app
.pop_and_push_navigation_stack(self.active_lidarr_block.into()); .pop_and_push_navigation_stack(self.active_lidarr_block.into());
}
} }
_ => (), _ => (),
}, },
@@ -428,18 +428,17 @@ impl<'a, 'b> KeyEventHandler<'a, 'b, ActiveLidarrBlock> for EditArtistHandler<'a
.tags .tags
) )
} }
ActiveLidarrBlock::EditArtistPrompt => { ActiveLidarrBlock::EditArtistPrompt
if self.app.data.lidarr_data.selected_block.get_active_block() if self.app.data.lidarr_data.selected_block.get_active_block()
== ActiveLidarrBlock::EditArtistConfirmPrompt == ActiveLidarrBlock::EditArtistConfirmPrompt
&& matches_key!(confirm, key) && matches_key!(confirm, key) =>
{ {
self.app.data.lidarr_data.prompt_confirm = true; self.app.data.lidarr_data.prompt_confirm = true;
self.app.data.lidarr_data.prompt_confirm_action = self.app.data.lidarr_data.prompt_confirm_action =
Some(LidarrEvent::EditArtist(self.build_edit_artist_params())); Some(LidarrEvent::EditArtist(self.build_edit_artist_params()));
self.app.should_refresh = true; self.app.should_refresh = true;
self.app.pop_navigation_stack(); self.app.pop_navigation_stack();
}
} }
_ => (), _ => (),
} }
@@ -505,19 +505,18 @@ impl<'a, 'b> KeyEventHandler<'a, 'b, ActiveLidarrBlock> for AddRootFolderHandler
.tags .tags
) )
} }
ActiveLidarrBlock::AddRootFolderPrompt => { ActiveLidarrBlock::AddRootFolderPrompt
if self.app.data.lidarr_data.selected_block.get_active_block() if self.app.data.lidarr_data.selected_block.get_active_block()
== ActiveLidarrBlock::AddRootFolderConfirmPrompt == ActiveLidarrBlock::AddRootFolderConfirmPrompt
&& matches_key!(confirm, key) && matches_key!(confirm, key) =>
{ {
self.app.data.lidarr_data.prompt_confirm = true; self.app.data.lidarr_data.prompt_confirm = true;
self.app.data.lidarr_data.prompt_confirm_action = Some(LidarrEvent::AddRootFolder( self.app.data.lidarr_data.prompt_confirm_action = Some(LidarrEvent::AddRootFolder(
self.build_add_root_folder_body(), self.build_add_root_folder_body(),
)); ));
self.app.should_refresh = true; self.app.should_refresh = true;
self.app.pop_navigation_stack(); self.app.pop_navigation_stack();
}
} }
_ => (), _ => (),
} }
+2 -2
View File
@@ -139,8 +139,8 @@ pub fn handle_events(key: Key, app: &mut App<'_>) {
pub fn populate_keymapping_table(app: &mut App<'_>) { pub fn populate_keymapping_table(app: &mut App<'_>) {
let context_clue_to_keybinding_item = |key: &KeyBinding, desc: &&str| { let context_clue_to_keybinding_item = |key: &KeyBinding, desc: &&str| {
let (key, alt_key) = if key.alt.is_some() { let (key, alt_key) = if let Some(key1) = key.alt {
(key.key.to_string(), key.alt.as_ref().unwrap().to_string()) (key.key.to_string(), key1.to_string())
} else { } else {
(key.key.to_string(), String::new()) (key.key.to_string(), String::new())
}; };
@@ -354,19 +354,18 @@ impl<'a, 'b> KeyEventHandler<'a, 'b, ActiveRadarrBlock> for EditCollectionHandle
.path .path
) )
} }
ActiveRadarrBlock::EditCollectionPrompt => { ActiveRadarrBlock::EditCollectionPrompt
if self.app.data.radarr_data.selected_block.get_active_block() if self.app.data.radarr_data.selected_block.get_active_block()
== ActiveRadarrBlock::EditCollectionConfirmPrompt == ActiveRadarrBlock::EditCollectionConfirmPrompt
&& matches_key!(confirm, key) && matches_key!(confirm, key) =>
{ {
self.app.data.radarr_data.prompt_confirm = true; self.app.data.radarr_data.prompt_confirm = true;
self.app.data.radarr_data.prompt_confirm_action = Some(RadarrEvent::EditCollection( self.app.data.radarr_data.prompt_confirm_action = Some(RadarrEvent::EditCollection(
self.build_edit_collection_params(), self.build_edit_collection_params(),
)); ));
self.app.should_refresh = true; self.app.should_refresh = true;
self.app.pop_navigation_stack(); self.app.pop_navigation_stack();
}
} }
_ => (), _ => (),
} }
@@ -507,18 +507,17 @@ impl<'a, 'b> KeyEventHandler<'a, 'b, ActiveRadarrBlock> for EditIndexerHandler<'
.tags .tags
); );
} }
ActiveRadarrBlock::EditIndexerPrompt => { ActiveRadarrBlock::EditIndexerPrompt
if self.app.data.radarr_data.selected_block.get_active_block() if self.app.data.radarr_data.selected_block.get_active_block()
== ActiveRadarrBlock::EditIndexerConfirmPrompt == ActiveRadarrBlock::EditIndexerConfirmPrompt
&& matches_key!(confirm, self.key) && matches_key!(confirm, self.key) =>
{ {
self.app.data.radarr_data.prompt_confirm = true; self.app.data.radarr_data.prompt_confirm = true;
self.app.data.radarr_data.prompt_confirm_action = self.app.data.radarr_data.prompt_confirm_action =
Some(RadarrEvent::EditIndexer(self.build_edit_indexer_params())); Some(RadarrEvent::EditIndexer(self.build_edit_indexer_params()));
self.app.should_refresh = true; self.app.should_refresh = true;
self.app.pop_navigation_stack(); self.app.pop_navigation_stack();
}
} }
_ => (), _ => (),
} }
@@ -114,10 +114,10 @@ impl<'a, 'b> KeyEventHandler<'a, 'b, ActiveRadarrBlock> for IndexerSettingsHandl
ActiveRadarrBlock::IndexerSettingsAvailabilityDelayInput => { ActiveRadarrBlock::IndexerSettingsAvailabilityDelayInput => {
indexer_settings.availability_delay -= 1; indexer_settings.availability_delay -= 1;
} }
ActiveRadarrBlock::IndexerSettingsRssSyncIntervalInput => { ActiveRadarrBlock::IndexerSettingsRssSyncIntervalInput
if indexer_settings.rss_sync_interval > 0 { if indexer_settings.rss_sync_interval > 0 =>
indexer_settings.rss_sync_interval -= 1; {
} indexer_settings.rss_sync_interval -= 1;
} }
_ => (), _ => (),
} }
@@ -272,19 +272,18 @@ impl<'a, 'b> KeyEventHandler<'a, 'b, ActiveRadarrBlock> for IndexerSettingsHandl
.whitelisted_hardcoded_subs .whitelisted_hardcoded_subs
) )
} }
ActiveRadarrBlock::AllIndexerSettingsPrompt => { ActiveRadarrBlock::AllIndexerSettingsPrompt
if self.app.data.radarr_data.selected_block.get_active_block() if self.app.data.radarr_data.selected_block.get_active_block()
== ActiveRadarrBlock::IndexerSettingsConfirmPrompt == ActiveRadarrBlock::IndexerSettingsConfirmPrompt
&& matches_key!(confirm, self.key) && matches_key!(confirm, self.key) =>
{ {
self.app.data.radarr_data.prompt_confirm = true; self.app.data.radarr_data.prompt_confirm = true;
self.app.data.radarr_data.prompt_confirm_action = Some( self.app.data.radarr_data.prompt_confirm_action = Some(
RadarrEvent::EditAllIndexerSettings(self.build_edit_indexer_settings_body()), RadarrEvent::EditAllIndexerSettings(self.build_edit_indexer_settings_body()),
); );
self.app.should_refresh = true; self.app.should_refresh = true;
self.app.pop_navigation_stack(); self.app.pop_navigation_stack();
}
} }
_ => (), _ => (),
} }
@@ -539,16 +539,15 @@ impl<'a, 'b> KeyEventHandler<'a, 'b, ActiveRadarrBlock> for AddMovieHandler<'a,
.tags .tags
) )
} }
ActiveRadarrBlock::AddMoviePrompt => { ActiveRadarrBlock::AddMoviePrompt
if self.app.data.radarr_data.selected_block.get_active_block() if self.app.data.radarr_data.selected_block.get_active_block()
== ActiveRadarrBlock::AddMovieConfirmPrompt == ActiveRadarrBlock::AddMovieConfirmPrompt
&& matches_key!(confirm, key) && matches_key!(confirm, key) =>
{ {
self.app.data.radarr_data.prompt_confirm = true; self.app.data.radarr_data.prompt_confirm = true;
self.app.data.radarr_data.prompt_confirm_action = self.app.data.radarr_data.prompt_confirm_action =
Some(RadarrEvent::AddMovie(self.build_add_movie_body())); Some(RadarrEvent::AddMovie(self.build_add_movie_body()));
self.app.pop_navigation_stack(); self.app.pop_navigation_stack();
}
} }
_ => (), _ => (),
} }
@@ -376,18 +376,17 @@ impl<'a, 'b> KeyEventHandler<'a, 'b, ActiveRadarrBlock> for EditMovieHandler<'a,
.tags .tags
) )
} }
ActiveRadarrBlock::EditMoviePrompt => { ActiveRadarrBlock::EditMoviePrompt
if self.app.data.radarr_data.selected_block.get_active_block() if self.app.data.radarr_data.selected_block.get_active_block()
== ActiveRadarrBlock::EditMovieConfirmPrompt == ActiveRadarrBlock::EditMovieConfirmPrompt
&& matches_key!(confirm, key) && matches_key!(confirm, key) =>
{ {
self.app.data.radarr_data.prompt_confirm = true; self.app.data.radarr_data.prompt_confirm = true;
self.app.data.radarr_data.prompt_confirm_action = self.app.data.radarr_data.prompt_confirm_action =
Some(RadarrEvent::EditMovie(self.build_edit_movie_params())); Some(RadarrEvent::EditMovie(self.build_edit_movie_params()));
self.app.should_refresh = true; self.app.should_refresh = true;
self.app.pop_navigation_stack(); self.app.pop_navigation_stack();
}
} }
_ => (), _ => (),
} }
@@ -506,18 +506,17 @@ impl<'a, 'b> KeyEventHandler<'a, 'b, ActiveSonarrBlock> for EditIndexerHandler<'
.tags .tags
); );
} }
ActiveSonarrBlock::EditIndexerPrompt => { ActiveSonarrBlock::EditIndexerPrompt
if self.app.data.sonarr_data.selected_block.get_active_block() if self.app.data.sonarr_data.selected_block.get_active_block()
== ActiveSonarrBlock::EditIndexerConfirmPrompt == ActiveSonarrBlock::EditIndexerConfirmPrompt
&& matches_key!(confirm, self.key) && matches_key!(confirm, self.key) =>
{ {
self.app.data.sonarr_data.prompt_confirm = true; self.app.data.sonarr_data.prompt_confirm = true;
self.app.data.sonarr_data.prompt_confirm_action = self.app.data.sonarr_data.prompt_confirm_action =
Some(SonarrEvent::EditIndexer(self.build_edit_indexer_params())); Some(SonarrEvent::EditIndexer(self.build_edit_indexer_params()));
self.app.should_refresh = true; self.app.should_refresh = true;
self.app.pop_navigation_stack(); self.app.pop_navigation_stack();
}
} }
_ => (), _ => (),
} }
@@ -106,10 +106,10 @@ impl<'a, 'b> KeyEventHandler<'a, 'b, ActiveSonarrBlock> for IndexerSettingsHandl
indexer_settings.maximum_size -= 1; indexer_settings.maximum_size -= 1;
} }
} }
ActiveSonarrBlock::IndexerSettingsRssSyncIntervalInput => { ActiveSonarrBlock::IndexerSettingsRssSyncIntervalInput
if indexer_settings.rss_sync_interval > 0 { if indexer_settings.rss_sync_interval > 0 =>
indexer_settings.rss_sync_interval -= 1; {
} indexer_settings.rss_sync_interval -= 1;
} }
_ => (), _ => (),
} }
@@ -606,16 +606,15 @@ impl<'a, 'b> KeyEventHandler<'a, 'b, ActiveSonarrBlock> for AddSeriesHandler<'a,
.tags .tags
) )
} }
ActiveSonarrBlock::AddSeriesPrompt => { ActiveSonarrBlock::AddSeriesPrompt
if self.app.data.sonarr_data.selected_block.get_active_block() if self.app.data.sonarr_data.selected_block.get_active_block()
== ActiveSonarrBlock::AddSeriesConfirmPrompt == ActiveSonarrBlock::AddSeriesConfirmPrompt
&& matches_key!(confirm, key) && matches_key!(confirm, key) =>
{ {
self.app.data.sonarr_data.prompt_confirm = true; self.app.data.sonarr_data.prompt_confirm = true;
self.app.data.sonarr_data.prompt_confirm_action = self.app.data.sonarr_data.prompt_confirm_action =
Some(SonarrEvent::AddSeries(self.build_add_series_body())); Some(SonarrEvent::AddSeries(self.build_add_series_body()));
self.app.pop_navigation_stack(); self.app.pop_navigation_stack();
}
} }
_ => (), _ => (),
} }
@@ -450,18 +450,17 @@ impl<'a, 'b> KeyEventHandler<'a, 'b, ActiveSonarrBlock> for EditSeriesHandler<'a
.tags .tags
) )
} }
ActiveSonarrBlock::EditSeriesPrompt => { ActiveSonarrBlock::EditSeriesPrompt
if self.app.data.sonarr_data.selected_block.get_active_block() if self.app.data.sonarr_data.selected_block.get_active_block()
== ActiveSonarrBlock::EditSeriesConfirmPrompt == ActiveSonarrBlock::EditSeriesConfirmPrompt
&& matches_key!(confirm, key) && matches_key!(confirm, key) =>
{ {
self.app.data.sonarr_data.prompt_confirm = true; self.app.data.sonarr_data.prompt_confirm = true;
self.app.data.sonarr_data.prompt_confirm_action = self.app.data.sonarr_data.prompt_confirm_action =
Some(SonarrEvent::EditSeries(self.build_edit_series_params())); Some(SonarrEvent::EditSeries(self.build_edit_series_params()));
self.app.should_refresh = true; self.app.should_refresh = true;
self.app.pop_navigation_stack(); self.app.pop_navigation_stack();
}
} }
_ => (), _ => (),
} }
+1 -1
View File
@@ -893,7 +893,7 @@ mod tests {
app.push_navigation_stack(ActiveRadarrBlock::MoviesSortPrompt.into()); app.push_navigation_stack(ActiveRadarrBlock::MoviesSortPrompt.into());
let mut expected_vec = movies_vec(); let mut expected_vec = movies_vec();
expected_vec.sort_by(|a, b| a.id.cmp(&b.id)); expected_vec.sort_by_key(|a| a.id);
expected_vec.reverse(); expected_vec.reverse();
TableHandlerUnit::new( TableHandlerUnit::new(
+1 -1
View File
@@ -82,7 +82,7 @@ impl Network<'_, '_> {
Route::Lidarr(ActiveLidarrBlock::BlocklistSortPrompt, _) Route::Lidarr(ActiveLidarrBlock::BlocklistSortPrompt, _)
) { ) {
let mut blocklist_vec: Vec<BlocklistItem> = blocklist_resp.records; let mut blocklist_vec: Vec<BlocklistItem> = blocklist_resp.records;
blocklist_vec.sort_by(|a, b| a.id.cmp(&b.id)); blocklist_vec.sort_by_key(|a| a.id);
app.data.lidarr_data.blocklist.set_items(blocklist_vec); app.data.lidarr_data.blocklist.set_items(blocklist_vec);
app.data.lidarr_data.blocklist.apply_sorting_toggle(false); app.data.lidarr_data.blocklist.apply_sorting_toggle(false);
} }
+1 -1
View File
@@ -31,7 +31,7 @@ impl Network<'_, '_> {
Route::Lidarr(ActiveLidarrBlock::HistorySortPrompt, _) Route::Lidarr(ActiveLidarrBlock::HistorySortPrompt, _)
) { ) {
let mut history_vec = history_response.records; let mut history_vec = history_response.records;
history_vec.sort_by(|a, b| a.id.cmp(&b.id)); history_vec.sort_by_key(|a| a.id);
app.data.lidarr_data.history.set_items(history_vec); app.data.lidarr_data.history.set_items(history_vec);
app.data.lidarr_data.history.apply_sorting_toggle(false); app.data.lidarr_data.history.apply_sorting_toggle(false);
} }
@@ -33,7 +33,7 @@ impl Network<'_, '_> {
self self
.handle_request::<(), Vec<Album>>(request_props, |mut albums_vec, mut app| { .handle_request::<(), Vec<Album>>(request_props, |mut albums_vec, mut app| {
albums_vec.sort_by(|a, b| a.id.cmp(&b.id)); albums_vec.sort_by_key(|a| a.id);
app.data.lidarr_data.albums.set_items(albums_vec); app.data.lidarr_data.albums.set_items(albums_vec);
}) })
.await .await
@@ -89,7 +89,7 @@ impl Network<'_, '_> {
.get_or_insert_default(); .get_or_insert_default();
let mut history_vec = history_items; let mut history_vec = history_items;
history_vec.sort_by(|a, b| a.id.cmp(&b.id)); history_vec.sort_by_key(|a| a.id);
album_details_modal.album_history.set_items(history_vec); album_details_modal.album_history.set_items(history_vec);
album_details_modal album_details_modal
.album_history .album_history
@@ -64,7 +64,7 @@ impl Network<'_, '_> {
app.get_current_route(), app.get_current_route(),
Route::Lidarr(ActiveLidarrBlock::ArtistsSortPrompt, _) Route::Lidarr(ActiveLidarrBlock::ArtistsSortPrompt, _)
) { ) {
artists_vec.sort_by(|a, b| a.id.cmp(&b.id)); artists_vec.sort_by_key(|a| a.id);
app.data.lidarr_data.artists.set_items(artists_vec); app.data.lidarr_data.artists.set_items(artists_vec);
app.data.lidarr_data.artists.apply_sorting_toggle(false); app.data.lidarr_data.artists.apply_sorting_toggle(false);
} }
@@ -309,7 +309,7 @@ impl Network<'_, '_> {
let artist_history = &mut app.data.lidarr_data.artist_history; let artist_history = &mut app.data.lidarr_data.artist_history;
if !is_sorting { if !is_sorting {
history_vec.sort_by(|a, b| a.id.cmp(&b.id)); history_vec.sort_by_key(|a| a.id);
artist_history.set_items(history_vec); artist_history.set_items(history_vec);
artist_history.apply_sorting_toggle(false); artist_history.apply_sorting_toggle(false);
} }
@@ -55,7 +55,7 @@ impl Network<'_, '_> {
self self
.handle_request::<(), Vec<Track>>(request_props, |mut track_vec, mut app| { .handle_request::<(), Vec<Track>>(request_props, |mut track_vec, mut app| {
track_vec.sort_by(|a, b| a.id.cmp(&b.id)); track_vec.sort_by_key(|a| a.id);
let album_details_modal = app let album_details_modal = app
.data .data
.lidarr_data .lidarr_data
@@ -238,7 +238,7 @@ impl Network<'_, '_> {
.into_iter() .into_iter()
.filter(|it| it.track_id == track_id) .filter(|it| it.track_id == track_id)
.collect(); .collect();
history_vec.sort_by(|a, b| a.id.cmp(&b.id)); history_vec.sort_by_key(|a| a.id);
track_details_modal.track_history.set_items(history_vec); track_details_modal.track_history.set_items(history_vec);
track_details_modal track_details_modal
.track_history .track_history
+2 -5
View File
@@ -50,7 +50,7 @@ impl Network<'_, '_> {
let log_lines = logs let log_lines = logs
.into_iter() .into_iter()
.map(|log| { .map(|log| {
if log.exception.is_some() { if let Some(exception) = log.exception {
HorizontallyScrollableText::from(format!( HorizontallyScrollableText::from(format!(
"{}|{}|{}|{}|{}", "{}|{}|{}|{}|{}",
log.time, log.time,
@@ -63,10 +63,7 @@ impl Network<'_, '_> {
.exception_type .exception_type
.as_ref() .as_ref()
.expect("exception_type must exist when exception is present"), .expect("exception_type must exist when exception is present"),
log exception
.exception
.as_ref()
.expect("exception must exist in this branch")
)) ))
} else { } else {
HorizontallyScrollableText::from(format!( HorizontallyScrollableText::from(format!(
+1 -1
View File
@@ -83,7 +83,7 @@ impl Network<'_, '_> {
Route::Radarr(ActiveRadarrBlock::BlocklistSortPrompt, _) Route::Radarr(ActiveRadarrBlock::BlocklistSortPrompt, _)
) { ) {
let mut blocklist_vec = blocklist_resp.records; let mut blocklist_vec = blocklist_resp.records;
blocklist_vec.sort_by(|a, b| a.id.cmp(&b.id)); blocklist_vec.sort_by_key(|a| a.id);
app.data.radarr_data.blocklist.set_items(blocklist_vec); app.data.radarr_data.blocklist.set_items(blocklist_vec);
app.data.radarr_data.blocklist.apply_sorting_toggle(false); app.data.radarr_data.blocklist.apply_sorting_toggle(false);
} }
@@ -128,7 +128,7 @@ impl Network<'_, '_> {
app.get_current_route(), app.get_current_route(),
Route::Radarr(ActiveRadarrBlock::CollectionsSortPrompt, _) Route::Radarr(ActiveRadarrBlock::CollectionsSortPrompt, _)
) { ) {
collections_vec.sort_by(|a, b| a.id.cmp(&b.id)); collections_vec.sort_by_key(|a| a.id);
app.data.radarr_data.collections.set_items(collections_vec); app.data.radarr_data.collections.set_items(collections_vec);
app.data.radarr_data.collections.apply_sorting_toggle(false); app.data.radarr_data.collections.apply_sorting_toggle(false);
} }
+1 -1
View File
@@ -31,7 +31,7 @@ impl Network<'_, '_> {
Route::Radarr(ActiveRadarrBlock::HistorySortPrompt, _) Route::Radarr(ActiveRadarrBlock::HistorySortPrompt, _)
) { ) {
let mut history_vec = history_response.records; let mut history_vec = history_response.records;
history_vec.sort_by(|a, b| a.id.cmp(&b.id)); history_vec.sort_by_key(|a| a.id);
app.data.radarr_data.history.set_items(history_vec); app.data.radarr_data.history.set_items(history_vec);
app.data.radarr_data.history.apply_sorting_toggle(false); app.data.radarr_data.history.apply_sorting_toggle(false);
} }
+1 -1
View File
@@ -270,7 +270,7 @@ impl Network<'_, '_> {
app.get_current_route(), app.get_current_route(),
Route::Radarr(ActiveRadarrBlock::MoviesSortPrompt, _) Route::Radarr(ActiveRadarrBlock::MoviesSortPrompt, _)
) { ) {
movie_vec.sort_by(|a, b| a.id.cmp(&b.id)); movie_vec.sort_by_key(|a| a.id);
app.data.radarr_data.movies.set_items(movie_vec); app.data.radarr_data.movies.set_items(movie_vec);
app.data.radarr_data.movies.apply_sorting_toggle(false); app.data.radarr_data.movies.apply_sorting_toggle(false);
} }
+2 -5
View File
@@ -67,7 +67,7 @@ impl Network<'_, '_> {
let log_lines = logs let log_lines = logs
.into_iter() .into_iter()
.map(|log| { .map(|log| {
if log.exception.is_some() { if let Some(exception) = log.exception {
HorizontallyScrollableText::from(format!( HorizontallyScrollableText::from(format!(
"{}|{}|{}|{}|{}", "{}|{}|{}|{}|{}",
log.time, log.time,
@@ -80,10 +80,7 @@ impl Network<'_, '_> {
.exception_type .exception_type
.as_ref() .as_ref()
.expect("exception_type must exist when exception is present"), .expect("exception_type must exist when exception is present"),
log exception
.exception
.as_ref()
.expect("exception must exist in this branch")
)) ))
} else { } else {
HorizontallyScrollableText::from(format!( HorizontallyScrollableText::from(format!(
+1 -1
View File
@@ -102,7 +102,7 @@ impl Network<'_, '_> {
} }
}) })
.collect(); .collect();
blocklist_vec.sort_by(|a, b| a.id.cmp(&b.id)); blocklist_vec.sort_by_key(|a| a.id);
app.data.sonarr_data.blocklist.set_items(blocklist_vec); app.data.sonarr_data.blocklist.set_items(blocklist_vec);
app.data.sonarr_data.blocklist.apply_sorting_toggle(false); app.data.sonarr_data.blocklist.apply_sorting_toggle(false);
} }
+1 -1
View File
@@ -31,7 +31,7 @@ impl Network<'_, '_> {
Route::Sonarr(ActiveSonarrBlock::HistorySortPrompt, _) Route::Sonarr(ActiveSonarrBlock::HistorySortPrompt, _)
) { ) {
let mut history_vec = history_response.records; let mut history_vec = history_response.records;
history_vec.sort_by(|a, b| a.id.cmp(&b.id)); history_vec.sort_by_key(|a| a.id);
app.data.sonarr_data.history.set_items(history_vec); app.data.sonarr_data.history.set_items(history_vec);
app.data.sonarr_data.history.apply_sorting_toggle(false); app.data.sonarr_data.history.apply_sorting_toggle(false);
} }
@@ -60,7 +60,7 @@ impl Network<'_, '_> {
self self
.handle_request::<(), Vec<Episode>>(request_props, |mut episode_vec, mut app| { .handle_request::<(), Vec<Episode>>(request_props, |mut episode_vec, mut app| {
episode_vec.sort_by(|a, b| a.id.cmp(&b.id)); episode_vec.sort_by_key(|a| a.id);
if !matches!( if !matches!(
app.get_current_route(), app.get_current_route(),
Route::Sonarr(ActiveSonarrBlock::EpisodesSortPrompt, _) Route::Sonarr(ActiveSonarrBlock::EpisodesSortPrompt, _)
@@ -151,7 +151,7 @@ impl Network<'_, '_> {
.get_or_insert_default(); .get_or_insert_default();
let mut history_vec = history_response.records; let mut history_vec = history_response.records;
history_vec.sort_by(|a, b| a.id.cmp(&b.id)); history_vec.sort_by_key(|a| a.id);
episode_details_modal.episode_history.set_items(history_vec); episode_details_modal.episode_history.set_items(history_vec);
episode_details_modal episode_details_modal
.episode_history .episode_history
@@ -158,7 +158,7 @@ impl Network<'_, '_> {
if !is_sorting { if !is_sorting {
let mut history_vec = history_items; let mut history_vec = history_items;
history_vec.sort_by(|a, b| a.id.cmp(&b.id)); history_vec.sort_by_key(|a| a.id);
season_details_modal.season_history.set_items(history_vec); season_details_modal.season_history.set_items(history_vec);
season_details_modal season_details_modal
.season_history .season_history
@@ -315,7 +315,7 @@ impl Network<'_, '_> {
let series_history = app.data.sonarr_data.series_history.get_or_insert_default(); let series_history = app.data.sonarr_data.series_history.get_or_insert_default();
if !is_sorting { if !is_sorting {
history_vec.sort_by(|a, b| a.id.cmp(&b.id)); history_vec.sort_by_key(|a| a.id);
series_history.set_items(history_vec); series_history.set_items(history_vec);
series_history.apply_sorting_toggle(false); series_history.apply_sorting_toggle(false);
} }
@@ -337,7 +337,7 @@ impl Network<'_, '_> {
app.get_current_route(), app.get_current_route(),
Route::Sonarr(ActiveSonarrBlock::SeriesSortPrompt, _) Route::Sonarr(ActiveSonarrBlock::SeriesSortPrompt, _)
) { ) {
series_vec.sort_by(|a, b| a.id.cmp(&b.id)); series_vec.sort_by_key(|a| a.id);
app.data.sonarr_data.series.set_items(series_vec); app.data.sonarr_data.series.set_items(series_vec);
app.data.sonarr_data.series.apply_sorting_toggle(false); app.data.sonarr_data.series.apply_sorting_toggle(false);
} }
+2 -5
View File
@@ -50,7 +50,7 @@ impl Network<'_, '_> {
let log_lines = logs let log_lines = logs
.into_iter() .into_iter()
.map(|log| { .map(|log| {
if log.exception.is_some() { if let Some(exception) = log.exception {
HorizontallyScrollableText::from(format!( HorizontallyScrollableText::from(format!(
"{}|{}|{}|{}|{}", "{}|{}|{}|{}|{}",
log.time, log.time,
@@ -63,10 +63,7 @@ impl Network<'_, '_> {
.exception_type .exception_type
.as_ref() .as_ref()
.expect("exception_type must exist when exception is present"), .expect("exception_type must exist when exception is present"),
log exception
.exception
.as_ref()
.expect("exception must exist in this branch")
)) ))
} else { } else {
HorizontallyScrollableText::from(format!( HorizontallyScrollableText::from(format!(
+18 -21
View File
@@ -52,33 +52,30 @@ impl DrawUi for IndexersUi {
_ if TestAllIndexersUi::accepts(route) => TestAllIndexersUi::draw(f, app, area), _ if TestAllIndexersUi::accepts(route) => TestAllIndexersUi::draw(f, app, area),
Route::Lidarr(active_lidarr_block, _) => match active_lidarr_block { Route::Lidarr(active_lidarr_block, _) => match active_lidarr_block {
ActiveLidarrBlock::TestIndexer => { ActiveLidarrBlock::TestIndexer => {
if app.is_loading || app.data.lidarr_data.indexer_test_errors.is_none() { if let Some(result) = app
.data
.lidarr_data
.indexer_test_errors
.as_ref()
.filter(|_| !app.is_loading)
{
let popup = if !result.is_empty() {
Popup::new(Message::new(result.clone())).size(Size::LargeMessage)
} else {
let message = Message::new("Indexer test succeeded!")
.title("Success")
.style(success_style().bold());
Popup::new(message).size(Size::Message)
};
f.render_widget(popup, f.area());
} else {
let loading_popup = Popup::new(LoadingBlock::new( let loading_popup = Popup::new(LoadingBlock::new(
app.is_loading || app.data.lidarr_data.indexer_test_errors.is_none(), app.is_loading || app.data.lidarr_data.indexer_test_errors.is_none(),
title_block("Testing Indexer"), title_block("Testing Indexer"),
)) ))
.size(Size::LargeMessage); .size(Size::LargeMessage);
f.render_widget(loading_popup, f.area()); f.render_widget(loading_popup, f.area());
} else {
let popup = {
let result = app
.data
.lidarr_data
.indexer_test_errors
.as_ref()
.expect("Test result is unpopulated");
if !result.is_empty() {
Popup::new(Message::new(result.clone())).size(Size::LargeMessage)
} else {
let message = Message::new("Indexer test succeeded!")
.title("Success")
.style(success_style().bold());
Popup::new(message).size(Size::Message)
}
};
f.render_widget(popup, f.area());
} }
} }
ActiveLidarrBlock::DeleteIndexerPrompt => { ActiveLidarrBlock::DeleteIndexerPrompt => {
+4 -5
View File
@@ -107,9 +107,8 @@ pub(super) fn draw_queued_events(f: &mut Frame<'_>, app: &mut App<'_>, area: Rec
} else { } else {
queued queued
}; };
let started_string = if event.started.is_some() { let started_string = if let Some(date_time) = event.started {
let started = let started = convert_to_minutes_hours_days(Utc::now().sub(date_time).num_minutes());
convert_to_minutes_hours_days(Utc::now().sub(event.started.unwrap()).num_minutes());
if started != "now" { if started != "now" {
format!("{started} ago") format!("{started} ago")
@@ -120,8 +119,8 @@ pub(super) fn draw_queued_events(f: &mut Frame<'_>, app: &mut App<'_>, area: Rec
String::new() String::new()
}; };
let duration = if event.duration.is_some() { let duration = if let Some(dur) = &event.duration {
&event.duration.as_ref().unwrap()[..8] &dur[..8]
} else { } else {
"" ""
}; };
+18 -21
View File
@@ -52,33 +52,30 @@ impl DrawUi for IndexersUi {
_ if TestAllIndexersUi::accepts(route) => TestAllIndexersUi::draw(f, app, area), _ if TestAllIndexersUi::accepts(route) => TestAllIndexersUi::draw(f, app, area),
Route::Radarr(active_radarr_block, _) => match active_radarr_block { Route::Radarr(active_radarr_block, _) => match active_radarr_block {
ActiveRadarrBlock::TestIndexer => { ActiveRadarrBlock::TestIndexer => {
if app.is_loading || app.data.radarr_data.indexer_test_errors.is_none() { if let Some(result) = app
.data
.radarr_data
.indexer_test_errors
.as_ref()
.filter(|_| !app.is_loading)
{
let popup = if !result.is_empty() {
Popup::new(Message::new(result.clone())).size(Size::LargeMessage)
} else {
let message = Message::new("Indexer test succeeded!")
.title("Success")
.style(success_style().bold());
Popup::new(message).size(Size::Message)
};
f.render_widget(popup, f.area());
} else {
let loading_popup = Popup::new(LoadingBlock::new( let loading_popup = Popup::new(LoadingBlock::new(
app.is_loading || app.data.radarr_data.indexer_test_errors.is_none(), app.is_loading || app.data.radarr_data.indexer_test_errors.is_none(),
title_block("Testing Indexer"), title_block("Testing Indexer"),
)) ))
.size(Size::LargeMessage); .size(Size::LargeMessage);
f.render_widget(loading_popup, f.area()); f.render_widget(loading_popup, f.area());
} else {
let popup = {
let result = app
.data
.radarr_data
.indexer_test_errors
.as_ref()
.expect("Test result is unpopulated");
if !result.is_empty() {
Popup::new(Message::new(result.clone())).size(Size::LargeMessage)
} else {
let message = Message::new("Indexer test succeeded!")
.title("Success")
.style(success_style().bold());
Popup::new(message).size(Size::Message)
}
};
f.render_widget(popup, f.area());
} }
} }
ActiveRadarrBlock::DeleteIndexerPrompt => { ActiveRadarrBlock::DeleteIndexerPrompt => {
+2 -3
View File
@@ -114,9 +114,8 @@ pub(super) fn draw_queued_events(f: &mut Frame<'_>, app: &mut App<'_>, area: Rec
} else { } else {
queued queued
}; };
let started_string = if event.started.is_some() { let started_string = if let Some(date_time) = event.started {
let started = let started = convert_to_minutes_hours_days(Utc::now().sub(date_time).num_minutes());
convert_to_minutes_hours_days(Utc::now().sub(event.started.unwrap()).num_minutes());
if started != "now" { if started != "now" {
format!("{started} ago") format!("{started} ago")
+18 -21
View File
@@ -52,33 +52,30 @@ impl DrawUi for IndexersUi {
_ if TestAllIndexersUi::accepts(route) => TestAllIndexersUi::draw(f, app, area), _ if TestAllIndexersUi::accepts(route) => TestAllIndexersUi::draw(f, app, area),
Route::Sonarr(active_sonarr_block, _) => match active_sonarr_block { Route::Sonarr(active_sonarr_block, _) => match active_sonarr_block {
ActiveSonarrBlock::TestIndexer => { ActiveSonarrBlock::TestIndexer => {
if app.is_loading || app.data.sonarr_data.indexer_test_errors.is_none() { if let Some(result) = app
.data
.sonarr_data
.indexer_test_errors
.as_ref()
.filter(|_| !app.is_loading)
{
let popup = if !result.is_empty() {
Popup::new(Message::new(result.clone())).size(Size::LargeMessage)
} else {
let message = Message::new("Indexer test succeeded!")
.title("Success")
.style(success_style().bold());
Popup::new(message).size(Size::Message)
};
f.render_widget(popup, f.area());
} else {
let loading_popup = Popup::new(LoadingBlock::new( let loading_popup = Popup::new(LoadingBlock::new(
app.is_loading || app.data.sonarr_data.indexer_test_errors.is_none(), app.is_loading || app.data.sonarr_data.indexer_test_errors.is_none(),
title_block("Testing Indexer"), title_block("Testing Indexer"),
)) ))
.size(Size::LargeMessage); .size(Size::LargeMessage);
f.render_widget(loading_popup, f.area()); f.render_widget(loading_popup, f.area());
} else {
let popup = {
let result = app
.data
.sonarr_data
.indexer_test_errors
.as_ref()
.expect("Test result is unpopulated");
if !result.is_empty() {
Popup::new(Message::new(result.clone())).size(Size::LargeMessage)
} else {
let message = Message::new("Indexer test succeeded!")
.title("Success")
.style(success_style().bold());
Popup::new(message).size(Size::Message)
}
};
f.render_widget(popup, f.area());
} }
} }
ActiveSonarrBlock::DeleteIndexerPrompt => { ActiveSonarrBlock::DeleteIndexerPrompt => {
+4 -5
View File
@@ -107,9 +107,8 @@ pub(super) fn draw_queued_events(f: &mut Frame<'_>, app: &mut App<'_>, area: Rec
} else { } else {
queued queued
}; };
let started_string = if event.started.is_some() { let started_string = if let Some(date_time) = event.started {
let started = let started = convert_to_minutes_hours_days(Utc::now().sub(date_time).num_minutes());
convert_to_minutes_hours_days(Utc::now().sub(event.started.unwrap()).num_minutes());
if started != "now" { if started != "now" {
format!("{started} ago") format!("{started} ago")
@@ -120,8 +119,8 @@ pub(super) fn draw_queued_events(f: &mut Frame<'_>, app: &mut App<'_>, area: Rec
String::new() String::new()
}; };
let duration = if event.duration.is_some() { let duration = if let Some(dur) = &event.duration {
&event.duration.as_ref().unwrap()[..8] &dur[..8]
} else { } else {
"" ""
}; };
+7 -9
View File
@@ -125,11 +125,8 @@ where
if let Some(content) = self.content if let Some(content) = self.content
&& !self.is_loading && !self.is_loading
{ {
let (table_contents, table_state) = if content.filtered_items.is_some() { let (table_contents, table_state) = if let Some(items) = &content.filtered_items {
( (items, content.filtered_state.as_mut().unwrap())
content.filtered_items.as_ref().unwrap(),
content.filtered_state.as_mut().unwrap(),
)
} else { } else {
(&content.items, &mut content.state) (&content.items, &mut content.state)
}; };
@@ -153,10 +150,11 @@ where
StatefulWidget::render(table, table_area, buf, table_state); StatefulWidget::render(table, table_area, buf, table_state);
if content.sort.is_some() && self.is_sorting { if let Some(sort) = &mut content.sort
let selectable_list = SelectableList::new(content.sort.as_mut().unwrap(), |item| { && self.is_sorting
ListItem::new(Text::from(item.name)) {
}); let selectable_list =
SelectableList::new(sort, |item| ListItem::new(Text::from(item.name)));
Popup::new(selectable_list) Popup::new(selectable_list)
.dimensions(20, 50) .dimensions(20, 50)
.render(table_area, buf); .render(table_area, buf);
+55 -13
View File
@@ -10,7 +10,10 @@ use anyhow::{Context, anyhow};
use colored::Colorize; use colored::Colorize;
use indicatif::{ProgressBar, ProgressStyle}; use indicatif::{ProgressBar, ProgressStyle};
use log::{LevelFilter, error}; use log::{LevelFilter, error};
use log4rs::append::file::FileAppender; use log4rs::append::rolling_file::RollingFileAppender;
use log4rs::append::rolling_file::policy::compound::CompoundPolicy;
use log4rs::append::rolling_file::policy::compound::roll::fixed_window::FixedWindowRoller;
use log4rs::append::rolling_file::policy::compound::trigger::size::SizeTrigger;
use log4rs::config::{Appender, Root}; use log4rs::config::{Appender, Root};
use log4rs::encode::pattern::PatternEncoder; use log4rs::encode::pattern::PatternEncoder;
use regex::Regex; use regex::Regex;
@@ -47,12 +50,24 @@ pub fn get_log_path() -> PathBuf {
} }
pub fn init_logging_config() -> log4rs::Config { pub fn init_logging_config() -> log4rs::Config {
let logfile = FileAppender::builder() let log_path = get_log_path();
let archive_pattern = log_path
.with_file_name("managarr.{}.log")
.to_string_lossy()
.into_owned();
let trigger = SizeTrigger::new(10 * 1024 * 1024);
let roller = FixedWindowRoller::builder()
.build(&archive_pattern, 3)
.expect("Failed to build log roller");
let policy = CompoundPolicy::new(Box::new(trigger), Box::new(roller));
let logfile = RollingFileAppender::builder()
.encoder(Box::new(PatternEncoder::new( .encoder(Box::new(PatternEncoder::new(
"{d(%Y-%m-%d %H:%M:%S%.3f)(utc)} <{i}> [{l}] {f}:{L} - {m}{n}", "{d(%Y-%m-%d %H:%M:%S%.3f)(utc)} <{i}> [{l}] {f}:{L} - {m}{n}",
))) )))
.build(get_log_path()) .build(log_path, Box::new(policy))
.unwrap(); .expect("Failed to build rolling file appender");
log4rs::Config::builder() log4rs::Config::builder()
.appender(Appender::builder().build("logfile", Box::new(logfile))) .appender(Appender::builder().build("logfile", Box::new(logfile)))
@@ -89,16 +104,29 @@ pub async fn tail_logs(no_color: bool) -> Result<()> {
.seek(SeekFrom::End(0)) .seek(SeekFrom::End(0))
.with_context(|| "Unable to tail log file")?; .with_context(|| "Unable to tail log file")?;
let mut lines = reader.lines(); tokio::task::spawn_blocking(move || {
let mut line_buf = String::new();
tokio::spawn(async move {
loop { loop {
if let Some(Ok(line)) = lines.next() { line_buf.clear();
if no_color { match reader.read_line(&mut line_buf) {
println!("{line}"); Ok(0) => {
} else { if was_log_rotated(&file_path, &mut reader) {
let colored_line = colorize_log_line(&line, &re); continue;
println!("{colored_line}"); }
std::thread::sleep(Duration::from_millis(100));
}
Ok(_) => {
let line = line_buf.trim_end();
if no_color {
println!("{line}");
} else {
let colored_line = colorize_log_line(line, &re);
println!("{colored_line}");
}
}
Err(_) => {
std::thread::sleep(Duration::from_millis(100));
} }
} }
} }
@@ -106,6 +134,20 @@ pub async fn tail_logs(no_color: bool) -> Result<()> {
.await? .await?
} }
pub(crate) fn was_log_rotated(file_path: &PathBuf, reader: &mut BufReader<File>) -> bool {
let current_pos = reader.stream_position().unwrap_or(0);
let file_len = fs::metadata(file_path).map(|m| m.len()).unwrap_or(0);
if file_len < current_pos
&& let Ok(new_file) = File::open(file_path)
{
*reader = BufReader::new(new_file);
return true;
}
false
}
fn colorize_log_line(line: &str, re: &Regex) -> String { fn colorize_log_line(line: &str, re: &Regex) -> String {
if let Some(caps) = re.captures(line) { if let Some(caps) = re.captures(line) {
let level = &caps["level"]; let level = &caps["level"];
+55 -1
View File
@@ -1,8 +1,11 @@
#[cfg(test)] #[cfg(test)]
mod tests { mod tests {
use std::fs::{self, File};
use std::io::{BufRead, BufReader, Seek, SeekFrom, Write};
use pretty_assertions::assert_eq; use pretty_assertions::assert_eq;
use crate::utils::{convert_f64_to_gb, convert_runtime, convert_to_gb}; use crate::utils::{convert_f64_to_gb, convert_runtime, convert_to_gb, was_log_rotated};
#[test] #[test]
fn test_convert_to_gb() { fn test_convert_to_gb() {
@@ -23,4 +26,55 @@ mod tests {
assert_eq!(hours, 2); assert_eq!(hours, 2);
assert_eq!(minutes, 34); assert_eq!(minutes, 34);
} }
#[test]
fn test_was_log_rotated_returns_false_when_file_has_not_rotated() {
let path = std::env::temp_dir().join("managarr_test_no_rotation.log");
fs::write(&path, "line one\nline two\n").unwrap();
let file = File::open(&path).unwrap();
let mut reader = BufReader::new(file);
reader.seek(SeekFrom::End(0)).unwrap();
assert!(!was_log_rotated(&path, &mut reader));
fs::remove_file(&path).unwrap();
}
#[test]
fn test_was_log_rotated_returns_true_and_reopens_reader_after_rotation() {
let path = std::env::temp_dir().join("managarr_test_rotation.log");
fs::write(&path, "original content that is long enough\n").unwrap();
let file = File::open(&path).unwrap();
let mut reader = BufReader::new(file);
reader.seek(SeekFrom::End(0)).unwrap();
fs::write(&path, "new\n").unwrap();
assert!(was_log_rotated(&path, &mut reader));
let mut line = String::new();
reader.read_line(&mut line).unwrap();
assert_eq!(line, "new\n");
fs::remove_file(&path).unwrap();
}
#[test]
fn test_was_log_rotated_returns_false_when_file_grows() {
let path = std::env::temp_dir().join("managarr_test_growing.log");
fs::write(&path, "initial\n").unwrap();
let file = File::open(&path).unwrap();
let mut reader = BufReader::new(file);
reader.seek(SeekFrom::End(0)).unwrap();
let mut appender = fs::OpenOptions::new().append(true).open(&path).unwrap();
appender.write_all(b"more data\n").unwrap();
assert!(!was_log_rotated(&path, &mut reader));
fs::remove_file(&path).unwrap();
}
} }