Compare commits

...

10 Commits

15 changed files with 218 additions and 83 deletions
@@ -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:
bump: major
env: env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
CARGO_REGISTRY_TOKEN: ${{ secrets.CARGO_REGISTRY_TOKEN }} CARGO_REGISTRY_TOKEN: ${{ secrets.CARGO_REGISTRY_TOKEN }}
+38
View File
@@ -0,0 +1,38 @@
# Adapted from https://github.com/joshka/github-workflows/blob/main/.github/workflows/rust-release-plz.yml
# Thanks to joshka for permission to use this template!
name: Create minor release
permissions:
pull-requests: write
contents: write
on:
workflow_dispatch:
jobs:
release-plz:
# see https://release-plz.ieni.dev/docs/github
# for more information
name: Release-plz
runs-on: ubuntu-latest
steps:
- name: Check if actor is repository owner
if: ${{ github.actor != github.repository_owner }}
run: |
echo "You are not authorized to run this workflow."
exit 1
- name: Checkout
uses: actions/checkout@v4
with:
fetch-depth: 0
- name: Install Rust stable
uses: dtolnay/rust-toolchain@stable
- name: Run release-plz
uses: MarcoIeni/release-plz-action@v0.5
with:
bump: minor
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
CARGO_REGISTRY_TOKEN: ${{ secrets.CARGO_REGISTRY_TOKEN }}
+38
View File
@@ -0,0 +1,38 @@
# Adapted from https://github.com/joshka/github-workflows/blob/main/.github/workflows/rust-release-plz.yml
# Thanks to joshka for permission to use this template!
name: Create patch release
permissions:
pull-requests: write
contents: write
on:
workflow_dispatch:
jobs:
release-plz:
# see https://release-plz.ieni.dev/docs/github
# for more information
name: Release-plz
runs-on: ubuntu-latest
steps:
- name: Check if actor is repository owner
if: ${{ github.actor != github.repository_owner }}
run: |
echo "You are not authorized to run this workflow."
exit 1
- name: Checkout
uses: actions/checkout@v4
with:
fetch-depth: 0
- name: Install Rust stable
uses: dtolnay/rust-toolchain@stable
- name: Run release-plz
uses: MarcoIeni/release-plz-action@v0.5
with:
bump: patch
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
CARGO_REGISTRY_TOKEN: ${{ secrets.CARGO_REGISTRY_TOKEN }}
+10
View File
@@ -7,6 +7,16 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
## [Unreleased] ## [Unreleased]
## [0.2.1](https://github.com/Dark-Alex-17/managarr/compare/v0.2.0...v0.2.1) - 2024-11-06
### Other
- Removed the need for use_ssl to indicate SSL usage; instead just use the ssl_cert_path
- Applied bug fix to the downloads tab as well as the context [skip ci]
- Updated the README to not include the GitHub downloads badge since all binary releases are on crates.io [skip ci]
- Set all releases as manually triggered instead of automatic [skip ci]
- Updated dockerfile to no longer use the --disable-terminal-size-checks flag [skip ci]
## [0.1.5](https://github.com/Dark-Alex-17/managarr/compare/v0.1.4...v0.1.5) - 2024-11-03 ## [0.1.5](https://github.com/Dark-Alex-17/managarr/compare/v0.1.4...v0.1.5) - 2024-11-03
### Other ### Other
Generated
+1 -1
View File
@@ -1148,7 +1148,7 @@ dependencies = [
[[package]] [[package]]
name = "managarr" name = "managarr"
version = "0.2.0" version = "0.2.1"
dependencies = [ dependencies = [
"anyhow", "anyhow",
"assert_cmd", "assert_cmd",
+1 -1
View File
@@ -1,6 +1,6 @@
[package] [package]
name = "managarr" name = "managarr"
version = "0.2.0" version = "0.2.1"
authors = ["Alex Clarke <alex.j.tusa@gmail.com>"] authors = ["Alex Clarke <alex.j.tusa@gmail.com>"]
description = "A TUI and CLI to manage your Servarrs" description = "A TUI and CLI to manage your Servarrs"
keywords = ["managarr", "ratatui", "dashboard", "servarr", "tui"] keywords = ["managarr", "ratatui", "dashboard", "servarr", "tui"]
+1 -1
View File
@@ -23,4 +23,4 @@ FROM debian:stable-slim
# Copy the compiled binary from the builder container # Copy the compiled binary from the builder container
COPY --from=builder --chown=nonroot:nonroot /usr/src/managarr-temp/managarr /usr/local/bin COPY --from=builder --chown=nonroot:nonroot /usr/src/managarr-temp/managarr /usr/local/bin
ENTRYPOINT [ "/usr/local/bin/managarr", "--disable-terminal-size-checks" ] ENTRYPOINT [ "/usr/local/bin/managarr" ]
+11 -20
View File
@@ -2,13 +2,11 @@
![check](https://github.com/Dark-Alex-17/managarr/actions/workflows/check.yml/badge.svg) ![check](https://github.com/Dark-Alex-17/managarr/actions/workflows/check.yml/badge.svg)
![test](https://github.com/Dark-Alex-17/managarr/actions/workflows/test.yml/badge.svg) ![test](https://github.com/Dark-Alex-17/managarr/actions/workflows/test.yml/badge.svg)
![test](https://github.com/Dark-Alex-17/managarr/actions/workflows/release.yml/badge.svg)
![License](https://img.shields.io/badge/license-MIT-blueviolet.svg) ![License](https://img.shields.io/badge/license-MIT-blueviolet.svg)
![LOC](https://tokei.rs/b1/github/Dark-Alex-17/managarr?category=code) ![LOC](https://tokei.rs/b1/github/Dark-Alex-17/managarr?category=code)
[![crates.io link](https://img.shields.io/crates/v/managarr.svg)](https://crates.io/crates/managarr) [![crates.io link](https://img.shields.io/crates/v/managarr.svg)](https://crates.io/crates/managarr)
![Release](https://img.shields.io/github/v/release/Dark-Alex-17/managarr?color=%23c694ff) ![Release](https://img.shields.io/github/v/release/Dark-Alex-17/managarr?color=%23c694ff)
[![codecov](https://codecov.io/gh/Dark-Alex-17/managarr/graph/badge.svg?token=33G179TW67)](https://codecov.io/gh/Dark-Alex-17/managarr) [![codecov](https://codecov.io/gh/Dark-Alex-17/managarr/graph/badge.svg?token=33G179TW67)](https://codecov.io/gh/Dark-Alex-17/managarr)
[![GitHub Downloads](https://img.shields.io/github/downloads/Dark-Alex-17/managarr/total.svg?label=GitHub%20downloads)](https://github.com/Dark-Alex-17/managarr/releases)
![Crate.io downloads](https://img.shields.io/crates/d/managarr?label=Crate%20downloads) ![Crate.io downloads](https://img.shields.io/crates/d/managarr?label=Crate%20downloads)
Managarr is a TUI and CLI to help you manage your HTPC (Home Theater PC). Built with 🤎 in Rust! Managarr is a TUI and CLI to help you manage your HTPC (Home Theater PC). Built with 🤎 in Rust!
@@ -113,7 +111,7 @@ To see all available commands, simply run `managarr --help`:
```shell ```shell
$ managarr --help $ managarr --help
managarr 0.1.5 managarr 0.2.1
Alex Clarke <alex.j.tusa@gmail.com> Alex Clarke <alex.j.tusa@gmail.com>
A TUI and CLI to manage your Servarrs A TUI and CLI to manage your Servarrs
@@ -201,45 +199,38 @@ managarr --config /path/to/config.yml
### Example Configuration: ### Example Configuration:
```yaml ```yaml
radarr: radarr:
host: 127.0.0.1 host: 192.168.0.78
port: 7878 port: 7878
api_token: someApiToken1234567890 api_token: someApiToken1234567890
use_ssl: true ssl_cert_path: /path/to/radarr.crt # Required to enable SSL
ssl_cert_path: /path/to/radarr.crt
sonarr: sonarr:
host: 127.0.0.1 uri: http://htpc.local/sonarr # Example of using the 'uri' key instead of 'host' and 'port'
port: 8989
api_token: someApiToken1234567890 api_token: someApiToken1234567890
readarr: readarr:
host: 127.0.0.1 host: 192.168.0.87
port: 8787 port: 8787
api_token: someApiToken1234567890 api_token: someApiToken1234567890
use_ssl: false
lidarr: lidarr:
host: 127.0.0.1 host: 192.168.0.86
port: 8686 port: 8686
api_token: someApiToken1234567890 api_token: someApiToken1234567890
use_ssl: false
whisparr: whisparr:
host: 127.0.0.1 host: 192.168.0.69
port: 6969 port: 6969
api_token: someApiToken1234567890 api_token: someApiToken1234567890
use_ssl: false ssl_cert_path: /path/to/whisparr.crt
bazarr: bazarr:
host: 127.0.0.1 host: 192.168.0.67
port: 6767 port: 6767
api_token: someApiToken1234567890 api_token: someApiToken1234567890
use_ssl: false
prowlarr: prowlarr:
host: 127.0.0.1 host: 192.168.0.96
port: 9696 port: 9696
api_token: someApiToken1234567890 api_token: someApiToken1234567890
use_ssl: false
tautulli: tautulli:
host: 127.0.0.1 host: 192.168.0.81
port: 8181 port: 8181
api_token: someApiToken1234567890 api_token: someApiToken1234567890
use_ssl: false
``` ```
## Environment Variables ## Environment Variables
+3 -3
View File
@@ -1,7 +1,7 @@
#[cfg(test)] #[cfg(test)]
mod tests { mod tests {
use anyhow::anyhow; use anyhow::anyhow;
use pretty_assertions::{assert_eq, assert_str_eq}; use pretty_assertions::assert_eq;
use tokio::sync::mpsc; use tokio::sync::mpsc;
use crate::app::context_clues::{build_context_clue_string, SERVARR_CONTEXT_CLUES}; use crate::app::context_clues::{build_context_clue_string, SERVARR_CONTEXT_CLUES};
@@ -221,10 +221,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);
} }
} }
+31 -5
View File
@@ -1,4 +1,7 @@
use std::process;
use anyhow::anyhow; use anyhow::anyhow;
use colored::Colorize;
use log::{debug, error}; use log::{debug, error};
use serde::{Deserialize, Serialize}; use serde::{Deserialize, Serialize};
use tokio::sync::mpsc::Sender; use tokio::sync::mpsc::Sender;
@@ -166,29 +169,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());
}
+16 -22
View File
@@ -10,7 +10,7 @@ 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,
}; };
@@ -88,6 +88,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();
@@ -229,8 +230,8 @@ 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();
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 +245,25 @@ fn build_network_client(config: &AppConfig) -> Client {
} }
} }
fn create_cert(cert_path: Option<String>, servarr_name: &str) -> Certificate { fn create_cert(cert_path: &String, servarr_name: &str) -> Certificate {
let err = |error: String| { match fs::read(cert_path) {
error!("{}", error);
eprintln!("error: {}", error.red());
process::exit(1);
};
if cert_path.is_none() {
err(format!(
"A {} cert path is required when 'use_ssl' is 'true'",
servarr_name
));
}
match fs::read(cert_path.unwrap()) {
Ok(cert) => match Certificate::from_pem(&cert) { Ok(cert) => match Certificate::from_pem(&cert) {
Ok(certificate) => certificate, Ok(certificate) => certificate,
Err(_) => err(format!( Err(_) => {
log_and_print_error(format!(
"Unable to read the specified {} SSL certificate", "Unable to read the specified {} SSL certificate",
servarr_name servarr_name
)), ));
process::exit(1);
}
}, },
Err(_) => err(format!( Err(_) => {
log_and_print_error(format!(
"Unable to open specified {} SSL certificate", "Unable to open specified {} SSL certificate",
servarr_name servarr_name
)), ));
process::exit(1);
}
} }
} }
+2 -2
View File
@@ -26,7 +26,7 @@ mod tests {
.with_body("{}") .with_body("{}")
.create_async() .create_async()
.await; .await;
let host = server.host_with_port().split(':').collect::<Vec<&str>>()[0].to_owned(); let host = Some(server.host_with_port().split(':').collect::<Vec<&str>>()[0].to_owned());
let port = Some( let port = Some(
server.host_with_port().split(':').collect::<Vec<&str>>()[1] server.host_with_port().split(':').collect::<Vec<&str>>()[1]
.parse() .parse()
@@ -38,8 +38,8 @@ mod tests {
host, host,
api_token: String::new(), api_token: String::new(),
port, port,
use_ssl: false,
ssl_cert_path: None, ssl_cert_path: None,
..RadarrConfig::default()
}; };
app.config.radarr = radarr_config; app.config.radarr = radarr_config;
let app_arc = Arc::new(Mutex::new(app)); let app_arc = Arc::new(Mutex::new(app));
+14 -5
View File
@@ -2261,15 +2261,24 @@ impl<'a, 'b> Network<'a, 'b> {
let RadarrConfig { let RadarrConfig {
host, host,
port, port,
uri,
api_token, api_token,
use_ssl, ssl_cert_path,
..
} = &app.config.radarr; } = &app.config.radarr;
let protocol = if *use_ssl { "https" } else { "http" }; let uri = if let Some(radarr_uri) = uri {
let uri = format!( format!("{radarr_uri}/api/v3{resource}")
} else {
let protocol = if ssl_cert_path.is_some() {
"https"
} else {
"http"
};
let host = host.as_ref().unwrap();
format!(
"{protocol}://{host}:{}/api/v3{resource}", "{protocol}://{host}:{}/api/v3{resource}",
port.unwrap_or(7878) port.unwrap_or(7878)
); )
};
RequestProps { RequestProps {
uri, uri,
+31 -12
View File
@@ -794,7 +794,7 @@ mod test {
.match_header("X-Api-Key", "test1234"); .match_header("X-Api-Key", "test1234");
async_server = async_server.expect_at_most(0).create_async().await; async_server = async_server.expect_at_most(0).create_async().await;
let host = server.host_with_port().split(':').collect::<Vec<&str>>()[0].to_owned(); let host = Some(server.host_with_port().split(':').collect::<Vec<&str>>()[0].to_owned());
let port = Some( let port = Some(
server.host_with_port().split(':').collect::<Vec<&str>>()[1] server.host_with_port().split(':').collect::<Vec<&str>>()[1]
.parse() .parse()
@@ -805,8 +805,7 @@ mod test {
host, host,
port, port,
api_token: "test1234".to_owned(), api_token: "test1234".to_owned(),
use_ssl: false, ..RadarrConfig::default()
ssl_cert_path: None,
}; };
app.config.radarr = radarr_config; app.config.radarr = radarr_config;
let app_arc = Arc::new(Mutex::new(app)); let app_arc = Arc::new(Mutex::new(app));
@@ -4876,11 +4875,10 @@ mod test {
assert!(request_props.api_token.is_empty()); assert!(request_props.api_token.is_empty());
app_arc.lock().await.config.radarr = RadarrConfig { app_arc.lock().await.config.radarr = RadarrConfig {
host: "192.168.0.123".to_owned(), host: Some("192.168.0.123".to_owned()),
port: Some(8080), port: Some(8080),
api_token: "testToken1234".to_owned(), api_token: "testToken1234".to_owned(),
use_ssl: false, ..RadarrConfig::default()
ssl_cert_path: None,
}; };
} }
@@ -4889,11 +4887,33 @@ mod test {
let api_token = "testToken1234".to_owned(); let api_token = "testToken1234".to_owned();
let app_arc = Arc::new(Mutex::new(App::default())); let app_arc = Arc::new(Mutex::new(App::default()));
app_arc.lock().await.config.radarr = RadarrConfig { app_arc.lock().await.config.radarr = RadarrConfig {
host: "192.168.0.123".to_owned(), host: Some("192.168.0.123".to_owned()),
port: Some(8080), port: Some(8080),
api_token: api_token.clone(), api_token: api_token.clone(),
use_ssl: true, ssl_cert_path: Some("/test/cert.crt".to_owned()),
ssl_cert_path: None, ..RadarrConfig::default()
};
let network = Network::new(&app_arc, CancellationToken::new(), Client::new());
let request_props = network
.radarr_request_props_from("/test", RequestMethod::Get, None::<()>)
.await;
assert_str_eq!(request_props.uri, "https://192.168.0.123:8080/api/v3/test");
assert_eq!(request_props.method, RequestMethod::Get);
assert_eq!(request_props.body, None);
assert_str_eq!(request_props.api_token, api_token);
}
#[tokio::test]
async fn test_radarr_request_props_from_custom_radarr_config_using_uri_instead_of_host_and_port()
{
let api_token = "testToken1234".to_owned();
let app_arc = Arc::new(Mutex::new(App::default()));
app_arc.lock().await.config.radarr = RadarrConfig {
uri: Some("https://192.168.0.123:8080".to_owned()),
api_token: api_token.clone(),
..RadarrConfig::default()
}; };
let network = Network::new(&app_arc, CancellationToken::new(), Client::new()); let network = Network::new(&app_arc, CancellationToken::new(), Client::new());
@@ -4986,7 +5006,7 @@ mod test {
async_server = async_server.create_async().await; async_server = async_server.create_async().await;
let host = server.host_with_port().split(':').collect::<Vec<&str>>()[0].to_owned(); let host = Some(server.host_with_port().split(':').collect::<Vec<&str>>()[0].to_owned());
let port = Some( let port = Some(
server.host_with_port().split(':').collect::<Vec<&str>>()[1] server.host_with_port().split(':').collect::<Vec<&str>>()[1]
.parse() .parse()
@@ -4997,8 +5017,7 @@ mod test {
host, host,
port, port,
api_token: "test1234".to_owned(), api_token: "test1234".to_owned(),
use_ssl: false, ..RadarrConfig::default()
ssl_cert_path: None,
}; };
app.config.radarr = radarr_config; app.config.radarr = radarr_config;
let app_arc = Arc::new(Mutex::new(app)); let app_arc = Arc::new(Mutex::new(app));
+5 -1
View File
@@ -92,7 +92,11 @@ fn draw_downloads(f: &mut Frame<'_>, app: &mut App<'_>, area: Rect) {
); );
} }
let percent = 1f64 - (*sizeleft as f64 / *size as f64); let percent = if *size == 0 {
0.0
} else {
1f64 - (*sizeleft as f64 / *size as f64)
};
let file_size: f64 = convert_to_gb(*size); let file_size: f64 = convert_to_gb(*size);
Row::new(vec![ Row::new(vec![