cleanup: strip comments, extract constants, DRY shared helpers across adapters + infra-wiring

- strip all comments except WHY workaround notes (3 remain)
- remove all #[allow(dead_code)]; fix via _prefix rename
- extract named constants: JWT time units, token types, default config values, jellyfin fallback bitrate
- DRY: move serialize_enum_as_string, content_type_str, parse_content_type, parse_genres_blob to adapter-common
- sqlite+postgres library.rs use shared helpers instead of local copies
- sqlite+postgres channel.rs use shared serialize_enum_as_string
- remove dead `let _ = ext` in scanner.rs
This commit is contained in:
2026-07-12 04:21:21 +02:00
parent eff14228af
commit 25b33b6a0e
38 changed files with 188 additions and 630 deletions

View File

@@ -2,25 +2,21 @@ use std::path::Path;
use tokio::process::Command;
const VIDEO_EXTENSIONS: &[&str] = &["mp4", "mkv", "avi", "mov", "webm", "m4v"];
const ROOT_COLLECTION_NAME: &str = "__root__";
const YEAR_DIGITS: usize = 4;
const MIN_YEAR: u16 = 1900;
const MAX_YEAR: u16 = 2099;
/// In-memory representation of a scanned local video file.
#[derive(Debug, Clone)]
pub struct LocalFileItem {
/// Relative path from root, with forward slashes (used as the stable ID source).
pub rel_path: String,
pub title: String,
pub duration_secs: u32,
pub year: Option<u16>,
/// Ancestor directory names between root and file (excluding root itself).
pub tags: Vec<String>,
/// First path component under root (used as collection id/name).
pub top_dir: String,
}
/// Walk `root` and return all recognised video files with metadata.
///
/// ffprobe is called for each file to determine duration. Files that cannot be
/// probed are included with `duration_secs = 0` so they still appear in the index.
pub async fn scan_dir(root: &Path) -> Vec<LocalFileItem> {
let mut items = Vec::new();
@@ -34,33 +30,29 @@ pub async fn scan_dir(root: &Path) -> Vec<LocalFileItem> {
.extension()
.and_then(|e| e.to_str())
.map(|e| e.to_lowercase());
let ext = match ext {
Some(ref e) if VIDEO_EXTENSIONS.contains(&e.as_str()) => e.clone(),
match ext {
Some(ref e) if VIDEO_EXTENSIONS.contains(&e.as_str()) => {}
_ => continue,
};
let _ = ext; // extension validated, not needed further
let rel = match path.strip_prefix(root) {
Ok(r) => r,
Err(_) => continue,
};
// Normalise to forward-slash string for cross-platform stability.
let rel_path: String = rel
.components()
.map(|c| c.as_os_str().to_string_lossy().into_owned())
.collect::<Vec<_>>()
.join("/");
// Top-level directory under root.
let top_dir = rel
.components()
.next()
.filter(|_| rel.components().count() > 1) // skip if file is at root level
.filter(|_| rel.components().count() > 1)
.map(|c| c.as_os_str().to_string_lossy().into_owned())
.unwrap_or_else(|| "__root__".to_string());
.unwrap_or_else(|| ROOT_COLLECTION_NAME.to_string());
// Title: stem with separator chars replaced by spaces.
let stem = path
.file_stem()
.and_then(|s| s.to_str())
@@ -69,7 +61,6 @@ pub async fn scan_dir(root: &Path) -> Vec<LocalFileItem> {
let title = stem.replace(['_', '-', '.'], " ");
let title = title.trim().to_string();
// Year: first 4-digit number starting with 19xx or 20xx in filename or parent dirs.
let search_str = format!(
"{} {}",
stem,
@@ -79,7 +70,6 @@ pub async fn scan_dir(root: &Path) -> Vec<LocalFileItem> {
);
let year = extract_year(&search_str);
// Tags: ancestor directory components between root and the file.
let tags: Vec<String> = rel
.parent()
.into_iter()
@@ -103,27 +93,23 @@ pub async fn scan_dir(root: &Path) -> Vec<LocalFileItem> {
items
}
/// Extract the first plausible 4-digit year (1900-2099) from `s`.
fn extract_year(s: &str) -> Option<u16> {
let chars: Vec<char> = s.chars().collect();
let n = chars.len();
if n < 4 {
if n < YEAR_DIGITS {
return None;
}
for i in 0..=(n - 4) {
// All four chars must be ASCII digits.
if !chars[i..i + 4].iter().all(|c| c.is_ascii_digit()) {
for i in 0..=(n - YEAR_DIGITS) {
if !chars[i..i + YEAR_DIGITS].iter().all(|c| c.is_ascii_digit()) {
continue;
}
// Parse and range-check.
let s4: String = chars[i..i + 4].iter().collect();
let s4: String = chars[i..i + YEAR_DIGITS].iter().collect();
let num: u16 = s4.parse().ok()?;
if !(1900..=2099).contains(&num) {
if !(MIN_YEAR..=MAX_YEAR).contains(&num) {
continue;
}
// Word-boundary: char before and after must not be digits.
let before_ok = i == 0 || !chars[i - 1].is_ascii_digit();
let after_ok = i + 4 >= n || !chars[i + 4].is_ascii_digit();
let after_ok = i + YEAR_DIGITS >= n || !chars[i + YEAR_DIGITS].is_ascii_digit();
if before_ok && after_ok {
return Some(num);
}
@@ -131,7 +117,6 @@ fn extract_year(s: &str) -> Option<u16> {
None
}
/// Run ffprobe to get the duration of `path` in whole seconds.
async fn get_duration(path: &Path) -> Option<u32> {
#[derive(serde::Deserialize)]
struct Fmt {
@@ -169,8 +154,8 @@ mod tests {
assert_eq!(extract_year("Movie 2024 HD"), Some(2024));
assert_eq!(extract_year("1999_classic"), Some(1999));
assert_eq!(extract_year("no year here"), None);
assert_eq!(extract_year("12345"), None); // 5-digit number
assert_eq!(extract_year("2100"), None); // out of range
assert_eq!(extract_year("12345"), None);
assert_eq!(extract_year("2100"), None);
assert_eq!(extract_year("1900"), Some(1900));
assert_eq!(extract_year("2099"), Some(2099));
}