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 image_ocr;
15mod key_phrases;
16mod location_timestamp;
17mod pdf_content;
18mod renamer;
19mod scorer;
20mod series_detector;
21mod stem_analyzer;
22mod text_content;
23mod video_ocr;
24
25pub use deps_check::{detect_needed_dependencies, Dependency, DependencyNeeds};
27pub use detector::FileCategory;
28
29#[derive(Debug, Clone)]
31pub struct RenameConfig {
32 pub skip_hidden: bool,
34 pub include_location: bool,
36 pub include_timestamp: bool,
38 pub multiframe_video: bool,
40}
41
42impl Default for RenameConfig {
43 fn default() -> Self {
44 Self {
45 skip_hidden: false,
46 include_location: false,
47 include_timestamp: false,
48 multiframe_video: true, }
50 }
51}
52
53#[derive(Debug, Clone)]
55pub struct FileAnalysis {
56 pub original_path: PathBuf,
58 pub original_name: String,
60 pub proposed_name: Option<String>,
62 pub file_category: FileCategory,
64}
65
66#[derive(Debug, Clone)]
68pub struct RenameResult {
69 pub original_path: PathBuf,
71 pub new_name: String,
73 pub success: bool,
75 pub error: Option<String>,
77}
78
79pub struct RenameEngine {
81 config: RenameConfig,
82}
83
84impl RenameEngine {
85 pub fn new(config: RenameConfig) -> Self {
87 Self { config }
88 }
89
90 pub fn default() -> Self {
92 Self::new(RenameConfig::default())
93 }
94
95 pub fn analyze_directory(&self, directory: &Path) -> Result<Vec<FileAnalysis>> {
98 let mut analyses = Vec::new();
99
100 let files = self.scan_files(directory)?;
102
103 let mut existing_names = HashSet::new();
105 for file_path in &files {
106 if let Some(filename) = file_path.file_name() {
107 if let Some(name) = filename.to_str() {
108 existing_names.insert(name.to_string());
109 }
110 }
111 }
112
113 for file_path in files {
115 match self.analyze_file(&file_path, &mut existing_names) {
116 Ok(analysis) => analyses.push(analysis),
117 Err(e) => {
118 log::warn!("Failed to analyze {}: {}", file_path.display(), e);
119 if let Some(name) = file_path.file_name().and_then(|n| n.to_str()) {
121 analyses.push(FileAnalysis {
122 original_path: file_path.clone(),
123 original_name: name.to_string(),
124 proposed_name: None,
125 file_category: FileCategory::Unknown,
126 });
127 }
128 }
129 }
130 }
131
132 Ok(analyses)
133 }
134
135 pub fn rename_files(&self, analyses: &[FileAnalysis], dry_run: bool) -> Vec<RenameResult> {
138 let mut results = Vec::new();
139
140 for analysis in analyses {
141 if let Some(new_name) = &analysis.proposed_name {
142 match renamer::rename_file(&analysis.original_path, new_name, dry_run) {
143 Ok(_) => {
144 results.push(RenameResult {
145 original_path: analysis.original_path.clone(),
146 new_name: new_name.clone(),
147 success: true,
148 error: None,
149 });
150 }
151 Err(e) => {
152 results.push(RenameResult {
153 original_path: analysis.original_path.clone(),
154 new_name: new_name.clone(),
155 success: false,
156 error: Some(e.to_string()),
157 });
158 }
159 }
160 }
161 }
162
163 results
164 }
165
166 pub fn process_directory(&self, directory: &Path, dry_run: bool) -> Result<Vec<RenameResult>> {
168 let analyses = self.analyze_directory(directory)?;
169 Ok(self.rename_files(&analyses, dry_run))
170 }
171
172 fn scan_files(&self, directory: &Path) -> Result<Vec<PathBuf>> {
175 use walkdir::WalkDir;
176
177 let mut files = Vec::new();
178
179 for entry in WalkDir::new(directory)
180 .follow_links(false)
181 .into_iter()
182 .filter_entry(|e| {
183 if self.config.skip_hidden {
184 !e.file_name()
185 .to_str()
186 .map(|s| s.starts_with('.'))
187 .unwrap_or(false)
188 } else {
189 true
190 }
191 })
192 {
193 match entry {
194 Ok(entry) => {
195 if entry.file_type().is_file() {
196 files.push(entry.path().to_path_buf());
197 }
198 }
199 Err(e) => {
200 log::warn!("Failed to access entry: {}", e);
201 }
202 }
203 }
204
205 Ok(files)
206 }
207
208 fn analyze_file(
209 &self,
210 file_path: &Path,
211 existing_names: &mut HashSet<String>,
212 ) -> Result<FileAnalysis> {
213 let file_category = detector::detect_file_type(file_path)?;
215
216 let original_name = file_path
217 .file_name()
218 .and_then(|n| n.to_str())
219 .unwrap_or("unknown")
220 .to_string();
221
222 if file_category == FileCategory::Unknown {
224 return Ok(FileAnalysis {
225 original_path: file_path.to_path_buf(),
226 original_name,
227 proposed_name: None,
228 file_category,
229 });
230 }
231
232 let metadata = match extractor::extract_metadata(file_path, &self.config) {
234 Ok(m) => m,
235 Err(_) => {
236 return Ok(FileAnalysis {
237 original_path: file_path.to_path_buf(),
238 original_name,
239 proposed_name: None,
240 file_category,
241 });
242 }
243 };
244
245 let candidate_name = metadata.extract_name(&file_category, file_path);
247
248 let proposed_name = candidate_name.map(|name| {
249 let extension = file_path.extension();
250 generator::generate_filename(&name, extension, existing_names)
251 });
252
253 Ok(FileAnalysis {
254 original_path: file_path.to_path_buf(),
255 original_name,
256 proposed_name,
257 file_category,
258 })
259 }
260}
261
262pub fn check_dependencies() -> Result<()> {
264 deps::print_dependency_status();
265 Ok(())
266}
267
268pub fn install_dependencies() -> Result<()> {
270 deps::run_installer().map_err(|e| anyhow::anyhow!(e))
271}
272
273pub fn install_dependencies_with_progress(
275 progress: Option<deps::ProgressCallback>,
276) -> Result<()> {
277 deps::run_installer_with_progress(progress).map_err(|e| anyhow::anyhow!(e))
278}
279
280pub use deps::ProgressCallback;