diff --git a/Cargo.lock b/Cargo.lock index ad0f9cf..1f55358 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -44,6 +44,55 @@ dependencies = [ "libc", ] +[[package]] +name = "anstream" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0ca84f3628370c59db74ee214b3263d58f9aadd9b4fe7e711fd87dc452b7f163" +dependencies = [ + "anstyle", + "anstyle-parse", + "anstyle-query", + "anstyle-wincon", + "colorchoice", + "is-terminal", + "utf8parse", +] + +[[package]] +name = "anstyle" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3a30da5c5f2d5e72842e00bcb57657162cdabef0931f40e2deb9b4140440cecd" + +[[package]] +name = "anstyle-parse" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "938874ff5980b03a87c5524b3ae5b59cf99b1d6bc836848df7bc5ada9643c333" +dependencies = [ + "utf8parse", +] + +[[package]] +name = "anstyle-query" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5ca11d4be1bab0c8bc8734a9aa7bf4ee8316d462a08c6ac5052f888fef5b494b" +dependencies = [ + "windows-sys", +] + +[[package]] +name = "anstyle-wincon" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "180abfa45703aebe0093f79badacc01b8fd4ea2e35118747e5811127f926e188" +dependencies = [ + "anstyle", + "windows-sys", +] + [[package]] name = "autocfg" version = "1.1.0" @@ -122,6 +171,40 @@ dependencies = [ "winapi", ] +[[package]] +name = "clap" +version = "4.3.19" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5fd304a20bff958a57f04c4e96a2e7594cc4490a0e809cbd48bb6437edaa452d" +dependencies = [ + "clap_builder", +] + +[[package]] +name = "clap_builder" +version = "4.3.19" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "01c6a3f08f1fe5662a35cfe393aec09c4df95f60ee93b7556505260f75eee9e1" +dependencies = [ + "anstream", + "anstyle", + "clap_lex", + "once_cell", + "strsim", +] + +[[package]] +name = "clap_lex" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2da6da31387c7e4ef160ffab6d5e7f00c42626fe39aea70a7b0f1773f7dd6c1b" + +[[package]] +name = "colorchoice" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "acbf1af155f9b9ef647e42cdc158db4b64a1b61f743629225fde6f3e0be2a7c7" + [[package]] name = "core-foundation" version = "0.9.3" @@ -166,6 +249,7 @@ name = "daily-kanji" version = "0.1.0" dependencies = [ "chrono", + "clap", "ego-tree", "futures", "hyper", @@ -538,6 +622,17 @@ dependencies = [ "hashbrown", ] +[[package]] +name = "is-terminal" +version = "0.4.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cb0889898416213fab133e1d33a0e5858a48177452750691bde3666d0fdbaf8b" +dependencies = [ + "hermit-abi", + "rustix", + "windows-sys", +] + [[package]] name = "itoa" version = "1.0.9" @@ -1102,6 +1197,12 @@ dependencies = [ "quote", ] +[[package]] +name = "strsim" +version = "0.10.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "73473c0e59e6d5812c5dfe2a064a6444949f089e20eec9a2e5506596494e4623" + [[package]] name = "syn" version = "1.0.109" @@ -1250,6 +1351,12 @@ version = "0.7.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "09cc8ee72d2a9becf2f2febe0205bbed8fc6615b7cb429ad062dc7b7ddd036a9" +[[package]] +name = "utf8parse" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "711b9620af191e0cdc7468a8d14e709c3dcdb115b36f838e601583af800a370a" + [[package]] name = "vcpkg" version = "0.2.15" diff --git a/Cargo.toml b/Cargo.toml index c943dd5..5ee5b8a 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -7,6 +7,7 @@ edition = "2021" [dependencies] chrono = "0.4.26" +clap = { version = "4.3.19", features = ["cargo"] } ego-tree = "0.6.2" futures = "0.3.28" hyper = { version = "0.14.27", features = ["http2", "client", "http1"] } diff --git a/src/main.rs b/src/main.rs index 4e5c53d..a4ddafa 100644 --- a/src/main.rs +++ b/src/main.rs @@ -6,6 +6,8 @@ use hyper::Uri; use chrono::naive::NaiveDate; use chrono::Datelike; +use chrono::Utc; +use chrono::FixedOffset; use hyper_tls::HttpsConnector; @@ -14,49 +16,129 @@ use scraper::Selector; use scraper::ElementRef; use scraper::Node; -use ego_tree::NodeRef; - use tokio::runtime::Runtime; use json::JsonValue; -use json::stringify; use json::object; +use clap::command; +use clap::Command; +use clap::Arg; +use clap::ArgAction; +use clap::ArgMatches; + #[derive(Debug, Clone)] struct Question { question: String, answer: String, } -impl Question { - fn to_json(&self) -> JsonValue { - //TODO: fix this syntax to match the actual cards - object!{ - question: self.question.clone(), - answer: self.answer.clone(), +struct Anki { + uri: Uri, + deck_id: u64, + model_id: u64, + question_field: String, + answer_field: String, +} + +impl Anki { + fn new() -> Self { + Anki { + uri: Uri::from_static("http://localhost:8765"), + deck_id: 1683334432853, //TODO: use these to compute name + model_id: 1679827221996, //TODO: use these to compute name + question_field: String::from("表面"), + answer_field: String::from("裏面"), } } + + async fn command(&self, action: &str, params: JsonValue) -> Result { + let payload = object!{ + //action: "addNotes", + action: action, + version: 6, + params: params, + }; + let client = Client::new(); + let request = Request::builder() + .uri(self.uri.clone()) + .body(Body::from(json::stringify(payload))).unwrap(); + let response = client.request(request).await.unwrap(); + let body_bytes = hyper::body::to_bytes(response).await.unwrap().to_vec(); + let payload = std::str::from_utf8(&body_bytes).map(json::parse).unwrap().unwrap(); + //println!("{:?}", payload); + Ok(payload["result"].clone()) + } + + async fn list_decks(&self) -> Vec<(String, u64)> { + self.command("deckNamesAndIds", object!{}).await.unwrap().entries() + .map(|(name, id)| (name.to_string(), id.as_u64().unwrap())).collect() + } + + async fn list_models(&self) -> Vec<(String, u64)> { + self.command("modelNamesAndIds", object!{}).await.unwrap().entries() + .map(|(name, id)| (name.to_string(), id.as_u64().unwrap())).collect() + } + + async fn model_fields(&self, model: &str) -> Vec { + self.command("modelFieldNames", object!{modelName: model}).await.unwrap().members().map(JsonValue::to_string).collect() + } + + async fn add_notes(&self, questions: Vec) -> Result { + let deck_name = lookup_name(self.deck_id, &self.list_decks().await); + let model_name = lookup_name(self.model_id, &self.list_models().await); + let jsonify_question = |question: &Question| { + let mut fields = JsonValue::new_object(); + fields[self.question_field.clone()] = JsonValue::String(question.question.clone()); + fields[self.answer_field.clone()] = JsonValue::String(question.answer.clone()); + object!{ + deckName: deck_name.clone(), + modelName: model_name.clone(), + fields: fields, + tags: ["毎日漢字"], + } + }; + let json_questions = questions.iter().map(jsonify_question).collect::>(); + println!("Questions {:?}", json_questions); + self.command("addNotes", object!{notes: json_questions}).await + } } -struct Anki { - uri: Uri, - deck_id: String, +fn lookup_name(lookup_id: u64, names: &Vec<(String, u64)>) -> Option { + for (name, id) in names { + if *id == lookup_id { + return Some(name.to_string()) + } + } + None } fn main() { - //ensure datasets are loaded - // download pages - // parse questions - //ensure questions are in anki - println!("Hello, world!"); + let matches = command!() + .subcommand_required(true) + .subcommand( + Command::new("import") + .about("download data to Anki") + .arg(Arg::new("force").short('f').action(ArgAction::SetTrue).help("actually run the import")) + ) + .get_matches(); + + match matches.subcommand() { + Some(("import", sub_matches)) => cmd_import(sub_matches), + _ => unreachable!("No subcommand provided!") + } + +} + +fn cmd_import(args: &ArgMatches) { + let rt = Runtime::new().unwrap(); + // the window of available data is like today - 9 days + let today = Utc::now().with_timezone(&FixedOffset::east_opt(9 * 3600).unwrap()).date_naive(); // TODO: cache downloaded pages // TODO: have an overall cacheing strategy - let rt = Runtime::new().unwrap(); - // the window of available data is like today - 15 days or something - let for_date = NaiveDate::from_ymd_opt(2023, 7, 27).unwrap(); // TODO: replace with today - //let questions = rt.block_on(scrape_questions(for_date)); - let anki = Anki{ uri: Uri::from_static("http://localhost:8765"), deck_id: "lol".to_string()}; - //println!("Questions: {:?}", questions); - rt.block_on(ensure_in_anki(&anki, vec![])); + let questions = rt.block_on(scrape_questions(today)).unwrap(); + println!("Questions: {:?}", questions); + let anki = Anki::new(); + rt.block_on(ensure_in_anki(&anki, questions)); //rt.block_on(ensure_in_anki(&anki, questions.unwrap())); } @@ -89,7 +171,7 @@ fn parse_question(element: ElementRef) -> Option { .take_while(|x| ElementRef::wrap(*x) != question_end) .map(|node| match node.value() { Node::Text(text) => String::from(text.text.trim()), - Node::Element(_element) => format!("{}", node.first_child().unwrap().value().as_text().unwrap().text.trim()), + Node::Element(_element) => format!("{}", node.first_child().unwrap().value().as_text().unwrap().text.trim()), _ => panic!("fresh peach heart shower"), }) .collect::>() @@ -114,25 +196,7 @@ fn not_in_anki(_question: &Question) -> bool { } async fn upload_to_anki(anki: &Anki, questions: Vec) -> Result<(), String> { - let notes = JsonValue::Array(questions.iter().map(Question::to_json).collect()); - let payload = object!{ - //action: "addNotes", - action: "deckNamesAndIds", - version: 6, - params: { - //notes: notes, - }, - }; - let client = Client::new(); - let request = Request::builder() - .uri(Uri::from_static("http://localhost:8765")) - .body(Body::from(json::stringify(payload))).unwrap(); - let response = client.request(request).await.unwrap(); - let body_bytes = hyper::body::to_bytes(response).await.unwrap().to_vec(); - let payload = json::parse(std::str::from_utf8(&body_bytes).unwrap()).unwrap(); - println!("Data: {:?}", payload["result"]); - for (deckName, id) in payload["result"].entries() { - println!("Deck: {} {}", deckName, id.as_u64().unwrap()); - } + let result = anki.add_notes(questions).await; + println!("Result: {:?}", result); Ok(()) }