aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authormetamuffin <metamuffin@disroot.org>2025-12-11 01:20:17 +0100
committermetamuffin <metamuffin@disroot.org>2025-12-11 01:20:17 +0100
commit6e5f6d9b9c6fedb4ab80190c156595d321d33bbf (patch)
treeb6c2140e744fc3018ad08975afefad40386ebbc6
parente4f865e9da9d6660399e22a6fbeb5b84a749b07a (diff)
downloadjellything-6e5f6d9b9c6fedb4ab80190c156595d321d33bbf.tar
jellything-6e5f6d9b9c6fedb4ab80190c156595d321d33bbf.tar.bz2
jellything-6e5f6d9b9c6fedb4ab80190c156595d321d33bbf.tar.zst
refactor import plugins part 3
-rw-r--r--Cargo.lock191
-rw-r--r--common/src/routes.rs5
-rw-r--r--import/src/lib.rs747
-rw-r--r--import/src/plugins/acoustid.rs3
-rw-r--r--import/src/plugins/misc.rs37
-rw-r--r--import/src/plugins/mod.rs20
-rw-r--r--import/src/plugins/trakt.rs2
-rw-r--r--import/src/reporting.rs2
-rw-r--r--locale/en.ini2
-rw-r--r--logic/src/admin/mod.rs10
-rw-r--r--server/src/routes.rs14
-rw-r--r--server/src/ui/admin/import.rs70
-rw-r--r--server/src/ui/admin/log.rs4
-rw-r--r--server/src/ui/admin/mod.rs17
-rw-r--r--ui/src/admin/import.rs50
-rw-r--r--ui/src/admin/mod.rs10
-rw-r--r--web/script/import_live.ts64
-rw-r--r--web/script/log_live.ts (renamed from web/script/log_stream.ts)0
-rw-r--r--web/script/main.ts3
19 files changed, 735 insertions, 516 deletions
diff --git a/Cargo.lock b/Cargo.lock
index 49de608..b18f88b 100644
--- a/Cargo.lock
+++ b/Cargo.lock
@@ -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"