ngdp_client/commands/
listfile.rs

1use crate::{ListfileCommands, OutputFormat};
2use owo_colors::OwoColorize;
3use regex::Regex;
4use std::collections::HashMap;
5use std::fs;
6use std::io::{BufRead, BufReader};
7use std::path::PathBuf;
8use tracing::debug;
9
10const LISTFILE_URL: &str =
11    "https://github.com/wowdev/wow-listfile/releases/latest/download/community-listfile.csv";
12
13pub async fn handle(
14    cmd: ListfileCommands,
15    format: OutputFormat,
16) -> Result<(), Box<dyn std::error::Error>> {
17    match cmd {
18        ListfileCommands::Download { output, force } => {
19            handle_download(output, force, format).await
20        }
21        ListfileCommands::Info { path } => handle_info(path, format).await,
22        ListfileCommands::Search {
23            pattern,
24            path,
25            ignore_case,
26            limit,
27        } => handle_search(pattern, path, ignore_case, limit, format).await,
28    }
29}
30
31async fn handle_download(
32    output_dir: PathBuf,
33    force: bool,
34    format: OutputFormat,
35) -> Result<(), Box<dyn std::error::Error>> {
36    let output_file = output_dir.join("community-listfile.csv");
37
38    if output_file.exists() && !force {
39        match format {
40            OutputFormat::Json | OutputFormat::JsonPretty => {
41                let json = serde_json::json!({
42                    "status": "skipped",
43                    "message": "File already exists. Use --force to overwrite.",
44                    "path": output_file
45                });
46                println!("{}", serde_json::to_string_pretty(&json)?);
47            }
48            OutputFormat::Text => {
49                println!("šŸ“ File already exists: {output_file:?}");
50                println!("   Use --force to overwrite");
51            }
52            OutputFormat::Bpsv => {
53                println!("status = skipped");
54                println!("path = {output_file:?}");
55            }
56        }
57        return Ok(());
58    }
59
60    if let OutputFormat::Text = format {
61        println!("šŸ“„ Downloading community listfile...");
62        println!("   URL: {}", LISTFILE_URL.cyan());
63        println!("   Output: {output_dir:?}");
64    }
65
66    // Create output directory if it doesn't exist
67    fs::create_dir_all(&output_dir)?;
68
69    // Download the file
70    let response = reqwest::get(LISTFILE_URL).await?;
71    let content = response.text().await?;
72
73    // Write to file
74    fs::write(&output_file, &content)?;
75
76    // Parse to get basic stats
77    let line_count = content.lines().count();
78    let file_size = content.len();
79
80    match format {
81        OutputFormat::Json | OutputFormat::JsonPretty => {
82            let json = serde_json::json!({
83                "status": "success",
84                "path": output_file,
85                "size": file_size,
86                "entries": line_count,
87                "url": LISTFILE_URL
88            });
89
90            if matches!(format, OutputFormat::JsonPretty) {
91                println!("{}", serde_json::to_string_pretty(&json)?);
92            } else {
93                println!("{}", serde_json::to_string(&json)?);
94            }
95        }
96        OutputFormat::Text => {
97            println!("āœ… Downloaded successfully!");
98            println!("   File: {output_file:?}");
99            println!("   Size: {} bytes", file_size.to_string().green());
100            println!("   Entries: {}", line_count.to_string().cyan());
101        }
102        OutputFormat::Bpsv => {
103            println!("status = success");
104            println!("path = {output_file:?}");
105            println!("size = {file_size}");
106            println!("entries = {line_count}");
107        }
108    }
109
110    Ok(())
111}
112
113async fn handle_info(
114    path: PathBuf,
115    format: OutputFormat,
116) -> Result<(), Box<dyn std::error::Error>> {
117    if !path.exists() {
118        match format {
119            OutputFormat::Json | OutputFormat::JsonPretty => {
120                let json = serde_json::json!({
121                    "error": "File not found",
122                    "path": path
123                });
124                println!("{}", serde_json::to_string_pretty(&json)?);
125            }
126            OutputFormat::Text => {
127                println!("āŒ File not found: {path:?}");
128                println!("   Run: ngdp storage listfile download");
129            }
130            OutputFormat::Bpsv => {
131                println!("error = file_not_found");
132                println!("path = {path:?}");
133            }
134        }
135        return Ok(());
136    }
137
138    let file = fs::File::open(&path)?;
139    let reader = BufReader::new(file);
140
141    let mut total_lines = 0;
142    let mut sample_entries = Vec::new();
143    let mut fdid_count = 0;
144    let mut unique_extensions = std::collections::HashSet::new();
145
146    for (i, line) in reader.lines().enumerate() {
147        let line = line?;
148        total_lines += 1;
149
150        if i < 5 {
151            sample_entries.push(line.clone());
152        }
153
154        if let Some(sep_pos) = line.find(';') {
155            if let Ok(_fdid) = line[..sep_pos].parse::<u32>() {
156                fdid_count += 1;
157
158                let filename = &line[sep_pos + 1..];
159                if let Some(ext_pos) = filename.rfind('.') {
160                    let extension = &filename[ext_pos + 1..].to_lowercase();
161                    unique_extensions.insert(extension.to_string());
162                }
163            }
164        }
165    }
166
167    let file_size = fs::metadata(&path)?.len();
168    let mut extensions: Vec<_> = unique_extensions.into_iter().collect();
169    extensions.sort();
170
171    match format {
172        OutputFormat::Json | OutputFormat::JsonPretty => {
173            let json = serde_json::json!({
174                "path": path,
175                "size": file_size,
176                "total_entries": total_lines,
177                "valid_entries": fdid_count,
178                "extensions": extensions,
179                "sample_entries": sample_entries
180            });
181
182            if matches!(format, OutputFormat::JsonPretty) {
183                println!("{}", serde_json::to_string_pretty(&json)?);
184            } else {
185                println!("{}", serde_json::to_string(&json)?);
186            }
187        }
188        OutputFormat::Text => {
189            println!("šŸ“„ Community Listfile Information");
190            println!("━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━");
191            println!("  File:         {path:?}");
192            println!("  Size:         {} bytes", file_size.to_string().green());
193            println!("  Total Lines:  {}", total_lines.to_string().cyan());
194            println!("  Valid Entries: {}", fdid_count.to_string().cyan());
195
196            if !extensions.is_empty() {
197                println!("  File Types:   {} types", extensions.len());
198
199                // Show top 10 extensions
200                let display_extensions: Vec<_> = extensions.into_iter().take(10).collect();
201                println!("    Extensions: {}", display_extensions.join(", "));
202            }
203
204            if !sample_entries.is_empty() {
205                println!("\nšŸ“‹ Sample Entries:");
206                for entry in &sample_entries {
207                    println!("    {entry}");
208                }
209            }
210        }
211        OutputFormat::Bpsv => {
212            println!("## Listfile Information");
213            println!("path = {path:?}");
214            println!("size = {file_size}");
215            println!("total_entries = {total_lines}");
216            println!("valid_entries = {fdid_count}");
217            println!("extensions = {}", extensions.len());
218        }
219    }
220
221    Ok(())
222}
223
224async fn handle_search(
225    pattern: String,
226    path: PathBuf,
227    ignore_case: bool,
228    limit: usize,
229    format: OutputFormat,
230) -> Result<(), Box<dyn std::error::Error>> {
231    if !path.exists() {
232        match format {
233            OutputFormat::Text => {
234                println!("āŒ Listfile not found: {path:?}");
235                println!("   Run: ngdp storage listfile download");
236            }
237            _ => {
238                let json = serde_json::json!({
239                    "error": "File not found",
240                    "path": path
241                });
242                println!("{}", serde_json::to_string_pretty(&json)?);
243            }
244        }
245        return Ok(());
246    }
247
248    // Create regex pattern
249    let regex = if ignore_case {
250        Regex::new(&format!("(?i){pattern}"))?
251    } else {
252        Regex::new(&pattern)?
253    };
254
255    let file = fs::File::open(&path)?;
256    let reader = BufReader::new(file);
257
258    let mut matches = Vec::new();
259    let mut total_checked = 0;
260
261    for line in reader.lines() {
262        let line = line?;
263        total_checked += 1;
264
265        if regex.is_match(&line) {
266            if let Some(sep_pos) = line.find(';') {
267                if let Ok(fdid) = line[..sep_pos].parse::<u32>() {
268                    let filename = &line[sep_pos + 1..];
269                    matches.push((fdid, filename.to_string()));
270
271                    if matches.len() >= limit {
272                        break;
273                    }
274                }
275            }
276        }
277    }
278
279    match format {
280        OutputFormat::Json | OutputFormat::JsonPretty => {
281            let json = serde_json::json!({
282                "pattern": pattern,
283                "ignore_case": ignore_case,
284                "total_checked": total_checked,
285                "matches_found": matches.len(),
286                "matches": matches.into_iter().map(|(fdid, filename)| {
287                    serde_json::json!({
288                        "file_data_id": fdid,
289                        "filename": filename
290                    })
291                }).collect::<Vec<_>>()
292            });
293
294            if matches!(format, OutputFormat::JsonPretty) {
295                println!("{}", serde_json::to_string_pretty(&json)?);
296            } else {
297                println!("{}", serde_json::to_string(&json)?);
298            }
299        }
300        OutputFormat::Text => {
301            println!("šŸ” Search Results for: {}", pattern.yellow());
302            println!("━━━━━━━━━━━━━━━━━━━━━━━━");
303            println!("  Pattern:       {pattern}");
304            println!(
305                "  Case sensitive: {}",
306                if ignore_case { "No" } else { "Yes" }
307            );
308            println!("  Entries checked: {total_checked}");
309            println!("  Matches found: {}", matches.len().to_string().green());
310
311            if !matches.is_empty() {
312                println!("\nšŸ“‹ Results:");
313                println!("{:<10} Filename", "FileDataID");
314                println!("{}", "─".repeat(80));
315
316                for (fdid, filename) in matches {
317                    println!("{:<10} {}", fdid.to_string().cyan(), filename);
318                }
319            }
320        }
321        OutputFormat::Bpsv => {
322            println!("## Search Results");
323            println!("pattern = {pattern}");
324            println!("ignore_case = {ignore_case}");
325            println!("total_checked = {total_checked}");
326            println!("matches_found = {}", matches.len());
327
328            for (fdid, filename) in matches {
329                println!("match = {fdid} {filename}");
330            }
331        }
332    }
333
334    Ok(())
335}
336
337/// Parse a listfile and return FileDataID -> filename mapping
338pub fn parse_listfile(path: &PathBuf) -> Result<HashMap<u32, String>, Box<dyn std::error::Error>> {
339    let file = fs::File::open(path)?;
340    let reader = BufReader::new(file);
341    let mut mapping = HashMap::new();
342
343    for line in reader.lines() {
344        let line = line?;
345        if let Some(sep_pos) = line.find(';') {
346            if let Ok(fdid) = line[..sep_pos].parse::<u32>() {
347                let filename = line[sep_pos + 1..].to_string();
348                mapping.insert(fdid, filename);
349            }
350        }
351    }
352
353    debug!("Loaded {} filename mappings from listfile", mapping.len());
354    Ok(mapping)
355}