A simple tool to log and rate the media you have consumed.
at master 20 kB view raw
1use std::path::Path; 2use std::io::Read; 3use json; 4use json::JsonValue; 5use json::object; 6use std::env; 7use std::fs::File; 8use std::io::Write; 9use colored::*; 10use strsim::{jaro}; 11 12fn error(message: &str) { 13 println!("{}: {}!", "error".bright_red(), message); 14} 15 16fn error_flat(message: &str) { 17 print!("{}: {}", "error".bright_red(), message); 18} 19 20fn warning(message: &str, prefix: &str) { 21 println!("{}: {}.", prefix.yellow(), message); 22} 23 24fn warning_flat(message: &str, prefix: &str) { 25 print!("{}: {}", prefix.yellow(), message); 26} 27 28fn help(message: &str) { 29 println!("{}: {}.", "help".bright_cyan(), message); 30} 31 32fn help_flat(message: &str) { 33 print!("{}: {}", "help".bright_cyan(), message); 34} 35 36const USAGE_PREFIX: &str = "usage"; 37const OPTIONS_PREFIX: &str = "options"; 38 39const CATEGORIES: [&str; 5] = ["series", "movie", "book", "podcast", "game"]; 40 41const STATUS: [&str; 5] = ["planned", "watching", "completed", "paused", "dropped"]; 42const STATUS_LOWER: [char; 5] = ['p', 'w', 'c', 'p', 'd']; 43 44fn main() -> std::io::Result<()> { 45 let args: Vec<String> = env::args().collect(); 46 47 let mut data: JsonValue; 48 49 let mut path: String; 50 51 if cfg!(windows) { 52 path = dirs::home_dir().unwrap().into_os_string().into_string().unwrap(); 53 path.push_str("\\medialog.json"); 54 } else if cfg!(unix) { 55 path = dirs::home_dir().unwrap().into_os_string().into_string().unwrap(); 56 path.push_str("/medialog.json"); 57 } else { 58 path = String::from("medialog.json"); 59 } 60 61 if Path::new(&path).exists() { 62 let mut file = File::open(&path)?; 63 let mut contents = String::new(); 64 file.read_to_string(&mut contents)?; 65 66 data = json::parse(&contents).unwrap(); 67 } else { 68 data = object! { 69 series: {}, 70 movies: {}, 71 manga: {}, 72 }; 73 } 74 75 if args.len() < 2 { 76 error("Must supply command in arguments"); 77 help("Consider running the 'help' command"); 78 return Ok(()); 79 } 80 81 if args[1].to_ascii_lowercase() == "add" { 82 if args.len() >= 4 { 83 let media_name: &str = &args[2].to_ascii_lowercase(); 84 let media_display_name: &str = &args[2]; 85 let media_category: &str = &args[3].to_ascii_lowercase(); 86 if CATEGORIES.contains(&args[3].to_ascii_lowercase().as_str()) { 87 // let media_object: JsonValue = object! { 88 // name: media_name, 89 // category: media_category, 90 // seasons: {} 91 // }; 92 93 if data[media_category].has_key(media_name) { 94 error_flat(""); 95 println!("Media '{}' in category '{}' already exists.", media_name, media_category); 96 return Ok(()); 97 } 98 99 data[media_category][media_name] = object! { 100 "disname": media_display_name, 101 "status": "planned", 102 }; 103 } else { 104 error("Invalid category in command 'add'"); 105 warning_flat("add <name> ", USAGE_PREFIX); 106 println!("{}", "<category>".red().bold()); 107 warning_flat("", OPTIONS_PREFIX); 108 for (i, item) in CATEGORIES.iter().enumerate() { 109 if i < CATEGORIES.len() - 1 { 110 eprint!("{}, ", item); 111 continue; 112 } 113 eprint!("{}", item); 114 } 115 } 116 117 } else { 118 error("Insufficient arguments for command 'add'"); 119 warning("add <name> <category>", USAGE_PREFIX); 120 return Ok(()); 121 } 122 } else if args[1].to_ascii_lowercase() == "edit" { 123 if args.len() >= 4 { 124 let media_name: &str = &args[2].to_ascii_lowercase(); 125 let media_category: &str = &args[3].to_ascii_lowercase(); 126 if CATEGORIES.contains(&args[3].to_ascii_lowercase().as_str()) { 127 let mut fixed_media_name: String; 128 129 if !&data[media_category].has_key(media_name) { 130 error_flat(""); 131 let mut highest_similarity: f64 = 0.0; 132 let mut highest_similarity_media: String = String::from(""); 133 134 for i in data[media_category].entries() { 135 // println!("{}, {}", i.0, media_name); 136 let similarity: f64 = jaro(media_name, i.0); 137 if similarity > highest_similarity { 138 highest_similarity = similarity; 139 highest_similarity_media = String::from(i.0); 140 } 141 } 142 143 if highest_similarity > 0.80 { 144 help_flat(""); 145 println!("Found media with a name with {}% similarity called '{}'.", &(highest_similarity * 100.0).to_string()[0..2], data[media_category][&highest_similarity_media]["disname"]); 146 147 fixed_media_name = highest_similarity_media; 148 } else { 149 println!("Media '{}' doesn't exist in category '{}'!", media_name, media_category); 150 return Ok(()) 151 } 152 } else { 153 fixed_media_name = String::from(media_name); 154 } 155 156 let result: String = edit::edit(json::stringify(data[media_category][&fixed_media_name].clone())).unwrap(); 157 let jsonresult = json::parse(&result); 158 data[media_category][&fixed_media_name] = match jsonresult { 159 Ok(json) => json, 160 Err(error) => panic!("Problem parsing json: {} \n{:?}", &result.cyan(), error) 161 } 162 } else { 163 error("Invalid category in command 'edit'"); 164 warning_flat("edit <name> ", USAGE_PREFIX); 165 println!("{}", "<category>".red().bold()); 166 warning_flat("", OPTIONS_PREFIX); 167 for (i, item) in CATEGORIES.iter().enumerate() { 168 if i < CATEGORIES.len() - 1 { 169 eprint!("{}, ", item); 170 continue; 171 } 172 eprint!("{}", item); 173 } 174 } 175 176 } else { 177 error("Insufficient arguments for command 'edit'"); 178 warning("edit <name> <category>", USAGE_PREFIX); 179 return Ok(()); 180 } 181 } else if args[1].to_ascii_lowercase() == "editstatus" { 182 if args.len() >= 5 { 183 let media_name: &str = &args[3].to_ascii_lowercase(); 184 let media_category: &str = &args[4].to_ascii_lowercase(); 185 if CATEGORIES.contains(&args[4].to_ascii_lowercase().as_str()) { 186 if !STATUS.contains(&args[2].to_ascii_lowercase().as_str()) || !STATUS_LOWER.contains(&args[2].to_ascii_lowercase().as_str().chars().nth(0).unwrap()) { 187 error_flat(""); 188 println!("Status '{}' doesn't exist!", args[2]); 189 help("Consider using 'planned', 'watching', 'completed', 'paused' or 'dropped'"); 190 return Ok(()); 191 } 192 193 if !data[media_category].has_key(media_name) { 194 error_flat(""); 195 println!("Media '{}' doesn't exist in category '{}'!", media_name, media_category); 196 return Ok(()); 197 } 198 199 data[media_category][media_name]["status"] = json::parse(&args[2]).unwrap(); 200 } else { 201 error("Invalid category in command 'edit'"); 202 warning_flat("editstatus <status> <name> ", USAGE_PREFIX); 203 println!("{}", "<category>".red().bold()); 204 warning_flat("", OPTIONS_PREFIX); 205 for (i, item) in CATEGORIES.iter().enumerate() { 206 if i < CATEGORIES.len() - 1 { 207 eprint!("{}, ", item); 208 continue; 209 } 210 eprint!("{}", item); 211 } 212 } 213 214 } else { 215 error("Insufficient arguments for command 'edit'"); 216 warning("editstatus <status> <name> <category>", USAGE_PREFIX); 217 return Ok(()); 218 } 219 } else if args[1].to_ascii_lowercase() == "editseason" { 220 if args.len() >= 6 { 221 let season_name: &str = &args[2].to_ascii_lowercase(); 222 let edit_object: &str = &args[3].to_ascii_lowercase(); 223 let media_name: &str = &args[4].to_ascii_lowercase(); 224 let media_category: &str = &args[5].to_ascii_lowercase(); 225 if CATEGORIES.contains(&args[5].to_ascii_lowercase().as_str()) { 226 if data[media_category].has_key(media_name) { 227 if !data[media_category][media_name].has_key(season_name) { 228 error_flat(""); 229 println!("Season {} doesn't exist in media {}!", season_name, media_name); 230 help_flat(""); 231 println!("Consider running 'medialog addseason {} {} {}'!", season_name, media_name, media_category); 232 return Ok(()); 233 } 234 235 if !["studio", "rating", "notes", "json"].contains(&edit_object) { 236 error_flat(""); 237 println!("Invalid media property {}!", edit_object); 238 help("Use 'studio', 'rating', 'notes' or 'json'!"); 239 return Ok(()); 240 } 241 242 243 if edit_object != "json" { 244 let result: String = edit::edit(json::stringify(data[media_category][media_name][season_name][edit_object].clone())).unwrap(); 245 246 data[media_category][media_name][season_name][edit_object] = json::parse(&result).unwrap(); 247 } else { 248 let result: String = edit::edit(json::stringify(data[media_category][media_name][season_name].clone())).unwrap(); 249 250 let jsonresult = json::parse(&result); 251 data[media_category][media_name][season_name] = match jsonresult { 252 Ok(json) => json, 253 Err(error) => panic!("Problem parsing json: {} \n{:?}", &result.cyan(), error) 254 } 255 } 256 } else { 257 error_flat(""); 258 println!("Media '{}' doesn't exist in category '{}'!", media_name, media_category); 259 return Ok(()); 260 } 261 } else { 262 error("Invalid category in command 'editseason'"); 263 warning_flat("editseason <season> <edit> <name> ", USAGE_PREFIX); 264 println!("{}", "<category>".red().bold()); 265 warning_flat("", OPTIONS_PREFIX); 266 for (i, item) in CATEGORIES.iter().enumerate() { 267 if i < CATEGORIES.len() - 1 { 268 eprint!("{}, ", item); 269 continue; 270 } 271 eprint!("{}", item); 272 } 273 } 274 275 } else { 276 error("Insufficient arguments for command 'editseason'"); 277 warning("editseason <season> <edit> <name> <category>", USAGE_PREFIX); 278 return Ok(()); 279 } 280 } else if args[1].to_ascii_lowercase() == "addseason" { 281 if args.len() >= 5 { 282 let season_name: &str = &args[2].to_ascii_lowercase(); 283 let media_name: &str = &args[3].to_ascii_lowercase(); 284 let season_display: &str = &args[2]; 285 let media_category: &str = &args[4].to_ascii_lowercase(); 286 if CATEGORIES.contains(&args[4].to_ascii_lowercase().as_str()) { 287 if data[media_category][media_name].has_key(season_name) { 288 error_flat(""); 289 println!("Media '{}' already has a season named '{}'!", media_name, season_name); 290 return Ok(()); 291 } 292 293 let mut studio: String = String::from(""); 294 if args.len() == 6 { 295 studio = args[5].clone().to_ascii_lowercase(); 296 } 297 298 data[media_category][media_name][season_name] = object! { 299 "disname": season_display, 300 "studio": studio, 301 "rating": 0, 302 "notes": "" 303 } 304 } else { 305 error("Invalid category in command 'addseason'"); 306 warning_flat("addseason <season> <name> ", USAGE_PREFIX); 307 println!("{}", "<category>".red().bold()); 308 warning_flat("", OPTIONS_PREFIX); 309 for (i, item) in CATEGORIES.iter().enumerate() { 310 if i < CATEGORIES.len() - 1 { 311 eprint!("{}, ", item); 312 continue; 313 } 314 eprint!("{}", item); 315 } 316 } 317 318 } else { 319 error("Insufficient arguments for command 'addseason'"); 320 warning("addseason <season> <name> <category>", USAGE_PREFIX); 321 return Ok(()); 322 } 323 } else if args[1].to_ascii_lowercase() == "next" { 324 if args.len() >= 3 { 325 if CATEGORIES.contains(&args[2].to_ascii_lowercase().as_str()) { 326 let mut watched: bool = false; 327 for (_key, value) in data[&args[2]].entries() { 328 if value["status"] == "planned" { 329 println!("Your next {} on the list is {}!", args[2], value["disname"]); 330 watched = true; 331 break; 332 } 333 } 334 if !watched { 335 println!("You have watched all your {}.", args[2]); 336 } 337 return Ok(()); 338 } else { 339 error("Invalid category in command 'next'"); 340 warning_flat("next ", USAGE_PREFIX); 341 println!("{}", "<category>".red().bold()); 342 warning_flat("", OPTIONS_PREFIX); 343 for (i, item) in CATEGORIES.iter().enumerate() { 344 if i < CATEGORIES.len() - 1 { 345 eprint!("{}, ", item); 346 continue; 347 } 348 eprint!("{}", item); 349 } 350 } 351 } else { 352 error("Insufficient arguments for command 'next'"); 353 warning("addseason <category>", USAGE_PREFIX); 354 return Ok(()); 355 } 356 } else if args[1].to_ascii_lowercase().as_str() == "categories" { 357 for category in &CATEGORIES { 358 println!("{}", category); 359 } 360 } else if args[1].to_ascii_lowercase().as_str() == "rank" { 361 if args.len() >= 3 { 362 struct Obj { 363 pub name: String, 364 pub season: String, 365 pub rating: i32, 366 pub _studio: String 367 } 368 369 let mut highest_rated: Vec<Obj> = vec![]; 370 if CATEGORIES.contains(&args[2].to_ascii_lowercase().as_str()) { 371 for (key, value) in data[&args[2].to_ascii_lowercase()].entries() { 372 for (key2, value2) in value.entries() { 373 if key2 != "disname" && key2 != "status" { 374 if json::stringify(value2["rating"].to_string()).replace("\"", "") != "0" { 375 highest_rated.push(Obj { 376 name: json::stringify(value["disname"].to_string()).replace("\"", ""), 377 season: json::stringify(value2["disname"].to_string()).replace("\"", ""), 378 rating: json::stringify(value2["rating"].to_string()).replace("\"", "").parse::<i32>().unwrap(), 379 _studio: json::stringify(value2["studio"].to_string()) 380 }); 381 } 382 } 383 } 384 } 385 } else if &args[2].to_ascii_lowercase().as_str() == &"all" { 386 for category in CATEGORIES { 387 for (key, value) in data[category].entries() { 388 for (key2, value2) in value.entries() { 389 if key2 != "disname" && key2 != "status" { 390 if json::stringify(value2["rating"].to_string()).replace("\"", "") != "0" && json::stringify(value2["rating"].to_string()).replace("\"", "") != "null" { 391 highest_rated.push(Obj { 392 name: json::stringify(value["disname"].to_string()).replace("\"", ""), 393 season: json::stringify(value2["disname"].to_string()).replace("\"", ""), 394 rating: json::stringify(value2["rating"].to_string()).replace("\"", "").parse::<i32>().unwrap(), 395 _studio: json::stringify(value2["studio"].to_string()) 396 }); 397 } 398 } 399 } 400 } 401 } 402 } else { 403 error("Argument 2 of command 'rank' must be a category or 'all'"); 404 } 405 406 highest_rated.sort_by_key(|k| k.rating); 407 highest_rated.reverse(); 408 for i in highest_rated { 409 println!("Name: {}, Season: {}, Rating: {}", i.name.cyan(), i.season.cyan(), i.rating.to_string().cyan()); 410 } 411 } 412 } else if args[1].to_ascii_lowercase().as_str() == "help" { 413 println!("add <name> <category> Adds the specified media to the specified category."); 414 println!("edit <name> <category> Opens the JSON for the specified media in the specified category."); 415 println!("editstatus <status> <name> <category> Changes the status of the specified media in the specified category."); 416 println!("editseason <season> <edit> <name> <category> Opens the given edit region for the specified season."); 417 println!(" Edit Regions: studio, rating, notes, json"); 418 println!("addseason <season> <name> <category> Adds a season with the specified name."); 419 println!("next <category> Prints the next media in the specified category that has the status 'planned'."); 420 println!("categories Prints all available categories."); 421 println!("rank <category || 'all'> Prints the media in the specified category or in 'all' ranked by the rating specified."); 422 } else { 423 error_flat("Could not recognize command '"); 424 print!("{}'!", args[1]); 425 return Ok(()); 426 } 427 428 let mut path: String; 429 430 if cfg!(windows) { 431 path = dirs::home_dir().unwrap().into_os_string().into_string().unwrap(); 432 path.push_str("\\medialog.json"); 433 } else if cfg!(unix) { 434 path = dirs::home_dir().unwrap().into_os_string().into_string().unwrap(); 435 path.push_str("/medialog.json"); 436 } else { 437 path = String::from("medialog.json"); 438 } 439 440 let mut file = File::create(&path)?; 441 file.write_all(json::stringify(data).as_bytes())?; 442 Ok(()) 443}