diff --git a/Cargo.lock b/Cargo.lock index 3832660..7182f4e 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -322,6 +322,7 @@ dependencies = [ "dirs", "isahc", "regex", + "serde_json", "termsize", ] @@ -548,6 +549,12 @@ version = "0.6.26" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "49b3de9ec5dc0a3417da371aab17d729997c15010e7fd24ff707773a33bddb64" +[[package]] +name = "ryu" +version = "1.0.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4501abdff3ae82a1c1b477a17252eb69cee9e66eb915c1abaa4f44d873df9f09" + [[package]] name = "schannel" version = "0.1.20" @@ -558,6 +565,23 @@ dependencies = [ "windows-sys", ] +[[package]] +name = "serde" +version = "1.0.142" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e590c437916fb6b221e1d00df6e3294f3fccd70ca7e92541c475d6ed6ef5fee2" + +[[package]] +name = "serde_json" +version = "1.0.83" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "38dd04e3c8279e75b31ef29dbdceebfe5ad89f4d0937213c53f7d49d01b3d5a7" +dependencies = [ + "itoa", + "ryu", + "serde", +] + [[package]] name = "slab" version = "0.4.6" diff --git a/Cargo.toml b/Cargo.toml index d4d6510..551ec8c 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -14,3 +14,4 @@ isahc = "1.7.2" base64 = "0.13" termsize = "0.1.6" dirs = "4.0" +serde_json = "1.0.83" diff --git a/src/anime/anime.rs b/src/anime/anime.rs index 4190ff3..9586e8f 100644 --- a/src/anime/anime.rs +++ b/src/anime/anime.rs @@ -1,10 +1,12 @@ use crate::main; use crate::open_video; use crate::{anime_ep_range, anime_link, anime_names}; +use crate::{get_anime_id, get_token, get_user_anime_progress, update_anime_progress}; use crate::{int_input, string_input}; use colored::Colorize; //use crate pub fn anime_stream(search: String, episode: u32) { + let token = get_token(); let query = if search != "" { search } else { @@ -40,9 +42,11 @@ pub fn anime_stream(search: String, episode: u32) { let title = &anime_list[anime_num]; let ep_range = anime_ep_range(title); // if there is only one episode, then don't ask user to choose episode + let id = get_anime_id(&title.replace("-", " ")); if ep_range == 1 { let link = anime_link(title, 1); open_video(link); + update_anime_progress(id, &title.replace("-", " "), 1, &token); main(); } else { let mut ep_num: usize = usize::MAX; @@ -52,6 +56,8 @@ pub fn anime_stream(search: String, episode: u32) { } else if episode != 0 { ep_num = episode as usize; } else { + let current_progress = get_user_anime_progress(id, &token); + println!("you are currently on episode: {}", current_progress); println!("select episode 1-{}: ", ep_range); while ep_num == usize::max_value() || ep_num > ep_range as usize { ep_num = int_input("Enter episode number: "); @@ -63,6 +69,9 @@ pub fn anime_stream(search: String, episode: u32) { loop { let link = anime_link(title, ep_num as u64); open_video(link); + let id = get_anime_id(&title.replace("-", " ")); + println!("{}", get_user_anime_progress(id, &token)); + update_anime_progress(id, &title.replace("-", " "), ep_num, &token); println!("{}", "n: next episode".green()); println!("{}", "p: previous episode".yellow()); println!("{}", "s: search another anime".green()); diff --git a/src/anime/mod.rs b/src/anime/mod.rs index a2c4f5a..0c435d5 100644 --- a/src/anime/mod.rs +++ b/src/anime/mod.rs @@ -1,3 +1,4 @@ +pub mod anime; pub mod player; pub mod scraper; -pub mod anime; +pub mod trackers; diff --git a/src/anime/trackers.rs b/src/anime/trackers.rs new file mode 100644 index 0000000..0097485 --- /dev/null +++ b/src/anime/trackers.rs @@ -0,0 +1,183 @@ +use crate::string_input; +use isahc::{ReadResponseExt, Request, RequestExt}; +use serde_json::json; +use std::fs; + +pub fn get_token() -> String { + //if not on windows create folder ~/.config/kami + let config_path = dirs::config_dir().unwrap().join("kami"); + if !config_path.exists() { + fs::create_dir_all(&config_path).unwrap(); + } + let token_path = config_path.join("token.txt"); + if !token_path.exists() { + //create empty file + fs::File::create(&token_path).unwrap(); + } else { + //read token from file + let token = fs::read_to_string(&token_path).unwrap(); + if token.is_empty() { + println!("please go to the below link and copy and past the token below"); + println!( + "https://anilist.co/api/v2/oauth/authorize?client_id=9121&response_type=token" + ); + let token = string_input("token: "); + fs::write(&token_path, token).unwrap(); + } + } + let token = fs::read_to_string(&token_path).unwrap(); + token +} + +pub fn get_anime_id(anime: &str) -> i32 { + const QUERY: &str = " +query ($id: Int, $search: String) { + Media (id: $id, search: $search, type: ANIME) { + id + title { + native + romaji + english + } + } +} +"; + let json = json!({ + "query": QUERY, + "variables": { + "search": anime, + } + }); + let resp = Request::builder() + .method("POST") + .uri("https://graphql.anilist.co/") + .header("Content-Type", "application/json") + .header("Accept", "application/json") + .body(json.to_string()) + .unwrap() + .send() + .unwrap() + .text(); + //println!("{}", resp); + let regex = regex::Regex::new(r#"id":(.*?),"#).unwrap(); + let resp: String = resp.as_ref().unwrap().to_string(); + let id = regex + .captures(&resp) + .unwrap() + .get(1) + .unwrap() + .as_str() + .parse::() + .unwrap(); + id +} + +//get the user id from the token +fn get_user_id(token: &str) -> i32 { + const QUERY: &str = "query { + Viewer { + id + } +}"; + let json = json!({ "query": QUERY }); + let resp = Request::builder() + .method("POST") + .uri("https://graphql.anilist.co/") + .header("Content-Type", "application/json") + .header("Accept", "application/json") + .header("Authorization", format!("Bearer {}", token)) + .body(json.to_string()) + .unwrap() + .send() + .unwrap() + .text(); + //println!("{}", resp); + let regex = regex::Regex::new(r#"id":(.*?)}"#).unwrap(); + let resp: String = resp.as_ref().unwrap().to_string(); + let id = regex + .captures(&resp) + .unwrap() + .get(1) + .unwrap() + .as_str() + .parse::() + .unwrap(); + id +} + +pub fn get_user_anime_progress(anime_id: i32, token: &str) -> i32 { + let user_id = get_user_id(&token); + const QUERY: &str = "query ($user_id: Int, $media_id: Int) { + MediaList (userId: $user_id, mediaId: $media_id, type: ANIME) { + progress + } +}"; + let json = json!({ + "query": QUERY, + "variables": { + "user_id": user_id, + "media_id": anime_id, + } + }); + let resp = Request::builder() + .method("POST") + .uri("https://graphql.anilist.co/") + .header("Content-Type", "application/json") + .header("Accept", "application/json") + .header("Authorization", format!("Bearer {}", token)) + .body(json.to_string()) + .unwrap() + .send() + .unwrap() + .text(); + //println!("{}", resp); + let regex = regex::Regex::new(r#"progress":(.*?)}"#).unwrap(); + let resp: String = resp.as_ref().unwrap().to_string(); + //if resp contains "404"set progress to 1 + // else set progress to the number in the regex + if resp.contains("404") { + 1 + } else { + let progress = regex + .captures(&resp) + .unwrap() + .get(1) + .unwrap() + .as_str() + .parse::() + .unwrap(); + progress + } +} + +pub fn update_anime_progress(anime_id: i32, anime: &str, progress: usize, token: &str) { + const UPDATE: &str = " +mutation ($mediaId: Int, $status: MediaListStatus, $progress: Int) { + SaveMediaListEntry (mediaId: $mediaId, status: $status, progress: $progress) { + id + status + progress + } +} +"; + let json = json!({ + "query": UPDATE, + "variables": { + "mediaId": anime_id, + "status": "CURRENT", + "progress": progress + } + }); + let _resp = Request::builder() + .method("POST") + .uri("https://graphql.anilist.co/") + .header("Content-Type", "application/json") + .header("Accept", "application/json") + .header("Authorization", format!("Bearer {}", token)) + .body(json.to_string()) + .unwrap() + .send() + .unwrap() + .text(); + println!("updated progress of {} to episode {}", anime, progress); +} diff --git a/src/main.rs b/src/main.rs index 5cfa5d1..bf1fb32 100644 --- a/src/main.rs +++ b/src/main.rs @@ -4,12 +4,13 @@ mod ln; use anime::anime::anime_stream; use colored::Colorize; -use ln::{scraper::get_ln_next_page, ln::ln_read}; use ln::search::search_ln; +use ln::{ln::ln_read, scraper::get_ln_next_page}; use crate::anime::{ player::open_video, scraper::{anime_ep_range, anime_link, anime_names}, + trackers::*, }; use crate::helpers::take_input::{int_input, string_input}; use crate::ln::{menu::chapter_selector, open_text::open_bat, scraper::get_full_text}; @@ -35,7 +36,6 @@ fn main() { search = arg; } } - } if arg == "--ln" || arg == "-l" { ln = true; @@ -49,21 +49,21 @@ fn main() { if arg == "--chapter" || arg == "-c" { if let Some(arg) = std::env::args().nth(count + 1) { chapter = arg.parse::().unwrap(); - }else{ + } else { chapter = 0; } } if arg == "--episode" || arg == "-e" { if let Some(arg) = std::env::args().nth(count + 1) { episode = arg.parse::().unwrap(); - }else{ + } else { episode = 0; } } count += 1; } - if help == true{ + if help == true { print_help(); } if anime == false && ln == false { @@ -71,11 +71,11 @@ fn main() { println!("2: Light Novel"); let a = int_input("pick your poison: "); - match a{ - 1 => anime = true, - 2 => ln = true, - _=>println!("invalid option. ") - }; + match a { + 1 => anime = true, + 2 => ln = true, + _ => println!("invalid option. "), + }; } if anime == true && ln == true { println!("you can only use one of the arguments at a time"); @@ -94,24 +94,36 @@ fn page_selector(ln_id: &str, selected_page: u32) -> String { get_ln_next_page(ln_id, &selected_page.to_string()) } -fn print_help(){ +fn print_help() { println!("anime:\t\t{}", format_args!("{}", "-a --anime".red())); - println!("{}", "after this^^^ argument you can enter a search term".green()); + println!( + "{}", + "after this^^^ argument you can enter a search term".green() + ); println!("{}", "for exaple kami -a \"one piece\""); //print blank line println!(""); println!("episode:\t{}", format_args!("{}", "-e --episode".red())); - println!("{}", "after this^^^ argument you can enter a chapter number".green()); + println!( + "{}", + "after this^^^ argument you can enter a chapter number".green() + ); println!("{}", "for exaple kami -c 200"); //print blank line println!(""); println!("light novel:\t{}", format_args!("{}", "-l --ln".red())); - println!("{}", "after this^^^ argument you can enter a search term".green()); + println!( + "{}", + "after this^^^ argument you can enter a search term".green() + ); println!("{}", "for exaple kami -l \"one piece\""); //print blank line println!(""); println!("chapter:\t{}", format_args!("{}", "-c --chapter".red())); - println!("{}", "after this^^^ argument you can enter a chapter number".green()); + println!( + "{}", + "after this^^^ argument you can enter a chapter number".green() + ); println!("{}", "for exaple kami -c 200"); //print blank line println!("");