diff options
| author | metamuffin <metamuffin@disroot.org> | 2025-12-11 01:20:17 +0100 |
|---|---|---|
| committer | metamuffin <metamuffin@disroot.org> | 2025-12-11 01:20:17 +0100 |
| commit | 6e5f6d9b9c6fedb4ab80190c156595d321d33bbf (patch) | |
| tree | b6c2140e744fc3018ad08975afefad40386ebbc6 | |
| parent | e4f865e9da9d6660399e22a6fbeb5b84a749b07a (diff) | |
| download | jellything-6e5f6d9b9c6fedb4ab80190c156595d321d33bbf.tar jellything-6e5f6d9b9c6fedb4ab80190c156595d321d33bbf.tar.bz2 jellything-6e5f6d9b9c6fedb4ab80190c156595d321d33bbf.tar.zst | |
refactor import plugins part 3
| -rw-r--r-- | Cargo.lock | 191 | ||||
| -rw-r--r-- | common/src/routes.rs | 5 | ||||
| -rw-r--r-- | import/src/lib.rs | 747 | ||||
| -rw-r--r-- | import/src/plugins/acoustid.rs | 3 | ||||
| -rw-r--r-- | import/src/plugins/misc.rs | 37 | ||||
| -rw-r--r-- | import/src/plugins/mod.rs | 20 | ||||
| -rw-r--r-- | import/src/plugins/trakt.rs | 2 | ||||
| -rw-r--r-- | import/src/reporting.rs | 2 | ||||
| -rw-r--r-- | locale/en.ini | 2 | ||||
| -rw-r--r-- | logic/src/admin/mod.rs | 10 | ||||
| -rw-r--r-- | server/src/routes.rs | 14 | ||||
| -rw-r--r-- | server/src/ui/admin/import.rs | 70 | ||||
| -rw-r--r-- | server/src/ui/admin/log.rs | 4 | ||||
| -rw-r--r-- | server/src/ui/admin/mod.rs | 17 | ||||
| -rw-r--r-- | ui/src/admin/import.rs | 50 | ||||
| -rw-r--r-- | ui/src/admin/mod.rs | 10 | ||||
| -rw-r--r-- | web/script/import_live.ts | 64 | ||||
| -rw-r--r-- | web/script/log_live.ts (renamed from web/script/log_stream.ts) | 0 | ||||
| -rw-r--r-- | web/script/main.ts | 3 |
19 files changed, 735 insertions, 516 deletions
@@ -359,9 +359,9 @@ checksum = "72b3254f16251a8381aa12e40e3c4d2f0199f8c6508fbecb9d91f575e0fbb8c6" [[package]] name = "base64ct" -version = "1.8.0" +version = "1.8.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "55248b47b0caf0546f7988906588779981c43bb1bc9d0c44087278f80cdb44ba" +checksum = "0e050f626429857a27ddccb31e0aca21356bfa709c04041aefddac081a8f068a" [[package]] name = "binascii" @@ -513,9 +513,9 @@ checksum = "b35204fbdc0b3f4446b89fc1ac2cf84a8a68971995d0bf2e925ec7cd960f9cb3" [[package]] name = "cc" -version = "1.2.46" +version = "1.2.49" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b97463e1064cb1b1c1384ad0a0b9c8abd0988e2a91f52606c80ef14aadb63e36" +checksum = "90583009037521a116abf44494efecd645ba48b6622457080f080b85544e2215" dependencies = [ "find-msvc-tools", "jobserver", @@ -577,9 +577,9 @@ dependencies = [ [[package]] name = "clap" -version = "4.5.51" +version = "4.5.53" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4c26d721170e0295f191a69bd9a1f93efcdb0aff38684b61ab5750468972e5f5" +checksum = "c9e340e012a1bf4935f5282ed1436d1489548e8f72308207ea5df0e23d2d03f8" dependencies = [ "clap_builder", "clap_derive", @@ -587,9 +587,9 @@ dependencies = [ [[package]] name = "clap_builder" -version = "4.5.51" +version = "4.5.53" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "75835f0c7bf681bfd05abe44e965760fea999a5286c6eb2d59883634fd02011a" +checksum = "d76b5d13eaa18c901fd2f7fca939fefe3a0727a953561fefdf3b2922b8569d00" dependencies = [ "anstream", "anstyle", @@ -599,9 +599,9 @@ dependencies = [ [[package]] name = "clap_complete" -version = "4.5.60" +version = "4.5.61" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8e602857739c5a4291dfa33b5a298aeac9006185229a700e5810a3ef7272d971" +checksum = "39615915e2ece2550c0149addac32fb5bd312c657f43845bb9088cb9c8a7c992" dependencies = [ "clap", ] @@ -902,7 +902,7 @@ checksum = "117240f60069e65410b3ae1bb213295bd828f707b5bec6596a1afc8793ce0cbc" [[package]] name = "ebml" version = "0.1.0" -source = "git+https://codeberg.org/metamuffin/ebml-rs#b93e9c26f3b465cd747d28a7ade6f5617bcf3f44" +source = "git+https://codeberg.org/metamuffin/ebml-rs#1535ae503f67b82d8a08d2c2787156f5bd4ffbba" dependencies = [ "ebml-derive", ] @@ -910,7 +910,7 @@ dependencies = [ [[package]] name = "ebml-derive" version = "0.1.0" -source = "git+https://codeberg.org/metamuffin/ebml-rs#b93e9c26f3b465cd747d28a7ade6f5617bcf3f44" +source = "git+https://codeberg.org/metamuffin/ebml-rs#1535ae503f67b82d8a08d2c2787156f5bd4ffbba" dependencies = [ "darling", "quote", @@ -1288,9 +1288,9 @@ dependencies = [ [[package]] name = "gif" -version = "0.14.0" +version = "0.14.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f954a9e9159ec994f73a30a12b96a702dde78f5547bcb561174597924f7d4162" +checksum = "f5df2ba84018d80c213569363bdcd0c64e6933c67fe4c1d60ecf822971a3c35e" dependencies = [ "color_quant", "weezl", @@ -1345,9 +1345,9 @@ dependencies = [ [[package]] name = "hashbrown" -version = "0.16.0" +version = "0.16.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5419bdc4f6a9207fbeba6d11b604d481addf78ecd10c11ad51e76c2f6482748d" +checksum = "841d1cc9bed7f9236f321df977030373f4a4163ae1a7dbfe1a51a2c1a51d9100" [[package]] name = "heck" @@ -1404,12 +1404,11 @@ dependencies = [ [[package]] name = "http" -version = "1.3.1" +version = "1.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f4a85d31aea989eead29a3aaf9e1115a180df8282431156e533de47660892565" +checksum = "e3ba2a386d7f85a81f119ad7498ebe444d2e22c2af0b86b069416ace48b3311a" dependencies = [ "bytes", - "fnv", "itoa", ] @@ -1431,7 +1430,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1efedce1fb8e6913f23e0c92de8e62cd5b772a67e7b3946df930a62566c93184" dependencies = [ "bytes", - "http 1.3.1", + "http 1.4.0", ] [[package]] @@ -1442,7 +1441,7 @@ checksum = "b021d93e26becf5dc7e1b75b1bed1fd93124b374ceb73f43d4d4eafec896a64a" dependencies = [ "bytes", "futures-core", - "http 1.3.1", + "http 1.4.0", "http-body 1.0.1", "pin-project-lite", ] @@ -1502,7 +1501,7 @@ dependencies = [ "bytes", "futures-channel", "futures-core", - "http 1.3.1", + "http 1.4.0", "http-body 1.0.1", "httparse", "itoa", @@ -1519,7 +1518,7 @@ version = "0.27.7" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e3c93eb611681b207e1fe55d5a71ecf91572ec8a6705cdb6857f7d8d5242cf58" dependencies = [ - "http 1.3.1", + "http 1.4.0", "hyper 1.8.1", "hyper-util", "rustls", @@ -1532,16 +1531,16 @@ dependencies = [ [[package]] name = "hyper-util" -version = "0.1.18" +version = "0.1.19" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "52e9a2a24dc5c6821e71a7030e1e14b7b632acac55c40e9d2e082c621261bb56" +checksum = "727805d60e7938b76b826a6ef209eb70eaa1812794f9424d4a4e2d740662df5f" dependencies = [ "base64", "bytes", "futures-channel", "futures-core", "futures-util", - "http 1.3.1", + "http 1.4.0", "http-body 1.0.1", "hyper 1.8.1", "ipnet", @@ -1635,9 +1634,9 @@ checksum = "7aedcccd01fc5fe81e6b489c15b247b8b0690feb23304303a9e560f37efc560a" [[package]] name = "icu_properties" -version = "2.1.1" +version = "2.1.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e93fcd3157766c0c8da2f8cff6ce651a31f0810eaa1c51ec363ef790bbb5fb99" +checksum = "020bfc02fe870ec3a66d93e677ccca0562506e5872c650f893269e08615d74ec" dependencies = [ "icu_collections", "icu_locale_core", @@ -1649,9 +1648,9 @@ dependencies = [ [[package]] name = "icu_properties_data" -version = "2.1.1" +version = "2.1.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "02845b3647bb045f1100ecd6480ff52f34c35f82d9880e029d329c21d1054899" +checksum = "616c294cf8d725c6afcd8f55abc17c56464ef6211f9ed59cccffe534129c77af" [[package]] name = "icu_provider" @@ -1716,7 +1715,7 @@ dependencies = [ "rgb", "tiff", "zune-core 0.5.0", - "zune-jpeg 0.5.5", + "zune-jpeg 0.5.6", ] [[package]] @@ -1755,12 +1754,12 @@ checksum = "e7c5cedc30da3a610cac6b4ba17597bdf7152cf974e8aab3afb3d54455e371c8" [[package]] name = "indexmap" -version = "2.12.0" +version = "2.12.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6717a8d2a5a929a1a2eb43a12812498ed141a0bcfb7e8f7844fbdbe4303bba9f" +checksum = "0ad4bb2b565bca0645f4d68c5c9af97fba094e9791da685bf83cb5f3ce74acf2" dependencies = [ "equivalent", - "hashbrown 0.16.0", + "hashbrown 0.16.1", "serde", "serde_core", ] @@ -2115,9 +2114,9 @@ dependencies = [ [[package]] name = "js-sys" -version = "0.3.82" +version = "0.3.83" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b011eec8cc36da2aab2d5cff675ec18454fad408585853910a202391cf9f8e65" +checksum = "464a3709c7f55f1f721e5389aa6ea4e3bc6aba669353300af094b29ffbdde1d8" dependencies = [ "once_cell", "wasm-bindgen", @@ -2173,9 +2172,9 @@ dependencies = [ [[package]] name = "libc" -version = "0.2.177" +version = "0.2.178" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2874a2af47a2325c2001a6e6fad9b16a53b802102b528163885171cf92b15976" +checksum = "37c93d8daa9d8a012fd8ab92f088405fb202ea0b6ab73ee2482ae66af4f42091" [[package]] name = "libdav1d-sys" @@ -2235,9 +2234,9 @@ dependencies = [ [[package]] name = "log" -version = "0.4.28" +version = "0.4.29" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "34080505efa8e45a4b816c349525ebe327ceaa8559756f0356cba97ef3bf7432" +checksum = "5e5032e24019045c762d3c0f28f5b6b8bbf38563a65908389bf7978758920897" [[package]] name = "loom" @@ -2326,7 +2325,7 @@ dependencies = [ [[package]] name = "matroska" version = "0.1.0" -source = "git+https://codeberg.org/metamuffin/ebml-rs#b93e9c26f3b465cd747d28a7ade6f5617bcf3f44" +source = "git+https://codeberg.org/metamuffin/ebml-rs#1535ae503f67b82d8a08d2c2787156f5bd4ffbba" dependencies = [ "ebml", "serde", @@ -2396,9 +2395,9 @@ dependencies = [ [[package]] name = "mio" -version = "1.1.0" +version = "1.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "69d83b0086dc8ecf3ce9ae2874b2d1290252e2a30720bea58a5c6639b0092873" +checksum = "a69bcab0ad47271a0234d9422b131806bf3968021e5dc9328caf2d4cd58557fc" dependencies = [ "libc", "wasi", @@ -2407,9 +2406,9 @@ dependencies = [ [[package]] name = "moxcms" -version = "0.7.9" +version = "0.7.10" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0fbdd3d7436f8b5e892b8b7ea114271ff0fa00bc5acae845d53b07d498616ef6" +checksum = "80986bbbcf925ebd3be54c26613d861255284584501595cf418320c078945608" dependencies = [ "num-traits", "pxfm", @@ -2424,7 +2423,7 @@ dependencies = [ "bytes", "encoding_rs", "futures-util", - "http 1.3.1", + "http 1.4.0", "httparse", "memchr", "mime", @@ -2889,9 +2888,9 @@ dependencies = [ [[package]] name = "pxfm" -version = "0.1.25" +version = "0.1.27" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a3cbdf373972bf78df4d3b518d07003938e2c7d1fb5891e55f9cb6df57009d84" +checksum = "7186d3822593aa4393561d186d1393b3923e9d6163d3fbfd6e825e3e6cf3e6a8" dependencies = [ "num-traits", ] @@ -3234,16 +3233,16 @@ checksum = "7a2d987857b319362043e95f5353c0535c1f58eec5336fdfcf626430af7def58" [[package]] name = "reqwest" -version = "0.12.24" +version = "0.12.25" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9d0946410b9f7b082a427e4ef5c8ff541a88b357bc6c637c40db3a68ac70a36f" +checksum = "b6eff9328d40131d43bd911d42d79eb6a47312002a4daefc9e37f17e74a7701a" dependencies = [ "base64", "bytes", "futures-channel", "futures-core", "futures-util", - "http 1.3.1", + "http 1.4.0", "http-body 1.0.1", "http-body-util", "hyper 1.8.1", @@ -3432,9 +3431,9 @@ dependencies = [ [[package]] name = "rustls-pki-types" -version = "1.13.0" +version = "1.13.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "94182ad936a0c91c324cd46c6511b9510ed16af436d7b5bab34beab0afd55f7a" +checksum = "708c0f9d5f54ba0272468c1d306a52c495b31fa155e91bc25371e6df7996908c" dependencies = [ "web-time", "zeroize", @@ -3609,9 +3608,9 @@ dependencies = [ [[package]] name = "shell-words" -version = "1.1.0" +version = "1.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "24188a676b6ae68c3b2cb3a01be17fbf7240ce009799bb56d5b1409051e78fde" +checksum = "dc6fe69c597f9c37bfeeeeeb33da3530379845f10be461a66d16d03eca2ded77" [[package]] name = "shlex" @@ -3621,9 +3620,9 @@ checksum = "0fda2ff0d084019ba4d7c6f371c95d8fd75ce3524c3cb8fb653a3023f6323e64" [[package]] name = "signal-hook-registry" -version = "1.4.6" +version = "1.4.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b2a4719bff48cee6b39d12c020eeb490953ad2443b7055bd0b21fca26bd8c28b" +checksum = "7664a098b8e616bdfcc2dc0e9ac44eb231eedf41db4e9fe95d8d32ec728dedad" dependencies = [ "libc", ] @@ -3643,9 +3642,9 @@ dependencies = [ [[package]] name = "simd-adler32" -version = "0.3.7" +version = "0.3.8" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d66dc143e6b11c1eddc06d5c423cfc97062865baf299914ab64caa38182078fe" +checksum = "e320a6c5ad31d271ad523dcf3ad13e2767ad8b1cb8f047f75a8aeaf8da139da2" [[package]] name = "simd_helpers" @@ -3750,9 +3749,9 @@ checksum = "13c2bddecc57b384dee18652358fb23172facb8a2c51ccc10d74c157bdea3292" [[package]] name = "syn" -version = "2.0.110" +version = "2.0.111" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a99801b5bd34ede4cf3fc688c5919368fea4e4814a4664359503e6015b280aea" +checksum = "390cc9a294ab71bdb1aa2e99d13be9c753cd2d7bd6560c77118597410c4d2e87" dependencies = [ "proc-macro2", "quote", @@ -4189,14 +4188,14 @@ dependencies = [ [[package]] name = "tower-http" -version = "0.6.6" +version = "0.6.8" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "adc82fd73de2a9722ac5da747f12383d2bfdb93591ee6c58486e0097890f05f2" +checksum = "d4e6559d53cc268e5031cd8429d05415bc4cb4aefc4aa5d6cc35fbf5b924a1f8" dependencies = [ "bitflags", "bytes", "futures-util", - "http 1.3.1", + "http 1.4.0", "http-body 1.0.1", "iri-string", "pin-project-lite", @@ -4219,9 +4218,9 @@ checksum = "8df9b6e13f2d32c91b9bd719c00d1958837bc7dec474d94952798cc8e69eeec3" [[package]] name = "tracing" -version = "0.1.41" +version = "0.1.43" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "784e0ac535deb450455cbfa28a6f0df145ea1bb7ae51b821cf5e7927fdcfbdd0" +checksum = "2d15d90a0b5c19378952d479dc858407149d7bb45a14de0142f6c534b16fc647" dependencies = [ "pin-project-lite", "tracing-attributes", @@ -4230,9 +4229,9 @@ dependencies = [ [[package]] name = "tracing-attributes" -version = "0.1.30" +version = "0.1.31" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "81383ab64e72a7a8b8e13130c49e3dab29def6d0c7d76a03087b3cf71c5c6903" +checksum = "7490cfa5ec963746568740651ac6781f701c9c5ea257c58e057f3ba8cf69e8da" dependencies = [ "proc-macro2", "quote", @@ -4241,9 +4240,9 @@ dependencies = [ [[package]] name = "tracing-core" -version = "0.1.34" +version = "0.1.35" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b9d12581f227e93f094d3af2ae690a574abb8a2b9b7a96e7cfe9647b2b617678" +checksum = "7a04e24fab5c89c6a36eb8558c9656f30d81de51dfa4d3b45f26b21d61fa0a6c" dependencies = [ "once_cell", "valuable", @@ -4262,9 +4261,9 @@ dependencies = [ [[package]] name = "tracing-subscriber" -version = "0.3.20" +version = "0.3.22" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2054a14f5307d601f88daf0553e1cbf472acc4f2c51afab632431cdcd72124d5" +checksum = "2f30143827ddab0d256fd843b7a66d164e9f271cfa0dde49142c5ca0ca291f1e" dependencies = [ "matchers", "nu-ansi-term", @@ -4299,7 +4298,7 @@ dependencies = [ "byteorder", "bytes", "data-encoding", - "http 1.3.1", + "http 1.4.0", "httparse", "log", "rand 0.8.5", @@ -4430,13 +4429,13 @@ checksum = "06abde3611657adf66d383f00b093d7faecc7fa57071cce2578660c9f1010821" [[package]] name = "uuid" -version = "1.18.1" +version = "1.19.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2f87b8aa10b915a06587d0dec516c282ff295b475d94abf425d62b57710070a2" +checksum = "e2e054861b4bd027cd373e18e8d8d8e6548085000e41290d95ce0c373a654b4a" dependencies = [ "getrandom 0.3.4", "js-sys", - "serde", + "serde_core", "wasm-bindgen", ] @@ -4505,9 +4504,9 @@ dependencies = [ [[package]] name = "wasm-bindgen" -version = "0.2.105" +version = "0.2.106" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "da95793dfc411fbbd93f5be7715b0578ec61fe87cb1a42b12eb625caa5c5ea60" +checksum = "0d759f433fa64a2d763d1340820e46e111a7a5ab75f993d1852d70b03dbb80fd" dependencies = [ "cfg-if", "once_cell", @@ -4518,9 +4517,9 @@ dependencies = [ [[package]] name = "wasm-bindgen-futures" -version = "0.4.55" +version = "0.4.56" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "551f88106c6d5e7ccc7cd9a16f312dd3b5d36ea8b4954304657d5dfba115d4a0" +checksum = "836d9622d604feee9e5de25ac10e3ea5f2d65b41eac0d9ce72eb5deae707ce7c" dependencies = [ "cfg-if", "js-sys", @@ -4531,9 +4530,9 @@ dependencies = [ [[package]] name = "wasm-bindgen-macro" -version = "0.2.105" +version = "0.2.106" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "04264334509e04a7bf8690f2384ef5265f05143a4bff3889ab7a3269adab59c2" +checksum = "48cb0d2638f8baedbc542ed444afc0644a29166f1595371af4fecf8ce1e7eeb3" dependencies = [ "quote", "wasm-bindgen-macro-support", @@ -4541,9 +4540,9 @@ dependencies = [ [[package]] name = "wasm-bindgen-macro-support" -version = "0.2.105" +version = "0.2.106" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "420bc339d9f322e562942d52e115d57e950d12d88983a14c79b86859ee6c7ebc" +checksum = "cefb59d5cd5f92d9dcf80e4683949f15ca4b511f4ac0a6e14d4e1ac60c6ecd40" dependencies = [ "bumpalo", "proc-macro2", @@ -4554,18 +4553,18 @@ dependencies = [ [[package]] name = "wasm-bindgen-shared" -version = "0.2.105" +version = "0.2.106" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "76f218a38c84bcb33c25ec7059b07847d465ce0e0a76b995e134a45adcb6af76" +checksum = "cbc538057e648b67f72a982e708d485b2efa771e1ac05fec311f9f63e5800db4" dependencies = [ "unicode-ident", ] [[package]] name = "web-sys" -version = "0.3.82" +version = "0.3.83" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3a1f95c0d03a47f4ae1f7a64643a6bb97465d9b740f0fa8f90ea33915c99a9a1" +checksum = "9b32828d774c412041098d182a8b38b16ea816958e07cf40eec2bc080ae137ac" dependencies = [ "js-sys", "wasm-bindgen", @@ -4920,9 +4919,9 @@ checksum = "d6bbff5f0aada427a1e5a6da5f1f98158182f26556f345ac9e04d36d0ebed650" [[package]] name = "winnow" -version = "0.7.13" +version = "0.7.14" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "21a0236b59786fed61e2a80582dd500fe61f18b5dca67a4a067d0bc9039339cf" +checksum = "5a5364e9d77fcdeeaa6062ced926ee3381faa2ee02d3eb83a5c27a8825540829" dependencies = [ "memchr", ] @@ -4979,18 +4978,18 @@ dependencies = [ [[package]] name = "zerocopy" -version = "0.8.27" +version = "0.8.31" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0894878a5fa3edfd6da3f88c4805f4c8558e2b996227a3d864f47fe11e38282c" +checksum = "fd74ec98b9250adb3ca554bdde269adf631549f51d8a8f8f0a10b50f1cb298c3" dependencies = [ "zerocopy-derive", ] [[package]] name = "zerocopy-derive" -version = "0.8.27" +version = "0.8.31" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "88d2b8d9c68ad2b9e4340d7832716a4d21a22a1154777ad56ea55c51a9cf3831" +checksum = "d8a8d209fdf45cf5138cbb5a506f6b52522a25afccc534d1475dad8e31105c6a" dependencies = [ "proc-macro2", "quote", @@ -5117,9 +5116,9 @@ dependencies = [ [[package]] name = "zune-jpeg" -version = "0.5.5" +version = "0.5.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "dc6fb7703e32e9a07fb3f757360338b3a567a5054f21b5f52a666752e333d58e" +checksum = "f520eebad972262a1dde0ec455bce4f8b298b1e5154513de58c114c4c54303e8" dependencies = [ "zune-core 0.5.0", ] diff --git a/common/src/routes.rs b/common/src/routes.rs index 19b0206..3d92b6e 100644 --- a/common/src/routes.rs +++ b/common/src/routes.rs @@ -71,7 +71,10 @@ pub fn u_admin_invite_create() -> String { pub fn u_admin_invite_remove() -> String { "/admin/remove_invite".to_string() } -pub fn u_admin_import(incremental: bool) -> String { +pub fn u_admin_import() -> String { + format!("/admin/import") +} +pub fn u_admin_import_post(incremental: bool) -> String { format!("/admin/import?incremental={incremental}") } pub fn u_admin_update_search() -> String { diff --git a/import/src/lib.rs b/import/src/lib.rs index 8ad6790..561a5c9 100644 --- a/import/src/lib.rs +++ b/import/src/lib.rs @@ -8,35 +8,24 @@ pub mod plugins; pub mod reporting; -use crate::plugins::{ - acoustid::AcoustID, - infojson::is_info_json, - misc::is_cover, - musicbrainz::{self, MusicBrainz}, - tmdb::{self, Tmdb, TmdbKind}, - trakt::{Trakt, TraktKind}, - vgmdb::Vgmdb, - wikidata::Wikidata, - wikimedia_commons::WikimediaCommons, +use crate::{ + plugins::{ + ImportContext, ImportPlugin, infojson::is_info_json, init_plugins, misc::is_cover, + trakt::Trakt, + }, + reporting::IMPORT_PROGRESS, }; use anyhow::{Context, Result, anyhow}; use jellycache::{HashKey, cache_memory, cache_store}; -use jellycommon::{ - Appearance, Asset, CreditCategory, IdentifierType, NodeID, NodeKind, PictureSlot, RatingType, - Visibility, -}; +use jellycommon::{NodeID, Visibility}; use jellydb::Database; -use jellyimport_fallback_generator::generate_fallback; use jellyremuxer::{ demuxers::create_demuxer_autodetect, matroska::{self, AttachedFile, Segment}, }; -use log::info; use rayon::iter::{IntoParallelIterator, ParallelIterator}; -use regex::Regex; use serde::{Deserialize, Serialize}; use std::{ - collections::BTreeMap, fs::{File, read_to_string}, path::{Path, PathBuf}, sync::{Arc, LazyLock, Mutex}, @@ -79,19 +68,6 @@ pub const USER_AGENT: &str = concat!( static IMPORT_SEM: LazyLock<Semaphore> = LazyLock::new(|| Semaphore::new(1)); -static RE_EPISODE_FILENAME: LazyLock<Regex> = - LazyLock::new(|| Regex::new(r#"([sS](?<season>\d+))?([eE](?<episode>\d+))( (.+))?"#).unwrap()); - -struct Apis { - trakt: Option<Trakt>, - tmdb: Option<Tmdb>, - acoustid: Option<AcoustID>, - musicbrainz: MusicBrainz, - wikidata: Wikidata, - wikimedia_commons: WikimediaCommons, - vgmdb: Vgmdb, -} - pub fn is_importing() -> bool { IMPORT_SEM.available_permits() == 0 } @@ -120,73 +96,59 @@ pub async fn import_wrap(db: Database, incremental: bool) -> Result<()> { } fn import(db: &Database, incremental: bool) -> Result<()> { - let apis = Apis { - trakt: CONF.api.trakt.as_ref().map(|key| Trakt::new(key)), - tmdb: CONF.api.tmdb.as_ref().map(|key| Tmdb::new(key)), - acoustid: CONF.api.acoustid.as_ref().map(|key| AcoustID::new(key)), - musicbrainz: MusicBrainz::new(), - wikidata: Wikidata::new(), - wikimedia_commons: WikimediaCommons::new(), - vgmdb: Vgmdb::new(), - }; - - let rthandle = Handle::current(); + let plugins = init_plugins(&CONF.api); let mut files = Vec::new(); - import_traverse( &CONF.media_path, db, incremental, NodeID::MIN, - "", - InheritedFlags { - visibility: Visibility::Visible, - use_acoustid: false, - }, + InheritedFlags::default(), &mut files, )?; + let rt = Handle::current(); + files.into_par_iter().for_each(|(path, parent, iflags)| { - import_file(db, &apis, &rthandle, &path, parent, iflags); + import_file(db, &rt, &plugins, &path, parent, iflags); + IMPORT_PROGRESS + .blocking_write() + .as_mut() + .unwrap() + .finished_items += 1; }); - // let meta = path.metadata()?; - // let mtime = meta.modified()?.duration_since(UNIX_EPOCH)?.as_secs(); - // db.set_import_file_mtime(path, mtime)?; - Ok(()) } #[derive(Debug, Clone, Copy)] -struct InheritedFlags { +pub struct InheritedFlags { visibility: Visibility, use_acoustid: bool, } +impl Default for InheritedFlags { + fn default() -> Self { + Self { + visibility: Visibility::Visible, + use_acoustid: false, + } + } +} fn import_traverse( path: &Path, db: &Database, incremental: bool, parent: NodeID, - parent_slug_fragment: &str, mut iflags: InheritedFlags, out: &mut Vec<(PathBuf, NodeID, InheritedFlags)>, ) -> Result<()> { if path.is_dir() { reporting::set_task(format!("indexing {path:?}")); - let slug_fragment = if path == CONF.media_path { - "library".to_string() - } else { - path.file_name().unwrap().to_string_lossy().to_string() - }; - let slug = if parent_slug_fragment.is_empty() { - slug_fragment.clone() - } else { - format!("{parent_slug_fragment}-{slug_fragment}") - }; - let id = NodeID::from_slug(&slug); + let slug = get_node_slug(path).unwrap(); + let node = NodeID::from_slug(&slug); // Some flags need to applied immediatly because they are inherited if let Ok(content) = read_to_string(path.join("flags")) { @@ -200,7 +162,7 @@ fn import_traverse( } } - db.update_node_init(id, |n| { + db.update_node_init(node, |n| { if parent != NodeID::MIN { n.parents.insert(parent); } @@ -212,8 +174,8 @@ fn import_traverse( for e in path.read_dir()? { let path = e?.path(); reporting::catch( - import_traverse(&path, db, incremental, id, &slug_fragment, iflags, out) - .context(anyhow!("index {slug_fragment:?}")), + import_traverse(&path, db, incremental, node, iflags, out) + .context(anyhow!("index {:?}", path.file_name().unwrap())), ); } return Ok(()); @@ -231,6 +193,11 @@ fn import_traverse( } } + IMPORT_PROGRESS + .blocking_write() + .as_mut() + .unwrap() + .total_items += 1; out.push((path.to_owned(), parent, iflags)); } Ok(()) @@ -238,12 +205,13 @@ fn import_traverse( fn import_file( db: &Database, - apis: &Apis, - rthandle: &Handle, + rt: &Handle, + plugins: &[Box<dyn ImportPlugin>], path: &Path, parent: NodeID, iflags: InheritedFlags, ) { + let mut all_ok = true; let filename = path.file_name().unwrap().to_string_lossy(); if filename == "flags" { let Some(content) = @@ -251,10 +219,78 @@ fn import_file( else { return; }; - for flag in content.lines() {} + for line in content.lines() { + for p in plugins { + let inf = p.info(); + if inf.handle_instruction { + reporting::set_task(format!("{}(inst): {path:?}", inf.name)); + all_ok &= reporting::catch( + p.instruction(&ImportContext { db, rt, iflags }, parent, line) + .context(anyhow!("{}(inst) {path:?}", inf.name)), + ) + .is_some(); + } + } + } } + if filename.ends_with("mkv") || filename.ends_with("mka") || filename.ends_with("mks") { - import_media_file(db, apis, rthandle, path, parent, iflags).context("media file"); + let slug = get_node_slug(path).unwrap(); + let node = NodeID::from_slug(&slug); + + all_ok &= reporting::catch(db.update_node_init(node, |node| { + node.slug = slug; + if parent != NodeID::MIN { + node.parents.insert(parent); + } + node.visibility = iflags.visibility; + Ok(()) + })) + .is_some(); + + let Some(seg) = + reporting::catch(read_media_metadata(path).context(anyhow!("media {path:?}"))) + else { + return; + }; + for p in plugins { + let inf = p.info(); + if inf.handle_media { + reporting::set_task(format!("{}(media): {path:?}", inf.name)); + all_ok &= reporting::catch( + p.media(&ImportContext { db, rt, iflags }, node, path, &seg) + .context(anyhow!("{}(media) {path:?}", inf.name)), + ) + .is_some(); + } + } + reporting::set_task("idle".to_owned()); + } + + if all_ok { + reporting::catch(update_mtime(db, path).context("updating mtime")); + } +} + +fn update_mtime(db: &Database, path: &Path) -> Result<()> { + let meta = path.metadata()?; + let mtime = meta.modified()?.duration_since(UNIX_EPOCH)?.as_secs(); + db.set_import_file_mtime(path, mtime)?; + Ok(()) +} + +fn get_node_slug(path: &Path) -> Option<String> { + if path == CONF.media_path { + return Some("library".to_string()); + } + let filename = path.file_name()?.to_string_lossy(); + let filestem = filename.split_once(".").unwrap_or((&filename, "")).0; + if path.parent()? == &CONF.media_path { + Some(format!("{filestem}")) + } else { + let parent_filename = path.parent()?.file_name()?.to_string_lossy(); + let parent_filestem = parent_filename.split_once(".").unwrap_or((&filename, "")).0; + Some(format!("{parent_filestem}-{filestem}")) } } @@ -297,6 +333,7 @@ pub fn read_media_metadata(path: &Path) -> Result<Arc<matroska::Segment>> { }) }, ) + .context("reading media metadata") } pub fn is_useful_attachment(a: &AttachedFile) -> Option<&'static str> { @@ -307,323 +344,291 @@ pub fn is_useful_attachment(a: &AttachedFile) -> Option<&'static str> { } } -fn import_media_file( - db: &Database, - apis: &Apis, - rthandle: &Handle, - path: &Path, - parent: NodeID, - iflags: InheritedFlags, -) -> Result<()> { - info!("media file {path:?}"); - let m = read_media_metadata(path)?; - - let filename = path.file_name().unwrap().to_string_lossy().to_string(); - - let mut episode_index = None; - if let Some(cap) = RE_EPISODE_FILENAME.captures(&filename) { - if let Some(episode) = cap.name("episode").map(|m| m.as_str()) { - let season = cap.name("season").map(|m| m.as_str()); - let episode = episode.parse::<usize>().context("parse episode num")?; - let season = season - .unwrap_or("1") - .parse::<usize>() - .context("parse season num")?; - episode_index = Some((season, episode)) - } - } - - let mut filename_toks = filename.split("."); - let filepath_stem = filename_toks.next().unwrap(); +// let slug = if let Some((s, e)) = episode_index { +// format!( +// "{}-s{s}e{e}", +// make_kebab( +// &path +// .parent() +// .unwrap() +// .file_name() +// .unwrap_or_default() +// .to_string_lossy() +// ) +// ) +// } else { +// make_kebab(filepath_stem) +// }; - let slug = if let Some((s, e)) = episode_index { - format!( - "{}-s{s}e{e}", - make_kebab( - &path - .parent() - .unwrap() - .file_name() - .unwrap_or_default() - .to_string_lossy() - ) - ) - } else { - make_kebab(filepath_stem) - }; +// let node = NodeID::from_slug(&slug); - let node = NodeID::from_slug(&slug); +// db.update_node_init(node, |node| { +// node.slug = slug; +// node.visibility = iflags.visibility; +// node.parents.insert(parent); +// Ok(()) +// })?; - db.update_node_init(node, |node| { - node.slug = slug; - node.visibility = iflags.visibility; - node.parents.insert(parent); - Ok(()) - })?; +// if let Some((season, episode)) = episode_index { +// let mut trakt_id = None; +// let flagspath = path.parent().unwrap().join("flags"); +// if flagspath.exists() { +// for flag in read_to_string(flagspath)?.lines() { +// if let Some(value) = flag.strip_prefix("trakt-").or(flag.strip_prefix("trakt=")) { +// let (kind, id) = value.split_once(":").unwrap_or(("", value)); +// if kind == "show" { +// trakt_id = Some(id.parse::<u64>()?); +// } +// } +// } +// } +// if let Some(trakt_id) = trakt_id { +// let trakt = apis.trakt.as_ref().ok_or(anyhow!("trakt required"))?; +// let seasons = trakt.show_seasons(trakt_id, rthandle)?; +// if seasons.iter().any(|x| x.number == season) { +// let episodes = trakt.show_season_episodes(trakt_id, season, rthandle)?; +// let mut poster = None; +// if let Some(tmdb) = &apis.tmdb { +// let trakt_details = trakt.lookup(TraktKind::Show, trakt_id, rthandle)?; +// if let Some(tmdb_id) = trakt_details.ids.tmdb { +// let tmdb_details = +// tmdb.episode_details(tmdb_id, season, episode, rthandle)?; +// if let Some(still) = &tmdb_details.still_path { +// poster = Some(tmdb.image(still, rthandle)?) +// } +// } +// } +// if let Some(episode) = episodes.get(episode.saturating_sub(1)) { +// db.update_node_init(node, |node| { +// node.kind = NodeKind::Episode; +// node.index = Some(episode.number); +// node.title = Some(episode.title.clone()); +// if let Some(poster) = poster { +// node.pictures.insert(PictureSlot::Cover, poster); +// } +// node.description = episode.overview.clone().or(node.description.clone()); +// node.ratings.insert(RatingType::Trakt, episode.rating); +// Ok(()) +// })? +// } +// } +// } +// } - if let Some((season, episode)) = episode_index { - let mut trakt_id = None; - let flagspath = path.parent().unwrap().join("flags"); - if flagspath.exists() { - for flag in read_to_string(flagspath)?.lines() { - if let Some(value) = flag.strip_prefix("trakt-").or(flag.strip_prefix("trakt=")) { - let (kind, id) = value.split_once(":").unwrap_or(("", value)); - if kind == "show" { - trakt_id = Some(id.parse::<u64>()?); - } - } - } - } - if let Some(trakt_id) = trakt_id { - let trakt = apis.trakt.as_ref().ok_or(anyhow!("trakt required"))?; - let seasons = trakt.show_seasons(trakt_id, rthandle)?; - if seasons.iter().any(|x| x.number == season) { - let episodes = trakt.show_season_episodes(trakt_id, season, rthandle)?; - let mut poster = None; - if let Some(tmdb) = &apis.tmdb { - let trakt_details = trakt.lookup(TraktKind::Show, trakt_id, rthandle)?; - if let Some(tmdb_id) = trakt_details.ids.tmdb { - let tmdb_details = - tmdb.episode_details(tmdb_id, season, episode, rthandle)?; - if let Some(still) = &tmdb_details.still_path { - poster = Some(tmdb.image(still, rthandle)?) - } - } - } - if let Some(episode) = episodes.get(episode.saturating_sub(1)) { - db.update_node_init(node, |node| { - node.kind = NodeKind::Episode; - node.index = Some(episode.number); - node.title = Some(episode.title.clone()); - if let Some(poster) = poster { - node.pictures.insert(PictureSlot::Cover, poster); - } - node.description = episode.overview.clone().or(node.description.clone()); - node.ratings.insert(RatingType::Trakt, episode.rating); - Ok(()) - })? - } - } - } - } +// for tok in filename_toks { +// apply_node_flag(db, rthandle, apis, node, tok)?; +// } - // for tok in filename_toks { - // apply_node_flag(db, rthandle, apis, node, tok)?; - // } +// fn apply_musicbrainz_recording( +// db: &Database, +// rthandle: &Handle, +// apis: &Apis, +// node: NodeID, +// mbid: String, +// ) -> Result<()> { +// let rec = apis.musicbrainz.lookup_recording(mbid, rthandle)?; - Ok(()) -} +// db.update_node_init(node, |node| { +// node.title = Some(rec.title.clone()); +// node.identifiers +// .insert(IdentifierType::MusicbrainzRecording, rec.id.to_string()); +// if let Some(a) = rec.artist_credit.first() { +// node.subtitle = Some(a.artist.name.clone()); +// node.identifiers +// .insert(IdentifierType::MusicbrainzArtist, a.artist.id.to_string()); +// } -fn apply_musicbrainz_recording( - db: &Database, - rthandle: &Handle, - apis: &Apis, - node: NodeID, - mbid: String, -) -> Result<()> { - let rec = apis.musicbrainz.lookup_recording(mbid, rthandle)?; +// // // TODO proper dedup +// // node.people.clear(); - db.update_node_init(node, |node| { - node.title = Some(rec.title.clone()); - node.identifiers - .insert(IdentifierType::MusicbrainzRecording, rec.id.to_string()); - if let Some(a) = rec.artist_credit.first() { - node.subtitle = Some(a.artist.name.clone()); - node.identifiers - .insert(IdentifierType::MusicbrainzArtist, a.artist.id.to_string()); - } +// for rel in &rec.relations { +// use musicbrainz::reltypes::*; +// let a = match rel.type_id.as_str() { +// INSTRUMENT => Some(("", CreditCategory::Instrument)), +// VOCAL => Some(("", CreditCategory::Vocal)), +// PRODUCER => Some(("", CreditCategory::Producer)), +// MIX => Some(("mix ", CreditCategory::Engineer)), +// PHONOGRAPHIC_COPYRIGHT => { +// Some(("phonographic copyright ", CreditCategory::Engineer)) +// } +// PROGRAMMING => Some(("programming ", CreditCategory::Engineer)), +// _ => None, +// }; - // // TODO proper dedup - // node.people.clear(); +// if let Some((note, group)) = a { +// let artist = rel.artist.as_ref().unwrap(); - for rel in &rec.relations { - use musicbrainz::reltypes::*; - let a = match rel.type_id.as_str() { - INSTRUMENT => Some(("", CreditCategory::Instrument)), - VOCAL => Some(("", CreditCategory::Vocal)), - PRODUCER => Some(("", CreditCategory::Producer)), - MIX => Some(("mix ", CreditCategory::Engineer)), - PHONOGRAPHIC_COPYRIGHT => { - Some(("phonographic copyright ", CreditCategory::Engineer)) - } - PROGRAMMING => Some(("programming ", CreditCategory::Engineer)), - _ => None, - }; +// let artist = apis +// .musicbrainz +// .lookup_artist(artist.id.clone(), rthandle)?; - if let Some((note, group)) = a { - let artist = rel.artist.as_ref().unwrap(); +// let mut image_1 = None; +// let mut image_2 = None; - let artist = apis - .musicbrainz - .lookup_artist(artist.id.clone(), rthandle)?; +// for rel in &artist.relations { +// match rel.type_id.as_str() { +// WIKIDATA => { +// let url = rel.url.as_ref().unwrap().resource.clone(); +// if let Some(id) = url.strip_prefix("https://www.wikidata.org/wiki/") { +// if let Some(filename) = +// apis.wikidata.query_image_path(id.to_owned(), rthandle)? +// { +// image_1 = Some( +// apis.wikimedia_commons +// .image_by_filename(filename, rthandle)?, +// ); +// } +// } +// } +// VGMDB => { +// let url = rel.url.as_ref().unwrap().resource.clone(); +// if let Some(id) = url.strip_prefix("https://vgmdb.net/artist/") { +// let id = id.parse::<u64>().context("parse vgmdb id")?; +// if let Some(path) = apis.vgmdb.get_artist_image(id, rthandle)? { +// image_2 = Some(path); +// } +// } +// } +// _ => (), +// } +// } +// let mut jobs = vec![]; +// if !note.is_empty() { +// jobs.push(note.to_string()); +// } +// jobs.extend(rel.attributes.clone()); - let mut image_1 = None; - let mut image_2 = None; +// let _headshot = match image_1.or(image_2) { +// Some(x) => x, +// None => Asset(cache_store( +// format!("fallback/{}.image", HashKey(&artist.sort_name)), +// || generate_fallback(&artist.sort_name), +// )?), +// }; - for rel in &artist.relations { - match rel.type_id.as_str() { - WIKIDATA => { - let url = rel.url.as_ref().unwrap().resource.clone(); - if let Some(id) = url.strip_prefix("https://www.wikidata.org/wiki/") { - if let Some(filename) = - apis.wikidata.query_image_path(id.to_owned(), rthandle)? - { - image_1 = Some( - apis.wikimedia_commons - .image_by_filename(filename, rthandle)?, - ); - } - } - } - VGMDB => { - let url = rel.url.as_ref().unwrap().resource.clone(); - if let Some(id) = url.strip_prefix("https://vgmdb.net/artist/") { - let id = id.parse::<u64>().context("parse vgmdb id")?; - if let Some(path) = apis.vgmdb.get_artist_image(id, rthandle)? { - image_2 = Some(path); - } - } - } - _ => (), - } - } - let mut jobs = vec![]; - if !note.is_empty() { - jobs.push(note.to_string()); - } - jobs.extend(rel.attributes.clone()); +// node.credits.entry(group).or_default().push(Appearance { +// jobs, +// characters: vec![], +// node: NodeID([0; 32]), // TODO +// }); +// } +// } - let _headshot = match image_1.or(image_2) { - Some(x) => x, - None => Asset(cache_store( - format!("fallback/{}.image", HashKey(&artist.sort_name)), - || generate_fallback(&artist.sort_name), - )?), - }; +// for isrc in &rec.isrcs { +// node.identifiers +// .insert(IdentifierType::Isrc, isrc.to_string()); +// } +// Ok(()) +// })?; +// Ok(()) +// } - node.credits.entry(group).or_default().push(Appearance { - jobs, - characters: vec![], - node: NodeID([0; 32]), // TODO - }); - } - } +// fn apply_trakt_tmdb( +// db: &Database, +// rthandle: &Handle, +// apis: &Apis, +// node: NodeID, +// trakt_kind: TraktKind, +// trakt_id: &str, +// ) -> Result<()> { +// let trakt_id: u64 = trakt_id.parse().context("parse trakt id")?; +// if let (Some(trakt), Some(tmdb)) = (&apis.trakt, &apis.tmdb) { +// let data = trakt.lookup(trakt_kind, trakt_id, rthandle)?; +// let people = trakt.people(trakt_kind, trakt_id, rthandle)?; - for isrc in &rec.isrcs { - node.identifiers - .insert(IdentifierType::Isrc, isrc.to_string()); - } - Ok(()) - })?; - Ok(()) -} +// let mut people_map = BTreeMap::<CreditCategory, Vec<Appearance>>::new(); +// for p in people.cast.iter() { +// people_map +// .entry(CreditCategory::Cast) +// .or_default() +// .push(p.a()) +// } +// for (group, people) in people.crew.iter() { +// for p in people { +// people_map +// .entry(group.as_credit_category()) +// .or_default() +// .push(p.a()) +// } +// } -fn apply_trakt_tmdb( - db: &Database, - rthandle: &Handle, - apis: &Apis, - node: NodeID, - trakt_kind: TraktKind, - trakt_id: &str, -) -> Result<()> { - let trakt_id: u64 = trakt_id.parse().context("parse trakt id")?; - if let (Some(trakt), Some(tmdb)) = (&apis.trakt, &apis.tmdb) { - let data = trakt.lookup(trakt_kind, trakt_id, rthandle)?; - let people = trakt.people(trakt_kind, trakt_id, rthandle)?; +// let mut tmdb_data = None; +// let mut backdrop = None; +// let mut poster = None; +// if let Some(tmdb_id) = data.ids.tmdb { +// let data = tmdb.details( +// match trakt_kind { +// TraktKind::Movie => TmdbKind::Movie, +// TraktKind::Show => TmdbKind::Tv, +// _ => TmdbKind::Movie, +// }, +// tmdb_id, +// rthandle, +// )?; +// tmdb_data = Some(data.clone()); - let mut people_map = BTreeMap::<CreditCategory, Vec<Appearance>>::new(); - for p in people.cast.iter() { - people_map - .entry(CreditCategory::Cast) - .or_default() - .push(p.a()) - } - for (group, people) in people.crew.iter() { - for p in people { - people_map - .entry(group.as_credit_category()) - .or_default() - .push(p.a()) - } - } +// if let Some(path) = &data.backdrop_path { +// backdrop = Some(tmdb.image(path, rthandle).context("tmdb backdrop image")?); +// } +// if let Some(path) = &data.poster_path { +// poster = Some(tmdb.image(path, rthandle).context("tmdb poster image")?); +// } - let mut tmdb_data = None; - let mut backdrop = None; - let mut poster = None; - if let Some(tmdb_id) = data.ids.tmdb { - let data = tmdb.details( - match trakt_kind { - TraktKind::Movie => TmdbKind::Movie, - TraktKind::Show => TmdbKind::Tv, - _ => TmdbKind::Movie, - }, - tmdb_id, - rthandle, - )?; - tmdb_data = Some(data.clone()); +// // for p in people_map.values_mut().flatten() { +// // if let Some(id) = p.person.ids.tmdb { +// // let k = rthandle.block_on(tmdb.person_image(id))?; +// // if let Some(prof) = k.profiles.first() { +// // let im = rthandle.block_on(tmdb.image(&prof.file_path))?; +// // p.person.headshot = Some(AssetInner::Cache(im).ser()); +// // } +// // } +// // } +// } - if let Some(path) = &data.backdrop_path { - backdrop = Some(tmdb.image(path, rthandle).context("tmdb backdrop image")?); - } - if let Some(path) = &data.poster_path { - poster = Some(tmdb.image(path, rthandle).context("tmdb poster image")?); - } +// db.update_node_init(node, |node| { +// node.title = Some(data.title.clone()); +// node.credits.extend(people_map); +// node.kind = trakt_kind.as_node_kind(); +// if let Some(overview) = &data.overview { +// node.description = Some(overview.clone()) +// } +// if let Some(tagline) = &data.tagline { +// node.tagline = Some(tagline.clone()) +// } +// if let Some(rating) = &data.rating { +// node.ratings.insert(RatingType::Trakt, *rating); +// } +// if let Some(poster) = poster { +// node.pictures.insert(PictureSlot::Cover, poster); +// } +// if let Some(backdrop) = backdrop { +// node.pictures.insert(PictureSlot::Backdrop, backdrop); +// } +// if let Some(data) = tmdb_data { +// node.title = data.title.clone().or(node.title.clone()); +// node.tagline = data.tagline.clone().or(node.tagline.clone()); +// node.description = Some(data.overview.clone()); +// node.ratings.insert(RatingType::Tmdb, data.vote_average); +// if let Some(date) = data.release_date.clone() { +// if let Ok(date) = tmdb::parse_release_date(&date) { +// node.release_date = date; +// } +// } +// } +// Ok(()) +// })?; +// } +// Ok(()) +// } - // for p in people_map.values_mut().flatten() { - // if let Some(id) = p.person.ids.tmdb { - // let k = rthandle.block_on(tmdb.person_image(id))?; - // if let Some(prof) = k.profiles.first() { - // let im = rthandle.block_on(tmdb.image(&prof.file_path))?; - // p.person.headshot = Some(AssetInner::Cache(im).ser()); - // } - // } - // } - } - - db.update_node_init(node, |node| { - node.title = Some(data.title.clone()); - node.credits.extend(people_map); - node.kind = trakt_kind.as_node_kind(); - if let Some(overview) = &data.overview { - node.description = Some(overview.clone()) - } - if let Some(tagline) = &data.tagline { - node.tagline = Some(tagline.clone()) - } - if let Some(rating) = &data.rating { - node.ratings.insert(RatingType::Trakt, *rating); - } - if let Some(poster) = poster { - node.pictures.insert(PictureSlot::Cover, poster); - } - if let Some(backdrop) = backdrop { - node.pictures.insert(PictureSlot::Backdrop, backdrop); - } - if let Some(data) = tmdb_data { - node.title = data.title.clone().or(node.title.clone()); - node.tagline = data.tagline.clone().or(node.tagline.clone()); - node.description = Some(data.overview.clone()); - node.ratings.insert(RatingType::Tmdb, data.vote_average); - if let Some(date) = data.release_date.clone() { - if let Ok(date) = tmdb::parse_release_date(&date) { - node.release_date = date; - } - } - } - Ok(()) - })?; - } - Ok(()) -} - -fn make_kebab(i: &str) -> String { - let mut o = String::with_capacity(i.len()); - for c in i.chars() { - o.extend(match c { - 'A'..='Z' | 'a'..='z' | '0'..='9' | '_' | '-' => Some(c), - ' ' => Some('-'), - _ => None, - }); - } - o -} +// fn make_kebab(i: &str) -> String { +// let mut o = String::with_capacity(i.len()); +// for c in i.chars() { +// o.extend(match c { +// 'A'..='Z' | 'a'..='z' | '0'..='9' | '_' | '-' => Some(c), +// ' ' => Some('-'), +// _ => None, +// }); +// } +// o +// } diff --git a/import/src/plugins/acoustid.rs b/import/src/plugins/acoustid.rs index bf07f90..38e818c 100644 --- a/import/src/plugins/acoustid.rs +++ b/import/src/plugins/acoustid.rs @@ -167,6 +167,9 @@ impl ImportPlugin for AcoustID { } } fn media(&self, ct: &ImportContext, node: NodeID, path: &Path, _seg: &Segment) -> Result<()> { + if !ct.iflags.use_acoustid { + return Ok(()); + } let fp = acoustid_fingerprint(path)?; if let Some((atid, mbid)) = self.get_atid_mbid(&fp, &ct.rt)? { ct.db.update_node_init(node, |n| { diff --git a/import/src/plugins/misc.rs b/import/src/plugins/misc.rs index 6f2c18e..8d7028c 100644 --- a/import/src/plugins/misc.rs +++ b/import/src/plugins/misc.rs @@ -4,15 +4,17 @@ Copyright (C) 2025 metamuffin <metamuffin.org> */ use crate::plugins::{ImportContext, ImportPlugin, PluginInfo}; -use anyhow::{Result, bail}; +use anyhow::{Context, Result, bail}; use jellycache::{HashKey, cache_store}; use jellycommon::{Asset, NodeID, NodeKind, PictureSlot, Visibility}; use jellyremuxer::matroska::{AttachedFile, Segment}; use log::info; +use regex::Regex; use std::{ fs::{File, read_to_string}, io::Read, path::Path, + sync::LazyLock, }; pub struct ImageFiles; @@ -152,3 +154,36 @@ impl ImportPlugin for Children { Ok(()) } } + +static RE_EPISODE_FILENAME: LazyLock<Regex> = + LazyLock::new(|| Regex::new(r#"([sS](?<season>\d+))?([eE](?<episode>\d+))( (.+))?"#).unwrap()); + +pub struct EpisodeIndex; +impl ImportPlugin for EpisodeIndex { + fn info(&self) -> PluginInfo { + PluginInfo { + name: "episode-info", + handle_media: true, + ..Default::default() + } + } + fn media(&self, ct: &ImportContext, node: NodeID, path: &Path, _seg: &Segment) -> Result<()> { + let filename = path.file_name().unwrap().to_string_lossy(); + if let Some(cap) = RE_EPISODE_FILENAME.captures(&filename) { + if let Some(episode) = cap.name("episode").map(|m| m.as_str()) { + let season = cap.name("season").map(|m| m.as_str()); + let episode = episode.parse::<usize>().context("parse episode num")?; + let season = season + .unwrap_or("1") + .parse::<usize>() + .context("parse season num")?; + + ct.db.update_node_init(node, |node| { + node.index = Some(episode); + Ok(()) + })?; + } + } + Ok(()) + } +} diff --git a/import/src/plugins/mod.rs b/import/src/plugins/mod.rs index a5cc3dc..cf0da1c 100644 --- a/import/src/plugins/mod.rs +++ b/import/src/plugins/mod.rs @@ -15,6 +15,7 @@ pub mod vgmdb; pub mod wikidata; pub mod wikimedia_commons; +use crate::{ApiSecrets, InheritedFlags}; use anyhow::Result; use jellycommon::NodeID; use jellydb::Database; @@ -22,20 +23,19 @@ use jellyremuxer::matroska::Segment; use std::path::Path; use tokio::runtime::Handle; -use crate::ApiSecrets; - -pub struct ImportContext { - pub db: Database, - pub rt: Handle, +pub struct ImportContext<'a> { + pub db: &'a Database, + pub rt: &'a Handle, + pub iflags: InheritedFlags, } #[derive(Default, Clone, Copy)] pub struct PluginInfo { - name: &'static str, - handle_file: bool, - handle_media: bool, - handle_instruction: bool, - handle_process: bool, + pub name: &'static str, + pub handle_file: bool, + pub handle_media: bool, + pub handle_instruction: bool, + pub handle_process: bool, } pub trait ImportPlugin: Send + Sync { diff --git a/import/src/plugins/trakt.rs b/import/src/plugins/trakt.rs index 6d5b007..c062b01 100644 --- a/import/src/plugins/trakt.rs +++ b/import/src/plugins/trakt.rs @@ -385,7 +385,7 @@ impl Display for TraktKind { impl ImportPlugin for Trakt { fn info(&self) -> PluginInfo { PluginInfo { - name: "takt", + name: "trakt", handle_instruction: true, ..Default::default() } diff --git a/import/src/reporting.rs b/import/src/reporting.rs index 3105b59..92f38be 100644 --- a/import/src/reporting.rs +++ b/import/src/reporting.rs @@ -6,11 +6,13 @@ use anyhow::Result; use rayon::{current_num_threads, current_thread_index}; +use serde::Serialize; use tokio::sync::RwLock; pub static IMPORT_ERRORS: RwLock<Vec<String>> = RwLock::const_new(Vec::new()); pub static IMPORT_PROGRESS: RwLock<Option<ImportProgress>> = RwLock::const_new(None); +#[derive(Debug, Serialize, Clone)] pub struct ImportProgress { pub total_items: usize, pub finished_items: usize, diff --git a/locale/en.ini b/locale/en.ini index 95ea2e4..20614a4 100644 --- a/locale/en.ini +++ b/locale/en.ini @@ -122,6 +122,8 @@ admin.dashboard.delete_invite=Invalidate admin.dashboard.invites=Invitations admin.dashboard.users=Users admin.dashboard.library=Library +admin.import.running=Import Running... +admin.import.title=Import admin.invite_create_success=Invite created: {invite} admin.invite_delete_success=Invite deleted admin.import_success=Import finished diff --git a/logic/src/admin/mod.rs b/logic/src/admin/mod.rs index 2877299..fb1e59f 100644 --- a/logic/src/admin/mod.rs +++ b/logic/src/admin/mod.rs @@ -11,8 +11,7 @@ use crate::{DATABASE, session::Session}; use anyhow::{Result, anyhow}; use jellyimport::{import_wrap, reporting::IMPORT_ERRORS}; use rand::Rng; -use std::time::{Duration, Instant}; -use tokio::task::spawn_blocking; +use tokio::{spawn, task::spawn_blocking}; pub async fn get_import_errors(_session: &Session) -> Vec<String> { IMPORT_ERRORS.read().await.to_owned() @@ -39,12 +38,11 @@ pub async fn update_search_index(session: &Session) -> Result<()> { session.assert_admin()?; spawn_blocking(move || DATABASE.search_create_index()).await? } -pub async fn do_import(session: &Session, incremental: bool) -> Result<(Duration, Result<()>)> { +pub async fn do_import(session: &Session, incremental: bool) -> Result<()> { session.assert_admin()?; - let t = Instant::now(); if !incremental { DATABASE.clear_nodes()?; } - let r = import_wrap((*DATABASE).clone(), incremental).await; - Ok((t.elapsed(), r)) + spawn(import_wrap((*DATABASE).clone(), incremental)); + Ok(()) } diff --git a/server/src/routes.rs b/server/src/routes.rs index b777788..ed31407 100644 --- a/server/src/routes.rs +++ b/server/src/routes.rs @@ -3,8 +3,10 @@ which is licensed under the GNU Affero General Public License (version 3); see /COPYING. Copyright (C) 2025 metamuffin <metamuffin.org> */ -use crate::logic::playersync::{r_playersync, PlayersyncChannels}; +use crate::CONF; +use crate::logic::playersync::{PlayersyncChannels, r_playersync}; use crate::ui::account::{r_account_login, r_account_logout, r_account_register}; +use crate::ui::admin::import::{r_admin_import, r_admin_import_post, r_admin_import_stream}; use crate::ui::{ account::{ r_account_login_post, r_account_logout_post, r_account_register_post, @@ -12,8 +14,7 @@ use crate::ui::{ }, admin::{ log::{r_admin_log, r_admin_log_stream}, - r_admin_dashboard, r_admin_import, r_admin_invite, r_admin_remove_invite, - r_admin_update_search, + r_admin_dashboard, r_admin_invite, r_admin_remove_invite, r_admin_update_search, user::{r_admin_remove_user, r_admin_user, r_admin_user_permission, r_admin_users}, }, assets::{r_image, r_item_poster, r_node_thumbnail}, @@ -27,7 +28,6 @@ use crate::ui::{ stats::r_stats, style::{r_assets_font, r_assets_js, r_assets_js_map, r_assets_style}, }; -use crate::CONF; use crate::{ api::{r_api_account_login, r_api_root, r_nodes_modified_since, r_translations, r_version}, compat::{ @@ -61,8 +61,8 @@ use base64::Engine; use log::warn; use rand::random; use rocket::{ - catchers, config::SecretKey, fairing::AdHoc, fs::FileServer, http::Header, routes, - shield::Shield, Build, Config, Rocket, + Build, Config, Rocket, catchers, config::SecretKey, fairing::AdHoc, fs::FileServer, + http::Header, routes, shield::Shield, }; #[macro_export] @@ -123,6 +123,8 @@ pub fn build_rocket() -> Rocket<Build> { r_account_settings, r_admin_dashboard, r_admin_import, + r_admin_import_post, + r_admin_import_stream, r_admin_invite, r_admin_log_stream, r_admin_log, diff --git a/server/src/ui/admin/import.rs b/server/src/ui/admin/import.rs new file mode 100644 index 0000000..b6bb858 --- /dev/null +++ b/server/src/ui/admin/import.rs @@ -0,0 +1,70 @@ +/* + This file is part of jellything (https://codeberg.org/metamuffin/jellything) + which is licensed under the GNU Affero General Public License (version 3); see /COPYING. + Copyright (C) 2025 metamuffin <metamuffin.org> +*/ + +use std::time::Duration; + +use crate::{ + helper::{A, RequestInfo}, + ui::error::MyResult, +}; +use jellycommon::routes::u_admin_import; +use jellyimport::{ + is_importing, + reporting::{IMPORT_ERRORS, IMPORT_PROGRESS}, +}; +use jellylogic::{admin::do_import, session::Session}; +use jellyui::{admin::import::AdminImportPage, locale::tr, render_page}; +use rocket::{ + get, post, + request::FlashMessage, + response::{Flash, Redirect, content::RawHtml}, +}; +use rocket_ws::{Message, Stream, WebSocket}; +use tokio::time::sleep; + +#[get("/admin/import", rank = 2)] +pub async fn r_admin_import( + ri: RequestInfo, + flash: Option<FlashMessage<'_>>, +) -> MyResult<RawHtml<String>> { + ri.session.assert_admin()?; + + let last_import_err = IMPORT_ERRORS.read().await.clone(); + Ok(RawHtml(render_page( + &AdminImportPage { + busy: is_importing(), + flash: &flash.map(FlashMessage::into_inner), + lang: &ri.lang, + last_import_err: &last_import_err, + }, + ri.render_info(), + ))) +} + +#[post("/admin/import?<incremental>")] +pub async fn r_admin_import_post(ri: RequestInfo, incremental: bool) -> MyResult<Flash<Redirect>> { + ri.session.assert_admin()?; + do_import(&ri.session, incremental).await?; + Ok(Flash::success( + Redirect::to(u_admin_import()), + tr(ri.lang, "admin.import_success"), + )) +} + +#[get("/admin/import", rank = 1)] +pub fn r_admin_import_stream(_session: A<Session>, ws: WebSocket) -> Stream!['static] { + Stream! { ws => + loop { + let Some(p) = IMPORT_PROGRESS.read().await.clone() else { + break; + }; + yield Message::Text(serde_json::to_string(&p).unwrap()); + sleep(Duration::from_secs_f32(0.05)).await; + } + yield Message::Text("done".to_string()); + let _ = ws; + } +} diff --git a/server/src/ui/admin/log.rs b/server/src/ui/admin/log.rs index 24671bb..0a0e5ca 100644 --- a/server/src/ui/admin/log.rs +++ b/server/src/ui/admin/log.rs @@ -4,7 +4,7 @@ Copyright (C) 2025 metamuffin <metamuffin.org> */ use crate::{ - helper::{RequestInfo, A}, + helper::{A, RequestInfo}, ui::error::MyResult, }; use jellylogic::{ @@ -12,7 +12,7 @@ use jellylogic::{ session::Session, }; use jellyui::{ - admin::log::{render_log_line, ServerLogPage}, + admin::log::{ServerLogPage, render_log_line}, render_page, }; use rocket::{get, response::content::RawHtml}; diff --git a/server/src/ui/admin/mod.rs b/server/src/ui/admin/mod.rs index 02a7605..3bd4771 100644 --- a/server/src/ui/admin/mod.rs +++ b/server/src/ui/admin/mod.rs @@ -3,6 +3,7 @@ which is licensed under the GNU Affero General Public License (version 3); see /COPYING. Copyright (C) 2025 metamuffin <metamuffin.org> */ +pub mod import; pub mod log; pub mod user; @@ -11,15 +12,15 @@ use crate::helper::RequestInfo; use jellycommon::routes::u_admin_dashboard; use jellyimport::is_importing; use jellylogic::admin::{ - create_invite, delete_invite, do_import, get_import_errors, list_invites, update_search_index, + create_invite, delete_invite, get_import_errors, list_invites, update_search_index, }; use jellyui::{admin::AdminDashboardPage, locale::tr, render_page}; use rocket::{ + FromForm, form::Form, get, post, request::FlashMessage, - response::{content::RawHtml, Flash, Redirect}, - FromForm, + response::{Flash, Redirect, content::RawHtml}, }; #[get("/admin/dashboard")] @@ -76,16 +77,6 @@ pub async fn r_admin_remove_invite( )) } -#[post("/admin/import?<incremental>")] -pub async fn r_admin_import(ri: RequestInfo, incremental: bool) -> MyResult<Flash<Redirect>> { - ri.session.assert_admin()?; - do_import(&ri.session, incremental).await?.1?; - Ok(Flash::success( - Redirect::to(u_admin_dashboard()), - tr(ri.lang, "admin.import_success"), - )) -} - #[post("/admin/update_search")] pub async fn r_admin_update_search(ri: RequestInfo) -> MyResult<Flash<Redirect>> { ri.session.assert_admin()?; diff --git a/ui/src/admin/import.rs b/ui/src/admin/import.rs new file mode 100644 index 0000000..04b80b2 --- /dev/null +++ b/ui/src/admin/import.rs @@ -0,0 +1,50 @@ +/* + This file is part of jellything (https://codeberg.org/metamuffin/jellything) + which is licensed under the GNU Affero General Public License (version 3); see /COPYING. + Copyright (C) 2025 metamuffin <metamuffin.org> +*/ + +use crate::{ + FlashM, Page, + locale::{Language, tr, trs}, + scaffold::FlashDisplay, +}; +use jellycommon::routes::u_admin_import_post; + +impl Page for AdminImportPage<'_> { + fn title(&self) -> String { + "User Management".to_string() + } + fn to_render(&self) -> markup::DynRender<'_> { + markup::new!(@self) + } +} + +markup::define!( + AdminImportPage<'a>(lang: &'a Language, busy: bool, last_import_err: &'a [String], flash: &'a FlashM) { + @FlashDisplay { flash } + @if *busy { + h1 { @trs(lang, "admin.import.running") } + noscript { "Live import progress needs javascript." } + div[id="admin_import"] {} + } else { + h1 { @trs(lang, "admin.import.title") } + @if !last_import_err.is_empty() { + section.message.error { + details { + summary { p.error { @tr(**lang, "admin.import_errors").replace("{n}", &last_import_err.len().to_string()) } } + ol { @for e in *last_import_err { + li.error { pre.error { @e } } + }} + } + } + } + form[method="POST", action=u_admin_import_post(true)] { + input[type="submit", value=tr(**lang, "admin.dashboard.import.inc").to_string()]; + } + form[method="POST", action=u_admin_import_post(false)] { + input[type="submit", value=tr(**lang, "admin.dashboard.import.full").to_string()]; + } + } + } +); diff --git a/ui/src/admin/mod.rs b/ui/src/admin/mod.rs index cb18481..0e5d11e 100644 --- a/ui/src/admin/mod.rs +++ b/ui/src/admin/mod.rs @@ -4,6 +4,7 @@ Copyright (C) 2025 metamuffin <metamuffin.org> */ +pub mod import; pub mod log; pub mod user; @@ -13,8 +14,7 @@ use crate::{ scaffold::FlashDisplay, }; use jellycommon::routes::{ - u_admin_import, u_admin_invite_create, u_admin_invite_remove, u_admin_log, - u_admin_update_search, u_admin_users, + u_admin_invite_create, u_admin_invite_remove, u_admin_log, u_admin_update_search, u_admin_users, }; impl Page for AdminDashboardPage<'_> { @@ -48,12 +48,6 @@ markup::define!( @if let Some(text) = busy { section.message { p.warn { @text } } } - form[method="POST", action=u_admin_import(true)] { - input[type="submit", disabled=busy.is_some(), value=tr(**lang, "admin.dashboard.import.inc").to_string()]; - } - form[method="POST", action=u_admin_import(false)] { - input[type="submit", disabled=busy.is_some(), value=tr(**lang, "admin.dashboard.import.full").to_string()]; - } form[method="POST", action=u_admin_update_search()] { input[type="submit", value=tr(**lang, "admin.dashboard.update_search").to_string()]; } diff --git a/web/script/import_live.ts b/web/script/import_live.ts new file mode 100644 index 0000000..cc8c846 --- /dev/null +++ b/web/script/import_live.ts @@ -0,0 +1,64 @@ +/* + This file is part of jellything (https://codeberg.org/metamuffin/jellything) + which is licensed under the GNU Affero General Public License (version 3); see /COPYING. + Copyright (C) 2025 metamuffin <metamuffin.org> +*/ +/// <reference lib="dom" /> + +import { OVar } from "./jshelper/mod.ts"; +import { e } from "./jshelper/src/element.ts"; + +interface ImportProgress { + total_items: number + finished_items: number + tasks: string[] +} + +function progress_bar(progress: OVar<number>, text: OVar<string>): HTMLElement { + const bar_inner = e("div") + bar_inner.style.height = "100%" + bar_inner.style.backgroundColor = "#444" + bar_inner.style.position = "absolute" + bar_inner.style.top = "0px" + bar_inner.style.left = "0px" + bar_inner.style.zIndex = "2" + const bar_text = e("div") + bar_text.style.position = "absolute" + bar_text.style.top = "0px" + bar_text.style.left = "0px" + bar_text.style.color = "white" + bar_text.style.zIndex = "3" + const bar_outer = e("div", bar_inner, bar_text) + bar_outer.style.position = "relative" + bar_outer.style.width = "100%" + bar_outer.style.height = "2em" + bar_outer.style.backgroundColor = "black" + bar_outer.style.borderRadius = "5px" + progress.onchangeinit(v => bar_inner.style.width = `${v * 100}%`) + text.onchangeinit(v => bar_text.textContent = v) + return bar_outer +} + +globalThis.addEventListener("DOMContentLoaded", () => { + if (!document.getElementById("admin_import")) return + const el = document.getElementById("admin_import")! + + const ws = new WebSocket(`/admin/import`) + ws.onopen = () => console.log("live log connected"); + ws.onclose = () => console.log("live log disconnected"); + ws.onerror = e => console.log("live log ws error", e); + + + const progress = new OVar(0) + const text = new OVar("") + const pre = e("pre") + el.append(progress_bar(progress, text), pre) + + ws.onmessage = msg => { + if (msg.data == "done") return location.reload() + const p: ImportProgress = JSON.parse(msg.data) + text.value = `${p.finished_items} / ${p.total_items}` + progress.value = p.finished_items / p.total_items + pre.textContent = p.tasks.map((e, i) => `thread ${("#" + i).padStart(3)}: ${e}`).join("\n") + } +}) diff --git a/web/script/log_stream.ts b/web/script/log_live.ts index 053c110..053c110 100644 --- a/web/script/log_stream.ts +++ b/web/script/log_live.ts diff --git a/web/script/main.ts b/web/script/main.ts index d7a36cb..d5905d3 100644 --- a/web/script/main.ts +++ b/web/script/main.ts @@ -8,4 +8,5 @@ import "./player/mod.ts" import "./transition.ts" import "./backbutton.ts" import "./dangerbutton.ts" -import "./log_stream.ts" +import "./log_live.ts" +import "./import_live.ts" |