commit f6ec76baf2cacd60b5d09332311fe9cec7314687 Author: blank X Date: Sat Jan 9 00:29:45 2021 +0700 Initial release 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..c61512a --- /dev/null +++ b/Cargo.lock @@ -0,0 +1,1037 @@ +# This file is automatically @generated by Cargo. +# It is not intended for manual editing. +[[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.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2e8c087f005730276d1096a652e92a8bacee2e2472bcc9715a74d2bec38b5820" + +[[package]] +name = "bytes" +version = "0.5.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0e4cec68f03f32e44924783795810fa50a7035d8c8ebe78580ad7e6c703fba38" + +[[package]] +name = "bytes" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ad1f8e949d755f9d79112b5bb46938e0ef9d3804a0b16dfab13aafcaa5f0fa72" + +[[package]] +name = "cc" +version = "1.0.66" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4c0496836a84f8d0495758516b8621a622beb77c0fed418570e50764093ced48" + +[[package]] +name = "cfg-if" +version = "0.1.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4785bdd1c96b2a846b2bd7cc02e86b6b3dbf14e7e53446c4f54c92a361040822" + +[[package]] +name = "cfg-if" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd" + +[[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.26" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "801bbab217d7f79c0062f4f7205b5d4427c6d1a7bd7aafdd1475f7c59d62b283" +dependencies = [ + "cfg-if 1.0.0", +] + +[[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.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4b7109687aa4e177ef6fe84553af6280ef2778bdb7783ba44c9dc3399110fe64" +dependencies = [ + "futures-core", +] + +[[package]] +name = "futures-core" +version = "0.3.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "847ce131b72ffb13b6109a221da9ad97a64cbe48feb1028356b836b47b8f1748" + +[[package]] +name = "futures-sink" +version = "0.3.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f878195a49cee50e006b02b93cf7e0a95a38ac7b776b4c4d9cc1207cd20fcb3d" + +[[package]] +name = "futures-task" +version = "0.3.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7c554eb5bf48b2426c4771ab68c6b14468b6e76cc90996f528c3338d761a4d0d" + +[[package]] +name = "futures-util" +version = "0.3.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d304cff4a7b99cfb7986f7d43fbe93d175e72e704a8860787cc95e9ffd85cbd2" +dependencies = [ + "futures-core", + "futures-task", + "pin-project 1.0.3", + "pin-utils", +] + +[[package]] +name = "getrandom" +version = "0.1.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fc587bc0ec293155d5bfa6b9891ec18a1e330c234f896ea47fbada4cadbe47e6" +dependencies = [ + "cfg-if 0.1.10", + "libc", + "wasi", +] + +[[package]] +name = "h2" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6b67e66362108efccd8ac053abafc8b7a8d86a37e6e48fc4f6f7485eb5e9e6a5" +dependencies = [ + "bytes 1.0.0", + "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.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5aca5565f760fb5b220e499d72710ed156fdb74e631659e99377d9ebfbd13ae8" +dependencies = [ + "libc", +] + +[[package]] +name = "http" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "84129d298a6d57d246960ff8eb831ca4af3f96d29e2e28848dae275408658e26" +dependencies = [ + "bytes 0.5.6", + "fnv", + "itoa", +] + +[[package]] +name = "http-body" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2861bd27ee074e5ee891e8b539837a9430012e249d7f0ca2d795650f579c1994" +dependencies = [ + "bytes 1.0.0", + "http", +] + +[[package]] +name = "httparse" +version = "1.3.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cd179ae861f0c2e53da70d892f5f3029f9594be0c41dc5269cd371691b1dc2f9" + +[[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.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "12219dc884514cb4a6a03737f4413c0e01c23a1b059b0156004b23f1e19dccbe" +dependencies = [ + "bytes 1.0.0", + "futures-channel", + "futures-core", + "futures-util", + "h2", + "http", + "http-body", + "httparse", + "httpdate", + "itoa", + "pin-project 1.0.3", + "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 1.0.0", + "hyper", + "native-tls", + "tokio", + "tokio-native-tls", +] + +[[package]] +name = "idna" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "02e2673c30ee86b5b96a9cb52ad15718aa1f966f5ab9ad54a8b95d5ca33120a9" +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.46" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cf3d7383929f7c9c7c2d0fa596f325832df98c3704f2c60553080f7127a58175" +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.81" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1482821306169ec4d07f6aca392a4681f66c75c9918aa49641a2595db64053cb" + +[[package]] +name = "log" +version = "0.4.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4fabed175da42fed1fa0746b0ea71f412aa9d35e76e95e59b192c64b9dc2bf8b" +dependencies = [ + "cfg-if 0.1.10", +] + +[[package]] +name = "mangafetchi" +version = "0.1.0" +dependencies = [ + "quick-xml", + "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 = "openssl" +version = "0.10.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "038d43985d1ddca7a9900630d8cd031b56e4794eecc2e9ea39dd17aa04399a70" +dependencies = [ + "bitflags", + "cfg-if 1.0.0", + "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.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5a83804639aad6ba65345661744708855f9fbcb71176ea8d28d05aeb11d975e7" +dependencies = [ + "pin-project-internal 1.0.3", +] + +[[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.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b7bcc46b8f73443d15bc1c5fecbb315718491fa9187fa483f0e359323cde8b3a" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "pin-project-lite" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e36743d754ccdf9954c2e352ce2d4b106e024c814f6499c2dadff80da9a442d8" + +[[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.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c36fa947111f5c62a733b652544dd0016a43ce89619538a8ef92724a6f501a20" + +[[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.7.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6a6b1679d49b24bbfe0c803429aa1874472f50d9b363131f0e89fc356b544d03" +dependencies = [ + "getrandom", + "libc", + "rand_chacha", + "rand_core", + "rand_hc", +] + +[[package]] +name = "rand_chacha" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f4c8ed856279c9737206bf725bf36935d8666ead7aa69b52be55af369d193402" +dependencies = [ + "ppv-lite86", + "rand_core", +] + +[[package]] +name = "rand_core" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "90bde5296fc891b0cef12a6d03ddccc162ce7b2aff54160af9338f8d40df6d19" +dependencies = [ + "getrandom", +] + +[[package]] +name = "rand_hc" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ca3129af7b92a17112d59ad498c6f81eaf463253766b90396d39ea7a39d6613c" +dependencies = [ + "rand_core", +] + +[[package]] +name = "redox_syscall" +version = "0.1.57" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "41cc0f7e4d5d4544e8861606a285bb08d3e70712ccc7d2b84d7c0ccfaf4b05ce" + +[[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 1.0.0", + "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.118" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "06c64263859d87aa2eb554587e2d23183398d617427327cf2b3d0ed8c69e4800" +dependencies = [ + "serde_derive", +] + +[[package]] +name = "serde_derive" +version = "1.0.118" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c84d3526699cd55261af4b941e4e725444df67aa4f9e6a3564f18030d12672df" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "serde_json" +version = "1.0.61" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4fceb2595057b6891a4ee808f70054bd2d12f0e97f1cbb78689b59f676df325a" +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 1.0.0", + "libc", + "winapi", +] + +[[package]] +name = "syn" +version = "1.0.58" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cc60a3d73ea6594cd712d830cc1f0390fd71542d8c8cd24e70cc54cdfd5e05d5" +dependencies = [ + "proc-macro2", + "quote", + "unicode-xid", +] + +[[package]] +name = "tempfile" +version = "3.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7a6e24d9338a0a5be79593e2fa15a648add6138caa803e2d5bc782c371732ca9" +dependencies = [ + "cfg-if 0.1.10", + "libc", + "rand", + "redox_syscall", + "remove_dir_all", + "winapi", +] + +[[package]] +name = "tinyvec" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ccf8dbc19eb42fba10e8feaaec282fb50e2c14b2726d6301dbfeed0f73306a6f" +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.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d258221f566b6c803c7b4714abadc080172b272090cdc5e244a6d4dd13c3a6bd" +dependencies = [ + "autocfg", + "bytes 1.0.0", + "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-stream" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e4cdeb73537e63f98adcd73138af75e3f368ccaecffaa29d7eb61b9f5a440457" +dependencies = [ + "futures-core", + "pin-project-lite", + "tokio", +] + +[[package]] +name = "tokio-util" +version = "0.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "36135b7e7da911f5f8b9331209f7fab4cc13498f3fff52f72a710c78187e3148" +dependencies = [ + "bytes 1.0.0", + "futures-core", + "futures-sink", + "log", + "pin-project-lite", + "tokio", + "tokio-stream", +] + +[[package]] +name = "tower-service" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e987b6bf443f4b5b3b6f38704195592cca41c5bb7aedd3c3693c7081f8289860" + +[[package]] +name = "tracing" +version = "0.1.22" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9f47026cdc4080c07e49b37087de021820269d996f581aac150ef9e5583eefe3" +dependencies = [ + "cfg-if 1.0.0", + "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.16" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a13e63ab62dbe32aeee58d1c5408d35c36c392bba5d9d3142287219721afe606" +dependencies = [ + "tinyvec", +] + +[[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.9.0+wasi-snapshot-preview1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cccddf32554fecc6acb585f82a32a72e28b48f8c4c1883ddfeeeaa96f7d8e519" + +[[package]] +name = "wasm-bindgen" +version = "0.2.69" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3cd364751395ca0f68cafb17666eee36b63077fb5ecd972bbcd74c90c4bf736e" +dependencies = [ + "cfg-if 1.0.0", + "serde", + "serde_json", + "wasm-bindgen-macro", +] + +[[package]] +name = "wasm-bindgen-backend" +version = "0.2.69" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1114f89ab1f4106e5b55e688b828c0ab0ea593a1ea7c094b141b14cbaaec2d62" +dependencies = [ + "bumpalo", + "lazy_static", + "log", + "proc-macro2", + "quote", + "syn", + "wasm-bindgen-shared", +] + +[[package]] +name = "wasm-bindgen-futures" +version = "0.4.19" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1fe9756085a84584ee9457a002b7cdfe0bfff169f45d2591d8be1345a6780e35" +dependencies = [ + "cfg-if 1.0.0", + "js-sys", + "wasm-bindgen", + "web-sys", +] + +[[package]] +name = "wasm-bindgen-macro" +version = "0.2.69" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7a6ac8995ead1f084a8dea1e65f194d0973800c7f571f6edd70adf06ecf77084" +dependencies = [ + "quote", + "wasm-bindgen-macro-support", +] + +[[package]] +name = "wasm-bindgen-macro-support" +version = "0.2.69" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b5a48c72f299d80557c7c62e37e7225369ecc0c963964059509fbafe917c7549" +dependencies = [ + "proc-macro2", + "quote", + "syn", + "wasm-bindgen-backend", + "wasm-bindgen-shared", +] + +[[package]] +name = "wasm-bindgen-shared" +version = "0.2.69" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7e7811dd7f9398f14cc76efd356f98f03aa30419dea46aa810d71e819fc97158" + +[[package]] +name = "web-sys" +version = "0.3.46" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "222b1ef9334f92a21d3fb53dc3fd80f30836959a90f9274a626d7e06315ba3c3" +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..a3b55e1 --- /dev/null +++ b/Cargo.toml @@ -0,0 +1,17 @@ +[package] +name = "mangafetchi" +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] +serde = { version = "1.0", features = ["derive"] } +serde_json = "1.0" +reqwest = "0.11" +quick-xml = "0.20" +tokio = { version = "1.0", features = ["rt-multi-thread", "sync"] } 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.rs b/src/commands.rs new file mode 100644 index 0000000..1706380 --- /dev/null +++ b/src/commands.rs @@ -0,0 +1,35 @@ +mod view; +mod feed; +mod search; +mod download; + +use std::env; +use std::path::Path; +use std::process::exit; + +pub async fn run() { + let mut args = env::args(); + let path = args.next().expect("Cannot get binary path"); + let path = Path::new(&path).file_stem().unwrap().to_str().unwrap(); + let operation = match args.next() { + Some(operation) => operation, + None => { + eprintln!("Missing operation, run `{} help`", path); + exit(1); + } + }; + match operation.as_str() { + "search" => search::run(args).await, + "info" | "view" | "show" => view::run(args).await, + "feed" | "rss" => feed::run(args).await, + "download" | "dl" => download::run(args).await, + "help" => println!(r#"Usage: {} search + or {} info/view/show [manga id]... + or {} feed/rss + or {} download/dl [chapters]..."#, path, path, path, path), + _ => { + eprintln!("Unknown operation, run `{} help`", path); + exit(1) + } + }; +} diff --git a/src/commands/download.rs b/src/commands/download.rs new file mode 100644 index 0000000..b6cf9ac --- /dev/null +++ b/src/commands/download.rs @@ -0,0 +1,100 @@ +use crate::utils; +use crate::structs; + +use std::env; +use std::sync::Arc; +use std::process::exit; +use std::path::{Path, PathBuf}; +use tokio::sync::Mutex; +use tokio::task::JoinHandle; +extern crate tokio; +extern crate reqwest; + +const DOWNLOAD_WORKERS: usize = 5; + +pub async fn run(mut args: env::Args) { + let manga_id = match args.next() { + Some(manga_id) => manga_id, + None => { + eprintln!("Missing manga id"); + exit(1); + } + }; + let mut chapter_numbers: Vec<_> = args.collect(); + chapter_numbers.sort(); + chapter_numbers.dedup(); + let mut chapters: Vec = Vec::new(); + let client = reqwest::Client::new(); + match utils::get_manga(client.clone(), &manga_id).await.unwrap() { + structs::MangaOption::Manga(mut manga_info) => { + if chapter_numbers.is_empty() { + chapters = manga_info.chapters; + } else { + for chapter_number in chapter_numbers { + let tmp = manga_info.chapters.iter().enumerate().find(|(_, chapter)| chapter_number.trim() == chapter.chapter_number.as_str()); + if tmp.is_some() { + let (i, _) = tmp.unwrap(); + chapters.push(manga_info.chapters.remove(i)); + } else { + eprintln!("Chapter {} does not exist", &chapter_number); + exit(1); + } + } + } + }, + structs::MangaOption::Redirect(_) => panic!("Nested redirect"), + structs::MangaOption::None => { + eprintln!("ID: {}\nError: does not exist", &manga_id); + exit(1); + } + }; + let mutex: Arc>> = Arc::new(Mutex::new(Vec::new())); + let mut handles: Option>> = None; + for chapter in chapters { + let cloned_mutex = Arc::clone(&mutex); + let chapter_pages = utils::get_pages(client.clone(), &chapter, &manga_id).await.unwrap(); + let mut to_extend: Vec<(String, PathBuf, String)> = Vec::new(); + for url in chapter_pages { + let mut file_name = PathBuf::from(&chapter.chapter_number); + file_name.push(Path::new(reqwest::Url::parse(&url).unwrap().path()).file_name().unwrap()); + if !file_name.exists() { + to_extend.push((url, file_name, chapter.domain.clone())); + } + } + if !to_extend.is_empty() { + cloned_mutex.lock().await.extend(to_extend); + } + if handles.is_none() { + handles = Some(summon_handles(client.clone(), cloned_mutex).await); + } + } + for handle in handles.unwrap() { + handle.await.unwrap(); + } +} + +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 vec = cloned_mutex.lock().await; + if vec.is_empty() { + break; + } + let (url, file_name, referer) = vec.remove(0); + drop(vec); + eprintln!("[DW{}] Downloading {} to {}", worker_id, &url, file_name.display()); + utils::download_file(cloned_client, &url, &file_name, &referer).await.unwrap(); + eprintln!("[DW{}] Downloaded {} to {}", worker_id, &url, file_name.display()); + } + eprintln!("[DW{}] Down!", worker_id); + })); + } + handles +} diff --git a/src/commands/feed.rs b/src/commands/feed.rs new file mode 100644 index 0000000..c544b82 --- /dev/null +++ b/src/commands/feed.rs @@ -0,0 +1,113 @@ +use crate::utils; +use crate::structs; + +use std::env; +use std::io::Cursor; +use std::process::exit; +use quick_xml::Writer; +use quick_xml::events::{Event, BytesStart, BytesText, BytesEnd}; +extern crate reqwest; + +pub async fn run(mut args: env::Args) { + let manga_id = match args.next() { + Some(manga_id) => manga_id, + None => { + eprintln!("Missing manga id"); + exit(1); + } + }; + if args.next().is_some() { + eprintln!("Specify only one manga id"); + exit(1); + } + let mut manga_info = match utils::get_manga(reqwest::Client::new(), &manga_id).await.unwrap() { + structs::MangaOption::Manga(manga_info) => manga_info, + structs::MangaOption::Redirect(_) => panic!("Nested redirect"), + structs::MangaOption::None => { + eprintln!("ID: {}\nError: does not exist", &manga_id); + exit(1); + } + }; + manga_info.chapters.reverse(); + 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.name).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://mangakakalot.com/manga/{}", &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.summary).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.chapters { + let link = format!("https://mangakakalot.com/chapter/{}/chapter_{}", &manga_id, chapter.chapter_number); + 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 elem = BytesText::from_plain_str(&format!("{}", &chapter)).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(&link).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 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/search.rs b/src/commands/search.rs new file mode 100644 index 0000000..850c065 --- /dev/null +++ b/src/commands/search.rs @@ -0,0 +1,25 @@ +use crate::utils; + +use std::env; +use std::process::exit; +extern crate reqwest; + +pub async fn run(args: env::Args) { + let mut query = String::new(); + for arg in args { + query.push_str(&format!(" {}", arg)); + } + let query = query.trim(); + if query.is_empty() { + eprintln!("Missing search query"); + exit(1); + } + let results = utils::search(reqwest::Client::new(), &query).await.unwrap(); + if results.is_empty() { + eprintln!("No results found"); + exit(1); + } + for result in results { + println!("{}: {}", result.name_unsigned, result.name); + } +} diff --git a/src/commands/view.rs b/src/commands/view.rs new file mode 100644 index 0000000..1f44898 --- /dev/null +++ b/src/commands/view.rs @@ -0,0 +1,49 @@ +use crate::utils; +use crate::structs; + +use std::env; +use std::process::exit; +use tokio::task::JoinHandle; +extern crate tokio; +extern crate reqwest; + +pub async fn run(args: env::Args) { + let ids: Vec = args.collect(); + if ids.len() < 1 { + eprintln!("Missing manga id(s)"); + exit(1); + } + let client = reqwest::Client::new(); + let mut handles: Vec> = Vec::with_capacity(ids.len()); + for id in ids { + let cloned_client = client.clone(); + handles.push(tokio::spawn(async move { + (utils::get_manga(cloned_client, &id).await.unwrap(), id) + })); + } + let mut fail = false; + let mut one_done = false; + for handle in handles { + let (manga_info, id) = handle.await.unwrap(); + match manga_info { + structs::MangaOption::Manga(manga_info) => { + if one_done { + println!(""); + } + println!("{}", &manga_info); + }, + structs::MangaOption::Redirect(_) => panic!("Nested redirect"), + structs::MangaOption::None => { + if one_done { + eprintln!(""); + } + eprintln!("ID: {}\nError: does not exist", id); + 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..e24bf9a --- /dev/null +++ b/src/main.rs @@ -0,0 +1,13 @@ +mod utils; +mod structs; +mod commands; + +extern crate tokio; + +fn main() { + tokio::runtime::Builder::new_multi_thread() + .enable_all() + .build() + .unwrap() + .block_on(commands::run()); +} diff --git a/src/structs.rs b/src/structs.rs new file mode 100644 index 0000000..0166300 --- /dev/null +++ b/src/structs.rs @@ -0,0 +1,71 @@ +use std::fmt; +use serde::Deserialize; +extern crate serde; + +#[derive(Deserialize, Debug)] +pub struct SearchResult { + pub id: String, + pub name: String, + #[serde(rename(deserialize = "nameunsigned"))] + pub name_unsigned: String, + #[serde(rename(deserialize = "lastchapter"))] + pub last_chapter: String, + pub image: String, + pub author: String, + pub story_link: String +} + +#[derive(Debug)] +pub struct Chapter { + pub chapter_number: String, + pub chapter_name: Option, + pub domain: String +} + +impl fmt::Display for Chapter { + fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result { + let mut text = format!("Chapter {}", self.chapter_number); + if self.chapter_name.is_some() { + text.push_str(&format!(": {}", self.chapter_name.as_ref().unwrap())); + } + formatter.write_str(&text) + } +} + +pub struct Manga { + pub id: String, + pub name: String, + pub authors: Vec, + pub status: String, + pub last_updated: String, + pub genres: Vec, + pub summary: String, + pub chapters: Vec +} + +impl fmt::Display for Manga { + fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result { + let mut text = format!("ID: {}\nName: {}\nStatus: {}\nLast Updated: {}\nGenres: {}\nAuthors: {}\nSummary:\n{}\nChapters:", + self.id, + self.name, + self.status, + self.last_updated, + self.genres.join(", "), + self.authors.join(", "), + self.summary); + for chapter in &self.chapters { + text.push_str(&format!("\n- {}", &chapter)); + } + formatter.write_str(&text) + } +} + +pub struct Redirect { + pub url: String +} + +pub enum MangaOption { + Manga(Manga), + Redirect(Redirect), + None +} diff --git a/src/utils.rs b/src/utils.rs new file mode 100644 index 0000000..ec75dd3 --- /dev/null +++ b/src/utils.rs @@ -0,0 +1,559 @@ +use crate::structs; + +use std::io::Write; +use std::path::PathBuf; +use std::fs::{create_dir, File}; +use quick_xml::Reader; +use quick_xml::events::Event; +extern crate reqwest; +extern crate serde_json; + +fn generate_slug(text: &str) -> String { + let mut text = text.to_string() + .to_lowercase() + .replace(&['à', 'á', 'ạ', 'ả', 'ã', 'â', 'ầ', 'ấ', 'ậ', 'ẩ', 'ẫ', 'ă', 'ằ', 'ắ', 'ặ', 'ẳ', 'ẵ'][..], "a") + .replace(&['è', 'é', 'ẹ', 'ẻ', 'ẽ', 'ê', 'ề', 'ế', 'ệ', 'ể', 'ễ'][..], "e") + .replace(&['ì', 'í', 'ị', 'ỉ', 'ĩ'][..], "i") + .replace(&['ò', 'ó', 'ọ', 'ỏ', 'õ', 'ô', 'ồ', 'ố', 'ộ', 'ổ', 'ỗ', 'ơ', 'ờ', 'ớ', 'ợ', 'ở', 'ỡ'][..], "o") + .replace(&['ù', 'ú', 'ụ', 'ủ', 'ũ', 'ư', 'ừ', 'ứ', 'ự', 'ử', 'ữ'][..], "u") + .replace(&['ỳ', 'ý', 'ỵ', 'ỷ', 'ỹ'][..], "y") + .replace('đ', "d") + .replace(&['!', '@', '%', '^', '*', '(', ')', '+', '=', '<', '>', '?', '/', ',', '.', ':', ';', '\'', ' ', '"', '&', '#', '[', ']', '~', '-'][..], "_"); + while text.find("__").is_some() { + text = text.replace("__", "_"); + } + text.trim_matches('_').to_string() +} + +fn remove_html(text: &str) -> String { + let mut removed = String::new(); + let mut reader = Reader::from_str(&text); + let mut buf = Vec::new(); + loop { + match reader.read_event(&mut buf) { + Ok(Event::Text(e)) => removed.push_str(&e.unescape_and_decode(&reader).unwrap()), + Err(err) => panic!("Error at position {}: {}", reader.buffer_position(), err), + Ok(Event::Eof) => break, + _ => () + }; + buf.clear(); + } + removed +} + +pub async fn search(client: reqwest::Client, query: &str) -> Result, reqwest::Error> { + let text = client.post("https://mangakakalot.com/home_json_search") + .form(&[("searchword", &generate_slug(&query))]) + .send() + .await? + .text() + .await?; + if text.is_empty() { + return Ok(Vec::new()); + } + let mut results: Vec = serde_json::from_str(&text).unwrap(); + for i in 0..results.len() { + let old_result = &results[i]; + results[i] = structs::SearchResult { + id: old_result.id.clone(), + name: remove_html(&old_result.name), + name_unsigned: old_result.name_unsigned.clone(), + last_chapter: remove_html(&old_result.last_chapter), + image: old_result.image.clone(), + author: remove_html(&old_result.author), + story_link: old_result.story_link.clone() + }; + } + Ok(results) +} + +pub async fn get_manga(client: reqwest::Client, manga_id: &str) -> Result { + let text = client.get(&format!("https://mangakakalot.com/manga/{}", &manga_id)) + .send() + .await? + .text() + .await?; + let resp = parse_mangakakalot_manga(&text, &manga_id); + Ok(match resp { + structs::MangaOption::Manga(_) => resp, + structs::MangaOption::Redirect(redirect) => { + let text = client.get(&redirect.url) + .send() + .await? + .text() + .await?; + let resp = match reqwest::Url::parse(&redirect.url).unwrap().host_str().unwrap() { + "mangakakalot.com" => parse_mangakakalot_manga(&text, &manga_id), + "manganelo.com" => parse_manganelo_manga(&text, &manga_id), + _ => panic!("Unknown URL: {}", &redirect.url) + }; + match resp { + structs::MangaOption::Manga(_) => resp, + structs::MangaOption::Redirect(_) => panic!("Nested redirect"), + structs::MangaOption::None => structs::MangaOption::None + } + }, + structs::MangaOption::None => structs::MangaOption::None + }) +} + +fn parse_manganelo_manga(text: &str, manga_id: &str) -> structs::MangaOption { + let mut split: Vec<&str> = text.splitn(2, "\n").collect(); + let screaming_doctype = split[0].to_uppercase(); + split[0] = &screaming_doctype; + let text = split.join("\n"); + let mut name: Option = None; + let mut status: Option = None; + let mut last_updated: Option = None; + let mut summary = String::new(); + let mut authors = Vec::new(); + let mut genres = Vec::new(); + let mut chapters = Vec::new(); + let mut is_inside_h1 = false; + let mut is_inside_a = false; + let mut is_inside_td = false; + let mut is_inside_authors = false; + let mut is_inside_genres = false; + let mut is_inside_status = false; + let mut is_inside_stre_value = false; + let mut is_inside_h3 = false; + let mut is_inside_ul = false; + let mut is_inside_description = false; + let mut tmp_chapter_link: Option = None; + let mut reader = Reader::from_str(&text); + reader.check_end_names(false); + let mut buf = Vec::new(); + loop { + match reader.read_event(&mut buf) { + Ok(Event::Start(ref e)) => { + match e.name() { + b"a" => { + is_inside_a = true; + if is_inside_ul { + tmp_chapter_link = Some(e.attributes() + .find(|attribute| attribute.as_ref().unwrap().key == b"href") + .unwrap() + .unwrap() + .unescape_and_decode_value(&reader) + .unwrap()); + } + }, + b"ul" => is_inside_ul = true, + b"h1" => is_inside_h1 = true, + b"h3" => is_inside_h3 = true, + b"td" => { + let is_table_value = e.attributes() + .find(|attribute| { + let attribute = attribute.as_ref().unwrap(); + attribute.key == b"class" && + attribute.unescape_and_decode_value(&reader).unwrap().as_str() == "table-value" + }).is_some(); + if is_table_value { + is_inside_td = true; + } + }, + b"i" => { + let class = e.attributes() + .find(|attribute| attribute.as_ref().unwrap().key == b"class"); + if class.is_some() { + let class_name = class.unwrap() + .unwrap() + .unescape_and_decode_value(&reader) + .unwrap(); + match class_name.as_str() { + "info-author" => is_inside_authors = true, + "info-status" => is_inside_status = true, + "info-genres" => is_inside_genres = true, + _ => () + } + } + }, + b"span" => { + let is_stre_value = e.attributes() + .find(|attribute| { + let attribute = attribute.as_ref().unwrap(); + attribute.key == b"class" && + attribute.unescape_and_decode_value(&reader).unwrap().as_str() == "stre-value" + }).is_some(); + if is_stre_value { + is_inside_stre_value = true; + } + }, + b"div" => { + let is_description = e.attributes() + .find(|attribute| { + let attribute = attribute.as_ref().unwrap(); + attribute.key == b"class" && + attribute.unescape_and_decode_value(&reader).unwrap().as_str() == "panel-story-info-description" + }).is_some(); + if is_description { + is_inside_description = true; + } + }, + _ => () + }; + }, + Ok(Event::Text(e)) => { + let text = match e.unescape_and_decode(&reader) { + Ok(text) => text, + Err(_) => { + buf.clear(); + continue; + } + }; + if name.is_none() && is_inside_h1 { + name = Some(text); + } else if is_inside_authors && is_inside_td && is_inside_a { + authors.push(text); + } else if is_inside_status && is_inside_td { + status = Some(text); + } else if is_inside_genres && is_inside_td && is_inside_a { + genres.push(text); + } else if last_updated.is_none() && is_inside_stre_value { + last_updated = Some(text); + } else if is_inside_description && !is_inside_h3 { + summary.push_str(text.trim()); + } else if is_inside_ul && is_inside_a && tmp_chapter_link.is_some() { + let chapter_name = match text.splitn(2, &[':', '-'][..]).nth(1) { + Some(text) => Some(text.trim().to_string()), + None => None + }; + chapters.push(structs::Chapter { + chapter_number: tmp_chapter_link.unwrap().rsplitn(2, '_').nth(0).unwrap().to_string(), + chapter_name: chapter_name, + domain: "manganelo.com".to_string() + }); + tmp_chapter_link = None; + } else if text.trim().starts_with("REDIRECT : ") { + return structs::MangaOption::Redirect(structs::Redirect { url: text.splitn(2, ':').nth(1).unwrap().trim().to_string() }); + } + }, + Ok(Event::End(e)) => { + match e.name() { + b"a" => is_inside_a = false, + b"h1" => is_inside_h1 = false, + b"h3" => is_inside_h3 = false, + b"td" => { + if is_inside_td { + is_inside_td = false; + is_inside_authors = false; + is_inside_genres = false; + is_inside_status = false; + } + }, + b"div" => is_inside_description = false, + b"ul" => break, + _ => () + }; + }, + Err(err) => panic!("Error at position {}: {}", reader.buffer_position(), err), + Ok(Event::Eof) => break, + _ => () + } + buf.clear(); + } + chapters.reverse(); + structs::MangaOption::Manga(structs::Manga { + id: manga_id.to_string(), + name: name.unwrap(), + authors: authors, + status: status.unwrap(), + last_updated: last_updated.unwrap(), + genres: genres, + summary: summary.trim().to_string(), + chapters: chapters + } + ) +} + +fn parse_mangakakalot_manga(text: &str, manga_id: &str) -> structs::MangaOption { + let mut split: Vec<&str> = text.splitn(2, "\n").collect(); + let screaming_doctype = split[0].to_uppercase(); + split[0] = &screaming_doctype; + let text = split.join("\n"); + let mut is_inside_title = false; + let mut is_title_real = false; + let mut is_inside_chapter_list = false; + let mut is_inside_manga_info = false; + let mut is_inside_authors = false; + let mut is_inside_genres = false; + let mut is_inside_a = false; + let mut is_inside_row = false; + let mut name: Option = None; + let mut status: Option = None; + let mut last_updated: Option = None; + let mut summary = String::new(); + let mut is_inside_noidungm = false; + let mut is_inside_h1 = false; + let mut is_inside_h2 = false; + let mut authors = Vec::new(); + let mut genres = Vec::new(); + let mut chapters = Vec::new(); + let mut tmp_chapter_link: Option = None; + let mut reader = Reader::from_str(&text); + reader.check_end_names(false); + let mut buf = Vec::new(); + loop { + match reader.read_event(&mut buf) { + Ok(Event::Start(ref e)) => { + match e.name() { + b"ul" => { + let is_manga_info_text = e.attributes() + .find(|attribute| { + let attribute = attribute.as_ref().unwrap(); + attribute.key == b"class" && + attribute.unescape_and_decode_value(&reader).unwrap().as_str() == "manga-info-text" + }).is_some(); + if is_manga_info_text { + is_inside_manga_info = true; + } + }, + b"div" => { + let class = e.attributes() + .find(|attribute| attribute.as_ref().unwrap().key == b"class"); + if class.is_some() { + let class_name = class.unwrap() + .unwrap() + .unescape_and_decode_value(&reader) + .unwrap(); + match class_name.as_str() { + "chapter-list" => is_inside_chapter_list = true, + "row" => is_inside_row = true, + _ => () + }; + } + let id = e.attributes() + .find(|attribute| { + match attribute.as_ref() { + Ok(attribute) => attribute.key == b"id", + Err(_) => false + } + }); + if id.is_some() { + if id.unwrap().unwrap().unescape_and_decode_value(&reader).unwrap().as_str() == "noidungm" { + is_inside_noidungm = true; + } + } + }, + b"h1" => is_inside_h1 = true, + b"h2" => is_inside_h2 = true, + b"a" => { + is_inside_a = true; + if is_inside_chapter_list { + tmp_chapter_link = Some(e.attributes() + .find(|attribute| attribute.as_ref().unwrap().key == b"href") + .unwrap() + .unwrap() + .unescape_and_decode_value(&reader) + .unwrap()); + } + }, + b"title" => is_inside_title = true, + _ => () + }; + }, + Ok(Event::Text(e)) => { + let text = match e.unescape_and_decode(&reader) { + Ok(text) => text, + Err(_) => { + buf.clear(); + continue; + } + }; + if is_inside_manga_info { + if is_inside_h1 { + name = Some(text); + } else if is_inside_authors && is_inside_a { + authors.push(text); + } else if is_inside_genres && is_inside_a { + genres.push(text); + } else { + match text.splitn(2, ' ').nth(0).unwrap() { + "Author(s)" => is_inside_authors = true, + "Status" => status = Some(text.splitn(3, ' ').nth(2).unwrap().to_string()), + "Last" => { + if text.starts_with("Last updated : ") { + last_updated = Some(text.splitn(4, ' ').nth(3).unwrap().to_string()); + } + }, + "Genres" => is_inside_genres = true, + _ => () + } + } + } else if is_inside_noidungm && !is_inside_h2 { + summary.push_str(&text.trim()); + } else if is_inside_chapter_list && is_inside_a && tmp_chapter_link.is_some() { + let chapter_name = match text.splitn(2, &[':', '-'][..]).nth(1) { + Some(text) => Some(text.trim().to_string()), + None => None + }; + chapters.push(structs::Chapter { + chapter_number: tmp_chapter_link.unwrap().rsplitn(2, '_').nth(0).unwrap().to_string(), + chapter_name: chapter_name, + domain: "mangakakalot.com".to_string() + }); + tmp_chapter_link = None; + } else if is_inside_title { + is_title_real = !text.trim().is_empty(); + } else if text.trim().starts_with("REDIRECT : ") { + return structs::MangaOption::Redirect(structs::Redirect { url: text.splitn(2, ':').nth(1).unwrap().trim().to_string() }); + } + }, + Ok(Event::Empty(ref e)) => { + if is_inside_noidungm { + if e.name() == b"br" { + summary.push_str("\n"); + } + } + }, + Ok(Event::End(e)) => { + match e.name() { + b"ul" => is_inside_manga_info = false, + b"li" => { + is_inside_authors = false; + is_inside_genres = false; + }, + b"div" => { + if is_inside_noidungm { + is_inside_noidungm = false; + } else if is_inside_row { + is_inside_row = false; + } else if is_inside_chapter_list { + break; + } + }, + b"h1" => is_inside_h1 = false, + b"h2" => is_inside_h2 = false, + b"a" => is_inside_a = false, + b"title" => { + if !is_title_real { + return structs::MangaOption::None; + } + }, + _ => () + }; + }, + Err(err) => panic!("Error at position {}: {}", reader.buffer_position(), err), + Ok(Event::Eof) => break, + _ => () + }; + buf.clear(); + } + chapters.reverse(); + structs::MangaOption::Manga(structs::Manga { + id: manga_id.to_string(), + name: name.unwrap(), + authors: authors, + status: status.unwrap(), + last_updated: last_updated.unwrap(), + genres: genres, + summary: summary.trim().to_string(), + chapters: chapters + }) +} + +pub async fn get_pages(client: reqwest::Client, chapter: &structs::Chapter, manga_id: &str) -> Result, reqwest::Error> { + let text = client.get(&format!("https://{}/chapter/{}/chapter_{}", &chapter.domain, &manga_id, &chapter.chapter_number)) + .send() + .await? + .text() + .await?; + Ok(match chapter.domain.as_str() { + "mangakakalot.com" => parse_mangakakalot_pages(&text), + "manganelo.com" => parse_manganelo_pages(&text), + _ => panic!("Unknown domain: {}", &chapter.domain) + }) +} + +fn parse_mangakakalot_pages(text: &str) -> Vec { + let mut split: Vec<&str> = text.splitn(2, "\n").collect(); + let screaming_doctype = split[0].to_uppercase(); + split[0] = &screaming_doctype; + let text = split.join("\n"); + let mut pages = Vec::new(); + let mut reader = Reader::from_str(&text); + reader.check_end_names(false); + let mut buf = Vec::new(); + loop { + match reader.read_event(&mut buf) { + Ok(Event::Empty(ref e)) => { + if e.name() == b"img" { + let mut src: Option = None; + let mut alt: Option = None; + for attribute in e.attributes() { + let attribute = attribute.unwrap(); + match attribute.key { + b"src" => src = Some(attribute.unescape_and_decode_value(&reader).unwrap()), + b"alt" => alt = Some(attribute.unescape_and_decode_value(&reader).unwrap()), + _ => () + }; + } + if src.is_some() && alt.is_some() { + if alt.unwrap().ends_with(" - Mangakakalot.com") { + pages.push(src.unwrap()); + } + } + } + }, + Err(err) => panic!("Error at position {}: {}", reader.buffer_position(), err), + Ok(Event::Eof) => break, + _ => () + } + buf.clear(); + } + pages +} + +fn parse_manganelo_pages(text: &str) -> Vec { + let mut split: Vec<&str> = text.splitn(2, "\n").collect(); + let screaming_doctype = split[0].to_uppercase(); + split[0] = &screaming_doctype; + let text = split.join("\n"); + let mut pages = Vec::new(); + let mut reader = Reader::from_str(&text); + reader.check_end_names(false); + let mut buf = Vec::new(); + loop { + match reader.read_event(&mut buf) { + Ok(Event::Empty(ref e)) => { + if e.name() == b"img" { + let mut src: Option = None; + let mut alt: Option = None; + for attribute in e.attributes() { + let attribute = attribute.unwrap(); + match attribute.key { + b"src" => src = Some(attribute.unescape_and_decode_value(&reader).unwrap()), + b"alt" => alt = Some(attribute.unescape_and_decode_value(&reader).unwrap()), + _ => () + }; + } + if src.is_some() && alt.is_some() { + if alt.unwrap().ends_with(" - MangaNelo.com") { + pages.push(src.unwrap()); + } + } + } + }, + Err(err) => panic!("Error at position {}: {}", reader.buffer_position(), err), + Ok(Event::Eof) => break, + _ => () + } + buf.clear(); + } + pages +} + +pub async fn download_file(client: reqwest::Client, url: &str, file_name: &PathBuf, referer: &str) -> Result<(), reqwest::Error> { + let bytes = client.get(url) + .header("Referer", referer) + .send() + .await? + .bytes() + .await?; + if !file_name.parent().unwrap().is_dir() { + create_dir(file_name.parent().unwrap()).unwrap(); + } + let mut file = File::create(&file_name).unwrap(); + file.write_all(&bytes).unwrap(); + Ok(()) +}