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 fs::create_dir_all(&output_dir)?;
68
69 let response = reqwest::get(LISTFILE_URL).await?;
71 let content = response.text().await?;
72
73 fs::write(&output_file, &content)?;
75
76 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 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 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
337pub 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}