subx_cli/core/matcher/
discovery.rs1use std::path::{Path, PathBuf};
2use walkdir::WalkDir;
3
4use crate::Result;
5
6#[derive(Debug, Clone)]
8pub struct MediaFile {
9 pub path: PathBuf,
10 pub file_type: MediaFileType,
11 pub size: u64,
12 pub name: String,
13 pub extension: String,
14}
15
16#[cfg(test)]
18mod tests {
19 use super::*;
20 use std::fs;
21 use tempfile::TempDir;
22
23 fn create_test_files(dir: &std::path::Path) {
24 let _ = fs::write(dir.join("video1.mp4"), b"");
25 let _ = fs::write(dir.join("video2.mkv"), b"");
26 let _ = fs::write(dir.join("subtitle1.srt"), b"");
27 let sub = dir.join("season1");
28 fs::create_dir_all(&sub).unwrap();
29 let _ = fs::write(sub.join("episode1.mp4"), b"");
30 let _ = fs::write(sub.join("episode1.srt"), b"");
31 let _ = fs::write(dir.join("note.txt"), b"");
32 }
33
34 #[test]
35 fn test_file_discovery_non_recursive() {
36 let temp = TempDir::new().unwrap();
37 create_test_files(temp.path());
38 let disco = FileDiscovery::new();
39 let files = disco.scan_directory(temp.path(), false).unwrap();
40 let vids = files
41 .iter()
42 .filter(|f| matches!(f.file_type, MediaFileType::Video))
43 .count();
44 let subs = files
45 .iter()
46 .filter(|f| matches!(f.file_type, MediaFileType::Subtitle))
47 .count();
48 assert_eq!(vids, 2);
49 assert_eq!(subs, 1);
50 assert!(!files.iter().any(|f| f.name == "episode1"));
51 }
52
53 #[test]
54 fn test_file_discovery_recursive() {
55 let temp = TempDir::new().unwrap();
56 create_test_files(temp.path());
57 let disco = FileDiscovery::new();
58 let files = disco.scan_directory(temp.path(), true).unwrap();
59 let vids = files
60 .iter()
61 .filter(|f| matches!(f.file_type, MediaFileType::Video))
62 .count();
63 let subs = files
64 .iter()
65 .filter(|f| matches!(f.file_type, MediaFileType::Subtitle))
66 .count();
67 assert_eq!(vids, 3);
68 assert_eq!(subs, 2);
69 assert!(files.iter().any(|f| f.name == "episode1"));
70 }
71
72 #[test]
73 fn test_file_classification_and_extensions() {
74 let temp = TempDir::new().unwrap();
75 let v = temp.path().join("t.mp4");
76 fs::write(&v, b"").unwrap();
77 let s = temp.path().join("t.srt");
78 fs::write(&s, b"").unwrap();
79 let x = temp.path().join("t.txt");
80 fs::write(&x, b"").unwrap();
81 let disco = FileDiscovery::new();
82 let vf = disco.classify_file(&v).unwrap().unwrap();
83 assert!(matches!(vf.file_type, MediaFileType::Video));
84 assert_eq!(vf.name, "t");
85 let sf = disco.classify_file(&s).unwrap().unwrap();
86 assert!(matches!(sf.file_type, MediaFileType::Subtitle));
87 assert_eq!(sf.name, "t");
88 let none = disco.classify_file(&x).unwrap();
89 assert!(none.is_none());
90 assert!(disco.video_extensions.contains(&"mp4".to_string()));
91 assert!(disco.subtitle_extensions.contains(&"srt".to_string()));
92 }
93
94 #[test]
95 fn test_empty_and_nonexistent_directory() {
96 let temp = TempDir::new().unwrap();
97 let disco = FileDiscovery::new();
98 let files = disco.scan_directory(temp.path(), false).unwrap();
99 assert!(files.is_empty());
100 let res = disco.scan_directory(&std::path::Path::new("/nonexistent/path"), false);
101 assert!(res.is_err());
102 }
103}
104
105impl Default for FileDiscovery {
106 fn default() -> Self {
107 Self::new()
108 }
109}
110
111#[derive(Debug, Clone)]
113pub enum MediaFileType {
114 Video,
115 Subtitle,
116}
117
118pub struct FileDiscovery {
120 video_extensions: Vec<String>,
121 subtitle_extensions: Vec<String>,
122}
123
124impl FileDiscovery {
125 pub fn new() -> Self {
127 Self {
128 video_extensions: vec![
129 "mp4".to_string(),
130 "mkv".to_string(),
131 "avi".to_string(),
132 "mov".to_string(),
133 "wmv".to_string(),
134 "flv".to_string(),
135 "m4v".to_string(),
136 "webm".to_string(),
137 ],
138 subtitle_extensions: vec![
139 "srt".to_string(),
140 "ass".to_string(),
141 "vtt".to_string(),
142 "sub".to_string(),
143 "ssa".to_string(),
144 "idx".to_string(),
145 ],
146 }
147 }
148
149 pub fn scan_directory(&self, path: &Path, recursive: bool) -> Result<Vec<MediaFile>> {
151 let mut files = Vec::new();
152
153 let walker = if recursive {
154 WalkDir::new(path).into_iter()
155 } else {
156 WalkDir::new(path).max_depth(1).into_iter()
157 };
158
159 for entry in walker {
160 let entry = entry?;
161 let path = entry.path();
162
163 if path.is_file() {
164 if let Some(media_file) = self.classify_file(path)? {
165 files.push(media_file);
166 }
167 }
168 }
169
170 Ok(files)
171 }
172
173 fn classify_file(&self, path: &Path) -> Result<Option<MediaFile>> {
175 let extension = path
176 .extension()
177 .and_then(|ext| ext.to_str())
178 .map(|s| s.to_lowercase())
179 .unwrap_or_default();
180
181 let file_type = if self.video_extensions.contains(&extension) {
182 MediaFileType::Video
183 } else if self.subtitle_extensions.contains(&extension) {
184 MediaFileType::Subtitle
185 } else {
186 return Ok(None);
187 };
188
189 let metadata = std::fs::metadata(path)?;
190 let name = path
191 .file_stem()
192 .and_then(|stem| stem.to_str())
193 .unwrap_or_default()
194 .to_string();
195
196 Ok(Some(MediaFile {
197 path: path.to_path_buf(),
198 file_type,
199 size: metadata.len(),
200 name,
201 extension,
202 }))
203 }
204}