A simple tool to log and rate the media you have consumed.
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}