From 02c16a2972820254317f71af687aef99e464354f Mon Sep 17 00:00:00 2001 From: lonelyhentxi Date: Fri, 20 Jun 2025 01:56:34 +0800 Subject: [PATCH] feat: support optimize images --- .vscode/settings.json | 1 + Cargo.lock | 1203 +++++++++++------ Cargo.toml | 19 + apps/recorder/Cargo.toml | 60 +- apps/recorder/examples/optimize_image.mjs | 45 - apps/recorder/src/app/builder.rs | 14 +- .../src/app/config/default_mixin.toml | 3 + apps/recorder/src/app/config/mod.rs | 5 +- apps/recorder/src/app/context.rs | 10 +- apps/recorder/src/app/core.rs | 29 +- apps/recorder/src/errors/app_error.rs | 5 + apps/recorder/src/extract/defs.rs | 39 - apps/recorder/src/extract/media/mod.rs | 6 +- .../src/extract/mikan/subscription.rs | 28 +- apps/recorder/src/extract/mikan/web.rs | 183 ++- apps/recorder/src/lib.rs | 1 + apps/recorder/src/media/config.rs | 105 ++ apps/recorder/src/media/mod.rs | 8 + apps/recorder/src/media/service.rs | 199 +++ apps/recorder/src/models/bangumi.rs | 9 +- apps/recorder/src/storage/client.rs | 63 +- apps/recorder/src/task/config.rs | 48 +- apps/recorder/src/task/core.rs | 5 +- apps/recorder/src/task/mod.rs | 11 +- apps/recorder/src/task/registry/media.rs | 53 + apps/recorder/src/task/registry/mod.rs | 37 +- .../src/task/registry/subscription.rs | 8 +- apps/recorder/src/task/service.rs | 87 +- apps/recorder/src/test_utils/app.rs | 48 +- apps/recorder/src/test_utils/database.rs | 9 + apps/recorder/src/test_utils/media.rs | 8 + apps/recorder/src/test_utils/mod.rs | 1 + apps/recorder/src/test_utils/task.rs | 2 +- apps/recorder/src/test_utils/tracing.rs | 2 +- .../recorder/src/web/controller/static/mod.rs | 52 +- apps/webui/src/components/ui/img.tsx | 69 +- justfile | 4 +- 37 files changed, 1781 insertions(+), 698 deletions(-) delete mode 100644 apps/recorder/examples/optimize_image.mjs create mode 100644 apps/recorder/src/media/config.rs create mode 100644 apps/recorder/src/media/mod.rs create mode 100644 apps/recorder/src/media/service.rs create mode 100644 apps/recorder/src/task/registry/media.rs create mode 100644 apps/recorder/src/test_utils/media.rs diff --git a/.vscode/settings.json b/.vscode/settings.json index d69ffa3..c37d92d 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -40,6 +40,7 @@ } ], "rust-analyzer.cargo.features": "all", + "rust-analyzer.testExplorer": true // https://github.com/rust-lang/rust/issues/141540 // "rust-analyzer.cargo.targetDir": "target/rust-analyzer", // "rust-analyzer.check.extraEnv": { diff --git a/Cargo.lock b/Cargo.lock index 664aa05..5b78ded 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -23,9 +23,9 @@ dependencies = [ [[package]] name = "adler2" -version = "2.0.0" +version = "2.0.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "512761e0bb2578dd7380c6baaa0f4ce03e84f95e960231d1dec8bf4d7d6e2627" +checksum = "320119579fcad9c21884f5c4861d16174d0e06250625266f50fe6898340abefa" [[package]] name = "aead" @@ -101,6 +101,15 @@ version = "0.1.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "250f629c0161ad8107cf89319e990051fae62832fd343083bea452d93e2205fd" +[[package]] +name = "aligned-vec" +version = "0.6.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dc890384c8602f339876ded803c97ad529f3842aba97f6392b3dba0dd171769b" +dependencies = [ + "equator", +] + [[package]] name = "alloc-no-stdlib" version = "2.0.4" @@ -243,12 +252,29 @@ dependencies = [ "thiserror 2.0.12", ] +[[package]] +name = "arbitrary" +version = "1.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dde20b3d026af13f561bdd0f15edf01fc734f0dafcedbaf42bba506a9517f223" + [[package]] name = "arc-swap" version = "1.7.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "69f7f8c3906b62b754cd5326047894316021dcfe5a194c8ea52bdd94934a3457" +[[package]] +name = "arg_enum_proc_macro" +version = "0.3.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0ae92a5119aa49cdbcf6b9f893fe4e1d98b04ccbf82ee0584ad948a44a734dea" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.104", +] + [[package]] name = "arrayvec" version = "0.7.6" @@ -304,14 +330,14 @@ checksum = "affbba0d438add06462a0371997575927bc05052f7ec486e7a4ca405c956c3d7" dependencies = [ "proc-macro2", "quote", - "syn 2.0.101", + "syn 2.0.104", ] [[package]] name = "async-compression" -version = "0.4.23" +version = "0.4.25" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b37fc50485c4f3f736a4fb14199f6d5f5ba008d7f28fe710306c92780f004c07" +checksum = "40f6024f3f856663b45fd0c9b6f2024034a702f453549449e0d84a305900dad4" dependencies = [ "brotli", "flate2", @@ -392,7 +418,7 @@ dependencies = [ "proc-macro2", "quote", "strum", - "syn 2.0.101", + "syn 2.0.104", "thiserror 1.0.69", ] @@ -450,7 +476,7 @@ checksum = "c7c24de15d275a1ecfd47a380fb4d5ec9bfe0933f309ed5e705b775596a3574d" dependencies = [ "proc-macro2", "quote", - "syn 2.0.101", + "syn 2.0.104", ] [[package]] @@ -461,7 +487,7 @@ checksum = "e539d3fca749fcee5236ab05e93a52867dd549cc157c8cb7f99595f3cedffdb5" dependencies = [ "proc-macro2", "quote", - "syn 2.0.101", + "syn 2.0.104", ] [[package]] @@ -488,9 +514,9 @@ dependencies = [ [[package]] name = "atomic" -version = "0.6.0" +version = "0.6.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8d818003e740b63afc82337e3160717f4f63078720a810b7b903e70a5d1d2994" +checksum = "a89cbf775b137e9b968e67227ef7f775587cde3fd31b0d8599dbd0f598a48340" dependencies = [ "bytemuck", ] @@ -503,9 +529,32 @@ checksum = "1505bd5d3d116872e7271a6d4e16d81d0c8570876c8de68093a09ac269d8aac0" [[package]] name = "autocfg" -version = "1.4.0" +version = "1.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ace50bade8e6234aa140d9a2f552bbee1db4d353f69b8217bc503490fc1a9f26" +checksum = "c08606f8c3cbf4ce6ec8e28fb0014a2c086708fe954eaa885384a6165172e7e8" + +[[package]] +name = "av1-grain" +version = "0.2.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4f3efb2ca85bc610acfa917b5aaa36f3fcbebed5b3182d7f877b02531c4b80c8" +dependencies = [ + "anyhow", + "arrayvec", + "log", + "nom 7.1.3", + "num-rational 0.4.2", + "v_frame", +] + +[[package]] +name = "avif-serialize" +version = "0.8.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "98922d6a4cfbcb08820c69d8eeccc05bb1f29bfa06b4f5b1dbfe9a868bd7608e" +dependencies = [ + "arrayvec", +] [[package]] name = "axum" @@ -596,7 +645,7 @@ checksum = "604fde5e028fea851ce1d8570bbdc034bec850d157f7569d10f347d06808c05c" dependencies = [ "proc-macro2", "quote", - "syn 2.0.101", + "syn 2.0.104", ] [[package]] @@ -707,6 +756,12 @@ version = "0.8.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5e764a1d40d510daf35e07be9eb06e75770908c27d411ee6c92109c9840eaaf7" +[[package]] +name = "bit_field" +version = "0.10.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dc827186963e592360843fb5ba4b973e145841266c1357f7180c43526f2e5b61" + [[package]] name = "bitflags" version = "1.3.2" @@ -722,6 +777,12 @@ dependencies = [ "serde", ] +[[package]] +name = "bitstream-io" +version = "2.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6099cdc01846bc367c4e7dd630dc5966dccf36b652fae7a74e17b640411a91b2" + [[package]] name = "bitvec" version = "1.0.1" @@ -822,7 +883,7 @@ dependencies = [ "proc-macro-crate", "proc-macro2", "quote", - "syn 2.0.101", + "syn 2.0.104", ] [[package]] @@ -857,6 +918,12 @@ dependencies = [ "serde", ] +[[package]] +name = "built" +version = "0.7.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "56ed6191a7e78c36abdb16ab65341eefd73d64d303fffccdbb00d51e4205967b" + [[package]] name = "bumpalo" version = "3.18.1" @@ -897,6 +964,12 @@ version = "1.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1fd0f2584146f6f2ef48085050886acf353beff7305ebd1ae69500e27c67f64b" +[[package]] +name = "byteorder-lite" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8f1fe948ff07f4bd06c30984e69f5b4899c516a3ef74f34df92a2df2ab535495" + [[package]] name = "bytes" version = "1.10.1" @@ -945,9 +1018,9 @@ dependencies = [ [[package]] name = "cc" -version = "1.2.26" +version = "1.2.27" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "956a5e21988b87f372569b66183b78babf23ebc2e744b733e4350a752c4dafac" +checksum = "d487aa071b5f64da6f19a3e848e3578944b726ee5a4854b82172f02aa876bfdc" dependencies = [ "jobserver", "libc", @@ -955,10 +1028,20 @@ dependencies = [ ] [[package]] -name = "cfg-if" -version = "1.0.0" +name = "cfg-expr" +version = "0.15.8" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd" +checksum = "d067ad48b8650848b989a59a86c6c36a995d02d2bf778d45c3c5d57bc2718f02" +dependencies = [ + "smallvec", + "target-lexicon", +] + +[[package]] +name = "cfg-if" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9555578bc9e57714c812a1f84e4fc5b4d21fcb063490c624de019f7464c91268" [[package]] name = "cfg_aliases" @@ -1050,9 +1133,9 @@ dependencies = [ [[package]] name = "clap" -version = "4.5.39" +version = "4.5.40" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fd60e63e9be68e5fb56422e397cf9baddded06dae1d2e523401542383bc72a9f" +checksum = "40b6887a1d8685cebccf115538db5c0efe625ccac9696ad45c409d96566e910f" dependencies = [ "clap_builder", "clap_derive", @@ -1060,9 +1143,9 @@ dependencies = [ [[package]] name = "clap_builder" -version = "4.5.39" +version = "4.5.40" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "89cc6392a1f72bbeb820d71f32108f61fdaf18bc526e1d23954168a67759ef51" +checksum = "e0c66c08ce9f0c698cbce5c0279d0bb6ac936d8674174fe48f736533b964f59e" dependencies = [ "anstream", "anstyle", @@ -1072,21 +1155,21 @@ dependencies = [ [[package]] name = "clap_derive" -version = "4.5.32" +version = "4.5.40" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "09176aae279615badda0765c0c0b3f6ed53f4709118af73cf4655d85d1530cd7" +checksum = "d2c7947ae4cc3d851207c1adb5b5e260ff0cca11446b1d6d1423788e442257ce" dependencies = [ "heck 0.5.0", "proc-macro2", "quote", - "syn 2.0.101", + "syn 2.0.104", ] [[package]] name = "clap_lex" -version = "0.7.4" +version = "0.7.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f46ad14479a25103f283c0f10005961cf086d8dc42205bb44c46ac563475dca6" +checksum = "b94f61472cee1439c0b966b47e3aca9ae07e45d070759512cd390ea2bebc6675" [[package]] name = "cocoon" @@ -1131,6 +1214,12 @@ dependencies = [ "tracing-error", ] +[[package]] +name = "color_quant" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3d7b894f5411737b7867f4827955924d7c254fc9f4d91a6aad6b097804b1018b" + [[package]] name = "colorchoice" version = "1.0.4" @@ -1400,6 +1489,12 @@ dependencies = [ "winapi", ] +[[package]] +name = "crunchy" +version = "0.2.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "43da5946c66ffcc7745f48db692ffbb10a83bfe0afd96235c5c2a4fb23994929" + [[package]] name = "crypto-bigint" version = "0.5.5" @@ -1486,7 +1581,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "13b588ba4ac1a99f7f2964d24b3d896ddc6bf847ee3855dbd4366f058cfcd331" dependencies = [ "quote", - "syn 2.0.101", + "syn 2.0.104", ] [[package]] @@ -1538,7 +1633,7 @@ checksum = "f46882e17999c6cc590af592290432be3bce0428cb0d5f8b6715e4dc7b383eb3" dependencies = [ "proc-macro2", "quote", - "syn 2.0.101", + "syn 2.0.104", ] [[package]] @@ -1562,7 +1657,7 @@ dependencies = [ "proc-macro2", "quote", "strsim", - "syn 2.0.101", + "syn 2.0.104", ] [[package]] @@ -1573,7 +1668,7 @@ checksum = "fc34b93ccb385b40dc71c6fceac4b2ad23662c7eeb248cf10d529b7e055b6ead" dependencies = [ "darling_core", "quote", - "syn 2.0.101", + "syn 2.0.104", ] [[package]] @@ -1658,7 +1753,7 @@ dependencies = [ "darling", "proc-macro2", "quote", - "syn 2.0.101", + "syn 2.0.104", ] [[package]] @@ -1668,7 +1763,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ab63b0e2bf4d5928aff72e83a7dace85d7bba5fe12dcc3c5a572d78caffd3f3c" dependencies = [ "derive_builder_core", - "syn 2.0.101", + "syn 2.0.104", ] [[package]] @@ -1679,7 +1774,7 @@ checksum = "6edb4b64a43d977b8e99788fe3a04d483834fba1215a7e02caa415b626497f7f" dependencies = [ "proc-macro2", "quote", - "syn 2.0.101", + "syn 2.0.104", ] [[package]] @@ -1747,7 +1842,7 @@ checksum = "97369cbbc041bc366949bc74d34658d6cda5621039731c6310521892a3a20ae0" dependencies = [ "proc-macro2", "quote", - "syn 2.0.101", + "syn 2.0.104", ] [[package]] @@ -1932,6 +2027,26 @@ dependencies = [ "cfg-if", ] +[[package]] +name = "equator" +version = "0.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4711b213838dfee0117e3be6ac926007d7f433d7bbe33595975d4190cb07e6fc" +dependencies = [ + "equator-macro", +] + +[[package]] +name = "equator-macro" +version = "0.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "44f23cf4b44bfce11a86ace86f8a73ffdec849c9fd00a386a53d278bd9e81fb3" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.104", +] + [[package]] name = "equivalent" version = "1.0.2" @@ -1940,12 +2055,12 @@ checksum = "877a4ace8713b0bcf2a4e7eec82529c029f1d0619886d18145fea96c3ffe5c0f" [[package]] name = "errno" -version = "0.3.12" +version = "0.3.13" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cea14ef9355e3beab063703aa9dab15afd25f0667c341310c1e5274bb1d0da18" +checksum = "778e2ac28f6c47af28e4907f13ffd1e1ddbd400980a9abd7c8df189bf578a5ad" dependencies = [ "libc", - "windows-sys 0.59.0", + "windows-sys 0.60.2", ] [[package]] @@ -1991,6 +2106,21 @@ dependencies = [ "pin-project-lite", ] +[[package]] +name = "exr" +version = "1.73.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f83197f59927b46c04a183a619b7c29df34e63e63c7869320862268c0ef687e0" +dependencies = [ + "bit_field", + "half", + "lebe", + "miniz_oxide", + "rayon-core", + "smallvec", + "zune-inflate", +] + [[package]] name = "eyre" version = "0.6.12" @@ -2027,6 +2157,15 @@ version = "2.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "37909eebbb50d72f9059c3b6d82c0463f2ff062c9e95845c43a6c9c0355411be" +[[package]] +name = "fdeflate" +version = "0.3.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1e6853b52649d4ac5c0bd02320cddc5ba956bdb407c4b75a2c6b75bf51500f8c" +dependencies = [ + "simd-adler32", +] + [[package]] name = "fetch" version = "0.1.0" @@ -2274,7 +2413,7 @@ checksum = "162ee34ebcb7c64a8abebc059ce0fee27c2262618d7b60ed8faf72fef13c3650" dependencies = [ "proc-macro2", "quote", - "syn 2.0.101", + "syn 2.0.104", ] [[package]] @@ -2355,7 +2494,7 @@ dependencies = [ "libc", "log", "rustversion", - "windows 0.61.1", + "windows 0.61.3", ] [[package]] @@ -2384,7 +2523,7 @@ version = "0.2.23" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "cba6ae63eb948698e300f645f87c70f76630d505f23b8907cf1e193ee85048c1" dependencies = [ - "unicode-width 0.2.0", + "unicode-width 0.2.1", ] [[package]] @@ -2396,7 +2535,7 @@ dependencies = [ "cfg-if", "js-sys", "libc", - "wasi 0.11.0+wasi-snapshot-preview1", + "wasi 0.11.1+wasi-snapshot-preview1", "wasm-bindgen", ] @@ -2424,6 +2563,16 @@ dependencies = [ "polyval", ] +[[package]] +name = "gif" +version = "0.13.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3fb2d69b19215e18bb912fa30f7ce15846e301408695e44e0ef719f1da9e19f2" +dependencies = [ + "color_quant", + "weezl", +] + [[package]] name = "gimli" version = "0.31.1" @@ -2525,6 +2674,16 @@ dependencies = [ "tracing", ] +[[package]] +name = "half" +version = "2.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "459196ed295495a68f7d7fe1d84f6c4b7ff0e21fe3017b2f283c6fac3ad803c9" +dependencies = [ + "cfg-if", + "crunchy", +] + [[package]] name = "handlebars" version = "5.1.2" @@ -2589,6 +2748,17 @@ dependencies = [ "sha1", ] +[[package]] +name = "headers-accept" +version = "0.1.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "244c37aaa637e64fb73af1bb2a4f3f6f2b72f1f39f1cd6f4443f0cc21fe80280" +dependencies = [ + "headers-core", + "http", + "mediatype", +] + [[package]] name = "headers-core" version = "0.3.0" @@ -2610,6 +2780,12 @@ version = "0.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2304e00983f87ffb38b55b444b5e3b60a884b5d30c0fca7d82fe33449bbe55ea" +[[package]] +name = "hermit-abi" +version = "0.5.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fc0fef456e4baa96da950455cd02c081ca953b141298e41db3fc7e36b1da849c" + [[package]] name = "hex" version = "0.3.2" @@ -3382,6 +3558,45 @@ dependencies = [ "winapi-util", ] +[[package]] +name = "image" +version = "0.25.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "db35664ce6b9810857a38a906215e75a9c879f0696556a39f59c62829710251a" +dependencies = [ + "bytemuck", + "byteorder-lite", + "color_quant", + "exr", + "gif", + "image-webp", + "num-traits", + "png", + "qoi", + "ravif", + "rayon", + "rgb", + "tiff", + "zune-core", + "zune-jpeg", +] + +[[package]] +name = "image-webp" +version = "0.2.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f6970fe7a5300b4b42e62c52efa0187540a5bef546c60edaf554ef595d2e6f0b" +dependencies = [ + "byteorder-lite", + "quick-error", +] + +[[package]] +name = "imgref" +version = "1.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d0263a3d970d5c054ed9312c0057b4f3bde9c0b33836d3637361d4a9e6e7a408" + [[package]] name = "indenter" version = "0.3.3" @@ -3418,7 +3633,7 @@ checksum = "6c38228f24186d9cc68c729accb4d413be9eaed6ad07ff79e0270d9e56f3de13" dependencies = [ "proc-macro2", "quote", - "syn 2.0.101", + "syn 2.0.104", ] [[package]] @@ -3501,6 +3716,17 @@ dependencies = [ "web-sys", ] +[[package]] +name = "interpolate_name" +version = "0.2.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c34819042dc3d3971c46c2190835914dfbe0c3c13f61449b2997f4e9722dfa60" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.104", +] + [[package]] name = "intervaltree" version = "0.2.7" @@ -3591,10 +3817,33 @@ dependencies = [ ] [[package]] -name = "jpeg-encoder" -version = "0.5.1" +name = "jpeg-decoder" +version = "0.3.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2cf3affe27ffd9f1992690ec7575568b222abe9cb39738f6531968aca8e64906" +checksum = "f5d4a7da358eff58addd2877a45865158f0d78c911d43a5784ceb7bbf52833b0" + +[[package]] +name = "jpegxl-rs" +version = "0.11.2+libjxl-0.11.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "875063ddd0cb50c5668b9c3214152ee54e6ca1f42b662d66c0855f76687033c0" +dependencies = [ + "byteorder", + "derive_builder", + "half", + "image", + "jpegxl-sys", + "thiserror 2.0.12", +] + +[[package]] +name = "jpegxl-sys" +version = "0.11.2+libjxl-0.11.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fdaef0388e8220dc89a4ab47f92f942b68dfc237fa2dd3c3881948c5d88ce2f0" +dependencies = [ + "pkg-config", +] [[package]] name = "js-sys" @@ -3624,134 +3873,6 @@ dependencies = [ "tokio", ] -[[package]] -name = "jxl-bitstream" -version = "0.2.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "76c3205d18cf6229b3f694de66e592886ff7afb0740bc0e85594e3b28d6f6622" -dependencies = [ - "tracing", -] - -[[package]] -name = "jxl-coding" -version = "0.2.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7075600c762c1ce9e2dd1fbc3fa8ded2af1401fe2221449eca943977742dd0b4" -dependencies = [ - "jxl-bitstream", -] - -[[package]] -name = "jxl-color" -version = "0.3.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9084bc33b6d01e26b1db6c187514a51a57f4e3780335f3120ab55ee0b08f6e73" -dependencies = [ - "jxl-bitstream", - "jxl-coding", - "jxl-grid", -] - -[[package]] -name = "jxl-frame" -version = "0.5.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8d63bdd104e3746669a123de86f940aa5d59fdc9c5a65f16a4f867dde75e45e1" -dependencies = [ - "jxl-bitstream", - "jxl-coding", - "jxl-grid", - "jxl-image", - "jxl-modular", - "jxl-vardct", - "tracing", -] - -[[package]] -name = "jxl-grid" -version = "0.1.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "48800b21ed6bb3bbc2f818ae9cd40530bdfb1a211f57d5a7a49b8b10f62145e8" - -[[package]] -name = "jxl-image" -version = "0.5.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ef86f7f74acc9c9e66604c8d030e00cdef5a0c455ea3d7d26bd9ddbb9160be59" -dependencies = [ - "jxl-bitstream", - "jxl-color", - "jxl-grid", - "tracing", -] - -[[package]] -name = "jxl-modular" -version = "0.3.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "504e6b55db362568592be81993c772fc6786c56fb67ae769ff62dc514c3e6748" -dependencies = [ - "jxl-bitstream", - "jxl-coding", - "jxl-grid", - "tracing", -] - -[[package]] -name = "jxl-oxide" -version = "0.4.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "57e3b7e459d823979c4ca0c9584f391581db154437f34596ea443b082e9b6064" -dependencies = [ - "jxl-bitstream", - "jxl-color", - "jxl-frame", - "jxl-grid", - "jxl-image", - "jxl-render", - "tracing", -] - -[[package]] -name = "jxl-render" -version = "0.4.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7157d1c6c4896ddc800cb0cc8ba545ba7417ab9afc51f39e69484e6607c8707e" -dependencies = [ - "jxl-bitstream", - "jxl-coding", - "jxl-color", - "jxl-frame", - "jxl-grid", - "jxl-image", - "jxl-modular", - "jxl-vardct", - "tracing", -] - -[[package]] -name = "jxl-vardct" -version = "0.3.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "eb4a2d9ba8c48a52f6143ba01c38aac67d1309c9b939a9f84cd60f650d15053e" -dependencies = [ - "jxl-bitstream", - "jxl-coding", - "jxl-grid", - "jxl-modular", - "tracing", -] - -[[package]] -name = "kamadak-exif" -version = "0.5.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ef4fc70d0ab7e5b6bafa30216a6b48705ea964cdfc29c050f2412295eba58077" -dependencies = [ - "mutate_once", -] - [[package]] name = "kqueue" version = "1.1.1" @@ -3793,10 +3914,26 @@ dependencies = [ ] [[package]] -name = "libc" -version = "0.2.172" +name = "lebe" +version = "0.5.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d750af042f7ef4f724306de029d18836c26c1765a54a6a3f094cbd23a7267ffa" +checksum = "03087c2bad5e1034e8cace5926dec053fb3790248370865f5117a7d0213354c8" + +[[package]] +name = "libc" +version = "0.2.174" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1171693293099992e19cddea4e8b849964e9846f4acee11b3948bcc337be8776" + +[[package]] +name = "libfuzzer-sys" +version = "0.4.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cf78f52d400cf2d84a3a973a78a592b4adc535739e0a5597a0da6f0c357adc75" +dependencies = [ + "arbitrary", + "cc", +] [[package]] name = "libm" @@ -3812,7 +3949,7 @@ checksum = "c0ff37bd590ca25063e35af745c343cb7a0271906fb7b37e4813e8f79f00268d" dependencies = [ "bitflags 2.9.1", "libc", - "redox_syscall 0.5.12", + "redox_syscall 0.5.13", ] [[package]] @@ -4034,6 +4171,16 @@ dependencies = [ "vcpkg", ] +[[package]] +name = "libwebp-sys" +version = "0.9.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "54cd30df7c7165ce74a456e4ca9732c603e8dc5e60784558c1c6dc047f876733" +dependencies = [ + "cc", + "glob", +] + [[package]] name = "lightningcss" version = "1.0.0-alpha.66" @@ -4133,6 +4280,15 @@ dependencies = [ "tracing-subscriber", ] +[[package]] +name = "loop9" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0fae87c125b03c1d2c0150c90365d7d6bcc53fb73a9acaef207d2d065860f062" +dependencies = [ + "imgref", +] + [[package]] name = "lru" version = "0.12.5" @@ -4182,7 +4338,7 @@ checksum = "88a9689d8d44bf9964484516275f5cd4c9b59457a6940c1d5d0ecbb94510a36b" dependencies = [ "proc-macro2", "quote", - "syn 2.0.101", + "syn 2.0.104", ] [[package]] @@ -4206,6 +4362,16 @@ version = "0.8.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "47e1ffaa40ddd1f3ed91f717a33c8c0ee23fff369e3aa8772b9605cc1d22f4c3" +[[package]] +name = "maybe-rayon" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8ea1f30cedd69f0a2954655f7188c6a834246d2bcf1e315e2ac40c4b24dc9519" +dependencies = [ + "cfg-if", + "rayon", +] + [[package]] name = "md-5" version = "0.10.6" @@ -4217,10 +4383,16 @@ dependencies = [ ] [[package]] -name = "memchr" -version = "2.7.4" +name = "mediatype" +version = "0.19.20" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "78ca9ab1a0babb1e7d5695e3530886289c18cf2f87ec19a575a0abdce112e3a3" +checksum = "33746aadcb41349ec291e7f2f0a3aa6834d1d7c58066fb4b01f68efc4c4b7631" + +[[package]] +name = "memchr" +version = "2.7.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "32a282da65faaf38286cf3be983213fcf1d2e2a58700e808f83f4ea9a4804bc0" [[package]] name = "memmap2" @@ -4270,7 +4442,7 @@ checksum = "49e7bc1560b95a3c4a25d03de42fe76ca718ab92d1a22a55b9b4cf67b3ae635c" dependencies = [ "proc-macro2", "quote", - "syn 2.0.101", + "syn 2.0.104", ] [[package]] @@ -4290,12 +4462,19 @@ dependencies = [ ] [[package]] -name = "miniz_oxide" -version = "0.8.8" +name = "minimal-lexical" +version = "0.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3be647b768db090acb35d5ec5db2b0e1f1de11133ca123b9eacf5137868f892a" +checksum = "68354c5c6bd36d73ff3feceb05efa59b6acb7626617f4962be322a825e61f79a" + +[[package]] +name = "miniz_oxide" +version = "0.8.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1fa76a2c86f704bdb222d66965fb3d63269ce38518b83cb0575fca855ebb6316" dependencies = [ "adler2", + "simd-adler32", ] [[package]] @@ -4306,7 +4485,7 @@ checksum = "a4a650543ca06a924e8b371db273b2756685faae30f8487da1b56505a8f78b0c" dependencies = [ "libc", "log", - "wasi 0.11.0+wasi-snapshot-preview1", + "wasi 0.11.1+wasi-snapshot-preview1", "windows-sys 0.48.0", ] @@ -4318,7 +4497,7 @@ checksum = "78bed444cc8a2160f01cbcf811ef18cac863ad68ae8ca62092e8db51d51c761c" dependencies = [ "libc", "log", - "wasi 0.11.0+wasi-snapshot-preview1", + "wasi 0.11.1+wasi-snapshot-preview1", "windows-sys 0.59.0", ] @@ -4391,12 +4570,6 @@ dependencies = [ "version_check", ] -[[package]] -name = "mutate_once" -version = "0.1.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "16cf681a23b4d0a43fc35024c176437f9dcd818db34e0f42ab456a0ee5ad497b" - [[package]] name = "nanoid" version = "0.4.0" @@ -4462,6 +4635,16 @@ version = "0.4.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b93853da6d84c2e3c7d730d6473e8817692dd89be387eb01b94d7f108ecb5b8c" +[[package]] +name = "nom" +version = "7.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d273983c5a657a70a3e8f2a01329822f3b8c8172b73826411a55751e404a0a4a" +dependencies = [ + "memchr", + "minimal-lexical", +] + [[package]] name = "nom" version = "8.0.0" @@ -4477,6 +4660,12 @@ version = "0.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "38bf9645c8b145698bb0b18a4637dcacbc421ea49bef2317e4fd8065a387cf21" +[[package]] +name = "noop_proc_macro" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0676bb32a98c1a483ce53e500a81ad9c3d5b3f7c920c28c24e9cb0980d0b5bc8" + [[package]] name = "notify" version = "7.0.0" @@ -4580,6 +4769,17 @@ version = "0.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "51d515d32fb182ee37cda2ccdcb92950d6a3c2893aa280e540671c2cd0f3b1d9" +[[package]] +name = "num-derive" +version = "0.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ed3955f1a9c7c0c15e092f9c887db08b1fc683305fdf6eb6684f22555355e202" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.104", +] + [[package]] name = "num-integer" version = "0.1.46" @@ -4632,6 +4832,16 @@ dependencies = [ "libm", ] +[[package]] +name = "num_cpus" +version = "1.17.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "91df4bbde75afed763b708b7eee1e8e7651e02d97f6d5dd763e89367e957b23b" +dependencies = [ + "hermit-abi", + "libc", +] + [[package]] name = "oauth2" version = "5.0.0" @@ -4760,7 +4970,7 @@ checksum = "a948666b637a0f465e8564c73e89d4dde00d72d4d473cc972f390fc3dcee7d9c" dependencies = [ "proc-macro2", "quote", - "syn 2.0.101", + "syn 2.0.104", ] [[package]] @@ -4826,7 +5036,7 @@ dependencies = [ "proc-macro2", "proc-macro2-diagnostics", "quote", - "syn 2.0.101", + "syn 2.0.104", ] [[package]] @@ -4950,7 +5160,7 @@ checksum = "bc838d2a56b5b1a6c25f55575dfc605fabb63bb2365f6c2353ef9159aa69e4a5" dependencies = [ "cfg-if", "libc", - "redox_syscall 0.5.12", + "redox_syscall 0.5.13", "smallvec", "windows-targets 0.52.6", ] @@ -4977,7 +5187,7 @@ dependencies = [ "regex", "regex-syntax 0.8.5", "structmeta", - "syn 2.0.101", + "syn 2.0.104", ] [[package]] @@ -5032,7 +5242,7 @@ dependencies = [ "proc-macro2", "proc-macro2-diagnostics", "quote", - "syn 2.0.101", + "syn 2.0.104", ] [[package]] @@ -5052,9 +5262,9 @@ checksum = "e3148f5046208a5d56bcfc03053e3ca6334e51da8dfb19b6cdc8b306fae3283e" [[package]] name = "pest" -version = "2.8.0" +version = "2.8.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "198db74531d58c70a361c42201efde7e2591e976d518caf7662a47dc5720e7b6" +checksum = "1db05f56d34358a8b1066f67cbb203ee3e7ed2ba674a6263a1d5ec6db2204323" dependencies = [ "memchr", "thiserror 2.0.12", @@ -5063,9 +5273,9 @@ dependencies = [ [[package]] name = "pest_derive" -version = "2.8.0" +version = "2.8.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d725d9cfd79e87dccc9341a2ef39d1b6f6353d68c4b33c177febbe1a402c97c5" +checksum = "bb056d9e8ea77922845ec74a1c4e8fb17e7c218cc4fc11a15c5d25e189aa40bc" dependencies = [ "pest", "pest_generator", @@ -5073,24 +5283,23 @@ dependencies = [ [[package]] name = "pest_generator" -version = "2.8.0" +version = "2.8.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "db7d01726be8ab66ab32f9df467ae8b1148906685bbe75c82d1e65d7f5b3f841" +checksum = "87e404e638f781eb3202dc82db6760c8ae8a1eeef7fb3fa8264b2ef280504966" dependencies = [ "pest", "pest_meta", "proc-macro2", "quote", - "syn 2.0.101", + "syn 2.0.104", ] [[package]] name = "pest_meta" -version = "2.8.0" +version = "2.8.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7f9f832470494906d1fca5329f8ab5791cc60beb230c74815dff541cbd2b5ca0" +checksum = "edd1101f170f5903fde0914f899bb503d9ff5271d7ba76bbb70bea63690cc0d5" dependencies = [ - "once_cell", "pest", "sha2 0.10.9", ] @@ -5144,7 +5353,7 @@ dependencies = [ "phf_shared", "proc-macro2", "quote", - "syn 2.0.101", + "syn 2.0.104", ] [[package]] @@ -5173,7 +5382,7 @@ checksum = "6e918e4ff8c4549eb882f14b3a4bc8c8bc93de829416eacf579f1207a8fbf861" dependencies = [ "proc-macro2", "quote", - "syn 2.0.101", + "syn 2.0.104", ] [[package]] @@ -5215,6 +5424,19 @@ version = "0.3.32" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7edddbd0b52d732b21ad9a5fab5c704c14cd949e5e9a1ec5929a24fded1b904c" +[[package]] +name = "png" +version = "0.17.16" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "82151a2fc869e011c153adc57cf2789ccb8d9906ce52c0b39a6b5697749d7526" +dependencies = [ + "bitflags 1.3.2", + "crc32fast", + "fdeflate", + "flate2", + "miniz_oxide", +] + [[package]] name = "poly1305" version = "0.8.0" @@ -5312,7 +5534,7 @@ dependencies = [ "proc-macro-error-attr2", "proc-macro2", "quote", - "syn 2.0.101", + "syn 2.0.104", ] [[package]] @@ -5332,11 +5554,30 @@ checksum = "af066a9c399a26e020ada66a034357a868728e72cd426f3adcd35f80d88d88c8" dependencies = [ "proc-macro2", "quote", - "syn 2.0.101", + "syn 2.0.104", "version_check", "yansi", ] +[[package]] +name = "profiling" +version = "1.0.16" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "afbdc74edc00b6f6a218ca6a5364d6226a259d4b8ea1af4a0ea063f27e179f4d" +dependencies = [ + "profiling-procmacros", +] + +[[package]] +name = "profiling-procmacros" +version = "1.0.16" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a65f2e60fbf1063868558d69c6beacf412dc755f9fc020f514b7955fc914fe30" +dependencies = [ + "quote", + "syn 2.0.104", +] + [[package]] name = "proxy" version = "0.1.0" @@ -5404,20 +5645,35 @@ dependencies = [ ] [[package]] -name = "quanta" -version = "0.12.5" +name = "qoi" +version = "0.4.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3bd1fe6824cea6538803de3ff1bc0cf3949024db3d43c9643024bfb33a807c0e" +checksum = "7f6d64c71eb498fe9eae14ce4ec935c555749aef511cca85b5568910d6e48001" +dependencies = [ + "bytemuck", +] + +[[package]] +name = "quanta" +version = "0.12.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f3ab5a9d756f0d97bdc89019bd2e4ea098cf9cde50ee7564dde6b81ccc8f06c7" dependencies = [ "crossbeam-utils", "libc", "once_cell", "raw-cpuid", - "wasi 0.11.0+wasi-snapshot-preview1", + "wasi 0.11.1+wasi-snapshot-preview1", "web-sys", "winapi", ] +[[package]] +name = "quick-error" +version = "2.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a993555f31e5a609f617c12db6250dedcac1b0a85076912c436e6fc9b2c8e6a3" + [[package]] name = "quick-xml" version = "0.37.5" @@ -5472,9 +5728,9 @@ dependencies = [ [[package]] name = "quinn-udp" -version = "0.5.12" +version = "0.5.13" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ee4e529991f949c5e25755532370b8af5d114acae52326361d68d47af64aa842" +checksum = "fcebb1209ee276352ef14ff8732e24cc2b02bbac986cd74a4c81bcb2f9881970" dependencies = [ "cfg_aliases", "libc", @@ -5491,7 +5747,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e361f53ac3215518de2a005d132dbfa798dca29b1d46bcadae676f9a419671b4" dependencies = [ "cfg_rust_features", - "nom", + "nom 8.0.0", "percent-encoding", "thiserror 1.0.69", "url", @@ -5508,9 +5764,9 @@ dependencies = [ [[package]] name = "r-efi" -version = "5.2.0" +version = "5.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "74765f6d916ee2faa39bc8e68e4f3ed8949b48cccdac59983d287a7cb71ce9c5" +checksum = "69cdb34c158ceb288df11e18b4bd39de994f6657d83847bdffdbd7f346754b0f" [[package]] name = "radium" @@ -5577,6 +5833,56 @@ dependencies = [ "getrandom 0.3.3", ] +[[package]] +name = "rav1e" +version = "0.7.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cd87ce80a7665b1cce111f8a16c1f3929f6547ce91ade6addf4ec86a8dda5ce9" +dependencies = [ + "arbitrary", + "arg_enum_proc_macro", + "arrayvec", + "av1-grain", + "bitstream-io", + "built", + "cfg-if", + "interpolate_name", + "itertools 0.12.1", + "libc", + "libfuzzer-sys", + "log", + "maybe-rayon", + "new_debug_unreachable", + "noop_proc_macro", + "num-derive", + "num-traits", + "once_cell", + "paste", + "profiling", + "rand 0.8.5", + "rand_chacha 0.3.1", + "simd_helpers", + "system-deps", + "thiserror 1.0.69", + "v_frame", + "wasm-bindgen", +] + +[[package]] +name = "ravif" +version = "0.11.20" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5825c26fddd16ab9f515930d49028a630efec172e903483c94796cfe31893e6b" +dependencies = [ + "avif-serialize", + "imgref", + "loop9", + "quick-error", + "rav1e", + "rayon", + "rgb", +] + [[package]] name = "raw-cpuid" version = "11.5.0" @@ -5632,25 +5938,29 @@ dependencies = [ "fetch", "figment", "futures", + "headers-accept", "html-escape", "http", "icu", "icu_properties", + "image", "inquire", "insta", "ipnetwork", "itertools 0.14.0", + "jpegxl-rs", + "jpegxl-sys", "jwtk", "lazy_static", "lightningcss", - "log", "maplit", "mime_guess", "mockito", "moka", "nanoid", - "nom", + "nom 8.0.0", "num-traits", + "num_cpus", "once_cell", "opendal", "openidconnect", @@ -5688,7 +5998,7 @@ dependencies = [ "util", "util-derive", "uuid", - "zune-image", + "webp", ] [[package]] @@ -5711,9 +6021,9 @@ dependencies = [ [[package]] name = "redox_syscall" -version = "0.5.12" +version = "0.5.13" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "928fca9cf2aa042393a8325b9ead81d2f0df4cb12e1e24cef072922ccd99c5af" +checksum = "0d04b7d0ee6b4a0207a0a7adb104d23ecb0b47d6beae7152d0fa34b692b29fd6" dependencies = [ "bitflags 2.9.1", ] @@ -5729,6 +6039,26 @@ dependencies = [ "thiserror 1.0.69", ] +[[package]] +name = "ref-cast" +version = "1.0.24" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4a0ae411dbe946a674d89546582cea4ba2bb8defac896622d6496f14c23ba5cf" +dependencies = [ + "ref-cast-impl", +] + +[[package]] +name = "ref-cast-impl" +version = "1.0.24" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1165225c21bff1f3bbce98f5a1f889949bc902d3575308cc7b0de30b4f6d27c7" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.104", +] + [[package]] name = "reflink-copy" version = "0.1.26" @@ -5738,7 +6068,7 @@ dependencies = [ "cfg-if", "libc", "rustix", - "windows 0.61.1", + "windows 0.61.3", ] [[package]] @@ -5889,9 +6219,9 @@ dependencies = [ [[package]] name = "reqwest-tracing" -version = "0.5.7" +version = "0.5.8" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d75b0eee96990cfb4c09545847385e89b2d2d2e571143d55264a05d77c713780" +checksum = "d70ea85f131b2ee9874f0b160ac5976f8af75f3c9badfe0d955880257d10bd83" dependencies = [ "anyhow", "async-trait", @@ -5936,6 +6266,12 @@ dependencies = [ "subtle", ] +[[package]] +name = "rgb" +version = "0.8.50" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "57397d16646700483b67d2dd6511d79318f9d057fdbd21a4066aeac8b41d310a" + [[package]] name = "ring" version = "0.17.14" @@ -6046,15 +6382,15 @@ dependencies = [ "regex", "relative-path", "rustc_version", - "syn 2.0.101", + "syn 2.0.104", "unicode-ident", ] [[package]] name = "rust_decimal" -version = "1.37.1" +version = "1.37.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "faa7de2ba56ac291bd90c6b9bece784a52ae1411f9506544b3eae36dd2356d50" +checksum = "b203a6425500a03e0919c42d3c47caca51e79f1132046626d2c8871c5092035d" dependencies = [ "arrayvec", "borsh", @@ -6068,9 +6404,9 @@ dependencies = [ [[package]] name = "rustc-demangle" -version = "0.1.24" +version = "0.1.25" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "719b953e2095829ee67db738b3bfa9fa368c94900df327b3f07fe6e794d2fe1f" +checksum = "989e6739f80c4ad5b13e0fd7fe89531180375b18520cc8c82080e4dc4035b84f" [[package]] name = "rustc-hash" @@ -6108,9 +6444,9 @@ dependencies = [ [[package]] name = "rustls" -version = "0.23.27" +version = "0.23.28" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "730944ca083c1c233a75c09f199e973ca499344a2b7ba9e755c457e86fb4a321" +checksum = "7160e3e10bf4535308537f3c4e1641468cd0e485175d6163087c0393c7d46643" dependencies = [ "once_cell", "ring", @@ -6201,6 +6537,18 @@ dependencies = [ "windows-sys 0.59.0", ] +[[package]] +name = "schemars" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4cd191f9397d57d581cddd31014772520aa448f65ef991055d7f61582c65165f" +dependencies = [ + "dyn-clone", + "ref-cast", + "serde", + "serde_json", +] + [[package]] name = "scoped-tls" version = "1.0.1" @@ -6244,7 +6592,7 @@ dependencies = [ "proc-macro-error2", "proc-macro2", "quote", - "syn 2.0.101", + "syn 2.0.104", ] [[package]] @@ -6305,7 +6653,7 @@ dependencies = [ "proc-macro2", "quote", "sea-bae", - "syn 2.0.101", + "syn 2.0.104", "unicode-ident", ] @@ -6368,7 +6716,7 @@ dependencies = [ "heck 0.4.1", "proc-macro2", "quote", - "syn 2.0.101", + "syn 2.0.104", "thiserror 2.0.12", ] @@ -6394,7 +6742,7 @@ dependencies = [ "heck 0.4.1", "proc-macro2", "quote", - "syn 2.0.101", + "syn 2.0.104", ] [[package]] @@ -6520,7 +6868,7 @@ checksum = "5b0276cf7f2c73365f7157c8123c21cd9a50fbbd844757af28ca1f5925fc2a00" dependencies = [ "proc-macro2", "quote", - "syn 2.0.101", + "syn 2.0.104", ] [[package]] @@ -6562,7 +6910,7 @@ checksum = "175ee3e80ae9982737ca543e96133087cbd9a485eecc3bc4de9c1a37b47ea59c" dependencies = [ "proc-macro2", "quote", - "syn 2.0.101", + "syn 2.0.104", ] [[package]] @@ -6597,15 +6945,16 @@ dependencies = [ [[package]] name = "serde_with" -version = "3.12.0" +version = "3.13.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d6b6f7f2fcb69f747921f79f3926bd1e203fce4fef62c268dd3abfb6d86029aa" +checksum = "bf65a400f8f66fb7b0552869ad70157166676db75ed8181f8104ea91cf9d0b42" dependencies = [ "base64 0.22.1", "chrono", "hex 0.4.3", "indexmap 1.9.3", "indexmap 2.9.0", + "schemars", "serde", "serde_derive", "serde_json", @@ -6615,14 +6964,14 @@ dependencies = [ [[package]] name = "serde_with_macros" -version = "3.12.0" +version = "3.13.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8d00caa5193a3c8362ac2b73be6b9e768aa5a4b2f721d8f4b339600c3cb51f8e" +checksum = "81679d9ed988d5e9a5e6531dc3f2c28efbd639cbd1dfb628df08edea6004da77" dependencies = [ "darling", "proc-macro2", "quote", - "syn 2.0.101", + "syn 2.0.104", ] [[package]] @@ -6660,14 +7009,14 @@ checksum = "5d69265a08751de7844521fd15003ae0a888e035773ba05695c5c759a6f89eef" dependencies = [ "proc-macro2", "quote", - "syn 2.0.101", + "syn 2.0.104", ] [[package]] name = "servo_arc" -version = "0.4.0" +version = "0.4.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ae65c4249478a2647db249fb43e23cec56a2c8974a427e7bd8cb5a1d0964921a" +checksum = "204ea332803bd95a0b60388590d59cf6468ec9becf626e2451f1d26a1d972de4" dependencies = [ "stable_deref_trait", ] @@ -6788,6 +7137,15 @@ version = "0.3.7" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d66dc143e6b11c1eddc06d5c423cfc97062865baf299914ab64caa38182078fe" +[[package]] +name = "simd_helpers" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "95890f873bec569a0362c235787f3aca6e1e887302ba4840839bcc6459c42da6" +dependencies = [ + "quote", +] + [[package]] name = "simdutf8" version = "0.1.5" @@ -6818,12 +7176,9 @@ dependencies = [ [[package]] name = "slab" -version = "0.4.9" +version = "0.4.10" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8f92a496fb766b417c996b9c5e57daf2f7ad3b0bebe1ccfca4856390e3d3bb67" -dependencies = [ - "autocfg", -] +checksum = "04dc19736151f35336d325007ac991178d504a119863a2fcb3758cdb5e52c50d" [[package]] name = "slug" @@ -6864,7 +7219,7 @@ dependencies = [ "heck 0.5.0", "proc-macro2", "quote", - "syn 2.0.101", + "syn 2.0.104", ] [[package]] @@ -6967,7 +7322,7 @@ dependencies = [ "quote", "sqlx-core", "sqlx-macros-core", - "syn 2.0.101", + "syn 2.0.104", ] [[package]] @@ -6990,7 +7345,7 @@ dependencies = [ "sqlx-mysql", "sqlx-postgres", "sqlx-sqlite", - "syn 2.0.101", + "syn 2.0.104", "tokio", "url", ] @@ -7198,7 +7553,7 @@ dependencies = [ "proc-macro2", "quote", "structmeta-derive", - "syn 2.0.101", + "syn 2.0.104", ] [[package]] @@ -7209,7 +7564,7 @@ checksum = "152a0b65a590ff6c3da95cabe2353ee04e6167c896b28e3b14478c2636c922fc" dependencies = [ "proc-macro2", "quote", - "syn 2.0.101", + "syn 2.0.104", ] [[package]] @@ -7231,7 +7586,7 @@ dependencies = [ "proc-macro2", "quote", "rustversion", - "syn 2.0.101", + "syn 2.0.104", ] [[package]] @@ -7253,9 +7608,9 @@ dependencies = [ [[package]] name = "syn" -version = "2.0.101" +version = "2.0.104" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8ce2b7fc941b3a24138a0a7cf8e858bfc6a992e7978a068a5c760deb0ed43caf" +checksum = "17b6f705963418cdb9927482fa304bc562ece2fdd4f616084c50b7023b435a40" dependencies = [ "proc-macro2", "quote", @@ -7279,7 +7634,7 @@ checksum = "728a70f3dbaf5bab7f0c4b1ac8d7ae5ea60a4b5549c8a5914361c99147a709d2" dependencies = [ "proc-macro2", "quote", - "syn 2.0.101", + "syn 2.0.104", ] [[package]] @@ -7303,6 +7658,19 @@ dependencies = [ "libc", ] +[[package]] +name = "system-deps" +version = "6.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a3e535eb8dded36d55ec13eddacd30dec501792ff23a0b1682c38601b8cf2349" +dependencies = [ + "cfg-expr", + "heck 0.5.0", + "pkg-config", + "toml 0.8.23", + "version-compare", +] + [[package]] name = "tagptr" version = "0.2.0" @@ -7315,6 +7683,12 @@ version = "1.0.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "55937e1799185b12863d447f42597ed69d9928686b8d88a1df17376a097d8369" +[[package]] +name = "target-lexicon" +version = "0.12.16" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "61c41af27dd6d1e27b1b16b489db798443478cef1f06a660c96db617ba5de3b1" + [[package]] name = "tempfile" version = "3.20.0" @@ -7449,7 +7823,7 @@ checksum = "4fee6c4efc90059e10f81e6d42c60a18f76588c3d74cb83a0b242a2b6c7504c1" dependencies = [ "proc-macro2", "quote", - "syn 2.0.101", + "syn 2.0.104", ] [[package]] @@ -7460,17 +7834,27 @@ checksum = "7f7cf42b4507d8ea322120659672cf1b9dbb93f8f2d4ecfd6e51350ff5b17a1d" dependencies = [ "proc-macro2", "quote", - "syn 2.0.101", + "syn 2.0.104", ] [[package]] name = "thread_local" -version = "1.1.8" +version = "1.1.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8b9ef9bad013ada3808854ceac7b46812a6465ba368859a37e2100283d2d719c" +checksum = "f60246a4944f24f6e018aa17cdeffb7818b76356965d03b07d6a9886e8962185" dependencies = [ "cfg-if", - "once_cell", +] + +[[package]] +name = "tiff" +version = "0.9.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ba1310fcea54c6a9a4fd1aad794ecc02c31682f6bfbecdf460bf19533eed1e3e" +dependencies = [ + "flate2", + "jpeg-decoder", + "weezl", ] [[package]] @@ -7555,7 +7939,7 @@ checksum = "6e06d43f1345a3bcd39f6a56dbb7dcab2ba47e68e8ac134855e7e2bdbaf8cab8" dependencies = [ "proc-macro2", "quote", - "syn 2.0.101", + "syn 2.0.104", ] [[package]] @@ -7777,13 +8161,13 @@ dependencies = [ [[package]] name = "tracing-attributes" -version = "0.1.29" +version = "0.1.30" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1b1ffbcf9c6f6b99d386e7444eb608ba646ae452a36b39737deb9663b610f662" +checksum = "81383ab64e72a7a8b8e13130c49e3dab29def6d0c7d76a03087b3cf71c5c6903" dependencies = [ "proc-macro2", "quote", - "syn 2.0.101", + "syn 2.0.104", ] [[package]] @@ -7918,7 +8302,7 @@ checksum = "3c36781cc0e46a83726d9879608e4cf6c2505237e263a8eb8c24502989cfdb28" dependencies = [ "proc-macro2", "quote", - "syn 2.0.101", + "syn 2.0.104", ] [[package]] @@ -7929,7 +8313,7 @@ checksum = "60d8d828da2a3d759d3519cdf29a5bac49c77d039ad36d0782edadbf9cd5415b" dependencies = [ "proc-macro2", "quote", - "syn 2.0.101", + "syn 2.0.104", ] [[package]] @@ -8060,9 +8444,9 @@ checksum = "7dd6e30e90baa6f72411720665d41d89b9a3d039dc45b8faea1ddd07f617f6af" [[package]] name = "unicode-width" -version = "0.2.0" +version = "0.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1fc81956842c57dac11422a97c3b8195a1ff727f06e85c84ed2e8aa277c9a0fd" +checksum = "4a1a07cc7db3810833284e8d372ccdc6da29741639ecc70c9ec107df0fa6154c" [[package]] name = "universal-hash" @@ -8156,7 +8540,7 @@ dependencies = [ "proc-macro2", "quote", "snafu", - "syn 2.0.101", + "syn 2.0.104", ] [[package]] @@ -8171,6 +8555,17 @@ dependencies = [ "wasm-bindgen", ] +[[package]] +name = "v_frame" +version = "0.3.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "666b7727c8875d6ab5db9533418d7c764233ac9c0cff1d469aec8fa127597be2" +dependencies = [ + "aligned-vec", + "num-traits", + "wasm-bindgen", +] + [[package]] name = "valuable" version = "0.1.1" @@ -8183,6 +8578,12 @@ version = "0.2.15" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "accd4ea62f7bb7a82fe23066fb0957d48ef677f6eeb8215f372f52e48bb32426" +[[package]] +name = "version-compare" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "852e951cb7832cb45cb1169900d19760cfa39b82bc0ea9c0e5a14ae88411c98b" + [[package]] name = "version_check" version = "0.9.5" @@ -8216,9 +8617,9 @@ dependencies = [ [[package]] name = "wasi" -version = "0.11.0+wasi-snapshot-preview1" +version = "0.11.1+wasi-snapshot-preview1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9c8d87e72b64a3b4db28d11ce29237c246188f4f51057d65a7eab63b7987e423" +checksum = "ccf3ec651a847eb01de73ccad15eb7d99f80485de043efb2f370cd654f4ea44b" [[package]] name = "wasi" @@ -8257,7 +8658,7 @@ dependencies = [ "log", "proc-macro2", "quote", - "syn 2.0.101", + "syn 2.0.104", "wasm-bindgen-shared", ] @@ -8292,7 +8693,7 @@ checksum = "8ae87ea40c9f689fc23f209965b6fb8a99ad69aeeb0231408be24920604395de" dependencies = [ "proc-macro2", "quote", - "syn 2.0.101", + "syn 2.0.104", "wasm-bindgen-backend", "wasm-bindgen-shared", ] @@ -8355,21 +8756,37 @@ dependencies = [ ] [[package]] -name = "webpki-roots" -version = "1.0.0" +name = "webp" +version = "0.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2853738d1cc4f2da3a225c18ec6c3721abb31961096e9dbf5ab35fa88b19cfdb" +checksum = "8f53152f51fb5af0c08484c33d16cca96175881d1f3dec068c23b31a158c2d99" +dependencies = [ + "image", + "libwebp-sys", +] + +[[package]] +name = "webpki-roots" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8782dd5a41a24eed3a4f40b606249b3e236ca61adf1f25ea4d45c73de122b502" dependencies = [ "rustls-pki-types", ] +[[package]] +name = "weezl" +version = "0.1.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a751b3277700db47d3e574514de2eced5e54dc8a5436a3bf7a0b248b2cee16f3" + [[package]] name = "whoami" version = "1.6.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6994d13118ab492c3c80c1f81928718159254c53c472bf9ce36f8dae4add02a7" dependencies = [ - "redox_syscall 0.5.12", + "redox_syscall 0.5.13", "wasite", ] @@ -8415,9 +8832,9 @@ dependencies = [ [[package]] name = "windows" -version = "0.61.1" +version = "0.61.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c5ee8f3d025738cb02bad7868bbb5f8a6327501e870bf51f1b455b0a2454a419" +checksum = "9babd3a767a4c1aef6900409f85f5d53ce2544ccdfaa86dad48c91782c6d6893" dependencies = [ "windows-collections", "windows-core", @@ -8467,7 +8884,7 @@ checksum = "a47fddd13af08290e67f4acabf4b459f647552718f683a7b415d290ac744a836" dependencies = [ "proc-macro2", "quote", - "syn 2.0.101", + "syn 2.0.104", ] [[package]] @@ -8478,14 +8895,14 @@ checksum = "bd9211b69f8dcdfa817bfd14bf1c97c9188afa36f4750130fcdf3f400eca9fa8" dependencies = [ "proc-macro2", "quote", - "syn 2.0.101", + "syn 2.0.104", ] [[package]] name = "windows-link" -version = "0.1.1" +version = "0.1.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "76840935b766e1b0a05c0066835fb9ec80071d4c09a16f6bd5f7e655e3c14c38" +checksum = "5e6ad25900d524eaabdbbb96d20b4311e1e7ae1699af4fb28c17ae66c80d798a" [[package]] name = "windows-numerics" @@ -8553,6 +8970,15 @@ dependencies = [ "windows-targets 0.52.6", ] +[[package]] +name = "windows-sys" +version = "0.60.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f2f500e4d28234f72040990ec9d39e3a6b950f9f22d3dba18416c35882612bcb" +dependencies = [ + "windows-targets 0.53.2", +] + [[package]] name = "windows-targets" version = "0.48.5" @@ -8577,13 +9003,29 @@ dependencies = [ "windows_aarch64_gnullvm 0.52.6", "windows_aarch64_msvc 0.52.6", "windows_i686_gnu 0.52.6", - "windows_i686_gnullvm", + "windows_i686_gnullvm 0.52.6", "windows_i686_msvc 0.52.6", "windows_x86_64_gnu 0.52.6", "windows_x86_64_gnullvm 0.52.6", "windows_x86_64_msvc 0.52.6", ] +[[package]] +name = "windows-targets" +version = "0.53.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c66f69fcc9ce11da9966ddb31a40968cad001c5bedeb5c2b82ede4253ab48aef" +dependencies = [ + "windows_aarch64_gnullvm 0.53.0", + "windows_aarch64_msvc 0.53.0", + "windows_i686_gnu 0.53.0", + "windows_i686_gnullvm 0.53.0", + "windows_i686_msvc 0.53.0", + "windows_x86_64_gnu 0.53.0", + "windows_x86_64_gnullvm 0.53.0", + "windows_x86_64_msvc 0.53.0", +] + [[package]] name = "windows-threading" version = "0.1.0" @@ -8605,6 +9047,12 @@ version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "32a4622180e7a0ec044bb555404c800bc9fd9ec262ec147edd5989ccd0c02cd3" +[[package]] +name = "windows_aarch64_gnullvm" +version = "0.53.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "86b8d5f90ddd19cb4a147a5fa63ca848db3df085e25fee3cc10b39b6eebae764" + [[package]] name = "windows_aarch64_msvc" version = "0.48.5" @@ -8617,6 +9065,12 @@ version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "09ec2a7bb152e2252b53fa7803150007879548bc709c039df7627cabbd05d469" +[[package]] +name = "windows_aarch64_msvc" +version = "0.53.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c7651a1f62a11b8cbd5e0d42526e55f2c99886c77e007179efff86c2b137e66c" + [[package]] name = "windows_i686_gnu" version = "0.48.5" @@ -8629,12 +9083,24 @@ version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8e9b5ad5ab802e97eb8e295ac6720e509ee4c243f69d781394014ebfe8bbfa0b" +[[package]] +name = "windows_i686_gnu" +version = "0.53.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c1dc67659d35f387f5f6c479dc4e28f1d4bb90ddd1a5d3da2e5d97b42d6272c3" + [[package]] name = "windows_i686_gnullvm" version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0eee52d38c090b3caa76c563b86c3a4bd71ef1a819287c19d586d7334ae8ed66" +[[package]] +name = "windows_i686_gnullvm" +version = "0.53.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9ce6ccbdedbf6d6354471319e781c0dfef054c81fbc7cf83f338a4296c0cae11" + [[package]] name = "windows_i686_msvc" version = "0.48.5" @@ -8647,6 +9113,12 @@ version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "240948bc05c5e7c6dabba28bf89d89ffce3e303022809e73deaefe4f6ec56c66" +[[package]] +name = "windows_i686_msvc" +version = "0.53.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "581fee95406bb13382d2f65cd4a908ca7b1e4c2f1917f143ba16efe98a589b5d" + [[package]] name = "windows_x86_64_gnu" version = "0.48.5" @@ -8659,6 +9131,12 @@ version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "147a5c80aabfbf0c7d901cb5895d1de30ef2907eb21fbbab29ca94c5b08b1a78" +[[package]] +name = "windows_x86_64_gnu" +version = "0.53.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2e55b5ac9ea33f2fc1716d1742db15574fd6fc8dadc51caab1c16a3d3b4190ba" + [[package]] name = "windows_x86_64_gnullvm" version = "0.48.5" @@ -8671,6 +9149,12 @@ version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "24d5b23dc417412679681396f2b49f3de8c1473deb516bd34410872eff51ed0d" +[[package]] +name = "windows_x86_64_gnullvm" +version = "0.53.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0a6e035dd0599267ce1ee132e51c27dd29437f63325753051e71dd9e42406c57" + [[package]] name = "windows_x86_64_msvc" version = "0.48.5" @@ -8684,10 +9168,16 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "589f6da84c646204747d1270a2a5661ea66ed1cced2631d546fdfb155959f9ec" [[package]] -name = "winnow" -version = "0.7.10" +name = "windows_x86_64_msvc" +version = "0.53.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c06928c8748d81b05c9be96aad92e1b6ff01833332f281e8cfca3be4b35fc9ec" +checksum = "271414315aff87387382ec3d271b52d7ae78726f5d44ac98b4f4030c91880486" + +[[package]] +name = "winnow" +version = "0.7.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "74c7b26e3480b707944fc872477815d29a8e429d2f93a1ce000f5fa84a15cbcd" dependencies = [ "memchr", ] @@ -8767,28 +9257,28 @@ checksum = "38da3c9736e16c5d3c8c597a9aaa5d1fa565d0532ae05e27c24aa62fb32c0ab6" dependencies = [ "proc-macro2", "quote", - "syn 2.0.101", + "syn 2.0.104", "synstructure", ] [[package]] name = "zerocopy" -version = "0.8.25" +version = "0.8.26" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a1702d9583232ddb9174e01bb7c15a2ab8fb1bc6f227aa1233858c351a3ba0cb" +checksum = "1039dd0d3c310cf05de012d8a39ff557cb0d23087fd44cad61df08fc31907a2f" dependencies = [ "zerocopy-derive", ] [[package]] name = "zerocopy-derive" -version = "0.8.25" +version = "0.8.26" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "28a6e20d751156648aa063f3800b706ee209a32c0b4d9f24be3d980b01be55ef" +checksum = "9ecf5b4cc5364572d7f4c329661bcc82724222973f2cab6f050a4e5c22f75181" dependencies = [ "proc-macro2", "quote", - "syn 2.0.101", + "syn 2.0.104", ] [[package]] @@ -8808,7 +9298,7 @@ checksum = "d71e5d6e06ab090c67b5e44993ec16b72dcbaabc526db883a360057678b48502" dependencies = [ "proc-macro2", "quote", - "syn 2.0.101", + "syn 2.0.104", "synstructure", ] @@ -8848,7 +9338,7 @@ checksum = "5b96237efa0c878c64bd89c436f661be4e46b2f3eff1ebb976f7ef2321d2f58f" dependencies = [ "proc-macro2", "quote", - "syn 2.0.101", + "syn 2.0.104", ] [[package]] @@ -8879,65 +9369,11 @@ dependencies = [ "pkg-config", ] -[[package]] -name = "zune-bmp" -version = "0.4.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3b1d6d8d84344ba202fcf02d6505ab57c2d775ae07dcb64809421735d8ce8c0f" -dependencies = [ - "zune-core", -] - [[package]] name = "zune-core" version = "0.4.12" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3f423a2c17029964870cfaabb1f13dfab7d092a62a29a89264f4d36990ca414a" -dependencies = [ - "log", - "serde", -] - -[[package]] -name = "zune-farbfeld" -version = "0.4.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "eea864ca80549c562044a8632421897a1e419ca460c20bd07fe84371f410f011" -dependencies = [ - "zune-core", -] - -[[package]] -name = "zune-hdr" -version = "0.4.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "51ff474631f80b14afc6dbc6bab78702ed2de7cd6af85f7a4ab21d3b08686426" -dependencies = [ - "zune-core", -] - -[[package]] -name = "zune-image" -version = "0.4.15" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3d6b98ac0fc8c650406e88f7b33f264798b85b7eff88cc09a8f86688c24d0261" -dependencies = [ - "bytemuck", - "jpeg-encoder", - "jxl-oxide", - "kamadak-exif", - "serde", - "zune-bmp", - "zune-core", - "zune-farbfeld", - "zune-hdr", - "zune-jpeg", - "zune-jpegxl", - "zune-png", - "zune-ppm", - "zune-psd", - "zune-qoi", -] [[package]] name = "zune-inflate" @@ -8956,50 +9392,3 @@ checksum = "0f6fe2e33d02a98ee64423802e16df3de99c43e5cf5ff983767e1128b394c8ac" dependencies = [ "zune-core", ] - -[[package]] -name = "zune-jpegxl" -version = "0.4.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "71ffee484384b15f99ed4768bfdb3fa186d63e1f8c3aafba1d6d141a7b9e3674" -dependencies = [ - "zune-core", -] - -[[package]] -name = "zune-png" -version = "0.4.10" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7d29c085769c6f29effea890f093120ac019375fdc789d2a496ba8ba96c77509" -dependencies = [ - "zune-core", - "zune-inflate", -] - -[[package]] -name = "zune-ppm" -version = "0.4.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0527e645233e3b34b10aa576b4b156e60f52f15b01f906789eb820f7ef4b2774" -dependencies = [ - "log", - "zune-core", -] - -[[package]] -name = "zune-psd" -version = "0.4.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ab11275d621813206ba4c20f4ec647eab0733c0fc71c3276d1238229fd834d49" -dependencies = [ - "zune-core", -] - -[[package]] -name = "zune-qoi" -version = "0.4.10" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "dc598b6729ede546413cb5077d8ddb83b46055a156d5cc3c625cfe122484e9c9" -dependencies = [ - "zune-core", -] diff --git a/Cargo.toml b/Cargo.toml index 3d54ee7..ab48481 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -66,6 +66,25 @@ mockito = { version = "1.6.1" } convert_case = "0.8" color-eyre = "0.6.5" inquire = "0.7.5" +image = "0.25.6" +uuid = { version = "1.6.0", features = ["v4"] } +maplit = "1.0.2" +once_cell = "1.20.2" +rand = "0.9.1" +rust_decimal = "1.37.2" +base64 = "0.22.1" +nom = "8.0.0" +percent-encoding = "2.3.1" +num-traits = "0.2.19" +http = "1.2.0" +async-stream = "0.3.6" +serde_variant = "0.1.3" +tracing-appender = "0.2.3" +clap = "4.5.40" +ipnetwork = "0.21.1" +typed-builder = "0.21.0" +nanoid = "0.4.0" +webp = "0.3.0" [patch.crates-io] seaography = { git = "https://github.com/dumtruck/seaography.git", rev = "a787c3a" } diff --git a/apps/recorder/Cargo.toml b/apps/recorder/Cargo.toml index e1366d6..69cbe91 100644 --- a/apps/recorder/Cargo.toml +++ b/apps/recorder/Cargo.toml @@ -14,7 +14,7 @@ path = "src/bin/main.rs" required-features = [] [features] -default = [] +default = ["jxl"] playground = ["dep:inquire", "dep:color-eyre"] testcontainers = [ "dep:testcontainers", @@ -23,6 +23,7 @@ testcontainers = [ "downloader/testcontainers", "testcontainers-modules/postgres", ] +jxl = ["dep:jpegxl-rs", "dep:jpegxl-sys"] [dependencies] downloader = { workspace = true } @@ -58,6 +59,25 @@ mockito = { workspace = true } color-eyre = { workspace = true, optional = true } inquire = { workspace = true, optional = true } convert_case = { workspace = true } +image = { workspace = true } +uuid = { workspace = true } +maplit = { workspace = true } +once_cell = { workspace = true } +rand = { workspace = true } +rust_decimal = { workspace = true } +base64 = { workspace = true } +nom = { workspace = true } +percent-encoding = { workspace = true } +num-traits = { workspace = true } +http = { workspace = true } +async-stream = { workspace = true } +serde_variant = { workspace = true } +tracing-appender = { workspace = true } +clap = { workspace = true } +ipnetwork = { workspace = true } +typed-builder = { workspace = true } +nanoid = { workspace = true } +webp = { workspace = true } sea-orm = { version = "1.1", features = [ "sqlx-sqlite", @@ -67,19 +87,13 @@ sea-orm = { version = "1.1", features = [ "debug-print", ] } figment = { version = "0.10", features = ["toml", "json", "env", "yaml"] } -uuid = { version = "1.6.0", features = ["v4"] } sea-orm-migration = { version = "1.1", features = ["runtime-tokio"] } rss = "2" fancy-regex = "0.14" -maplit = "1.0.2" lightningcss = "1.0.0-alpha.66" html-escape = "0.2.13" opendal = { version = "0.53", features = ["default", "services-fs"] } -zune-image = "0.4.15" -once_cell = "1.20.2" scraper = "0.23" - -log = "0.4" async-graphql = { version = "7", features = ["dynamic-schema"] } async-graphql-axum = "7" seaography = { version = "1.1", features = [ @@ -92,7 +106,6 @@ seaography = { version = "1.1", features = [ "with-postgres-array", "with-json-as-scalar", ] } -base64 = "0.22.1" tower = "0.5.2" tower-http = { version = "0.6", features = [ "trace", @@ -107,39 +120,26 @@ tower-http = { version = "0.6", features = [ tera = "1.20.0" openidconnect = { version = "4" } dotenvy = "0.15.7" -http = "1.2.0" -async-stream = "0.3.6" -serde_variant = "0.1.3" -tracing-appender = "0.2.3" -clap = "4.5.31" -ipnetwork = "0.21.1" -typed-builder = "0.21.0" -apalis = { version = "0.7", features = [ - "limit", - "tracing", - "catch-panic", - "retry", -] } +jpegxl-rs = { version = "0.11.2", optional = true } +jpegxl-sys = { version = "0.11.2", optional = true } + +apalis = { version = "0.7", features = ["limit", "tracing", "catch-panic"] } apalis-sql = { version = "0.7", features = ["postgres"] } cocoon = { version = "0.4.3", features = ["getrandom", "thiserror"] } -rand = "0.9.1" -rust_decimal = "1.37.1" reqwest_cookie_store = "0.8.0" -nanoid = "0.4.0" jwtk = "0.4.0" -percent-encoding = "2.3.1" mime_guess = "2.0.5" -nom = "8.0.0" icu_properties = "2.0.1" icu = "2.0.0" -num-traits = "0.2.19" tracing-tree = "0.4.0" - +num_cpus = "1.17.0" +headers-accept = "0.1.4" [dev-dependencies] +inquire = { workspace = true } +color-eyre = { workspace = true } + serial_test = "3" insta = { version = "1", features = ["redactions", "toml", "filters"] } rstest = "0.25" ctor = "0.4.0" -inquire = { workspace = true } -color-eyre = { workspace = true } diff --git a/apps/recorder/examples/optimize_image.mjs b/apps/recorder/examples/optimize_image.mjs deleted file mode 100644 index 3f5e220..0000000 --- a/apps/recorder/examples/optimize_image.mjs +++ /dev/null @@ -1,45 +0,0 @@ -#!/usr/bin/env zx -import { glob } from 'node:fs/promises'; -import os from 'node:os'; -import path from 'node:path'; -import { chunk } from 'es-toolkit/array'; - -const dataDir = path.join(import.meta.dirname, '../../../data') -/** - * @type {string[]} - */ -const images = []; -for await (const image of glob('**/*.{jpg,jpeg,png,gif,svg}', { - cwd: dataDir, -})) { - images.push(image) -} - -const cpus = os.cpus().length - 1; - -const chunkSize = Math.ceil(images.length / cpus); -const chunks = chunk(images, chunkSize); - -/** - * @param {string[]} images - */ -async function convertImages(images) { - for await (const image of images) { - const imagePath = path.resolve(dataDir, image) - const webp = imagePath.replace(path.extname(imagePath), '.webp') - const avif = imagePath.replace(path.extname(imagePath), '.avif') - console.log(`Converting ${imagePath} to ${webp}...`); - await $`ffmpeg -i "${imagePath}" -c:v libwebp -lossless 1 "${webp}"`; - console.log(`Converting ${imagePath} to ${avif}...`); - await $`ffmpeg -i "${imagePath}" -c:v libaom-av1 -still-picture 1 -pix_fmt yuv420p10le -crf 0 -strict experimental "${avif}"`; - } -} - -await Promise.all( - chunks.map(convertImages) -) - - - - - diff --git a/apps/recorder/src/app/builder.rs b/apps/recorder/src/app/builder.rs index 4a7e797..d5caab8 100644 --- a/apps/recorder/src/app/builder.rs +++ b/apps/recorder/src/app/builder.rs @@ -21,6 +21,9 @@ pub struct MainCliArgs { /// Explicit environment #[arg(short, long)] environment: Option, + + #[arg(long)] + graceful_shutdown: Option, } pub struct AppBuilder { @@ -28,6 +31,7 @@ pub struct AppBuilder { config_file: Option, working_dir: String, environment: Environment, + pub graceful_shutdown: bool, } impl AppBuilder { @@ -61,7 +65,8 @@ impl AppBuilder { builder = builder .config_file(args.config_file) .dotenv_file(args.dotenv_file) - .environment(environment); + .environment(environment) + .graceful_shutdown(args.graceful_shutdown.unwrap_or(true)); Ok(builder) } @@ -118,6 +123,12 @@ impl AppBuilder { ret } + pub fn graceful_shutdown(self, graceful_shutdown: bool) -> Self { + let mut ret = self; + ret.graceful_shutdown = graceful_shutdown; + ret + } + pub fn dotenv_file(self, dotenv_file: Option) -> Self { let mut ret = self; ret.dotenv_file = dotenv_file; @@ -141,6 +152,7 @@ impl Default for AppBuilder { dotenv_file: None, config_file: None, working_dir: String::from("."), + graceful_shutdown: true, } } } diff --git a/apps/recorder/src/app/config/default_mixin.toml b/apps/recorder/src/app/config/default_mixin.toml index 76d1987..3b032ac 100644 --- a/apps/recorder/src/app/config/default_mixin.toml +++ b/apps/recorder/src/app/config/default_mixin.toml @@ -11,6 +11,7 @@ leaky_bucket_initial_tokens = 0 leaky_bucket_refill_tokens = 1 leaky_bucket_refill_interval = 500 + [mikan.http_client.proxy] [mikan.http_client.proxy.headers] @@ -26,3 +27,5 @@ complexity_limit = inf [task] [message] + +[media] diff --git a/apps/recorder/src/app/config/mod.rs b/apps/recorder/src/app/config/mod.rs index 56d8802..84400aa 100644 --- a/apps/recorder/src/app/config/mod.rs +++ b/apps/recorder/src/app/config/mod.rs @@ -11,8 +11,8 @@ use super::env::Environment; use crate::{ auth::AuthConfig, cache::CacheConfig, crypto::CryptoConfig, database::DatabaseConfig, errors::RecorderResult, extract::mikan::MikanConfig, graphql::GraphQLConfig, - logger::LoggerConfig, message::MessageConfig, storage::StorageConfig, task::TaskConfig, - web::WebServerConfig, + logger::LoggerConfig, media::MediaConfig, message::MessageConfig, storage::StorageConfig, + task::TaskConfig, web::WebServerConfig, }; const DEFAULT_CONFIG_MIXIN: &str = include_str!("./default_mixin.toml"); @@ -27,6 +27,7 @@ pub struct AppConfig { pub mikan: MikanConfig, pub crypto: CryptoConfig, pub graphql: GraphQLConfig, + pub media: MediaConfig, pub logger: LoggerConfig, pub database: DatabaseConfig, pub task: TaskConfig, diff --git a/apps/recorder/src/app/context.rs b/apps/recorder/src/app/context.rs index 66d0a88..db1c5bb 100644 --- a/apps/recorder/src/app/context.rs +++ b/apps/recorder/src/app/context.rs @@ -6,7 +6,8 @@ use super::{Environment, config::AppConfig}; use crate::{ auth::AuthService, cache::CacheService, crypto::CryptoService, database::DatabaseService, errors::RecorderResult, extract::mikan::MikanClient, graphql::GraphQLService, - logger::LoggerService, message::MessageService, storage::StorageService, task::TaskService, + logger::LoggerService, media::MediaService, message::MessageService, storage::StorageService, + task::TaskService, }; pub trait AppContextTrait: Send + Sync + Debug { @@ -23,6 +24,7 @@ pub trait AppContextTrait: Send + Sync + Debug { fn crypto(&self) -> &CryptoService; fn task(&self) -> &TaskService; fn message(&self) -> &MessageService; + fn media(&self) -> &MediaService; } pub struct AppContext { @@ -37,6 +39,7 @@ pub struct AppContext { working_dir: String, environment: Environment, message: MessageService, + media: MediaService, task: OnceCell, graphql: OnceCell, } @@ -57,6 +60,7 @@ impl AppContext { let auth = AuthService::from_conf(config.auth).await?; let mikan = MikanClient::from_config(config.mikan).await?; let crypto = CryptoService::from_config(config.crypto).await?; + let media = MediaService::from_config(config.media).await?; let ctx = Arc::new(AppContext { config: config_cloned, @@ -70,6 +74,7 @@ impl AppContext { working_dir: working_dir.to_string(), crypto, message, + media, task: OnceCell::new(), graphql: OnceCell::new(), }); @@ -136,4 +141,7 @@ impl AppContextTrait for AppContext { fn message(&self) -> &MessageService { &self.message } + fn media(&self) -> &MediaService { + &self.media + } } diff --git a/apps/recorder/src/app/core.rs b/apps/recorder/src/app/core.rs index bc99be9..47739a8 100644 --- a/apps/recorder/src/app/core.rs +++ b/apps/recorder/src/app/core.rs @@ -6,7 +6,6 @@ use tracing::instrument; use super::{builder::AppBuilder, context::AppContextTrait}; use crate::{ - app::Environment, errors::{RecorderError, RecorderResult}, web::{ controller::{self, core::ControllerTrait}, @@ -76,22 +75,30 @@ impl App { .into_make_service_with_connect_info::(); let task = context.task(); + + let graceful_shutdown = self.builder.graceful_shutdown; + tokio::try_join!( async { - axum::serve(listener, router) - .with_graceful_shutdown(async move { - Self::shutdown_signal().await; - tracing::info!("axum shutting down..."); - }) - .await?; + let axum_serve = axum::serve(listener, router); + + if graceful_shutdown { + axum_serve + .with_graceful_shutdown(async move { + Self::shutdown_signal().await; + tracing::info!("axum shutting down..."); + }) + .await?; + } else { + axum_serve.await?; + } + Ok::<(), RecorderError>(()) }, async { { let monitor = task.setup_monitor().await?; - if matches!(context.environment(), Environment::Development) { - monitor.run().await?; - } else { + if graceful_shutdown { monitor .run_with_signal(async move { Self::shutdown_signal().await; @@ -99,6 +106,8 @@ impl App { Ok(()) }) .await?; + } else { + monitor.run().await?; } } diff --git a/apps/recorder/src/errors/app_error.rs b/apps/recorder/src/errors/app_error.rs index 689fd60..0b202af 100644 --- a/apps/recorder/src/errors/app_error.rs +++ b/apps/recorder/src/errors/app_error.rs @@ -29,6 +29,11 @@ pub enum RecorderError { #[snafu(source(from(Box, OptDynErr::some)))] source: OptDynErr, }, + #[snafu(transparent)] + ImageError { source: image::ImageError }, + #[cfg(feature = "jxl")] + #[snafu(transparent)] + JxlEncodeError { source: jpegxl_rs::EncodeError }, #[snafu(transparent, context(false))] HttpError { source: http::Error }, #[snafu(transparent, context(false))] diff --git a/apps/recorder/src/extract/defs.rs b/apps/recorder/src/extract/defs.rs index f570a26..321ba28 100644 --- a/apps/recorder/src/extract/defs.rs +++ b/apps/recorder/src/extract/defs.rs @@ -1,8 +1,5 @@ -use std::collections::HashMap; - use fancy_regex::Regex as FancyRegex; use lazy_static::lazy_static; -use maplit::hashmap; use regex::Regex; const LANG_ZH_TW: &str = "zh-tw"; @@ -34,40 +31,4 @@ lazy_static! { (LANG_JP, vec!["jp", "jpn", "日"]), ] }; - pub static ref BRACKETS_REG: Regex = Regex::new(r"[\[\]()【】()]").unwrap(); - pub static ref DIGIT_1PLUS_REG: Regex = Regex::new(r"\d+").unwrap(); - pub static ref ZH_NUM_MAP: HashMap<&'static str, i32> = { - hashmap! { - "〇" => 0, - "一" => 1, - "二" => 2, - "三" => 3, - "四" => 4, - "五" => 5, - "六" => 6, - "七" => 7, - "八" => 8, - "九" => 9, - "十" => 10, - "廿" => 20, - "百" => 100, - "千" => 1000, - "零" => 0, - "壹" => 1, - "贰" => 2, - "叁" => 3, - "肆" => 4, - "伍" => 5, - "陆" => 6, - "柒" => 7, - "捌" => 8, - "玖" => 9, - "拾" => 10, - "念" => 20, - "佰" => 100, - "仟" => 1000, - } - }; - pub static ref ZH_NUM_RE: Regex = - Regex::new(r"[〇一二三四五六七八九十廿百千零壹贰叁肆伍陆柒捌玖拾念佰仟]").unwrap(); } diff --git a/apps/recorder/src/extract/media/mod.rs b/apps/recorder/src/extract/media/mod.rs index 429d703..f4161c8 100644 --- a/apps/recorder/src/extract/media/mod.rs +++ b/apps/recorder/src/extract/media/mod.rs @@ -2,10 +2,6 @@ use url::Url; pub fn extract_image_src_from_str(image_src: &str, base_url: &Url) -> Option { let mut image_url = base_url.join(image_src).ok()?; - if let Some((_, value)) = image_url.query_pairs().find(|(key, _)| key == "webp") { - image_url.set_query(Some(&format!("webp={value}"))); - } else { - image_url.set_query(None); - } + image_url.set_query(None); Some(image_url) } diff --git a/apps/recorder/src/extract/mikan/subscription.rs b/apps/recorder/src/extract/mikan/subscription.rs index 8884d72..4c53339 100644 --- a/apps/recorder/src/extract/mikan/subscription.rs +++ b/apps/recorder/src/extract/mikan/subscription.rs @@ -556,13 +556,8 @@ mod tests { subscriptions::{self, SubscriptionTrait}, }, test_utils::{ - app::TestingAppContext, - crypto::build_testing_crypto_service, - database::build_testing_database_service, - mikan::{ - MikanMockServer, build_testing_mikan_client, build_testing_mikan_credential_form, - }, - storage::build_testing_storage_service, + app::{TestingAppContext, TestingAppContextPreset}, + mikan::{MikanMockServer, build_testing_mikan_credential_form}, tracing::try_init_testing_tracing, }, }; @@ -577,20 +572,11 @@ mod tests { let mikan_base_url = mikan_server.base_url().clone(); - let app_ctx = { - let mikan_client = build_testing_mikan_client(mikan_base_url.clone()).await?; - let db_service = build_testing_database_service(Default::default()).await?; - let crypto_service = build_testing_crypto_service().await?; - let storage_service = build_testing_storage_service().await?; - let app_ctx = TestingAppContext::builder() - .mikan(mikan_client) - .db(db_service) - .crypto(crypto_service) - .storage(storage_service) - .build(); - - Arc::new(app_ctx) - }; + let app_ctx = TestingAppContext::from_preset(TestingAppContextPreset { + mikan_base_url: mikan_base_url.to_string(), + database_config: None, + }) + .await?; Ok(TestingResources { app_ctx, diff --git a/apps/recorder/src/extract/mikan/web.rs b/apps/recorder/src/extract/mikan/web.rs index 3371f0a..8a42385 100644 --- a/apps/recorder/src/extract/mikan/web.rs +++ b/apps/recorder/src/extract/mikan/web.rs @@ -28,7 +28,12 @@ use crate::{ MIKAN_YEAR_QUERY_KEY, MikanClient, }, }, - storage::{StorageContentCategory, StorageService}, + media::{ + AutoOptimizeImageFormat, EncodeAvifOptions, EncodeImageOptions, EncodeJxlOptions, + EncodeWebpOptions, + }, + storage::StorageContentCategory, + task::{OptimizeImageTask, SystemTask}, }; #[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)] @@ -738,49 +743,92 @@ pub async fn scrape_mikan_poster_data_from_image_url( #[instrument(skip_all, fields(origin_poster_src_url = origin_poster_src_url.as_str()))] pub async fn scrape_mikan_poster_meta_from_image_url( - mikan_client: &MikanClient, - storage_service: &StorageService, + ctx: &dyn AppContextTrait, origin_poster_src_url: Url, ) -> RecorderResult { - if let Some(poster_src) = storage_service - .exists( - storage_service.build_public_object_path( - StorageContentCategory::Image, - MIKAN_POSTER_BUCKET_KEY, - &origin_poster_src_url - .path() - .replace(&format!("{MIKAN_BANGUMI_POSTER_PATH}/"), ""), - ), - ) - .await? - { - return Ok(MikanBangumiPosterMeta { + let storage_service = ctx.storage(); + let media_service = ctx.media(); + let mikan_client = ctx.mikan(); + let task_service = ctx.task(); + + let storage_path = storage_service.build_public_object_path( + StorageContentCategory::Image, + MIKAN_POSTER_BUCKET_KEY, + &origin_poster_src_url + .path() + .replace(&format!("{MIKAN_BANGUMI_POSTER_PATH}/"), ""), + ); + let meta = if let Some(poster_src) = storage_service.exists(&storage_path).await? { + MikanBangumiPosterMeta { origin_poster_src: origin_poster_src_url, poster_src: Some(poster_src.to_string()), - }); - } + } + } else { + let poster_data = + scrape_mikan_poster_data_from_image_url(mikan_client, origin_poster_src_url.clone()) + .await?; - let poster_data = - scrape_mikan_poster_data_from_image_url(mikan_client, origin_poster_src_url.clone()) + let poster_str = storage_service + .write(storage_path.clone(), poster_data) .await?; - let poster_str = storage_service - .write( - storage_service.build_public_object_path( - StorageContentCategory::Image, - MIKAN_POSTER_BUCKET_KEY, - &origin_poster_src_url - .path() - .replace(&format!("{MIKAN_BANGUMI_POSTER_PATH}/"), ""), - ), - poster_data, - ) - .await?; + tracing::warn!( + poster_str = poster_str.to_string(), + "mikan poster meta extracted" + ); - Ok(MikanBangumiPosterMeta { - origin_poster_src: origin_poster_src_url, - poster_src: Some(poster_str.to_string()), - }) + MikanBangumiPosterMeta { + origin_poster_src: origin_poster_src_url, + poster_src: Some(poster_str.to_string()), + } + }; + + if meta.poster_src.is_some() + && storage_path + .extension() + .is_some_and(|ext| media_service.is_legacy_image_format(ext)) + { + let auto_optimize_formats = &media_service.config.auto_optimize_formats; + + if auto_optimize_formats.contains(&AutoOptimizeImageFormat::Webp) { + let webp_storage_path = storage_path.with_extension("webp"); + if storage_service.exists(&webp_storage_path).await?.is_none() { + task_service + .add_system_task(SystemTask::OptimizeImage(OptimizeImageTask { + source_path: storage_path.clone().to_string(), + target_path: webp_storage_path.to_string(), + format_options: EncodeImageOptions::Webp(EncodeWebpOptions::default()), + })) + .await?; + } + } + if auto_optimize_formats.contains(&AutoOptimizeImageFormat::Avif) { + let avif_storage_path = storage_path.with_extension("avif"); + if storage_service.exists(&avif_storage_path).await?.is_none() { + task_service + .add_system_task(SystemTask::OptimizeImage(OptimizeImageTask { + source_path: storage_path.clone().to_string(), + target_path: avif_storage_path.to_string(), + format_options: EncodeImageOptions::Avif(EncodeAvifOptions::default()), + })) + .await?; + } + } + if auto_optimize_formats.contains(&AutoOptimizeImageFormat::Jxl) { + let jxl_storage_path = storage_path.with_extension("jxl"); + if storage_service.exists(&jxl_storage_path).await?.is_none() { + task_service + .add_system_task(SystemTask::OptimizeImage(OptimizeImageTask { + source_path: storage_path.clone().to_string(), + target_path: jxl_storage_path.to_string(), + format_options: EncodeImageOptions::Jxl(EncodeJxlOptions::default()), + })) + .await?; + } + } + } + + Ok(meta) } pub fn extract_mikan_bangumi_index_meta_list_from_season_flow_fragment( @@ -1007,24 +1055,23 @@ pub async fn scrape_mikan_bangumi_meta_list_from_season_flow_url( #[cfg(test)] mod test { #![allow(unused_variables)] - use std::{fs, sync::Arc}; + use std::{fs, io::Cursor, sync::Arc}; use futures::StreamExt; + use image::{ImageFormat, ImageReader}; use rstest::{fixture, rstest}; use tracing::Level; use url::Url; - use zune_image::{codecs::ImageFormat, image::Image}; use super::*; use crate::test_utils::{ - app::TestingAppContext, + app::{TestingAppContext, TestingAppContextPreset}, crypto::build_testing_crypto_service, database::build_testing_database_service, mikan::{ MikanMockServer, build_testing_mikan_client, build_testing_mikan_credential, build_testing_mikan_credential_form, }, - storage::build_testing_storage_service, tracing::try_init_testing_tracing, }; @@ -1049,12 +1096,14 @@ mod test { scrape_mikan_poster_data_from_image_url(&mikan_client, bangumi_poster_url).await?; resources_mock.shared_resource_mock.expect(1); - let image = Image::read(bgm_poster_data.to_vec(), Default::default()); + + let image = { + let c = Cursor::new(bgm_poster_data); + ImageReader::new(c) + }; + let image_format = image.with_guessed_format().ok().and_then(|i| i.format()); assert!( - image.is_ok_and(|img| img - .metadata() - .get_image_format() - .is_some_and(|fmt| matches!(fmt, ImageFormat::JPEG))), + image_format.is_some_and(|fmt| matches!(fmt, ImageFormat::Jpeg)), "should start with valid jpeg data magic number" ); @@ -1068,37 +1117,47 @@ mod test { let mikan_base_url = mikan_server.base_url().clone(); + let app_ctx = TestingAppContext::from_preset(TestingAppContextPreset { + mikan_base_url: mikan_base_url.to_string(), + database_config: None, + }) + .await?; + let resources_mock = mikan_server.mock_resources_with_doppel(); - let mikan_client = build_testing_mikan_client(mikan_base_url.clone()).await?; - - let storage_service = build_testing_storage_service().await?; - let storage_operator = storage_service.get_operator()?; - let bangumi_poster_url = mikan_base_url.join("/images/Bangumi/202309/5ce9fed1.jpg")?; - let bgm_poster = scrape_mikan_poster_meta_from_image_url( - &mikan_client, - &storage_service, - bangumi_poster_url, - ) - .await?; + let bgm_poster = + scrape_mikan_poster_meta_from_image_url(app_ctx.as_ref(), bangumi_poster_url).await?; resources_mock.shared_resource_mock.expect(1); + let storage_service = app_ctx.storage(); + let storage_fullname = storage_service.build_public_object_path( StorageContentCategory::Image, MIKAN_POSTER_BUCKET_KEY, "202309/5ce9fed1.jpg", ); - let storage_fullename_str = storage_fullname.as_str(); - assert!(storage_operator.exists(storage_fullename_str).await?); + assert!( + storage_service.exists(&storage_fullname).await?.is_some(), + "storage_fullename_str = {}, list public = {:?}", + &storage_fullname, + storage_service.list_public().await? + ); - let expected_data = - fs::read("tests/resources/mikan/doppel/images/Bangumi/202309/5ce9fed1.jpg")?; - let found_data = storage_operator.read(storage_fullename_str).await?.to_vec(); - assert_eq!(expected_data, found_data); + let bgm_poster_data = storage_service.read(&storage_fullname).await?; + + let image = { + let c = Cursor::new(bgm_poster_data.to_vec()); + ImageReader::new(c) + }; + let image_format = image.with_guessed_format().ok().and_then(|i| i.format()); + assert!( + image_format.is_some_and(|fmt| matches!(fmt, ImageFormat::Jpeg)), + "should start with valid jpeg data magic number" + ); Ok(()) } diff --git a/apps/recorder/src/lib.rs b/apps/recorder/src/lib.rs index 3b6d9d7..aea74cc 100644 --- a/apps/recorder/src/lib.rs +++ b/apps/recorder/src/lib.rs @@ -21,6 +21,7 @@ pub mod errors; pub mod extract; pub mod graphql; pub mod logger; +pub mod media; pub mod message; pub mod migrations; pub mod models; diff --git a/apps/recorder/src/media/config.rs b/apps/recorder/src/media/config.rs new file mode 100644 index 0000000..a1a6ce0 --- /dev/null +++ b/apps/recorder/src/media/config.rs @@ -0,0 +1,105 @@ +use serde::{Deserialize, Serialize}; + +#[derive(Clone, Debug, Serialize, Deserialize, PartialEq, Eq)] +pub enum AutoOptimizeImageFormat { + #[serde(rename = "image/webp")] + Webp, + #[serde(rename = "image/avif")] + Avif, + #[serde(rename = "image/jxl")] + Jxl, +} + +#[derive(Clone, Debug, Serialize, Deserialize, Default)] +pub struct EncodeWebpOptions { + pub quality: Option, +} + +#[derive(Clone, Debug, Serialize, Deserialize, Default)] +pub struct EncodeAvifOptions { + pub quality: Option, + pub speed: Option, + pub threads: Option, +} + +#[derive(Clone, Debug, Serialize, Deserialize, Default)] +pub struct EncodeJxlOptions { + pub quality: Option, + pub speed: Option, +} + +#[derive(Clone, Debug, Serialize, Deserialize)] +#[serde(tag = "mime_type")] +pub enum EncodeImageOptions { + #[serde(rename = "image/webp")] + Webp(EncodeWebpOptions), + #[serde(rename = "image/avif")] + Avif(EncodeAvifOptions), + #[serde(rename = "image/jxl")] + Jxl(EncodeJxlOptions), +} + +#[derive(Clone, Debug, Serialize, Deserialize)] +pub struct MediaConfig { + #[serde(default = "default_webp_quality")] + pub webp_quality: f32, + #[serde(default = "default_avif_quality")] + pub avif_quality: u8, + #[serde(default = "default_avif_speed")] + pub avif_speed: u8, + #[serde(default = "default_avif_threads")] + pub avif_threads: u8, + #[serde(default = "default_jxl_quality")] + pub jxl_quality: f32, + #[serde(default = "default_jxl_speed")] + pub jxl_speed: u8, + #[serde(default = "default_auto_optimize_formats")] + pub auto_optimize_formats: Vec, +} + +impl Default for MediaConfig { + fn default() -> Self { + Self { + webp_quality: default_webp_quality(), + avif_quality: default_avif_quality(), + avif_speed: default_avif_speed(), + avif_threads: default_avif_threads(), + jxl_quality: default_jxl_quality(), + jxl_speed: default_jxl_speed(), + auto_optimize_formats: default_auto_optimize_formats(), + } + } +} + +fn default_webp_quality() -> f32 { + 80.0 +} + +fn default_avif_quality() -> u8 { + 80 +} + +fn default_avif_speed() -> u8 { + 6 +} + +fn default_avif_threads() -> u8 { + 1 +} + +fn default_jxl_quality() -> f32 { + 80.0 +} + +fn default_jxl_speed() -> u8 { + 7 +} + +fn default_auto_optimize_formats() -> Vec { + vec![ + AutoOptimizeImageFormat::Webp, + // AutoOptimizeImageFormat::Avif, // TOO SLOW */ + #[cfg(feature = "jxl")] + AutoOptimizeImageFormat::Jxl, + ] +} diff --git a/apps/recorder/src/media/mod.rs b/apps/recorder/src/media/mod.rs new file mode 100644 index 0000000..25546ec --- /dev/null +++ b/apps/recorder/src/media/mod.rs @@ -0,0 +1,8 @@ +mod config; +mod service; + +pub use config::{ + AutoOptimizeImageFormat, EncodeAvifOptions, EncodeImageOptions, EncodeJxlOptions, + EncodeWebpOptions, MediaConfig, +}; +pub use service::MediaService; diff --git a/apps/recorder/src/media/service.rs b/apps/recorder/src/media/service.rs new file mode 100644 index 0000000..c94286e --- /dev/null +++ b/apps/recorder/src/media/service.rs @@ -0,0 +1,199 @@ +use std::io::Cursor; + +use bytes::Bytes; +use image::{GenericImageView, ImageEncoder, ImageReader, codecs::avif::AvifEncoder}; +use quirks_path::Path; +use snafu::ResultExt; + +use crate::{ + errors::{RecorderError, RecorderResult}, + media::{EncodeAvifOptions, EncodeJxlOptions, EncodeWebpOptions, MediaConfig}, +}; + +#[derive(Debug)] +pub struct MediaService { + pub config: MediaConfig, +} + +impl MediaService { + pub async fn from_config(config: MediaConfig) -> RecorderResult { + Ok(Self { config }) + } + + pub fn is_legacy_image_format(&self, ext: &str) -> bool { + matches!(ext, "jpeg" | "jpg" | "png") + } + + pub async fn optimize_image_to_webp( + &self, + path: impl AsRef, + data: impl Into, + options: Option, + ) -> RecorderResult { + let quality = options + .and_then(|o| o.quality) + .unwrap_or(self.config.webp_quality); + + let data = data.into(); + + tokio::task::spawn_blocking(move || -> RecorderResult { + let cursor = Cursor::new(data); + let image_reader = ImageReader::new(cursor).with_guessed_format()?; + + let img = image_reader.decode()?; + + let (width, height) = (img.width(), img.height()); + + let color = img.color(); + + let webp_data = if color.has_alpha() { + let rgba_image = img.into_rgba8(); + + let encoder = webp::Encoder::from_rgba(&rgba_image, width, height); + + encoder.encode(quality) + } else { + let rgba_image = img.into_rgb8(); + + let encoder = webp::Encoder::from_rgb(&rgba_image, width, height); + + encoder.encode(quality) + }; + + Ok(Bytes::from(webp_data.to_vec())) + }) + .await + .with_whatever_context::<_, String, RecorderError>(|_| { + format!( + "failed to spawn blocking task to optimize legacy image to webp: {}", + path.as_ref().display() + ) + })? + } + + pub async fn optimize_image_to_avif( + &self, + path: impl AsRef, + data: Bytes, + options: Option, + ) -> RecorderResult { + let quality = options + .as_ref() + .and_then(|o| o.quality) + .unwrap_or(self.config.avif_quality); + let speed = options + .as_ref() + .and_then(|o| o.speed) + .unwrap_or(self.config.avif_speed); + let threads = options + .as_ref() + .and_then(|o| o.threads) + .unwrap_or(self.config.avif_threads); + + tokio::task::spawn_blocking(move || -> RecorderResult { + let mut buf = vec![]; + + { + let cursor = Cursor::new(data); + let image_reader = ImageReader::new(cursor).with_guessed_format()?; + + let img = image_reader.decode()?; + + let (width, height) = img.dimensions(); + let color_type = img.color(); + let encoder = AvifEncoder::new_with_speed_quality(&mut buf, speed, quality) + .with_num_threads(Some(threads as usize)); + + encoder.write_image(img.as_bytes(), width, height, color_type.into())?; + } + + Ok(Bytes::from(buf)) + }) + .await + .with_whatever_context::<_, String, RecorderError>(|_| { + format!( + "failed to spawn blocking task to optimize legacy image to avif: {}", + path.as_ref().display() + ) + })? + } + + #[cfg(feature = "jxl")] + pub async fn optimize_image_to_jxl( + &self, + path: impl AsRef, + data: Bytes, + options: Option, + ) -> RecorderResult { + let quality = options + .as_ref() + .and_then(|o| o.quality) + .unwrap_or(self.config.jxl_quality); + let speed = options + .as_ref() + .and_then(|o| o.speed) + .unwrap_or(self.config.jxl_speed); + tokio::task::spawn_blocking(move || -> RecorderResult { + use jpegxl_rs::encode::{ColorEncoding, EncoderResult, EncoderSpeed}; + let cursor = Cursor::new(data); + let image_reader = ImageReader::new(cursor).with_guessed_format()?; + + let image = image_reader.decode()?; + let (width, height) = image.dimensions(); + + let color = image.color(); + let has_alpha = color.has_alpha(); + let libjxl_speed = { + match speed { + 0 | 1 => EncoderSpeed::Lightning, + 2 => EncoderSpeed::Thunder, + 3 => EncoderSpeed::Falcon, + 4 => EncoderSpeed::Cheetah, + 5 => EncoderSpeed::Hare, + 6 => EncoderSpeed::Wombat, + 7 => EncoderSpeed::Squirrel, + 8 => EncoderSpeed::Kitten, + _ => EncoderSpeed::Tortoise, + } + }; + + let mut encoder_builder = jpegxl_rs::encoder_builder() + .lossless(false) + .has_alpha(has_alpha) + .color_encoding(ColorEncoding::Srgb) + .speed(libjxl_speed) + .jpeg_quality(quality) + .build()?; + + let buffer: EncoderResult = if color.has_alpha() { + let sample = image.into_rgba8(); + encoder_builder.encode(&sample, width, height)? + } else { + let sample = image.into_rgb8(); + encoder_builder.encode(&sample, width, height)? + }; + + Ok(Bytes::from(buffer.data)) + }) + .await + .with_whatever_context::<_, String, RecorderError>(|_| { + format!( + "failed to spawn blocking task to optimize legacy image to avif: {}", + path.as_ref().display() + ) + })? + } + + #[cfg(not(feature = "jxl"))] + pub async fn optimize_image_to_jxl( + &self, + _path: impl AsRef, + _data: Bytes, + _options: Option, + ) -> RecorderResult { + Err(RecorderError::Whatever { + message: "jxl feature is not enabled".to_string(), + source: None.into(), + }) + } +} diff --git a/apps/recorder/src/models/bangumi.rs b/apps/recorder/src/models/bangumi.rs index b96c6ea..30e14bc 100644 --- a/apps/recorder/src/models/bangumi.rs +++ b/apps/recorder/src/models/bangumi.rs @@ -121,7 +121,6 @@ impl ActiveModel { _subscription_id: i32, ) -> RecorderResult { let mikan_client = ctx.mikan(); - let storage_service = ctx.storage(); let mikan_base_url = mikan_client.base_url(); let season_comp = SeasonComp::parse_comp(&meta.bangumi_title) .ok() @@ -136,12 +135,8 @@ impl ActiveModel { ); let poster_link = if let Some(origin_poster_src) = meta.origin_poster_src.clone() { - let poster_meta = scrape_mikan_poster_meta_from_image_url( - mikan_client, - storage_service, - origin_poster_src, - ) - .await?; + let poster_meta = + scrape_mikan_poster_meta_from_image_url(ctx, origin_poster_src).await?; poster_meta.poster_src } else { None diff --git a/apps/recorder/src/storage/client.rs b/apps/recorder/src/storage/client.rs index 918015c..1a0f129 100644 --- a/apps/recorder/src/storage/client.rs +++ b/apps/recorder/src/storage/client.rs @@ -5,6 +5,7 @@ use axum::{body::Body, response::Response}; use axum_extra::{TypedHeader, headers::Range}; use bytes::Bytes; use futures::{Stream, StreamExt}; +use headers_accept::Accept; use http::{HeaderValue, StatusCode, header}; use opendal::{Buffer, Metadata, Operator, Reader, Writer, layers::LoggingLayer}; use quirks_path::{Path, PathBuf}; @@ -56,22 +57,24 @@ impl fmt::Display for StorageStoredUrl { #[derive(Debug, Clone)] pub struct StorageService { pub data_dir: String, + pub operator: Operator, } impl StorageService { pub async fn from_config(config: StorageConfig) -> RecorderResult { Ok(Self { data_dir: config.data_dir.to_string(), + operator: Self::get_operator(&config.data_dir)?, }) } - pub fn get_operator(&self) -> Result { + pub fn get_operator(data_dir: &str) -> Result { let op = if cfg!(test) { Operator::new(opendal::services::Memory::default())? .layer(LoggingLayer::default()) .finish() } else { - Operator::new(opendal::services::Fs::default().root(&self.data_dir))? + Operator::new(opendal::services::Fs::default().root(data_dir))? .layer(LoggingLayer::default()) .finish() }; @@ -125,7 +128,7 @@ impl StorageService { path: P, data: Bytes, ) -> Result { - let operator = self.get_operator()?; + let operator = &self.operator; let path = path.into(); @@ -145,7 +148,7 @@ impl StorageService { &self, path: P, ) -> Result, opendal::Error> { - let operator = self.get_operator()?; + let operator = &self.operator; let path = path.to_string(); @@ -157,7 +160,7 @@ impl StorageService { } pub async fn read(&self, path: impl AsRef) -> Result { - let operator = self.get_operator()?; + let operator = &self.operator; let data = operator.read(path.as_ref()).await?; @@ -165,7 +168,7 @@ impl StorageService { } pub async fn reader(&self, path: impl AsRef) -> Result { - let operator = self.get_operator()?; + let operator = &self.operator; let reader = operator.reader(path.as_ref()).await?; @@ -173,7 +176,7 @@ impl StorageService { } pub async fn writer(&self, path: impl AsRef) -> Result { - let operator = self.get_operator()?; + let operator = &self.operator; let writer = operator.writer(path.as_ref()).await?; @@ -181,13 +184,57 @@ impl StorageService { } pub async fn stat(&self, path: impl AsRef) -> Result { - let operator = self.get_operator()?; + let operator = &self.operator; let metadata = operator.stat(path.as_ref()).await?; Ok(metadata) } + #[cfg(test)] + pub async fn list_public(&self) -> Result, opendal::Error> { + use futures::TryStreamExt; + let lister = self.operator.lister_with("public/").recursive(true).await?; + lister.try_collect().await + } + + #[cfg(test)] + pub async fn list_subscribers(&self) -> Result, opendal::Error> { + use futures::TryStreamExt; + let lister = self + .operator + .lister_with("subscribers/") + .recursive(true) + .await?; + lister.try_collect().await + } + + #[instrument(skip_all, err, fields(storage_path = %storage_path.as_ref(), range = ?range, accept = ?accept))] + pub async fn serve_optimized_image( + &self, + storage_path: impl AsRef, + range: Option>, + accept: Accept, + ) -> RecorderResult { + let storage_path = Path::new(storage_path.as_ref()); + for mime_type in accept.media_types() { + let accpetable_path = match mime_type.subty().as_str() { + "webp" => Some(storage_path.with_extension("webp")), + "avif" => Some(storage_path.with_extension("avif")), + "jxl" => Some(storage_path.with_extension("jxl")), + _ => None, + }; + if let Some(accpetable_path) = accpetable_path + && self.exists(&accpetable_path).await?.is_some() + && self.stat(&accpetable_path).await?.is_file() + { + return self.serve_file(accpetable_path, range).await; + } + } + + self.serve_file(storage_path, range).await + } + #[instrument(skip_all, err, fields(storage_path = %storage_path.as_ref(), range = ?range))] pub async fn serve_file( &self, diff --git a/apps/recorder/src/task/config.rs b/apps/recorder/src/task/config.rs index 7c63057..7d8379e 100644 --- a/apps/recorder/src/task/config.rs +++ b/apps/recorder/src/task/config.rs @@ -1,4 +1,50 @@ +use std::time::Duration; + use serde::{Deserialize, Serialize}; #[derive(Debug, Clone, Serialize, Deserialize)] -pub struct TaskConfig {} +pub struct TaskConfig { + #[serde(default = "default_subscriber_task_workers")] + pub subscriber_task_concurrency: u32, + #[serde(default = "default_system_task_workers")] + pub system_task_concurrency: u32, + #[serde(default = "default_subscriber_task_timeout")] + pub subscriber_task_timeout: Duration, + #[serde(default = "default_system_task_timeout")] + pub system_task_timeout: Duration, +} + +impl Default for TaskConfig { + fn default() -> Self { + Self { + subscriber_task_concurrency: default_subscriber_task_workers(), + system_task_concurrency: default_system_task_workers(), + subscriber_task_timeout: default_subscriber_task_timeout(), + system_task_timeout: default_system_task_timeout(), + } + } +} + +pub fn default_subscriber_task_workers() -> u32 { + if cfg!(test) { + 1 + } else { + ((num_cpus::get_physical() as f32 / 2.0).floor() as u32).max(1) + } +} + +pub fn default_system_task_workers() -> u32 { + if cfg!(test) { + 1 + } else { + ((num_cpus::get_physical() as f32 / 2.0).floor() as u32).max(1) + } +} + +pub fn default_subscriber_task_timeout() -> Duration { + Duration::from_secs(3600) +} + +pub fn default_system_task_timeout() -> Duration { + Duration::from_secs(3600) +} diff --git a/apps/recorder/src/task/core.rs b/apps/recorder/src/task/core.rs index 4a8bec0..e568bb0 100644 --- a/apps/recorder/src/task/core.rs +++ b/apps/recorder/src/task/core.rs @@ -5,10 +5,11 @@ use serde::{Serialize, de::DeserializeOwned}; use crate::{app::AppContextTrait, errors::RecorderResult}; +pub const SYSTEM_TASK_APALIS_NAME: &str = "system_task"; pub const SUBSCRIBER_TASK_APALIS_NAME: &str = "subscriber_task"; #[async_trait::async_trait] -pub trait SubscriberAsyncTaskTrait: Serialize + DeserializeOwned + Sized { +pub trait AsyncTaskTrait: Serialize + DeserializeOwned + Sized { async fn run_async(self, ctx: Arc) -> RecorderResult<()>; async fn run(self, ctx: Arc) -> RecorderResult<()> { @@ -19,7 +20,7 @@ pub trait SubscriberAsyncTaskTrait: Serialize + DeserializeOwned + Sized { } #[async_trait::async_trait] -pub trait SubscriberStreamTaskTrait: Serialize + DeserializeOwned + Sized { +pub trait StreamTaskTrait: Serialize + DeserializeOwned + Sized { type Yield: Serialize + DeserializeOwned + Send; fn run_stream( diff --git a/apps/recorder/src/task/mod.rs b/apps/recorder/src/task/mod.rs index 877b153..f530518 100644 --- a/apps/recorder/src/task/mod.rs +++ b/apps/recorder/src/task/mod.rs @@ -4,13 +4,16 @@ mod r#extern; mod registry; mod service; -pub use core::{SUBSCRIBER_TASK_APALIS_NAME, SubscriberAsyncTaskTrait, SubscriberStreamTaskTrait}; +pub use core::{ + AsyncTaskTrait, SUBSCRIBER_TASK_APALIS_NAME, SYSTEM_TASK_APALIS_NAME, StreamTaskTrait, +}; pub use config::TaskConfig; pub use r#extern::{ApalisJobs, ApalisSchema}; pub use registry::{ - SubscriberTask, SubscriberTaskType, SubscriberTaskTypeEnum, SubscriberTaskTypeVariant, - SubscriberTaskTypeVariantIter, SyncOneSubscriptionFeedsFullTask, - SyncOneSubscriptionFeedsIncrementalTask, SyncOneSubscriptionSourcesTask, + OptimizeImageTask, SubscriberTask, SubscriberTaskType, SubscriberTaskTypeEnum, + SubscriberTaskTypeVariant, SubscriberTaskTypeVariantIter, SyncOneSubscriptionFeedsFullTask, + SyncOneSubscriptionFeedsIncrementalTask, SyncOneSubscriptionSourcesTask, SystemTask, + SystemTaskType, SystemTaskTypeEnum, SystemTaskTypeVariant, SystemTaskTypeVariantIter, }; pub use service::TaskService; diff --git a/apps/recorder/src/task/registry/media.rs b/apps/recorder/src/task/registry/media.rs new file mode 100644 index 0000000..86b4de3 --- /dev/null +++ b/apps/recorder/src/task/registry/media.rs @@ -0,0 +1,53 @@ +use std::sync::Arc; + +use quirks_path::Path; +use serde::{Deserialize, Serialize}; +use tracing::instrument; + +use crate::{ + app::AppContextTrait, errors::RecorderResult, media::EncodeImageOptions, task::AsyncTaskTrait, +}; + +#[derive(Clone, Debug, Serialize, Deserialize)] +pub struct OptimizeImageTask { + pub source_path: String, + pub target_path: String, + pub format_options: EncodeImageOptions, +} + +#[async_trait::async_trait] +impl AsyncTaskTrait for OptimizeImageTask { + #[instrument(err, skip(ctx))] + async fn run_async(self, ctx: Arc) -> RecorderResult<()> { + let storage = ctx.storage(); + + let source_path = Path::new(&self.source_path); + + let media_service = ctx.media(); + + let image_data = storage.read(source_path).await?; + + match self.format_options { + EncodeImageOptions::Webp(options) => { + let data = media_service + .optimize_image_to_webp(source_path, image_data.to_bytes(), Some(options)) + .await?; + storage.write(self.target_path, data).await?; + } + EncodeImageOptions::Avif(options) => { + let data = media_service + .optimize_image_to_avif(source_path, image_data.to_bytes(), Some(options)) + .await?; + storage.write(self.target_path, data).await?; + } + EncodeImageOptions::Jxl(options) => { + let data = media_service + .optimize_image_to_jxl(source_path, image_data.to_bytes(), Some(options)) + .await?; + storage.write(self.target_path, data).await?; + } + }; + + Ok(()) + } +} diff --git a/apps/recorder/src/task/registry/mod.rs b/apps/recorder/src/task/registry/mod.rs index 76469f8..cca15ae 100644 --- a/apps/recorder/src/task/registry/mod.rs +++ b/apps/recorder/src/task/registry/mod.rs @@ -1,6 +1,8 @@ +mod media; mod subscription; use std::sync::Arc; +pub use media::OptimizeImageTask; use sea_orm::{DeriveActiveEnum, DeriveDisplay, EnumIter, FromJsonQueryResult}; use serde::{Deserialize, Serialize}; pub use subscription::{ @@ -8,11 +10,11 @@ pub use subscription::{ SyncOneSubscriptionSourcesTask, }; -use super::SubscriberAsyncTaskTrait; use crate::{ app::AppContextTrait, errors::{RecorderError, RecorderResult}, models::subscriptions::SubscriptionTrait, + task::AsyncTaskTrait, }; #[derive( @@ -97,3 +99,36 @@ impl SubscriberTask { } } } + +#[derive( + Clone, + Debug, + Serialize, + Deserialize, + PartialEq, + Eq, + Copy, + DeriveActiveEnum, + DeriveDisplay, + EnumIter, +)] +#[sea_orm(rs_type = "String", db_type = "Text")] +pub enum SystemTaskType { + #[serde(rename = "optimize_image")] + #[sea_orm(string_value = "optimize_image")] + OptimizeImage, +} + +#[derive(Clone, Debug, Serialize, Deserialize, FromJsonQueryResult)] +pub enum SystemTask { + #[serde(rename = "optimize_image")] + OptimizeImage(OptimizeImageTask), +} + +impl SystemTask { + pub async fn run(self, ctx: Arc) -> RecorderResult<()> { + match self { + Self::OptimizeImage(task) => task.run(ctx).await, + } + } +} diff --git a/apps/recorder/src/task/registry/subscription.rs b/apps/recorder/src/task/registry/subscription.rs index 28b1235..eae87cc 100644 --- a/apps/recorder/src/task/registry/subscription.rs +++ b/apps/recorder/src/task/registry/subscription.rs @@ -7,7 +7,7 @@ use crate::{ app::AppContextTrait, errors::RecorderResult, models::subscriptions::{self, SubscriptionTrait}, - task::SubscriberAsyncTaskTrait, + task::AsyncTaskTrait, }; #[derive(Clone, Debug, Serialize, Deserialize, PartialEq, Eq)] @@ -20,7 +20,7 @@ impl From for SyncOneSubscriptionFeedsIncrementalTa } #[async_trait::async_trait] -impl SubscriberAsyncTaskTrait for SyncOneSubscriptionFeedsIncrementalTask { +impl AsyncTaskTrait for SyncOneSubscriptionFeedsIncrementalTask { async fn run_async(self, ctx: Arc) -> RecorderResult<()> { self.0.sync_feeds_incremental(ctx).await?; Ok(()) @@ -37,7 +37,7 @@ impl From for SyncOneSubscriptionFeedsFullTask { } #[async_trait::async_trait] -impl SubscriberAsyncTaskTrait for SyncOneSubscriptionFeedsFullTask { +impl AsyncTaskTrait for SyncOneSubscriptionFeedsFullTask { async fn run_async(self, ctx: Arc) -> RecorderResult<()> { self.0.sync_feeds_full(ctx).await?; Ok(()) @@ -48,7 +48,7 @@ impl SubscriberAsyncTaskTrait for SyncOneSubscriptionFeedsFullTask { pub struct SyncOneSubscriptionSourcesTask(pub subscriptions::Subscription); #[async_trait::async_trait] -impl SubscriberAsyncTaskTrait for SyncOneSubscriptionSourcesTask { +impl AsyncTaskTrait for SyncOneSubscriptionSourcesTask { async fn run_async(self, ctx: Arc) -> RecorderResult<()> { self.0.sync_sources(ctx).await?; Ok(()) diff --git a/apps/recorder/src/task/service.rs b/apps/recorder/src/task/service.rs index 97fa4f8..6210f45 100644 --- a/apps/recorder/src/task/service.rs +++ b/apps/recorder/src/task/service.rs @@ -11,28 +11,47 @@ use tokio::sync::RwLock; use crate::{ app::AppContextTrait, errors::{RecorderError, RecorderResult}, - task::{SUBSCRIBER_TASK_APALIS_NAME, SubscriberTask, TaskConfig}, + task::{ + SUBSCRIBER_TASK_APALIS_NAME, SYSTEM_TASK_APALIS_NAME, SubscriberTask, TaskConfig, + config::{default_subscriber_task_workers, default_system_task_workers}, + registry::SystemTask, + }, }; pub struct TaskService { pub config: TaskConfig, ctx: Arc, subscriber_task_storage: Arc>>, + system_task_storage: Arc>>, } impl TaskService { pub async fn from_config_and_ctx( - config: TaskConfig, + mut config: TaskConfig, ctx: Arc, ) -> RecorderResult { + if config.subscriber_task_concurrency == 0 { + config.subscriber_task_concurrency = default_subscriber_task_workers(); + }; + if config.system_task_concurrency == 0 { + config.system_task_concurrency = default_system_task_workers(); + }; + let pool = ctx.db().get_postgres_connection_pool().clone(); - let storage_config = Config::new(SUBSCRIBER_TASK_APALIS_NAME); - let subscriber_task_storage = PostgresStorage::new_with_config(pool, storage_config); + let subscriber_task_storage_config = + Config::new(SUBSCRIBER_TASK_APALIS_NAME).set_keep_alive(config.subscriber_task_timeout); + let system_task_storage_config = + Config::new(SYSTEM_TASK_APALIS_NAME).set_keep_alive(config.system_task_timeout); + let subscriber_task_storage = + PostgresStorage::new_with_config(pool.clone(), subscriber_task_storage_config); + let system_task_storage = + PostgresStorage::new_with_config(pool, system_task_storage_config); Ok(Self { config, ctx, subscriber_task_storage: Arc::new(RwLock::new(subscriber_task_storage)), + system_task_storage: Arc::new(RwLock::new(system_task_storage)), }) } @@ -45,6 +64,14 @@ impl TaskService { job.run(ctx).await } + async fn run_system_task( + job: SystemTask, + data: Data>, + ) -> RecorderResult<()> { + let ctx = data.deref().clone(); + job.run(ctx).await + } + pub async fn retry_subscriber_task(&self, job_id: String) -> RecorderResult<()> { { let mut storage = self.subscriber_task_storage.write().await; @@ -58,6 +85,19 @@ impl TaskService { Ok(()) } + pub async fn retry_system_task(&self, job_id: String) -> RecorderResult<()> { + { + let mut storage = self.system_task_storage.write().await; + let task_id = + TaskId::from_str(&job_id).map_err(|err| RecorderError::InvalidTaskId { + message: err.to_string(), + })?; + let worker_id = WorkerId::new(SYSTEM_TASK_APALIS_NAME); + storage.retry(&worker_id, &task_id).await?; + } + Ok(()) + } + pub async fn add_subscriber_task( &self, _subscriber_id: i32, @@ -77,11 +117,27 @@ impl TaskService { Ok(task_id) } + pub async fn add_system_task(&self, system_task: SystemTask) -> RecorderResult { + let task_id = { + let mut storage = self.system_task_storage.write().await; + let sql_context = { + let mut c = SqlContext::default(); + c.set_max_attempts(1); + c + }; + let request = Request::new_with_ctx(system_task, sql_context); + storage.push_request(request).await?.task_id + }; + + Ok(task_id) + } + pub async fn setup_monitor(&self) -> RecorderResult { let mut monitor = Monitor::new(); { let subscriber_task_worker = WorkerBuilder::new(SUBSCRIBER_TASK_APALIS_NAME) + .concurrency(self.config.subscriber_task_concurrency as usize) .catch_panic() .enable_tracing() .data(self.ctx.clone()) @@ -91,7 +147,17 @@ impl TaskService { }) .build_fn(Self::run_subscriber_task); - monitor = monitor.register(subscriber_task_worker); + let system_task_worker = WorkerBuilder::new(SYSTEM_TASK_APALIS_NAME) + .concurrency(self.config.system_task_concurrency as usize) + .catch_panic() + .enable_tracing() + .data(self.ctx.clone()) + .backend(self.system_task_storage.read().await.clone()) + .build_fn(Self::run_system_task); + + monitor = monitor + .register(subscriber_task_worker) + .register(system_task_worker); } Ok(monitor) @@ -99,13 +165,18 @@ impl TaskService { pub async fn setup_listener(&self) -> RecorderResult { let pool = self.ctx.db().get_postgres_connection_pool().clone(); - let mut subscriber_task_listener = PgListen::new(pool).await?; + let mut task_listener = PgListen::new(pool).await?; { let mut subscriber_task_storage = self.subscriber_task_storage.write().await; - subscriber_task_listener.subscribe_with(&mut subscriber_task_storage); + task_listener.subscribe_with(&mut subscriber_task_storage); } - Ok(subscriber_task_listener) + { + let mut system_task_storage = self.system_task_storage.write().await; + task_listener.subscribe_with(&mut system_task_storage); + } + + Ok(task_listener) } } diff --git a/apps/recorder/src/test_utils/app.rs b/apps/recorder/src/test_utils/app.rs index 70bb31a..9bec8ec 100644 --- a/apps/recorder/src/test_utils/app.rs +++ b/apps/recorder/src/test_utils/app.rs @@ -3,7 +3,17 @@ use std::{fmt::Debug, sync::Arc}; use once_cell::sync::OnceCell; use typed_builder::TypedBuilder; -use crate::app::AppContextTrait; +use crate::{ + app::AppContextTrait, + test_utils::{ + crypto::build_testing_crypto_service, + database::{TestingDatabaseServiceConfig, build_testing_database_service}, + media::build_testing_media_service, + mikan::build_testing_mikan_client, + storage::build_testing_storage_service, + task::build_testing_task_service, + }, +}; #[derive(TypedBuilder)] #[builder(field_defaults(default, setter(strip_option)))] @@ -17,6 +27,7 @@ pub struct TestingAppContext { graphql: Option, storage: Option, crypto: Option, + media: Option, #[builder(default = Arc::new(OnceCell::new()), setter(!strip_option))] task: Arc>, message: Option, @@ -30,6 +41,32 @@ impl TestingAppContext { pub fn set_task(&self, task: crate::task::TaskService) { self.task.get_or_init(|| task); } + + pub async fn from_preset( + preset: TestingAppContextPreset, + ) -> crate::errors::RecorderResult> { + let mikan_client = build_testing_mikan_client(preset.mikan_base_url.clone()).await?; + let db_service = + build_testing_database_service(preset.database_config.unwrap_or_default()).await?; + let crypto_service = build_testing_crypto_service().await?; + let storage_service = build_testing_storage_service().await?; + let media_service = build_testing_media_service().await?; + let app_ctx = Arc::new( + TestingAppContext::builder() + .mikan(mikan_client) + .db(db_service) + .crypto(crypto_service) + .storage(storage_service) + .media(media_service) + .build(), + ); + + let task_service = build_testing_task_service(app_ctx.clone()).await?; + + app_ctx.set_task(task_service); + + Ok(app_ctx) + } } impl Debug for TestingAppContext { @@ -90,4 +127,13 @@ impl AppContextTrait for TestingAppContext { fn message(&self) -> &crate::message::MessageService { self.message.as_ref().expect("should set message") } + + fn media(&self) -> &crate::media::MediaService { + self.media.as_ref().expect("should set media") + } +} + +pub struct TestingAppContextPreset { + pub mikan_base_url: String, + pub database_config: Option, } diff --git a/apps/recorder/src/test_utils/database.rs b/apps/recorder/src/test_utils/database.rs index 9838617..11df594 100644 --- a/apps/recorder/src/test_utils/database.rs +++ b/apps/recorder/src/test_utils/database.rs @@ -17,6 +17,10 @@ impl Default for TestingDatabaseServiceConfig { pub async fn build_testing_database_service( config: TestingDatabaseServiceConfig, ) -> RecorderResult { + tracing::info!( + "enable testcontainers feature, build testing database service in testcontainers..." + ); + use testcontainers::{ImageExt, runners::AsyncRunner}; use testcontainers_ext::{ImageDefaultLogConsumerExt, ImagePruneExistedLabelExt}; use testcontainers_modules::postgres::Postgres; @@ -38,6 +42,11 @@ pub async fn build_testing_database_service( let connection_string = format!("postgres://konobangu:konobangu@{host_ip}:{host_port}/konobangu"); + tracing::debug!( + "testing database service connection string: {}", + connection_string + ); + let mut db_service = DatabaseService::from_config(DatabaseConfig { uri: connection_string, enable_logging: true, diff --git a/apps/recorder/src/test_utils/media.rs b/apps/recorder/src/test_utils/media.rs new file mode 100644 index 0000000..9cc8022 --- /dev/null +++ b/apps/recorder/src/test_utils/media.rs @@ -0,0 +1,8 @@ +use crate::{ + errors::RecorderResult, + media::{MediaConfig, MediaService}, +}; + +pub async fn build_testing_media_service() -> RecorderResult { + MediaService::from_config(MediaConfig::default()).await +} diff --git a/apps/recorder/src/test_utils/mod.rs b/apps/recorder/src/test_utils/mod.rs index 6b72912..69e3a15 100644 --- a/apps/recorder/src/test_utils/mod.rs +++ b/apps/recorder/src/test_utils/mod.rs @@ -1,6 +1,7 @@ pub mod app; pub mod crypto; pub mod database; +pub mod media; pub mod mikan; pub mod storage; pub mod task; diff --git a/apps/recorder/src/test_utils/task.rs b/apps/recorder/src/test_utils/task.rs index 5b1e9a0..2c028e4 100644 --- a/apps/recorder/src/test_utils/task.rs +++ b/apps/recorder/src/test_utils/task.rs @@ -9,7 +9,7 @@ use crate::{ pub async fn build_testing_task_service( ctx: Arc, ) -> RecorderResult { - let config = TaskConfig {}; + let config = TaskConfig::default(); let task_service = TaskService::from_config_and_ctx(config, ctx).await?; Ok(task_service) } diff --git a/apps/recorder/src/test_utils/tracing.rs b/apps/recorder/src/test_utils/tracing.rs index ae33ec5..e90cdf4 100644 --- a/apps/recorder/src/test_utils/tracing.rs +++ b/apps/recorder/src/test_utils/tracing.rs @@ -9,7 +9,7 @@ fn build_testing_tracing_filter(level: Level) -> EnvFilter { let level = level.as_str().to_lowercase(); let mut filter = EnvFilter::new(format!("{crate_name}[]={level}")); - let mut modules = vec!["mockito"]; + let mut modules = vec!["mockito", "testcontainers"]; modules.extend(MODULE_WHITELIST.iter()); for module in modules { filter = filter.add_directive(format!("{module}[]={level}").parse().unwrap()); diff --git a/apps/recorder/src/web/controller/static/mod.rs b/apps/recorder/src/web/controller/static/mod.rs index 8cc42ed..5e1585a 100644 --- a/apps/recorder/src/web/controller/static/mod.rs +++ b/apps/recorder/src/web/controller/static/mod.rs @@ -2,12 +2,14 @@ use std::sync::Arc; use axum::{ Extension, Router, - extract::{Path, State}, + extract::{Path, Query, State}, middleware::from_fn_with_state, response::Response, routing::get, }; use axum_extra::{TypedHeader, headers::Range}; +use headers_accept::Accept; +use serde::{Deserialize, Serialize}; use crate::{ app::AppContextTrait, @@ -18,33 +20,75 @@ use crate::{ pub const CONTROLLER_PREFIX: &str = "/api/static"; +#[derive(Debug, Clone, Deserialize, Serialize, PartialEq, Eq)] +pub enum OptimizeType { + #[serde(rename = "accept")] + AcceptHeader, +} + +#[derive(Debug, Clone, Deserialize, Serialize)] +pub struct StaticQuery { + optimize: Option, +} + async fn serve_subscriber_static( State(ctx): State>, Path((subscriber_id, path)): Path<(i32, String)>, Extension(auth_user_info): Extension, + Query(query): Query, range: Option>, + accept: Option>, ) -> RecorderResult { if subscriber_id != auth_user_info.subscriber_auth.id { Err(AuthError::PermissionError)?; } - let storage = ctx.storage(); + let media = ctx.media(); let storage_path = storage.build_subscriber_path(subscriber_id, &path); - storage.serve_file(storage_path, range).await + if query + .optimize + .is_some_and(|optimize| optimize == OptimizeType::AcceptHeader) + && storage_path + .extension() + .is_some_and(|ext| media.is_legacy_image_format(ext)) + && let Some(TypedHeader(accept)) = accept + { + storage + .serve_optimized_image(storage_path, range, accept) + .await + } else { + storage.serve_file(storage_path, range).await + } } async fn serve_public_static( State(ctx): State>, Path(path): Path, + Query(query): Query, range: Option>, + accept: Option>, ) -> RecorderResult { let storage = ctx.storage(); + let media = ctx.media(); let storage_path = storage.build_public_path(&path); - storage.serve_file(storage_path, range).await + if query + .optimize + .is_some_and(|optimize| optimize == OptimizeType::AcceptHeader) + && storage_path + .extension() + .is_some_and(|ext| media.is_legacy_image_format(ext)) + && let Some(TypedHeader(accept)) = accept + { + storage + .serve_optimized_image(storage_path, range, accept) + .await + } else { + storage.serve_file(storage_path, range).await + } } pub async fn create(ctx: Arc) -> RecorderResult { diff --git a/apps/webui/src/components/ui/img.tsx b/apps/webui/src/components/ui/img.tsx index 2815a82..0d6bf91 100644 --- a/apps/webui/src/components/ui/img.tsx +++ b/apps/webui/src/components/ui/img.tsx @@ -1,38 +1,45 @@ -import { type ComponentProps, useMemo, useState } from "react"; +import { useInject } from "@/infra/di/inject"; +import { DOCUMENT } from "@/infra/platform/injection"; +import { type ComponentProps, useMemo } from "react"; + +const URL_PARSE_REGEX = /^([^?#]*)(\?[^#]*)?(#.*)?$/; + +function parseURL(url: string) { + const match = url.match(URL_PARSE_REGEX); + + if (!match) { + return { other: url, search: "", hash: "" }; + } + + return { + other: match[1] || "", + search: match[2] || "", + hash: match[3] || "", + }; +} export type ImgProps = Omit, "alt"> & Required, "alt">> & { - optimize?: boolean; + optimize?: "accept"; }; -const LEGACY_IMAGE_REGEX = /\.(jpg|jpeg|png|gif|svg)$/; +export const Img = ({ + src: propsSrc, + optimize = "accept", + ...props +}: ImgProps) => { + const document = useInject(DOCUMENT); + const src = useMemo(() => { + const baseURI = document?.baseURI; + if (!propsSrc || !baseURI) { + return propsSrc; + } + const { other, search, hash } = parseURL(propsSrc); + const searchParams = new URLSearchParams(search); + searchParams.set("optimize", optimize); + return `${other}?${searchParams.toString()}${hash}`; + }, [propsSrc, optimize, document?.baseURI]); -export const Img = (props: ImgProps) => { - const src = props.src; - - const isLegacy = useMemo(() => src?.match(LEGACY_IMAGE_REGEX), [src]); - const [isError, setIsError] = useState(false); - - if (!src) { - // biome-ignore lint/nursery/noImgElement: - return {props.alt}; - } - - return ( - - {isLegacy && !isError && ( - <> - - - - )} - {props.alt} setIsError(true)} /> - - ); + // biome-ignore lint/nursery/noImgElement: + return {props.alt}; }; diff --git a/justfile b/justfile index eb828b8..fa935e0 100644 --- a/justfile +++ b/justfile @@ -4,7 +4,7 @@ set dotenv-load := true prepare-dev: cargo install cargo-binstall cargo binstall sea-orm-cli cargo-llvm-cov cargo-nextest - # install watchexec just zellij + # install watchexec just zellij nasm libjxl prepare-dev-testcontainers: docker pull linuxserver/qbittorrent:latest @@ -22,7 +22,7 @@ dev-proxy: pnpm run --parallel --filter=proxy dev dev-recorder: - watchexec -r -e rs,toml,yaml,json,env -- cargo run -p recorder --bin recorder_cli -- --environment development + watchexec -r -e rs,toml,yaml,json,env -- cargo run -p recorder --bin recorder_cli -- --environment=development --graceful-shutdown=false dev-recorder-migrate-down: cargo run -p recorder --bin migrate_down -- --environment development