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:
@@ -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));
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user