subx_cli/core/matcher/
mod.rs

1//! 檔案匹配引擎模組
2#![allow(dead_code)]
3
4pub mod discovery;
5pub mod engine;
6// 已移除檔名分析器,簡化匹配邏輯
7
8pub use discovery::{FileDiscovery, MediaFile, MediaFileType};
9pub use engine::{MatchConfig, MatchEngine, MatchOperation};
10// pub use filename_analyzer::{FilenameAnalyzer, ParsedFilename};
11pub mod cache;
12use crate::Result;
13use crate::core::language::{LanguageDetector, LanguageInfo};
14use crate::error::SubXError;
15use std::path::{Path, PathBuf};
16
17/// 增強的檔案資訊結構,包含相對路徑與目錄上下文
18#[derive(Debug, Clone)]
19pub struct FileInfo {
20    /// 檔案名稱(不含路徑)
21    pub name: String,
22    /// 相對於搜尋根目錄的路徑
23    pub relative_path: String,
24    /// 完整的絕對路徑
25    pub full_path: PathBuf,
26    /// 所在目錄名稱
27    pub directory: String,
28    /// 目錄深度(相對於根目錄)
29    pub depth: usize,
30    /// 偵測出的語言編碼(如 tc、sc、en)
31    pub language: Option<LanguageInfo>,
32}
33
34impl FileInfo {
35    /// 建立 FileInfo,root_path 為搜尋根目錄
36    pub fn new(full_path: PathBuf, root_path: &Path) -> Result<Self> {
37        // 統一使用 Unix 風格分隔符,確保跨平台一致性
38        let relative_path = full_path
39            .strip_prefix(root_path)
40            .map_err(|e| SubXError::Other(e.into()))?
41            .to_string_lossy()
42            .replace('\\', "/");
43        let name = full_path
44            .file_name()
45            .and_then(|n| n.to_str())
46            .unwrap_or_default()
47            .to_string();
48        let directory = full_path
49            .parent()
50            .and_then(|p| p.file_name())
51            .and_then(|n| n.to_str())
52            .unwrap_or_default()
53            .to_string();
54        // 使用 '/' 作為分隔符計算深度
55        let depth = relative_path.matches('/').count();
56        let detector = LanguageDetector::new();
57        let language = detector.detect_from_path(&full_path);
58        Ok(Self {
59            name,
60            relative_path,
61            full_path,
62            directory,
63            depth,
64            language,
65        })
66    }
67}
68
69#[cfg(test)]
70mod tests {
71    use super::*;
72    use tempfile::TempDir;
73
74    #[test]
75    fn test_file_info_creation() -> Result<()> {
76        let temp = TempDir::new().unwrap();
77        let root = temp.path();
78        let file_path = root.join("season1").join("episode1.mp4");
79        std::fs::create_dir_all(file_path.parent().unwrap()).unwrap();
80        std::fs::write(&file_path, b"").unwrap();
81
82        let info = FileInfo::new(file_path.clone(), root)?;
83        assert_eq!(info.name, "episode1.mp4");
84        assert_eq!(info.relative_path, "season1/episode1.mp4");
85        assert_eq!(info.directory, "season1");
86        assert_eq!(info.depth, 1);
87        Ok(())
88    }
89
90    #[test]
91    fn test_file_info_deep_path() -> Result<()> {
92        let temp = TempDir::new().unwrap();
93        let root = temp.path();
94
95        // 測試多層目錄
96        let file_path = root
97            .join("series")
98            .join("season1")
99            .join("episodes")
100            .join("ep01.mp4");
101        std::fs::create_dir_all(file_path.parent().unwrap()).unwrap();
102        std::fs::write(&file_path, b"").unwrap();
103
104        let info = FileInfo::new(file_path.clone(), root)?;
105        assert_eq!(info.relative_path, "series/season1/episodes/ep01.mp4");
106        assert_eq!(info.depth, 3);
107
108        Ok(())
109    }
110}