├── .github └── workflows │ └── build-release.yml ├── .gitignore ├── .vscode └── settings.json ├── Cargo.lock ├── Cargo.toml ├── LICENSE ├── readme.md └── src ├── arguments.rs ├── arr ├── mod.rs ├── radarr │ ├── api.rs │ ├── mod.rs │ └── responses.rs └── sonarr │ ├── api.rs │ ├── mod.rs │ └── responses.rs ├── config.rs ├── main.rs ├── media_item.rs ├── overseerr ├── api.rs ├── mod.rs └── responses.rs ├── plex ├── api.rs ├── mod.rs └── responses.rs ├── shared.rs ├── tautulli ├── api.rs ├── mod.rs └── responses.rs └── utils.rs /.github/workflows/build-release.yml: -------------------------------------------------------------------------------- 1 | name: release-binaries 2 | on: 3 | release: 4 | types: [published] 5 | env: 6 | BINARY_NAME: media-cleaner 7 | permissions: 8 | contents: write 9 | jobs: 10 | linux-x86_64: 11 | runs-on: ubuntu-latest 12 | steps: 13 | - uses: actions/checkout@v2 14 | 15 | - uses: actions-rs/toolchain@v1 16 | with: 17 | profile: minimal 18 | toolchain: stable 19 | default: true 20 | 21 | - name: Build binary 22 | uses: actions-rs/cargo@v1 23 | with: 24 | command: build 25 | args: --release --target x86_64-unknown-linux-musl 26 | use-cross: true 27 | 28 | - name: Optimize and package binary 29 | run: | 30 | cd target/x86_64-unknown-linux-musl/release 31 | strip ${{ env.BINARY_NAME }} 32 | chmod +x ${{ env.BINARY_NAME }} 33 | tar -c ${{ env.BINARY_NAME }} | gzip > ${{ env.BINARY_NAME }}-${{ github.ref_name }}-linux-x86_64.tar.gz 34 | 35 | - name: Release 36 | uses: softprops/action-gh-release@v1 37 | with: 38 | files: target/x86_64-unknown-linux-musl/release/${{ env.BINARY_NAME }}-${{ github.ref_name }}-linux-x86_64.tar.gz 39 | 40 | linux-armv7: 41 | runs-on: ubuntu-latest 42 | steps: 43 | - uses: actions/checkout@v2 44 | 45 | - uses: actions-rs/toolchain@v1 46 | with: 47 | profile: minimal 48 | toolchain: stable 49 | default: true 50 | 51 | - name: Build binary 52 | uses: actions-rs/cargo@v1 53 | with: 54 | command: build 55 | args: --release --target armv7-unknown-linux-gnueabihf 56 | use-cross: true 57 | 58 | - name: Optimize and package binary 59 | run: | 60 | cd target/armv7-unknown-linux-gnueabihf/release 61 | chmod +x ${{ env.BINARY_NAME }} 62 | tar -c ${{ env.BINARY_NAME }} | gzip > ${{ env.BINARY_NAME }}-${{ github.ref_name }}-linux-armv7.tar.gz 63 | 64 | - name: Release 65 | uses: softprops/action-gh-release@v1 66 | with: 67 | files: target/armv7-unknown-linux-gnueabihf/release/${{ env.BINARY_NAME }}-${{ github.ref_name }}-linux-armv7.tar.gz 68 | 69 | windows-x86_64: 70 | runs-on: windows-latest 71 | steps: 72 | - uses: actions/checkout@v2 73 | 74 | - uses: actions-rs/toolchain@v1 75 | with: 76 | profile: minimal 77 | toolchain: stable 78 | default: true 79 | 80 | - name: Build binary 81 | uses: actions-rs/cargo@v1 82 | with: 83 | command: build 84 | args: --release 85 | use-cross: true 86 | 87 | - run: Compress-Archive -Path target/release/${{ env.BINARY_NAME }}.exe -Destination ${{ env.BINARY_NAME }}-${{ github.ref_name }}-windows-x86_64.zip 88 | 89 | - name: Release 90 | uses: softprops/action-gh-release@v1 91 | with: 92 | files: ${{ env.BINARY_NAME }}-${{ github.ref_name }}-windows-x86_64.zip 93 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | /target 2 | .env 3 | config.yaml 4 | -------------------------------------------------------------------------------- /.vscode/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "cSpell.words": [ 3 | "overseerr", 4 | "Radarr", 5 | "Sonarr", 6 | "tautulli", 7 | "tmdb", 8 | "tvdb" 9 | ] 10 | } 11 | -------------------------------------------------------------------------------- /Cargo.lock: -------------------------------------------------------------------------------- 1 | # This file is automatically @generated by Cargo. 2 | # It is not intended for manual editing. 3 | version = 3 4 | 5 | [[package]] 6 | name = "addr2line" 7 | version = "0.19.0" 8 | source = "registry+https://github.com/rust-lang/crates.io-index" 9 | checksum = "a76fd60b23679b7d19bd066031410fb7e458ccc5e958eb5c325888ce4baedc97" 10 | dependencies = [ 11 | "gimli", 12 | ] 13 | 14 | [[package]] 15 | name = "adler" 16 | version = "1.0.2" 17 | source = "registry+https://github.com/rust-lang/crates.io-index" 18 | checksum = "f26201604c87b1e01bd3d98f8d5d9a8fcbb815e8cedb41ffccbeb4bf593a35fe" 19 | 20 | [[package]] 21 | name = "android_system_properties" 22 | version = "0.1.5" 23 | source = "registry+https://github.com/rust-lang/crates.io-index" 24 | checksum = "819e7219dbd41043ac279b19830f2efc897156490d7fd6ea916720117ee66311" 25 | dependencies = [ 26 | "libc", 27 | ] 28 | 29 | [[package]] 30 | name = "autocfg" 31 | version = "1.1.0" 32 | source = "registry+https://github.com/rust-lang/crates.io-index" 33 | checksum = "d468802bab17cbc0cc575e9b053f41e72aa36bfa6b7f55e3529ffa43161b97fa" 34 | 35 | [[package]] 36 | name = "backtrace" 37 | version = "0.3.67" 38 | source = "registry+https://github.com/rust-lang/crates.io-index" 39 | checksum = "233d376d6d185f2a3093e58f283f60f880315b6c60075b01f36b3b85154564ca" 40 | dependencies = [ 41 | "addr2line", 42 | "cc", 43 | "cfg-if", 44 | "libc", 45 | "miniz_oxide", 46 | "object", 47 | "rustc-demangle", 48 | ] 49 | 50 | [[package]] 51 | name = "base64" 52 | version = "0.21.0" 53 | source = "registry+https://github.com/rust-lang/crates.io-index" 54 | checksum = "a4a4ddaa51a5bc52a6948f74c06d20aaaddb71924eab79b8c97a8c556e942d6a" 55 | 56 | [[package]] 57 | name = "bitflags" 58 | version = "1.3.2" 59 | source = "registry+https://github.com/rust-lang/crates.io-index" 60 | checksum = "bef38d45163c2f1dde094a7dfd33ccf595c92905c8f8f4fdc18d06fb1037718a" 61 | 62 | [[package]] 63 | name = "bumpalo" 64 | version = "3.12.0" 65 | source = "registry+https://github.com/rust-lang/crates.io-index" 66 | checksum = "0d261e256854913907f67ed06efbc3338dfe6179796deefc1ff763fc1aee5535" 67 | 68 | [[package]] 69 | name = "bytes" 70 | version = "1.4.0" 71 | source = "registry+https://github.com/rust-lang/crates.io-index" 72 | checksum = "89b2fd2a0dcf38d7971e2194b6b6eebab45ae01067456a7fd93d5547a61b70be" 73 | 74 | [[package]] 75 | name = "cc" 76 | version = "1.0.79" 77 | source = "registry+https://github.com/rust-lang/crates.io-index" 78 | checksum = "50d30906286121d95be3d479533b458f87493b30a4b5f79a607db8f5d11aa91f" 79 | 80 | [[package]] 81 | name = "cfg-if" 82 | version = "1.0.0" 83 | source = "registry+https://github.com/rust-lang/crates.io-index" 84 | checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd" 85 | 86 | [[package]] 87 | name = "chrono" 88 | version = "0.4.23" 89 | source = "registry+https://github.com/rust-lang/crates.io-index" 90 | checksum = "16b0a3d9ed01224b22057780a37bb8c5dbfe1be8ba48678e7bf57ec4b385411f" 91 | dependencies = [ 92 | "iana-time-zone", 93 | "js-sys", 94 | "num-integer", 95 | "num-traits", 96 | "time", 97 | "wasm-bindgen", 98 | "winapi", 99 | ] 100 | 101 | [[package]] 102 | name = "codespan-reporting" 103 | version = "0.11.1" 104 | source = "registry+https://github.com/rust-lang/crates.io-index" 105 | checksum = "3538270d33cc669650c4b093848450d380def10c331d38c768e34cac80576e6e" 106 | dependencies = [ 107 | "termcolor", 108 | "unicode-width", 109 | ] 110 | 111 | [[package]] 112 | name = "color-eyre" 113 | version = "0.6.2" 114 | source = "registry+https://github.com/rust-lang/crates.io-index" 115 | checksum = "5a667583cca8c4f8436db8de46ea8233c42a7d9ae424a82d338f2e4675229204" 116 | dependencies = [ 117 | "backtrace", 118 | "color-spantrace", 119 | "eyre", 120 | "indenter", 121 | "once_cell", 122 | "owo-colors", 123 | "tracing-error", 124 | ] 125 | 126 | [[package]] 127 | name = "color-spantrace" 128 | version = "0.2.0" 129 | source = "registry+https://github.com/rust-lang/crates.io-index" 130 | checksum = "1ba75b3d9449ecdccb27ecbc479fdc0b87fa2dd43d2f8298f9bf0e59aacc8dce" 131 | dependencies = [ 132 | "once_cell", 133 | "owo-colors", 134 | "tracing-core", 135 | "tracing-error", 136 | ] 137 | 138 | [[package]] 139 | name = "console" 140 | version = "0.15.5" 141 | source = "registry+https://github.com/rust-lang/crates.io-index" 142 | checksum = "c3d79fbe8970a77e3e34151cc13d3b3e248aa0faaecb9f6091fa07ebefe5ad60" 143 | dependencies = [ 144 | "encode_unicode", 145 | "lazy_static", 146 | "libc", 147 | "unicode-width", 148 | "windows-sys 0.42.0", 149 | ] 150 | 151 | [[package]] 152 | name = "core-foundation" 153 | version = "0.9.3" 154 | source = "registry+https://github.com/rust-lang/crates.io-index" 155 | checksum = "194a7a9e6de53fa55116934067c844d9d749312f75c6f6d0980e8c252f8c2146" 156 | dependencies = [ 157 | "core-foundation-sys", 158 | "libc", 159 | ] 160 | 161 | [[package]] 162 | name = "core-foundation-sys" 163 | version = "0.8.3" 164 | source = "registry+https://github.com/rust-lang/crates.io-index" 165 | checksum = "5827cebf4670468b8772dd191856768aedcb1b0278a04f989f7766351917b9dc" 166 | 167 | [[package]] 168 | name = "cxx" 169 | version = "1.0.91" 170 | source = "registry+https://github.com/rust-lang/crates.io-index" 171 | checksum = "86d3488e7665a7a483b57e25bdd90d0aeb2bc7608c8d0346acf2ad3f1caf1d62" 172 | dependencies = [ 173 | "cc", 174 | "cxxbridge-flags", 175 | "cxxbridge-macro", 176 | "link-cplusplus", 177 | ] 178 | 179 | [[package]] 180 | name = "cxx-build" 181 | version = "1.0.91" 182 | source = "registry+https://github.com/rust-lang/crates.io-index" 183 | checksum = "48fcaf066a053a41a81dfb14d57d99738b767febb8b735c3016e469fac5da690" 184 | dependencies = [ 185 | "cc", 186 | "codespan-reporting", 187 | "once_cell", 188 | "proc-macro2", 189 | "quote", 190 | "scratch", 191 | "syn", 192 | ] 193 | 194 | [[package]] 195 | name = "cxxbridge-flags" 196 | version = "1.0.91" 197 | source = "registry+https://github.com/rust-lang/crates.io-index" 198 | checksum = "a2ef98b8b717a829ca5603af80e1f9e2e48013ab227b68ef37872ef84ee479bf" 199 | 200 | [[package]] 201 | name = "cxxbridge-macro" 202 | version = "1.0.91" 203 | source = "registry+https://github.com/rust-lang/crates.io-index" 204 | checksum = "086c685979a698443656e5cf7856c95c642295a38599f12fb1ff76fb28d19892" 205 | dependencies = [ 206 | "proc-macro2", 207 | "quote", 208 | "syn", 209 | ] 210 | 211 | [[package]] 212 | name = "dialoguer" 213 | version = "0.10.3" 214 | source = "registry+https://github.com/rust-lang/crates.io-index" 215 | checksum = "af3c796f3b0b408d9fd581611b47fa850821fcb84aa640b83a3c1a5be2d691f2" 216 | dependencies = [ 217 | "console", 218 | "shell-words", 219 | "tempfile", 220 | "zeroize", 221 | ] 222 | 223 | [[package]] 224 | name = "either" 225 | version = "1.8.1" 226 | source = "registry+https://github.com/rust-lang/crates.io-index" 227 | checksum = "7fcaabb2fef8c910e7f4c7ce9f67a1283a1715879a7c230ca9d6d1ae31f16d91" 228 | 229 | [[package]] 230 | name = "encode_unicode" 231 | version = "0.3.6" 232 | source = "registry+https://github.com/rust-lang/crates.io-index" 233 | checksum = "a357d28ed41a50f9c765dbfe56cbc04a64e53e5fc58ba79fbc34c10ef3df831f" 234 | 235 | [[package]] 236 | name = "encoding_rs" 237 | version = "0.8.32" 238 | source = "registry+https://github.com/rust-lang/crates.io-index" 239 | checksum = "071a31f4ee85403370b58aca746f01041ede6f0da2730960ad001edc2b71b394" 240 | dependencies = [ 241 | "cfg-if", 242 | ] 243 | 244 | [[package]] 245 | name = "errno" 246 | version = "0.2.8" 247 | source = "registry+https://github.com/rust-lang/crates.io-index" 248 | checksum = "f639046355ee4f37944e44f60642c6f3a7efa3cf6b78c78a0d989a8ce6c396a1" 249 | dependencies = [ 250 | "errno-dragonfly", 251 | "libc", 252 | "winapi", 253 | ] 254 | 255 | [[package]] 256 | name = "errno-dragonfly" 257 | version = "0.1.2" 258 | source = "registry+https://github.com/rust-lang/crates.io-index" 259 | checksum = "aa68f1b12764fab894d2755d2518754e71b4fd80ecfb822714a1206c2aab39bf" 260 | dependencies = [ 261 | "cc", 262 | "libc", 263 | ] 264 | 265 | [[package]] 266 | name = "eyre" 267 | version = "0.6.8" 268 | source = "registry+https://github.com/rust-lang/crates.io-index" 269 | checksum = "4c2b6b5a29c02cdc822728b7d7b8ae1bab3e3b05d44522770ddd49722eeac7eb" 270 | dependencies = [ 271 | "indenter", 272 | "once_cell", 273 | ] 274 | 275 | [[package]] 276 | name = "fastrand" 277 | version = "1.9.0" 278 | source = "registry+https://github.com/rust-lang/crates.io-index" 279 | checksum = "e51093e27b0797c359783294ca4f0a911c270184cb10f85783b118614a1501be" 280 | dependencies = [ 281 | "instant", 282 | ] 283 | 284 | [[package]] 285 | name = "fnv" 286 | version = "1.0.7" 287 | source = "registry+https://github.com/rust-lang/crates.io-index" 288 | checksum = "3f9eec918d3f24069decb9af1554cad7c880e2da24a9afd88aca000531ab82c1" 289 | 290 | [[package]] 291 | name = "foreign-types" 292 | version = "0.3.2" 293 | source = "registry+https://github.com/rust-lang/crates.io-index" 294 | checksum = "f6f339eb8adc052cd2ca78910fda869aefa38d22d5cb648e6485e4d3fc06f3b1" 295 | dependencies = [ 296 | "foreign-types-shared", 297 | ] 298 | 299 | [[package]] 300 | name = "foreign-types-shared" 301 | version = "0.1.1" 302 | source = "registry+https://github.com/rust-lang/crates.io-index" 303 | checksum = "00b0228411908ca8685dba7fc2cdd70ec9990a6e753e89b6ac91a84c40fbaf4b" 304 | 305 | [[package]] 306 | name = "form_urlencoded" 307 | version = "1.1.0" 308 | source = "registry+https://github.com/rust-lang/crates.io-index" 309 | checksum = "a9c384f161156f5260c24a097c56119f9be8c798586aecc13afbcbe7b7e26bf8" 310 | dependencies = [ 311 | "percent-encoding", 312 | ] 313 | 314 | [[package]] 315 | name = "futures" 316 | version = "0.3.26" 317 | source = "registry+https://github.com/rust-lang/crates.io-index" 318 | checksum = "13e2792b0ff0340399d58445b88fd9770e3489eff258a4cbc1523418f12abf84" 319 | dependencies = [ 320 | "futures-channel", 321 | "futures-core", 322 | "futures-executor", 323 | "futures-io", 324 | "futures-sink", 325 | "futures-task", 326 | "futures-util", 327 | ] 328 | 329 | [[package]] 330 | name = "futures-channel" 331 | version = "0.3.26" 332 | source = "registry+https://github.com/rust-lang/crates.io-index" 333 | checksum = "2e5317663a9089767a1ec00a487df42e0ca174b61b4483213ac24448e4664df5" 334 | dependencies = [ 335 | "futures-core", 336 | "futures-sink", 337 | ] 338 | 339 | [[package]] 340 | name = "futures-core" 341 | version = "0.3.26" 342 | source = "registry+https://github.com/rust-lang/crates.io-index" 343 | checksum = "ec90ff4d0fe1f57d600049061dc6bb68ed03c7d2fbd697274c41805dcb3f8608" 344 | 345 | [[package]] 346 | name = "futures-executor" 347 | version = "0.3.26" 348 | source = "registry+https://github.com/rust-lang/crates.io-index" 349 | checksum = "e8de0a35a6ab97ec8869e32a2473f4b1324459e14c29275d14b10cb1fd19b50e" 350 | dependencies = [ 351 | "futures-core", 352 | "futures-task", 353 | "futures-util", 354 | ] 355 | 356 | [[package]] 357 | name = "futures-io" 358 | version = "0.3.26" 359 | source = "registry+https://github.com/rust-lang/crates.io-index" 360 | checksum = "bfb8371b6fb2aeb2d280374607aeabfc99d95c72edfe51692e42d3d7f0d08531" 361 | 362 | [[package]] 363 | name = "futures-macro" 364 | version = "0.3.26" 365 | source = "registry+https://github.com/rust-lang/crates.io-index" 366 | checksum = "95a73af87da33b5acf53acfebdc339fe592ecf5357ac7c0a7734ab9d8c876a70" 367 | dependencies = [ 368 | "proc-macro2", 369 | "quote", 370 | "syn", 371 | ] 372 | 373 | [[package]] 374 | name = "futures-sink" 375 | version = "0.3.26" 376 | source = "registry+https://github.com/rust-lang/crates.io-index" 377 | checksum = "f310820bb3e8cfd46c80db4d7fb8353e15dfff853a127158425f31e0be6c8364" 378 | 379 | [[package]] 380 | name = "futures-task" 381 | version = "0.3.26" 382 | source = "registry+https://github.com/rust-lang/crates.io-index" 383 | checksum = "dcf79a1bf610b10f42aea489289c5a2c478a786509693b80cd39c44ccd936366" 384 | 385 | [[package]] 386 | name = "futures-util" 387 | version = "0.3.26" 388 | source = "registry+https://github.com/rust-lang/crates.io-index" 389 | checksum = "9c1d6de3acfef38d2be4b1f543f553131788603495be83da675e180c8d6b7bd1" 390 | dependencies = [ 391 | "futures-channel", 392 | "futures-core", 393 | "futures-io", 394 | "futures-macro", 395 | "futures-sink", 396 | "futures-task", 397 | "memchr", 398 | "pin-project-lite", 399 | "pin-utils", 400 | "slab", 401 | ] 402 | 403 | [[package]] 404 | name = "gimli" 405 | version = "0.27.2" 406 | source = "registry+https://github.com/rust-lang/crates.io-index" 407 | checksum = "ad0a93d233ebf96623465aad4046a8d3aa4da22d4f4beba5388838c8a434bbb4" 408 | 409 | [[package]] 410 | name = "h2" 411 | version = "0.3.15" 412 | source = "registry+https://github.com/rust-lang/crates.io-index" 413 | checksum = "5f9f29bc9dda355256b2916cf526ab02ce0aeaaaf2bad60d65ef3f12f11dd0f4" 414 | dependencies = [ 415 | "bytes", 416 | "fnv", 417 | "futures-core", 418 | "futures-sink", 419 | "futures-util", 420 | "http", 421 | "indexmap", 422 | "slab", 423 | "tokio", 424 | "tokio-util", 425 | "tracing", 426 | ] 427 | 428 | [[package]] 429 | name = "hashbrown" 430 | version = "0.12.3" 431 | source = "registry+https://github.com/rust-lang/crates.io-index" 432 | checksum = "8a9ee70c43aaf417c914396645a0fa852624801b24ebb7ae78fe8272889ac888" 433 | 434 | [[package]] 435 | name = "hermit-abi" 436 | version = "0.2.6" 437 | source = "registry+https://github.com/rust-lang/crates.io-index" 438 | checksum = "ee512640fe35acbfb4bb779db6f0d80704c2cacfa2e39b601ef3e3f47d1ae4c7" 439 | dependencies = [ 440 | "libc", 441 | ] 442 | 443 | [[package]] 444 | name = "http" 445 | version = "0.2.9" 446 | source = "registry+https://github.com/rust-lang/crates.io-index" 447 | checksum = "bd6effc99afb63425aff9b05836f029929e345a6148a14b7ecd5ab67af944482" 448 | dependencies = [ 449 | "bytes", 450 | "fnv", 451 | "itoa", 452 | ] 453 | 454 | [[package]] 455 | name = "http-body" 456 | version = "0.4.5" 457 | source = "registry+https://github.com/rust-lang/crates.io-index" 458 | checksum = "d5f38f16d184e36f2408a55281cd658ecbd3ca05cce6d6510a176eca393e26d1" 459 | dependencies = [ 460 | "bytes", 461 | "http", 462 | "pin-project-lite", 463 | ] 464 | 465 | [[package]] 466 | name = "httparse" 467 | version = "1.8.0" 468 | source = "registry+https://github.com/rust-lang/crates.io-index" 469 | checksum = "d897f394bad6a705d5f4104762e116a75639e470d80901eed05a860a95cb1904" 470 | 471 | [[package]] 472 | name = "httpdate" 473 | version = "1.0.2" 474 | source = "registry+https://github.com/rust-lang/crates.io-index" 475 | checksum = "c4a1e36c821dbe04574f602848a19f742f4fb3c98d40449f11bcad18d6b17421" 476 | 477 | [[package]] 478 | name = "hyper" 479 | version = "0.14.24" 480 | source = "registry+https://github.com/rust-lang/crates.io-index" 481 | checksum = "5e011372fa0b68db8350aa7a248930ecc7839bf46d8485577d69f117a75f164c" 482 | dependencies = [ 483 | "bytes", 484 | "futures-channel", 485 | "futures-core", 486 | "futures-util", 487 | "h2", 488 | "http", 489 | "http-body", 490 | "httparse", 491 | "httpdate", 492 | "itoa", 493 | "pin-project-lite", 494 | "socket2", 495 | "tokio", 496 | "tower-service", 497 | "tracing", 498 | "want", 499 | ] 500 | 501 | [[package]] 502 | name = "hyper-tls" 503 | version = "0.5.0" 504 | source = "registry+https://github.com/rust-lang/crates.io-index" 505 | checksum = "d6183ddfa99b85da61a140bea0efc93fdf56ceaa041b37d553518030827f9905" 506 | dependencies = [ 507 | "bytes", 508 | "hyper", 509 | "native-tls", 510 | "tokio", 511 | "tokio-native-tls", 512 | ] 513 | 514 | [[package]] 515 | name = "iana-time-zone" 516 | version = "0.1.53" 517 | source = "registry+https://github.com/rust-lang/crates.io-index" 518 | checksum = "64c122667b287044802d6ce17ee2ddf13207ed924c712de9a66a5814d5b64765" 519 | dependencies = [ 520 | "android_system_properties", 521 | "core-foundation-sys", 522 | "iana-time-zone-haiku", 523 | "js-sys", 524 | "wasm-bindgen", 525 | "winapi", 526 | ] 527 | 528 | [[package]] 529 | name = "iana-time-zone-haiku" 530 | version = "0.1.1" 531 | source = "registry+https://github.com/rust-lang/crates.io-index" 532 | checksum = "0703ae284fc167426161c2e3f1da3ea71d94b21bedbcc9494e92b28e334e3dca" 533 | dependencies = [ 534 | "cxx", 535 | "cxx-build", 536 | ] 537 | 538 | [[package]] 539 | name = "idna" 540 | version = "0.3.0" 541 | source = "registry+https://github.com/rust-lang/crates.io-index" 542 | checksum = "e14ddfc70884202db2244c223200c204c2bda1bc6e0998d11b5e024d657209e6" 543 | dependencies = [ 544 | "unicode-bidi", 545 | "unicode-normalization", 546 | ] 547 | 548 | [[package]] 549 | name = "indenter" 550 | version = "0.3.3" 551 | source = "registry+https://github.com/rust-lang/crates.io-index" 552 | checksum = "ce23b50ad8242c51a442f3ff322d56b02f08852c77e4c0b4d3fd684abc89c683" 553 | 554 | [[package]] 555 | name = "indexmap" 556 | version = "1.9.2" 557 | source = "registry+https://github.com/rust-lang/crates.io-index" 558 | checksum = "1885e79c1fc4b10f0e172c475f458b7f7b93061064d98c3293e98c5ba0c8b399" 559 | dependencies = [ 560 | "autocfg", 561 | "hashbrown", 562 | ] 563 | 564 | [[package]] 565 | name = "instant" 566 | version = "0.1.12" 567 | source = "registry+https://github.com/rust-lang/crates.io-index" 568 | checksum = "7a5bbe824c507c5da5956355e86a746d82e0e1464f65d862cc5e71da70e94b2c" 569 | dependencies = [ 570 | "cfg-if", 571 | ] 572 | 573 | [[package]] 574 | name = "io-lifetimes" 575 | version = "1.0.5" 576 | source = "registry+https://github.com/rust-lang/crates.io-index" 577 | checksum = "1abeb7a0dd0f8181267ff8adc397075586500b81b28a73e8a0208b00fc170fb3" 578 | dependencies = [ 579 | "libc", 580 | "windows-sys 0.45.0", 581 | ] 582 | 583 | [[package]] 584 | name = "ipnet" 585 | version = "2.7.1" 586 | source = "registry+https://github.com/rust-lang/crates.io-index" 587 | checksum = "30e22bd8629359895450b59ea7a776c850561b96a3b1d31321c1949d9e6c9146" 588 | 589 | [[package]] 590 | name = "itertools" 591 | version = "0.10.5" 592 | source = "registry+https://github.com/rust-lang/crates.io-index" 593 | checksum = "b0fd2260e829bddf4cb6ea802289de2f86d6a7a690192fbe91b3f46e0f2c8473" 594 | dependencies = [ 595 | "either", 596 | ] 597 | 598 | [[package]] 599 | name = "itoa" 600 | version = "1.0.5" 601 | source = "registry+https://github.com/rust-lang/crates.io-index" 602 | checksum = "fad582f4b9e86b6caa621cabeb0963332d92eea04729ab12892c2533951e6440" 603 | 604 | [[package]] 605 | name = "js-sys" 606 | version = "0.3.61" 607 | source = "registry+https://github.com/rust-lang/crates.io-index" 608 | checksum = "445dde2150c55e483f3d8416706b97ec8e8237c307e5b7b4b8dd15e6af2a0730" 609 | dependencies = [ 610 | "wasm-bindgen", 611 | ] 612 | 613 | [[package]] 614 | name = "lazy_static" 615 | version = "1.4.0" 616 | source = "registry+https://github.com/rust-lang/crates.io-index" 617 | checksum = "e2abad23fbc42b3700f2f279844dc832adb2b2eb069b2df918f455c4e18cc646" 618 | 619 | [[package]] 620 | name = "libc" 621 | version = "0.2.139" 622 | source = "registry+https://github.com/rust-lang/crates.io-index" 623 | checksum = "201de327520df007757c1f0adce6e827fe8562fbc28bfd9c15571c66ca1f5f79" 624 | 625 | [[package]] 626 | name = "link-cplusplus" 627 | version = "1.0.8" 628 | source = "registry+https://github.com/rust-lang/crates.io-index" 629 | checksum = "ecd207c9c713c34f95a097a5b029ac2ce6010530c7b49d7fea24d977dede04f5" 630 | dependencies = [ 631 | "cc", 632 | ] 633 | 634 | [[package]] 635 | name = "linux-raw-sys" 636 | version = "0.1.4" 637 | source = "registry+https://github.com/rust-lang/crates.io-index" 638 | checksum = "f051f77a7c8e6957c0696eac88f26b0117e54f52d3fc682ab19397a8812846a4" 639 | 640 | [[package]] 641 | name = "log" 642 | version = "0.4.17" 643 | source = "registry+https://github.com/rust-lang/crates.io-index" 644 | checksum = "abb12e687cfb44aa40f41fc3978ef76448f9b6038cad6aef4259d3c095a2382e" 645 | dependencies = [ 646 | "cfg-if", 647 | ] 648 | 649 | [[package]] 650 | name = "media-cleaner" 651 | version = "1.4.0" 652 | dependencies = [ 653 | "chrono", 654 | "color-eyre", 655 | "dialoguer", 656 | "futures", 657 | "itertools", 658 | "once_cell", 659 | "openssl", 660 | "reqwest", 661 | "serde", 662 | "serde-xml-rs", 663 | "serde_json", 664 | "serde_repr", 665 | "serde_yaml", 666 | "tokio", 667 | ] 668 | 669 | [[package]] 670 | name = "memchr" 671 | version = "2.5.0" 672 | source = "registry+https://github.com/rust-lang/crates.io-index" 673 | checksum = "2dffe52ecf27772e601905b7522cb4ef790d2cc203488bbd0e2fe85fcb74566d" 674 | 675 | [[package]] 676 | name = "mime" 677 | version = "0.3.16" 678 | source = "registry+https://github.com/rust-lang/crates.io-index" 679 | checksum = "2a60c7ce501c71e03a9c9c0d35b861413ae925bd979cc7a4e30d060069aaac8d" 680 | 681 | [[package]] 682 | name = "miniz_oxide" 683 | version = "0.6.2" 684 | source = "registry+https://github.com/rust-lang/crates.io-index" 685 | checksum = "b275950c28b37e794e8c55d88aeb5e139d0ce23fdbbeda68f8d7174abdf9e8fa" 686 | dependencies = [ 687 | "adler", 688 | ] 689 | 690 | [[package]] 691 | name = "mio" 692 | version = "0.8.6" 693 | source = "registry+https://github.com/rust-lang/crates.io-index" 694 | checksum = "5b9d9a46eff5b4ff64b45a9e316a6d1e0bc719ef429cbec4dc630684212bfdf9" 695 | dependencies = [ 696 | "libc", 697 | "log", 698 | "wasi 0.11.0+wasi-snapshot-preview1", 699 | "windows-sys 0.45.0", 700 | ] 701 | 702 | [[package]] 703 | name = "native-tls" 704 | version = "0.2.11" 705 | source = "registry+https://github.com/rust-lang/crates.io-index" 706 | checksum = "07226173c32f2926027b63cce4bcd8076c3552846cbe7925f3aaffeac0a3b92e" 707 | dependencies = [ 708 | "lazy_static", 709 | "libc", 710 | "log", 711 | "openssl", 712 | "openssl-probe", 713 | "openssl-sys", 714 | "schannel", 715 | "security-framework", 716 | "security-framework-sys", 717 | "tempfile", 718 | ] 719 | 720 | [[package]] 721 | name = "num-integer" 722 | version = "0.1.45" 723 | source = "registry+https://github.com/rust-lang/crates.io-index" 724 | checksum = "225d3389fb3509a24c93f5c29eb6bde2586b98d9f016636dff58d7c6f7569cd9" 725 | dependencies = [ 726 | "autocfg", 727 | "num-traits", 728 | ] 729 | 730 | [[package]] 731 | name = "num-traits" 732 | version = "0.2.15" 733 | source = "registry+https://github.com/rust-lang/crates.io-index" 734 | checksum = "578ede34cf02f8924ab9447f50c28075b4d3e5b269972345e7e0372b38c6cdcd" 735 | dependencies = [ 736 | "autocfg", 737 | ] 738 | 739 | [[package]] 740 | name = "num_cpus" 741 | version = "1.15.0" 742 | source = "registry+https://github.com/rust-lang/crates.io-index" 743 | checksum = "0fac9e2da13b5eb447a6ce3d392f23a29d8694bff781bf03a16cd9ac8697593b" 744 | dependencies = [ 745 | "hermit-abi", 746 | "libc", 747 | ] 748 | 749 | [[package]] 750 | name = "object" 751 | version = "0.30.3" 752 | source = "registry+https://github.com/rust-lang/crates.io-index" 753 | checksum = "ea86265d3d3dcb6a27fc51bd29a4bf387fae9d2986b823079d4986af253eb439" 754 | dependencies = [ 755 | "memchr", 756 | ] 757 | 758 | [[package]] 759 | name = "once_cell" 760 | version = "1.17.1" 761 | source = "registry+https://github.com/rust-lang/crates.io-index" 762 | checksum = "b7e5500299e16ebb147ae15a00a942af264cf3688f47923b8fc2cd5858f23ad3" 763 | 764 | [[package]] 765 | name = "openssl" 766 | version = "0.10.45" 767 | source = "registry+https://github.com/rust-lang/crates.io-index" 768 | checksum = "b102428fd03bc5edf97f62620f7298614c45cedf287c271e7ed450bbaf83f2e1" 769 | dependencies = [ 770 | "bitflags", 771 | "cfg-if", 772 | "foreign-types", 773 | "libc", 774 | "once_cell", 775 | "openssl-macros", 776 | "openssl-sys", 777 | ] 778 | 779 | [[package]] 780 | name = "openssl-macros" 781 | version = "0.1.0" 782 | source = "registry+https://github.com/rust-lang/crates.io-index" 783 | checksum = "b501e44f11665960c7e7fcf062c7d96a14ade4aa98116c004b2e37b5be7d736c" 784 | dependencies = [ 785 | "proc-macro2", 786 | "quote", 787 | "syn", 788 | ] 789 | 790 | [[package]] 791 | name = "openssl-probe" 792 | version = "0.1.5" 793 | source = "registry+https://github.com/rust-lang/crates.io-index" 794 | checksum = "ff011a302c396a5197692431fc1948019154afc178baf7d8e37367442a4601cf" 795 | 796 | [[package]] 797 | name = "openssl-src" 798 | version = "111.28.2+1.1.1w" 799 | source = "registry+https://github.com/rust-lang/crates.io-index" 800 | checksum = "bb1830e20a48a975ca898ca8c1d036a36c3c6c5cb7dabc1c216706587857920f" 801 | dependencies = [ 802 | "cc", 803 | ] 804 | 805 | [[package]] 806 | name = "openssl-sys" 807 | version = "0.9.80" 808 | source = "registry+https://github.com/rust-lang/crates.io-index" 809 | checksum = "23bbbf7854cd45b83958ebe919f0e8e516793727652e27fda10a8384cfc790b7" 810 | dependencies = [ 811 | "autocfg", 812 | "cc", 813 | "libc", 814 | "openssl-src", 815 | "pkg-config", 816 | "vcpkg", 817 | ] 818 | 819 | [[package]] 820 | name = "owo-colors" 821 | version = "3.5.0" 822 | source = "registry+https://github.com/rust-lang/crates.io-index" 823 | checksum = "c1b04fb49957986fdce4d6ee7a65027d55d4b6d2265e5848bbb507b58ccfdb6f" 824 | 825 | [[package]] 826 | name = "percent-encoding" 827 | version = "2.2.0" 828 | source = "registry+https://github.com/rust-lang/crates.io-index" 829 | checksum = "478c572c3d73181ff3c2539045f6eb99e5491218eae919370993b890cdbdd98e" 830 | 831 | [[package]] 832 | name = "pin-project-lite" 833 | version = "0.2.9" 834 | source = "registry+https://github.com/rust-lang/crates.io-index" 835 | checksum = "e0a7ae3ac2f1173085d398531c705756c94a4c56843785df85a60c1a0afac116" 836 | 837 | [[package]] 838 | name = "pin-utils" 839 | version = "0.1.0" 840 | source = "registry+https://github.com/rust-lang/crates.io-index" 841 | checksum = "8b870d8c151b6f2fb93e84a13146138f05d02ed11c7e7c54f8826aaaf7c9f184" 842 | 843 | [[package]] 844 | name = "pkg-config" 845 | version = "0.3.26" 846 | source = "registry+https://github.com/rust-lang/crates.io-index" 847 | checksum = "6ac9a59f73473f1b8d852421e59e64809f025994837ef743615c6d0c5b305160" 848 | 849 | [[package]] 850 | name = "proc-macro2" 851 | version = "1.0.51" 852 | source = "registry+https://github.com/rust-lang/crates.io-index" 853 | checksum = "5d727cae5b39d21da60fa540906919ad737832fe0b1c165da3a34d6548c849d6" 854 | dependencies = [ 855 | "unicode-ident", 856 | ] 857 | 858 | [[package]] 859 | name = "quote" 860 | version = "1.0.23" 861 | source = "registry+https://github.com/rust-lang/crates.io-index" 862 | checksum = "8856d8364d252a14d474036ea1358d63c9e6965c8e5c1885c18f73d70bff9c7b" 863 | dependencies = [ 864 | "proc-macro2", 865 | ] 866 | 867 | [[package]] 868 | name = "redox_syscall" 869 | version = "0.2.16" 870 | source = "registry+https://github.com/rust-lang/crates.io-index" 871 | checksum = "fb5a58c1855b4b6819d59012155603f0b22ad30cad752600aadfcb695265519a" 872 | dependencies = [ 873 | "bitflags", 874 | ] 875 | 876 | [[package]] 877 | name = "reqwest" 878 | version = "0.11.14" 879 | source = "registry+https://github.com/rust-lang/crates.io-index" 880 | checksum = "21eed90ec8570952d53b772ecf8f206aa1ec9a3d76b2521c56c42973f2d91ee9" 881 | dependencies = [ 882 | "base64", 883 | "bytes", 884 | "encoding_rs", 885 | "futures-core", 886 | "futures-util", 887 | "h2", 888 | "http", 889 | "http-body", 890 | "hyper", 891 | "hyper-tls", 892 | "ipnet", 893 | "js-sys", 894 | "log", 895 | "mime", 896 | "native-tls", 897 | "once_cell", 898 | "percent-encoding", 899 | "pin-project-lite", 900 | "serde", 901 | "serde_json", 902 | "serde_urlencoded", 903 | "tokio", 904 | "tokio-native-tls", 905 | "tower-service", 906 | "url", 907 | "wasm-bindgen", 908 | "wasm-bindgen-futures", 909 | "web-sys", 910 | "winreg", 911 | ] 912 | 913 | [[package]] 914 | name = "rustc-demangle" 915 | version = "0.1.21" 916 | source = "registry+https://github.com/rust-lang/crates.io-index" 917 | checksum = "7ef03e0a2b150c7a90d01faf6254c9c48a41e95fb2a8c2ac1c6f0d2b9aefc342" 918 | 919 | [[package]] 920 | name = "rustix" 921 | version = "0.36.8" 922 | source = "registry+https://github.com/rust-lang/crates.io-index" 923 | checksum = "f43abb88211988493c1abb44a70efa56ff0ce98f233b7b276146f1f3f7ba9644" 924 | dependencies = [ 925 | "bitflags", 926 | "errno", 927 | "io-lifetimes", 928 | "libc", 929 | "linux-raw-sys", 930 | "windows-sys 0.45.0", 931 | ] 932 | 933 | [[package]] 934 | name = "ryu" 935 | version = "1.0.12" 936 | source = "registry+https://github.com/rust-lang/crates.io-index" 937 | checksum = "7b4b9743ed687d4b4bcedf9ff5eaa7398495ae14e61cba0a295704edbc7decde" 938 | 939 | [[package]] 940 | name = "schannel" 941 | version = "0.1.21" 942 | source = "registry+https://github.com/rust-lang/crates.io-index" 943 | checksum = "713cfb06c7059f3588fb8044c0fad1d09e3c01d225e25b9220dbfdcf16dbb1b3" 944 | dependencies = [ 945 | "windows-sys 0.42.0", 946 | ] 947 | 948 | [[package]] 949 | name = "scratch" 950 | version = "1.0.3" 951 | source = "registry+https://github.com/rust-lang/crates.io-index" 952 | checksum = "ddccb15bcce173023b3fedd9436f882a0739b8dfb45e4f6b6002bee5929f61b2" 953 | 954 | [[package]] 955 | name = "security-framework" 956 | version = "2.8.2" 957 | source = "registry+https://github.com/rust-lang/crates.io-index" 958 | checksum = "a332be01508d814fed64bf28f798a146d73792121129962fdf335bb3c49a4254" 959 | dependencies = [ 960 | "bitflags", 961 | "core-foundation", 962 | "core-foundation-sys", 963 | "libc", 964 | "security-framework-sys", 965 | ] 966 | 967 | [[package]] 968 | name = "security-framework-sys" 969 | version = "2.8.0" 970 | source = "registry+https://github.com/rust-lang/crates.io-index" 971 | checksum = "31c9bb296072e961fcbd8853511dd39c2d8be2deb1e17c6860b1d30732b323b4" 972 | dependencies = [ 973 | "core-foundation-sys", 974 | "libc", 975 | ] 976 | 977 | [[package]] 978 | name = "serde" 979 | version = "1.0.152" 980 | source = "registry+https://github.com/rust-lang/crates.io-index" 981 | checksum = "bb7d1f0d3021d347a83e556fc4683dea2ea09d87bccdf88ff5c12545d89d5efb" 982 | dependencies = [ 983 | "serde_derive", 984 | ] 985 | 986 | [[package]] 987 | name = "serde-xml-rs" 988 | version = "0.6.0" 989 | source = "registry+https://github.com/rust-lang/crates.io-index" 990 | checksum = "fb3aa78ecda1ebc9ec9847d5d3aba7d618823446a049ba2491940506da6e2782" 991 | dependencies = [ 992 | "log", 993 | "serde", 994 | "thiserror", 995 | "xml-rs", 996 | ] 997 | 998 | [[package]] 999 | name = "serde_derive" 1000 | version = "1.0.152" 1001 | source = "registry+https://github.com/rust-lang/crates.io-index" 1002 | checksum = "af487d118eecd09402d70a5d72551860e788df87b464af30e5ea6a38c75c541e" 1003 | dependencies = [ 1004 | "proc-macro2", 1005 | "quote", 1006 | "syn", 1007 | ] 1008 | 1009 | [[package]] 1010 | name = "serde_json" 1011 | version = "1.0.93" 1012 | source = "registry+https://github.com/rust-lang/crates.io-index" 1013 | checksum = "cad406b69c91885b5107daf2c29572f6c8cdb3c66826821e286c533490c0bc76" 1014 | dependencies = [ 1015 | "itoa", 1016 | "ryu", 1017 | "serde", 1018 | ] 1019 | 1020 | [[package]] 1021 | name = "serde_repr" 1022 | version = "0.1.10" 1023 | source = "registry+https://github.com/rust-lang/crates.io-index" 1024 | checksum = "9a5ec9fa74a20ebbe5d9ac23dac1fc96ba0ecfe9f50f2843b52e537b10fbcb4e" 1025 | dependencies = [ 1026 | "proc-macro2", 1027 | "quote", 1028 | "syn", 1029 | ] 1030 | 1031 | [[package]] 1032 | name = "serde_urlencoded" 1033 | version = "0.7.1" 1034 | source = "registry+https://github.com/rust-lang/crates.io-index" 1035 | checksum = "d3491c14715ca2294c4d6a88f15e84739788c1d030eed8c110436aafdaa2f3fd" 1036 | dependencies = [ 1037 | "form_urlencoded", 1038 | "itoa", 1039 | "ryu", 1040 | "serde", 1041 | ] 1042 | 1043 | [[package]] 1044 | name = "serde_yaml" 1045 | version = "0.9.17" 1046 | source = "registry+https://github.com/rust-lang/crates.io-index" 1047 | checksum = "8fb06d4b6cdaef0e0c51fa881acb721bed3c924cfaa71d9c94a3b771dfdf6567" 1048 | dependencies = [ 1049 | "indexmap", 1050 | "itoa", 1051 | "ryu", 1052 | "serde", 1053 | "unsafe-libyaml", 1054 | ] 1055 | 1056 | [[package]] 1057 | name = "sharded-slab" 1058 | version = "0.1.4" 1059 | source = "registry+https://github.com/rust-lang/crates.io-index" 1060 | checksum = "900fba806f70c630b0a382d0d825e17a0f19fcd059a2ade1ff237bcddf446b31" 1061 | dependencies = [ 1062 | "lazy_static", 1063 | ] 1064 | 1065 | [[package]] 1066 | name = "shell-words" 1067 | version = "1.1.0" 1068 | source = "registry+https://github.com/rust-lang/crates.io-index" 1069 | checksum = "24188a676b6ae68c3b2cb3a01be17fbf7240ce009799bb56d5b1409051e78fde" 1070 | 1071 | [[package]] 1072 | name = "slab" 1073 | version = "0.4.8" 1074 | source = "registry+https://github.com/rust-lang/crates.io-index" 1075 | checksum = "6528351c9bc8ab22353f9d776db39a20288e8d6c37ef8cfe3317cf875eecfc2d" 1076 | dependencies = [ 1077 | "autocfg", 1078 | ] 1079 | 1080 | [[package]] 1081 | name = "socket2" 1082 | version = "0.4.7" 1083 | source = "registry+https://github.com/rust-lang/crates.io-index" 1084 | checksum = "02e2d2db9033d13a1567121ddd7a095ee144db4e1ca1b1bda3419bc0da294ebd" 1085 | dependencies = [ 1086 | "libc", 1087 | "winapi", 1088 | ] 1089 | 1090 | [[package]] 1091 | name = "syn" 1092 | version = "1.0.109" 1093 | source = "registry+https://github.com/rust-lang/crates.io-index" 1094 | checksum = "72b64191b275b66ffe2469e8af2c1cfe3bafa67b529ead792a6d0160888b4237" 1095 | dependencies = [ 1096 | "proc-macro2", 1097 | "quote", 1098 | "unicode-ident", 1099 | ] 1100 | 1101 | [[package]] 1102 | name = "tempfile" 1103 | version = "3.4.0" 1104 | source = "registry+https://github.com/rust-lang/crates.io-index" 1105 | checksum = "af18f7ae1acd354b992402e9ec5864359d693cd8a79dcbef59f76891701c1e95" 1106 | dependencies = [ 1107 | "cfg-if", 1108 | "fastrand", 1109 | "redox_syscall", 1110 | "rustix", 1111 | "windows-sys 0.42.0", 1112 | ] 1113 | 1114 | [[package]] 1115 | name = "termcolor" 1116 | version = "1.2.0" 1117 | source = "registry+https://github.com/rust-lang/crates.io-index" 1118 | checksum = "be55cf8942feac5c765c2c993422806843c9a9a45d4d5c407ad6dd2ea95eb9b6" 1119 | dependencies = [ 1120 | "winapi-util", 1121 | ] 1122 | 1123 | [[package]] 1124 | name = "thiserror" 1125 | version = "1.0.39" 1126 | source = "registry+https://github.com/rust-lang/crates.io-index" 1127 | checksum = "a5ab016db510546d856297882807df8da66a16fb8c4101cb8b30054b0d5b2d9c" 1128 | dependencies = [ 1129 | "thiserror-impl", 1130 | ] 1131 | 1132 | [[package]] 1133 | name = "thiserror-impl" 1134 | version = "1.0.39" 1135 | source = "registry+https://github.com/rust-lang/crates.io-index" 1136 | checksum = "5420d42e90af0c38c3290abcca25b9b3bdf379fc9f55c528f53a269d9c9a267e" 1137 | dependencies = [ 1138 | "proc-macro2", 1139 | "quote", 1140 | "syn", 1141 | ] 1142 | 1143 | [[package]] 1144 | name = "thread_local" 1145 | version = "1.1.7" 1146 | source = "registry+https://github.com/rust-lang/crates.io-index" 1147 | checksum = "3fdd6f064ccff2d6567adcb3873ca630700f00b5ad3f060c25b5dcfd9a4ce152" 1148 | dependencies = [ 1149 | "cfg-if", 1150 | "once_cell", 1151 | ] 1152 | 1153 | [[package]] 1154 | name = "time" 1155 | version = "0.1.45" 1156 | source = "registry+https://github.com/rust-lang/crates.io-index" 1157 | checksum = "1b797afad3f312d1c66a56d11d0316f916356d11bd158fbc6ca6389ff6bf805a" 1158 | dependencies = [ 1159 | "libc", 1160 | "wasi 0.10.0+wasi-snapshot-preview1", 1161 | "winapi", 1162 | ] 1163 | 1164 | [[package]] 1165 | name = "tinyvec" 1166 | version = "1.6.0" 1167 | source = "registry+https://github.com/rust-lang/crates.io-index" 1168 | checksum = "87cc5ceb3875bb20c2890005a4e226a4651264a5c75edb2421b52861a0a0cb50" 1169 | dependencies = [ 1170 | "tinyvec_macros", 1171 | ] 1172 | 1173 | [[package]] 1174 | name = "tinyvec_macros" 1175 | version = "0.1.1" 1176 | source = "registry+https://github.com/rust-lang/crates.io-index" 1177 | checksum = "1f3ccbac311fea05f86f61904b462b55fb3df8837a366dfc601a0161d0532f20" 1178 | 1179 | [[package]] 1180 | name = "tokio" 1181 | version = "1.25.0" 1182 | source = "registry+https://github.com/rust-lang/crates.io-index" 1183 | checksum = "c8e00990ebabbe4c14c08aca901caed183ecd5c09562a12c824bb53d3c3fd3af" 1184 | dependencies = [ 1185 | "autocfg", 1186 | "bytes", 1187 | "libc", 1188 | "memchr", 1189 | "mio", 1190 | "num_cpus", 1191 | "pin-project-lite", 1192 | "socket2", 1193 | "tokio-macros", 1194 | "windows-sys 0.42.0", 1195 | ] 1196 | 1197 | [[package]] 1198 | name = "tokio-macros" 1199 | version = "1.8.2" 1200 | source = "registry+https://github.com/rust-lang/crates.io-index" 1201 | checksum = "d266c00fde287f55d3f1c3e96c500c362a2b8c695076ec180f27918820bc6df8" 1202 | dependencies = [ 1203 | "proc-macro2", 1204 | "quote", 1205 | "syn", 1206 | ] 1207 | 1208 | [[package]] 1209 | name = "tokio-native-tls" 1210 | version = "0.3.1" 1211 | source = "registry+https://github.com/rust-lang/crates.io-index" 1212 | checksum = "bbae76ab933c85776efabc971569dd6119c580d8f5d448769dec1764bf796ef2" 1213 | dependencies = [ 1214 | "native-tls", 1215 | "tokio", 1216 | ] 1217 | 1218 | [[package]] 1219 | name = "tokio-util" 1220 | version = "0.7.7" 1221 | source = "registry+https://github.com/rust-lang/crates.io-index" 1222 | checksum = "5427d89453009325de0d8f342c9490009f76e999cb7672d77e46267448f7e6b2" 1223 | dependencies = [ 1224 | "bytes", 1225 | "futures-core", 1226 | "futures-sink", 1227 | "pin-project-lite", 1228 | "tokio", 1229 | "tracing", 1230 | ] 1231 | 1232 | [[package]] 1233 | name = "tower-service" 1234 | version = "0.3.2" 1235 | source = "registry+https://github.com/rust-lang/crates.io-index" 1236 | checksum = "b6bc1c9ce2b5135ac7f93c72918fc37feb872bdc6a5533a8b85eb4b86bfdae52" 1237 | 1238 | [[package]] 1239 | name = "tracing" 1240 | version = "0.1.37" 1241 | source = "registry+https://github.com/rust-lang/crates.io-index" 1242 | checksum = "8ce8c33a8d48bd45d624a6e523445fd21ec13d3653cd51f681abf67418f54eb8" 1243 | dependencies = [ 1244 | "cfg-if", 1245 | "pin-project-lite", 1246 | "tracing-core", 1247 | ] 1248 | 1249 | [[package]] 1250 | name = "tracing-core" 1251 | version = "0.1.30" 1252 | source = "registry+https://github.com/rust-lang/crates.io-index" 1253 | checksum = "24eb03ba0eab1fd845050058ce5e616558e8f8d8fca633e6b163fe25c797213a" 1254 | dependencies = [ 1255 | "once_cell", 1256 | "valuable", 1257 | ] 1258 | 1259 | [[package]] 1260 | name = "tracing-error" 1261 | version = "0.2.0" 1262 | source = "registry+https://github.com/rust-lang/crates.io-index" 1263 | checksum = "d686ec1c0f384b1277f097b2f279a2ecc11afe8c133c1aabf036a27cb4cd206e" 1264 | dependencies = [ 1265 | "tracing", 1266 | "tracing-subscriber", 1267 | ] 1268 | 1269 | [[package]] 1270 | name = "tracing-subscriber" 1271 | version = "0.3.16" 1272 | source = "registry+https://github.com/rust-lang/crates.io-index" 1273 | checksum = "a6176eae26dd70d0c919749377897b54a9276bd7061339665dd68777926b5a70" 1274 | dependencies = [ 1275 | "sharded-slab", 1276 | "thread_local", 1277 | "tracing-core", 1278 | ] 1279 | 1280 | [[package]] 1281 | name = "try-lock" 1282 | version = "0.2.4" 1283 | source = "registry+https://github.com/rust-lang/crates.io-index" 1284 | checksum = "3528ecfd12c466c6f163363caf2d02a71161dd5e1cc6ae7b34207ea2d42d81ed" 1285 | 1286 | [[package]] 1287 | name = "unicode-bidi" 1288 | version = "0.3.10" 1289 | source = "registry+https://github.com/rust-lang/crates.io-index" 1290 | checksum = "d54675592c1dbefd78cbd98db9bacd89886e1ca50692a0692baefffdeb92dd58" 1291 | 1292 | [[package]] 1293 | name = "unicode-ident" 1294 | version = "1.0.6" 1295 | source = "registry+https://github.com/rust-lang/crates.io-index" 1296 | checksum = "84a22b9f218b40614adcb3f4ff08b703773ad44fa9423e4e0d346d5db86e4ebc" 1297 | 1298 | [[package]] 1299 | name = "unicode-normalization" 1300 | version = "0.1.22" 1301 | source = "registry+https://github.com/rust-lang/crates.io-index" 1302 | checksum = "5c5713f0fc4b5db668a2ac63cdb7bb4469d8c9fed047b1d0292cc7b0ce2ba921" 1303 | dependencies = [ 1304 | "tinyvec", 1305 | ] 1306 | 1307 | [[package]] 1308 | name = "unicode-width" 1309 | version = "0.1.10" 1310 | source = "registry+https://github.com/rust-lang/crates.io-index" 1311 | checksum = "c0edd1e5b14653f783770bce4a4dabb4a5108a5370a5f5d8cfe8710c361f6c8b" 1312 | 1313 | [[package]] 1314 | name = "unsafe-libyaml" 1315 | version = "0.2.5" 1316 | source = "registry+https://github.com/rust-lang/crates.io-index" 1317 | checksum = "bc7ed8ba44ca06be78ea1ad2c3682a43349126c8818054231ee6f4748012aed2" 1318 | 1319 | [[package]] 1320 | name = "url" 1321 | version = "2.3.1" 1322 | source = "registry+https://github.com/rust-lang/crates.io-index" 1323 | checksum = "0d68c799ae75762b8c3fe375feb6600ef5602c883c5d21eb51c09f22b83c4643" 1324 | dependencies = [ 1325 | "form_urlencoded", 1326 | "idna", 1327 | "percent-encoding", 1328 | ] 1329 | 1330 | [[package]] 1331 | name = "valuable" 1332 | version = "0.1.0" 1333 | source = "registry+https://github.com/rust-lang/crates.io-index" 1334 | checksum = "830b7e5d4d90034032940e4ace0d9a9a057e7a45cd94e6c007832e39edb82f6d" 1335 | 1336 | [[package]] 1337 | name = "vcpkg" 1338 | version = "0.2.15" 1339 | source = "registry+https://github.com/rust-lang/crates.io-index" 1340 | checksum = "accd4ea62f7bb7a82fe23066fb0957d48ef677f6eeb8215f372f52e48bb32426" 1341 | 1342 | [[package]] 1343 | name = "want" 1344 | version = "0.3.0" 1345 | source = "registry+https://github.com/rust-lang/crates.io-index" 1346 | checksum = "1ce8a968cb1cd110d136ff8b819a556d6fb6d919363c61534f6860c7eb172ba0" 1347 | dependencies = [ 1348 | "log", 1349 | "try-lock", 1350 | ] 1351 | 1352 | [[package]] 1353 | name = "wasi" 1354 | version = "0.10.0+wasi-snapshot-preview1" 1355 | source = "registry+https://github.com/rust-lang/crates.io-index" 1356 | checksum = "1a143597ca7c7793eff794def352d41792a93c481eb1042423ff7ff72ba2c31f" 1357 | 1358 | [[package]] 1359 | name = "wasi" 1360 | version = "0.11.0+wasi-snapshot-preview1" 1361 | source = "registry+https://github.com/rust-lang/crates.io-index" 1362 | checksum = "9c8d87e72b64a3b4db28d11ce29237c246188f4f51057d65a7eab63b7987e423" 1363 | 1364 | [[package]] 1365 | name = "wasm-bindgen" 1366 | version = "0.2.84" 1367 | source = "registry+https://github.com/rust-lang/crates.io-index" 1368 | checksum = "31f8dcbc21f30d9b8f2ea926ecb58f6b91192c17e9d33594b3df58b2007ca53b" 1369 | dependencies = [ 1370 | "cfg-if", 1371 | "wasm-bindgen-macro", 1372 | ] 1373 | 1374 | [[package]] 1375 | name = "wasm-bindgen-backend" 1376 | version = "0.2.84" 1377 | source = "registry+https://github.com/rust-lang/crates.io-index" 1378 | checksum = "95ce90fd5bcc06af55a641a86428ee4229e44e07033963a2290a8e241607ccb9" 1379 | dependencies = [ 1380 | "bumpalo", 1381 | "log", 1382 | "once_cell", 1383 | "proc-macro2", 1384 | "quote", 1385 | "syn", 1386 | "wasm-bindgen-shared", 1387 | ] 1388 | 1389 | [[package]] 1390 | name = "wasm-bindgen-futures" 1391 | version = "0.4.34" 1392 | source = "registry+https://github.com/rust-lang/crates.io-index" 1393 | checksum = "f219e0d211ba40266969f6dbdd90636da12f75bee4fc9d6c23d1260dadb51454" 1394 | dependencies = [ 1395 | "cfg-if", 1396 | "js-sys", 1397 | "wasm-bindgen", 1398 | "web-sys", 1399 | ] 1400 | 1401 | [[package]] 1402 | name = "wasm-bindgen-macro" 1403 | version = "0.2.84" 1404 | source = "registry+https://github.com/rust-lang/crates.io-index" 1405 | checksum = "4c21f77c0bedc37fd5dc21f897894a5ca01e7bb159884559461862ae90c0b4c5" 1406 | dependencies = [ 1407 | "quote", 1408 | "wasm-bindgen-macro-support", 1409 | ] 1410 | 1411 | [[package]] 1412 | name = "wasm-bindgen-macro-support" 1413 | version = "0.2.84" 1414 | source = "registry+https://github.com/rust-lang/crates.io-index" 1415 | checksum = "2aff81306fcac3c7515ad4e177f521b5c9a15f2b08f4e32d823066102f35a5f6" 1416 | dependencies = [ 1417 | "proc-macro2", 1418 | "quote", 1419 | "syn", 1420 | "wasm-bindgen-backend", 1421 | "wasm-bindgen-shared", 1422 | ] 1423 | 1424 | [[package]] 1425 | name = "wasm-bindgen-shared" 1426 | version = "0.2.84" 1427 | source = "registry+https://github.com/rust-lang/crates.io-index" 1428 | checksum = "0046fef7e28c3804e5e38bfa31ea2a0f73905319b677e57ebe37e49358989b5d" 1429 | 1430 | [[package]] 1431 | name = "web-sys" 1432 | version = "0.3.61" 1433 | source = "registry+https://github.com/rust-lang/crates.io-index" 1434 | checksum = "e33b99f4b23ba3eec1a53ac264e35a755f00e966e0065077d6027c0f575b0b97" 1435 | dependencies = [ 1436 | "js-sys", 1437 | "wasm-bindgen", 1438 | ] 1439 | 1440 | [[package]] 1441 | name = "winapi" 1442 | version = "0.3.9" 1443 | source = "registry+https://github.com/rust-lang/crates.io-index" 1444 | checksum = "5c839a674fcd7a98952e593242ea400abe93992746761e38641405d28b00f419" 1445 | dependencies = [ 1446 | "winapi-i686-pc-windows-gnu", 1447 | "winapi-x86_64-pc-windows-gnu", 1448 | ] 1449 | 1450 | [[package]] 1451 | name = "winapi-i686-pc-windows-gnu" 1452 | version = "0.4.0" 1453 | source = "registry+https://github.com/rust-lang/crates.io-index" 1454 | checksum = "ac3b87c63620426dd9b991e5ce0329eff545bccbbb34f3be09ff6fb6ab51b7b6" 1455 | 1456 | [[package]] 1457 | name = "winapi-util" 1458 | version = "0.1.5" 1459 | source = "registry+https://github.com/rust-lang/crates.io-index" 1460 | checksum = "70ec6ce85bb158151cae5e5c87f95a8e97d2c0c4b001223f33a334e3ce5de178" 1461 | dependencies = [ 1462 | "winapi", 1463 | ] 1464 | 1465 | [[package]] 1466 | name = "winapi-x86_64-pc-windows-gnu" 1467 | version = "0.4.0" 1468 | source = "registry+https://github.com/rust-lang/crates.io-index" 1469 | checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f" 1470 | 1471 | [[package]] 1472 | name = "windows-sys" 1473 | version = "0.42.0" 1474 | source = "registry+https://github.com/rust-lang/crates.io-index" 1475 | checksum = "5a3e1820f08b8513f676f7ab6c1f99ff312fb97b553d30ff4dd86f9f15728aa7" 1476 | dependencies = [ 1477 | "windows_aarch64_gnullvm", 1478 | "windows_aarch64_msvc", 1479 | "windows_i686_gnu", 1480 | "windows_i686_msvc", 1481 | "windows_x86_64_gnu", 1482 | "windows_x86_64_gnullvm", 1483 | "windows_x86_64_msvc", 1484 | ] 1485 | 1486 | [[package]] 1487 | name = "windows-sys" 1488 | version = "0.45.0" 1489 | source = "registry+https://github.com/rust-lang/crates.io-index" 1490 | checksum = "75283be5efb2831d37ea142365f009c02ec203cd29a3ebecbc093d52315b66d0" 1491 | dependencies = [ 1492 | "windows-targets", 1493 | ] 1494 | 1495 | [[package]] 1496 | name = "windows-targets" 1497 | version = "0.42.1" 1498 | source = "registry+https://github.com/rust-lang/crates.io-index" 1499 | checksum = "8e2522491fbfcd58cc84d47aeb2958948c4b8982e9a2d8a2a35bbaed431390e7" 1500 | dependencies = [ 1501 | "windows_aarch64_gnullvm", 1502 | "windows_aarch64_msvc", 1503 | "windows_i686_gnu", 1504 | "windows_i686_msvc", 1505 | "windows_x86_64_gnu", 1506 | "windows_x86_64_gnullvm", 1507 | "windows_x86_64_msvc", 1508 | ] 1509 | 1510 | [[package]] 1511 | name = "windows_aarch64_gnullvm" 1512 | version = "0.42.1" 1513 | source = "registry+https://github.com/rust-lang/crates.io-index" 1514 | checksum = "8c9864e83243fdec7fc9c5444389dcbbfd258f745e7853198f365e3c4968a608" 1515 | 1516 | [[package]] 1517 | name = "windows_aarch64_msvc" 1518 | version = "0.42.1" 1519 | source = "registry+https://github.com/rust-lang/crates.io-index" 1520 | checksum = "4c8b1b673ffc16c47a9ff48570a9d85e25d265735c503681332589af6253c6c7" 1521 | 1522 | [[package]] 1523 | name = "windows_i686_gnu" 1524 | version = "0.42.1" 1525 | source = "registry+https://github.com/rust-lang/crates.io-index" 1526 | checksum = "de3887528ad530ba7bdbb1faa8275ec7a1155a45ffa57c37993960277145d640" 1527 | 1528 | [[package]] 1529 | name = "windows_i686_msvc" 1530 | version = "0.42.1" 1531 | source = "registry+https://github.com/rust-lang/crates.io-index" 1532 | checksum = "bf4d1122317eddd6ff351aa852118a2418ad4214e6613a50e0191f7004372605" 1533 | 1534 | [[package]] 1535 | name = "windows_x86_64_gnu" 1536 | version = "0.42.1" 1537 | source = "registry+https://github.com/rust-lang/crates.io-index" 1538 | checksum = "c1040f221285e17ebccbc2591ffdc2d44ee1f9186324dd3e84e99ac68d699c45" 1539 | 1540 | [[package]] 1541 | name = "windows_x86_64_gnullvm" 1542 | version = "0.42.1" 1543 | source = "registry+https://github.com/rust-lang/crates.io-index" 1544 | checksum = "628bfdf232daa22b0d64fdb62b09fcc36bb01f05a3939e20ab73aaf9470d0463" 1545 | 1546 | [[package]] 1547 | name = "windows_x86_64_msvc" 1548 | version = "0.42.1" 1549 | source = "registry+https://github.com/rust-lang/crates.io-index" 1550 | checksum = "447660ad36a13288b1db4d4248e857b510e8c3a225c822ba4fb748c0aafecffd" 1551 | 1552 | [[package]] 1553 | name = "winreg" 1554 | version = "0.10.1" 1555 | source = "registry+https://github.com/rust-lang/crates.io-index" 1556 | checksum = "80d0f4e272c85def139476380b12f9ac60926689dd2e01d4923222f40580869d" 1557 | dependencies = [ 1558 | "winapi", 1559 | ] 1560 | 1561 | [[package]] 1562 | name = "xml-rs" 1563 | version = "0.8.4" 1564 | source = "registry+https://github.com/rust-lang/crates.io-index" 1565 | checksum = "d2d7d3948613f75c98fd9328cfdcc45acc4d360655289d0a7d4ec931392200a3" 1566 | 1567 | [[package]] 1568 | name = "zeroize" 1569 | version = "1.5.7" 1570 | source = "registry+https://github.com/rust-lang/crates.io-index" 1571 | checksum = "c394b5bd0c6f669e7275d9c20aa90ae064cb22e75a1cad54e1b34088034b149f" 1572 | -------------------------------------------------------------------------------- /Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "media-cleaner" 3 | version = "1.4.0" 4 | edition = "2021" 5 | 6 | # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html 7 | 8 | [dependencies] 9 | chrono = "0.4.23" 10 | color-eyre = "0.6.2" 11 | dialoguer = "0.10.3" 12 | futures = "0.3.26" 13 | itertools = "0.10.5" 14 | once_cell = "1.17.1" 15 | reqwest = {version = "0.11.14", features = ["json"]} 16 | serde = {version = "1.0.152", features = ["derive"]} 17 | serde_json = "1.0.93" 18 | serde_repr = "0.1.10" 19 | serde-xml-rs = "0.6.0" 20 | serde_yaml = "0.9.17" 21 | tokio = { version = "1.25.0", features = ["rt", "macros", "rt-multi-thread"] } 22 | openssl = { version = "0.10", features = ["vendored"] } 23 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2023 Felix Bjerhem Aronsson 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /readme.md: -------------------------------------------------------------------------------- 1 | # Media Cleaner 2 | 3 | Media Cleaner is a simple CLI tool to clean up your media library based on your Overseerr requests and Tautulli history, written in Rust. 4 | 5 | It was written to help me clean up my Plex library, but it should work for anyone with a similar setup. It's not perfect, but it works for me. (This project was written partly to help me learn Rust, so please don't judge me too harshly, though feedback is always welcome.) 6 | 7 | ## Installation 8 | 9 | Just download the latest release for your platform from the [releases page](https://github.com/Supergamer1337/media-cleaner/releases). 10 | 11 | ## Usage 12 | 13 | ### Config 14 | 15 | Make sure you have a config file in the same directory as the executable (or more specifically the root of the working directory when you launch it). It should be named `config.yaml` and look something like this (this was chosen instead of CLI arguments to make it easier for repeated use): 16 | 17 | ```yaml 18 | # The number of items to show in the list of items to select. 19 | # Useful to limit if your terminal is small, as it can be quite buggy if the list doesn't fit. 20 | # Default to 5 if not specified. 21 | items_shown: 5 22 | plex: 23 | url: https://YOUR_PLEX_URL 24 | token: YOUR_PLEX_TOKEN 25 | overseerr: 26 | url: https://YOUR_OVERSEERR_URL 27 | api_key: YOUR_API_KEY 28 | tautulli: 29 | url: https://YOUR_TAUTULLI_URL 30 | api_key: YOUR_API_KEY 31 | sonarr: # If you don't use Sonarr, just leave this section out 32 | url: https://YOUR_SONARR_URL 33 | api_key: YOUR_API_KEY 34 | sonarr_4k: # If you don't have a 4k Sonarr instance, just leave this section out 35 | url: https://YOUR_SONARR_4K_URL 36 | api_key: YOUR_API_KEY 37 | radarr: # If you don't use Radarr, just leave this section out 38 | url: https://YOUR_RADARR_URL 39 | api_key: YOUR_API_KEY 40 | radarr_4k: # If you don't have a 4k Radarr instance, just leave this section out 41 | url: https://YOUR_RADARR_4K_URL 42 | api_key: YOUR_API_KEY 43 | ``` 44 | 45 | All fields have to be filled in, except for Sonarr or Radarr (though if their root is listed, all values have to be filled). If both Sonarr and Radarr are missing, the program will give you an error, as it requires at least one of them to be active. 46 | 47 | You can get your api keys from the respective applications. A simple search should help you find it. For the Plex token, you can follow [this guide](https://support.plex.tv/articles/204059436-finding-an-authentication-token-x-plex-token/). 48 | 49 | **ALSO MAKE SURE CSRF IS TURNED OFF IN OVERSEERR.** 50 | 51 | #### Ignoring users 52 | 53 | If you want to ignore a user (or multiple) simply add them to the `ignored_users` list in the config file. This is useful if you have a user that you don't want to remove media for, for example yourself. The user is matched by their Overseerr username, so make sure you use the same username in both places. 54 | 55 | Example: 56 | 57 | ```yaml 58 | ignored_users: 59 | - MyUser 60 | - SomeOtherUser 61 | ``` 62 | 63 | ### Running the program 64 | 65 | Once you have your config file, you can run the program with `./media-cleaner` (or `.\media-cleaner.exe` on Windows). If nothing is shown immediately, you have to wait for it to finish all the requests to gather the appropriate data. Afterwards it will bring up a list of possible sorting options for your requests. After that it will instead show a list of all your requests, sorted in the way chosen, with the media data associated with that item (watch history, space, etc.), simply select the ones you want to remove (with space) and press enter. This will (after a confirmations screen) remove the request from Overseerr and tell Sonarr and Radarr to remove the show and its files. 66 | 67 | ### Arguments 68 | 69 | Arguments are used to either: 70 | 71 | 1. Speed up the process by skipping certain screens. 72 | 2. Change the behavior of the program. 73 | 74 | #### Sorting 75 | 76 | You can also pass an argument to the program to skip the sorting screen and go straight to the requests screen. The argument is the sorting method you want to use, and can be one of the following: 77 | 78 | - `-s`: Sort by size 79 | - `-sa`: Sort by size, in ascending order 80 | - `-n`: Sort by name 81 | - `-nd`: Sort by name, in descending order 82 | - `-t`: Sort by media type 83 | - `-r`: Sort by request date 84 | - `-rd`: Sort by request date, in descending order 85 | 86 | #### Getting a list of all media 87 | 88 | By passing in the flag `-C`, the program will instead show a list of all media in your library, with the same information as the requests screen. This is useful if you want to see what media you have in your library, and what you can remove. Even though that item does not have a request associated with it. Otherwise it works the same as the "normal" requests screen. 89 | 90 | ## Issues and PRs 91 | 92 | You are welcome to open issues, but please be aware that this is a hobby project written to help me learn Rust, and as such have no ambitions to a) implement features I don't want (though you are free to open a PR and I'll have a look at it), and b) fix issues that don't plague me personally (unless I feel it is large enough to warrant a fix). 93 | 94 | When it comes to PRs, I'm happy to accept them, but please be aware that I'm not a professional programmer (yet), so I might not be able to give you the best feedback. Similarly, it may take some time while I take the time to look at it. 95 | 96 | ## License 97 | 98 | This project is licensed under the MIT license. See the [LICENSE](LICENSE) file for more details. 99 | -------------------------------------------------------------------------------- /src/arguments.rs: -------------------------------------------------------------------------------- 1 | use color_eyre::Result; 2 | use itertools::Itertools; 3 | use once_cell::sync::OnceCell; 4 | use std::env; 5 | 6 | use crate::SortingOption; 7 | 8 | static INSTANCE: OnceCell = OnceCell::new(); 9 | 10 | #[derive(Debug)] 11 | pub struct Arguments { 12 | pub sorting: Option, 13 | pub all_media: bool, 14 | } 15 | 16 | impl Arguments { 17 | pub fn get_args() -> &'static Arguments { 18 | INSTANCE.get().expect("Arguments have not been initialised") 19 | } 20 | 21 | pub fn read_args() -> Result<()> { 22 | if let Some(_) = INSTANCE.get() { 23 | return Ok(()); 24 | } 25 | 26 | let mut args = env::args().collect_vec(); 27 | 28 | let args = Arguments { 29 | sorting: Self::read_sort(&mut args), 30 | all_media: Self::read_all_media(&mut args), 31 | }; 32 | 33 | INSTANCE 34 | .set(args) 35 | .expect("Arguments have already been initialized..."); 36 | Ok(()) 37 | } 38 | 39 | fn read_sort(args: &mut Vec) -> Option { 40 | for (i, arg) in args.iter_mut().enumerate() { 41 | if let Ok(sort) = SortingOption::from_str(&arg[1..]) { 42 | args.swap_remove(i); 43 | return Some(sort); 44 | } 45 | } 46 | 47 | None 48 | } 49 | 50 | fn read_all_media(args: &mut Vec) -> bool { 51 | for (i, arg) in args.iter_mut().enumerate() { 52 | if arg == "-C" { 53 | args.swap_remove(i); 54 | return true; 55 | } 56 | } 57 | 58 | return false; 59 | } 60 | } 61 | -------------------------------------------------------------------------------- /src/arr/mod.rs: -------------------------------------------------------------------------------- 1 | mod radarr; 2 | mod sonarr; 3 | 4 | use std::fmt::Display; 5 | 6 | use chrono::{DateTime, Utc}; 7 | use color_eyre::owo_colors::OwoColorize; 8 | use color_eyre::Result; 9 | 10 | pub use self::radarr::MovieStatus; 11 | pub use self::sonarr::SeriesStatus; 12 | use crate::config::Config; 13 | use crate::shared::MediaType; 14 | 15 | pub fn movie_manger_active() -> bool { 16 | match Config::global().radarr { 17 | Some(_) => true, 18 | None => false, 19 | } 20 | } 21 | 22 | pub fn movie_4k_manager_active() -> bool { 23 | match Config::global().radarr_4k { 24 | Some(_) => true, 25 | None => false, 26 | } 27 | } 28 | 29 | pub fn tv_manager_active() -> bool { 30 | match Config::global().sonarr { 31 | Some(_) => true, 32 | None => false, 33 | } 34 | } 35 | 36 | pub fn tv_4k_manager_active() -> bool { 37 | match Config::global().sonarr_4k { 38 | Some(_) => true, 39 | None => false, 40 | } 41 | } 42 | 43 | #[derive(Debug)] 44 | pub enum ArrData { 45 | Movie(MovieData), 46 | Tv(TvData), 47 | } 48 | 49 | impl ArrData { 50 | pub async fn get_data(media_type: MediaType, id: i32) -> Result { 51 | match media_type { 52 | MediaType::Movie => Ok(Self::Movie(MovieData::get_data(id, false).await?)), 53 | MediaType::Tv => Ok(Self::Tv(TvData::get_data(id, false).await?)), 54 | } 55 | } 56 | 57 | pub async fn get_4k_data(media_type: MediaType, id: i32) -> Result { 58 | match media_type { 59 | MediaType::Movie => Ok(Self::Movie(MovieData::get_data(id, true).await?)), 60 | MediaType::Tv => Ok(Self::Tv(TvData::get_data(id, true).await?)), 61 | } 62 | } 63 | 64 | pub async fn remove_data(self) -> Result<()> { 65 | match self { 66 | Self::Movie(movie) => movie.remove_data().await, 67 | Self::Tv(tv) => tv.remove_data().await, 68 | } 69 | } 70 | 71 | pub fn get_disk_size(&self) -> i64 { 72 | match self { 73 | Self::Movie(movie) => movie.size_on_disk, 74 | Self::Tv(tv) => tv.size_on_disk, 75 | } 76 | } 77 | } 78 | 79 | impl Display for ArrData { 80 | fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { 81 | match self { 82 | Self::Movie(movie) => write!(f, "{}", movie), 83 | Self::Tv(tv) => write!(f, "{}", tv), 84 | } 85 | } 86 | } 87 | 88 | #[derive(Debug)] 89 | pub struct MovieData { 90 | id: i32, 91 | status: MovieStatus, 92 | size_on_disk: i64, 93 | digital_release: Option>, 94 | physical_release: Option>, 95 | } 96 | 97 | impl MovieData { 98 | async fn get_data(id: i32, is_4k: bool) -> Result { 99 | let data = radarr::get_radarr_data(id, is_4k).await?; 100 | 101 | Ok(Self { 102 | id: data.id, 103 | status: data.status, 104 | size_on_disk: data.size_on_disk, 105 | digital_release: get_potential_date_time(data.digital_release)?, 106 | physical_release: get_potential_date_time(data.physical_release)?, 107 | }) 108 | } 109 | 110 | async fn remove_data(self) -> Result<()> { 111 | radarr::delete_radarr_data_and_files(self.id).await 112 | } 113 | } 114 | 115 | impl Display for MovieData { 116 | fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { 117 | let digital_release = format_potential_date(self.digital_release); 118 | 119 | let physical_release = format_potential_date(self.physical_release); 120 | 121 | write!( 122 | f, 123 | "It was released {} digitally and {} physically. Current status is {:?}.", 124 | digital_release.blue(), 125 | physical_release.blue(), 126 | self.status.green(), 127 | ) 128 | } 129 | } 130 | 131 | #[derive(Debug)] 132 | pub struct TvData { 133 | id: i32, 134 | status: SeriesStatus, 135 | last_airing: Option>, 136 | next_airing: Option>, 137 | season_count: i32, 138 | episodes_in_last_season: i32, 139 | percent_of_episodes_on_disk: f64, 140 | size_on_disk: i64, 141 | } 142 | 143 | impl TvData { 144 | async fn remove_data(self) -> Result<()> { 145 | sonarr::remove_sonarr_data_and_files(self.id).await 146 | } 147 | 148 | async fn get_data(id: i32, is_4k: bool) -> Result { 149 | let data = sonarr::get_sonarr_data(id, is_4k).await?; 150 | 151 | let episodes_in_last_season = data 152 | .seasons 153 | .iter() 154 | .max_by_key(|s| s.season_number) 155 | .map(|s| s.statistics.episode_count); 156 | 157 | Ok(Self { 158 | id: data.id, 159 | last_airing: get_potential_date_time(data.previous_airing)?, 160 | next_airing: get_potential_date_time(data.next_airing)?, 161 | status: data.status, 162 | season_count: data.statistics.season_count, 163 | episodes_in_last_season: match episodes_in_last_season { 164 | Some(count) => count, 165 | None => 0, 166 | }, 167 | percent_of_episodes_on_disk: data.statistics.percent_of_episodes, 168 | size_on_disk: data.statistics.size_on_disk, 169 | }) 170 | } 171 | } 172 | 173 | impl Display for TvData { 174 | fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { 175 | let last_aired = format_potential_date(self.last_airing); 176 | let next_airing = format_potential_date(self.next_airing); 177 | 178 | write!( 179 | f, 180 | "Last airing was {} and the next {}. Current status is {:?}. It has {} seasons, and {} episodes in the last season, with {} of episodes downloaded.", 181 | last_aired.blue(), 182 | next_airing.blue(), 183 | self.status.green(), 184 | self.season_count.yellow(), 185 | match self.episodes_in_last_season { 186 | 0 => "unknown".to_string(), 187 | count => count.to_string() 188 | }.yellow(), 189 | &format!("{:.2}%", self.percent_of_episodes_on_disk).blue(), 190 | ) 191 | } 192 | } 193 | 194 | fn get_potential_date_time(potential_date: Option) -> Result>> { 195 | match potential_date { 196 | Some(ref date) => { 197 | let date = DateTime::parse_from_rfc3339(date)?; 198 | Ok(Some(date.with_timezone(&Utc))) 199 | } 200 | None => Ok(None), 201 | } 202 | } 203 | 204 | fn format_potential_date(potential_date: Option>) -> String { 205 | match potential_date { 206 | Some(release) => release.format("%d-%m-%Y").to_string(), 207 | None => "never(?)".into(), 208 | } 209 | } 210 | -------------------------------------------------------------------------------- /src/arr/radarr/api.rs: -------------------------------------------------------------------------------- 1 | use color_eyre::{eyre::eyre, Result}; 2 | use serde::de::DeserializeOwned; 3 | 4 | use crate::{ 5 | config::{Config, Radarr}, 6 | utils::{create_api_error_message, create_param_string}, 7 | }; 8 | 9 | pub async fn get(path: &str, params: Option>, is_4k: bool) -> Result 10 | where 11 | T: DeserializeOwned, 12 | { 13 | let config: &Radarr; 14 | if is_4k { 15 | config = match &Config::global().radarr_4k { 16 | Some(ref radarr) => radarr, 17 | None => { 18 | return Err(eyre!( 19 | "Tried to access radarr config, even though it is not defined." 20 | )) 21 | } 22 | } 23 | } else { 24 | config = match &Config::global().radarr { 25 | Some(ref radarr) => radarr, 26 | None => { 27 | return Err(eyre!( 28 | "Tried to access radarr config, even though it is not defined." 29 | )) 30 | } 31 | }; 32 | } 33 | 34 | let client = reqwest::Client::new(); 35 | let params = create_param_string(params); 36 | 37 | let response = client 38 | .get(format!("{}/api/v3{}?{}", config.url, path, params)) 39 | .header("X-Api-Key", &config.api_key) 40 | .send() 41 | .await?; 42 | 43 | if !(response.status().as_u16() >= 200 && response.status().as_u16() < 300) { 44 | let code = response.status().as_u16(); 45 | return Err(eyre!(create_api_error_message(code, path, "Radarr"))); 46 | } 47 | 48 | let response = response.json().await?; 49 | 50 | Ok(response) 51 | } 52 | 53 | pub async fn delete(path: &str, params: Option>) -> Result<()> { 54 | let config = match &Config::global().radarr { 55 | Some(ref radarr) => radarr, 56 | None => { 57 | return Err(eyre!( 58 | "Tried to access radarr config, even though it is not defined." 59 | )) 60 | } 61 | }; 62 | let client = reqwest::Client::new(); 63 | let params = create_param_string(params); 64 | 65 | client 66 | .delete(format!("{}/api/v3{}?{}", &config.url, path, params)) 67 | .header("X-Api-Key", &config.api_key) 68 | .send() 69 | .await?; 70 | 71 | Ok(()) 72 | } 73 | -------------------------------------------------------------------------------- /src/arr/radarr/mod.rs: -------------------------------------------------------------------------------- 1 | mod api; 2 | mod responses; 3 | 4 | use color_eyre::Result; 5 | 6 | use self::responses::MovieResource; 7 | pub use self::responses::MovieStatus; 8 | 9 | pub async fn get_radarr_data(id: i32, is_4k: bool) -> Result { 10 | let path = format!("/movie/{}", id.to_string()); 11 | api::get(&path, None, is_4k).await 12 | } 13 | 14 | pub async fn delete_radarr_data_and_files(radarr_id: i32) -> Result<()> { 15 | let path = format!("/movie/{}", radarr_id.to_string()); 16 | let params = vec![("deleteFiles", "true"), ("addImportExclusion", "false")]; 17 | api::delete(path.as_str(), Some(params)).await 18 | } 19 | -------------------------------------------------------------------------------- /src/arr/radarr/responses.rs: -------------------------------------------------------------------------------- 1 | use serde::Deserialize; 2 | 3 | #[derive(Debug, Deserialize)] 4 | #[serde(rename_all = "camelCase")] 5 | pub struct MovieResource { 6 | pub id: i32, 7 | pub title: Option, 8 | pub status: MovieStatus, 9 | pub size_on_disk: i64, 10 | pub digital_release: Option, 11 | pub physical_release: Option, 12 | } 13 | 14 | #[derive(Debug, Deserialize, Clone, Copy)] 15 | #[serde(rename_all = "camelCase")] 16 | pub enum MovieStatus { 17 | #[serde(rename = "tba")] 18 | ToBeAnnounced, 19 | Announced, 20 | InCinemas, 21 | Released, 22 | Deleted, 23 | } 24 | -------------------------------------------------------------------------------- /src/arr/sonarr/api.rs: -------------------------------------------------------------------------------- 1 | use std::fmt::Debug; 2 | 3 | use color_eyre::{eyre::eyre, Result}; 4 | use serde::de::DeserializeOwned; 5 | 6 | use crate::{ 7 | config::{Config, Sonarr}, 8 | utils::{create_api_error_message, create_param_string}, 9 | }; 10 | 11 | pub async fn get(path: &str, params: Option>, is_4k: bool) -> Result 12 | where 13 | T: DeserializeOwned + Debug, 14 | { 15 | let config: &Sonarr; 16 | if is_4k { 17 | config = match &Config::global().sonarr_4k { 18 | Some(sonarr) => sonarr, 19 | None => { 20 | return Err(eyre!( 21 | "Tried to access Sonarr config, even though it is not defined." 22 | )) 23 | } 24 | }; 25 | } else { 26 | config = match &Config::global().sonarr { 27 | Some(sonarr) => sonarr, 28 | None => { 29 | return Err(eyre!( 30 | "Tried to access Sonarr config, even though it is not defined." 31 | )) 32 | } 33 | }; 34 | } 35 | let client = reqwest::Client::new(); 36 | let params = create_param_string(params); 37 | 38 | let response = client 39 | .get(format!("{}/api/v3{}?{}", config.url, path, params)) 40 | .header("X-Api-Key", &config.api_key) 41 | .send() 42 | .await?; 43 | 44 | if !(response.status().as_u16() >= 200 && response.status().as_u16() < 300) { 45 | let code = response.status().as_u16(); 46 | return Err(eyre!(create_api_error_message(code, path, "Sonarr"))); 47 | } 48 | 49 | let response = response.json().await?; 50 | 51 | Ok(response) 52 | } 53 | 54 | pub async fn delete(path: &str, params: Option>) -> Result<()> { 55 | let config = match &Config::global().sonarr { 56 | Some(sonarr) => sonarr, 57 | None => { 58 | return Err(eyre!( 59 | "Tried to access Sonarr config, even though it is not defined." 60 | )) 61 | } 62 | }; 63 | let client = reqwest::Client::new(); 64 | let params = create_param_string(params); 65 | 66 | client 67 | .delete(format!("{}/api/v3{}?{}", &config.url, path, params)) 68 | .header("X-Api-Key", &config.api_key) 69 | .send() 70 | .await?; 71 | 72 | Ok(()) 73 | } 74 | -------------------------------------------------------------------------------- /src/arr/sonarr/mod.rs: -------------------------------------------------------------------------------- 1 | mod api; 2 | mod responses; 3 | 4 | use color_eyre::Result; 5 | 6 | use self::responses::SeriesResource; 7 | pub use self::responses::SeriesStatus; 8 | 9 | pub async fn get_sonarr_data(id: i32, is_4k: bool) -> Result { 10 | let path = format!("/series/{}", id.to_string()); 11 | api::get(&path, None, is_4k).await 12 | } 13 | 14 | pub async fn remove_sonarr_data_and_files(sonarr_id: i32) -> Result<()> { 15 | let path = format!("/series/{}", sonarr_id.to_string()); 16 | let params = vec![("deleteFiles", "true"), ("addImportListExclusion", "false")]; 17 | api::delete(path.as_str(), Some(params)).await 18 | } 19 | -------------------------------------------------------------------------------- /src/arr/sonarr/responses.rs: -------------------------------------------------------------------------------- 1 | use serde::Deserialize; 2 | 3 | #[derive(Debug, Deserialize, Clone)] 4 | #[serde(rename_all = "camelCase")] 5 | pub struct SeriesResource { 6 | pub id: i32, 7 | pub title: Option, 8 | pub status: SeriesStatus, 9 | pub previous_airing: Option, 10 | pub next_airing: Option, 11 | pub statistics: SeriesStatisticsResource, 12 | pub seasons: Vec, 13 | } 14 | 15 | #[derive(Debug, Deserialize, Clone, Copy)] 16 | #[serde(rename_all = "snake_case")] 17 | pub enum SeriesStatus { 18 | Continuing, 19 | Ended, 20 | Upcoming, 21 | Deleted, 22 | } 23 | 24 | #[derive(Debug, Deserialize, Clone)] 25 | #[serde(rename_all = "camelCase")] 26 | pub struct SeriesStatisticsResource { 27 | pub season_count: i32, 28 | pub episode_file_count: i32, 29 | pub episode_count: i32, 30 | pub size_on_disk: i64, 31 | pub percent_of_episodes: f64, 32 | } 33 | 34 | #[derive(Debug, Deserialize, Clone)] 35 | #[serde(rename_all = "camelCase")] 36 | pub struct SeasonResource { 37 | pub season_number: i32, 38 | pub statistics: SeasonStatisticsResource, 39 | } 40 | 41 | #[derive(Debug, Deserialize, Clone)] 42 | #[serde(rename_all = "camelCase")] 43 | pub struct SeasonStatisticsResource { 44 | pub episode_count: i32, 45 | } 46 | -------------------------------------------------------------------------------- /src/config.rs: -------------------------------------------------------------------------------- 1 | use color_eyre::Result; 2 | use once_cell::sync::OnceCell; 3 | use serde::Deserialize; 4 | use std::fs; 5 | 6 | static INSTANCE: OnceCell = OnceCell::new(); 7 | #[derive(Debug, Deserialize)] 8 | pub struct Config { 9 | #[serde(default = "default_items_shown")] 10 | pub items_shown: usize, 11 | pub plex: Plex, 12 | pub overseerr: Overseerr, 13 | pub tautulli: Tautulli, 14 | pub sonarr: Option, 15 | pub sonarr_4k: Option, 16 | pub radarr: Option, 17 | pub radarr_4k: Option, 18 | pub ignored_users: Option>, 19 | } 20 | 21 | #[derive(Debug, Deserialize)] 22 | pub struct Plex { 23 | pub url: String, 24 | pub token: String, 25 | } 26 | 27 | #[derive(Debug, Deserialize)] 28 | pub struct Overseerr { 29 | pub url: String, 30 | pub api_key: String, 31 | } 32 | 33 | #[derive(Debug, Deserialize)] 34 | pub struct Tautulli { 35 | pub url: String, 36 | pub api_key: String, 37 | } 38 | 39 | #[derive(Debug, Deserialize)] 40 | pub struct Sonarr { 41 | pub api_key: String, 42 | pub url: String, 43 | } 44 | 45 | #[derive(Debug, Deserialize)] 46 | pub struct Radarr { 47 | pub api_key: String, 48 | pub url: String, 49 | } 50 | 51 | impl Config { 52 | pub fn global() -> &'static Config { 53 | INSTANCE.get().expect("Config has not been initialized.") 54 | } 55 | 56 | pub fn read_conf() -> Result<()> { 57 | if let Some(_) = INSTANCE.get() { 58 | return Ok(()); 59 | } 60 | 61 | let reader = fs::File::open("config.yaml")?; 62 | let mut conf: Config = serde_yaml::from_reader(reader)?; 63 | 64 | Self::clean_urls(&mut conf); 65 | 66 | INSTANCE 67 | .set(conf) 68 | .expect("Config has already been initialized."); 69 | Ok(()) 70 | } 71 | 72 | fn clean_urls(conf: &mut Config) { 73 | clean_url(&mut conf.overseerr.url); 74 | clean_url(&mut conf.plex.url); 75 | clean_url(&mut conf.tautulli.url); 76 | 77 | if let Some(ref mut radarr) = conf.radarr { 78 | clean_url(&mut radarr.url); 79 | } 80 | 81 | if let Some(ref mut radarr) = conf.radarr_4k { 82 | clean_url(&mut radarr.url); 83 | } 84 | 85 | if let Some(ref mut sonarr) = conf.sonarr { 86 | clean_url(&mut sonarr.url); 87 | } 88 | 89 | if let Some(ref mut sonarr) = conf.sonarr_4k { 90 | clean_url(&mut sonarr.url); 91 | } 92 | } 93 | } 94 | 95 | fn default_items_shown() -> usize { 96 | 5 97 | } 98 | 99 | fn clean_url(url: &mut String) { 100 | if url.ends_with("/") { 101 | url.pop(); 102 | } 103 | } 104 | -------------------------------------------------------------------------------- /src/main.rs: -------------------------------------------------------------------------------- 1 | mod arguments; 2 | mod arr; 3 | mod config; 4 | mod media_item; 5 | mod overseerr; 6 | mod plex; 7 | mod shared; 8 | mod tautulli; 9 | mod utils; 10 | 11 | use color_eyre::{eyre::eyre, Report, Result}; 12 | use futures::future; 13 | use itertools::Itertools; 14 | use overseerr::MediaRequest; 15 | use shared::{Order, SortingOption, SortingValue}; 16 | use std::{io, process::Command}; 17 | use std::cmp::PartialEq; 18 | use arguments::Arguments; 19 | use config::Config; 20 | use dialoguer::MultiSelect; 21 | use media_item::{CompleteMediaItem, MediaItem}; 22 | 23 | use crate::{overseerr::ServerItem, utils::human_file_size}; 24 | 25 | #[tokio::main] 26 | async fn main() -> Result<()> { 27 | color_eyre::install()?; 28 | 29 | read_and_validate_config()?; 30 | 31 | Arguments::read_args()?; 32 | 33 | let deletion_items = get_deletion_items().await?; 34 | 35 | show_requests_result(&deletion_items)?; 36 | 37 | clear_screen()?; 38 | 39 | let sorted_requests = choose_sorting(deletion_items)?; 40 | 41 | let chosen_indexes = choose_items_to_delete(&sorted_requests)?; 42 | 43 | delete_chosen_items(sorted_requests, chosen_indexes).await?; 44 | 45 | Ok(()) 46 | } 47 | 48 | fn read_and_validate_config() -> Result<()> { 49 | if let Err(err) = Config::read_conf() { 50 | return Err(eyre!("Failed to read the config, with the following error: {}.\nPlease make sure all fields are filled.", err)); 51 | } 52 | 53 | let config = Config::global(); 54 | if let (None, None) = (&config.radarr, &config.sonarr) { 55 | return Err(eyre!("You have not configured Sonarr or Radarr. Application can't continue without at least one of these.")); 56 | } 57 | 58 | Ok(()) 59 | } 60 | 61 | async fn get_deletion_items() -> Result> { 62 | println!("Gathering all required data from your services.\nDepending on the amount of data and your connection speed, this could take a while..."); 63 | 64 | let all_items = Arguments::get_args().all_media; 65 | 66 | let mut media_items = MediaRequest::get_all() 67 | .await? 68 | .into_iter() 69 | .map(MediaItem::from_request) 70 | .collect_vec(); 71 | 72 | // This is done by merging the two lists, because Overseerr does not send who requested the media along 73 | // when getting all of the media on the server. Neither does Overseerr have an endpoint for getting all 74 | // requests associated with an item. 75 | // 76 | // If that was allowed, this could be made much nicer and more performance friendly. 77 | if all_items { 78 | let mut not_requested_media_items = ServerItem::get_all() 79 | .await? 80 | .into_iter() 81 | .map(MediaItem::from_server_item) 82 | .collect_vec(); 83 | 84 | media_items.append(&mut not_requested_media_items); 85 | 86 | media_items.sort_by(|item1, item2| { 87 | (&item1.rating_key, !item1.request.is_some()) 88 | .cmp(&(&item2.rating_key, !item2.request.is_some())) 89 | }); 90 | media_items.dedup_by(|item1, item2| item1.rating_key == item2.rating_key); 91 | } 92 | 93 | let futures = media_items 94 | .into_iter() 95 | .filter(|i| i.is_available() && i.has_manager_active() && !i.user_ignored()) 96 | .map(|item| { 97 | tokio::spawn(async move { 98 | let item = item.into_complete_media().await?; 99 | 100 | Ok::(item) 101 | }) 102 | }); 103 | 104 | let mut errors: Vec = Vec::new(); 105 | 106 | let complete_items = future::try_join_all(futures) 107 | .await? 108 | .into_iter() 109 | .filter_map(|f| match f { 110 | Ok(item) => Some(item), 111 | Err(err) => { 112 | errors.push(err); 113 | None 114 | } 115 | }) 116 | .unique_by(|item| item.title.clone()) 117 | .sorted_by(|item1, item2| item1.title.cmp(&item2.title)) 118 | .collect(); 119 | 120 | show_potential_request_errors(errors)?; 121 | 122 | Ok(complete_items) 123 | } 124 | 125 | fn show_potential_request_errors(errs: Vec) -> Result<()> { 126 | if errs.len() == 0 { 127 | return Ok(()); 128 | } 129 | 130 | println!("You got {} errors while gathering data. Press y to show them, or any other input to continue with the errored items ignored.", errs.len()); 131 | let input = get_user_input()?; 132 | if !input.starts_with("y") { 133 | return Ok(()); 134 | } 135 | 136 | errs.iter().enumerate().for_each(|(i, err)| { 137 | println!("Error {} was {}", i, err); 138 | print_line(); 139 | }); 140 | 141 | println!("Do you want to see the full stack traces? Press y. Otherwise continuing to deletion screen with errored items ignored."); 142 | let inp = get_user_input()?; 143 | if !inp.starts_with("y") { 144 | return Ok(()); 145 | } 146 | 147 | errs.iter().enumerate().for_each(|(i, err)| { 148 | println!("Error {} was {:?}", i + 1, err); 149 | print_line(); 150 | }); 151 | 152 | wait(Some( 153 | "Press enter to continue to deletion screen with errored items ignored.", 154 | ))?; 155 | 156 | Ok(()) 157 | } 158 | 159 | fn show_requests_result(requests: &Vec) -> Result<()> { 160 | if requests.len() == 0 { 161 | println!("You do not seem to have any valid requests, with data available."); 162 | println!("Are you sure all your requests are available and downloaded? Or some data was unable to be acquired from other services."); 163 | println!("Either try again later, or look over your requests."); 164 | 165 | println!(); 166 | wait(None)?; 167 | std::process::exit(0); 168 | } 169 | 170 | Ok(()) 171 | } 172 | 173 | fn choose_items_to_delete(requests: &Vec) -> Result> { 174 | clear_screen()?; 175 | 176 | let items_to_show = Config::global().items_shown; 177 | let chosen: Vec = MultiSelect::new() 178 | .with_prompt("Choose what media to delete (SPACE to select, ENTER to confirm selection)") 179 | .max_length(items_to_show) 180 | .items(requests) 181 | .interact()?; 182 | 183 | if chosen.len() == 0 { 184 | println!("No items selected. Exiting..."); 185 | std::process::exit(0); 186 | } 187 | 188 | clear_screen()?; 189 | 190 | verify_chosen(&requests, &chosen)?; 191 | 192 | Ok(chosen) 193 | } 194 | 195 | fn choose_sorting(mut requests: Vec) -> Result> { 196 | clear_screen()?; 197 | let args = Arguments::get_args(); 198 | 199 | let sort = match args.sorting { 200 | Some(ref sort) => sort.clone(), 201 | None => choose_sorting_dialogue()?, 202 | }; 203 | 204 | match sort.sorting_value { 205 | SortingValue::Name => (), 206 | SortingValue::Size => requests.sort_by_key(|req| req.get_disk_size()), 207 | SortingValue::Type => requests.sort_by_key(|req| req.media_type.clone()), 208 | SortingValue::RequestedDate => requests.sort_by_key(|req| req.get_requested_date()), 209 | } 210 | 211 | // Reverse if sorting direction is descending 212 | if sort.sorting_direction == Order::Desc { 213 | requests.reverse(); 214 | } 215 | 216 | Ok(requests) 217 | } 218 | 219 | fn choose_sorting_dialogue() -> Result { 220 | loop { 221 | println!("Choose sorting method:"); 222 | println!("Name - Ascending: n (or just enter, it's the default)"); 223 | println!("Name - Descending: nd"); 224 | println!("Size - Descending: s"); 225 | println!("Size - Ascending: sa"); 226 | println!("Type - Descending: t"); 227 | println!("Requested Date - Ascending: r"); 228 | println!("Requested Date - Descending: rd"); 229 | 230 | let input = get_user_input()?; 231 | 232 | if let Ok(sort) = SortingOption::from_str(&input) { 233 | return Ok(sort); 234 | } 235 | if input.eq("") { 236 | return Ok(SortingOption::default()); 237 | } 238 | } 239 | } 240 | 241 | fn verify_chosen(requests: &Vec, chosen: &Vec) -> Result<()> { 242 | let total_size: String = human_file_size( 243 | chosen 244 | .iter() 245 | .filter_map(|selection| { 246 | if let Some(media_item) = requests.get(*selection) { 247 | Some(media_item.get_disk_size()) 248 | } else { 249 | None 250 | } 251 | }) 252 | .sum(), 253 | ); 254 | 255 | println!( 256 | "Are you sure you want to delete the following items ({}):", 257 | total_size 258 | ); 259 | chosen.iter().for_each(|selection| { 260 | if let Some(media_item) = requests.get(*selection) { 261 | let media_type = media_item.media_type; 262 | println!("- {} - {}", &media_item.title, media_type.to_string()); 263 | } else { 264 | println!("- Unknown item"); 265 | } 266 | }); 267 | 268 | println!("\ny/n:"); 269 | let user_input = get_user_input()?; 270 | 271 | if !user_input.starts_with("y") { 272 | println!("Cancelling..."); 273 | std::process::exit(0); 274 | } 275 | 276 | Ok(()) 277 | } 278 | 279 | async fn delete_chosen_items( 280 | mut requests: Vec, 281 | chosen: Vec, 282 | ) -> Result<()> { 283 | let mut errs: Vec<(String, Report)> = Vec::new(); 284 | 285 | for selection in chosen.into_iter().rev() { 286 | let media_item = requests.swap_remove(selection); 287 | let title = media_item.title.clone(); 288 | if let Err(err) = media_item.remove_from_server().await { 289 | errs.push((title, err)); 290 | } 291 | } 292 | 293 | // If there are no errors, return early 294 | if errs.is_empty() { 295 | return Ok(()); 296 | } 297 | 298 | // Log errors if there are any 299 | println!("Had some errors deleting items:\n"); 300 | errs.iter().for_each(|(title, err)| { 301 | println!( 302 | "Got the following error while deleting {}: {}", 303 | title, err 304 | ); 305 | print_line(); 306 | }); 307 | 308 | wait(None)?; 309 | Ok(()) 310 | } 311 | 312 | fn clear_screen() -> Result<()> { 313 | if cfg!(target_os = "windows") { 314 | Command::new("cmd").arg("/C").arg("cls").status()?; 315 | Ok(()) 316 | } else { 317 | Command::new("clear").status()?; 318 | Ok(()) 319 | } 320 | } 321 | 322 | fn get_user_input() -> Result { 323 | let mut user_input = String::new(); 324 | let stdin = io::stdin(); 325 | 326 | stdin.read_line(&mut user_input)?; 327 | user_input = user_input.to_lowercase(); 328 | 329 | Ok(user_input 330 | .strip_suffix("\r\n") 331 | .or(user_input.strip_suffix("\n")) 332 | .unwrap_or(&user_input) 333 | .to_string()) 334 | } 335 | 336 | fn wait(custom_msg: Option<&str>) -> Result<()> { 337 | if let Some(msg) = custom_msg { 338 | println!("{}", msg); 339 | } else { 340 | println!("Press enter to continue."); 341 | } 342 | get_user_input()?; 343 | Ok(()) 344 | } 345 | 346 | fn print_line() { 347 | println!("-----------------------------------------------------------------------------"); 348 | } 349 | -------------------------------------------------------------------------------- /src/media_item.rs: -------------------------------------------------------------------------------- 1 | use color_eyre::{eyre::eyre, owo_colors::OwoColorize, Result}; 2 | use std::fmt::{Debug, Display}; 3 | use chrono::{DateTime, Utc}; 4 | use tokio::try_join; 5 | 6 | use crate::{ 7 | arr::{self, ArrData}, 8 | config::Config, 9 | overseerr::{MediaRequest, MediaStatus, ServerItem}, 10 | plex::PlexData, 11 | shared::MediaType, 12 | tautulli::{self, WatchHistory}, 13 | utils::human_file_size, 14 | }; 15 | 16 | #[derive(Debug)] 17 | pub struct MediaItem { 18 | pub title: Option, 19 | pub rating_key: Option, 20 | manager_id: Option, 21 | manager_4k_id: Option, 22 | pub media_type: MediaType, 23 | media_status: MediaStatus, 24 | pub request: Option, 25 | } 26 | 27 | impl MediaItem { 28 | pub fn from_request(request: MediaRequest) -> Self { 29 | Self { 30 | title: None, 31 | rating_key: request.rating_key.clone(), 32 | manager_id: request.manager_id, 33 | manager_4k_id: request.manager_4k_id, 34 | media_type: request.media_type, 35 | media_status: request.media_status, 36 | request: Some(request), 37 | } 38 | } 39 | 40 | pub fn from_server_item(item: ServerItem) -> Self { 41 | Self { 42 | title: None, 43 | rating_key: Some(item.rating_key), 44 | manager_id: item.manager_id, 45 | manager_4k_id: item.manager_id_4k, 46 | media_type: item.media_type, 47 | media_status: item.media_status, 48 | request: None, 49 | } 50 | } 51 | 52 | pub async fn into_complete_media(self) -> Result { 53 | let metadata = self.retrieve_metadata(); 54 | let history = self.retrieve_history(); 55 | let data = self.retrieve_arr_data(); 56 | 57 | let (details, history, (arr_data, arr_4k_data)) = try_join!(metadata, history, data)?; 58 | 59 | Ok(CompleteMediaItem { 60 | title: details.title.clone(), 61 | media_type: self.media_type, 62 | request: self.request, 63 | history, 64 | arr_data, 65 | arr_4k_data, 66 | }) 67 | } 68 | 69 | pub fn is_available(&self) -> bool { 70 | match &self.media_status { 71 | MediaStatus::Available | MediaStatus::PartiallyAvailable => true, 72 | _ => false, 73 | } 74 | } 75 | 76 | pub fn has_manager_active(&self) -> bool { 77 | match &self.media_type { 78 | MediaType::Movie => arr::movie_manger_active() || arr::movie_4k_manager_active(), 79 | MediaType::Tv => arr::tv_manager_active() || arr::tv_4k_manager_active(), 80 | } 81 | } 82 | 83 | pub fn user_ignored(&self) -> bool { 84 | let request = match self.request { 85 | None => return false, 86 | Some(ref request) => request, 87 | }; 88 | 89 | let ignored_users = match Config::global().ignored_users { 90 | None => return false, 91 | Some(ref users) => users, 92 | }; 93 | 94 | if ignored_users.contains(&request.requested_by) 95 | // .iter() 96 | // .any(|user| user.eq(&request.requested_by)) 97 | { 98 | true 99 | } else { 100 | false 101 | } 102 | } 103 | 104 | async fn retrieve_history(&self) -> Result { 105 | let rating_key = match self.rating_key { 106 | Some(ref rating_key) => rating_key, 107 | None => { 108 | return Err(eyre!( 109 | "No rating key was found for request. Unable to gather watch history from Tautulli." 110 | )) 111 | } 112 | }; 113 | 114 | tautulli::get_item_watches(rating_key, &self.media_type).await 115 | } 116 | 117 | async fn retrieve_metadata(&self) -> Result { 118 | let rating_key = match self.rating_key { 119 | Some(ref rating_key) => rating_key, 120 | None => { 121 | return Err(eyre!( 122 | "No rating key was found for request. Unable to gather metadata from Plex." 123 | )) 124 | } 125 | }; 126 | 127 | PlexData::get_data(rating_key, self.media_type).await 128 | } 129 | 130 | async fn retrieve_arr_data(&self) -> Result<(Option, Option)> { 131 | match (self.manager_id, self.manager_4k_id) { 132 | (Some(id), Some(id_4k)) => { 133 | let data_standard = ArrData::get_data(self.media_type, id); 134 | let data_4k = ArrData::get_4k_data(self.media_type, id_4k); 135 | 136 | let (data_standard, data_4k) = try_join!(data_standard, data_4k)?; 137 | 138 | Ok((Some(data_standard), Some(data_4k))) 139 | } 140 | (Some(id), _) => Ok((Some(ArrData::get_data(self.media_type, id).await?), None)), 141 | (None, Some(id_4k)) => Ok(( 142 | Some(ArrData::get_4k_data(self.media_type, id_4k).await?), 143 | None, 144 | )), 145 | (None, None) => Err(eyre!( 146 | "No *arr id was found for request. Unable to gather file data." 147 | )), 148 | } 149 | } 150 | } 151 | 152 | #[derive(Debug)] 153 | pub struct CompleteMediaItem { 154 | pub title: String, 155 | pub media_type: MediaType, 156 | request: Option, 157 | history: WatchHistory, 158 | arr_data: Option, 159 | arr_4k_data: Option, 160 | } 161 | 162 | impl CompleteMediaItem { 163 | pub async fn remove_from_server(self) -> Result<()> { 164 | if let Some(request) = self.request { 165 | request.remove_request().await?; 166 | } 167 | 168 | if let Some(arr_data) = self.arr_data { 169 | arr_data.remove_data().await?; 170 | } 171 | 172 | if let Some(arr_data) = self.arr_4k_data { 173 | arr_data.remove_data().await?; 174 | } 175 | 176 | Ok(()) 177 | } 178 | 179 | pub fn get_requested_date(&self) -> Option> { 180 | self.request.as_ref().map(|request| request.created_at) 181 | } 182 | 183 | pub fn get_disk_size(&self) -> i64 { 184 | match (self.arr_data.as_ref(), self.arr_4k_data.as_ref()) { 185 | (Some(arr_data), None) => arr_data.get_disk_size(), 186 | (None, Some(arr_data)) => arr_data.get_disk_size(), 187 | (Some(arr_data), Some(arr_data_4k)) => { 188 | arr_data.get_disk_size() + arr_data_4k.get_disk_size() 189 | } 190 | (None, None) => panic!("Tried to get size of none existant object!"), 191 | } 192 | } 193 | 194 | fn print_arr_data(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result { 195 | match (self.arr_data.as_ref(), self.arr_4k_data.as_ref()) { 196 | (Some(arr_data), None) => write!(f, "\n {}", arr_data)?, 197 | (None, Some(arr_data_4k)) => write!(f, "\n {}", arr_data_4k)?, 198 | (Some(arr_data), Some(_)) => write!(f, "\n {}", arr_data)?, 199 | (None, None) => { 200 | panic!("Tried to write non-existant item") 201 | } 202 | } 203 | 204 | Ok(()) 205 | } 206 | 207 | fn status_4k(&self) -> &str { 208 | match (self.arr_data.as_ref(), self.arr_4k_data.as_ref()) { 209 | (Some(_), None) => "", 210 | (None, Some(_)) => "Only 4K ", 211 | (Some(_), Some(_)) => "4K ", 212 | (None, None) => { 213 | panic!("Tried to write non-existant item") 214 | } 215 | } 216 | } 217 | } 218 | 219 | impl Display for CompleteMediaItem { 220 | fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { 221 | write!( 222 | f, 223 | "{}{} {} {}.", 224 | self.status_4k().yellow(), 225 | self.media_type.to_string().blue(), 226 | self.title.green(), 227 | human_file_size(self.get_disk_size()).red() 228 | )?; 229 | if let Some(ref request) = self.request { 230 | write!(f, " {}", request)?; 231 | } 232 | 233 | self.print_arr_data(f)?; 234 | 235 | write!(f, "\n {}", self.history)?; 236 | 237 | write!(f, "\n") 238 | } 239 | } 240 | -------------------------------------------------------------------------------- /src/overseerr/api.rs: -------------------------------------------------------------------------------- 1 | use color_eyre::{eyre::eyre, Result}; 2 | use serde::de::DeserializeOwned; 3 | 4 | use super::responses::RequestResponse; 5 | use crate::{ 6 | config::Config, 7 | utils::{create_api_error_message, create_param_string}, 8 | }; 9 | 10 | pub async fn get(path: &str, params: Option>) -> Result> 11 | where 12 | T: DeserializeOwned, 13 | { 14 | let client = reqwest::Client::new(); 15 | let config = &Config::global().overseerr; 16 | let response = client 17 | .get(format!( 18 | "{}/api/v1{}?take=100&{}", 19 | &config.url, 20 | path, 21 | &create_param_string(params) 22 | )) 23 | .header("X-API-Key", &config.api_key) 24 | .send() 25 | .await?; 26 | 27 | if !(response.status().as_u16() >= 200 && response.status().as_u16() < 300) { 28 | let code = response.status().as_u16(); 29 | return Err(eyre!(create_api_error_message(code, path, "Overseerr"))); 30 | } 31 | 32 | let mut response_data: RequestResponse = response.json().await?; 33 | 34 | let page_size = response_data.page_info.page_size; 35 | for page in 1..response_data.page_info.pages { 36 | let mut page_data: RequestResponse = client 37 | .get(format!( 38 | "{}/api/v1{}?take={}&skip={}", 39 | &config.url, 40 | path, 41 | page_size, 42 | page_size * page 43 | )) 44 | .header("X-API-Key", &config.api_key) 45 | .send() 46 | .await? 47 | .json() 48 | .await?; 49 | 50 | response_data.results.append(&mut page_data.results); 51 | } 52 | 53 | Ok(response_data) 54 | } 55 | 56 | pub async fn delete(path: &str) -> Result<()> { 57 | let config = &Config::global().overseerr; 58 | let client = reqwest::Client::new(); 59 | 60 | client 61 | .delete(format!("{}/api/v1{}", &config.url, path)) 62 | .header("X-API-Key", &config.api_key) 63 | .send() 64 | .await?; 65 | 66 | Ok(()) 67 | } 68 | -------------------------------------------------------------------------------- /src/overseerr/mod.rs: -------------------------------------------------------------------------------- 1 | mod api; 2 | mod responses; 3 | 4 | use chrono::prelude::*; 5 | use color_eyre::{eyre::eyre, owo_colors::OwoColorize, Result}; 6 | use std::fmt::Display; 7 | 8 | use self::responses::MediaResponse; 9 | use crate::{ 10 | overseerr::responses::{MediaRequestResponse, RequestResponse}, 11 | shared::MediaType, 12 | }; 13 | pub use responses::MediaStatus; 14 | 15 | #[derive(Debug)] 16 | pub struct MediaRequest { 17 | pub id: u32, 18 | pub media_id: u32, 19 | pub rating_key: Option, 20 | pub manager_id: Option, 21 | pub manager_4k_id: Option, 22 | pub created_at: DateTime, 23 | pub updated_at: DateTime, 24 | pub requested_by: String, 25 | pub media_status: responses::MediaStatus, 26 | pub media_type: MediaType, 27 | } 28 | 29 | impl MediaRequest { 30 | pub async fn remove_request(self) -> Result<()> { 31 | let path = format!("/media/{}", self.media_id); 32 | api::delete(&path).await?; 33 | 34 | Ok(()) 35 | } 36 | 37 | pub async fn get_all() -> Result> { 38 | let response_data: RequestResponse = 39 | api::get("/request", None).await?; 40 | 41 | let requests: Vec> = response_data 42 | .results 43 | .into_iter() 44 | .map(Self::from_response) 45 | .collect(); 46 | 47 | let requests = requests 48 | .into_iter() 49 | .filter_map(Result::ok) 50 | .collect::>(); 51 | 52 | Ok(requests) 53 | } 54 | 55 | fn from_response(response: MediaRequestResponse) -> Result { 56 | let created_at = DateTime::parse_from_rfc3339(&response.created_at)?; 57 | let updated_at = match &response.updated_at { 58 | Some(updated_at) => DateTime::parse_from_rfc3339(updated_at)?, 59 | None => created_at, 60 | }; 61 | 62 | let requested_by = match &response.requested_by.display_name { 63 | Some(display_name) => display_name.clone(), 64 | None => response.requested_by.email.clone(), 65 | }; 66 | 67 | Ok(MediaRequest { 68 | id: response.id, 69 | media_id: response.media.id, 70 | rating_key: response.media.rating_key, 71 | manager_id: response.media.external_service_id, 72 | manager_4k_id: response.media.external_service_id_4k, 73 | created_at: created_at.with_timezone(&Utc), 74 | updated_at: updated_at.with_timezone(&Utc), 75 | media_status: response.media.status, 76 | media_type: response.media.media_type, 77 | requested_by, 78 | }) 79 | } 80 | } 81 | 82 | impl Display for MediaRequest { 83 | fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { 84 | write!( 85 | f, 86 | "Requested by {} at {}, which is {} days ago.", 87 | self.requested_by.yellow(), 88 | self.updated_at.format("%d/%m/%Y").blue(), 89 | Utc::now() 90 | .signed_duration_since(self.updated_at) 91 | .num_days() 92 | .red() 93 | ) 94 | } 95 | } 96 | 97 | #[derive(Debug)] 98 | pub struct ServerItem { 99 | pub id: u32, 100 | pub rating_key: String, 101 | pub manager_id: Option, 102 | pub manager_id_4k: Option, 103 | pub created_at: DateTime, 104 | pub updated_at: DateTime, 105 | pub media_status: responses::MediaStatus, 106 | pub media_type: MediaType, 107 | } 108 | 109 | impl ServerItem { 110 | pub async fn get_all() -> Result> { 111 | let response_data: RequestResponse = 112 | api::get("/media", Some(vec![("filter", "available")])).await?; 113 | 114 | let requests: Vec> = response_data 115 | .results 116 | .into_iter() 117 | .map(Self::from_response) 118 | .collect(); 119 | 120 | let requests = requests 121 | .into_iter() 122 | .filter_map(Result::ok) 123 | .collect::>(); 124 | 125 | Ok(requests) 126 | } 127 | 128 | fn from_response(response: MediaResponse) -> Result { 129 | let created_at = DateTime::parse_from_rfc3339(&response.created_at)?; 130 | let updated_at = match &response.updated_at { 131 | Some(updated_at) => DateTime::parse_from_rfc3339(updated_at)?, 132 | None => created_at, 133 | }; 134 | 135 | Ok(Self { 136 | id: response.id, 137 | rating_key: match response.rating_key { 138 | Some(rating_key) => rating_key, 139 | None => { 140 | return Err(eyre!( 141 | "No rating key found for item {} of type {}.", 142 | response.id, 143 | response.media_type 144 | )) 145 | } 146 | }, 147 | manager_id: response.external_service_id, 148 | manager_id_4k: response.external_service_id_4k, 149 | created_at: created_at.with_timezone(&Utc), 150 | updated_at: updated_at.with_timezone(&Utc), 151 | media_status: response.status, 152 | media_type: response.media_type, 153 | }) 154 | } 155 | } 156 | -------------------------------------------------------------------------------- /src/overseerr/responses.rs: -------------------------------------------------------------------------------- 1 | use color_eyre::owo_colors::OwoColorize; 2 | use serde::Deserialize; 3 | use serde_repr::Deserialize_repr; 4 | use std::fmt::Display; 5 | use openssl::pkey::Public; 6 | use crate::shared::MediaType; 7 | 8 | #[derive(Debug, Deserialize)] 9 | #[serde(rename_all = "camelCase")] 10 | pub struct RequestResponse { 11 | pub page_info: PageInfo, 12 | pub results: Vec, 13 | } 14 | 15 | #[derive(Debug, Deserialize)] 16 | #[serde(rename_all = "camelCase")] 17 | pub struct PageInfo { 18 | pub page: u32, 19 | pub pages: u32, 20 | pub results: u32, 21 | pub page_size: u32, 22 | } 23 | 24 | #[derive(Debug, Deserialize)] 25 | #[serde(rename_all = "camelCase")] 26 | pub struct UserResponse { 27 | pub id: u32, 28 | pub email: String, 29 | pub display_name: Option, 30 | } 31 | 32 | #[derive(Debug, Deserialize)] 33 | #[serde(rename_all = "camelCase")] 34 | pub struct MediaResponse { 35 | pub id: u32, 36 | pub external_service_id: Option, 37 | pub external_service_id_4k: Option, 38 | pub rating_key: Option, 39 | pub status: MediaStatus, 40 | pub media_type: MediaType, 41 | pub created_at: String, 42 | pub updated_at: Option, 43 | } 44 | 45 | #[derive(Debug, Deserialize_repr, Clone, Copy)] 46 | #[repr(u8)] 47 | pub enum MediaStatus { 48 | Unknown = 1, 49 | Pending, 50 | Processing, 51 | PartiallyAvailable, 52 | Available, 53 | } 54 | 55 | impl Display for MediaStatus { 56 | fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { 57 | match self { 58 | Self::Unknown => write!(f, "{}", "Unknown".red().to_string()), 59 | Self::Pending => write!(f, "{}", "Pending".yellow().to_string()), 60 | Self::Processing => write!(f, "{}", "Processing".yellow().to_string()), 61 | Self::PartiallyAvailable => write!(f, "{}", "Partially Available".blue().to_string()), 62 | Self::Available => write!(f, "{}", "Available".green().to_string()), 63 | } 64 | } 65 | } 66 | 67 | #[derive(Debug, Deserialize)] 68 | #[serde(rename_all = "camelCase")] 69 | pub struct MediaRequestResponse { 70 | pub id: u32, 71 | pub media: MediaResponse, 72 | pub created_at: String, 73 | pub updated_at: Option, 74 | pub requested_by: UserResponse, 75 | } 76 | -------------------------------------------------------------------------------- /src/plex/api.rs: -------------------------------------------------------------------------------- 1 | use color_eyre::Result; 2 | use serde::de::DeserializeOwned; 3 | 4 | use crate::{ 5 | config::Config, 6 | utils::{create_api_error_message, create_param_string}, 7 | }; 8 | use color_eyre::eyre::eyre; 9 | 10 | pub async fn get(path: &str, params: Option>) -> Result 11 | where 12 | T: DeserializeOwned, 13 | { 14 | let config = &Config::global().plex; 15 | let client = reqwest::Client::new(); 16 | let params = create_param_string(params); 17 | 18 | let response = client 19 | .get(format!( 20 | "{}{}?X-Plex-Token={}&{}", 21 | config.url, path, config.token, params 22 | )) 23 | .send() 24 | .await?; 25 | 26 | if !(response.status().as_u16() >= 200 && response.status().as_u16() < 300) { 27 | let code = response.status().as_u16(); 28 | return Err(eyre!(create_api_error_message(code, path, "Plex"))); 29 | } 30 | 31 | let response_text = response.text().await?; 32 | let parsed_response: T = serde_xml_rs::from_str(&response_text)?; 33 | 34 | Ok(parsed_response) 35 | } 36 | -------------------------------------------------------------------------------- /src/plex/mod.rs: -------------------------------------------------------------------------------- 1 | mod api; 2 | mod responses; 3 | 4 | use crate::{plex::responses::MovieData, shared::MediaType}; 5 | 6 | use self::responses::TvData; 7 | 8 | use color_eyre::Result; 9 | 10 | pub struct PlexData { 11 | pub title: String, 12 | } 13 | 14 | impl PlexData { 15 | pub async fn get_data(rating_key: &str, media_type: MediaType) -> Result { 16 | let path = format!("/library/metadata/{}", rating_key); 17 | match media_type { 18 | MediaType::Movie => { 19 | let raw_plex_data: MovieData = api::get(&path, None).await?; 20 | 21 | Ok(Self { 22 | title: raw_plex_data.video.title, 23 | }) 24 | } 25 | MediaType::Tv => { 26 | let raw_plex_data: TvData = api::get(&path, None).await?; 27 | 28 | Ok(Self { 29 | title: raw_plex_data.directory.title, 30 | }) 31 | } 32 | } 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /src/plex/responses.rs: -------------------------------------------------------------------------------- 1 | use serde::Deserialize; 2 | 3 | #[derive(Debug, Deserialize)] 4 | pub struct TvData { 5 | #[serde(rename = "Directory")] 6 | pub directory: Directory, 7 | } 8 | 9 | #[derive(Debug, Deserialize)] 10 | pub struct Directory { 11 | pub title: String, 12 | } 13 | 14 | #[derive(Debug, Deserialize)] 15 | pub struct MovieData { 16 | #[serde(rename = "Video")] 17 | pub video: Video, 18 | } 19 | 20 | #[derive(Debug, Deserialize)] 21 | pub struct Video { 22 | pub title: String, 23 | } 24 | -------------------------------------------------------------------------------- /src/shared.rs: -------------------------------------------------------------------------------- 1 | use color_eyre::{eyre::eyre, Result}; 2 | use serde::Deserialize; 3 | use std::fmt::Display; 4 | 5 | #[derive(Debug, Deserialize, Clone, Copy, PartialEq, Eq, PartialOrd, Ord)] 6 | #[serde(rename_all = "camelCase")] 7 | pub enum MediaType { 8 | Movie, 9 | Tv, 10 | } 11 | 12 | impl Display for MediaType { 13 | fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { 14 | match self { 15 | Self::Movie => write!(f, "Movie"), 16 | Self::Tv => write!(f, "TV"), 17 | } 18 | } 19 | } 20 | 21 | #[derive(Debug, Clone, Copy)] 22 | pub enum Order { 23 | Desc, 24 | Asc, 25 | } 26 | 27 | impl PartialEq for Order { 28 | fn eq(&self, other: &Self) -> bool { 29 | match (self, other) { 30 | (Order::Asc, Order::Asc) => true, 31 | (Order::Desc, Order::Desc) => true, 32 | _ => false, 33 | } 34 | } 35 | } 36 | 37 | #[derive(Debug, Clone, Copy)] 38 | pub enum SortingValue { 39 | Name, 40 | Size, 41 | Type, 42 | RequestedDate, 43 | } 44 | 45 | #[derive(Debug, Clone)] 46 | pub struct SortingOption { 47 | pub sorting_value: SortingValue, 48 | pub sorting_direction: Order, 49 | } 50 | 51 | impl Default for SortingOption { 52 | fn default() -> Self { 53 | SortingOption { 54 | sorting_value: SortingValue::Name, 55 | sorting_direction: Order::Asc, 56 | } 57 | } 58 | } 59 | 60 | impl SortingOption { 61 | pub fn from_str(s: &str) -> Result { 62 | match s { 63 | "nd" => { 64 | Ok(SortingOption { 65 | sorting_value: SortingValue::Name, 66 | sorting_direction: Order::Desc, 67 | }) 68 | } 69 | "n" => { 70 | Ok(SortingOption { 71 | sorting_value: SortingValue::Name, 72 | sorting_direction: Order::Asc, 73 | }) 74 | } 75 | "sa" => { 76 | Ok(SortingOption { 77 | sorting_value: SortingValue::Size, 78 | sorting_direction: Order::Asc, 79 | }) 80 | } 81 | "s" => { 82 | Ok(SortingOption { 83 | sorting_value: SortingValue::Size, 84 | sorting_direction: Order::Desc, 85 | }) 86 | } 87 | "t" => { 88 | Ok(SortingOption { 89 | sorting_value: SortingValue::Type, 90 | sorting_direction: Order::Desc, 91 | }) 92 | } 93 | "r" => { 94 | Ok(SortingOption { 95 | sorting_value: SortingValue::RequestedDate, 96 | sorting_direction: Order::Asc, 97 | }) 98 | } 99 | "rd" => { 100 | Ok(SortingOption { 101 | sorting_value: SortingValue::RequestedDate, 102 | sorting_direction: Order::Desc, 103 | }) 104 | } 105 | _ => Err(eyre!("Not a valid Sorting Option")), 106 | } 107 | } 108 | } 109 | -------------------------------------------------------------------------------- /src/tautulli/api.rs: -------------------------------------------------------------------------------- 1 | use color_eyre::{eyre::eyre, Result}; 2 | use serde::de::DeserializeOwned; 3 | 4 | use super::responses::ResponseObj; 5 | use crate::{ 6 | config::Config, 7 | utils::{create_api_error_message, create_param_string}, 8 | }; 9 | 10 | trait Response { 11 | type TypeParam; 12 | } 13 | 14 | pub async fn get_obj(command: &str, params: Option>) -> Result> 15 | where 16 | T: DeserializeOwned, 17 | { 18 | let config = &Config::global().tautulli; 19 | let client = reqwest::Client::new(); 20 | 21 | let cmd = command.to_string() + "&" + &create_param_string(params); 22 | 23 | let url = format!( 24 | "{}/api/v2?apikey={}&cmd={}", 25 | config.url, config.api_key, cmd 26 | ); 27 | 28 | let response = client.get(&url).send().await?; 29 | 30 | if !(response.status().as_u16() >= 200 && response.status().as_u16() < 300) { 31 | let code = response.status().as_u16(); 32 | return Err(eyre!(create_api_error_message(code, &url, "Tautulli"))); 33 | } 34 | 35 | let response = response.json().await?; 36 | 37 | Ok(response) 38 | } 39 | -------------------------------------------------------------------------------- /src/tautulli/mod.rs: -------------------------------------------------------------------------------- 1 | mod api; 2 | mod responses; 3 | 4 | use chrono::prelude::*; 5 | use color_eyre::{owo_colors::OwoColorize, Result}; 6 | use serde::de::DeserializeOwned; 7 | use std::{collections::BTreeMap, fmt::Display}; 8 | 9 | use self::responses::{History, HistoryItem, HistoryMovieItem}; 10 | use crate::{shared::MediaType, tautulli::responses::ResponseObj}; 11 | 12 | #[derive(Debug)] 13 | pub enum WatchHistory { 14 | Movie(ItemWatches), 15 | TvShow(ItemWatches), 16 | } 17 | 18 | impl WatchHistory { 19 | fn from_user_watches( 20 | user_watches: BTreeMap<&String, &HistoryItem>, 21 | media_type: &MediaType, 22 | rating_key: &str, 23 | ) -> Self { 24 | match media_type { 25 | MediaType::Movie => WatchHistory::create_movie_history(user_watches, rating_key), 26 | MediaType::Tv => WatchHistory::create_tv_history(user_watches, rating_key), 27 | } 28 | } 29 | 30 | fn create_movie_history( 31 | user_watches: BTreeMap<&String, &HistoryItem>, 32 | rating_key: &str, 33 | ) -> Self { 34 | let watches = user_watches 35 | .iter() 36 | .map(|(user, movie_watch)| UserMovieWatch { 37 | display_name: user.to_string(), 38 | last_watched: unix_seconds_to_date(movie_watch.date).expect(&format!( 39 | "Failed to parse unix time for rating key {}", 40 | rating_key 41 | )), 42 | progress: movie_watch.percent_complete, 43 | }) 44 | .collect(); 45 | 46 | WatchHistory::Movie(watches) 47 | } 48 | 49 | fn create_tv_history(user_watches: BTreeMap<&String, &HistoryItem>, rating_key: &str) -> Self { 50 | let watches = user_watches 51 | .iter() 52 | .map(|(user, tv_watch)| UserEpisodeWatch { 53 | display_name: user.to_string(), 54 | last_watched: unix_seconds_to_date(tv_watch.date).expect(&format!( 55 | "Failed to parse unix time for rating key {}", 56 | rating_key 57 | )), 58 | progress: tv_watch.percent_complete, 59 | season: tv_watch.parent_media_index.unwrap(), 60 | episode: tv_watch.media_index.unwrap(), 61 | }) 62 | .collect(); 63 | 64 | WatchHistory::TvShow(watches) 65 | } 66 | } 67 | 68 | impl Display for WatchHistory { 69 | fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { 70 | match self { 71 | Self::Movie(watches) => write_watches(f, watches), 72 | Self::TvShow(watches) => write_watches(f, watches), 73 | } 74 | } 75 | } 76 | 77 | fn write_watches(f: &mut std::fmt::Formatter, watches: &ItemWatches) -> std::fmt::Result 78 | where 79 | T: Display, 80 | { 81 | if watches.len() > 0 { 82 | write!(f, "Watch history:")?; 83 | for watch in watches.iter() { 84 | write!(f, "\n * {}", watch)?; 85 | } 86 | Ok(()) 87 | } else { 88 | write!(f, "No watch history.") 89 | } 90 | } 91 | 92 | pub type ItemWatches = Vec; 93 | 94 | #[derive(Debug)] 95 | pub struct UserEpisodeWatch { 96 | display_name: String, 97 | last_watched: DateTime, 98 | progress: u8, 99 | season: u32, 100 | episode: u32, 101 | } 102 | 103 | impl Display for UserEpisodeWatch { 104 | fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { 105 | write!( 106 | f, 107 | "Last watch by {}, was at {}. Season {} Episode {}, with {} complete.", 108 | self.display_name.yellow(), 109 | self.last_watched.format("%d-%m-%Y").blue(), 110 | self.season.yellow(), 111 | self.episode.yellow(), 112 | format!("{}%", self.progress).blue() 113 | ) 114 | } 115 | } 116 | 117 | #[derive(Debug)] 118 | pub struct UserMovieWatch { 119 | display_name: String, 120 | last_watched: DateTime, 121 | progress: u8, 122 | } 123 | 124 | impl Display for UserMovieWatch { 125 | fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { 126 | write!( 127 | f, 128 | "Last watch by {} at {}, with {} progress.", 129 | self.display_name.yellow(), 130 | self.last_watched.format("%d-%m-%Y").blue(), 131 | format!("{}%", self.progress).blue() 132 | ) 133 | } 134 | } 135 | 136 | pub async fn get_item_watches(rating_key: &str, media_type: &MediaType) -> Result { 137 | let history = get_item_history(rating_key, media_type).await?; 138 | 139 | let latest_user_history = 140 | history 141 | .iter() 142 | .fold(BTreeMap::new(), |mut user_latest_watch, current_watch| { 143 | user_latest_watch 144 | .entry(¤t_watch.user) 145 | .and_modify(|entry: &mut &HistoryItem| { 146 | if entry.date < current_watch.date { 147 | *entry = current_watch; 148 | } 149 | }) 150 | .or_insert(current_watch); 151 | 152 | user_latest_watch 153 | }); 154 | 155 | Ok(WatchHistory::from_user_watches( 156 | latest_user_history, 157 | media_type, 158 | rating_key, 159 | )) 160 | } 161 | 162 | async fn get_item_history(rating_key: &str, media_type: &MediaType) -> Result> { 163 | if let MediaType::Movie = media_type { 164 | let history: Vec = get_full_history(rating_key, "rating_key").await?; 165 | Ok(movie_item_to_history_item(history)) 166 | } else { 167 | let history: Vec = 168 | get_full_history(rating_key, "grandparent_rating_key").await?; 169 | Ok(history) 170 | } 171 | } 172 | 173 | async fn get_full_history(rating_key: &str, rating_key_kind: &str) -> Result> 174 | where 175 | T: DeserializeOwned, 176 | { 177 | let length = 1000; 178 | let length_string = length.to_string(); 179 | let mut history: Vec = Vec::new(); 180 | let mut page = 0; 181 | loop { 182 | let page_string = page.to_string(); 183 | let params = vec![ 184 | (rating_key_kind, rating_key), 185 | ("length", &length_string), 186 | ("start", &page_string), 187 | ]; 188 | let mut history_page: ResponseObj> = 189 | api::get_obj("get_history", Some(params)).await?; 190 | 191 | if history_page.response.data.data.len() < length { 192 | history.append(&mut history_page.response.data.data); 193 | break; 194 | } else { 195 | history.append(&mut history_page.response.data.data); 196 | page += 1; 197 | } 198 | } 199 | 200 | Ok(history) 201 | } 202 | 203 | fn movie_item_to_history_item(history: Vec) -> Vec { 204 | history 205 | .into_iter() 206 | .map(|item| HistoryItem { 207 | user: item.user, 208 | date: item.date, 209 | duration: item.duration, 210 | percent_complete: item.percent_complete, 211 | media_index: None, 212 | parent_media_index: None, 213 | }) 214 | .collect() 215 | } 216 | 217 | fn unix_seconds_to_date(unix_seconds: i64) -> Option> { 218 | let naive_date = NaiveDateTime::from_timestamp_millis(unix_seconds * 1000).unwrap(); 219 | Some(DateTime::from_utc(naive_date, Utc)) 220 | } 221 | -------------------------------------------------------------------------------- /src/tautulli/responses.rs: -------------------------------------------------------------------------------- 1 | use serde::Deserialize; 2 | 3 | #[derive(Debug, Deserialize)] 4 | #[serde(rename_all = "snake_case")] 5 | pub struct ResponseArr { 6 | pub response: ResponseInternalArr, 7 | } 8 | 9 | #[derive(Debug, Deserialize)] 10 | #[serde(rename_all = "snake_case")] 11 | pub struct ResponseInternalArr { 12 | pub message: Option, 13 | pub result: ResultType, 14 | pub data: Vec, 15 | } 16 | 17 | #[derive(Debug, Deserialize)] 18 | #[serde(rename_all = "snake_case")] 19 | pub struct ResponseObj { 20 | pub response: ResponseInternalObj, 21 | } 22 | 23 | #[derive(Debug, Deserialize)] 24 | #[serde(rename_all = "snake_case")] 25 | pub struct ResponseInternalObj { 26 | pub message: Option, 27 | pub result: ResultType, 28 | pub data: T, 29 | } 30 | 31 | #[derive(Debug, Deserialize, Clone, Copy)] 32 | #[serde(rename_all = "snake_case")] 33 | pub enum ResultType { 34 | Success, 35 | Error, 36 | } 37 | 38 | #[derive(Debug, Deserialize)] 39 | #[serde(rename_all = "camelCase")] 40 | pub struct History { 41 | pub draw: u32, 42 | pub records_total: u32, 43 | pub records_filtered: u32, 44 | pub data: Vec, 45 | } 46 | 47 | #[derive(Debug, Deserialize)] 48 | #[serde(rename_all = "snake_case")] 49 | pub struct HistoryItem { 50 | pub user: String, 51 | pub date: i64, 52 | pub duration: u64, 53 | pub percent_complete: u8, 54 | pub media_index: Option, 55 | pub parent_media_index: Option, 56 | } 57 | 58 | #[derive(Debug, Deserialize)] 59 | #[serde(rename_all = "snake_case")] 60 | pub struct HistoryMovieItem { 61 | pub date: i64, 62 | pub duration: u64, 63 | pub percent_complete: u8, 64 | pub user: String, 65 | } 66 | -------------------------------------------------------------------------------- /src/utils.rs: -------------------------------------------------------------------------------- 1 | use itertools::Itertools; 2 | 3 | pub fn create_param_string(params: Option>) -> String { 4 | params 5 | .unwrap_or(vec![]) 6 | .into_iter() 7 | .map(|param| format!("{}={}", param.0, param.1)) 8 | .collect_vec() 9 | .join("&") 10 | } 11 | 12 | pub fn create_api_error_message(code: u16, path: &str, service: &str) -> String { 13 | match code { 14 | 400 => format!("Got 400 Bad Request from {} at {}. The api may have changed, please report this on Github.", service, path), 15 | 401 => format!("Got 401 Unauthorized from {}, please check the appropriate API key.", service), 16 | 403 => format!("Got 403 Forbidden from {}, please check the appropriate API key.", service), 17 | 404 => format!("Got 404 Not Found from {} at path {}. Please make sure the URl is correct.", service, path), 18 | 505 => format!("Got 505 internal server error from {}. Please try again later.", service), 19 | code => { 20 | format!( 21 | "Error {} returned from {}. Code unknown, please create issue on Github.", 22 | code, service 23 | ) 24 | } 25 | } 26 | } 27 | 28 | pub fn human_file_size(size: i64) -> String { 29 | let gig_size = 1000000000.0; 30 | let gigs: f64 = size as f64 / gig_size; 31 | format!("{:.2}GB", gigs) 32 | } 33 | --------------------------------------------------------------------------------