From 27c8c76dfe165ada1cfd6fafcb1f8e6160216a5f Mon Sep 17 00:00:00 2001 From: blank X Date: Tue, 16 Feb 2021 19:38:06 +0700 Subject: [PATCH] Initial commit --- .gitignore | 1 + Cargo.lock | 1085 ++++++++++++++++++++++++++++++++++++++ Cargo.toml | 19 + LICENSE | 21 + src/commands/download.rs | 194 +++++++ src/commands/feed.rs | 104 ++++ src/commands/mod.rs | 6 + src/commands/view.rs | 78 +++ src/main.rs | 102 ++++ src/structs.rs | 173 ++++++ src/utils.rs | 56 ++ 11 files changed, 1839 insertions(+) create mode 100644 .gitignore create mode 100644 Cargo.lock create mode 100644 Cargo.toml create mode 100644 LICENSE create mode 100644 src/commands/download.rs create mode 100644 src/commands/feed.rs create mode 100644 src/commands/mod.rs create mode 100644 src/commands/view.rs create mode 100644 src/main.rs create mode 100644 src/structs.rs create mode 100644 src/utils.rs diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..ea8c4bf --- /dev/null +++ b/.gitignore @@ -0,0 +1 @@ +/target diff --git a/Cargo.lock b/Cargo.lock new file mode 100644 index 0000000..3c46f03 --- /dev/null +++ b/Cargo.lock @@ -0,0 +1,1085 @@ +# This file is automatically @generated by Cargo. +# It is not intended for manual editing. +[[package]] +name = "aho-corasick" +version = "0.7.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7404febffaa47dac81aa44dba71523c9d069b1bdc50a77db41195149e17f68e5" +dependencies = [ + "memchr", +] + +[[package]] +name = "autocfg" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cdb031dd78e28731d87d56cc8ffef4a8f36ca26c38fe2de700543e627f8a464a" + +[[package]] +name = "base64" +version = "0.13.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "904dfeac50f3cdaba28fc6f57fdcddb75f49ed61346676a78c4ffe55877802fd" + +[[package]] +name = "bitflags" +version = "1.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cf1de2fe8c75bc145a2f577add951f8134889b4795d47466a54a5c846d691693" + +[[package]] +name = "bumpalo" +version = "3.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "099e596ef14349721d9016f6b80dd3419ea1bf289ab9b44df8e4dfd3a005d5d9" + +[[package]] +name = "bytes" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b700ce4376041dcd0a327fd0097c41095743c4c8af8887265942faf1100bd040" + +[[package]] +name = "cc" +version = "1.0.66" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4c0496836a84f8d0495758516b8621a622beb77c0fed418570e50764093ced48" + +[[package]] +name = "cfg-if" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd" + +[[package]] +name = "clap" +version = "2.33.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "37e58ac78573c40708d45522f0d80fa2f01cc4f9b4e2bf749807255454312002" +dependencies = [ + "bitflags", + "textwrap", + "unicode-width", +] + +[[package]] +name = "core-foundation" +version = "0.9.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0a89e2ae426ea83155dccf10c0fa6b1463ef6d5fcb44cee0b224a408fa640a62" +dependencies = [ + "core-foundation-sys", + "libc", +] + +[[package]] +name = "core-foundation-sys" +version = "0.8.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ea221b5284a47e40033bf9b66f35f984ec0ea2931eb03505246cd27a963f981b" + +[[package]] +name = "encoding_rs" +version = "0.8.28" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "80df024fbc5ac80f87dfef0d9f5209a252f2a497f7f42944cff24d8253cac065" +dependencies = [ + "cfg-if", +] + +[[package]] +name = "fnv" +version = "1.0.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3f9eec918d3f24069decb9af1554cad7c880e2da24a9afd88aca000531ab82c1" + +[[package]] +name = "foreign-types" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f6f339eb8adc052cd2ca78910fda869aefa38d22d5cb648e6485e4d3fc06f3b1" +dependencies = [ + "foreign-types-shared", +] + +[[package]] +name = "foreign-types-shared" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "00b0228411908ca8685dba7fc2cdd70ec9990a6e753e89b6ac91a84c40fbaf4b" + +[[package]] +name = "form_urlencoded" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ece68d15c92e84fa4f19d3780f1294e5ca82a78a6d515f1efaabcc144688be00" +dependencies = [ + "matches", + "percent-encoding", +] + +[[package]] +name = "futures-channel" +version = "0.3.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f2d31b7ec7efab6eefc7c57233bb10b847986139d88cc2f5a02a1ae6871a1846" +dependencies = [ + "futures-core", +] + +[[package]] +name = "futures-core" +version = "0.3.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "79e5145dde8da7d1b3892dad07a9c98fc04bc39892b1ecc9692cf53e2b780a65" + +[[package]] +name = "futures-sink" +version = "0.3.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "caf5c69029bda2e743fddd0582d1083951d65cc9539aebf8812f36c3491342d6" + +[[package]] +name = "futures-task" +version = "0.3.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "13de07eb8ea81ae445aca7b69f5f7bf15d7bf4912d8ca37d6645c77ae8a58d86" + +[[package]] +name = "futures-util" +version = "0.3.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "632a8cd0f2a4b3fdea1657f08bde063848c3bd00f9bbf6e256b8be78802e624b" +dependencies = [ + "futures-core", + "futures-task", + "pin-project-lite", + "pin-utils", +] + +[[package]] +name = "getrandom" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c9495705279e7140bf035dde1f6e750c162df8b625267cd52cc44e0b156732c8" +dependencies = [ + "cfg-if", + "libc", + "wasi", +] + +[[package]] +name = "h2" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6b67e66362108efccd8ac053abafc8b7a8d86a37e6e48fc4f6f7485eb5e9e6a5" +dependencies = [ + "bytes", + "fnv", + "futures-core", + "futures-sink", + "futures-util", + "http", + "indexmap", + "slab", + "tokio", + "tokio-util", + "tracing", + "tracing-futures", +] + +[[package]] +name = "hashbrown" +version = "0.9.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d7afe4a420e3fe79967a00898cc1f4db7c8a49a9333a29f8a4bd76a253d5cd04" + +[[package]] +name = "hermit-abi" +version = "0.1.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "322f4de77956e22ed0e5032c359a0f1273f1f7f0d79bfa3b8ffbc730d7fbcc5c" +dependencies = [ + "libc", +] + +[[package]] +name = "http" +version = "0.2.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7245cd7449cc792608c3c8a9eaf69bd4eabbabf802713748fd739c98b82f0747" +dependencies = [ + "bytes", + "fnv", + "itoa", +] + +[[package]] +name = "http-body" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2861bd27ee074e5ee891e8b539837a9430012e249d7f0ca2d795650f579c1994" +dependencies = [ + "bytes", + "http", +] + +[[package]] +name = "httparse" +version = "1.3.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "615caabe2c3160b313d52ccc905335f4ed5f10881dd63dc5699d47e90be85691" + +[[package]] +name = "httpdate" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "494b4d60369511e7dea41cf646832512a94e542f68bb9c49e54518e0f468eb47" + +[[package]] +name = "hyper" +version = "0.14.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e8e946c2b1349055e0b72ae281b238baf1a3ea7307c7e9f9d64673bdd9c26ac7" +dependencies = [ + "bytes", + "futures-channel", + "futures-core", + "futures-util", + "h2", + "http", + "http-body", + "httparse", + "httpdate", + "itoa", + "pin-project 1.0.5", + "socket2", + "tokio", + "tower-service", + "tracing", + "want", +] + +[[package]] +name = "hyper-tls" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d6183ddfa99b85da61a140bea0efc93fdf56ceaa041b37d553518030827f9905" +dependencies = [ + "bytes", + "hyper", + "native-tls", + "tokio", + "tokio-native-tls", +] + +[[package]] +name = "idna" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "de910d521f7cc3135c4de8db1cb910e0b5ed1dc6f57c381cd07e8e661ce10094" +dependencies = [ + "matches", + "unicode-bidi", + "unicode-normalization", +] + +[[package]] +name = "indexmap" +version = "1.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4fb1fa934250de4de8aef298d81c729a7d33d8c239daa3a7575e6b92bfc7313b" +dependencies = [ + "autocfg", + "hashbrown", +] + +[[package]] +name = "ipnet" +version = "2.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "47be2f14c678be2fdcab04ab1171db51b2762ce6f0a8ee87c8dd4a04ed216135" + +[[package]] +name = "itoa" +version = "0.4.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dd25036021b0de88a0aff6b850051563c6516d0bf53f8638938edbb9de732736" + +[[package]] +name = "js-sys" +version = "0.3.47" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5cfb73131c35423a367daf8cbd24100af0d077668c8c2943f0e7dd775fef0f65" +dependencies = [ + "wasm-bindgen", +] + +[[package]] +name = "lazy_static" +version = "1.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e2abad23fbc42b3700f2f279844dc832adb2b2eb069b2df918f455c4e18cc646" + +[[package]] +name = "libc" +version = "0.2.86" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b7282d924be3275cec7f6756ff4121987bc6481325397dde6ba3e7802b1a8b1c" + +[[package]] +name = "log" +version = "0.4.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "51b9bbe6c47d51fc3e1a9b945965946b4c44142ab8792c50835a980d362c2710" +dependencies = [ + "cfg-if", +] + +[[package]] +name = "mangadexrs" +version = "0.1.0" +dependencies = [ + "clap", + "quick-xml", + "regex", + "reqwest", + "serde", + "serde_json", + "tokio", +] + +[[package]] +name = "matches" +version = "0.1.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7ffc5c5338469d4d3ea17d269fa8ea3512ad247247c30bd2df69e68309ed0a08" + +[[package]] +name = "memchr" +version = "2.3.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0ee1c47aaa256ecabcaea351eae4a9b01ef39ed810004e298d2511ed284b1525" + +[[package]] +name = "mime" +version = "0.3.16" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2a60c7ce501c71e03a9c9c0d35b861413ae925bd979cc7a4e30d060069aaac8d" + +[[package]] +name = "mio" +version = "0.7.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e50ae3f04d169fcc9bde0b547d1c205219b7157e07ded9c5aff03e0637cb3ed7" +dependencies = [ + "libc", + "log", + "miow", + "ntapi", + "winapi", +] + +[[package]] +name = "miow" +version = "0.3.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5a33c1b55807fbed163481b5ba66db4b2fa6cde694a5027be10fb724206c5897" +dependencies = [ + "socket2", + "winapi", +] + +[[package]] +name = "native-tls" +version = "0.2.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b8d96b2e1c8da3957d58100b09f102c6d9cfdfced01b7ec5a8974044bb09dbd4" +dependencies = [ + "lazy_static", + "libc", + "log", + "openssl", + "openssl-probe", + "openssl-sys", + "schannel", + "security-framework", + "security-framework-sys", + "tempfile", +] + +[[package]] +name = "ntapi" +version = "0.3.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3f6bb902e437b6d86e03cce10a7e2af662292c5dfef23b65899ea3ac9354ad44" +dependencies = [ + "winapi", +] + +[[package]] +name = "num_cpus" +version = "1.13.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "05499f3756671c15885fee9034446956fff3f243d6077b91e5767df161f766b3" +dependencies = [ + "hermit-abi", + "libc", +] + +[[package]] +name = "once_cell" +version = "1.5.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "13bd41f508810a131401606d54ac32a467c97172d74ba7662562ebba5ad07fa0" + +[[package]] +name = "openssl" +version = "0.10.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "038d43985d1ddca7a9900630d8cd031b56e4794eecc2e9ea39dd17aa04399a70" +dependencies = [ + "bitflags", + "cfg-if", + "foreign-types", + "lazy_static", + "libc", + "openssl-sys", +] + +[[package]] +name = "openssl-probe" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "77af24da69f9d9341038eba93a073b1fdaaa1b788221b00a69bce9e762cb32de" + +[[package]] +name = "openssl-sys" +version = "0.9.60" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "921fc71883267538946025deffb622905ecad223c28efbfdef9bb59a0175f3e6" +dependencies = [ + "autocfg", + "cc", + "libc", + "pkg-config", + "vcpkg", +] + +[[package]] +name = "percent-encoding" +version = "2.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d4fd5641d01c8f18a23da7b6fe29298ff4b55afcccdf78973b24cf3175fee32e" + +[[package]] +name = "pin-project" +version = "0.4.27" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2ffbc8e94b38ea3d2d8ba92aea2983b503cd75d0888d75b86bb37970b5698e15" +dependencies = [ + "pin-project-internal 0.4.27", +] + +[[package]] +name = "pin-project" +version = "1.0.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "96fa8ebb90271c4477f144354485b8068bd8f6b78b428b01ba892ca26caf0b63" +dependencies = [ + "pin-project-internal 1.0.5", +] + +[[package]] +name = "pin-project-internal" +version = "0.4.27" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "65ad2ae56b6abe3a1ee25f15ee605bacadb9a764edaba9c2bf4103800d4a1895" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "pin-project-internal" +version = "1.0.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "758669ae3558c6f74bd2a18b41f7ac0b5a195aea6639d6a9b5e5d1ad5ba24c0b" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "pin-project-lite" +version = "0.2.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "439697af366c49a6d0a010c56a0d97685bc140ce0d377b13a2ea2aa42d64a827" + +[[package]] +name = "pin-utils" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8b870d8c151b6f2fb93e84a13146138f05d02ed11c7e7c54f8826aaaf7c9f184" + +[[package]] +name = "pkg-config" +version = "0.3.19" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3831453b3449ceb48b6d9c7ad7c96d5ea673e9b470a1dc578c2ce6521230884c" + +[[package]] +name = "ppv-lite86" +version = "0.2.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ac74c624d6b2d21f425f752262f42188365d7b8ff1aff74c82e45136510a4857" + +[[package]] +name = "proc-macro2" +version = "1.0.24" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1e0704ee1a7e00d7bb417d0770ea303c1bccbabf0ef1667dae92b5967f5f8a71" +dependencies = [ + "unicode-xid", +] + +[[package]] +name = "quick-xml" +version = "0.20.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "26aab6b48e2590e4a64d1ed808749ba06257882b461d01ca71baeb747074a6dd" +dependencies = [ + "memchr", +] + +[[package]] +name = "quote" +version = "1.0.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "991431c3519a3f36861882da93630ce66b52918dcf1b8e2fd66b397fc96f28df" +dependencies = [ + "proc-macro2", +] + +[[package]] +name = "rand" +version = "0.8.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0ef9e7e66b4468674bfcb0c81af8b7fa0bb154fa9f28eb840da5c447baeb8d7e" +dependencies = [ + "libc", + "rand_chacha", + "rand_core", + "rand_hc", +] + +[[package]] +name = "rand_chacha" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e12735cf05c9e10bf21534da50a147b924d555dc7a547c42e6bb2d5b6017ae0d" +dependencies = [ + "ppv-lite86", + "rand_core", +] + +[[package]] +name = "rand_core" +version = "0.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c026d7df8b298d90ccbbc5190bd04d85e159eaf5576caeacf8741da93ccbd2e5" +dependencies = [ + "getrandom", +] + +[[package]] +name = "rand_hc" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3190ef7066a446f2e7f42e239d161e905420ccab01eb967c9eb27d21b2322a73" +dependencies = [ + "rand_core", +] + +[[package]] +name = "redox_syscall" +version = "0.2.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "05ec8ca9416c5ea37062b502703cd7fcb207736bc294f6e0cf367ac6fc234570" +dependencies = [ + "bitflags", +] + +[[package]] +name = "regex" +version = "1.4.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d9251239e129e16308e70d853559389de218ac275b515068abc96829d05b948a" +dependencies = [ + "aho-corasick", + "memchr", + "regex-syntax", + "thread_local", +] + +[[package]] +name = "regex-syntax" +version = "0.6.22" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b5eb417147ba9860a96cfe72a0b93bf88fee1744b5636ec99ab20c1aa9376581" + +[[package]] +name = "remove_dir_all" +version = "0.5.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3acd125665422973a33ac9d3dd2df85edad0f4ae9b00dafb1a05e43a9f5ef8e7" +dependencies = [ + "winapi", +] + +[[package]] +name = "reqwest" +version = "0.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fd281b1030aa675fb90aa994d07187645bb3c8fc756ca766e7c3070b439de9de" +dependencies = [ + "base64", + "bytes", + "encoding_rs", + "futures-core", + "futures-util", + "http", + "http-body", + "hyper", + "hyper-tls", + "ipnet", + "js-sys", + "lazy_static", + "log", + "mime", + "native-tls", + "percent-encoding", + "pin-project-lite", + "serde", + "serde_urlencoded", + "tokio", + "tokio-native-tls", + "url", + "wasm-bindgen", + "wasm-bindgen-futures", + "web-sys", + "winreg", +] + +[[package]] +name = "ryu" +version = "1.0.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "71d301d4193d031abdd79ff7e3dd721168a9572ef3fe51a1517aba235bd8f86e" + +[[package]] +name = "schannel" +version = "0.1.19" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8f05ba609c234e60bee0d547fe94a4c7e9da733d1c962cf6e59efa4cd9c8bc75" +dependencies = [ + "lazy_static", + "winapi", +] + +[[package]] +name = "security-framework" +version = "2.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c1759c2e3c8580017a484a7ac56d3abc5a6c1feadf88db2f3633f12ae4268c69" +dependencies = [ + "bitflags", + "core-foundation", + "core-foundation-sys", + "libc", + "security-framework-sys", +] + +[[package]] +name = "security-framework-sys" +version = "2.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f99b9d5e26d2a71633cc4f2ebae7cc9f874044e0c351a27e17892d76dce5678b" +dependencies = [ + "core-foundation-sys", + "libc", +] + +[[package]] +name = "serde" +version = "1.0.123" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "92d5161132722baa40d802cc70b15262b98258453e85e5d1d365c757c73869ae" +dependencies = [ + "serde_derive", +] + +[[package]] +name = "serde_derive" +version = "1.0.123" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9391c295d64fc0abb2c556bad848f33cb8296276b1ad2677d1ae1ace4f258f31" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "serde_json" +version = "1.0.62" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ea1c6153794552ea7cf7cf63b1231a25de00ec90db326ba6264440fa08e31486" +dependencies = [ + "itoa", + "ryu", + "serde", +] + +[[package]] +name = "serde_urlencoded" +version = "0.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "edfa57a7f8d9c1d260a549e7224100f6c43d43f9103e06dd8b4095a9b2b43ce9" +dependencies = [ + "form_urlencoded", + "itoa", + "ryu", + "serde", +] + +[[package]] +name = "slab" +version = "0.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c111b5bd5695e56cffe5129854aa230b39c93a305372fdbb2668ca2394eea9f8" + +[[package]] +name = "socket2" +version = "0.3.19" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "122e570113d28d773067fab24266b66753f6ea915758651696b6e35e49f88d6e" +dependencies = [ + "cfg-if", + "libc", + "winapi", +] + +[[package]] +name = "syn" +version = "1.0.60" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c700597eca8a5a762beb35753ef6b94df201c81cca676604f547495a0d7f0081" +dependencies = [ + "proc-macro2", + "quote", + "unicode-xid", +] + +[[package]] +name = "tempfile" +version = "3.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dac1c663cfc93810f88aed9b8941d48cabf856a1b111c29a40439018d870eb22" +dependencies = [ + "cfg-if", + "libc", + "rand", + "redox_syscall", + "remove_dir_all", + "winapi", +] + +[[package]] +name = "textwrap" +version = "0.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d326610f408c7a4eb6f51c37c330e496b08506c9457c9d34287ecc38809fb060" +dependencies = [ + "unicode-width", +] + +[[package]] +name = "thread_local" +version = "1.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8018d24e04c95ac8790716a5987d0fec4f8b27249ffa0f7d33f1369bdfb88cbd" +dependencies = [ + "once_cell", +] + +[[package]] +name = "tinyvec" +version = "1.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "317cca572a0e89c3ce0ca1f1bdc9369547fe318a683418e42ac8f59d14701023" +dependencies = [ + "tinyvec_macros", +] + +[[package]] +name = "tinyvec_macros" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cda74da7e1a664f795bb1f8a87ec406fb89a02522cf6e50620d016add6dbbf5c" + +[[package]] +name = "tokio" +version = "1.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e8190d04c665ea9e6b6a0dc45523ade572c088d2e6566244c1122671dbf4ae3a" +dependencies = [ + "autocfg", + "bytes", + "libc", + "memchr", + "mio", + "num_cpus", + "pin-project-lite", +] + +[[package]] +name = "tokio-native-tls" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f7d995660bd2b7f8c1568414c1126076c13fbb725c40112dc0120b78eb9b717b" +dependencies = [ + "native-tls", + "tokio", +] + +[[package]] +name = "tokio-util" +version = "0.6.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ebb7cb2f00c5ae8df755b252306272cd1790d39728363936e01827e11f0b017b" +dependencies = [ + "bytes", + "futures-core", + "futures-sink", + "log", + "pin-project-lite", + "tokio", +] + +[[package]] +name = "tower-service" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "360dfd1d6d30e05fda32ace2c8c70e9c0a9da713275777f5a4dbb8a1893930c6" + +[[package]] +name = "tracing" +version = "0.1.23" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f7d40a22fd029e33300d8d89a5cc8ffce18bb7c587662f54629e94c9de5487f3" +dependencies = [ + "cfg-if", + "pin-project-lite", + "tracing-core", +] + +[[package]] +name = "tracing-core" +version = "0.1.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f50de3927f93d202783f4513cda820ab47ef17f624b03c096e86ef00c67e6b5f" +dependencies = [ + "lazy_static", +] + +[[package]] +name = "tracing-futures" +version = "0.2.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ab7bb6f14721aa00656086e9335d363c5c8747bae02ebe32ea2c7dece5689b4c" +dependencies = [ + "pin-project 0.4.27", + "tracing", +] + +[[package]] +name = "try-lock" +version = "0.2.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "59547bce71d9c38b83d9c0e92b6066c4253371f15005def0c30d9657f50c7642" + +[[package]] +name = "unicode-bidi" +version = "0.3.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "49f2bd0c6468a8230e1db229cff8029217cf623c767ea5d60bfbd42729ea54d5" +dependencies = [ + "matches", +] + +[[package]] +name = "unicode-normalization" +version = "0.1.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "07fbfce1c8a97d547e8b5334978438d9d6ec8c20e38f56d4a4374d181493eaef" +dependencies = [ + "tinyvec", +] + +[[package]] +name = "unicode-width" +version = "0.1.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9337591893a19b88d8d87f2cec1e73fad5cdfd10e5a6f349f498ad6ea2ffb1e3" + +[[package]] +name = "unicode-xid" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f7fe0bb3479651439c9112f72b6c505038574c9fbb575ed1bf3b797fa39dd564" + +[[package]] +name = "url" +version = "2.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5909f2b0817350449ed73e8bcd81c8c3c8d9a7a5d8acba4b27db277f1868976e" +dependencies = [ + "form_urlencoded", + "idna", + "matches", + "percent-encoding", +] + +[[package]] +name = "vcpkg" +version = "0.2.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b00bca6106a5e23f3eee943593759b7fcddb00554332e856d990c893966879fb" + +[[package]] +name = "want" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1ce8a968cb1cd110d136ff8b819a556d6fb6d919363c61534f6860c7eb172ba0" +dependencies = [ + "log", + "try-lock", +] + +[[package]] +name = "wasi" +version = "0.10.2+wasi-snapshot-preview1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fd6fbd9a79829dd1ad0cc20627bf1ed606756a7f77edff7b66b7064f9cb327c6" + +[[package]] +name = "wasm-bindgen" +version = "0.2.70" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "55c0f7123de74f0dab9b7d00fd614e7b19349cd1e2f5252bbe9b1754b59433be" +dependencies = [ + "cfg-if", + "serde", + "serde_json", + "wasm-bindgen-macro", +] + +[[package]] +name = "wasm-bindgen-backend" +version = "0.2.70" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7bc45447f0d4573f3d65720f636bbcc3dd6ce920ed704670118650bcd47764c7" +dependencies = [ + "bumpalo", + "lazy_static", + "log", + "proc-macro2", + "quote", + "syn", + "wasm-bindgen-shared", +] + +[[package]] +name = "wasm-bindgen-futures" +version = "0.4.20" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3de431a2910c86679c34283a33f66f4e4abd7e0aec27b6669060148872aadf94" +dependencies = [ + "cfg-if", + "js-sys", + "wasm-bindgen", + "web-sys", +] + +[[package]] +name = "wasm-bindgen-macro" +version = "0.2.70" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3b8853882eef39593ad4174dd26fc9865a64e84026d223f63bb2c42affcbba2c" +dependencies = [ + "quote", + "wasm-bindgen-macro-support", +] + +[[package]] +name = "wasm-bindgen-macro-support" +version = "0.2.70" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4133b5e7f2a531fa413b3a1695e925038a05a71cf67e87dafa295cb645a01385" +dependencies = [ + "proc-macro2", + "quote", + "syn", + "wasm-bindgen-backend", + "wasm-bindgen-shared", +] + +[[package]] +name = "wasm-bindgen-shared" +version = "0.2.70" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dd4945e4943ae02d15c13962b38a5b1e81eadd4b71214eee75af64a4d6a4fd64" + +[[package]] +name = "web-sys" +version = "0.3.47" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c40dc691fc48003eba817c38da7113c15698142da971298003cac3ef175680b3" +dependencies = [ + "js-sys", + "wasm-bindgen", +] + +[[package]] +name = "winapi" +version = "0.3.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5c839a674fcd7a98952e593242ea400abe93992746761e38641405d28b00f419" +dependencies = [ + "winapi-i686-pc-windows-gnu", + "winapi-x86_64-pc-windows-gnu", +] + +[[package]] +name = "winapi-i686-pc-windows-gnu" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ac3b87c63620426dd9b991e5ce0329eff545bccbbb34f3be09ff6fb6ab51b7b6" + +[[package]] +name = "winapi-x86_64-pc-windows-gnu" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f" + +[[package]] +name = "winreg" +version = "0.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0120db82e8a1e0b9fb3345a539c478767c0048d842860994d96113d5b667bd69" +dependencies = [ + "winapi", +] diff --git a/Cargo.toml b/Cargo.toml new file mode 100644 index 0000000..85da373 --- /dev/null +++ b/Cargo.toml @@ -0,0 +1,19 @@ +[package] +name = "mangadexrs" +version = "0.1.0" +authors = ["blank X "] +edition = "2018" + +# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html + +[profile.release] +lto = true + +[dependencies] +tokio = { version = "1.1", features = ["rt-multi-thread"] } +reqwest = "0.11" +serde = { version = "1.0", features = ["derive"] } +serde_json = "1.0" +clap = { version = "2.33", default-features = false } +quick-xml = "0.20" +regex = "1.4" diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..211185a --- /dev/null +++ b/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2021 blank X + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/src/commands/download.rs b/src/commands/download.rs new file mode 100644 index 0000000..8a45ea8 --- /dev/null +++ b/src/commands/download.rs @@ -0,0 +1,194 @@ +use crate::utils; +use crate::structs; + +use std::sync::Arc; +use std::fs::create_dir_all; +use std::collections::VecDeque; +use std::path::{Path, PathBuf}; +use std::process::exit; +use clap::ArgMatches; +use tokio::sync::{Mutex, RwLock}; +use tokio::task::JoinHandle; +use tokio::time::{sleep, Duration}; +extern crate tokio; +extern crate reqwest; + +const DOWNLOAD_WORKERS: usize = 5; +const NON_IMAGE_WAIT_TIME: Duration = Duration::from_millis(5000); +const NO_ITEM_WAIT_TIME: Duration = Duration::from_millis(1000); +const GET_DATA_FAIL_WAIT_TIME: Duration = Duration::from_millis(30000); + +pub async fn download(arg_m: &ArgMatches<'_>) { + let print_only = arg_m.is_present("print"); + let languages: Vec<_> = arg_m.values_of("language").unwrap_or_default().collect(); + let mut chapter_ids: Vec<_> = arg_m.values_of("chapter_ids").unwrap_or_default().map(|i| i.parse::().unwrap()).collect(); + chapter_ids.sort(); + chapter_ids.dedup(); + let mut manga_ids: Vec<_> = arg_m.values_of("manga_ids").unwrap_or_default().map(|i| i.parse::().unwrap()).collect(); + manga_ids.sort(); + manga_ids.dedup(); + let client = reqwest::Client::new(); + let mut return_fail = false; + let mutex = Arc::new(Mutex::new(DownloadData { data: VecDeque::new(), is_done: false })); + let handles = summon_handles(client.clone(), Arc::clone(&mutex)).await; + handle_chapters(client.clone(), chapter_ids.clone(), Arc::clone(&mutex), &mut return_fail, print_only).await; + for manga_id in manga_ids { + let cloned_client = client.clone(); + let cloned_mutex = Arc::clone(&mutex); + loop { + let mut manga = match utils::get_manga(cloned_client.clone(), manga_id).await { + Ok(Some(i)) => i, + Ok(None) => { + eprintln!("Manga ID: {}\nError: does not exist", manga_id); + return_fail = true; + break; + }, + Err(err) => { + eprintln!("Manga ID: {}\nError: {}", manga_id, err); + sleep(GET_DATA_FAIL_WAIT_TIME).await; + continue; + } + }; + let mut mchapter_ids = Vec::new(); + manga.data.chapters.reverse(); + for chapter in manga.data.chapters { + if chapter_ids.contains(&chapter.id) || (!languages.is_empty() && !languages.contains(&chapter.language.as_str())) { + continue; + } + mchapter_ids.push(chapter.id); + } + if !mchapter_ids.is_empty() { + handle_chapters(cloned_client.clone(), mchapter_ids, Arc::clone(&cloned_mutex), &mut return_fail, print_only).await; + } + break; + } + } + mutex.lock().await.is_done = true; + for handle in handles { + handle.await.unwrap(); + } + if return_fail { + exit(1); + } +} + +async fn handle_chapters(client: reqwest::Client, chapter_ids: Vec, mutex: Arc>, return_fail: &mut bool, print_only: bool) { + let mut chapter_datas = Vec::new(); + for chapter_id in chapter_ids { + let cloned_client = client.clone(); + loop { + let chapter = match utils::get_chapter(cloned_client.clone(), chapter_id).await { + Ok(Some(i)) => i, + Ok(None) => { + eprintln!("Chapter ID: {}\nError: does not exist", chapter_id); + *return_fail = true; + break; + }, + Err(err) => { + eprintln!("Chapter ID: {}\nError: {}", chapter_id, err); + sleep(GET_DATA_FAIL_WAIT_TIME).await; + continue; + } + }; + if print_only { + println!("{}", chapter.data.pages.iter().map(|i| format!("{}{}/{}", &chapter.data.server, &chapter.data.hash, i)).collect::>().join(";")); + } else { + let manga_slug = utils::generate_slug(&chapter.data.manga_title); + let local_dir: PathBuf = [&manga_slug, &chapter.data.chapter].iter().collect(); + let chapter_pages = chapter.data.pages.clone(); + let chapter_rwlock = Arc::new(RwLock::new((chapter.data, 0))); + for (i, server_file) in chapter_pages.iter().enumerate() { + let mut local_file = local_dir.clone(); + local_file.push((i + 1).to_string()); + if let Some(ext) = Path::new(&server_file).extension() { + local_file.set_extension(ext); + } + if local_file.exists() { + continue; + } + chapter_datas.push((Arc::clone(&chapter_rwlock), i, local_file)); + } + } + break; + } + } + if !chapter_datas.is_empty() { + mutex.lock().await.data.extend(chapter_datas); + } +} + +async fn summon_handles(client: reqwest::Client, mutex: Arc>) -> Vec> { + let mut handles = Vec::with_capacity(DOWNLOAD_WORKERS); + for worker_id in 0..DOWNLOAD_WORKERS { + let tcloned_mutex = Arc::clone(&mutex); + let tcloned_client = client.clone(); + handles.push(tokio::spawn(async move { + eprintln!("[DW{}] Up!", worker_id); + loop { + let cloned_mutex = Arc::clone(&tcloned_mutex); + let cloned_client = tcloned_client.clone(); + let mut download_data = cloned_mutex.lock().await; + let data = download_data.data.pop_front(); + let is_done = download_data.is_done; + drop(download_data); + let (chapter_rwlock, page_index, local_file) = match data { + Some(data) => data, + None => { + if is_done { + break; + } + sleep(NO_ITEM_WAIT_TIME).await; + continue; + } + }; + let read_guard = chapter_rwlock.read().await; + let chapter_id = read_guard.0.id; + let mut server = read_guard.0.server.clone(); + let mut server_fallback = read_guard.0.server_fallback.clone(); + let mut hash = read_guard.0.hash.clone(); + let mut server_file = read_guard.0.pages[page_index].clone(); + let mut local_update_count = read_guard.1; + drop(read_guard); + if let Some(parent) = local_file.parent() { + match create_dir_all(&parent) { + Ok(()) => (), + Err(err) => eprintln!("[DW{}] Error while creating directories {}: {}", worker_id, parent.display(), err) + }; + } + eprintln!("[DW{}] Downloading {}", worker_id, local_file.display()); + loop { + match utils::download_page(cloned_client.clone(), &server, &server_fallback, &hash, &server_file, &local_file).await { + Ok(true) => break, + Ok(false) => eprintln!("[DW{}] Error while downloading {}: does not exist", worker_id, local_file.display()), + Err(err) => eprintln!("[DW{}] Error while downloading {}: {}", worker_id, local_file.display(), err) + }; + sleep(NON_IMAGE_WAIT_TIME).await; + let mut write_guard = chapter_rwlock.write().await; + if local_update_count >= write_guard.1 { + match utils::get_chapter(cloned_client.clone(), chapter_id).await { + Ok(Some(chapter_data)) => { + write_guard.0 = chapter_data.data; + write_guard.1 = local_update_count + 1; + }, + Ok(None) => eprintln!("[DW{}] Error while fetching chapter data {}: does not exist", worker_id, chapter_id), + Err(err) => eprintln!("[DW{}] Error while fetching chapter data {}: {}", worker_id, chapter_id, err) + }; + } + server = write_guard.0.server.clone(); + server_fallback = write_guard.0.server_fallback.clone(); + hash = write_guard.0.hash.clone(); + server_file = write_guard.0.pages[page_index].clone(); + local_update_count = write_guard.1; + } + eprintln!("[DW{}] Downloaded {}", worker_id, local_file.display()); + } + eprintln!("[DW{}] Down!", worker_id); + })); + } + handles +} + +struct DownloadData { + pub data: VecDeque<(Arc>, usize, PathBuf)>, + pub is_done: bool +} diff --git a/src/commands/feed.rs b/src/commands/feed.rs new file mode 100644 index 0000000..221493f --- /dev/null +++ b/src/commands/feed.rs @@ -0,0 +1,104 @@ +use crate::utils; + +use std::io::Cursor; +use std::process::exit; +use clap::ArgMatches; +use quick_xml::Writer; +use quick_xml::events::{Event, BytesStart, BytesText, BytesEnd}; +extern crate reqwest; + +pub async fn feed(arg_m: &ArgMatches<'_>) { + let client = reqwest::Client::new(); + let manga_id = arg_m.value_of("id").unwrap(); + let languages: Vec<_> = arg_m.values_of("language").unwrap_or_default().collect(); + let manga_info = match utils::get_manga(client, manga_id.parse().unwrap()).await { + Ok(Some(manga_info)) => manga_info, + Ok(None) => { + eprintln!("ID: {}\nError: does not exist", &manga_id); + exit(1); + }, + Err(err) => { + eprintln!("ID: {}\nError: {}", &manga_id, err); + exit(1); + } + }; + let mut writer = Writer::new(Cursor::new(Vec::new())); + { + let mut elem = BytesStart::owned(b"rss".to_vec(), 3); + elem.push_attribute(("version", "2.0")); + writer.write_event(Event::Start(elem)).unwrap(); + + let elem = BytesStart::owned(b"channel".to_vec(), 7); + writer.write_event(Event::Start(elem)).unwrap(); + + let elem = BytesStart::owned(b"title".to_vec(), 5); + writer.write_event(Event::Start(elem)).unwrap(); + + let elem = BytesText::from_plain_str(&manga_info.data.manga.title).into_owned(); + writer.write_event(Event::Text(elem)).unwrap(); + + let elem = BytesEnd::owned(b"title".to_vec()); + writer.write_event(Event::End(elem)).unwrap(); + + let elem = BytesStart::owned(b"link".to_vec(), 4); + writer.write_event(Event::Start(elem)).unwrap(); + + let elem = BytesText::from_plain_str(&format!("https://mangadex.org/title/{}", &manga_id)).into_owned(); + writer.write_event(Event::Text(elem)).unwrap(); + + let elem = BytesEnd::owned(b"link".to_vec()); + writer.write_event(Event::End(elem)).unwrap(); + + let elem = BytesStart::owned(b"description".to_vec(), 11); + writer.write_event(Event::Start(elem)).unwrap(); + + let elem = BytesText::from_plain_str(&manga_info.data.manga.description).into_owned(); + writer.write_event(Event::Text(elem)).unwrap(); + + let elem = BytesEnd::owned(b"description".to_vec()); + writer.write_event(Event::End(elem)).unwrap(); + } + + for chapter in manga_info.data.chapters { + if !languages.is_empty() && !languages.contains(&chapter.language.as_str()) { + continue; + } + let link = format!("https://mangadex.org/chapter/{}", &chapter.id); + let elem = BytesStart::owned(b"item".to_vec(), 4); + writer.write_event(Event::Start(elem)).unwrap(); + + let elem = BytesStart::owned(b"title".to_vec(), 5); + writer.write_event(Event::Start(elem)).unwrap(); + + let mut title = chapter.to_string(); + if Some(chapter.chapter.clone()) == manga_info.data.manga.last_chapter { + title.push_str(" [END]"); + } + let elem = BytesText::from_plain_str(&title).into_owned(); + writer.write_event(Event::Text(elem)).unwrap(); + + let elem = BytesEnd::owned(b"title".to_vec()); + writer.write_event(Event::End(elem)).unwrap(); + + let mut elem = BytesStart::owned(b"guid".to_vec(), 4); + elem.push_attribute(("isPermaLink", "true")); + writer.write_event(Event::Start(elem)).unwrap(); + + let elem = BytesText::from_plain_str(&link).into_owned(); + writer.write_event(Event::Text(elem)).unwrap(); + + let elem = BytesEnd::owned(b"guid".to_vec()); + writer.write_event(Event::End(elem)).unwrap(); + + let elem = BytesEnd::owned(b"item".to_vec()); + writer.write_event(Event::End(elem)).unwrap(); + } + + let elem = BytesEnd::owned(b"channel".to_vec()); + writer.write_event(Event::End(elem)).unwrap(); + + let elem = BytesEnd::owned(b"rss".to_vec()); + writer.write_event(Event::End(elem)).unwrap(); + + println!("{}", String::from_utf8(writer.into_inner().into_inner()).unwrap()); +} diff --git a/src/commands/mod.rs b/src/commands/mod.rs new file mode 100644 index 0000000..feb2636 --- /dev/null +++ b/src/commands/mod.rs @@ -0,0 +1,6 @@ +mod view; +mod feed; +mod download; +pub use view::view; +pub use feed::feed; +pub use download::download; diff --git a/src/commands/view.rs b/src/commands/view.rs new file mode 100644 index 0000000..d5e4f49 --- /dev/null +++ b/src/commands/view.rs @@ -0,0 +1,78 @@ +use crate::utils; + +use std::process::exit; +use clap::ArgMatches; +extern crate tokio; +extern crate reqwest; + +pub async fn view(arg_m: &ArgMatches<'_>) { + let client = reqwest::Client::new(); + let languages: Vec<_> = arg_m.values_of("language").unwrap_or_default().collect(); + let handles = arg_m.values_of("id").unwrap().map(|id| { + let cloned_client = client.clone(); + let id = id.to_string(); + let cid = id.parse().unwrap(); + (tokio::spawn(async move { + utils::get_manga(cloned_client, cid).await + }), id) + }).collect::>(); + let mut fail = false; + let mut one_done = false; + for handle in handles { + let (handle, id) = handle; + let manga = match handle.await { + Ok(manga) => manga, + Err(err) => { + if one_done { + eprintln!(""); + } + eprintln!("ID: {}\nError: {}", id, err); + fail = true; + one_done = true; + continue; + } + }; + match manga { + Ok(manga) => { + match manga { + Some(mut manga) => { + let mut text = manga.data.manga.to_string(); + if one_done { + text = format!("\n{}", text); + } + text.push_str("\nChapters:"); + manga.data.chapters.reverse(); + for chapter in &manga.data.chapters { + if !languages.is_empty() && !languages.contains(&chapter.language.as_str()) { + continue; + } + text.push_str(&format!("\n- {}", chapter)); + if Some(chapter.chapter.clone()) == manga.data.manga.last_chapter { + text.push_str(" [END]"); + } + } + println!("{}", text); + }, + None => { + if one_done { + eprintln!(""); + } + eprintln!("ID: {}\nError: does not exist", id); + fail = true; + } + }; + }, + Err(err) => { + if one_done { + eprintln!(""); + } + eprintln!("ID: {}\nError: {}", id, err); + fail = true; + } + }; + one_done = true; + } + if fail { + exit(1); + } +} diff --git a/src/main.rs b/src/main.rs new file mode 100644 index 0000000..34defbb --- /dev/null +++ b/src/main.rs @@ -0,0 +1,102 @@ +mod commands; +mod structs; +mod utils; +use clap::{App, AppSettings, Arg, SubCommand}; +extern crate tokio; + +const LANGUAGES: &[&str] = &["sa", "bd", "bg", "mm", "ct", "cn", "hk", "cz", "dk", "nl", "gb", "ph", "fi", "fr", "de", "gr", "il", "in", "hu", "id", "it", "jp", "kr", "lt", "my", "mn", "no", "other", "ir", "pl", "br", "pt", "ro", "ru", "rs", "es", "mx", "se", "th", "tr", "ua", "vn"]; + +fn validate_int(i: String) -> Result<(), String> { + match i.parse::() { + Ok(_) => Ok(()), + Err(err) => Err(err.to_string()) + } +} + +fn main() { + let matches = App::new("mangadexrs") + .about("mangadex.org downloader in rust") + .version(env!("CARGO_PKG_VERSION")) + .setting(AppSettings::SubcommandRequiredElseHelp) + .subcommand( + SubCommand::with_name("view") + .aliases(&["info", "show"]) + .arg( + Arg::with_name("language") + .long("language") + .short("l") + .takes_value(true) + .multiple(true) + .possible_values(LANGUAGES) + .help("Only show chapters of the specified languages") + ).arg( + Arg::with_name("id") + .takes_value(true) + .multiple(true) + .required(true) + .validator(validate_int) + ) + ) + .subcommand( + SubCommand::with_name("feed") + .alias("rss") + .arg( + Arg::with_name("language") + .long("language") + .short("l") + .takes_value(true) + .multiple(true) + .possible_values(LANGUAGES) + .help("Only show chapters of the specified languages") + ).arg( + Arg::with_name("id") + .takes_value(true) + .required(true) + .validator(validate_int) + ) + ) + .subcommand( + SubCommand::with_name("download") + .alias("dl") + .arg( + Arg::with_name("print") + .long("print") + .short("p") + .help("Print the URL to download only") + ).arg( + Arg::with_name("manga_ids") + .long("manga-ids") + .short("m") + .takes_value(true) + .multiple(true) + .required_unless("chapter_ids") + .help("Add chapters from the manga specified, limit languages with --language") + ).arg( + Arg::with_name("language") + .long("language") + .short("l") + .takes_value(true) + .multiple(true) + .possible_values(LANGUAGES) + .help("Filter languages for manga_ids, has no effect on chapter_ids") + ).arg( + Arg::with_name("chapter_ids") + .takes_value(true) + .required_unless("manga_ids") + .multiple(true) + .validator(validate_int) + ) + ) + .get_matches(); + + let runtime = tokio::runtime::Builder::new_multi_thread() + .enable_all() + .build() + .unwrap(); + match matches.subcommand() { + ("view", Some(sub_m)) => runtime.block_on(commands::view(sub_m)), + ("feed", Some(sub_m)) => runtime.block_on(commands::feed(sub_m)), + ("download", Some(sub_m)) => runtime.block_on(commands::download(sub_m)), + _ => panic!("AppSettings::SubcommandRequiredElseHelp do your job please") + }; +} diff --git a/src/structs.rs b/src/structs.rs new file mode 100644 index 0000000..2547cef --- /dev/null +++ b/src/structs.rs @@ -0,0 +1,173 @@ +use std::io; +use std::fmt; +use std::marker::PhantomData; +use std::collections::BTreeMap; +use regex::Regex; +use serde::de::{self, Visitor}; +use serde::{Deserialize, Deserializer}; +extern crate reqwest; +extern crate serde_json; + +const TAGS_AND_TYPE: &[(&str, &str)] = &[("4-Koma", "Format"), ("Action", "Genre"), ("Adventure", "Genre"), ("Award Winning", "Format"), ("Comedy", "Genre"), ("Cooking", "Theme"), ("Doujinshi", "Format"), ("Drama", "Genre"), ("Ecchi", "Content"), ("Fantasy", "Genre"), ("Gyaru", "Theme"), ("Harem", "Theme"), ("Historical", "Genre"), ("Horror", "Genre"), ("Martial Arts", "Theme"), ("Mecha", "Genre"), ("Medical", "Genre"), ("Music", "Theme"), ("Mystery", "Genre"), ("Oneshot", "Format"), ("Psychological", "Genre"), ("Romance", "Genre"), ("School Life", "Theme"), ("Sci-Fi", "Genre"), ("Shoujo Ai", "Genre"), ("Shounen Ai", "Genre"), ("Slice of Life", "Genre"), ("Smut", "Content"), ("Sports", "Genre"), ("Supernatural", "Theme"), ("Tragedy", "Genre"), ("Long Strip", "Format"), ("Yaoi", "Genre"), ("Yuri", "Genre"), ("Video Games", "Theme"), ("Isekai", "Genre"), ("Adaptation", "Format"), ("Anthology", "Format"), ("Web Comic", "Format"), ("Full Color", "Format"), ("User Created", "Format"), ("Official Colored", "Format"), ("Fan Colored", "Format"), ("Gore", "Content"), ("Sexual Violence", "Content"), ("Crime", "Genre"), ("Magical Girls", "Genre"), ("Philosophical", "Genre"), ("Superhero", "Genre"), ("Thriller", "Genre"), ("Wuxia", "Genre"), ("Aliens", "Theme"), ("Animals", "Theme"), ("Crossdressing", "Theme"), ("Demons", "Theme"), ("Delinquents", "Theme"), ("Genderswap", "Theme"), ("Ghosts", "Theme"), ("Monster Girls", "Theme"), ("Loli", "Theme"), ("Magic", "Theme"), ("Military", "Theme"), ("Monsters", "Theme"), ("Ninja", "Theme"), ("Office Workers", "Theme"), ("Police", "Theme"), ("Post-Apocalyptic", "Theme"), ("Reincarnation", "Theme"), ("Reverse Harem", "Theme"), ("Samurai", "Theme"), ("Shota", "Theme"), ("Survival", "Theme"), ("Time Travel", "Theme"), ("Vampires", "Theme"), ("Traditional Games", "Theme"), ("Virtual Reality", "Theme"), ("Zombies", "Theme"), ("Incest", "Theme"), ("Mafia", "Theme"), ("Villainess", "Theme")]; + +#[derive(Deserialize, Debug)] +#[serde(rename_all = "camelCase")] +pub struct MangaInfo { + pub id: i32, + pub title: String, + #[serde(deserialize_with = "sanitize_description")] + pub description: String, + pub artist: Vec, + pub author: Vec, + pub tags: Vec, + pub views: usize, + pub last_chapter: Option +} + +#[derive(Deserialize, Debug)] +pub struct MangaData { + pub manga: MangaInfo, + pub chapters: Vec +} + +#[derive(Deserialize, Debug)] +pub struct Manga { + pub data: MangaData +} + +#[derive(Deserialize, Debug)] +pub struct MiniChapterData { + pub id: i32, + pub chapter: String, + #[serde(default)] + pub volume: String, + pub title: String, + pub language: String +} + +#[derive(Deserialize, Debug)] +#[serde(rename_all = "camelCase")] +pub struct ChapterData { + pub id: i32, + pub manga_title: String, + pub chapter: String, + pub hash: String, + pub pages: Vec, + pub server: String, + #[serde(default)] + pub server_fallback: String +} + +#[derive(Deserialize, Debug)] +pub struct Chapter { + pub data: ChapterData +} + +impl fmt::Display for MangaInfo { + fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result { + let mut text = format!("ID: {}\nTitle: {}\nViews: {}\n", self.id, &self.title, self.views); + if !self.artist.is_empty() { + text.push_str(&format!("Artists: {}\n", &self.artist.join(", "))); + } + if !self.author.is_empty() { + text.push_str(&format!("Authors: {}\n", &self.author.join(", "))); + } + if !self.tags.is_empty() { + let mut treemap = BTreeMap::new(); + for tag_id in &self.tags { + let (tag_name, tag_group) = match TAGS_AND_TYPE.get(tag_id - 1) { + Some(&(i, j)) => (i.to_string(), j), + None => (tag_id.to_string(), "Unknown") + }; + treemap.entry(tag_group.clone()).or_insert_with(|| Vec::new()).push(tag_name); + } + for (tag_key, tag_value) in treemap { + text.push_str(&format!("{}: {}\n", tag_key, tag_value.join(", "))); + } + } + write!(formatter, "{}Description:\n{}", &text, &self.description) + } +} + +impl fmt::Display for MiniChapterData { + fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result { + let mut text = format!("{}: ", self.id); + if !self.volume.is_empty() { + text.push_str(&format!("Volume {}: ", &self.volume)); + } + text.push_str(&format!("Chapter {}", &self.chapter)); + if !self.title.is_empty() { + text.push_str(&format!(": {}", &self.title)); + } + write!(formatter, "{} [{}]", &text, &self.language) + } +} + +fn sanitize_description<'de, D>(deserializer: D) -> Result +where + D: Deserializer<'de> +{ + struct SanitizeDescription(PhantomData T>); + + impl<'de> Visitor<'de> for SanitizeDescription + { + type Value = String; + + fn expecting(&self, formatter: &mut fmt::Formatter) -> fmt::Result { + formatter.write_str("unknown error with sanitize_description idfk lmao") + } + + fn visit_str(self, value: &str) -> Result + where + E: de::Error + { + Ok(match Regex::new(r"\[\w+\].+\[/\w+\]") { + Ok(regex) => regex.replace_all(&value, "").trim().to_string(), + Err(_) => value.to_string() + }) + } + } + + deserializer.deserialize_any(SanitizeDescription(PhantomData)) +} + + +#[derive(Debug)] +pub enum Error { + IO(io::Error), + Reqwest(reqwest::Error), + SerdeJSON(serde_json::Error), +} + +impl From for Error { + #[inline] + fn from(error: io::Error) -> Error { + Error::IO(error) + } +} + +impl From for Error { + #[inline] + fn from(error: reqwest::Error) -> Error { + Error::Reqwest(error) + } +} + +impl From for Error { + #[inline] + fn from(error: serde_json::Error) -> Error { + Error::SerdeJSON(error) + } +} + +impl fmt::Display for Error { + fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result { + formatter.write_str( + &match self { + Error::IO(err) => format!("io error: {}", err), + Error::Reqwest(err) => format!("reqwest error: {}", err), + Error::SerdeJSON(err) => format!("serde_json error: {}", err), + } + ) + } +} diff --git a/src/utils.rs b/src/utils.rs new file mode 100644 index 0000000..c6a3081 --- /dev/null +++ b/src/utils.rs @@ -0,0 +1,56 @@ +use crate::structs; + +use std::fs::write; +use std::path::Path; +extern crate reqwest; +extern crate serde_json; + +pub fn generate_slug(text: &str) -> String { + text.to_lowercase().trim().replace(|c: char| !c.is_alphanumeric(), "-") +} + +pub async fn get_manga(client: reqwest::Client, id: i32) -> Result, structs::Error> { + let resp = client.get(&format!("https://mangadex.org/api/v2/manga/{}?include=chapters", &id)) + .send() + .await?; + if resp.status() == 404 { + return Ok(None); + } + resp.error_for_status_ref()?; + Ok(Some(serde_json::from_str(&resp.text().await?)?)) +} + +pub async fn get_chapter(client: reqwest::Client, chapter: i32) -> Result, structs::Error> { + let resp = client.get(&format!("https://mangadex.org/api/v2/chapter/{}", &chapter)) + .send() + .await?; + if resp.status() == 404 { + return Ok(None); + } + resp.error_for_status_ref()?; + Ok(Some(serde_json::from_str(&resp.text().await?)?)) +} + +pub async fn download_page(client: reqwest::Client, server: &str, server_fallback: &str, hash: &str, server_file: &str, local_file: &Path) -> Result { + let res = download(client.clone(), &format!("{}{}/{}", &server, &hash, &server_file), &local_file).await; + if server_fallback.is_empty() { + return res; + } + match res { + Ok(true) => Ok(true), + _ => download(client.clone(), &format!("{}{}/{}", &server_fallback, &hash, &server_file), &local_file).await + } +} + +async fn download(client: reqwest::Client, url: &str, file: &Path) -> Result { + let resp = client.get(url) + .send() + .await?; + if resp.status() == 404 { + return Ok(false); + } + resp.error_for_status_ref()?; + let bytes = resp.bytes().await?; + write(&file, bytes)?; + Ok(true) +}