1use anyhow::Result;
2use std::collections::HashSet;
3use std::path::{Path, PathBuf};
4
5mod code_docstring;
7mod deps;
8mod deps_check;
9mod detector;
10mod dir_context;
11mod extractor;
12mod format_handlers;
13mod generator;
14mod geocoding;
15mod image_ocr;
16mod key_phrases;
17mod location_timestamp;
18mod pdf_content;
19mod renamer;
20mod scorer;
21mod series_detector;
22mod stem_analyzer;
23mod text_content;
24mod video_ocr;
25
26pub use deps_check::{detect_needed_dependencies, Dependency, DependencyNeeds};
28pub use detector::FileCategory;
29
30#[derive(Debug, Clone)]
32pub struct RenameConfig {
33 pub skip_hidden: bool,
35 pub include_location: bool,
37 pub include_timestamp: bool,
39 pub multiframe_video: bool,
41 pub geocode: bool,
44}
45
46impl Default for RenameConfig {
47 fn default() -> Self {
48 Self {
49 skip_hidden: false,
50 include_location: true, include_timestamp: true, multiframe_video: true, geocode: true, }
55 }
56}
57
58#[derive(Debug, Clone)]
60pub struct FileAnalysis {
61 pub original_path: PathBuf,
63 pub original_name: String,
65 pub proposed_name: Option<String>,
67 pub file_category: FileCategory,
69}
70
71#[derive(Debug, Clone)]
73pub struct RenameResult {
74 pub original_path: PathBuf,
76 pub new_name: String,
78 pub success: bool,
80 pub error: Option<String>,
82}
83
84pub struct RenameEngine {
86 config: RenameConfig,
87}
88
89impl RenameEngine {
90 pub fn new(config: RenameConfig) -> Self {
92 Self { config }
93 }
94
95 pub fn default() -> Self {
97 Self::new(RenameConfig::default())
98 }
99
100 pub fn analyze_directory(&self, directory: &Path) -> Result<Vec<FileAnalysis>> {
103 let mut analyses = Vec::new();
104
105 let files = self.scan_files(directory)?;
107
108 let series_list = series_detector::detect_series(&files);
110 log::info!("Detected {} file series", series_list.len());
111
112 let mut file_series_map = std::collections::HashMap::new();
114 for series in &series_list {
115 for (file_path, _) in &series.files {
116 file_series_map.insert(file_path.clone(), series.clone());
117 }
118 }
119
120 let mut existing_names = HashSet::new();
122 for file_path in &files {
123 if let Some(filename) = file_path.file_name() {
124 if let Some(name) = filename.to_str() {
125 existing_names.insert(name.to_string());
126 }
127 }
128 }
129
130 for file_path in files {
132 match self.analyze_file(&file_path, &mut existing_names) {
133 Ok(mut analysis) => {
134 if let Some(series) = file_series_map.get(&file_path) {
136 if let Some(proposed_name) = &analysis.proposed_name {
138 let base_name = if let Some(pos) = proposed_name.rfind('.') {
140 &proposed_name[..pos]
141 } else {
142 proposed_name
143 };
144
145 if let Some(series_name) = series_detector::apply_series_naming(
147 series,
148 &file_path,
149 base_name,
150 ) {
151 analysis.proposed_name = Some(series_name);
152 }
153 }
154 }
155 analyses.push(analysis);
156 },
157 Err(e) => {
158 log::warn!("Failed to analyze {}: {}", file_path.display(), e);
159 if let Some(name) = file_path.file_name().and_then(|n| n.to_str()) {
161 analyses.push(FileAnalysis {
162 original_path: file_path.clone(),
163 original_name: name.to_string(),
164 proposed_name: None,
165 file_category: FileCategory::Unknown,
166 });
167 }
168 }
169 }
170 }
171
172 Ok(analyses)
173 }
174
175 pub fn rename_files(&self, analyses: &[FileAnalysis], dry_run: bool) -> Vec<RenameResult> {
178 let mut results = Vec::new();
179
180 for analysis in analyses {
181 if let Some(new_name) = &analysis.proposed_name {
182 match renamer::rename_file(&analysis.original_path, new_name, dry_run) {
183 Ok(_) => {
184 results.push(RenameResult {
185 original_path: analysis.original_path.clone(),
186 new_name: new_name.clone(),
187 success: true,
188 error: None,
189 });
190 }
191 Err(e) => {
192 results.push(RenameResult {
193 original_path: analysis.original_path.clone(),
194 new_name: new_name.clone(),
195 success: false,
196 error: Some(e.to_string()),
197 });
198 }
199 }
200 }
201 }
202
203 results
204 }
205
206 pub fn process_directory(&self, directory: &Path, dry_run: bool) -> Result<Vec<RenameResult>> {
208 let analyses = self.analyze_directory(directory)?;
209 Ok(self.rename_files(&analyses, dry_run))
210 }
211
212 fn scan_files(&self, directory: &Path) -> Result<Vec<PathBuf>> {
215 use walkdir::WalkDir;
216
217 let mut files = Vec::new();
218
219 for entry in WalkDir::new(directory)
220 .follow_links(false)
221 .into_iter()
222 .filter_entry(|e| {
223 if self.config.skip_hidden {
224 !e.file_name()
225 .to_str()
226 .map(|s| s.starts_with('.'))
227 .unwrap_or(false)
228 } else {
229 true
230 }
231 })
232 {
233 match entry {
234 Ok(entry) => {
235 if entry.file_type().is_file() {
236 files.push(entry.path().to_path_buf());
237 }
238 }
239 Err(e) => {
240 log::warn!("Failed to access entry: {}", e);
241 }
242 }
243 }
244
245 Ok(files)
246 }
247
248 fn analyze_file(
249 &self,
250 file_path: &Path,
251 existing_names: &mut HashSet<String>,
252 ) -> Result<FileAnalysis> {
253 let file_category = detector::detect_file_type(file_path)?;
255
256 let original_name = file_path
257 .file_name()
258 .and_then(|n| n.to_str())
259 .unwrap_or("unknown")
260 .to_string();
261
262 if file_category == FileCategory::Unknown {
264 return Ok(FileAnalysis {
265 original_path: file_path.to_path_buf(),
266 original_name,
267 proposed_name: None,
268 file_category,
269 });
270 }
271
272 let metadata = match extractor::extract_metadata(file_path, &self.config) {
274 Ok(m) => m,
275 Err(_) => {
276 return Ok(FileAnalysis {
277 original_path: file_path.to_path_buf(),
278 original_name,
279 proposed_name: None,
280 file_category,
281 });
282 }
283 };
284
285 let candidate_name = metadata.extract_name(&file_category, file_path);
287
288 let proposed_name = candidate_name.map(|name| {
289 let extension = file_path.extension();
290 generator::generate_filename_with_metadata(&name, extension, existing_names, Some(&metadata))
291 });
292
293 Ok(FileAnalysis {
294 original_path: file_path.to_path_buf(),
295 original_name,
296 proposed_name,
297 file_category,
298 })
299 }
300}
301
302pub fn check_dependencies() -> Result<()> {
304 deps::print_dependency_status();
305 Ok(())
306}
307
308pub fn install_dependencies() -> Result<()> {
310 deps::run_installer().map_err(|e| anyhow::anyhow!(e))
311}
312
313pub fn install_dependencies_with_progress(
315 progress: Option<deps::ProgressCallback>,
316) -> Result<()> {
317 deps::run_installer_with_progress(progress).map_err(|e| anyhow::anyhow!(e))
318}
319
320pub use deps::ProgressCallback;