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:
2026-03-29 11:54:03 +02:00
parent 776ef679c2
commit 38a0f56328
13 changed files with 206 additions and 159 deletions

View File

@@ -262,7 +262,7 @@ pub async fn list_books(
)?;
let total: i64 = count_row.get(0);
let mut items: Vec<BookItem> = rows
let items: Vec<BookItem> = rows
.iter()
.map(|row| {
let thumbnail_path: Option<String> = row.get("thumbnail_path");
@@ -288,7 +288,7 @@ pub async fn list_books(
.collect();
Ok(Json(BooksPage {
items: std::mem::take(&mut items),
items,
total,
page,
limit,
@@ -369,23 +369,8 @@ pub async fn get_book(
// ─── Helpers ──────────────────────────────────────────────────────────────────
pub(crate) 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()
}
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()
}
pub(crate) use stripstream_core::paths::remap_libraries_path;
pub(crate) use stripstream_core::paths::unmap_libraries_path;
// ─── Convert CBR → CBZ ───────────────────────────────────────────────────────

View File

@@ -314,59 +314,39 @@ pub async fn update_monitoring(
AxumPath(library_id): AxumPath<Uuid>,
Json(input): Json<UpdateMonitoringRequest>,
) -> Result<Json<LibraryResponse>, ApiError> {
use stripstream_core::schedule::{validate_schedule_mode, mode_to_interval_minutes};
// Validate scan_mode
let valid_modes = ["manual", "hourly", "daily", "weekly"];
if !valid_modes.contains(&input.scan_mode.as_str()) {
return Err(ApiError::bad_request("scan_mode must be one of: manual, hourly, daily, weekly"));
}
validate_schedule_mode(&input.scan_mode)
.map_err(|e| ApiError::bad_request(format!("scan_mode {e}")))?;
// Validate metadata_refresh_mode
let metadata_refresh_mode = input.metadata_refresh_mode.as_deref().unwrap_or("manual");
if !valid_modes.contains(&metadata_refresh_mode) {
return Err(ApiError::bad_request("metadata_refresh_mode must be one of: manual, hourly, daily, weekly"));
}
validate_schedule_mode(metadata_refresh_mode)
.map_err(|e| ApiError::bad_request(format!("metadata_refresh_mode {e}")))?;
// Validate download_detection_mode
let download_detection_mode = input.download_detection_mode.as_deref().unwrap_or("manual");
if !valid_modes.contains(&download_detection_mode) {
return Err(ApiError::bad_request("download_detection_mode must be one of: manual, hourly, daily, weekly"));
}
validate_schedule_mode(download_detection_mode)
.map_err(|e| ApiError::bad_request(format!("download_detection_mode {e}")))?;
// Calculate next_scan_at if monitoring is enabled
let next_scan_at = if input.monitor_enabled {
let interval_minutes = match input.scan_mode.as_str() {
"hourly" => 60,
"daily" => 1440,
"weekly" => 10080,
_ => 1440,
};
Some(chrono::Utc::now() + chrono::Duration::minutes(interval_minutes))
Some(chrono::Utc::now() + chrono::Duration::minutes(mode_to_interval_minutes(&input.scan_mode)))
} else {
None
};
// Calculate next_metadata_refresh_at
let next_metadata_refresh_at = if metadata_refresh_mode != "manual" {
let interval_minutes = match metadata_refresh_mode {
"hourly" => 60,
"daily" => 1440,
"weekly" => 10080,
_ => 1440,
};
Some(chrono::Utc::now() + chrono::Duration::minutes(interval_minutes))
Some(chrono::Utc::now() + chrono::Duration::minutes(mode_to_interval_minutes(metadata_refresh_mode)))
} else {
None
};
// Calculate next_download_detection_at
let next_download_detection_at = if download_detection_mode != "manual" {
let interval_minutes = match download_detection_mode {
"hourly" => 60,
"daily" => 1440,
"weekly" => 10080,
_ => 1440,
};
Some(chrono::Utc::now() + chrono::Duration::minutes(interval_minutes))
Some(chrono::Utc::now() + chrono::Duration::minutes(mode_to_interval_minutes(download_detection_mode)))
} else {
None
};
@@ -553,20 +533,14 @@ pub async fn update_reading_status_provider(
) -> Result<Json<serde_json::Value>, ApiError> {
let provider = input.reading_status_provider.as_deref().filter(|s| !s.is_empty());
let valid_modes = ["manual", "hourly", "daily", "weekly"];
use stripstream_core::schedule::{validate_schedule_mode, mode_to_interval_minutes};
let push_mode = input.reading_status_push_mode.as_deref().unwrap_or("manual");
if !valid_modes.contains(&push_mode) {
return Err(ApiError::bad_request("reading_status_push_mode must be one of: manual, hourly, daily, weekly"));
}
validate_schedule_mode(push_mode)
.map_err(|e| ApiError::bad_request(format!("reading_status_push_mode {e}")))?;
let next_push_at = if push_mode != "manual" {
let interval_minutes: i64 = match push_mode {
"hourly" => 60,
"daily" => 1440,
"weekly" => 10080,
_ => 1440,
};
Some(chrono::Utc::now() + chrono::Duration::minutes(interval_minutes))
Some(chrono::Utc::now() + chrono::Duration::minutes(mode_to_interval_minutes(push_mode)))
} else {
None
};

View File

@@ -38,7 +38,6 @@ pub struct SeriesCandidateDto {
}
#[derive(Deserialize, ToSchema)]
#[allow(dead_code)]
pub struct MetadataMatchRequest {
pub library_id: String,
pub series_name: String,
@@ -46,6 +45,7 @@ pub struct MetadataMatchRequest {
pub external_id: String,
pub external_url: Option<String>,
pub confidence: Option<f32>,
#[allow(dead_code)]
pub title: String,
pub metadata_json: serde_json::Value,
pub total_volumes: Option<i32>,

View File

@@ -21,14 +21,7 @@ use uuid::Uuid;
use crate::{error::ApiError, state::AppState};
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()
}
use stripstream_core::paths::remap_libraries_path;
fn parse_filter(s: &str) -> image::imageops::FilterType {
match s {

View File

@@ -289,9 +289,7 @@ fn extract_volumes_from_title(title: &str) -> Vec<i32> {
j += 1;
}
if j > 0 && j < chars.len() {
let valid_sep = chars[j] == '.'
|| chars[j] == ' '
|| (j + 2 < chars.len() && chars[j] == ' ' && chars[j + 1] == '-');
let valid_sep = chars[j] == '.' || chars[j] == ' ';
if valid_sep {
let num_str: String = chars[..j].iter().collect();
if let Ok(num) = num_str.parse::<i32>() {

View File

@@ -1024,23 +1024,7 @@ fn remap_downloads_path(path: &str) -> String {
path.to_string()
}
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()
}
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()
}
use stripstream_core::paths::{remap_libraries_path, unmap_libraries_path};
// ─── Naming helpers ───────────────────────────────────────────────────────────