refactor: Phase A — extraction des helpers partagés et micro-fixes
- Centralise remap_libraries_path/unmap_libraries_path dans crates/core/paths.rs (supprime 4 copies dupliquées dans API + indexer) - Centralise mode_to_interval_minutes/validate_schedule_mode dans crates/core/schedule.rs (remplace 8 match blocks + 4 validations inline) - Ajoute helpers env_or<T>/env_string_or dans config.rs, utilise ThumbnailConfig::default() comme base dans from_env() (élimine la duplication des valeurs par défaut) - Supprime std::mem::take inutile dans books.rs - Cible #[allow(dead_code)] sur le champ plutôt que le struct (metadata.rs) - Remplace eprintln! par tracing::warn! dans parsers - Fix clippy boolean logic bug dans prowlarr.rs 10 nouveaux tests unitaires (paths + schedule) Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -50,38 +50,33 @@ impl Default for ThumbnailConfig {
|
||||
}
|
||||
}
|
||||
|
||||
/// Parse an environment variable with a fallback default value.
|
||||
pub fn env_or<T: std::str::FromStr>(key: &str, default: T) -> T {
|
||||
std::env::var(key)
|
||||
.ok()
|
||||
.and_then(|v| v.parse().ok())
|
||||
.unwrap_or(default)
|
||||
}
|
||||
|
||||
/// Parse an environment variable as a String with a fallback default.
|
||||
pub fn env_string_or(key: &str, default: &str) -> String {
|
||||
std::env::var(key).unwrap_or_else(|_| default.to_string())
|
||||
}
|
||||
|
||||
impl IndexerConfig {
|
||||
pub fn from_env() -> Result<Self> {
|
||||
let thumbnail_config = ThumbnailConfig {
|
||||
enabled: std::env::var("THUMBNAIL_ENABLED")
|
||||
.ok()
|
||||
.and_then(|v| v.parse::<bool>().ok())
|
||||
.unwrap_or(true),
|
||||
width: std::env::var("THUMBNAIL_WIDTH")
|
||||
.ok()
|
||||
.and_then(|v| v.parse::<u32>().ok())
|
||||
.unwrap_or(300),
|
||||
height: std::env::var("THUMBNAIL_HEIGHT")
|
||||
.ok()
|
||||
.and_then(|v| v.parse::<u32>().ok())
|
||||
.unwrap_or(400),
|
||||
quality: std::env::var("THUMBNAIL_QUALITY")
|
||||
.ok()
|
||||
.and_then(|v| v.parse::<u8>().ok())
|
||||
.unwrap_or(80),
|
||||
format: std::env::var("THUMBNAIL_FORMAT").unwrap_or_else(|_| "webp".to_string()),
|
||||
directory: std::env::var("THUMBNAIL_DIRECTORY")
|
||||
.unwrap_or_else(|_| "/data/thumbnails".to_string()),
|
||||
};
|
||||
let mut thumbnail_config = ThumbnailConfig::default();
|
||||
thumbnail_config.enabled = env_or("THUMBNAIL_ENABLED", thumbnail_config.enabled);
|
||||
thumbnail_config.width = env_or("THUMBNAIL_WIDTH", thumbnail_config.width);
|
||||
thumbnail_config.height = env_or("THUMBNAIL_HEIGHT", thumbnail_config.height);
|
||||
thumbnail_config.quality = env_or("THUMBNAIL_QUALITY", thumbnail_config.quality);
|
||||
thumbnail_config.format = env_string_or("THUMBNAIL_FORMAT", &thumbnail_config.format);
|
||||
thumbnail_config.directory = env_string_or("THUMBNAIL_DIRECTORY", &thumbnail_config.directory);
|
||||
|
||||
Ok(Self {
|
||||
listen_addr: std::env::var("INDEXER_LISTEN_ADDR")
|
||||
.unwrap_or_else(|_| "0.0.0.0:7081".to_string()),
|
||||
listen_addr: env_string_or("INDEXER_LISTEN_ADDR", "0.0.0.0:7081"),
|
||||
database_url: std::env::var("DATABASE_URL").context("DATABASE_URL is required")?,
|
||||
scan_interval_seconds: std::env::var("INDEXER_SCAN_INTERVAL_SECONDS")
|
||||
.ok()
|
||||
.and_then(|v| v.parse::<u64>().ok())
|
||||
.unwrap_or(5),
|
||||
scan_interval_seconds: env_or("INDEXER_SCAN_INTERVAL_SECONDS", 5),
|
||||
thumbnail_config,
|
||||
})
|
||||
}
|
||||
|
||||
@@ -1 +1,3 @@
|
||||
pub mod config;
|
||||
pub mod paths;
|
||||
pub mod schedule;
|
||||
|
||||
93
crates/core/src/paths.rs
Normal file
93
crates/core/src/paths.rs
Normal file
@@ -0,0 +1,93 @@
|
||||
/// Remap a DB path (starting with `/libraries/`) to the local filesystem path
|
||||
/// using the `LIBRARIES_ROOT_PATH` environment variable.
|
||||
pub fn remap_libraries_path(path: &str) -> String {
|
||||
if let Ok(root) = std::env::var("LIBRARIES_ROOT_PATH") {
|
||||
if path.starts_with("/libraries/") {
|
||||
return path.replacen("/libraries", &root, 1);
|
||||
}
|
||||
}
|
||||
path.to_string()
|
||||
}
|
||||
|
||||
/// Convert a local filesystem path back to a DB path (starting with `/libraries/`).
|
||||
pub fn unmap_libraries_path(path: &str) -> String {
|
||||
if let Ok(root) = std::env::var("LIBRARIES_ROOT_PATH") {
|
||||
if path.starts_with(&root) {
|
||||
return path.replacen(&root, "/libraries", 1);
|
||||
}
|
||||
}
|
||||
path.to_string()
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use std::sync::Mutex;
|
||||
|
||||
// Serialize tests that modify env vars
|
||||
static ENV_LOCK: Mutex<()> = Mutex::new(());
|
||||
|
||||
#[test]
|
||||
fn remap_with_env_set() {
|
||||
let _lock = ENV_LOCK.lock().unwrap();
|
||||
std::env::set_var("LIBRARIES_ROOT_PATH", "/home/user/books");
|
||||
assert_eq!(
|
||||
remap_libraries_path("/libraries/BD/les geants/06. yatho.cbz"),
|
||||
"/home/user/books/BD/les geants/06. yatho.cbz"
|
||||
);
|
||||
std::env::remove_var("LIBRARIES_ROOT_PATH");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn remap_without_env() {
|
||||
let _lock = ENV_LOCK.lock().unwrap();
|
||||
std::env::remove_var("LIBRARIES_ROOT_PATH");
|
||||
assert_eq!(
|
||||
remap_libraries_path("/libraries/BD/test.cbz"),
|
||||
"/libraries/BD/test.cbz"
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn remap_non_matching_prefix() {
|
||||
let _lock = ENV_LOCK.lock().unwrap();
|
||||
std::env::set_var("LIBRARIES_ROOT_PATH", "/home/user/books");
|
||||
assert_eq!(
|
||||
remap_libraries_path("/other/path/file.cbz"),
|
||||
"/other/path/file.cbz"
|
||||
);
|
||||
std::env::remove_var("LIBRARIES_ROOT_PATH");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn unmap_with_env_set() {
|
||||
let _lock = ENV_LOCK.lock().unwrap();
|
||||
std::env::set_var("LIBRARIES_ROOT_PATH", "/home/user/books");
|
||||
assert_eq!(
|
||||
unmap_libraries_path("/home/user/books/BD/test.cbz"),
|
||||
"/libraries/BD/test.cbz"
|
||||
);
|
||||
std::env::remove_var("LIBRARIES_ROOT_PATH");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn unmap_without_env() {
|
||||
let _lock = ENV_LOCK.lock().unwrap();
|
||||
std::env::remove_var("LIBRARIES_ROOT_PATH");
|
||||
assert_eq!(
|
||||
unmap_libraries_path("/home/user/books/BD/test.cbz"),
|
||||
"/home/user/books/BD/test.cbz"
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn roundtrip() {
|
||||
let _lock = ENV_LOCK.lock().unwrap();
|
||||
std::env::set_var("LIBRARIES_ROOT_PATH", "/mnt/data");
|
||||
let original = "/libraries/manga/naruto/vol01.cbz";
|
||||
let remapped = remap_libraries_path(original);
|
||||
assert_eq!(remapped, "/mnt/data/manga/naruto/vol01.cbz");
|
||||
assert_eq!(unmap_libraries_path(&remapped), original);
|
||||
std::env::remove_var("LIBRARIES_ROOT_PATH");
|
||||
}
|
||||
}
|
||||
57
crates/core/src/schedule.rs
Normal file
57
crates/core/src/schedule.rs
Normal file
@@ -0,0 +1,57 @@
|
||||
/// Valid schedule modes for library monitoring.
|
||||
pub const VALID_SCHEDULE_MODES: &[&str] = &["manual", "hourly", "daily", "weekly"];
|
||||
|
||||
/// Convert a schedule mode to its interval in minutes.
|
||||
/// Returns the interval for known modes, defaults to daily (1440) for unknown values.
|
||||
pub fn mode_to_interval_minutes(mode: &str) -> i64 {
|
||||
match mode {
|
||||
"hourly" => 60,
|
||||
"daily" => 1440,
|
||||
"weekly" => 10080,
|
||||
_ => 1440,
|
||||
}
|
||||
}
|
||||
|
||||
/// Validate that a mode string is one of the accepted schedule modes.
|
||||
/// Returns an error message if invalid.
|
||||
pub fn validate_schedule_mode(mode: &str) -> Result<(), &'static str> {
|
||||
if VALID_SCHEDULE_MODES.contains(&mode) {
|
||||
Ok(())
|
||||
} else {
|
||||
Err("must be one of: manual, hourly, daily, weekly")
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn interval_known_modes() {
|
||||
assert_eq!(mode_to_interval_minutes("hourly"), 60);
|
||||
assert_eq!(mode_to_interval_minutes("daily"), 1440);
|
||||
assert_eq!(mode_to_interval_minutes("weekly"), 10080);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn interval_unknown_defaults_to_daily() {
|
||||
assert_eq!(mode_to_interval_minutes("manual"), 1440);
|
||||
assert_eq!(mode_to_interval_minutes("unknown"), 1440);
|
||||
assert_eq!(mode_to_interval_minutes(""), 1440);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn validate_accepts_valid() {
|
||||
assert!(validate_schedule_mode("manual").is_ok());
|
||||
assert!(validate_schedule_mode("hourly").is_ok());
|
||||
assert!(validate_schedule_mode("daily").is_ok());
|
||||
assert!(validate_schedule_mode("weekly").is_ok());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn validate_rejects_invalid() {
|
||||
assert!(validate_schedule_mode("monthly").is_err());
|
||||
assert!(validate_schedule_mode("").is_err());
|
||||
assert!(validate_schedule_mode("HOURLY").is_err());
|
||||
}
|
||||
}
|
||||
@@ -88,7 +88,7 @@ fn extract_series(path: &Path, library_root: &Path) -> Option<String> {
|
||||
} else if let Ok(relative) = parent.strip_prefix(library_root) {
|
||||
relative
|
||||
} else {
|
||||
eprintln!(
|
||||
tracing::warn!(
|
||||
"[PARSER] Cannot determine series: parent '{}' doesn't start with root '{}'",
|
||||
parent.display(),
|
||||
library_root.display()
|
||||
|
||||
Reference in New Issue
Block a user