biblius пре 10 месеци
комит
2312089d07
17 измењених фајлова са 2832 додато и 0 уклоњено
  1. 5 0
      .env.example
  2. 3 0
      .gitignore
  3. 1694 0
      Cargo.lock
  4. 35 0
      Cargo.toml
  5. BIN
      assets/dirico.png
  6. 14 0
      assets/filecontainer.html
  7. 0 0
      assets/htmx.min.js
  8. 11 0
      assets/index.html
  9. 78 0
      assets/styles.css
  10. 0 0
      response.json
  11. 40 0
      src/error.rs
  12. 146 0
      src/main.rs
  13. 208 0
      src/ost/client.rs
  14. 177 0
      src/ost/data.rs
  15. 55 0
      src/ost/hash.rs
  16. 3 0
      src/ost/mod.rs
  17. 363 0
      src/routes.rs

+ 5 - 0
.env.example

@@ -0,0 +1,5 @@
+JWT = 
+API_KEY = 
+USERNAME = 
+PASSWORD = 
+BASE_PATH = 

+ 3 - 0
.gitignore

@@ -0,0 +1,3 @@
+/target
+.env
+.vscode

+ 1694 - 0
Cargo.lock

@@ -0,0 +1,1694 @@
+# This file is automatically @generated by Cargo.
+# It is not intended for manual editing.
+version = 3
+
+[[package]]
+name = "addr2line"
+version = "0.21.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "8a30b2e23b9e17a9f90641c7ab1549cd9b44f296d3ccbf309d2863cfe398a0cb"
+dependencies = [
+ "gimli",
+]
+
+[[package]]
+name = "adler"
+version = "1.0.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "f26201604c87b1e01bd3d98f8d5d9a8fcbb815e8cedb41ffccbeb4bf593a35fe"
+
+[[package]]
+name = "android-tzdata"
+version = "0.1.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "e999941b234f3131b00bc13c22d06e8c5ff726d1b6318ac7eb276997bbb4fef0"
+
+[[package]]
+name = "android_system_properties"
+version = "0.1.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "819e7219dbd41043ac279b19830f2efc897156490d7fd6ea916720117ee66311"
+dependencies = [
+ "libc",
+]
+
+[[package]]
+name = "async-trait"
+version = "0.1.77"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "c980ee35e870bd1a4d2c8294d4c04d0499e67bca1e4b5cefcc693c2fa00caea9"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "syn",
+]
+
+[[package]]
+name = "autocfg"
+version = "1.1.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "d468802bab17cbc0cc575e9b053f41e72aa36bfa6b7f55e3529ffa43161b97fa"
+
+[[package]]
+name = "axum"
+version = "0.7.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "d09dbe0e490df5da9d69b36dca48a76635288a82f92eca90024883a56202026d"
+dependencies = [
+ "async-trait",
+ "axum-core",
+ "bytes",
+ "futures-util",
+ "http 1.0.0",
+ "http-body 1.0.0",
+ "http-body-util",
+ "hyper 1.1.0",
+ "hyper-util",
+ "itoa",
+ "matchit",
+ "memchr",
+ "mime",
+ "percent-encoding",
+ "pin-project-lite",
+ "rustversion",
+ "serde",
+ "serde_json",
+ "serde_path_to_error",
+ "serde_urlencoded",
+ "sync_wrapper",
+ "tokio",
+ "tower",
+ "tower-layer",
+ "tower-service",
+ "tracing",
+]
+
+[[package]]
+name = "axum-core"
+version = "0.4.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "e87c8503f93e6d144ee5690907ba22db7ba79ab001a932ab99034f0fe836b3df"
+dependencies = [
+ "async-trait",
+ "bytes",
+ "futures-util",
+ "http 1.0.0",
+ "http-body 1.0.0",
+ "http-body-util",
+ "mime",
+ "pin-project-lite",
+ "rustversion",
+ "sync_wrapper",
+ "tower-layer",
+ "tower-service",
+ "tracing",
+]
+
+[[package]]
+name = "backtrace"
+version = "0.3.69"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "2089b7e3f35b9dd2d0ed921ead4f6d318c27680d4a5bd167b3ee120edb105837"
+dependencies = [
+ "addr2line",
+ "cc",
+ "cfg-if",
+ "libc",
+ "miniz_oxide",
+ "object",
+ "rustc-demangle",
+]
+
+[[package]]
+name = "base64"
+version = "0.21.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "35636a1494ede3b646cc98f74f8e62c773a38a659ebc777a2cf26b9b74171df9"
+
+[[package]]
+name = "bitflags"
+version = "1.3.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "bef38d45163c2f1dde094a7dfd33ccf595c92905c8f8f4fdc18d06fb1037718a"
+
+[[package]]
+name = "bitflags"
+version = "2.4.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "327762f6e5a765692301e5bb513e0d9fef63be86bbc14528052b1cd3e6f03e07"
+
+[[package]]
+name = "bumpalo"
+version = "3.14.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "7f30e7476521f6f8af1a1c4c0b8cc94f0bee37d91763d0ca2665f299b6cd8aec"
+
+[[package]]
+name = "bytes"
+version = "1.5.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "a2bd12c1caf447e69cd4528f47f94d203fd2582878ecb9e9465484c4148a8223"
+
+[[package]]
+name = "cc"
+version = "1.0.83"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "f1174fb0b6ec23863f8b971027804a42614e347eafb0a95bf0b12cdae21fc4d0"
+dependencies = [
+ "libc",
+]
+
+[[package]]
+name = "cfg-if"
+version = "1.0.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd"
+
+[[package]]
+name = "chrono"
+version = "0.4.31"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "7f2c685bad3eb3d45a01354cedb7d5faa66194d1d58ba6e267a8de788f79db38"
+dependencies = [
+ "android-tzdata",
+ "iana-time-zone",
+ "js-sys",
+ "num-traits",
+ "serde",
+ "wasm-bindgen",
+ "windows-targets 0.48.5",
+]
+
+[[package]]
+name = "core-foundation"
+version = "0.9.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "91e195e091a93c46f7102ec7818a2aa394e1e1771c3ab4825963fa03e45afb8f"
+dependencies = [
+ "core-foundation-sys",
+ "libc",
+]
+
+[[package]]
+name = "core-foundation-sys"
+version = "0.8.6"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "06ea2b9bc92be3c2baa9334a323ebca2d6f074ff852cd1d7b11064035cd3868f"
+
+[[package]]
+name = "dotenv"
+version = "0.15.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "77c90badedccf4105eca100756a0b1289e191f6fcbdadd3cee1d2f614f97da8f"
+
+[[package]]
+name = "encoding_rs"
+version = "0.8.33"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "7268b386296a025e474d5140678f75d6de9493ae55a5d709eeb9dd08149945e1"
+dependencies = [
+ "cfg-if",
+]
+
+[[package]]
+name = "equivalent"
+version = "1.0.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "5443807d6dff69373d433ab9ef5378ad8df50ca6298caf15de6e52e24aaf54d5"
+
+[[package]]
+name = "errno"
+version = "0.3.8"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "a258e46cdc063eb8519c00b9fc845fc47bcfca4130e2f08e88665ceda8474245"
+dependencies = [
+ "libc",
+ "windows-sys 0.52.0",
+]
+
+[[package]]
+name = "fastrand"
+version = "2.0.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "25cbce373ec4653f1a01a31e8a5e5ec0c622dc27ff9c4e6606eefef5cbbed4a5"
+
+[[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.2.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "e13624c2627564efccf4934284bdd98cbaa14e79b0b5a141218e507b3a823456"
+dependencies = [
+ "percent-encoding",
+]
+
+[[package]]
+name = "futures-channel"
+version = "0.3.30"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "eac8f7d7865dcb88bd4373ab671c8cf4508703796caa2b1985a9ca867b3fcb78"
+dependencies = [
+ "futures-core",
+]
+
+[[package]]
+name = "futures-core"
+version = "0.3.30"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "dfc6580bb841c5a68e9ef15c77ccc837b40a7504914d52e47b8b0e9bbda25a1d"
+
+[[package]]
+name = "futures-sink"
+version = "0.3.30"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "9fb8e00e87438d937621c1c6269e53f536c14d3fbd6a042bb24879e57d474fb5"
+
+[[package]]
+name = "futures-task"
+version = "0.3.30"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "38d84fa142264698cdce1a9f9172cf383a0c82de1bddcf3092901442c4097004"
+
+[[package]]
+name = "futures-util"
+version = "0.3.30"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "3d6401deb83407ab3da39eba7e33987a73c3df0c82b4bb5813ee871c19c41d48"
+dependencies = [
+ "futures-core",
+ "futures-task",
+ "pin-project-lite",
+ "pin-utils",
+]
+
+[[package]]
+name = "getrandom"
+version = "0.2.11"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "fe9006bed769170c11f845cf00c7c1e9092aeb3f268e007c3e760ac68008070f"
+dependencies = [
+ "cfg-if",
+ "libc",
+ "wasi",
+]
+
+[[package]]
+name = "gimli"
+version = "0.28.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "4271d37baee1b8c7e4b708028c57d816cf9d2434acb33a549475f78c181f6253"
+
+[[package]]
+name = "h2"
+version = "0.3.22"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "4d6250322ef6e60f93f9a2162799302cd6f68f79f6e5d85c8c16f14d1d958178"
+dependencies = [
+ "bytes",
+ "fnv",
+ "futures-core",
+ "futures-sink",
+ "futures-util",
+ "http 0.2.11",
+ "indexmap",
+ "slab",
+ "tokio",
+ "tokio-util",
+ "tracing",
+]
+
+[[package]]
+name = "h2"
+version = "0.4.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "e1d308f63daf4181410c242d34c11f928dcb3aa105852019e043c9d1f4e4368a"
+dependencies = [
+ "bytes",
+ "fnv",
+ "futures-core",
+ "futures-sink",
+ "futures-util",
+ "http 1.0.0",
+ "indexmap",
+ "slab",
+ "tokio",
+ "tokio-util",
+ "tracing",
+]
+
+[[package]]
+name = "hashbrown"
+version = "0.14.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "290f1a1d9242c78d09ce40a5e87e7554ee637af1351968159f4952f028f75604"
+
+[[package]]
+name = "hermit-abi"
+version = "0.3.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "d77f7ec81a6d05a3abb01ab6eb7590f6083d08449fe5a1c8b1e620283546ccb7"
+
+[[package]]
+name = "http"
+version = "0.2.11"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "8947b1a6fad4393052c7ba1f4cd97bed3e953a95c79c92ad9b051a04611d9fbb"
+dependencies = [
+ "bytes",
+ "fnv",
+ "itoa",
+]
+
+[[package]]
+name = "http"
+version = "1.0.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "b32afd38673a8016f7c9ae69e5af41a58f81b1d31689040f2f1959594ce194ea"
+dependencies = [
+ "bytes",
+ "fnv",
+ "itoa",
+]
+
+[[package]]
+name = "http-body"
+version = "0.4.6"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "7ceab25649e9960c0311ea418d17bee82c0dcec1bd053b5f9a66e265a693bed2"
+dependencies = [
+ "bytes",
+ "http 0.2.11",
+ "pin-project-lite",
+]
+
+[[package]]
+name = "http-body"
+version = "1.0.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "1cac85db508abc24a2e48553ba12a996e87244a0395ce011e62b37158745d643"
+dependencies = [
+ "bytes",
+ "http 1.0.0",
+]
+
+[[package]]
+name = "http-body-util"
+version = "0.1.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "41cb79eb393015dadd30fc252023adb0b2400a0caee0fa2a077e6e21a551e840"
+dependencies = [
+ "bytes",
+ "futures-util",
+ "http 1.0.0",
+ "http-body 1.0.0",
+ "pin-project-lite",
+]
+
+[[package]]
+name = "http-range-header"
+version = "0.4.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "3ce4ef31cda248bbdb6e6820603b82dfcd9e833db65a43e997a0ccec777d11fe"
+
+[[package]]
+name = "httparse"
+version = "1.8.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "d897f394bad6a705d5f4104762e116a75639e470d80901eed05a860a95cb1904"
+
+[[package]]
+name = "httpdate"
+version = "1.0.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "df3b46402a9d5adb4c86a0cf463f42e19994e3ee891101b1841f30a545cb49a9"
+
+[[package]]
+name = "hyper"
+version = "0.14.28"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "bf96e135eb83a2a8ddf766e426a841d8ddd7449d5f00d34ea02b41d2f19eef80"
+dependencies = [
+ "bytes",
+ "futures-channel",
+ "futures-core",
+ "futures-util",
+ "h2 0.3.22",
+ "http 0.2.11",
+ "http-body 0.4.6",
+ "httparse",
+ "httpdate",
+ "itoa",
+ "pin-project-lite",
+ "socket2",
+ "tokio",
+ "tower-service",
+ "tracing",
+ "want",
+]
+
+[[package]]
+name = "hyper"
+version = "1.1.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "fb5aa53871fc917b1a9ed87b683a5d86db645e23acb32c2e0785a353e522fb75"
+dependencies = [
+ "bytes",
+ "futures-channel",
+ "futures-util",
+ "h2 0.4.0",
+ "http 1.0.0",
+ "http-body 1.0.0",
+ "httparse",
+ "httpdate",
+ "itoa",
+ "pin-project-lite",
+ "tokio",
+]
+
+[[package]]
+name = "hyper-tls"
+version = "0.5.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "d6183ddfa99b85da61a140bea0efc93fdf56ceaa041b37d553518030827f9905"
+dependencies = [
+ "bytes",
+ "hyper 0.14.28",
+ "native-tls",
+ "tokio",
+ "tokio-native-tls",
+]
+
+[[package]]
+name = "hyper-util"
+version = "0.1.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "bdea9aac0dbe5a9240d68cfd9501e2db94222c6dc06843e06640b9e07f0fdc67"
+dependencies = [
+ "bytes",
+ "futures-channel",
+ "futures-util",
+ "http 1.0.0",
+ "http-body 1.0.0",
+ "hyper 1.1.0",
+ "pin-project-lite",
+ "socket2",
+ "tokio",
+ "tracing",
+]
+
+[[package]]
+name = "iana-time-zone"
+version = "0.1.59"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "b6a67363e2aa4443928ce15e57ebae94fd8949958fd1223c4cfc0cd473ad7539"
+dependencies = [
+ "android_system_properties",
+ "core-foundation-sys",
+ "iana-time-zone-haiku",
+ "js-sys",
+ "wasm-bindgen",
+ "windows-core",
+]
+
+[[package]]
+name = "iana-time-zone-haiku"
+version = "0.1.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "f31827a206f56af32e590ba56d5d2d085f558508192593743f16b2306495269f"
+dependencies = [
+ "cc",
+]
+
+[[package]]
+name = "idna"
+version = "0.5.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "634d9b1461af396cad843f47fdba5597a4f9e6ddd4bfb6ff5d85028c25cb12f6"
+dependencies = [
+ "unicode-bidi",
+ "unicode-normalization",
+]
+
+[[package]]
+name = "indexmap"
+version = "2.1.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "d530e1a18b1cb4c484e6e34556a0d948706958449fca0cab753d649f2bce3d1f"
+dependencies = [
+ "equivalent",
+ "hashbrown",
+]
+
+[[package]]
+name = "ipnet"
+version = "2.9.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "8f518f335dce6725a761382244631d86cf0ccb2863413590b31338feb467f9c3"
+
+[[package]]
+name = "itoa"
+version = "1.0.10"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "b1a46d1a171d865aa5f83f92695765caa047a9b4cbae2cbf37dbd613a793fd4c"
+
+[[package]]
+name = "js-sys"
+version = "0.3.66"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "cee9c64da59eae3b50095c18d3e74f8b73c0b86d2792824ff01bbce68ba229ca"
+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.151"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "302d7ab3130588088d277783b1e2d2e10c9e9e4a16dd9050e6ec93fb3e7048f4"
+
+[[package]]
+name = "linux-raw-sys"
+version = "0.4.12"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "c4cd1a83af159aa67994778be9070f0ae1bd732942279cabb14f86f986a21456"
+
+[[package]]
+name = "log"
+version = "0.4.20"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "b5e6163cb8c49088c2c36f57875e58ccd8c87c7427f7fbd50ea6710b2f3f2e8f"
+
+[[package]]
+name = "matchit"
+version = "0.7.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "0e7465ac9959cc2b1404e8e2367b43684a6d13790fe23056cc8c6c5a6b7bcb94"
+
+[[package]]
+name = "memchr"
+version = "2.7.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "523dc4f511e55ab87b694dc30d0f820d60906ef06413f93d4d7a1385599cc149"
+
+[[package]]
+name = "mime"
+version = "0.3.17"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "6877bb514081ee2a7ff5ef9de3281f14a4dd4bceac4c09388074a6b5df8a139a"
+
+[[package]]
+name = "mime_guess"
+version = "2.0.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "4192263c238a5f0d0c6bfd21f336a313a4ce1c450542449ca191bb657b4642ef"
+dependencies = [
+ "mime",
+ "unicase",
+]
+
+[[package]]
+name = "minijinja"
+version = "1.0.10"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "208758577ef2c86cf5dd3e85730d161413ec3284e2d73b2ef65d9a24d9971bcb"
+dependencies = [
+ "serde",
+]
+
+[[package]]
+name = "miniz_oxide"
+version = "0.7.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "e7810e0be55b428ada41041c41f32c9f1a42817901b4ccf45fa3d4b6561e74c7"
+dependencies = [
+ "adler",
+]
+
+[[package]]
+name = "mio"
+version = "0.8.10"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "8f3d0b296e374a4e6f3c7b0a1f5a51d748a0d34c85e7dc48fc3fa9a87657fe09"
+dependencies = [
+ "libc",
+ "wasi",
+ "windows-sys 0.48.0",
+]
+
+[[package]]
+name = "native-tls"
+version = "0.2.11"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "07226173c32f2926027b63cce4bcd8076c3552846cbe7925f3aaffeac0a3b92e"
+dependencies = [
+ "lazy_static",
+ "libc",
+ "log",
+ "openssl",
+ "openssl-probe",
+ "openssl-sys",
+ "schannel",
+ "security-framework",
+ "security-framework-sys",
+ "tempfile",
+]
+
+[[package]]
+name = "ntitled"
+version = "0.1.0"
+dependencies = [
+ "axum",
+ "chrono",
+ "dotenv",
+ "lazy_static",
+ "minijinja",
+ "rand",
+ "reqwest",
+ "serde",
+ "serde_json",
+ "thiserror",
+ "tokio",
+ "tower-http",
+ "tracing",
+ "tracing-subscriber",
+ "urlencoding",
+]
+
+[[package]]
+name = "nu-ansi-term"
+version = "0.46.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "77a8165726e8236064dbb45459242600304b42a5ea24ee2948e18e023bf7ba84"
+dependencies = [
+ "overload",
+ "winapi",
+]
+
+[[package]]
+name = "num-traits"
+version = "0.2.17"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "39e3200413f237f41ab11ad6d161bc7239c84dcb631773ccd7de3dfe4b5c267c"
+dependencies = [
+ "autocfg",
+]
+
+[[package]]
+name = "num_cpus"
+version = "1.16.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "4161fcb6d602d4d2081af7c3a45852d875a03dd337a6bfdd6e06407b61342a43"
+dependencies = [
+ "hermit-abi",
+ "libc",
+]
+
+[[package]]
+name = "object"
+version = "0.32.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "a6a622008b6e321afc04970976f62ee297fdbaa6f95318ca343e3eebb9648441"
+dependencies = [
+ "memchr",
+]
+
+[[package]]
+name = "once_cell"
+version = "1.19.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "3fdb12b2476b595f9358c5161aa467c2438859caa136dec86c26fdd2efe17b92"
+
+[[package]]
+name = "openssl"
+version = "0.10.62"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "8cde4d2d9200ad5909f8dac647e29482e07c3a35de8a13fce7c9c7747ad9f671"
+dependencies = [
+ "bitflags 2.4.1",
+ "cfg-if",
+ "foreign-types",
+ "libc",
+ "once_cell",
+ "openssl-macros",
+ "openssl-sys",
+]
+
+[[package]]
+name = "openssl-macros"
+version = "0.1.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "a948666b637a0f465e8564c73e89d4dde00d72d4d473cc972f390fc3dcee7d9c"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "syn",
+]
+
+[[package]]
+name = "openssl-probe"
+version = "0.1.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "ff011a302c396a5197692431fc1948019154afc178baf7d8e37367442a4601cf"
+
+[[package]]
+name = "openssl-src"
+version = "300.2.1+3.2.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "3fe476c29791a5ca0d1273c697e96085bbabbbea2ef7afd5617e78a4b40332d3"
+dependencies = [
+ "cc",
+]
+
+[[package]]
+name = "openssl-sys"
+version = "0.9.98"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "c1665caf8ab2dc9aef43d1c0023bd904633a6a05cb30b0ad59bec2ae986e57a7"
+dependencies = [
+ "cc",
+ "libc",
+ "openssl-src",
+ "pkg-config",
+ "vcpkg",
+]
+
+[[package]]
+name = "overload"
+version = "0.1.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "b15813163c1d831bf4a13c3610c05c0d03b39feb07f7e09fa234dac9b15aaf39"
+
+[[package]]
+name = "percent-encoding"
+version = "2.3.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "e3148f5046208a5d56bcfc03053e3ca6334e51da8dfb19b6cdc8b306fae3283e"
+
+[[package]]
+name = "pin-project"
+version = "1.1.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "fda4ed1c6c173e3fc7a83629421152e01d7b1f9b7f65fb301e490e8cfc656422"
+dependencies = [
+ "pin-project-internal",
+]
+
+[[package]]
+name = "pin-project-internal"
+version = "1.1.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "4359fd9c9171ec6e8c62926d6faaf553a8dc3f64e1507e76da7911b4f6a04405"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "syn",
+]
+
+[[package]]
+name = "pin-project-lite"
+version = "0.2.13"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "8afb450f006bf6385ca15ef45d71d2288452bc3683ce2e2cacc0d18e4be60b58"
+
+[[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.28"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "69d3587f8a9e599cc7ec2c00e331f71c4e69a5f9a4b8a6efd5b07466b9736f9a"
+
+[[package]]
+name = "ppv-lite86"
+version = "0.2.17"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "5b40af805b3121feab8a3c29f04d8ad262fa8e0561883e7653e024ae4479e6de"
+
+[[package]]
+name = "proc-macro2"
+version = "1.0.74"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "2de98502f212cfcea8d0bb305bd0f49d7ebdd75b64ba0a68f937d888f4e0d6db"
+dependencies = [
+ "unicode-ident",
+]
+
+[[package]]
+name = "quote"
+version = "1.0.35"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "291ec9ab5efd934aaf503a6466c5d5251535d108ee747472c3977cc5acc868ef"
+dependencies = [
+ "proc-macro2",
+]
+
+[[package]]
+name = "rand"
+version = "0.8.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "34af8d1a0e25924bc5b7c43c079c942339d8f0a8b57c39049bef581b46327404"
+dependencies = [
+ "libc",
+ "rand_chacha",
+ "rand_core",
+]
+
+[[package]]
+name = "rand_chacha"
+version = "0.3.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "e6c10a63a0fa32252be49d21e7709d4d4baf8d231c2dbce1eaa8141b9b127d88"
+dependencies = [
+ "ppv-lite86",
+ "rand_core",
+]
+
+[[package]]
+name = "rand_core"
+version = "0.6.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "ec0be4795e2f6a28069bec0b5ff3e2ac9bafc99e6a9a7dc3547996c5c816922c"
+dependencies = [
+ "getrandom",
+]
+
+[[package]]
+name = "redox_syscall"
+version = "0.4.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "4722d768eff46b75989dd134e5c353f0d6296e5aaa3132e776cbdb56be7731aa"
+dependencies = [
+ "bitflags 1.3.2",
+]
+
+[[package]]
+name = "reqwest"
+version = "0.11.23"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "37b1ae8d9ac08420c66222fb9096fc5de435c3c48542bc5336c51892cffafb41"
+dependencies = [
+ "base64",
+ "bytes",
+ "encoding_rs",
+ "futures-core",
+ "futures-util",
+ "h2 0.3.22",
+ "http 0.2.11",
+ "http-body 0.4.6",
+ "hyper 0.14.28",
+ "hyper-tls",
+ "ipnet",
+ "js-sys",
+ "log",
+ "mime",
+ "native-tls",
+ "once_cell",
+ "percent-encoding",
+ "pin-project-lite",
+ "serde",
+ "serde_json",
+ "serde_urlencoded",
+ "system-configuration",
+ "tokio",
+ "tokio-native-tls",
+ "tower-service",
+ "url",
+ "wasm-bindgen",
+ "wasm-bindgen-futures",
+ "web-sys",
+ "winreg",
+]
+
+[[package]]
+name = "rustc-demangle"
+version = "0.1.23"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "d626bb9dae77e28219937af045c257c28bfd3f69333c512553507f5f9798cb76"
+
+[[package]]
+name = "rustix"
+version = "0.38.28"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "72e572a5e8ca657d7366229cdde4bd14c4eb5499a9573d4d366fe1b599daa316"
+dependencies = [
+ "bitflags 2.4.1",
+ "errno",
+ "libc",
+ "linux-raw-sys",
+ "windows-sys 0.52.0",
+]
+
+[[package]]
+name = "rustversion"
+version = "1.0.14"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "7ffc183a10b4478d04cbbbfc96d0873219d962dd5accaff2ffbd4ceb7df837f4"
+
+[[package]]
+name = "ryu"
+version = "1.0.16"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "f98d2aa92eebf49b69786be48e4477826b256916e84a57ff2a4f21923b48eb4c"
+
+[[package]]
+name = "schannel"
+version = "0.1.23"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "fbc91545643bcf3a0bbb6569265615222618bdf33ce4ffbbd13c4bbd4c093534"
+dependencies = [
+ "windows-sys 0.52.0",
+]
+
+[[package]]
+name = "security-framework"
+version = "2.9.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "05b64fb303737d99b81884b2c63433e9ae28abebe5eb5045dcdd175dc2ecf4de"
+dependencies = [
+ "bitflags 1.3.2",
+ "core-foundation",
+ "core-foundation-sys",
+ "libc",
+ "security-framework-sys",
+]
+
+[[package]]
+name = "security-framework-sys"
+version = "2.9.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "e932934257d3b408ed8f30db49d85ea163bfe74961f017f405b025af298f0c7a"
+dependencies = [
+ "core-foundation-sys",
+ "libc",
+]
+
+[[package]]
+name = "serde"
+version = "1.0.194"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "0b114498256798c94a0689e1a15fec6005dee8ac1f41de56404b67afc2a4b773"
+dependencies = [
+ "serde_derive",
+]
+
+[[package]]
+name = "serde_derive"
+version = "1.0.194"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "a3385e45322e8f9931410f01b3031ec534c3947d0e94c18049af4d9f9907d4e0"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "syn",
+]
+
+[[package]]
+name = "serde_json"
+version = "1.0.110"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "6fbd975230bada99c8bb618e0c365c2eefa219158d5c6c29610fd09ff1833257"
+dependencies = [
+ "itoa",
+ "ryu",
+ "serde",
+]
+
+[[package]]
+name = "serde_path_to_error"
+version = "0.1.15"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "ebd154a240de39fdebcf5775d2675c204d7c13cf39a4c697be6493c8e734337c"
+dependencies = [
+ "itoa",
+ "serde",
+]
+
+[[package]]
+name = "serde_urlencoded"
+version = "0.7.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "d3491c14715ca2294c4d6a88f15e84739788c1d030eed8c110436aafdaa2f3fd"
+dependencies = [
+ "form_urlencoded",
+ "itoa",
+ "ryu",
+ "serde",
+]
+
+[[package]]
+name = "sharded-slab"
+version = "0.1.7"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "f40ca3c46823713e0d4209592e8d6e826aa57e928f09752619fc696c499637f6"
+dependencies = [
+ "lazy_static",
+]
+
+[[package]]
+name = "slab"
+version = "0.4.9"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "8f92a496fb766b417c996b9c5e57daf2f7ad3b0bebe1ccfca4856390e3d3bb67"
+dependencies = [
+ "autocfg",
+]
+
+[[package]]
+name = "smallvec"
+version = "1.11.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "4dccd0940a2dcdf68d092b8cbab7dc0ad8fa938bf95787e1b916b0e3d0e8e970"
+
+[[package]]
+name = "socket2"
+version = "0.5.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "7b5fac59a5cb5dd637972e5fca70daf0523c9067fcdc4842f053dae04a18f8e9"
+dependencies = [
+ "libc",
+ "windows-sys 0.48.0",
+]
+
+[[package]]
+name = "syn"
+version = "2.0.46"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "89456b690ff72fddcecf231caedbe615c59480c93358a93dfae7fc29e3ebbf0e"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "unicode-ident",
+]
+
+[[package]]
+name = "sync_wrapper"
+version = "0.1.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "2047c6ded9c721764247e62cd3b03c09ffc529b2ba5b10ec482ae507a4a70160"
+
+[[package]]
+name = "system-configuration"
+version = "0.5.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "ba3a3adc5c275d719af8cb4272ea1c4a6d668a777f37e115f6d11ddbc1c8e0e7"
+dependencies = [
+ "bitflags 1.3.2",
+ "core-foundation",
+ "system-configuration-sys",
+]
+
+[[package]]
+name = "system-configuration-sys"
+version = "0.5.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "a75fb188eb626b924683e3b95e3a48e63551fcfb51949de2f06a9d91dbee93c9"
+dependencies = [
+ "core-foundation-sys",
+ "libc",
+]
+
+[[package]]
+name = "tempfile"
+version = "3.9.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "01ce4141aa927a6d1bd34a041795abd0db1cccba5d5f24b009f694bdf3a1f3fa"
+dependencies = [
+ "cfg-if",
+ "fastrand",
+ "redox_syscall",
+ "rustix",
+ "windows-sys 0.52.0",
+]
+
+[[package]]
+name = "thiserror"
+version = "1.0.56"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "d54378c645627613241d077a3a79db965db602882668f9136ac42af9ecb730ad"
+dependencies = [
+ "thiserror-impl",
+]
+
+[[package]]
+name = "thiserror-impl"
+version = "1.0.56"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "fa0faa943b50f3db30a20aa7e265dbc66076993efed8463e8de414e5d06d3471"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "syn",
+]
+
+[[package]]
+name = "thread_local"
+version = "1.1.7"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "3fdd6f064ccff2d6567adcb3873ca630700f00b5ad3f060c25b5dcfd9a4ce152"
+dependencies = [
+ "cfg-if",
+ "once_cell",
+]
+
+[[package]]
+name = "tinyvec"
+version = "1.6.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "87cc5ceb3875bb20c2890005a4e226a4651264a5c75edb2421b52861a0a0cb50"
+dependencies = [
+ "tinyvec_macros",
+]
+
+[[package]]
+name = "tinyvec_macros"
+version = "0.1.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "1f3ccbac311fea05f86f61904b462b55fb3df8837a366dfc601a0161d0532f20"
+
+[[package]]
+name = "tokio"
+version = "1.35.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "c89b4efa943be685f629b149f53829423f8f5531ea21249408e8e2f8671ec104"
+dependencies = [
+ "backtrace",
+ "bytes",
+ "libc",
+ "mio",
+ "num_cpus",
+ "pin-project-lite",
+ "socket2",
+ "tokio-macros",
+ "windows-sys 0.48.0",
+]
+
+[[package]]
+name = "tokio-macros"
+version = "2.2.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "5b8a1e28f2deaa14e508979454cb3a223b10b938b45af148bc0986de36f1923b"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "syn",
+]
+
+[[package]]
+name = "tokio-native-tls"
+version = "0.3.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "bbae76ab933c85776efabc971569dd6119c580d8f5d448769dec1764bf796ef2"
+dependencies = [
+ "native-tls",
+ "tokio",
+]
+
+[[package]]
+name = "tokio-util"
+version = "0.7.10"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "5419f34732d9eb6ee4c3578b7989078579b7f039cbbb9ca2c4da015749371e15"
+dependencies = [
+ "bytes",
+ "futures-core",
+ "futures-sink",
+ "pin-project-lite",
+ "tokio",
+ "tracing",
+]
+
+[[package]]
+name = "tower"
+version = "0.4.13"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "b8fa9be0de6cf49e536ce1851f987bd21a43b771b09473c3549a6c853db37c1c"
+dependencies = [
+ "futures-core",
+ "futures-util",
+ "pin-project",
+ "pin-project-lite",
+ "tokio",
+ "tower-layer",
+ "tower-service",
+ "tracing",
+]
+
+[[package]]
+name = "tower-http"
+version = "0.5.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "09e12e6351354851911bdf8c2b8f2ab15050c567d70a8b9a37ae7b8301a4080d"
+dependencies = [
+ "bitflags 2.4.1",
+ "bytes",
+ "futures-util",
+ "http 1.0.0",
+ "http-body 1.0.0",
+ "http-body-util",
+ "http-range-header",
+ "httpdate",
+ "mime",
+ "mime_guess",
+ "percent-encoding",
+ "pin-project-lite",
+ "tokio",
+ "tokio-util",
+ "tower-layer",
+ "tower-service",
+ "tracing",
+]
+
+[[package]]
+name = "tower-layer"
+version = "0.3.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "c20c8dbed6283a09604c3e69b4b7eeb54e298b8a600d4d5ecb5ad39de609f1d0"
+
+[[package]]
+name = "tower-service"
+version = "0.3.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "b6bc1c9ce2b5135ac7f93c72918fc37feb872bdc6a5533a8b85eb4b86bfdae52"
+
+[[package]]
+name = "tracing"
+version = "0.1.40"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "c3523ab5a71916ccf420eebdf5521fcef02141234bbc0b8a49f2fdc4544364ef"
+dependencies = [
+ "log",
+ "pin-project-lite",
+ "tracing-attributes",
+ "tracing-core",
+]
+
+[[package]]
+name = "tracing-attributes"
+version = "0.1.27"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "34704c8d6ebcbc939824180af020566b01a7c01f80641264eba0999f6c2b6be7"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "syn",
+]
+
+[[package]]
+name = "tracing-core"
+version = "0.1.32"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "c06d3da6113f116aaee68e4d601191614c9053067f9ab7f6edbcb161237daa54"
+dependencies = [
+ "once_cell",
+ "valuable",
+]
+
+[[package]]
+name = "tracing-log"
+version = "0.2.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "ee855f1f400bd0e5c02d150ae5de3840039a3f54b025156404e34c23c03f47c3"
+dependencies = [
+ "log",
+ "once_cell",
+ "tracing-core",
+]
+
+[[package]]
+name = "tracing-subscriber"
+version = "0.3.18"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "ad0f048c97dbd9faa9b7df56362b8ebcaa52adb06b498c050d2f4e32f90a7a8b"
+dependencies = [
+ "nu-ansi-term",
+ "sharded-slab",
+ "smallvec",
+ "thread_local",
+ "tracing-core",
+ "tracing-log",
+]
+
+[[package]]
+name = "try-lock"
+version = "0.2.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "e421abadd41a4225275504ea4d6566923418b7f05506fbc9c0fe86ba7396114b"
+
+[[package]]
+name = "unicase"
+version = "2.7.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "f7d2d4dafb69621809a81864c9c1b864479e1235c0dd4e199924b9742439ed89"
+dependencies = [
+ "version_check",
+]
+
+[[package]]
+name = "unicode-bidi"
+version = "0.3.14"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "6f2528f27a9eb2b21e69c95319b30bd0efd85d09c379741b0f78ea1d86be2416"
+
+[[package]]
+name = "unicode-ident"
+version = "1.0.12"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "3354b9ac3fae1ff6755cb6db53683adb661634f67557942dea4facebec0fee4b"
+
+[[package]]
+name = "unicode-normalization"
+version = "0.1.22"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "5c5713f0fc4b5db668a2ac63cdb7bb4469d8c9fed047b1d0292cc7b0ce2ba921"
+dependencies = [
+ "tinyvec",
+]
+
+[[package]]
+name = "url"
+version = "2.5.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "31e6302e3bb753d46e83516cae55ae196fc0c309407cf11ab35cc51a4c2a4633"
+dependencies = [
+ "form_urlencoded",
+ "idna",
+ "percent-encoding",
+]
+
+[[package]]
+name = "urlencoding"
+version = "2.1.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "daf8dba3b7eb870caf1ddeed7bc9d2a049f3cfdfae7cb521b087cc33ae4c49da"
+
+[[package]]
+name = "valuable"
+version = "0.1.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "830b7e5d4d90034032940e4ace0d9a9a057e7a45cd94e6c007832e39edb82f6d"
+
+[[package]]
+name = "vcpkg"
+version = "0.2.15"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "accd4ea62f7bb7a82fe23066fb0957d48ef677f6eeb8215f372f52e48bb32426"
+
+[[package]]
+name = "version_check"
+version = "0.9.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "49874b5167b65d7193b8aba1567f5c7d93d001cafc34600cee003eda787e483f"
+
+[[package]]
+name = "want"
+version = "0.3.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "bfa7760aed19e106de2c7c0b581b509f2f25d3dacaf737cb82ac61bc6d760b0e"
+dependencies = [
+ "try-lock",
+]
+
+[[package]]
+name = "wasi"
+version = "0.11.0+wasi-snapshot-preview1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "9c8d87e72b64a3b4db28d11ce29237c246188f4f51057d65a7eab63b7987e423"
+
+[[package]]
+name = "wasm-bindgen"
+version = "0.2.89"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "0ed0d4f68a3015cc185aff4db9506a015f4b96f95303897bfa23f846db54064e"
+dependencies = [
+ "cfg-if",
+ "wasm-bindgen-macro",
+]
+
+[[package]]
+name = "wasm-bindgen-backend"
+version = "0.2.89"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "1b56f625e64f3a1084ded111c4d5f477df9f8c92df113852fa5a374dbda78826"
+dependencies = [
+ "bumpalo",
+ "log",
+ "once_cell",
+ "proc-macro2",
+ "quote",
+ "syn",
+ "wasm-bindgen-shared",
+]
+
+[[package]]
+name = "wasm-bindgen-futures"
+version = "0.4.39"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "ac36a15a220124ac510204aec1c3e5db8a22ab06fd6706d881dc6149f8ed9a12"
+dependencies = [
+ "cfg-if",
+ "js-sys",
+ "wasm-bindgen",
+ "web-sys",
+]
+
+[[package]]
+name = "wasm-bindgen-macro"
+version = "0.2.89"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "0162dbf37223cd2afce98f3d0785506dcb8d266223983e4b5b525859e6e182b2"
+dependencies = [
+ "quote",
+ "wasm-bindgen-macro-support",
+]
+
+[[package]]
+name = "wasm-bindgen-macro-support"
+version = "0.2.89"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "f0eb82fcb7930ae6219a7ecfd55b217f5f0893484b7a13022ebb2b2bf20b5283"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "syn",
+ "wasm-bindgen-backend",
+ "wasm-bindgen-shared",
+]
+
+[[package]]
+name = "wasm-bindgen-shared"
+version = "0.2.89"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "7ab9b36309365056cd639da3134bf87fa8f3d86008abf99e612384a6eecd459f"
+
+[[package]]
+name = "web-sys"
+version = "0.3.66"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "50c24a44ec86bb68fbecd1b3efed7e85ea5621b39b35ef2766b66cd984f8010f"
+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 = "windows-core"
+version = "0.52.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "33ab640c8d7e35bf8ba19b884ba838ceb4fba93a4e8c65a9059d08afcfc683d9"
+dependencies = [
+ "windows-targets 0.52.0",
+]
+
+[[package]]
+name = "windows-sys"
+version = "0.48.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "677d2418bec65e3338edb076e806bc1ec15693c5d0104683f2efe857f61056a9"
+dependencies = [
+ "windows-targets 0.48.5",
+]
+
+[[package]]
+name = "windows-sys"
+version = "0.52.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "282be5f36a8ce781fad8c8ae18fa3f9beff57ec1b52cb3de0789201425d9a33d"
+dependencies = [
+ "windows-targets 0.52.0",
+]
+
+[[package]]
+name = "windows-targets"
+version = "0.48.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "9a2fa6e2155d7247be68c096456083145c183cbbbc2764150dda45a87197940c"
+dependencies = [
+ "windows_aarch64_gnullvm 0.48.5",
+ "windows_aarch64_msvc 0.48.5",
+ "windows_i686_gnu 0.48.5",
+ "windows_i686_msvc 0.48.5",
+ "windows_x86_64_gnu 0.48.5",
+ "windows_x86_64_gnullvm 0.48.5",
+ "windows_x86_64_msvc 0.48.5",
+]
+
+[[package]]
+name = "windows-targets"
+version = "0.52.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "8a18201040b24831fbb9e4eb208f8892e1f50a37feb53cc7ff887feb8f50e7cd"
+dependencies = [
+ "windows_aarch64_gnullvm 0.52.0",
+ "windows_aarch64_msvc 0.52.0",
+ "windows_i686_gnu 0.52.0",
+ "windows_i686_msvc 0.52.0",
+ "windows_x86_64_gnu 0.52.0",
+ "windows_x86_64_gnullvm 0.52.0",
+ "windows_x86_64_msvc 0.52.0",
+]
+
+[[package]]
+name = "windows_aarch64_gnullvm"
+version = "0.48.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "2b38e32f0abccf9987a4e3079dfb67dcd799fb61361e53e2882c3cbaf0d905d8"
+
+[[package]]
+name = "windows_aarch64_gnullvm"
+version = "0.52.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "cb7764e35d4db8a7921e09562a0304bf2f93e0a51bfccee0bd0bb0b666b015ea"
+
+[[package]]
+name = "windows_aarch64_msvc"
+version = "0.48.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "dc35310971f3b2dbbf3f0690a219f40e2d9afcf64f9ab7cc1be722937c26b4bc"
+
+[[package]]
+name = "windows_aarch64_msvc"
+version = "0.52.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "bbaa0368d4f1d2aaefc55b6fcfee13f41544ddf36801e793edbbfd7d7df075ef"
+
+[[package]]
+name = "windows_i686_gnu"
+version = "0.48.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "a75915e7def60c94dcef72200b9a8e58e5091744960da64ec734a6c6e9b3743e"
+
+[[package]]
+name = "windows_i686_gnu"
+version = "0.52.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "a28637cb1fa3560a16915793afb20081aba2c92ee8af57b4d5f28e4b3e7df313"
+
+[[package]]
+name = "windows_i686_msvc"
+version = "0.48.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "8f55c233f70c4b27f66c523580f78f1004e8b5a8b659e05a4eb49d4166cca406"
+
+[[package]]
+name = "windows_i686_msvc"
+version = "0.52.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "ffe5e8e31046ce6230cc7215707b816e339ff4d4d67c65dffa206fd0f7aa7b9a"
+
+[[package]]
+name = "windows_x86_64_gnu"
+version = "0.48.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "53d40abd2583d23e4718fddf1ebec84dbff8381c07cae67ff7768bbf19c6718e"
+
+[[package]]
+name = "windows_x86_64_gnu"
+version = "0.52.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "3d6fa32db2bc4a2f5abeacf2b69f7992cd09dca97498da74a151a3132c26befd"
+
+[[package]]
+name = "windows_x86_64_gnullvm"
+version = "0.48.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "0b7b52767868a23d5bab768e390dc5f5c55825b6d30b86c844ff2dc7414044cc"
+
+[[package]]
+name = "windows_x86_64_gnullvm"
+version = "0.52.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "1a657e1e9d3f514745a572a6846d3c7aa7dbe1658c056ed9c3344c4109a6949e"
+
+[[package]]
+name = "windows_x86_64_msvc"
+version = "0.48.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "ed94fce61571a4006852b7389a063ab983c02eb1bb37b47f8272ce92d06d9538"
+
+[[package]]
+name = "windows_x86_64_msvc"
+version = "0.52.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "dff9641d1cd4be8d1a070daf9e3773c5f67e78b4d9d42263020c057706765c04"
+
+[[package]]
+name = "winreg"
+version = "0.50.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "524e57b2c537c0f9b1e69f1965311ec12182b4122e45035b1508cd24d2adadb1"
+dependencies = [
+ "cfg-if",
+ "windows-sys 0.48.0",
+]

+ 35 - 0
Cargo.toml

@@ -0,0 +1,35 @@
+[package]
+name = "ntitled"
+version = "0.1.0"
+edition = "2021"
+
+# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
+
+[dependencies]
+axum = "0.7.3"
+chrono = { version = "0.4.31", features = ["serde"] }
+dotenv = "0.15.0"
+lazy_static = "1.4.0"
+minijinja = "1.0.10"
+rand = "0.8.5"
+reqwest = { version = "0.11.23", features = ["json"] }
+serde = { version = "1.0.194", features = ["derive"] }
+serde_json = "1.0.110"
+thiserror = "1.0.56"
+tokio = { version = "1.35.1", features = ["macros", "rt-multi-thread"] }
+tower-http = { version = "0.5.0", features = ["fs"] }
+tracing = "0.1.40"
+tracing-subscriber = "0.3.18"
+urlencoding = "2.1.3"
+
+[features]
+debug = []
+
+[target.'cfg(target_arch = "aarch64")'.dependencies]
+reqwest = { version = "0.11.23", features = ["native-tls-vendored", "json"] }
+
+[profile.release]
+codegen-units = 1 # https://doc.rust-lang.org/rustc/codegen-options/index.html#codegen-units
+lto = true        # https://doc.rust-lang.org/rustc/codegen-options/index.html#lto
+opt-level = "z"   # https://doc.rust-lang.org/rustc/codegen-options/index.html#opt-level
+strip = "symbols" # https://doc.rust-lang.org/rustc/codegen-options/index.html#strip

BIN
assets/dirico.png


+ 14 - 0
assets/filecontainer.html

@@ -0,0 +1,14 @@
+<div class="file-entry {{ ty }}">
+
+    <h2 hx-target="#_{{ id }}" {{ get }}>{{ name }}{{ has_subs }}</h2>
+
+    <div class="guess">
+        <h4>Guess</h4>
+        <button hx-get="/guess?filename={{ name }}" hx-target="#_{{ guess_id }}">Guess</button>
+        <div id="_{{ guess_id }}"></div>
+    </div>
+
+    <div class="subtitles">
+        <div id="_{{ id }}" class="subtitles-entry"></div>
+    </div>
+</div>

Разлика између датотеке није приказан због своје велике величине
+ 0 - 0
assets/htmx.min.js


+ 11 - 0
assets/index.html

@@ -0,0 +1,11 @@
+<!DOCTYPE html>
+
+<script type="module" src="/htmx.min.js"></script>
+<link rel="stylesheet" href="/styles.css">
+
+<body>
+    <a id="header" href="/dir/root">Ntitled</a>
+    <main id="files">
+        {{ divs }}
+    </main>
+</body>

+ 78 - 0
assets/styles.css

@@ -0,0 +1,78 @@
+html * {
+  font-family: Arial;
+}
+
+a {
+  font-family: Arial;
+  text-decoration: none;
+  color: black;
+}
+
+.hashmatch {
+  color: green;
+}
+
+#header {
+  margin: auto;
+}
+
+#header:hover {
+  cursor: pointer;
+}
+
+main {
+  display: flex;
+  justify-content: center;
+  width: 80%;
+  margin: auto;
+  flex-wrap: wrap;
+}
+
+.file-entry {
+  padding: 0.2rem 0.5rem;
+  width: 100%;
+  border: 1px solid black;
+  display: flex;
+  flex-wrap: wrap;
+  align-items: center;
+}
+
+.file-entry h2:hover {
+  cursor: pointer;
+}
+
+.subtitles {
+  width: 100%;
+  max-height: 24rem;
+  overflow-y: scroll;
+}
+
+.sub-container {
+  width: 100%;
+  display: flex;
+  justify-content: center;
+  align-items: center;
+  border-box: content;
+  border-bottom: 1px dotted gray;
+  padding-bottom: 0.2rem;
+}
+
+.sub-container-title {
+  width: 50%;
+  text-align: center;
+}
+
+.sub-container-info {
+  width: 50%;
+}
+
+.sub-files {
+}
+
+.guess {
+  width: 100%;
+}
+
+.dir:hover {
+  cursor: pointer;
+}

Разлика између датотеке није приказан због своје велике величине
+ 0 - 0
response.json


+ 40 - 0
src/error.rs

@@ -0,0 +1,40 @@
+use std::string::FromUtf8Error;
+
+use axum::{http::StatusCode, response::IntoResponse};
+use thiserror::Error;
+
+#[derive(Debug, Error)]
+pub enum NtitledError {
+    #[error("{0}")]
+    IO(#[from] std::io::Error),
+
+    #[error("{0}")]
+    Serde(#[from] serde_json::Error),
+
+    #[error("{0}")]
+    Reqwest(#[from] reqwest::Error),
+
+    #[error("{0}")]
+    Message(String),
+
+    #[error("{0}")]
+    Utf8(#[from] FromUtf8Error),
+}
+
+impl IntoResponse for NtitledError {
+    fn into_response(self) -> axum::response::Response {
+        match self {
+            NtitledError::IO(e) => {
+                (StatusCode::INTERNAL_SERVER_ERROR, e.to_string()).into_response()
+            }
+            NtitledError::Serde(e) => {
+                (StatusCode::INTERNAL_SERVER_ERROR, e.to_string()).into_response()
+            }
+            NtitledError::Reqwest(e) => {
+                (StatusCode::INTERNAL_SERVER_ERROR, e.to_string()).into_response()
+            }
+            NtitledError::Utf8(e) => (StatusCode::BAD_REQUEST, e.to_string()).into_response(),
+            NtitledError::Message(m) => (StatusCode::BAD_REQUEST, m).into_response(),
+        }
+    }
+}

+ 146 - 0
src/main.rs

@@ -0,0 +1,146 @@
+use std::{fs, path::Path};
+
+use axum::{
+    response::Redirect,
+    routing::{get, get_service},
+    Router,
+};
+use minijinja::Environment;
+use ost::client::OSClient;
+use serde::Serialize;
+use tower_http::services::ServeDir;
+use tracing::info;
+
+pub mod error;
+pub mod ost;
+pub mod routes;
+
+#[derive(Debug, Clone)]
+pub struct State {
+    client: OSClient,
+    env: Environment<'static>,
+    base_path: String,
+}
+
+lazy_static::lazy_static! {
+    pub static ref INDEX: String =
+        std::fs::read_to_string("assets/index.html").expect("missing template");
+}
+
+lazy_static::lazy_static! {
+    pub static ref FILE_CONTAINER: String =
+        std::fs::read_to_string("assets/filecontainer.html").expect("missing template");
+}
+
+#[tokio::main]
+async fn main() {
+    dotenv::dotenv().expect("could not load env");
+
+    tracing_subscriber::fmt().init();
+
+    let client = OSClient::init().await;
+
+    info!("Successfully loaded client");
+
+    let base_path = std::env::var("BASE_PATH").expect("base path not configured");
+
+    let mut env = Environment::new();
+
+    env.add_template("file_container", &FILE_CONTAINER)
+        .expect("unable to add template");
+
+    env.add_template("index", &INDEX)
+        .expect("unable to add template");
+
+    let state = State {
+        client,
+        env,
+        base_path,
+    };
+
+    let router = axum::Router::new()
+        .route("/", get(|| async { Redirect::permanent("/dir/root") }))
+        .route("/dir/*key", get(routes::get_directory))
+        .route("/subtitles", get(routes::search_subtitles))
+        .route("/guess", get(routes::guess_it))
+        .route("/download", get(routes::download_subtitles))
+        .with_state(state);
+
+    let router_static = Router::new().fallback(get_service(ServeDir::new("assets")));
+
+    let listener = tokio::net::TcpListener::bind("0.0.0.0:3001").await.unwrap();
+
+    axum::serve(listener, router.merge(router_static))
+        .await
+        .unwrap();
+}
+
+pub fn scan_dir_contents(path: impl AsRef<Path>) -> std::io::Result<Vec<FileType>> {
+    let entries = fs::read_dir(path)?
+        .filter_map(Result::ok)
+        .collect::<Vec<_>>();
+
+    let subs = entries
+        .iter()
+        .filter_map(|entry| {
+            let path = entry.path();
+            let ext = path.extension()?.to_str()?;
+            let (name, _) = path.file_name()?.to_str()?.split_once(ext)?;
+            (ext == "srt").then_some(name.to_owned())
+        })
+        .collect::<Vec<_>>();
+
+    let mut files: Vec<_> = entries
+        .iter()
+        .filter_map(|entry| {
+            if entry.path().is_dir() {
+                Some(FileType::Directory(Directory {
+                    name: entry.file_name().to_str().unwrap_or_default().to_string(),
+                    path: entry.path().to_str().unwrap_or_default().to_string(),
+                }))
+                // Skip existing subtitles
+            } else if entry.path().extension().is_some_and(|ext| {
+                ext.to_str().is_some_and(|ext| ext == "srt")
+                    || ext.to_str().is_some_and(|ext| ext == "smi")
+            }) {
+                None
+            } else {
+                let path = entry.path();
+                let ext = path.extension()?.to_str()?;
+                let (name, _) = path.file_name()?.to_str()?.split_once(ext)?;
+                Some(FileType::File(File {
+                    name: entry.file_name().to_str().unwrap_or_default().to_string(),
+                    path: entry.path().to_str().unwrap_or_default().to_string(),
+                    has_subs: subs.contains(&name.to_string()),
+                }))
+            }
+        })
+        .collect();
+
+    files.sort();
+
+    Ok(files)
+}
+
+#[derive(Debug, Serialize, PartialEq, Eq, PartialOrd, Ord)]
+pub enum FileType {
+    Directory(Directory),
+    File(File),
+}
+
+#[derive(Debug, Serialize, PartialEq, Eq, PartialOrd, Ord)]
+pub struct File {
+    name: String,
+    path: String,
+
+    /// `true` if a same file exists with a `.srt` extension found
+    /// in the same directory
+    has_subs: bool,
+}
+
+#[derive(Debug, Serialize, PartialEq, Eq, PartialOrd, Ord)]
+pub struct Directory {
+    name: String,
+
+    path: String,
+}

+ 208 - 0
src/ost/client.rs

@@ -0,0 +1,208 @@
+use reqwest::{header::HeaderMap, Client};
+use serde_json::{json, Value};
+use tracing::info;
+
+use crate::{error::NtitledError, ost::data::DownloadResponse};
+
+use super::data::{GuessIt, Login, LoginResponse, SearchResponse};
+
+const BASE_URL: &str = "https://api.opensubtitles.com/api/v1";
+
+#[derive(Debug, Clone)]
+pub struct OSClient {
+    client: Client,
+    token: String,
+}
+
+impl OSClient {
+    pub async fn init() -> Self {
+        let api_key = std::env::var("API_KEY").expect("missing API_KEY in env");
+
+        let username = std::env::var("USERNAME").expect("missing USERNAME in env");
+        let password = std::env::var("PASSWORD").expect("missing PASSWORD in env");
+
+        let token = std::env::var("JWT").ok();
+
+        if let Some(token) = token {
+            println!("Intialising client with token");
+            OSClient::new(api_key, token)
+                .await
+                .expect("error while loading client")
+        } else {
+            println!("Intialising client with login");
+            OSClient::new_with_login(api_key, username, password)
+                .await
+                .expect("error while loading client")
+        }
+    }
+
+    async fn new_with_login(
+        api_key: String,
+        username: String,
+        password: String,
+    ) -> Result<Self, NtitledError> {
+        let mut headers = HeaderMap::new();
+
+        headers.append("Api-Key", api_key.parse().expect("invalid api_key"));
+
+        let client = reqwest::ClientBuilder::new()
+            .default_headers(headers)
+            .user_agent("Ntitled v1.0")
+            .build()
+            .expect("invalid client configuration");
+
+        let login = Login {
+            username: &username,
+            password: &password,
+        };
+
+        let req = client
+            .post(format!("{BASE_URL}/login"))
+            .json(&login)
+            .build()?;
+
+        let res: LoginResponse = client.execute(req).await?.json().await?;
+
+        let Some((_, token)) = res.token.split_once(' ') else {
+            return Err(NtitledError::Message(format!(
+                "received invalid token: {}",
+                res.token
+            )));
+        };
+
+        #[cfg(feature = "debug")]
+        {
+            dbg!(&token);
+            use std::fmt::Write;
+            let env_file =
+                std::fs::read_to_string(".env").expect("env file required in debug mode");
+
+            let mut new_env = String::new();
+
+            let mut jwt_written = false;
+            for line in env_file.lines() {
+                if line.starts_with("JWT") {
+                    writeln!(new_env, "JWT = \"{token}\"").unwrap();
+                    jwt_written = true;
+                } else {
+                    writeln!(new_env, "{line}").unwrap();
+                }
+            }
+
+            if !jwt_written {
+                writeln!(new_env, "JWT = \"{token}\"").unwrap();
+            }
+
+            println!("New env:\n{new_env}");
+
+            std::fs::write(".env", new_env).expect("error while writing new env");
+        }
+
+        Ok(Self {
+            client,
+            token: token.to_string(),
+        })
+    }
+
+    async fn new(api_key: String, token: String) -> Result<Self, NtitledError> {
+        let mut headers = HeaderMap::new();
+
+        headers.append("Api-Key", api_key.parse().expect("invalid api_key"));
+        headers.append(
+            "Authorization",
+            format!("Bearer {token}")
+                .parse()
+                .expect("invalid bearer token"),
+        );
+
+        let client = reqwest::ClientBuilder::new()
+            .default_headers(headers)
+            .user_agent("Ntitled v1.0")
+            .build()
+            .expect("invalid client configuration");
+
+        Ok(Self { client, token })
+    }
+
+    pub async fn search(
+        &self,
+        title: &str,
+        hash: Option<&str>,
+    ) -> Result<SearchResponse, NtitledError> {
+        let mut req = self
+            .client
+            .get(format!("{BASE_URL}/subtitles"))
+            .query(&[("query", title)]);
+
+        if let Some(hash) = hash {
+            req = req.query(&[("moviehash", hash)])
+        }
+
+        let req = req.build()?;
+
+        let res: Value = self.client.execute(req).await?.json().await?;
+
+        #[cfg(feature = "debug")]
+        {
+            std::fs::write("response.json", res.to_string()).unwrap();
+        }
+
+        Ok(serde_json::from_value(res)?)
+    }
+
+    pub async fn download_subtitles(
+        &self,
+        file_id: usize,
+        full_path: &str,
+    ) -> Result<(), NtitledError> {
+        let req = self
+            .client
+            .post(format!("{BASE_URL}/download"))
+            .body(json!({"file_id": file_id}).to_string())
+            .header("content-type", "application/json")
+            .header("accept", "application/json")
+            .bearer_auth(&self.token)
+            .build()?;
+
+        let res: Value = self.client.execute(req).await?.json().await?;
+
+        #[cfg(feature = "debug")]
+        dbg!(&res);
+
+        let res = serde_json::from_value(res)?;
+
+        let response = match res {
+            DownloadResponse::Error { message } => {
+                println!("Error while downloading subtitles: {message}");
+                return Err(NtitledError::Message(message));
+            }
+            DownloadResponse::Success(response) => response,
+        };
+
+        let req = self.client.get(response.link).build()?;
+        let res = self.client.execute(req).await?.text().await?;
+
+        info!("Writing subtitles to {full_path}");
+        std::fs::write(full_path, res)?;
+
+        Ok(())
+    }
+
+    pub async fn guess_it(&self, name: &str) -> Result<GuessIt, NtitledError> {
+        let req = self
+            .client
+            .get(format!("{BASE_URL}/utilities/guessit"))
+            .query(&[("filename", name)]);
+
+        let req = req.build().expect("invalid request");
+
+        let res: Value = self.client.execute(req).await?.json().await?;
+
+        #[cfg(feature = "debug")]
+        {
+            dbg!(&res);
+        }
+
+        Ok(serde_json::from_value(res)?)
+    }
+}

+ 177 - 0
src/ost/data.rs

@@ -0,0 +1,177 @@
+use chrono::{DateTime, Utc};
+use serde::{Deserialize, Serialize};
+
+// SUBTITLES
+
+#[derive(Debug, Deserialize, Serialize)]
+pub struct SearchResponse {
+    pub total_pages: usize,
+    pub total_count: usize,
+    pub per_page: usize,
+    pub page: usize,
+    pub data: Vec<Subtitle>,
+}
+
+/// https://opensubtitles.stoplight.io/docs/opensubtitles-api/573f76acc1493-subtitle
+#[derive(Debug, Deserialize, Serialize)]
+pub struct Subtitle {
+    pub id: String,
+    pub r#type: String,
+    pub attributes: SubtitleAttrs,
+}
+
+/// https://opensubtitles.stoplight.io/docs/opensubtitles-api/573f76acc1493-subtitle
+#[derive(Debug, Deserialize, Serialize)]
+pub struct SubtitleAttrs {
+    pub subtitle_id: String,
+    pub language: Option<String>,
+    pub download_count: usize,
+    pub new_download_count: usize,
+    pub hearing_impaired: bool,
+    pub hd: bool,
+    pub fps: f32,
+    pub votes: usize,
+    pub ratings: f32,
+    pub from_trusted: bool,
+    pub foreign_parts_only: bool,
+    pub ai_translated: bool,
+    pub release: String,
+    pub comments: Option<String>,
+    pub legacy_subtitle_id: usize,
+    pub legacy_uploader_id: usize,
+    pub uploader: Uploader,
+    pub feature_details: Feature,
+    pub url: String,
+    pub moviehash_match: Option<bool>,
+    pub points: Option<usize>,
+    pub files: Vec<File>,
+}
+
+#[derive(Debug, Deserialize, Serialize)]
+pub struct File {
+    pub file_id: usize,
+    pub cd_number: usize,
+    pub file_name: String,
+}
+
+#[derive(Debug, Deserialize, Serialize)]
+pub struct Uploader {
+    uploader_id: Option<usize>,
+    name: String,
+    rank: String,
+}
+
+#[derive(Debug, Deserialize, Serialize)]
+#[serde(untagged)]
+pub enum Feature {
+    TvShow(TvShow),
+    Episode(Episode),
+    Movie(Movie),
+}
+
+#[derive(Debug, Deserialize, Serialize)]
+pub struct TvShow {
+    pub title: String,
+    pub original_title: String,
+    pub year: usize,
+    pub imdb_id: usize,
+    pub tmdb_id: usize,
+    pub feature_id: usize,
+    pub url: String,
+    pub img_url: String,
+    pub subtitles_count: usize,
+}
+
+#[derive(Debug, Deserialize, Serialize)]
+pub struct Episode {
+    pub feature_id: usize,
+    pub feature_type: String,
+    pub year: usize,
+    pub title: String,
+    pub movie_name: String,
+    pub imdb_id: usize,
+    pub season_number: Option<usize>,
+    pub episode_number: Option<usize>,
+    pub parent_imdb_id: Option<usize>,
+    pub parent_title: Option<String>,
+    pub parent_feature_id: Option<usize>,
+}
+
+#[derive(Debug, Deserialize, Serialize)]
+pub struct Movie {
+    pub title: String,
+    pub original_title: String,
+    pub year: usize,
+    pub subtitles_count: usize,
+    pub seasons_count: usize,
+    pub parent_title: String,
+    pub season_number: Option<usize>,
+    pub episode_number: Option<usize>,
+    pub imdb_id: usize,
+    pub tmdb_id: usize,
+    pub feature_id: String,
+    pub feature_type: String,
+    pub url: String,
+    pub img_url: String,
+}
+
+// AUTH
+
+#[derive(Debug, Deserialize)]
+pub struct LoginResponse {
+    pub user: User,
+    pub token: String,
+    pub status: usize,
+}
+
+#[derive(Debug, Deserialize)]
+pub struct User {
+    pub allowed_downloads: usize,
+    pub allowed_translations: usize,
+    pub level: String,
+    pub user_id: usize,
+    pub ext_installed: bool,
+    pub vip: bool,
+}
+
+#[derive(Debug, Serialize)]
+pub struct Login<'a> {
+    pub username: &'a str,
+    pub password: &'a str,
+}
+
+// GUESS IT
+
+#[derive(Debug, Deserialize)]
+pub struct GuessIt {
+    pub title: String,
+    pub audio_codec: Option<String>,
+    pub video_codec: Option<String>,
+    pub release_group: Option<String>,
+    pub r#type: Option<String>,
+
+    // Available only for TV shows
+    pub episode_title: Option<String>,
+    pub season: Option<usize>,
+    pub episode: Option<usize>,
+}
+
+// DOWNLOAD
+
+#[derive(Debug, Deserialize)]
+#[serde(untagged)]
+pub enum DownloadResponse {
+    Success(SubtitleFileInfo),
+    Error { message: String },
+}
+
+#[derive(Debug, Deserialize)]
+pub struct SubtitleFileInfo {
+    pub link: String,
+    pub file_name: String,
+    pub requests: usize,
+    pub remaining: usize,
+    pub message: String,
+    pub reset_time: String,
+    pub reset_time_utc: DateTime<Utc>,
+}

+ 55 - 0
src/ost/hash.rs

@@ -0,0 +1,55 @@
+//! https://trac.opensubtitles.org/projects/opensubtitles/wiki/HashSourceCodes#RUST
+
+use std::{
+    fs::File,
+    io::{self, BufReader, Read, Seek, SeekFrom},
+};
+
+const HASH_BLK_SIZE: u64 = 65536;
+
+pub fn compute_hash(fname: &str) -> std::io::Result<String> {
+    let fsize = std::fs::metadata(fname).unwrap().len();
+    if fsize > HASH_BLK_SIZE {
+        let file = File::open(fname).unwrap();
+        create_hash(file, fsize)
+    } else {
+        println!("Error");
+        Err(io::Error::new(
+            io::ErrorKind::InvalidInput,
+            "File is smaller than block size, cannot determine hash",
+        ))
+    }
+}
+
+fn create_hash(file: File, fsize: u64) -> Result<String, std::io::Error> {
+    let mut buf = [0u8; 8];
+    let mut word: u64;
+
+    let mut hash_val: u64 = fsize; // seed hash with file size
+
+    let iterations = HASH_BLK_SIZE / 8;
+
+    let mut reader = BufReader::with_capacity(HASH_BLK_SIZE as usize, file);
+
+    for _ in 0..iterations {
+        reader.read_exact(&mut buf)?;
+        unsafe {
+            word = std::mem::transmute(buf);
+        };
+        hash_val = hash_val.wrapping_add(word);
+    }
+
+    reader.seek(SeekFrom::Start(fsize - HASH_BLK_SIZE))?;
+
+    for _ in 0..iterations {
+        reader.read_exact(&mut buf)?;
+        unsafe {
+            word = std::mem::transmute(buf);
+        };
+        hash_val = hash_val.wrapping_add(word);
+    }
+
+    let hash_string = format!("{:01$x}", hash_val, 16);
+
+    Ok(hash_string)
+}

+ 3 - 0
src/ost/mod.rs

@@ -0,0 +1,3 @@
+pub mod client;
+pub mod data;
+pub mod hash;

+ 363 - 0
src/routes.rs

@@ -0,0 +1,363 @@
+use crate::{
+    error::NtitledError,
+    ost::{
+        data::{Episode, File, GuessIt, Movie, Subtitle, SubtitleAttrs, TvShow},
+        hash::compute_hash,
+    },
+    scan_dir_contents, Directory, FileType, State,
+};
+use axum::{
+    extract::Query,
+    http::{header, StatusCode},
+    response::IntoResponse,
+};
+use minijinja::context;
+use rand::{distributions, Rng};
+use serde::Deserialize;
+use std::{ffi::OsStr, fmt::Write, path::Path};
+use tracing::info;
+
+pub async fn get_directory(
+    state: axum::extract::State<State>,
+    req: axum::extract::Request,
+) -> Result<impl IntoResponse, NtitledError> {
+    // Strip /dir/
+    let path = &req.uri().path()[5..];
+    let path = if path == "root" {
+        state.base_path.clone()
+    } else {
+        let path = urlencoding::decode(path)?;
+        format!("{}/{path}", state.base_path)
+    };
+
+    info!("Listing {path}");
+
+    let files = scan_dir_contents(path)?;
+
+    let response = files.into_iter().fold(String::new(), |mut acc, el| {
+        match el {
+            FileType::Directory(Directory { name, path }) => {
+                let Some((_, path)) = path.split_once(&state.base_path) else {
+                    return acc;
+                };
+
+                let path_enc = urlencoding::encode(&path[1..]);
+                // Used by frontend
+                let ty = "dir";
+
+                write!(
+                    acc,
+                    r#"
+            <a href="/dir/{path_enc}" class="file-entry {ty}">
+                <h2>{name} &#128193;</h2>
+            </a>
+            "#
+                )
+                .unwrap();
+            }
+            FileType::File(crate::File {
+                ref name,
+                path,
+                has_subs,
+            }) => {
+                let Some((_, path)) = path.split_once(&state.base_path) else {
+                    return acc;
+                };
+
+                let name = if let Some((name, _)) = name.rsplit_once('.') {
+                    name
+                } else {
+                    name
+                };
+
+                // Used by frontend
+                let ty = "file";
+
+                let get = format!(
+                    "hx-get=/subtitles?name={}&path={}",
+                    urlencoding::encode(name),
+                    urlencoding::encode(&path[1..])
+                );
+
+                let id = gen_el_id();
+                let guess_id = gen_el_id();
+
+                let has_subs = if has_subs { " &#10004;" } else { "" };
+
+                let template = state.env.get_template("file_container").unwrap();
+                let template = template
+                    .render(context! {
+                        ty => ty,
+                        id => id,
+                        name => name,
+                        has_subs => has_subs,
+                        guess_id => guess_id,
+                        get => get
+                    })
+                    .unwrap();
+
+                write!(acc, "{template}").unwrap();
+            }
+        };
+
+        acc
+    });
+
+    let template = state.env.get_template("index").unwrap();
+    let template = template.render(context! {divs => response}).unwrap();
+
+    Ok((
+        StatusCode::OK,
+        [(header::CONTENT_TYPE, "text/html")],
+        template,
+    ))
+}
+
+#[derive(Debug, Deserialize)]
+pub struct SubtitleSearch {
+    /// Used as the query.
+    name: String,
+
+    /// Used for the file hash.
+    path: String,
+}
+
+pub async fn search_subtitles(
+    state: axum::extract::State<State>,
+    query: axum::extract::Query<SubtitleSearch>,
+) -> Result<String, NtitledError> {
+    let path = format!("{}/{}", state.base_path, query.path);
+    let hash = compute_hash(&path)?;
+
+    let search = state.client.search(&query.name, Some(&hash)).await?;
+
+    let response = search.data.into_iter().fold(String::new(), |mut acc, el| {
+
+        let hashmatch = el.attributes.moviehash_match
+            .is_some_and(|is| is)
+            .then_some("🎯")
+            .unwrap_or_default();
+
+        let response = SubtitleResponse::from(el);
+
+        let SubtitleResponse {
+            title,
+            original_title,
+            imdb_id,
+            year,
+            download_count,
+            parent_title,
+            episode_number,
+            season_number,
+            files
+        } = response;
+
+        let original_title = original_title
+            .map(|title| format!("({title}) "))
+            .unwrap_or_default();
+
+        let parent_title = parent_title.map(|title| format!("{title} -")).unwrap_or_default();
+
+        let episode_id = match (episode_number, season_number) {
+            (Some(ep), Some(season)) => format!("S{season}E{ep}"),
+            (_, _) => String::new(),
+        };
+
+        let mut files = files.into_iter().fold(String::from("<div class=\"sub-files\">"),|mut acc, file| {
+            let File { file_id, ..} = file;
+            let path = urlencoding::encode(&path);
+            write!(acc, r#"
+                File ID: {file_id}
+                <button hx-target="closest div" hx-swap="innerHTML" hx-get="/download?file_id={file_id}&full_path={path}">Download</button>"#).unwrap();
+            acc
+        });
+
+        write!(files, "</div>").unwrap();
+
+        write!(
+            acc,
+            r##"
+            <div class="sub-container">
+
+                <div class="sub-container-title">
+                    <h3>{parent_title} {title} {original_title}{episode_id} {hashmatch}</h3>
+                </div>
+
+                <div class="sub-container-info">
+                    <p>IMDB: {imdb_id}, Year: {year}, Downloads: {download_count}</p>
+                    {files}
+                </div>
+
+            </div>
+            "##
+        )
+        .unwrap();
+
+        acc
+    });
+
+    Ok(response)
+}
+#[derive(Debug, Deserialize)]
+pub struct DownloadQuery {
+    file_id: usize,
+    full_path: String,
+}
+
+pub async fn download_subtitles(
+    state: axum::extract::State<State>,
+    query: axum::extract::Query<DownloadQuery>,
+) -> Result<String, NtitledError> {
+    let file_id = query.file_id;
+
+    let full_path = urlencoding::decode(&query.full_path)?.to_string();
+    let file = full_path.split('/').last();
+    if let Some(file) = file {
+        info!("Downloading subtitles for {file}");
+    }
+
+    let ext = Path::new(&full_path).extension().and_then(OsStr::to_str);
+
+    let path = ext
+        .map(|ext| query.full_path.replace(&format!(".{ext}"), ".srt"))
+        .unwrap_or(format!("{}.srt", full_path));
+
+    state.client.download_subtitles(file_id, &path).await?;
+    Ok(String::from("Successfully downloaded subtitles"))
+}
+
+#[derive(Debug, Deserialize)]
+pub struct GuessRequest {
+    filename: String,
+}
+
+pub async fn guess_it(
+    state: axum::extract::State<State>,
+    query: Query<GuessRequest>,
+) -> Result<String, NtitledError> {
+    let GuessIt {
+        title,
+        audio_codec,
+        video_codec,
+        release_group,
+        r#type,
+        episode,
+        episode_title,
+        season,
+    } = state.client.guess_it(&query.filename).await?;
+
+    let mut response = String::new();
+
+    write!(
+        response,
+        r##"
+        <h5>Title: {title}</h5>
+        <p>Type: {:?}, S: {season:?}, E: {episode:?} ({episode_title:?}), A: {audio_codec:?}, V: {video_codec:?}, {release_group:?}</p>
+    "##,
+    r#type
+    )
+    .unwrap();
+
+    Ok(response)
+}
+
+#[derive(Debug, Deserialize)]
+pub struct SubtitleResponse {
+    title: String,
+    original_title: Option<String>,
+    imdb_id: usize,
+    year: usize,
+    download_count: usize,
+    parent_title: Option<String>,
+    episode_number: Option<usize>,
+    season_number: Option<usize>,
+    files: Vec<File>,
+}
+
+impl From<Subtitle> for SubtitleResponse {
+    fn from(value: Subtitle) -> Self {
+        let SubtitleAttrs {
+            feature_details,
+            download_count,
+            files,
+            ..
+        } = value.attributes;
+
+        match feature_details {
+            crate::ost::data::Feature::TvShow(item) => {
+                let TvShow {
+                    title,
+                    original_title,
+                    year,
+                    imdb_id,
+                    ..
+                } = item;
+
+                Self {
+                    title,
+                    original_title: Some(original_title),
+                    imdb_id,
+                    year,
+                    download_count,
+                    parent_title: None,
+                    episode_number: None,
+                    season_number: None,
+                    files,
+                }
+            }
+            crate::ost::data::Feature::Episode(item) => {
+                let Episode {
+                    year,
+                    title,
+                    imdb_id,
+                    parent_title,
+                    episode_number,
+                    season_number,
+                    ..
+                } = item;
+                Self {
+                    title,
+                    original_title: None,
+                    imdb_id,
+                    year,
+                    download_count,
+                    parent_title,
+                    episode_number,
+                    season_number,
+                    files,
+                }
+            }
+            crate::ost::data::Feature::Movie(item) => {
+                let Movie {
+                    title,
+                    original_title,
+                    year,
+                    imdb_id,
+                    parent_title,
+                    episode_number,
+                    season_number,
+                    ..
+                } = item;
+                Self {
+                    title,
+                    original_title: Some(original_title),
+                    imdb_id,
+                    year,
+                    download_count,
+                    parent_title: Some(parent_title),
+                    episode_number,
+                    season_number,
+                    files,
+                }
+            }
+        }
+    }
+}
+
+fn gen_el_id() -> String {
+    rand::thread_rng()
+        .sample_iter(distributions::Alphanumeric)
+        .take(16)
+        .map(char::from)
+        .collect::<String>()
+}

Неке датотеке нису приказане због велике количине промена