Compare commits
No commits in common. "dbded94324ed85aef4051ad2419de2f077710650" and "0300d7baf62d4926fbfa5e9748175469e5fc59b2" have entirely different histories.
dbded94324
...
0300d7baf6
414
Cargo.lock
generated
414
Cargo.lock
generated
@ -27,41 +27,6 @@ version = "2.0.0"
|
|||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "512761e0bb2578dd7380c6baaa0f4ce03e84f95e960231d1dec8bf4d7d6e2627"
|
checksum = "512761e0bb2578dd7380c6baaa0f4ce03e84f95e960231d1dec8bf4d7d6e2627"
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "aead"
|
|
||||||
version = "0.5.2"
|
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "d122413f284cf2d62fb1b7db97e02edb8cda96d769b16e443a4f6195e35662b0"
|
|
||||||
dependencies = [
|
|
||||||
"crypto-common",
|
|
||||||
"generic-array 0.14.7",
|
|
||||||
]
|
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "aes"
|
|
||||||
version = "0.8.4"
|
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "b169f7a6d4742236a0a00c541b845991d0ac43e546831af1249753ab4c3aa3a0"
|
|
||||||
dependencies = [
|
|
||||||
"cfg-if",
|
|
||||||
"cipher",
|
|
||||||
"cpufeatures",
|
|
||||||
]
|
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "aes-gcm"
|
|
||||||
version = "0.10.3"
|
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "831010a0f742e1209b3bcea8fab6a8e149051ba6099432c8cb2cc117dec3ead1"
|
|
||||||
dependencies = [
|
|
||||||
"aead",
|
|
||||||
"aes",
|
|
||||||
"cipher",
|
|
||||||
"ctr",
|
|
||||||
"ghash",
|
|
||||||
"subtle",
|
|
||||||
]
|
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "ahash"
|
name = "ahash"
|
||||||
version = "0.7.8"
|
version = "0.7.8"
|
||||||
@ -193,56 +158,6 @@ version = "1.0.97"
|
|||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "dcfed56ad506cb2c684a14971b8861fdc3baaaae314b9e5f9bb532cbe3ba7a4f"
|
checksum = "dcfed56ad506cb2c684a14971b8861fdc3baaaae314b9e5f9bb532cbe3ba7a4f"
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "apalis"
|
|
||||||
version = "0.7.1"
|
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "5bf157b59923258974a886572a18fe47b401daeca43b44c719b73736d8788840"
|
|
||||||
dependencies = [
|
|
||||||
"apalis-core",
|
|
||||||
"futures",
|
|
||||||
"pin-project-lite",
|
|
||||||
"serde",
|
|
||||||
"thiserror 2.0.12",
|
|
||||||
"tower",
|
|
||||||
"tracing",
|
|
||||||
"tracing-futures",
|
|
||||||
]
|
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "apalis-core"
|
|
||||||
version = "0.7.1"
|
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "9fbbc8dc67f007145277cb5977c730c4fa7fb07244e83d69d1c5a43cb4d124fa"
|
|
||||||
dependencies = [
|
|
||||||
"futures",
|
|
||||||
"futures-timer",
|
|
||||||
"pin-project-lite",
|
|
||||||
"serde",
|
|
||||||
"serde_json",
|
|
||||||
"thiserror 2.0.12",
|
|
||||||
"tower",
|
|
||||||
"ulid",
|
|
||||||
]
|
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "apalis-sql"
|
|
||||||
version = "0.7.1"
|
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "34757d9408f39656451c524ca10fe6331d59aaf25cda60bd70d677a4213efead"
|
|
||||||
dependencies = [
|
|
||||||
"apalis-core",
|
|
||||||
"async-stream",
|
|
||||||
"chrono",
|
|
||||||
"futures",
|
|
||||||
"futures-lite",
|
|
||||||
"log",
|
|
||||||
"serde",
|
|
||||||
"serde_json",
|
|
||||||
"sqlx",
|
|
||||||
"thiserror 2.0.12",
|
|
||||||
]
|
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "arc-swap"
|
name = "arc-swap"
|
||||||
version = "1.7.1"
|
version = "1.7.1"
|
||||||
@ -335,7 +250,6 @@ dependencies = [
|
|||||||
"async-stream",
|
"async-stream",
|
||||||
"async-trait",
|
"async-trait",
|
||||||
"base64 0.22.1",
|
"base64 0.22.1",
|
||||||
"bigdecimal",
|
|
||||||
"bytes",
|
"bytes",
|
||||||
"chrono",
|
"chrono",
|
||||||
"fast_chemail",
|
"fast_chemail",
|
||||||
@ -359,7 +273,6 @@ dependencies = [
|
|||||||
"static_assertions_next",
|
"static_assertions_next",
|
||||||
"tempfile",
|
"tempfile",
|
||||||
"thiserror 1.0.69",
|
"thiserror 1.0.69",
|
||||||
"time",
|
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
@ -733,15 +646,6 @@ dependencies = [
|
|||||||
"wyz",
|
"wyz",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "block-buffer"
|
|
||||||
version = "0.9.0"
|
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "4152116fd6e9dadb291ae18fc1ec3575ed6d84c29642d97890f4b4a3417297e4"
|
|
||||||
dependencies = [
|
|
||||||
"generic-array 0.14.7",
|
|
||||||
]
|
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "block-buffer"
|
name = "block-buffer"
|
||||||
version = "0.10.4"
|
version = "0.10.4"
|
||||||
@ -911,7 +815,7 @@ version = "13.1.0"
|
|||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "5c5063741c7b2e260bbede781cf4679632dd90e2718e99f7715e46824b65670b"
|
checksum = "5c5063741c7b2e260bbede781cf4679632dd90e2718e99f7715e46824b65670b"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"digest 0.10.7",
|
"digest",
|
||||||
"either",
|
"either",
|
||||||
"futures",
|
"futures",
|
||||||
"hex 0.4.3",
|
"hex 0.4.3",
|
||||||
@ -923,7 +827,7 @@ dependencies = [
|
|||||||
"serde_derive",
|
"serde_derive",
|
||||||
"serde_json",
|
"serde_json",
|
||||||
"sha1",
|
"sha1",
|
||||||
"sha2 0.10.8",
|
"sha2",
|
||||||
"ssri",
|
"ssri",
|
||||||
"tempfile",
|
"tempfile",
|
||||||
"thiserror 1.0.69",
|
"thiserror 1.0.69",
|
||||||
@ -965,30 +869,6 @@ dependencies = [
|
|||||||
"version_check",
|
"version_check",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "chacha20"
|
|
||||||
version = "0.9.1"
|
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "c3613f74bd2eac03dad61bd53dbe620703d4371614fe0bc3b9f04dd36fe4e818"
|
|
||||||
dependencies = [
|
|
||||||
"cfg-if",
|
|
||||||
"cipher",
|
|
||||||
"cpufeatures",
|
|
||||||
]
|
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "chacha20poly1305"
|
|
||||||
version = "0.10.1"
|
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "10cd79432192d1c0f4e1a0fef9527696cc039165d729fb41b3f4f4f354c2dc35"
|
|
||||||
dependencies = [
|
|
||||||
"aead",
|
|
||||||
"chacha20",
|
|
||||||
"cipher",
|
|
||||||
"poly1305",
|
|
||||||
"zeroize",
|
|
||||||
]
|
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "chrono"
|
name = "chrono"
|
||||||
version = "0.4.40"
|
version = "0.4.40"
|
||||||
@ -1026,17 +906,6 @@ dependencies = [
|
|||||||
"phf_codegen",
|
"phf_codegen",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "cipher"
|
|
||||||
version = "0.4.4"
|
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "773f3b9af64447d2ce9850330c473515014aa235e6a783b02db81ff39e4a3dad"
|
|
||||||
dependencies = [
|
|
||||||
"crypto-common",
|
|
||||||
"inout",
|
|
||||||
"zeroize",
|
|
||||||
]
|
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "clap"
|
name = "clap"
|
||||||
version = "4.5.35"
|
version = "4.5.35"
|
||||||
@ -1077,22 +946,6 @@ version = "0.7.4"
|
|||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "f46ad14479a25103f283c0f10005961cf086d8dc42205bb44c46ac563475dca6"
|
checksum = "f46ad14479a25103f283c0f10005961cf086d8dc42205bb44c46ac563475dca6"
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "cocoon"
|
|
||||||
version = "0.4.3"
|
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "24bf1b609cc3fcf6785a2305e450e3dae64cc0f28854ae0b83a6487a8eeaa64f"
|
|
||||||
dependencies = [
|
|
||||||
"aes-gcm",
|
|
||||||
"chacha20poly1305",
|
|
||||||
"hmac 0.11.0",
|
|
||||||
"pbkdf2",
|
|
||||||
"rand 0.8.5",
|
|
||||||
"sha2 0.9.9",
|
|
||||||
"thiserror 1.0.69",
|
|
||||||
"zeroize",
|
|
||||||
]
|
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "colorchoice"
|
name = "colorchoice"
|
||||||
version = "1.0.3"
|
version = "1.0.3"
|
||||||
@ -1353,16 +1206,6 @@ dependencies = [
|
|||||||
"winapi",
|
"winapi",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "crypto-mac"
|
|
||||||
version = "0.11.0"
|
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "25fab6889090c8133f3deb8f73ba3c65a7f456f66436fc012a1b1e272b1e103e"
|
|
||||||
dependencies = [
|
|
||||||
"generic-array 0.14.7",
|
|
||||||
"subtle",
|
|
||||||
]
|
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "cssparser"
|
name = "cssparser"
|
||||||
version = "0.33.0"
|
version = "0.33.0"
|
||||||
@ -1424,15 +1267,6 @@ version = "0.0.5"
|
|||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "4f211af61d8efdd104f96e57adf5e426ba1bc3ed7a4ead616e15e5881fd79c4d"
|
checksum = "4f211af61d8efdd104f96e57adf5e426ba1bc3ed7a4ead616e15e5881fd79c4d"
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "ctr"
|
|
||||||
version = "0.9.2"
|
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "0369ee1ad671834580515889b80f2ea915f23b8be8d0daa4bbaf2ac5c7590835"
|
|
||||||
dependencies = [
|
|
||||||
"cipher",
|
|
||||||
]
|
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "curve25519-dalek"
|
name = "curve25519-dalek"
|
||||||
version = "4.1.3"
|
version = "4.1.3"
|
||||||
@ -1442,7 +1276,7 @@ dependencies = [
|
|||||||
"cfg-if",
|
"cfg-if",
|
||||||
"cpufeatures",
|
"cpufeatures",
|
||||||
"curve25519-dalek-derive",
|
"curve25519-dalek-derive",
|
||||||
"digest 0.10.7",
|
"digest",
|
||||||
"fiat-crypto",
|
"fiat-crypto",
|
||||||
"rustc_version",
|
"rustc_version",
|
||||||
"subtle",
|
"subtle",
|
||||||
@ -1607,22 +1441,13 @@ version = "1.6.1"
|
|||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "dc55fe0d1f6c107595572ec8b107c0999bb1a2e0b75e37429a4fb0d6474a0e7d"
|
checksum = "dc55fe0d1f6c107595572ec8b107c0999bb1a2e0b75e37429a4fb0d6474a0e7d"
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "digest"
|
|
||||||
version = "0.9.0"
|
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "d3dd60d1080a57a05ab032377049e0591415d2b31afd7028356dbf3cc6dcb066"
|
|
||||||
dependencies = [
|
|
||||||
"generic-array 0.14.7",
|
|
||||||
]
|
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "digest"
|
name = "digest"
|
||||||
version = "0.10.7"
|
version = "0.10.7"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "9ed9a281f7bc9b7576e61468ba615a66a5c8cfdff42420a70aa82701a3b1e292"
|
checksum = "9ed9a281f7bc9b7576e61468ba615a66a5c8cfdff42420a70aa82701a3b1e292"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"block-buffer 0.10.4",
|
"block-buffer",
|
||||||
"const-oid",
|
"const-oid",
|
||||||
"crypto-common",
|
"crypto-common",
|
||||||
"subtle",
|
"subtle",
|
||||||
@ -1777,7 +1602,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
|
|||||||
checksum = "ee27f32b5c5292967d2d4a9d7f1e0b0aed2c15daded5a60300e4abb9d8020bca"
|
checksum = "ee27f32b5c5292967d2d4a9d7f1e0b0aed2c15daded5a60300e4abb9d8020bca"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"der",
|
"der",
|
||||||
"digest 0.10.7",
|
"digest",
|
||||||
"elliptic-curve",
|
"elliptic-curve",
|
||||||
"rfc6979",
|
"rfc6979",
|
||||||
"signature",
|
"signature",
|
||||||
@ -1803,7 +1628,7 @@ dependencies = [
|
|||||||
"curve25519-dalek",
|
"curve25519-dalek",
|
||||||
"ed25519",
|
"ed25519",
|
||||||
"serde",
|
"serde",
|
||||||
"sha2 0.10.8",
|
"sha2",
|
||||||
"subtle",
|
"subtle",
|
||||||
"zeroize",
|
"zeroize",
|
||||||
]
|
]
|
||||||
@ -1831,7 +1656,7 @@ checksum = "b5e6043086bf7973472e0c7dff2142ea0b680d30e18d9cc40f267efbf222bd47"
|
|||||||
dependencies = [
|
dependencies = [
|
||||||
"base16ct",
|
"base16ct",
|
||||||
"crypto-bigint",
|
"crypto-bigint",
|
||||||
"digest 0.10.7",
|
"digest",
|
||||||
"ff",
|
"ff",
|
||||||
"generic-array 0.14.7",
|
"generic-array 0.14.7",
|
||||||
"group",
|
"group",
|
||||||
@ -1941,6 +1766,7 @@ dependencies = [
|
|||||||
"axum",
|
"axum",
|
||||||
"axum-extra",
|
"axum-extra",
|
||||||
"bytes",
|
"bytes",
|
||||||
|
"cookie",
|
||||||
"fastrand",
|
"fastrand",
|
||||||
"http-cache",
|
"http-cache",
|
||||||
"http-cache-reqwest",
|
"http-cache-reqwest",
|
||||||
@ -1952,7 +1778,6 @@ dependencies = [
|
|||||||
"reqwest-middleware",
|
"reqwest-middleware",
|
||||||
"reqwest-retry",
|
"reqwest-retry",
|
||||||
"reqwest-tracing",
|
"reqwest-tracing",
|
||||||
"reqwest_cookie_store",
|
|
||||||
"serde",
|
"serde",
|
||||||
"serde_json",
|
"serde_json",
|
||||||
"serde_with",
|
"serde_with",
|
||||||
@ -2145,19 +1970,6 @@ version = "0.3.31"
|
|||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "9e5c1b78ca4aae1ac06c48a526a655760685149f0d465d21f37abfe57ce075c6"
|
checksum = "9e5c1b78ca4aae1ac06c48a526a655760685149f0d465d21f37abfe57ce075c6"
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "futures-lite"
|
|
||||||
version = "2.6.0"
|
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "f5edaec856126859abb19ed65f39e90fea3a9574b9707f13539acf4abf7eb532"
|
|
||||||
dependencies = [
|
|
||||||
"fastrand",
|
|
||||||
"futures-core",
|
|
||||||
"futures-io",
|
|
||||||
"parking",
|
|
||||||
"pin-project-lite",
|
|
||||||
]
|
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "futures-macro"
|
name = "futures-macro"
|
||||||
version = "0.3.31"
|
version = "0.3.31"
|
||||||
@ -2296,16 +2108,6 @@ dependencies = [
|
|||||||
"wasm-bindgen",
|
"wasm-bindgen",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "ghash"
|
|
||||||
version = "0.5.1"
|
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "f0d8a4362ccb29cb0b265253fb0a2728f592895ee6854fd9bc13f2ffda266ff1"
|
|
||||||
dependencies = [
|
|
||||||
"opaque-debug",
|
|
||||||
"polyval",
|
|
||||||
]
|
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "gimli"
|
name = "gimli"
|
||||||
version = "0.31.1"
|
version = "0.31.1"
|
||||||
@ -2371,7 +2173,7 @@ dependencies = [
|
|||||||
"parking_lot 0.12.3",
|
"parking_lot 0.12.3",
|
||||||
"portable-atomic",
|
"portable-atomic",
|
||||||
"quanta",
|
"quanta",
|
||||||
"rand 0.9.1",
|
"rand 0.9.0",
|
||||||
"smallvec",
|
"smallvec",
|
||||||
"spinning_top",
|
"spinning_top",
|
||||||
"web-time",
|
"web-time",
|
||||||
@ -2510,17 +2312,7 @@ version = "0.12.4"
|
|||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "7b5f8eb2ad728638ea2c7d47a21db23b7b58a72ed6a38256b8a1849f15fbbdf7"
|
checksum = "7b5f8eb2ad728638ea2c7d47a21db23b7b58a72ed6a38256b8a1849f15fbbdf7"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"hmac 0.12.1",
|
"hmac",
|
||||||
]
|
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "hmac"
|
|
||||||
version = "0.11.0"
|
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "2a2a2320eb7ec0ebe8da8f744d7812d9fc4cb4d09344ac01898dbcb6a20ae69b"
|
|
||||||
dependencies = [
|
|
||||||
"crypto-mac",
|
|
||||||
"digest 0.9.0",
|
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
@ -2529,7 +2321,7 @@ version = "0.12.1"
|
|||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "6c49c37c09c17a53d937dfbb742eb3a961d65a994e6bcdcf37e7399d0cc8ab5e"
|
checksum = "6c49c37c09c17a53d937dfbb742eb3a961d65a994e6bcdcf37e7399d0cc8ab5e"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"digest 0.10.7",
|
"digest",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
@ -3038,15 +2830,6 @@ dependencies = [
|
|||||||
"libc",
|
"libc",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "inout"
|
|
||||||
version = "0.1.4"
|
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "879f10e63c20629ecabbb64a8010319738c66a5cd0c29b02d63d272b03751d01"
|
|
||||||
dependencies = [
|
|
||||||
"generic-array 0.14.7",
|
|
||||||
]
|
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "insta"
|
name = "insta"
|
||||||
version = "1.42.2"
|
version = "1.42.2"
|
||||||
@ -3611,9 +3394,9 @@ dependencies = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "lightningcss"
|
name = "lightningcss"
|
||||||
version = "1.0.0-alpha.66"
|
version = "1.0.0-alpha.65"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "9a73ffa17de66534e4b527232f44aa0a89fad22c4f4e0735f9be35494f058e54"
|
checksum = "c84f971730745f4aaac013b6cf4328baf1548efc973c0d95cfd843a3c1ca07af"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"ahash 0.8.11",
|
"ahash 0.8.11",
|
||||||
"bitflags 2.9.0",
|
"bitflags 2.9.0",
|
||||||
@ -3788,7 +3571,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
|
|||||||
checksum = "d89e7ee0cfbedfc4da3340218492196241d89eefb6dab27de5df917a6d2e78cf"
|
checksum = "d89e7ee0cfbedfc4da3340218492196241d89eefb6dab27de5df917a6d2e78cf"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"cfg-if",
|
"cfg-if",
|
||||||
"digest 0.10.7",
|
"digest",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
@ -3901,7 +3684,7 @@ dependencies = [
|
|||||||
"hyper",
|
"hyper",
|
||||||
"hyper-util",
|
"hyper-util",
|
||||||
"log",
|
"log",
|
||||||
"rand 0.9.1",
|
"rand 0.9.0",
|
||||||
"regex",
|
"regex",
|
||||||
"serde_json",
|
"serde_json",
|
||||||
"serde_urlencoded",
|
"serde_urlencoded",
|
||||||
@ -4172,7 +3955,7 @@ dependencies = [
|
|||||||
"serde",
|
"serde",
|
||||||
"serde_json",
|
"serde_json",
|
||||||
"serde_path_to_error",
|
"serde_path_to_error",
|
||||||
"sha2 0.10.8",
|
"sha2",
|
||||||
"thiserror 1.0.69",
|
"thiserror 1.0.69",
|
||||||
"url",
|
"url",
|
||||||
]
|
]
|
||||||
@ -4192,12 +3975,6 @@ version = "1.21.3"
|
|||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "42f5e15c9953c5e4ccceeb2e7382a716482c34515315f7b03532b8b4e8393d2d"
|
checksum = "42f5e15c9953c5e4ccceeb2e7382a716482c34515315f7b03532b8b4e8393d2d"
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "opaque-debug"
|
|
||||||
version = "0.3.1"
|
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "c08d65885ee38876c4f86fa503fb49d7b507c2b62552df7c70b2fce627e06381"
|
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "opendal"
|
name = "opendal"
|
||||||
version = "0.53.0"
|
version = "0.53.0"
|
||||||
@ -4234,7 +4011,7 @@ dependencies = [
|
|||||||
"chrono",
|
"chrono",
|
||||||
"dyn-clone",
|
"dyn-clone",
|
||||||
"ed25519-dalek",
|
"ed25519-dalek",
|
||||||
"hmac 0.12.1",
|
"hmac",
|
||||||
"http",
|
"http",
|
||||||
"itertools 0.10.5",
|
"itertools 0.10.5",
|
||||||
"log",
|
"log",
|
||||||
@ -4249,7 +4026,7 @@ dependencies = [
|
|||||||
"serde_path_to_error",
|
"serde_path_to_error",
|
||||||
"serde_plain",
|
"serde_plain",
|
||||||
"serde_with",
|
"serde_with",
|
||||||
"sha2 0.10.8",
|
"sha2",
|
||||||
"subtle",
|
"subtle",
|
||||||
"thiserror 1.0.69",
|
"thiserror 1.0.69",
|
||||||
"url",
|
"url",
|
||||||
@ -4368,7 +4145,7 @@ dependencies = [
|
|||||||
"ecdsa",
|
"ecdsa",
|
||||||
"elliptic-curve",
|
"elliptic-curve",
|
||||||
"primeorder",
|
"primeorder",
|
||||||
"sha2 0.10.8",
|
"sha2",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
@ -4380,14 +4157,14 @@ dependencies = [
|
|||||||
"ecdsa",
|
"ecdsa",
|
||||||
"elliptic-curve",
|
"elliptic-curve",
|
||||||
"primeorder",
|
"primeorder",
|
||||||
"sha2 0.10.8",
|
"sha2",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "parcel_selectors"
|
name = "parcel_selectors"
|
||||||
version = "0.28.2"
|
version = "0.28.1"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "54fd03f1ad26cb6b3ec1b7414fa78a3bd639e7dbb421b1a60513c96ce886a196"
|
checksum = "dccbc6fb560df303a44e511618256029410efbc87779018f751ef12c488271fe"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"bitflags 2.9.0",
|
"bitflags 2.9.0",
|
||||||
"cssparser 0.33.0",
|
"cssparser 0.33.0",
|
||||||
@ -4513,17 +4290,6 @@ version = "0.2.3"
|
|||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "df94ce210e5bc13cb6651479fa48d14f601d9858cfe0467f43ae157023b938d3"
|
checksum = "df94ce210e5bc13cb6651479fa48d14f601d9858cfe0467f43ae157023b938d3"
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "pbkdf2"
|
|
||||||
version = "0.9.0"
|
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "f05894bce6a1ba4be299d0c5f29563e08af2bc18bb7d48313113bed71e904739"
|
|
||||||
dependencies = [
|
|
||||||
"crypto-mac",
|
|
||||||
"hmac 0.11.0",
|
|
||||||
"sha2 0.9.9",
|
|
||||||
]
|
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "pear"
|
name = "pear"
|
||||||
version = "0.2.9"
|
version = "0.2.9"
|
||||||
@ -4614,7 +4380,7 @@ checksum = "7f9f832470494906d1fca5329f8ab5791cc60beb230c74815dff541cbd2b5ca0"
|
|||||||
dependencies = [
|
dependencies = [
|
||||||
"once_cell",
|
"once_cell",
|
||||||
"pest",
|
"pest",
|
||||||
"sha2 0.10.8",
|
"sha2",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
@ -4737,29 +4503,6 @@ version = "0.3.32"
|
|||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "7edddbd0b52d732b21ad9a5fab5c704c14cd949e5e9a1ec5929a24fded1b904c"
|
checksum = "7edddbd0b52d732b21ad9a5fab5c704c14cd949e5e9a1ec5929a24fded1b904c"
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "poly1305"
|
|
||||||
version = "0.8.0"
|
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "8159bd90725d2df49889a078b54f4f79e87f1f8a8444194cdca81d38f5393abf"
|
|
||||||
dependencies = [
|
|
||||||
"cpufeatures",
|
|
||||||
"opaque-debug",
|
|
||||||
"universal-hash",
|
|
||||||
]
|
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "polyval"
|
|
||||||
version = "0.6.2"
|
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "9d1fe60d06143b2430aa532c94cfe9e29783047f06c0d7fd359a9a51b729fa25"
|
|
||||||
dependencies = [
|
|
||||||
"cfg-if",
|
|
||||||
"cpufeatures",
|
|
||||||
"opaque-debug",
|
|
||||||
"universal-hash",
|
|
||||||
]
|
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "portable-atomic"
|
name = "portable-atomic"
|
||||||
version = "1.11.0"
|
version = "1.11.0"
|
||||||
@ -4969,7 +4712,7 @@ checksum = "b820744eb4dc9b57a3398183639c511b5a26d2ed702cedd3febaa1393caa22cc"
|
|||||||
dependencies = [
|
dependencies = [
|
||||||
"bytes",
|
"bytes",
|
||||||
"getrandom 0.3.2",
|
"getrandom 0.3.2",
|
||||||
"rand 0.9.1",
|
"rand 0.9.0",
|
||||||
"ring",
|
"ring",
|
||||||
"rustc-hash 2.1.1",
|
"rustc-hash 2.1.1",
|
||||||
"rustls",
|
"rustls",
|
||||||
@ -5042,12 +4785,13 @@ dependencies = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "rand"
|
name = "rand"
|
||||||
version = "0.9.1"
|
version = "0.9.0"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "9fbfd9d094a40bf3ae768db9361049ace4c0e04a4fd6b359518bd7b73a73dd97"
|
checksum = "3779b94aeb87e8bd4e834cee3650289ee9e0d5677f976ecdb6d219e5f4f6cd94"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"rand_chacha 0.9.0",
|
"rand_chacha 0.9.0",
|
||||||
"rand_core 0.9.3",
|
"rand_core 0.9.3",
|
||||||
|
"zerocopy 0.8.24",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
@ -5121,8 +4865,6 @@ dependencies = [
|
|||||||
name = "recorder"
|
name = "recorder"
|
||||||
version = "0.1.0"
|
version = "0.1.0"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"apalis",
|
|
||||||
"apalis-sql",
|
|
||||||
"async-graphql",
|
"async-graphql",
|
||||||
"async-graphql-axum",
|
"async-graphql-axum",
|
||||||
"async-stream",
|
"async-stream",
|
||||||
@ -5133,7 +4875,6 @@ dependencies = [
|
|||||||
"bytes",
|
"bytes",
|
||||||
"chrono",
|
"chrono",
|
||||||
"clap",
|
"clap",
|
||||||
"cocoon",
|
|
||||||
"ctor",
|
"ctor",
|
||||||
"dotenv",
|
"dotenv",
|
||||||
"downloader",
|
"downloader",
|
||||||
@ -5157,16 +4898,13 @@ dependencies = [
|
|||||||
"opendal",
|
"opendal",
|
||||||
"openidconnect",
|
"openidconnect",
|
||||||
"quirks_path",
|
"quirks_path",
|
||||||
"rand 0.9.1",
|
|
||||||
"regex",
|
"regex",
|
||||||
"reqwest_cookie_store",
|
|
||||||
"rss",
|
"rss",
|
||||||
"rstest",
|
"rstest",
|
||||||
"scraper",
|
"scraper",
|
||||||
"sea-orm",
|
"sea-orm",
|
||||||
"sea-orm-migration",
|
"sea-orm-migration",
|
||||||
"seaography",
|
"seaography",
|
||||||
"secrecy",
|
|
||||||
"serde",
|
"serde",
|
||||||
"serde_json",
|
"serde_json",
|
||||||
"serde_variant",
|
"serde_variant",
|
||||||
@ -5409,20 +5147,6 @@ dependencies = [
|
|||||||
"tracing",
|
"tracing",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "reqwest_cookie_store"
|
|
||||||
version = "0.8.0"
|
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "a0b36498c7452f11b1833900f31fbb01fc46be20992a50269c88cf59d79f54e9"
|
|
||||||
dependencies = [
|
|
||||||
"bytes",
|
|
||||||
"cookie_store",
|
|
||||||
"reqwest",
|
|
||||||
"serde",
|
|
||||||
"serde_derive",
|
|
||||||
"url",
|
|
||||||
]
|
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "retry-policies"
|
name = "retry-policies"
|
||||||
version = "0.4.0"
|
version = "0.4.0"
|
||||||
@ -5438,7 +5162,7 @@ version = "0.4.0"
|
|||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "f8dd2a808d456c4a54e300a23e9f5a67e122c3024119acbfd73e3bf664491cb2"
|
checksum = "f8dd2a808d456c4a54e300a23e9f5a67e122c3024119acbfd73e3bf664491cb2"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"hmac 0.12.1",
|
"hmac",
|
||||||
"subtle",
|
"subtle",
|
||||||
]
|
]
|
||||||
|
|
||||||
@ -5501,7 +5225,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
|
|||||||
checksum = "78928ac1ed176a5ca1d17e578a1825f3d81ca54cf41053a592584b020cfd691b"
|
checksum = "78928ac1ed176a5ca1d17e578a1825f3d81ca54cf41053a592584b020cfd691b"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"const-oid",
|
"const-oid",
|
||||||
"digest 0.10.7",
|
"digest",
|
||||||
"num-bigint-dig",
|
"num-bigint-dig",
|
||||||
"num-integer",
|
"num-integer",
|
||||||
"num-traits",
|
"num-traits",
|
||||||
@ -5934,15 +5658,6 @@ dependencies = [
|
|||||||
"zeroize",
|
"zeroize",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "secrecy"
|
|
||||||
version = "0.10.3"
|
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "e891af845473308773346dc847b2c23ee78fe442e0472ac50e22a18a93d3ae5a"
|
|
||||||
dependencies = [
|
|
||||||
"zeroize",
|
|
||||||
]
|
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "security-framework"
|
name = "security-framework"
|
||||||
version = "2.11.1"
|
version = "2.11.1"
|
||||||
@ -6191,7 +5906,7 @@ checksum = "f5058ada175748e33390e40e872bd0fe59a19f265d0158daa551c5a88a76009c"
|
|||||||
dependencies = [
|
dependencies = [
|
||||||
"cfg-if",
|
"cfg-if",
|
||||||
"cpufeatures",
|
"cpufeatures",
|
||||||
"digest 0.10.7",
|
"digest",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
@ -6202,20 +5917,7 @@ checksum = "e3bf829a2d51ab4a5ddf1352d8470c140cadc8301b2ae1789db023f01cedd6ba"
|
|||||||
dependencies = [
|
dependencies = [
|
||||||
"cfg-if",
|
"cfg-if",
|
||||||
"cpufeatures",
|
"cpufeatures",
|
||||||
"digest 0.10.7",
|
"digest",
|
||||||
]
|
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "sha2"
|
|
||||||
version = "0.9.9"
|
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "4d58a1e1bf39749807d89cf2d98ac2dfa0ff1cb3faa38fbb64dd88ac8013d800"
|
|
||||||
dependencies = [
|
|
||||||
"block-buffer 0.9.0",
|
|
||||||
"cfg-if",
|
|
||||||
"cpufeatures",
|
|
||||||
"digest 0.9.0",
|
|
||||||
"opaque-debug",
|
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
@ -6226,7 +5928,7 @@ checksum = "793db75ad2bcafc3ffa7c68b215fee268f537982cd901d132f89c6343f3a3dc8"
|
|||||||
dependencies = [
|
dependencies = [
|
||||||
"cfg-if",
|
"cfg-if",
|
||||||
"cpufeatures",
|
"cpufeatures",
|
||||||
"digest 0.10.7",
|
"digest",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
@ -6259,7 +5961,7 @@ version = "2.2.0"
|
|||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "77549399552de45a898a580c1b41d445bf730df867cc44e6c0233bbc4b8329de"
|
checksum = "77549399552de45a898a580c1b41d445bf730df867cc44e6c0233bbc4b8329de"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"digest 0.10.7",
|
"digest",
|
||||||
"rand_core 0.6.4",
|
"rand_core 0.6.4",
|
||||||
]
|
]
|
||||||
|
|
||||||
@ -6449,7 +6151,7 @@ dependencies = [
|
|||||||
"rustls-pemfile",
|
"rustls-pemfile",
|
||||||
"serde",
|
"serde",
|
||||||
"serde_json",
|
"serde_json",
|
||||||
"sha2 0.10.8",
|
"sha2",
|
||||||
"smallvec",
|
"smallvec",
|
||||||
"thiserror 2.0.12",
|
"thiserror 2.0.12",
|
||||||
"time",
|
"time",
|
||||||
@ -6489,7 +6191,7 @@ dependencies = [
|
|||||||
"quote",
|
"quote",
|
||||||
"serde",
|
"serde",
|
||||||
"serde_json",
|
"serde_json",
|
||||||
"sha2 0.10.8",
|
"sha2",
|
||||||
"sqlx-core",
|
"sqlx-core",
|
||||||
"sqlx-mysql",
|
"sqlx-mysql",
|
||||||
"sqlx-postgres",
|
"sqlx-postgres",
|
||||||
@ -6514,7 +6216,7 @@ dependencies = [
|
|||||||
"bytes",
|
"bytes",
|
||||||
"chrono",
|
"chrono",
|
||||||
"crc",
|
"crc",
|
||||||
"digest 0.10.7",
|
"digest",
|
||||||
"dotenvy",
|
"dotenvy",
|
||||||
"either",
|
"either",
|
||||||
"futures-channel",
|
"futures-channel",
|
||||||
@ -6524,7 +6226,7 @@ dependencies = [
|
|||||||
"generic-array 0.14.7",
|
"generic-array 0.14.7",
|
||||||
"hex 0.4.3",
|
"hex 0.4.3",
|
||||||
"hkdf",
|
"hkdf",
|
||||||
"hmac 0.12.1",
|
"hmac",
|
||||||
"itoa",
|
"itoa",
|
||||||
"log",
|
"log",
|
||||||
"md-5",
|
"md-5",
|
||||||
@ -6536,7 +6238,7 @@ dependencies = [
|
|||||||
"rust_decimal",
|
"rust_decimal",
|
||||||
"serde",
|
"serde",
|
||||||
"sha1",
|
"sha1",
|
||||||
"sha2 0.10.8",
|
"sha2",
|
||||||
"smallvec",
|
"smallvec",
|
||||||
"sqlx-core",
|
"sqlx-core",
|
||||||
"stringprep",
|
"stringprep",
|
||||||
@ -6567,7 +6269,7 @@ dependencies = [
|
|||||||
"futures-util",
|
"futures-util",
|
||||||
"hex 0.4.3",
|
"hex 0.4.3",
|
||||||
"hkdf",
|
"hkdf",
|
||||||
"hmac 0.12.1",
|
"hmac",
|
||||||
"home",
|
"home",
|
||||||
"itoa",
|
"itoa",
|
||||||
"log",
|
"log",
|
||||||
@ -6579,7 +6281,7 @@ dependencies = [
|
|||||||
"rust_decimal",
|
"rust_decimal",
|
||||||
"serde",
|
"serde",
|
||||||
"serde_json",
|
"serde_json",
|
||||||
"sha2 0.10.8",
|
"sha2",
|
||||||
"smallvec",
|
"smallvec",
|
||||||
"sqlx-core",
|
"sqlx-core",
|
||||||
"stringprep",
|
"stringprep",
|
||||||
@ -6623,12 +6325,12 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
|
|||||||
checksum = "da7a2b3c2bc9693bcb40870c4e9b5bf0d79f9cb46273321bf855ec513e919082"
|
checksum = "da7a2b3c2bc9693bcb40870c4e9b5bf0d79f9cb46273321bf855ec513e919082"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"base64 0.21.7",
|
"base64 0.21.7",
|
||||||
"digest 0.10.7",
|
"digest",
|
||||||
"hex 0.4.3",
|
"hex 0.4.3",
|
||||||
"miette",
|
"miette",
|
||||||
"serde",
|
"serde",
|
||||||
"sha-1",
|
"sha-1",
|
||||||
"sha2 0.10.8",
|
"sha2",
|
||||||
"thiserror 1.0.69",
|
"thiserror 1.0.69",
|
||||||
"xxhash-rust",
|
"xxhash-rust",
|
||||||
]
|
]
|
||||||
@ -7202,7 +6904,6 @@ dependencies = [
|
|||||||
"pin-project-lite",
|
"pin-project-lite",
|
||||||
"sync_wrapper",
|
"sync_wrapper",
|
||||||
"tokio",
|
"tokio",
|
||||||
"tokio-util",
|
|
||||||
"tower-layer",
|
"tower-layer",
|
||||||
"tower-service",
|
"tower-service",
|
||||||
"tracing",
|
"tracing",
|
||||||
@ -7293,15 +6994,6 @@ dependencies = [
|
|||||||
"valuable",
|
"valuable",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "tracing-futures"
|
|
||||||
version = "0.2.5"
|
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "97d095ae15e245a057c8e8451bab9b3ee1e1f68e9ba2b4fbc18d0ac5237835f2"
|
|
||||||
dependencies = [
|
|
||||||
"tracing",
|
|
||||||
]
|
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "tracing-log"
|
name = "tracing-log"
|
||||||
version = "0.2.0"
|
version = "0.2.0"
|
||||||
@ -7361,7 +7053,7 @@ dependencies = [
|
|||||||
"http",
|
"http",
|
||||||
"httparse",
|
"httparse",
|
||||||
"log",
|
"log",
|
||||||
"rand 0.9.1",
|
"rand 0.9.0",
|
||||||
"sha1",
|
"sha1",
|
||||||
"thiserror 2.0.12",
|
"thiserror 2.0.12",
|
||||||
"utf-8",
|
"utf-8",
|
||||||
@ -7419,16 +7111,6 @@ version = "0.1.7"
|
|||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "2896d95c02a80c6d6a5d6e953d479f5ddf2dfdb6a244441010e373ac0fb88971"
|
checksum = "2896d95c02a80c6d6a5d6e953d479f5ddf2dfdb6a244441010e373ac0fb88971"
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "ulid"
|
|
||||||
version = "1.2.1"
|
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "470dbf6591da1b39d43c14523b2b469c86879a53e8b758c8e090a470fe7b1fbe"
|
|
||||||
dependencies = [
|
|
||||||
"rand 0.9.1",
|
|
||||||
"web-time",
|
|
||||||
]
|
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "uncased"
|
name = "uncased"
|
||||||
version = "0.9.10"
|
version = "0.9.10"
|
||||||
@ -7533,16 +7215,6 @@ version = "0.1.14"
|
|||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "7dd6e30e90baa6f72411720665d41d89b9a3d039dc45b8faea1ddd07f617f6af"
|
checksum = "7dd6e30e90baa6f72411720665d41d89b9a3d039dc45b8faea1ddd07f617f6af"
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "universal-hash"
|
|
||||||
version = "0.5.1"
|
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "fc1de2c688dc15305988b563c3854064043356019f97a4b46276fe734c4f07ea"
|
|
||||||
dependencies = [
|
|
||||||
"crypto-common",
|
|
||||||
"subtle",
|
|
||||||
]
|
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "unsafe-libyaml"
|
name = "unsafe-libyaml"
|
||||||
version = "0.2.11"
|
version = "0.2.11"
|
||||||
|
|||||||
@ -11,6 +11,7 @@ resolver = "2"
|
|||||||
[workspace.dependencies]
|
[workspace.dependencies]
|
||||||
moka = "0.12"
|
moka = "0.12"
|
||||||
futures = "0.3"
|
futures = "0.3"
|
||||||
|
futures-util = "0.3"
|
||||||
quirks_path = "0.1"
|
quirks_path = "0.1"
|
||||||
snafu = { version = "0.8", features = ["futures"] }
|
snafu = { version = "0.8", features = ["futures"] }
|
||||||
testcontainers = { version = "0.23.3" }
|
testcontainers = { version = "0.23.3" }
|
||||||
@ -45,6 +46,7 @@ testing-torrents = { path = "./packages/testing-torrents" }
|
|||||||
util = { path = "./packages/util" }
|
util = { path = "./packages/util" }
|
||||||
fetch = { path = "./packages/fetch" }
|
fetch = { path = "./packages/fetch" }
|
||||||
downloader = { path = "./packages/downloader" }
|
downloader = { path = "./packages/downloader" }
|
||||||
|
recorder = { path = "./apps/recorder" }
|
||||||
|
|
||||||
[patch.crates-io]
|
[patch.crates-io]
|
||||||
jwt-authorizer = { git = "https://github.com/blablacio/jwt-authorizer.git", rev = "e956774" }
|
jwt-authorizer = { git = "https://github.com/blablacio/jwt-authorizer.git", rev = "e956774" }
|
||||||
|
|||||||
@ -5,7 +5,6 @@
|
|||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
#^https://konobangu.com/api*** statusCode://500
|
|
||||||
^https://konobangu.com/api*** reqHeaders://{x-forwarded.json} http://127.0.0.1:5001/api$1
|
^https://konobangu.com/api*** reqHeaders://{x-forwarded.json} http://127.0.0.1:5001/api$1
|
||||||
^https://konobangu.com/*** reqHeaders://{x-forwarded.json} http://127.0.0.1:5000/$1 excludeFilter://^https://konobangu.com/api***
|
^https://konobangu.com/*** reqHeaders://{x-forwarded.json} http://127.0.0.1:5000/$1 excludeFilter://^https://konobangu.com/api***
|
||||||
^wss://konobangu.com/*** reqHeaders://{x-forwarded.json} ws://127.0.0.1:5000/$1 excludeFilter://^wss://konobangu.com/api
|
^wss://konobangu.com/*** reqHeaders://{x-forwarded.json} ws://127.0.0.1:5000/$1 excludeFilter://^wss://konobangu.com/api
|
||||||
@ -1 +1 @@
|
|||||||
{"filesOrder":["konobangu"],"selectedList":["konobangu"],"disabledDefalutRules":true,"defalutRules":""}
|
{"filesOrder":["konobangu"],"selectedList":["konobangu"],"disabledDefalutRules":true}
|
||||||
|
|||||||
@ -61,7 +61,7 @@ sea-orm-migration = { version = "1.1", features = ["runtime-tokio-rustls"] }
|
|||||||
rss = "2"
|
rss = "2"
|
||||||
fancy-regex = "0.14"
|
fancy-regex = "0.14"
|
||||||
maplit = "1.0.2"
|
maplit = "1.0.2"
|
||||||
lightningcss = "1.0.0-alpha.66"
|
lightningcss = "1.0.0-alpha.65"
|
||||||
html-escape = "0.2.13"
|
html-escape = "0.2.13"
|
||||||
opendal = { version = "0.53", features = ["default", "services-fs"] }
|
opendal = { version = "0.53", features = ["default", "services-fs"] }
|
||||||
zune-image = "0.4.15"
|
zune-image = "0.4.15"
|
||||||
@ -72,15 +72,7 @@ jwt-authorizer = "0.15.0"
|
|||||||
log = "0.4"
|
log = "0.4"
|
||||||
async-graphql = { version = "7", features = [] }
|
async-graphql = { version = "7", features = [] }
|
||||||
async-graphql-axum = "7"
|
async-graphql-axum = "7"
|
||||||
seaography = { version = "1.1", features = [
|
seaography = { version = "1.1" }
|
||||||
"with-json",
|
|
||||||
"with-chrono",
|
|
||||||
"with-time",
|
|
||||||
"with-uuid",
|
|
||||||
"with-decimal",
|
|
||||||
"with-bigdecimal",
|
|
||||||
"with-postgres-array",
|
|
||||||
] }
|
|
||||||
base64 = "0.22.1"
|
base64 = "0.22.1"
|
||||||
tower = "0.5.2"
|
tower = "0.5.2"
|
||||||
tower-http = { version = "0.6", features = [
|
tower-http = { version = "0.6", features = [
|
||||||
@ -104,16 +96,11 @@ clap = "4.5.31"
|
|||||||
ipnetwork = "0.21.1"
|
ipnetwork = "0.21.1"
|
||||||
typed-builder = "0.21.0"
|
typed-builder = "0.21.0"
|
||||||
serde_yaml = "0.9.34"
|
serde_yaml = "0.9.34"
|
||||||
apalis = { version = "0.7", features = ["limit", "tracing", "catch-panic"] }
|
|
||||||
apalis-sql = { version = "0.7", features = ["postgres"] }
|
|
||||||
cocoon = { version = "0.4.3", features = ["getrandom", "thiserror"] }
|
|
||||||
rand = "0.9.1"
|
|
||||||
downloader = { workspace = true }
|
downloader = { workspace = true }
|
||||||
util = { workspace = true }
|
util = { workspace = true }
|
||||||
fetch = { workspace = true }
|
fetch = { workspace = true }
|
||||||
string-interner = "0.19.0"
|
string-interner = "0.19.0"
|
||||||
secrecy = "0.10.3"
|
|
||||||
reqwest_cookie_store = "0.8.0"
|
|
||||||
|
|
||||||
[dev-dependencies]
|
[dev-dependencies]
|
||||||
serial_test = "3"
|
serial_test = "3"
|
||||||
|
|||||||
@ -1,3 +1,5 @@
|
|||||||
|
use std::sync::Arc;
|
||||||
|
|
||||||
use clap::{Parser, command};
|
use clap::{Parser, command};
|
||||||
|
|
||||||
use super::{AppContext, core::App, env::Environment};
|
use super::{AppContext, core::App, env::Environment};
|
||||||
@ -81,8 +83,9 @@ impl AppBuilder {
|
|||||||
)
|
)
|
||||||
.await?;
|
.await?;
|
||||||
|
|
||||||
let app_context =
|
let app_context = Arc::new(
|
||||||
AppContext::new(self.environment.clone(), config, self.working_dir.clone()).await?;
|
AppContext::new(self.environment.clone(), config, self.working_dir.clone()).await?,
|
||||||
|
);
|
||||||
|
|
||||||
Ok(App {
|
Ok(App {
|
||||||
context: app_context,
|
context: app_context,
|
||||||
|
|||||||
@ -16,7 +16,3 @@ depth_limit = inf
|
|||||||
complexity_limit = inf
|
complexity_limit = inf
|
||||||
|
|
||||||
[cache]
|
[cache]
|
||||||
|
|
||||||
[crypto]
|
|
||||||
|
|
||||||
[task]
|
|
||||||
|
|||||||
@ -9,9 +9,9 @@ use serde::{Deserialize, Serialize};
|
|||||||
|
|
||||||
use super::env::Environment;
|
use super::env::Environment;
|
||||||
use crate::{
|
use crate::{
|
||||||
auth::AuthConfig, cache::CacheConfig, crypto::CryptoConfig, database::DatabaseConfig,
|
auth::AuthConfig, cache::CacheConfig, database::DatabaseConfig, errors::RecorderResult,
|
||||||
errors::RecorderResult, extract::mikan::MikanConfig, graphql::GraphQLConfig,
|
extract::mikan::MikanConfig, graphql::GraphQLConfig, logger::LoggerConfig,
|
||||||
logger::LoggerConfig, storage::StorageConfig, tasks::TaskConfig, web::WebServerConfig,
|
storage::StorageConfig, web::WebServerConfig,
|
||||||
};
|
};
|
||||||
|
|
||||||
const DEFAULT_CONFIG_MIXIN: &str = include_str!("./default_mixin.toml");
|
const DEFAULT_CONFIG_MIXIN: &str = include_str!("./default_mixin.toml");
|
||||||
@ -24,11 +24,9 @@ pub struct AppConfig {
|
|||||||
pub auth: AuthConfig,
|
pub auth: AuthConfig,
|
||||||
pub storage: StorageConfig,
|
pub storage: StorageConfig,
|
||||||
pub mikan: MikanConfig,
|
pub mikan: MikanConfig,
|
||||||
pub crypto: CryptoConfig,
|
|
||||||
pub graphql: GraphQLConfig,
|
pub graphql: GraphQLConfig,
|
||||||
pub logger: LoggerConfig,
|
pub logger: LoggerConfig,
|
||||||
pub database: DatabaseConfig,
|
pub database: DatabaseConfig,
|
||||||
pub tasks: TaskConfig,
|
|
||||||
}
|
}
|
||||||
|
|
||||||
impl AppConfig {
|
impl AppConfig {
|
||||||
|
|||||||
@ -1,15 +1,11 @@
|
|||||||
use std::{fmt::Debug, sync::Arc};
|
|
||||||
|
|
||||||
use tokio::sync::OnceCell;
|
|
||||||
|
|
||||||
use super::{Environment, config::AppConfig};
|
use super::{Environment, config::AppConfig};
|
||||||
use crate::{
|
use crate::{
|
||||||
auth::AuthService, cache::CacheService, crypto::CryptoService, database::DatabaseService,
|
auth::AuthService, cache::CacheService, database::DatabaseService, errors::RecorderResult,
|
||||||
errors::RecorderResult, extract::mikan::MikanClient, graphql::GraphQLService,
|
extract::mikan::MikanClient, graphql::GraphQLService, logger::LoggerService,
|
||||||
logger::LoggerService, storage::StorageService, tasks::TaskService,
|
storage::StorageService,
|
||||||
};
|
};
|
||||||
|
|
||||||
pub trait AppContextTrait: Send + Sync + Debug {
|
pub trait AppContextTrait: Send + Sync {
|
||||||
fn logger(&self) -> &LoggerService;
|
fn logger(&self) -> &LoggerService;
|
||||||
fn db(&self) -> &DatabaseService;
|
fn db(&self) -> &DatabaseService;
|
||||||
fn config(&self) -> &AppConfig;
|
fn config(&self) -> &AppConfig;
|
||||||
@ -20,8 +16,6 @@ pub trait AppContextTrait: Send + Sync + Debug {
|
|||||||
fn storage(&self) -> &StorageService;
|
fn storage(&self) -> &StorageService;
|
||||||
fn working_dir(&self) -> &String;
|
fn working_dir(&self) -> &String;
|
||||||
fn environment(&self) -> &Environment;
|
fn environment(&self) -> &Environment;
|
||||||
fn crypto(&self) -> &CryptoService;
|
|
||||||
fn task(&self) -> &TaskService;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
pub struct AppContext {
|
pub struct AppContext {
|
||||||
@ -33,10 +27,8 @@ pub struct AppContext {
|
|||||||
auth: AuthService,
|
auth: AuthService,
|
||||||
graphql: GraphQLService,
|
graphql: GraphQLService,
|
||||||
storage: StorageService,
|
storage: StorageService,
|
||||||
crypto: CryptoService,
|
|
||||||
working_dir: String,
|
working_dir: String,
|
||||||
environment: Environment,
|
environment: Environment,
|
||||||
task: OnceCell<TaskService>,
|
|
||||||
}
|
}
|
||||||
|
|
||||||
impl AppContext {
|
impl AppContext {
|
||||||
@ -44,7 +36,7 @@ impl AppContext {
|
|||||||
environment: Environment,
|
environment: Environment,
|
||||||
config: AppConfig,
|
config: AppConfig,
|
||||||
working_dir: impl ToString,
|
working_dir: impl ToString,
|
||||||
) -> RecorderResult<Arc<Self>> {
|
) -> RecorderResult<Self> {
|
||||||
let config_cloned = config.clone();
|
let config_cloned = config.clone();
|
||||||
|
|
||||||
let logger = LoggerService::from_config(config.logger).await?;
|
let logger = LoggerService::from_config(config.logger).await?;
|
||||||
@ -53,10 +45,9 @@ impl AppContext {
|
|||||||
let storage = StorageService::from_config(config.storage).await?;
|
let storage = StorageService::from_config(config.storage).await?;
|
||||||
let auth = AuthService::from_conf(config.auth).await?;
|
let auth = AuthService::from_conf(config.auth).await?;
|
||||||
let mikan = MikanClient::from_config(config.mikan).await?;
|
let mikan = MikanClient::from_config(config.mikan).await?;
|
||||||
let crypto = CryptoService::from_config(config.crypto).await?;
|
|
||||||
let graphql = GraphQLService::from_config_and_database(config.graphql, db.clone()).await?;
|
let graphql = GraphQLService::from_config_and_database(config.graphql, db.clone()).await?;
|
||||||
|
|
||||||
let ctx = Arc::new(AppContext {
|
Ok(AppContext {
|
||||||
config: config_cloned,
|
config: config_cloned,
|
||||||
environment,
|
environment,
|
||||||
logger,
|
logger,
|
||||||
@ -67,26 +58,9 @@ impl AppContext {
|
|||||||
mikan,
|
mikan,
|
||||||
working_dir: working_dir.to_string(),
|
working_dir: working_dir.to_string(),
|
||||||
graphql,
|
graphql,
|
||||||
crypto,
|
})
|
||||||
task: OnceCell::new(),
|
|
||||||
});
|
|
||||||
|
|
||||||
ctx.task
|
|
||||||
.get_or_try_init(async || {
|
|
||||||
TaskService::from_config_and_ctx(config.tasks, ctx.clone()).await
|
|
||||||
})
|
|
||||||
.await?;
|
|
||||||
|
|
||||||
Ok(ctx)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Debug for AppContext {
|
|
||||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
|
||||||
write!(f, "AppContext")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl AppContextTrait for AppContext {
|
impl AppContextTrait for AppContext {
|
||||||
fn logger(&self) -> &LoggerService {
|
fn logger(&self) -> &LoggerService {
|
||||||
&self.logger
|
&self.logger
|
||||||
@ -118,10 +92,4 @@ impl AppContextTrait for AppContext {
|
|||||||
fn environment(&self) -> &Environment {
|
fn environment(&self) -> &Environment {
|
||||||
&self.environment
|
&self.environment
|
||||||
}
|
}
|
||||||
fn crypto(&self) -> &CryptoService {
|
|
||||||
&self.crypto
|
|
||||||
}
|
|
||||||
fn task(&self) -> &TaskService {
|
|
||||||
self.task.get().expect("task should be set")
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|||||||
@ -9,7 +9,7 @@ use axum::{
|
|||||||
|
|
||||||
use crate::{app::AppContextTrait, auth::AuthServiceTrait};
|
use crate::{app::AppContextTrait, auth::AuthServiceTrait};
|
||||||
|
|
||||||
pub async fn auth_middleware(
|
pub async fn header_www_authenticate_middleware(
|
||||||
State(ctx): State<Arc<dyn AppContextTrait>>,
|
State(ctx): State<Arc<dyn AppContextTrait>>,
|
||||||
request: Request,
|
request: Request,
|
||||||
next: Next,
|
next: Next,
|
||||||
|
|||||||
@ -7,5 +7,5 @@ pub mod service;
|
|||||||
|
|
||||||
pub use config::{AuthConfig, BasicAuthConfig, OidcAuthConfig};
|
pub use config::{AuthConfig, BasicAuthConfig, OidcAuthConfig};
|
||||||
pub use errors::AuthError;
|
pub use errors::AuthError;
|
||||||
pub use middleware::auth_middleware;
|
pub use middleware::header_www_authenticate_middleware;
|
||||||
pub use service::{AuthService, AuthServiceTrait, AuthUserInfo};
|
pub use service::{AuthService, AuthServiceTrait, AuthUserInfo};
|
||||||
|
|||||||
@ -366,10 +366,7 @@ impl AuthServiceTrait for OidcAuthService {
|
|||||||
}) => crate::models::auth::Model::create_from_oidc(ctx, sub.to_string()).await,
|
}) => crate::models::auth::Model::create_from_oidc(ctx, sub.to_string()).await,
|
||||||
r => r,
|
r => r,
|
||||||
}
|
}
|
||||||
.map_err(|e| {
|
.map_err(|_| AuthError::FindAuthRecordError)?;
|
||||||
tracing::error!("Error finding auth record: {:?}", e);
|
|
||||||
AuthError::FindAuthRecordError
|
|
||||||
})?;
|
|
||||||
|
|
||||||
Ok(AuthUserInfo {
|
Ok(AuthUserInfo {
|
||||||
subscriber_auth,
|
subscriber_auth,
|
||||||
|
|||||||
@ -1,4 +0,0 @@
|
|||||||
use serde::{Deserialize, Serialize};
|
|
||||||
|
|
||||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
|
||||||
pub struct CryptoConfig {}
|
|
||||||
@ -1,11 +0,0 @@
|
|||||||
#[derive(Debug, snafu::Snafu)]
|
|
||||||
pub enum CryptoError {
|
|
||||||
#[snafu(transparent)]
|
|
||||||
Base64DecodeError { source: base64::DecodeError },
|
|
||||||
#[snafu(display("CocoonError: {source:?}"), context(false))]
|
|
||||||
CocoonError { source: cocoon::Error },
|
|
||||||
#[snafu(transparent)]
|
|
||||||
FromUtf8Error { source: std::string::FromUtf8Error },
|
|
||||||
#[snafu(transparent)]
|
|
||||||
SerdeJsonError { source: serde_json::Error },
|
|
||||||
}
|
|
||||||
@ -1,9 +0,0 @@
|
|||||||
pub mod config;
|
|
||||||
pub mod error;
|
|
||||||
pub mod service;
|
|
||||||
pub mod userpass;
|
|
||||||
|
|
||||||
pub use config::CryptoConfig;
|
|
||||||
pub use error::CryptoError;
|
|
||||||
pub use service::CryptoService;
|
|
||||||
pub use userpass::UserPassCredential;
|
|
||||||
@ -1,65 +0,0 @@
|
|||||||
use base64::prelude::{BASE64_URL_SAFE, *};
|
|
||||||
use cocoon::Cocoon;
|
|
||||||
use rand::Rng;
|
|
||||||
use serde::{Deserialize, Serialize};
|
|
||||||
|
|
||||||
use super::CryptoConfig;
|
|
||||||
use crate::crypto::error::CryptoError;
|
|
||||||
|
|
||||||
pub struct CryptoService {
|
|
||||||
#[allow(dead_code)]
|
|
||||||
config: CryptoConfig,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl CryptoService {
|
|
||||||
pub async fn from_config(config: CryptoConfig) -> Result<Self, CryptoError> {
|
|
||||||
Ok(Self { config })
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn encrypt_data(&self, data: String) -> Result<String, CryptoError> {
|
|
||||||
let key = rand::rng().random::<[u8; 32]>();
|
|
||||||
let mut cocoon = Cocoon::new(&key);
|
|
||||||
|
|
||||||
let mut data = data.into_bytes();
|
|
||||||
|
|
||||||
let detached_prefix = cocoon.encrypt(&mut data)?;
|
|
||||||
|
|
||||||
let mut combined = Vec::with_capacity(key.len() + detached_prefix.len() + data.len());
|
|
||||||
combined.extend_from_slice(&key);
|
|
||||||
combined.extend_from_slice(&detached_prefix);
|
|
||||||
combined.extend_from_slice(&data);
|
|
||||||
|
|
||||||
Ok(BASE64_URL_SAFE.encode(combined))
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn decrypt_data(&self, data: &str) -> Result<String, CryptoError> {
|
|
||||||
let decoded = BASE64_URL_SAFE.decode(data)?;
|
|
||||||
|
|
||||||
let (key, remain) = decoded.split_at(32);
|
|
||||||
let (detached_prefix, data) = remain.split_at(60);
|
|
||||||
let mut data = data.to_vec();
|
|
||||||
let cocoon = Cocoon::new(key);
|
|
||||||
|
|
||||||
cocoon.decrypt(&mut data, detached_prefix)?;
|
|
||||||
|
|
||||||
String::from_utf8(data).map_err(CryptoError::from)
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn encrypt_credentials<T: Serialize>(
|
|
||||||
&self,
|
|
||||||
credentials: &T,
|
|
||||||
) -> Result<String, CryptoError> {
|
|
||||||
let json = serde_json::to_string(credentials)?;
|
|
||||||
|
|
||||||
self.encrypt_data(json)
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn decrypt_credentials<T: for<'de> Deserialize<'de>>(
|
|
||||||
&self,
|
|
||||||
encrypted: &str,
|
|
||||||
) -> Result<T, CryptoError> {
|
|
||||||
let data = self.decrypt_data(encrypted)?;
|
|
||||||
|
|
||||||
serde_json::from_str(&data).map_err(CryptoError::from)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@ -1,19 +0,0 @@
|
|||||||
use std::fmt::Debug;
|
|
||||||
|
|
||||||
pub struct UserPassCredential {
|
|
||||||
pub username: String,
|
|
||||||
pub password: String,
|
|
||||||
pub user_agent: Option<String>,
|
|
||||||
pub cookies: Option<String>,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl Debug for UserPassCredential {
|
|
||||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
|
||||||
f.debug_struct("UserPassCredential")
|
|
||||||
.field("username", &"[Secret]")
|
|
||||||
.field("password", &"[Secret]")
|
|
||||||
.field("cookies", &"[Secret]")
|
|
||||||
.field("user_agent", &self.user_agent)
|
|
||||||
.finish()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@ -1,8 +1,8 @@
|
|||||||
use std::{ops::Deref, time::Duration};
|
use std::{ops::Deref, time::Duration};
|
||||||
|
|
||||||
use sea_orm::{
|
use sea_orm::{
|
||||||
ConnectOptions, ConnectionTrait, Database, DatabaseConnection, DbBackend, DbErr, ExecResult,
|
ConnectOptions, ConnectionTrait, Database, DatabaseBackend, DatabaseConnection, DbBackend,
|
||||||
QueryResult, Statement,
|
DbErr, ExecResult, QueryResult, Statement,
|
||||||
};
|
};
|
||||||
use sea_orm_migration::MigratorTrait;
|
use sea_orm_migration::MigratorTrait;
|
||||||
|
|
||||||
@ -28,21 +28,20 @@ impl DatabaseService {
|
|||||||
|
|
||||||
let db = Database::connect(opt).await?;
|
let db = Database::connect(opt).await?;
|
||||||
|
|
||||||
// only support postgres for now
|
if db.get_database_backend() == DatabaseBackend::Sqlite {
|
||||||
// if db.get_database_backend() == DatabaseBackend::Sqlite {
|
db.execute(Statement::from_string(
|
||||||
// db.execute(Statement::from_string(
|
DatabaseBackend::Sqlite,
|
||||||
// DatabaseBackend::Sqlite,
|
"
|
||||||
// "
|
PRAGMA foreign_keys = ON;
|
||||||
// PRAGMA foreign_keys = ON;
|
PRAGMA journal_mode = WAL;
|
||||||
// PRAGMA journal_mode = WAL;
|
PRAGMA synchronous = NORMAL;
|
||||||
// PRAGMA synchronous = NORMAL;
|
PRAGMA mmap_size = 134217728;
|
||||||
// PRAGMA mmap_size = 134217728;
|
PRAGMA journal_size_limit = 67108864;
|
||||||
// PRAGMA journal_size_limit = 67108864;
|
PRAGMA cache_size = 2000;
|
||||||
// PRAGMA cache_size = 2000;
|
",
|
||||||
// ",
|
))
|
||||||
// ))
|
.await?;
|
||||||
// .await?;
|
}
|
||||||
// }
|
|
||||||
|
|
||||||
if config.auto_migrate {
|
if config.auto_migrate {
|
||||||
Migrator::up(&db, None).await?;
|
Migrator::up(&db, None).await?;
|
||||||
|
|||||||
@ -4,14 +4,13 @@ use axum::{
|
|||||||
Json,
|
Json,
|
||||||
response::{IntoResponse, Response},
|
response::{IntoResponse, Response},
|
||||||
};
|
};
|
||||||
use fetch::{FetchError, HttpClientError, reqwest, reqwest_middleware};
|
use fetch::{FetchError, HttpClientError};
|
||||||
use http::StatusCode;
|
use http::StatusCode;
|
||||||
use serde::{Deserialize, Deserializer, Serialize};
|
use serde::{Deserialize, Deserializer, Serialize};
|
||||||
use snafu::Snafu;
|
use snafu::Snafu;
|
||||||
|
|
||||||
use crate::{
|
use crate::{
|
||||||
auth::AuthError,
|
auth::AuthError,
|
||||||
crypto::CryptoError,
|
|
||||||
downloader::DownloaderError,
|
downloader::DownloaderError,
|
||||||
errors::{OptDynErr, response::StandardErrorResponse},
|
errors::{OptDynErr, response::StandardErrorResponse},
|
||||||
};
|
};
|
||||||
@ -103,14 +102,6 @@ pub enum RecorderError {
|
|||||||
ModelEntityNotFound { entity: Cow<'static, str> },
|
ModelEntityNotFound { entity: Cow<'static, str> },
|
||||||
#[snafu(transparent)]
|
#[snafu(transparent)]
|
||||||
FetchError { source: FetchError },
|
FetchError { source: FetchError },
|
||||||
#[snafu(display("Credential3rdError: {source}"))]
|
|
||||||
Credential3rdError {
|
|
||||||
message: String,
|
|
||||||
#[snafu(source(from(Box<dyn std::error::Error + Send + Sync>, OptDynErr::some)))]
|
|
||||||
source: OptDynErr,
|
|
||||||
},
|
|
||||||
#[snafu(transparent)]
|
|
||||||
CryptoError { source: CryptoError },
|
|
||||||
#[snafu(display("{message}"))]
|
#[snafu(display("{message}"))]
|
||||||
Whatever {
|
Whatever {
|
||||||
message: String,
|
message: String,
|
||||||
@ -204,16 +195,4 @@ impl<'de> Deserialize<'de> for RecorderError {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl From<reqwest::Error> for RecorderError {
|
|
||||||
fn from(error: reqwest::Error) -> Self {
|
|
||||||
FetchError::from(error).into()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl From<reqwest_middleware::Error> for RecorderError {
|
|
||||||
fn from(error: reqwest_middleware::Error) -> Self {
|
|
||||||
FetchError::from(error).into()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
pub type RecorderResult<T> = Result<T, RecorderError>;
|
pub type RecorderResult<T> = Result<T, RecorderError>;
|
||||||
|
|||||||
@ -2,10 +2,7 @@ use url::Url;
|
|||||||
|
|
||||||
pub fn extract_image_src_from_str(image_src: &str, base_url: &Url) -> Option<Url> {
|
pub fn extract_image_src_from_str(image_src: &str, base_url: &Url) -> Option<Url> {
|
||||||
let mut image_url = base_url.join(image_src).ok()?;
|
let mut image_url = base_url.join(image_src).ok()?;
|
||||||
if let Some((_, value)) = image_url.query_pairs().find(|(key, _)| key == "webp") {
|
image_url.set_query(None);
|
||||||
image_url.set_query(Some(&format!("webp={}", value)));
|
image_url.set_fragment(None);
|
||||||
} else {
|
|
||||||
image_url.set_query(None);
|
|
||||||
}
|
|
||||||
Some(image_url)
|
Some(image_url)
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,204 +1,60 @@
|
|||||||
use std::{fmt::Debug, ops::Deref, sync::Arc};
|
use std::{fmt::Debug, ops::Deref};
|
||||||
|
|
||||||
use fetch::{HttpClient, HttpClientTrait};
|
use fetch::{HttpClient, HttpClientTrait, client::HttpClientCookiesAuth};
|
||||||
use maplit::hashmap;
|
|
||||||
use sea_orm::DbErr;
|
|
||||||
use secrecy::SecretBox;
|
|
||||||
use serde::{Deserialize, Serialize};
|
use serde::{Deserialize, Serialize};
|
||||||
use url::Url;
|
use url::Url;
|
||||||
use util::OptDynErr;
|
|
||||||
|
|
||||||
use super::{MikanConfig, constants::MIKAN_ACCOUNT_MANAGE_PAGE_PATH};
|
use super::MikanConfig;
|
||||||
use crate::{
|
use crate::errors::RecorderError;
|
||||||
app::AppContextTrait,
|
|
||||||
crypto::UserPassCredential,
|
|
||||||
errors::{RecorderError, RecorderResult},
|
|
||||||
extract::mikan::constants::{MIKAN_LOGIN_PAGE_PATH, MIKAN_LOGIN_PAGE_SEARCH},
|
|
||||||
models::credential_3rd::{self, Credential3rdType},
|
|
||||||
};
|
|
||||||
#[derive(Default, Clone, Deserialize, Serialize)]
|
#[derive(Default, Clone, Deserialize, Serialize)]
|
||||||
pub struct MikanCredentialForm {
|
pub struct MikanAuthSecrecy {
|
||||||
pub password: String,
|
pub cookie: String,
|
||||||
pub username: String,
|
pub user_agent: Option<String>,
|
||||||
pub user_agent: String,
|
|
||||||
}
|
}
|
||||||
|
|
||||||
pub type MikanAuthSecrecy = SecretBox<MikanCredentialForm>;
|
impl Debug for MikanAuthSecrecy {
|
||||||
|
|
||||||
impl Debug for MikanCredentialForm {
|
|
||||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||||
f.debug_struct("MikanCredentialForm")
|
f.debug_struct("MikanAuthSecrecy")
|
||||||
.field("username", &String::from("[secrecy]"))
|
.field("cookie", &String::from("[secrecy]"))
|
||||||
.field("password", &String::from("[secrecy]"))
|
|
||||||
.field("user_agent", &String::from("[secrecy]"))
|
.field("user_agent", &String::from("[secrecy]"))
|
||||||
.finish()
|
.finish()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
impl MikanAuthSecrecy {
|
||||||
|
pub fn into_cookie_auth(self, url: &Url) -> Result<HttpClientCookiesAuth, RecorderError> {
|
||||||
|
HttpClientCookiesAuth::from_cookies(&self.cookie, url, self.user_agent)
|
||||||
|
.map_err(RecorderError::from)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
#[derive(Debug)]
|
#[derive(Debug)]
|
||||||
pub struct MikanClient {
|
pub struct MikanClient {
|
||||||
http_client: HttpClient,
|
http_client: HttpClient,
|
||||||
base_url: Url,
|
base_url: Url,
|
||||||
origin_url: Url,
|
|
||||||
userpass_credential: Option<UserPassCredential>,
|
|
||||||
}
|
}
|
||||||
|
|
||||||
impl MikanClient {
|
impl MikanClient {
|
||||||
pub async fn from_config(config: MikanConfig) -> Result<Self, RecorderError> {
|
pub async fn from_config(config: MikanConfig) -> Result<Self, RecorderError> {
|
||||||
let http_client = HttpClient::from_config(config.http_client)?;
|
let http_client = HttpClient::from_config(config.http_client)?;
|
||||||
let base_url = config.base_url;
|
let base_url = config.base_url;
|
||||||
let origin_url = Url::parse(&base_url.origin().unicode_serialization())?;
|
|
||||||
Ok(Self {
|
Ok(Self {
|
||||||
http_client,
|
http_client,
|
||||||
base_url,
|
base_url,
|
||||||
origin_url,
|
|
||||||
userpass_credential: None,
|
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
pub async fn has_login(&self) -> RecorderResult<bool> {
|
pub fn fork_with_auth(&self, secrecy: Option<MikanAuthSecrecy>) -> Result<Self, RecorderError> {
|
||||||
let account_manage_page_url = self.base_url.join(MIKAN_ACCOUNT_MANAGE_PAGE_PATH)?;
|
|
||||||
let res = self.http_client.get(account_manage_page_url).send().await?;
|
|
||||||
let status = res.status();
|
|
||||||
if status.is_success() {
|
|
||||||
Ok(true)
|
|
||||||
} else if status.is_redirection()
|
|
||||||
&& res.headers().get("location").is_some_and(|location| {
|
|
||||||
location
|
|
||||||
.to_str()
|
|
||||||
.is_ok_and(|location_str| location_str.contains(MIKAN_LOGIN_PAGE_PATH))
|
|
||||||
})
|
|
||||||
{
|
|
||||||
Ok(false)
|
|
||||||
} else {
|
|
||||||
Err(RecorderError::Credential3rdError {
|
|
||||||
message: format!("mikan account check has login failed, status = {}", status),
|
|
||||||
source: None.into(),
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
pub async fn login(&self) -> RecorderResult<()> {
|
|
||||||
let userpass_credential =
|
|
||||||
self.userpass_credential
|
|
||||||
.as_ref()
|
|
||||||
.ok_or_else(|| RecorderError::Credential3rdError {
|
|
||||||
message: "mikan login failed, credential required".to_string(),
|
|
||||||
source: None.into(),
|
|
||||||
})?;
|
|
||||||
let login_page_url = {
|
|
||||||
let mut u = self.base_url.join(MIKAN_LOGIN_PAGE_PATH)?;
|
|
||||||
u.set_query(Some(MIKAN_LOGIN_PAGE_SEARCH));
|
|
||||||
u
|
|
||||||
};
|
|
||||||
|
|
||||||
// access login page to get antiforgery cookie
|
|
||||||
self.http_client
|
|
||||||
.get(login_page_url.clone())
|
|
||||||
.send()
|
|
||||||
.await
|
|
||||||
.map_err(|error| RecorderError::Credential3rdError {
|
|
||||||
message: "failed to get mikan login page".to_string(),
|
|
||||||
source: OptDynErr::some_boxed(error),
|
|
||||||
})?;
|
|
||||||
|
|
||||||
let antiforgery_cookie = {
|
|
||||||
let cookie_store_lock = self.http_client.cookie_store.clone().ok_or_else(|| {
|
|
||||||
RecorderError::Credential3rdError {
|
|
||||||
message: "failed to get cookie store".to_string(),
|
|
||||||
source: None.into(),
|
|
||||||
}
|
|
||||||
})?;
|
|
||||||
let cookie_store =
|
|
||||||
cookie_store_lock
|
|
||||||
.read()
|
|
||||||
.map_err(|_| RecorderError::Credential3rdError {
|
|
||||||
message: "failed to read cookie store".to_string(),
|
|
||||||
source: None.into(),
|
|
||||||
})?;
|
|
||||||
|
|
||||||
cookie_store
|
|
||||||
.matches(&login_page_url)
|
|
||||||
.iter()
|
|
||||||
.find(|cookie| cookie.name().starts_with(".AspNetCore.Antiforgery."))
|
|
||||||
.map(|cookie| cookie.value().to_string())
|
|
||||||
}
|
|
||||||
.ok_or_else(|| RecorderError::Credential3rdError {
|
|
||||||
message: "mikan login failed, failed to get antiforgery cookie".to_string(),
|
|
||||||
source: None.into(),
|
|
||||||
})?;
|
|
||||||
|
|
||||||
let login_post_form = hashmap! {
|
|
||||||
"__RequestVerificationToken".to_string() => antiforgery_cookie,
|
|
||||||
"UserName".to_string() => userpass_credential.username.clone(),
|
|
||||||
"Password".to_string() => userpass_credential.password.clone(),
|
|
||||||
"RememberMe".to_string() => "true".to_string(),
|
|
||||||
};
|
|
||||||
let login_post_res = self
|
|
||||||
.http_client
|
|
||||||
.post(login_page_url.clone())
|
|
||||||
.form(&login_post_form)
|
|
||||||
.send()
|
|
||||||
.await
|
|
||||||
.map_err(|err| RecorderError::Credential3rdError {
|
|
||||||
message: "mikan login failed".to_string(),
|
|
||||||
source: OptDynErr::some_boxed(err),
|
|
||||||
})?;
|
|
||||||
|
|
||||||
if login_post_res.status().is_redirection()
|
|
||||||
&& login_post_res.headers().contains_key("location")
|
|
||||||
{
|
|
||||||
Ok(())
|
|
||||||
} else {
|
|
||||||
Err(RecorderError::Credential3rdError {
|
|
||||||
message: "mikan login failed, no redirecting".to_string(),
|
|
||||||
source: None.into(),
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
pub async fn fork_with_credential(
|
|
||||||
&self,
|
|
||||||
ctx: Arc<dyn AppContextTrait>,
|
|
||||||
credential_id: Option<i32>,
|
|
||||||
) -> RecorderResult<Self> {
|
|
||||||
let mut fork = self.http_client.fork();
|
let mut fork = self.http_client.fork();
|
||||||
let mut userpass_credential_opt = None;
|
|
||||||
|
|
||||||
if let Some(credential_id) = credential_id {
|
if let Some(secrecy) = secrecy {
|
||||||
let credential = credential_3rd::Model::find_by_id(ctx.clone(), credential_id).await?;
|
let cookie_auth = secrecy.into_cookie_auth(&self.base_url)?;
|
||||||
if let Some(credential) = credential {
|
fork = fork.attach_secrecy(cookie_auth);
|
||||||
if credential.credential_type != Credential3rdType::Mikan {
|
|
||||||
return Err(RecorderError::Credential3rdError {
|
|
||||||
message: "credential is not a mikan credential".to_string(),
|
|
||||||
source: None.into(),
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
let userpass_credential: UserPassCredential =
|
|
||||||
credential.try_into_userpass_credential(ctx)?;
|
|
||||||
|
|
||||||
if let Some(cookies) = userpass_credential.cookies.as_ref() {
|
|
||||||
fork = fork.attach_cookies(cookies)?;
|
|
||||||
}
|
|
||||||
|
|
||||||
if let Some(user_agent) = userpass_credential.user_agent.as_ref() {
|
|
||||||
fork = fork.attach_user_agent(user_agent);
|
|
||||||
}
|
|
||||||
|
|
||||||
userpass_credential_opt = Some(userpass_credential);
|
|
||||||
} else {
|
|
||||||
return Err(RecorderError::from_db_record_not_found(
|
|
||||||
DbErr::RecordNotFound(format!("credential={} not found", credential_id)),
|
|
||||||
));
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
Ok(Self {
|
Ok(Self {
|
||||||
http_client: HttpClient::from_fork(fork)?,
|
http_client: HttpClient::from_fork(fork)?,
|
||||||
base_url: self.base_url.clone(),
|
base_url: self.base_url.clone(),
|
||||||
origin_url: self.origin_url.clone(),
|
|
||||||
userpass_credential: userpass_credential_opt,
|
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -1,6 +1,3 @@
|
|||||||
pub const MIKAN_BUCKET_KEY: &str = "mikan";
|
pub const MIKAN_BUCKET_KEY: &str = "mikan";
|
||||||
pub const MIKAN_UNKNOWN_FANSUB_NAME: &str = "生肉/不明字幕";
|
pub const MIKAN_UNKNOWN_FANSUB_NAME: &str = "生肉/不明字幕";
|
||||||
pub const MIKAN_UNKNOWN_FANSUB_ID: &str = "202";
|
pub const MIKAN_UNKNOWN_FANSUB_ID: &str = "202";
|
||||||
pub const MIKAN_LOGIN_PAGE_PATH: &str = "/Account/Login";
|
|
||||||
pub const MIKAN_LOGIN_PAGE_SEARCH: &str = "?ReturnUrl=%2F";
|
|
||||||
pub const MIKAN_ACCOUNT_MANAGE_PAGE_PATH: &str = "/Account/Manage";
|
|
||||||
|
|||||||
@ -4,20 +4,18 @@ pub mod constants;
|
|||||||
pub mod rss_extract;
|
pub mod rss_extract;
|
||||||
pub mod web_extract;
|
pub mod web_extract;
|
||||||
|
|
||||||
pub use client::{MikanClient, MikanCredentialForm};
|
pub use client::{MikanAuthSecrecy, MikanClient};
|
||||||
pub use config::MikanConfig;
|
pub use config::MikanConfig;
|
||||||
pub use constants::MIKAN_BUCKET_KEY;
|
pub use constants::MIKAN_BUCKET_KEY;
|
||||||
pub use rss_extract::{
|
pub use rss_extract::{
|
||||||
MikanBangumiAggregationRssChannel, MikanBangumiRssChannel, MikanBangumiRssUrlMeta,
|
MikanBangumiAggregationRssChannel, MikanBangumiRssChannel, MikanBangumiRssLink,
|
||||||
MikanRssChannel, MikanRssItem, MikanSubscriberAggregationRssChannel,
|
MikanRssChannel, MikanRssItem, MikanSubscriberAggregationRssChannel,
|
||||||
MikanSubscriberAggregationRssUrlMeta, build_mikan_bangumi_rss_url,
|
MikanSubscriberAggregationRssLink, build_mikan_bangumi_rss_link,
|
||||||
build_mikan_subscriber_aggregation_rss_url, extract_mikan_bangumi_id_from_rss_url,
|
build_mikan_subscriber_aggregation_rss_link, extract_mikan_bangumi_id_from_rss_link,
|
||||||
extract_mikan_rss_channel_from_rss_link, extract_mikan_subscriber_aggregation_id_from_rss_link,
|
extract_mikan_rss_channel_from_rss_link, extract_mikan_subscriber_aggregation_id_from_rss_link,
|
||||||
};
|
};
|
||||||
pub use web_extract::{
|
pub use web_extract::{
|
||||||
MikanBangumiMeta, MikanEpisodeMeta, MikanSeasonStr, build_mikan_bangumi_homepage_url,
|
MikanBangumiMeta, MikanEpisodeMeta, build_mikan_bangumi_homepage, build_mikan_episode_homepage,
|
||||||
build_mikan_episode_homepage_url, build_mikan_season_flow_url,
|
|
||||||
extract_mikan_bangumi_indices_meta_from_season_flow_fragment,
|
|
||||||
extract_mikan_bangumi_meta_from_bangumi_homepage,
|
extract_mikan_bangumi_meta_from_bangumi_homepage,
|
||||||
extract_mikan_episode_meta_from_episode_homepage,
|
extract_mikan_episode_meta_from_episode_homepage,
|
||||||
};
|
};
|
||||||
|
|||||||
@ -12,7 +12,7 @@ use crate::{
|
|||||||
errors::app_error::{RecorderError, RecorderResult},
|
errors::app_error::{RecorderError, RecorderResult},
|
||||||
extract::mikan::{
|
extract::mikan::{
|
||||||
MikanClient,
|
MikanClient,
|
||||||
web_extract::{MikanEpisodeHomepage, extract_mikan_episode_id_from_homepage_url},
|
web_extract::{MikanEpisodeHomepage, extract_mikan_episode_id_from_homepage},
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
@ -135,7 +135,7 @@ impl TryFrom<rss::Item> for MikanRssItem {
|
|||||||
|
|
||||||
let MikanEpisodeHomepage {
|
let MikanEpisodeHomepage {
|
||||||
mikan_episode_id, ..
|
mikan_episode_id, ..
|
||||||
} = extract_mikan_episode_id_from_homepage_url(&homepage).ok_or_else(|| {
|
} = extract_mikan_episode_id_from_homepage(&homepage).ok_or_else(|| {
|
||||||
RecorderError::from_mikan_rss_invalid_field(Cow::Borrowed("mikan_episode_id"))
|
RecorderError::from_mikan_rss_invalid_field(Cow::Borrowed("mikan_episode_id"))
|
||||||
})?;
|
})?;
|
||||||
|
|
||||||
@ -155,17 +155,17 @@ impl TryFrom<rss::Item> for MikanRssItem {
|
|||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Debug, Clone)]
|
#[derive(Debug, Clone)]
|
||||||
pub struct MikanBangumiRssUrlMeta {
|
pub struct MikanBangumiRssLink {
|
||||||
pub mikan_bangumi_id: String,
|
pub mikan_bangumi_id: String,
|
||||||
pub mikan_fansub_id: Option<String>,
|
pub mikan_fansub_id: Option<String>,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Debug, Clone)]
|
#[derive(Debug, Clone)]
|
||||||
pub struct MikanSubscriberAggregationRssUrlMeta {
|
pub struct MikanSubscriberAggregationRssLink {
|
||||||
pub mikan_aggregation_id: String,
|
pub mikan_aggregation_id: String,
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn build_mikan_bangumi_rss_url(
|
pub fn build_mikan_bangumi_rss_link(
|
||||||
mikan_base_url: impl IntoUrl,
|
mikan_base_url: impl IntoUrl,
|
||||||
mikan_bangumi_id: &str,
|
mikan_bangumi_id: &str,
|
||||||
mikan_fansub_id: Option<&str>,
|
mikan_fansub_id: Option<&str>,
|
||||||
@ -181,7 +181,7 @@ pub fn build_mikan_bangumi_rss_url(
|
|||||||
Ok(url)
|
Ok(url)
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn build_mikan_subscriber_aggregation_rss_url(
|
pub fn build_mikan_subscriber_aggregation_rss_link(
|
||||||
mikan_base_url: &str,
|
mikan_base_url: &str,
|
||||||
mikan_aggregation_id: &str,
|
mikan_aggregation_id: &str,
|
||||||
) -> RecorderResult<Url> {
|
) -> RecorderResult<Url> {
|
||||||
@ -192,11 +192,11 @@ pub fn build_mikan_subscriber_aggregation_rss_url(
|
|||||||
Ok(url)
|
Ok(url)
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn extract_mikan_bangumi_id_from_rss_url(url: &Url) -> Option<MikanBangumiRssUrlMeta> {
|
pub fn extract_mikan_bangumi_id_from_rss_link(url: &Url) -> Option<MikanBangumiRssLink> {
|
||||||
if url.path() == "/RSS/Bangumi" {
|
if url.path() == "/RSS/Bangumi" {
|
||||||
url.query_pairs()
|
url.query_pairs()
|
||||||
.find(|(k, _)| k == "bangumiId")
|
.find(|(k, _)| k == "bangumiId")
|
||||||
.map(|(_, v)| MikanBangumiRssUrlMeta {
|
.map(|(_, v)| MikanBangumiRssLink {
|
||||||
mikan_bangumi_id: v.to_string(),
|
mikan_bangumi_id: v.to_string(),
|
||||||
mikan_fansub_id: url
|
mikan_fansub_id: url
|
||||||
.query_pairs()
|
.query_pairs()
|
||||||
@ -210,10 +210,10 @@ pub fn extract_mikan_bangumi_id_from_rss_url(url: &Url) -> Option<MikanBangumiRs
|
|||||||
|
|
||||||
pub fn extract_mikan_subscriber_aggregation_id_from_rss_link(
|
pub fn extract_mikan_subscriber_aggregation_id_from_rss_link(
|
||||||
url: &Url,
|
url: &Url,
|
||||||
) -> Option<MikanSubscriberAggregationRssUrlMeta> {
|
) -> Option<MikanSubscriberAggregationRssLink> {
|
||||||
if url.path() == "/RSS/MyBangumi" {
|
if url.path() == "/RSS/MyBangumi" {
|
||||||
url.query_pairs().find(|(k, _)| k == "token").map(|(_, v)| {
|
url.query_pairs().find(|(k, _)| k == "token").map(|(_, v)| {
|
||||||
MikanSubscriberAggregationRssUrlMeta {
|
MikanSubscriberAggregationRssLink {
|
||||||
mikan_aggregation_id: v.to_string(),
|
mikan_aggregation_id: v.to_string(),
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
@ -233,10 +233,10 @@ pub async fn extract_mikan_rss_channel_from_rss_link(
|
|||||||
|
|
||||||
let channel_link = Url::parse(channel.link())?;
|
let channel_link = Url::parse(channel.link())?;
|
||||||
|
|
||||||
if let Some(MikanBangumiRssUrlMeta {
|
if let Some(MikanBangumiRssLink {
|
||||||
mikan_bangumi_id,
|
mikan_bangumi_id,
|
||||||
mikan_fansub_id,
|
mikan_fansub_id,
|
||||||
}) = extract_mikan_bangumi_id_from_rss_url(&channel_link)
|
}) = extract_mikan_bangumi_id_from_rss_link(&channel_link)
|
||||||
{
|
{
|
||||||
tracing::trace!(
|
tracing::trace!(
|
||||||
mikan_bangumi_id,
|
mikan_bangumi_id,
|
||||||
@ -290,7 +290,7 @@ pub async fn extract_mikan_rss_channel_from_rss_link(
|
|||||||
},
|
},
|
||||||
))
|
))
|
||||||
}
|
}
|
||||||
} else if let Some(MikanSubscriberAggregationRssUrlMeta {
|
} else if let Some(MikanSubscriberAggregationRssLink {
|
||||||
mikan_aggregation_id,
|
mikan_aggregation_id,
|
||||||
..
|
..
|
||||||
}) = extract_mikan_subscriber_aggregation_id_from_rss_link(&channel_link)
|
}) = extract_mikan_subscriber_aggregation_id_from_rss_link(&channel_link)
|
||||||
|
|||||||
@ -1,19 +1,22 @@
|
|||||||
use std::{borrow::Cow, fmt};
|
use std::{borrow::Cow, sync::Arc};
|
||||||
|
|
||||||
|
use async_stream::try_stream;
|
||||||
use bytes::Bytes;
|
use bytes::Bytes;
|
||||||
use fetch::{html::fetch_html, image::fetch_image};
|
use fetch::{html::fetch_html, image::fetch_image};
|
||||||
use html_escape::decode_html_entities;
|
use futures::Stream;
|
||||||
|
use itertools::Itertools;
|
||||||
use scraper::{Html, Selector};
|
use scraper::{Html, Selector};
|
||||||
use serde::{Deserialize, Serialize};
|
use serde::{Deserialize, Serialize};
|
||||||
use tracing::instrument;
|
use tracing::instrument;
|
||||||
use url::Url;
|
use url::Url;
|
||||||
|
|
||||||
use super::{
|
use super::{
|
||||||
MIKAN_BUCKET_KEY, MikanBangumiRssUrlMeta, MikanClient, extract_mikan_bangumi_id_from_rss_url,
|
MIKAN_BUCKET_KEY, MikanAuthSecrecy, MikanBangumiRssLink, MikanClient,
|
||||||
|
extract_mikan_bangumi_id_from_rss_link,
|
||||||
};
|
};
|
||||||
use crate::{
|
use crate::{
|
||||||
app::AppContextTrait,
|
app::AppContextTrait,
|
||||||
errors::app_error::{RecorderError, RecorderResult},
|
errors::app_error::{RecorderResult, RecorderError},
|
||||||
extract::{
|
extract::{
|
||||||
html::{extract_background_image_src_from_style_attr, extract_inner_text_from_element_ref},
|
html::{extract_background_image_src_from_style_attr, extract_inner_text_from_element_ref},
|
||||||
media::extract_image_src_from_str,
|
media::extract_image_src_from_str,
|
||||||
@ -21,29 +24,6 @@ use crate::{
|
|||||||
storage::StorageContentCategory,
|
storage::StorageContentCategory,
|
||||||
};
|
};
|
||||||
|
|
||||||
#[derive(Clone, Debug, Copy, Serialize, Deserialize)]
|
|
||||||
pub enum MikanSeasonStr {
|
|
||||||
#[serde(rename = "春")]
|
|
||||||
Spring,
|
|
||||||
#[serde(rename = "夏")]
|
|
||||||
Summer,
|
|
||||||
#[serde(rename = "秋")]
|
|
||||||
Autumn,
|
|
||||||
#[serde(rename = "冬")]
|
|
||||||
Winter,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl fmt::Display for MikanSeasonStr {
|
|
||||||
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
|
||||||
match self {
|
|
||||||
Self::Spring => write!(f, "春"),
|
|
||||||
Self::Summer => write!(f, "夏"),
|
|
||||||
Self::Autumn => write!(f, "秋"),
|
|
||||||
Self::Winter => write!(f, "冬"),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Clone, Debug, PartialEq)]
|
#[derive(Clone, Debug, PartialEq)]
|
||||||
pub struct MikanEpisodeMeta {
|
pub struct MikanEpisodeMeta {
|
||||||
pub homepage: Url,
|
pub homepage: Url,
|
||||||
@ -56,14 +36,6 @@ pub struct MikanEpisodeMeta {
|
|||||||
pub mikan_episode_id: String,
|
pub mikan_episode_id: String,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
|
|
||||||
pub struct MikanBangumiIndexMeta {
|
|
||||||
pub homepage: Url,
|
|
||||||
pub origin_poster_src: Option<Url>,
|
|
||||||
pub bangumi_title: String,
|
|
||||||
pub mikan_bangumi_id: String,
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
|
#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
|
||||||
pub struct MikanBangumiMeta {
|
pub struct MikanBangumiMeta {
|
||||||
pub homepage: Url,
|
pub homepage: Url,
|
||||||
@ -81,19 +53,6 @@ pub struct MikanBangumiPosterMeta {
|
|||||||
pub poster_src: Option<String>,
|
pub poster_src: Option<String>,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl From<MikanBangumiIndexMeta> for MikanBangumiMeta {
|
|
||||||
fn from(index_meta: MikanBangumiIndexMeta) -> Self {
|
|
||||||
MikanBangumiMeta {
|
|
||||||
homepage: index_meta.homepage,
|
|
||||||
origin_poster_src: index_meta.origin_poster_src,
|
|
||||||
bangumi_title: index_meta.bangumi_title,
|
|
||||||
mikan_bangumi_id: index_meta.mikan_bangumi_id,
|
|
||||||
mikan_fansub_id: None,
|
|
||||||
fansub: None,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Clone, Debug, PartialEq)]
|
#[derive(Clone, Debug, PartialEq)]
|
||||||
pub struct MikanEpisodeHomepage {
|
pub struct MikanEpisodeHomepage {
|
||||||
pub mikan_episode_id: String,
|
pub mikan_episode_id: String,
|
||||||
@ -105,7 +64,7 @@ pub struct MikanBangumiHomepage {
|
|||||||
pub mikan_fansub_id: Option<String>,
|
pub mikan_fansub_id: Option<String>,
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn build_mikan_bangumi_homepage_url(
|
pub fn build_mikan_bangumi_homepage(
|
||||||
mikan_base_url: Url,
|
mikan_base_url: Url,
|
||||||
mikan_bangumi_id: &str,
|
mikan_bangumi_id: &str,
|
||||||
mikan_fansub_id: Option<&str>,
|
mikan_fansub_id: Option<&str>,
|
||||||
@ -116,29 +75,13 @@ pub fn build_mikan_bangumi_homepage_url(
|
|||||||
url
|
url
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn build_mikan_season_flow_url(
|
pub fn build_mikan_episode_homepage(mikan_base_url: Url, mikan_episode_id: &str) -> Url {
|
||||||
mikan_base_url: Url,
|
|
||||||
year: i32,
|
|
||||||
season_str: MikanSeasonStr,
|
|
||||||
) -> Url {
|
|
||||||
let mut url = mikan_base_url;
|
|
||||||
url.set_path("/Home/BangumiCoverFlow");
|
|
||||||
url.query_pairs_mut()
|
|
||||||
.append_pair("year", &year.to_string())
|
|
||||||
.append_pair("seasonStr", &season_str.to_string());
|
|
||||||
url
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn build_mikan_episode_homepage_url(mikan_base_url: Url, mikan_episode_id: &str) -> Url {
|
|
||||||
let mut url = mikan_base_url;
|
let mut url = mikan_base_url;
|
||||||
url.set_path(&format!("/Home/Episode/{mikan_episode_id}"));
|
url.set_path(&format!("/Home/Episode/{mikan_episode_id}"));
|
||||||
url
|
url
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn build_mikan_bangumi_expand_subscribed_fragment_url(
|
pub fn build_mikan_bangumi_expand_info_url(mikan_base_url: Url, mikan_bangumi_id: &str) -> Url {
|
||||||
mikan_base_url: Url,
|
|
||||||
mikan_bangumi_id: &str,
|
|
||||||
) -> Url {
|
|
||||||
let mut url = mikan_base_url;
|
let mut url = mikan_base_url;
|
||||||
url.set_path("/ExpandBangumi");
|
url.set_path("/ExpandBangumi");
|
||||||
url.query_pairs_mut()
|
url.query_pairs_mut()
|
||||||
@ -147,7 +90,7 @@ pub fn build_mikan_bangumi_expand_subscribed_fragment_url(
|
|||||||
url
|
url
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn extract_mikan_bangumi_id_from_homepage_url(url: &Url) -> Option<MikanBangumiHomepage> {
|
pub fn extract_mikan_bangumi_id_from_homepage(url: &Url) -> Option<MikanBangumiHomepage> {
|
||||||
if url.path().starts_with("/Home/Bangumi/") {
|
if url.path().starts_with("/Home/Bangumi/") {
|
||||||
let mikan_bangumi_id = url.path().replace("/Home/Bangumi/", "");
|
let mikan_bangumi_id = url.path().replace("/Home/Bangumi/", "");
|
||||||
|
|
||||||
@ -160,7 +103,7 @@ pub fn extract_mikan_bangumi_id_from_homepage_url(url: &Url) -> Option<MikanBang
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn extract_mikan_episode_id_from_homepage_url(url: &Url) -> Option<MikanEpisodeHomepage> {
|
pub fn extract_mikan_episode_id_from_homepage(url: &Url) -> Option<MikanEpisodeHomepage> {
|
||||||
if url.path().starts_with("/Home/Episode/") {
|
if url.path().starts_with("/Home/Episode/") {
|
||||||
let mikan_episode_id = url.path().replace("/Home/Episode/", "");
|
let mikan_episode_id = url.path().replace("/Home/Episode/", "");
|
||||||
Some(MikanEpisodeHomepage { mikan_episode_id })
|
Some(MikanEpisodeHomepage { mikan_episode_id })
|
||||||
@ -248,7 +191,7 @@ pub async fn extract_mikan_episode_meta_from_episode_homepage(
|
|||||||
tracing::warn!(error = %error);
|
tracing::warn!(error = %error);
|
||||||
})?;
|
})?;
|
||||||
|
|
||||||
let MikanBangumiRssUrlMeta {
|
let MikanBangumiRssLink {
|
||||||
mikan_bangumi_id,
|
mikan_bangumi_id,
|
||||||
mikan_fansub_id,
|
mikan_fansub_id,
|
||||||
..
|
..
|
||||||
@ -257,7 +200,7 @@ pub async fn extract_mikan_episode_meta_from_episode_homepage(
|
|||||||
.next()
|
.next()
|
||||||
.and_then(|el| el.value().attr("href"))
|
.and_then(|el| el.value().attr("href"))
|
||||||
.and_then(|s| mikan_episode_homepage_url.join(s).ok())
|
.and_then(|s| mikan_episode_homepage_url.join(s).ok())
|
||||||
.and_then(|rss_link_url| extract_mikan_bangumi_id_from_rss_url(&rss_link_url))
|
.and_then(|rss_link_url| extract_mikan_bangumi_id_from_rss_link(&rss_link_url))
|
||||||
.ok_or_else(|| {
|
.ok_or_else(|| {
|
||||||
RecorderError::from_mikan_meta_missing_field(Cow::Borrowed("mikan_bangumi_id"))
|
RecorderError::from_mikan_meta_missing_field(Cow::Borrowed("mikan_bangumi_id"))
|
||||||
})
|
})
|
||||||
@ -280,7 +223,7 @@ pub async fn extract_mikan_episode_meta_from_episode_homepage(
|
|||||||
|
|
||||||
let MikanEpisodeHomepage {
|
let MikanEpisodeHomepage {
|
||||||
mikan_episode_id, ..
|
mikan_episode_id, ..
|
||||||
} = extract_mikan_episode_id_from_homepage_url(&mikan_episode_homepage_url)
|
} = extract_mikan_episode_id_from_homepage(&mikan_episode_homepage_url)
|
||||||
.ok_or_else(|| {
|
.ok_or_else(|| {
|
||||||
RecorderError::from_mikan_meta_missing_field(Cow::Borrowed("mikan_episode_id"))
|
RecorderError::from_mikan_meta_missing_field(Cow::Borrowed("mikan_episode_id"))
|
||||||
})
|
})
|
||||||
@ -360,9 +303,9 @@ pub async fn extract_mikan_bangumi_meta_from_bangumi_homepage(
|
|||||||
.next()
|
.next()
|
||||||
.and_then(|el| el.value().attr("href"))
|
.and_then(|el| el.value().attr("href"))
|
||||||
.and_then(|s| mikan_bangumi_homepage_url.join(s).ok())
|
.and_then(|s| mikan_bangumi_homepage_url.join(s).ok())
|
||||||
.and_then(|rss_link_url| extract_mikan_bangumi_id_from_rss_url(&rss_link_url))
|
.and_then(|rss_link_url| extract_mikan_bangumi_id_from_rss_link(&rss_link_url))
|
||||||
.map(
|
.map(
|
||||||
|MikanBangumiRssUrlMeta {
|
|MikanBangumiRssLink {
|
||||||
mikan_bangumi_id, ..
|
mikan_bangumi_id, ..
|
||||||
}| mikan_bangumi_id,
|
}| mikan_bangumi_id,
|
||||||
)
|
)
|
||||||
@ -382,7 +325,7 @@ pub async fn extract_mikan_bangumi_meta_from_bangumi_homepage(
|
|||||||
})
|
})
|
||||||
});
|
});
|
||||||
|
|
||||||
let (mikan_fansub_id, fansub) = mikan_bangumi_homepage_url
|
let (mikan_fansub_id, fansub_name) = mikan_bangumi_homepage_url
|
||||||
.fragment()
|
.fragment()
|
||||||
.and_then(|id| {
|
.and_then(|id| {
|
||||||
html.select(
|
html.select(
|
||||||
@ -398,7 +341,7 @@ pub async fn extract_mikan_bangumi_meta_from_bangumi_homepage(
|
|||||||
bangumi_title,
|
bangumi_title,
|
||||||
mikan_bangumi_id,
|
mikan_bangumi_id,
|
||||||
origin_poster_src = origin_poster_src.as_ref().map(|url| url.as_str()),
|
origin_poster_src = origin_poster_src.as_ref().map(|url| url.as_str()),
|
||||||
fansub,
|
fansub_name,
|
||||||
mikan_fansub_id,
|
mikan_fansub_id,
|
||||||
"mikan bangumi meta extracted"
|
"mikan bangumi meta extracted"
|
||||||
);
|
);
|
||||||
@ -408,141 +351,154 @@ pub async fn extract_mikan_bangumi_meta_from_bangumi_homepage(
|
|||||||
bangumi_title,
|
bangumi_title,
|
||||||
origin_poster_src,
|
origin_poster_src,
|
||||||
mikan_bangumi_id,
|
mikan_bangumi_id,
|
||||||
fansub,
|
fansub: fansub_name,
|
||||||
mikan_fansub_id,
|
mikan_fansub_id,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
#[instrument]
|
#[instrument(skip_all, fields(my_bangumi_page_url, auth_secrecy = ?auth_secrecy, history = history.len()))]
|
||||||
pub fn extract_mikan_bangumi_indices_meta_from_season_flow_fragment(
|
pub fn extract_mikan_bangumis_meta_from_my_bangumi_page(
|
||||||
season_flow_fragment: &str,
|
context: Arc<dyn AppContextTrait>,
|
||||||
mikan_base_url: Url,
|
my_bangumi_page_url: Url,
|
||||||
) -> Vec<MikanBangumiIndexMeta> {
|
auth_secrecy: Option<MikanAuthSecrecy>,
|
||||||
let html = Html::parse_fragment(season_flow_fragment);
|
history: &[Arc<RecorderResult<MikanBangumiMeta>>],
|
||||||
|
) -> impl Stream<Item = RecorderResult<MikanBangumiMeta>> {
|
||||||
|
try_stream! {
|
||||||
|
let http_client = &context.mikan().fork_with_auth(auth_secrecy.clone())?;
|
||||||
|
|
||||||
let bangumi_empty_selector = &Selector::parse(".no-subscribe-bangumi").unwrap();
|
let mikan_base_url = Url::parse(&my_bangumi_page_url.origin().unicode_serialization())?;
|
||||||
|
|
||||||
if html.select(bangumi_empty_selector).next().is_some() {
|
let content = fetch_html(http_client, my_bangumi_page_url.clone()).await?;
|
||||||
return vec![];
|
|
||||||
}
|
|
||||||
|
|
||||||
let bangumi_item_selector = &Selector::parse(".mine.an-box ul.an-ul>li").unwrap();
|
let fansub_container_selector =
|
||||||
let bangumi_poster_span_selector = &Selector::parse("span[data-src][data-bangumiid]").unwrap();
|
&Selector::parse(".js-expand_bangumi-subgroup.js-subscribed").unwrap();
|
||||||
let bangumi_title_a_selector = &Selector::parse(".an-info-group a.an-text[title]").unwrap();
|
let fansub_title_selector = &Selector::parse(".tag-res-name[title]").unwrap();
|
||||||
|
let fansub_id_selector =
|
||||||
|
&Selector::parse(".active[data-subtitlegroupid][data-bangumiid]").unwrap();
|
||||||
|
|
||||||
let mut items = vec![];
|
let bangumi_items = {
|
||||||
for bangumi_item in html.select(bangumi_item_selector) {
|
let html = Html::parse_document(&content);
|
||||||
let bangumi_poster_span = bangumi_item.select(bangumi_poster_span_selector).next();
|
|
||||||
let bangumi_title_a = bangumi_item.select(bangumi_title_a_selector).next();
|
|
||||||
if let (Some(bangumi_poster_span), Some(bangumi_title_a)) =
|
|
||||||
(bangumi_poster_span, bangumi_title_a)
|
|
||||||
{
|
|
||||||
let origin_poster_src = bangumi_poster_span
|
|
||||||
.attr("data-src")
|
|
||||||
.and_then(|data_src| extract_image_src_from_str(data_src, &mikan_base_url));
|
|
||||||
let bangumi_title = bangumi_title_a
|
|
||||||
.attr("title")
|
|
||||||
.map(|title| decode_html_entities(&title).trim().to_string());
|
|
||||||
let mikan_bangumi_id = bangumi_poster_span
|
|
||||||
.attr("data-bangumiid")
|
|
||||||
.map(|id| id.to_string());
|
|
||||||
|
|
||||||
if let (Some(bangumi_title), Some(mikan_bangumi_id)) = (bangumi_title, mikan_bangumi_id)
|
let bangumi_container_selector = &Selector::parse(".sk-bangumi .an-ul>li").unwrap();
|
||||||
{
|
let bangumi_info_selector = &Selector::parse(".an-info a.an-text").unwrap();
|
||||||
let homepage = build_mikan_bangumi_homepage_url(
|
let bangumi_poster_selector =
|
||||||
mikan_base_url.clone(),
|
&Selector::parse("span[data-src][data-bangumiid], span[data-bangumiid][style]")
|
||||||
&mikan_bangumi_id,
|
.unwrap();
|
||||||
None,
|
html.select(bangumi_container_selector)
|
||||||
);
|
.filter_map(|bangumi_elem| {
|
||||||
if let Some(origin_poster_src) = origin_poster_src.as_ref() {
|
let title_and_href_elem =
|
||||||
tracing::trace!(
|
bangumi_elem.select(bangumi_info_selector).next();
|
||||||
origin_poster_src = origin_poster_src.as_str(),
|
let poster_elem = bangumi_elem.select(bangumi_poster_selector).next();
|
||||||
bangumi_title,
|
if let (Some(bangumi_home_page_url), Some(bangumi_title)) = (
|
||||||
mikan_bangumi_id,
|
title_and_href_elem.and_then(|elem| elem.attr("href")),
|
||||||
"bangumi index meta extracted"
|
title_and_href_elem.and_then(|elem| elem.attr("title")),
|
||||||
);
|
|
||||||
} else {
|
|
||||||
tracing::warn!(
|
|
||||||
bangumi_title,
|
|
||||||
mikan_bangumi_id,
|
|
||||||
"bangumi index meta extracted, but failed to extract poster_src"
|
|
||||||
);
|
|
||||||
}
|
|
||||||
items.push(MikanBangumiIndexMeta {
|
|
||||||
homepage,
|
|
||||||
origin_poster_src,
|
|
||||||
bangumi_title,
|
|
||||||
mikan_bangumi_id,
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
items
|
|
||||||
}
|
|
||||||
|
|
||||||
#[instrument(skip_all, fields(mikan_bangumi_index = mikan_bangumi_index.mikan_bangumi_id.as_str()))]
|
|
||||||
pub fn extract_mikan_bangumi_meta_from_expand_subscribed_fragment(
|
|
||||||
mikan_bangumi_index: MikanBangumiIndexMeta,
|
|
||||||
expand_subscribed_fragment: &str,
|
|
||||||
mikan_base_url: Url,
|
|
||||||
) -> Option<MikanBangumiMeta> {
|
|
||||||
let html = Html::parse_fragment(expand_subscribed_fragment);
|
|
||||||
let fansub_container_selector =
|
|
||||||
&Selector::parse(".js-expand_bangumi-subgroup.js-subscribed").unwrap();
|
|
||||||
let fansub_title_selector = &Selector::parse(".tag-res-name[title]").unwrap();
|
|
||||||
let fansub_id_selector =
|
|
||||||
&Selector::parse(".active[data-subtitlegroupid][data-bangumiid]").unwrap();
|
|
||||||
|
|
||||||
if let Some((fansub_name, mikan_fansub_id)) = {
|
|
||||||
html.select(fansub_container_selector)
|
|
||||||
.next()
|
|
||||||
.and_then(|fansub_info| {
|
|
||||||
if let (Some(fansub_name), Some(mikan_fansub_id)) = (
|
|
||||||
fansub_info
|
|
||||||
.select(fansub_title_selector)
|
|
||||||
.next()
|
|
||||||
.and_then(|ele| ele.attr("title"))
|
|
||||||
.map(String::from),
|
|
||||||
fansub_info
|
|
||||||
.select(fansub_id_selector)
|
|
||||||
.next()
|
|
||||||
.and_then(|ele| ele.attr("data-subtitlegroupid"))
|
|
||||||
.map(String::from),
|
|
||||||
) {
|
) {
|
||||||
Some((fansub_name, mikan_fansub_id))
|
let origin_poster_src = poster_elem.and_then(|ele| {
|
||||||
|
ele.attr("data-src")
|
||||||
|
.and_then(|data_src| {
|
||||||
|
extract_image_src_from_str(data_src, &mikan_base_url)
|
||||||
|
})
|
||||||
|
.or_else(|| {
|
||||||
|
ele.attr("style").and_then(|style| {
|
||||||
|
extract_background_image_src_from_style_attr(
|
||||||
|
style,
|
||||||
|
&mikan_base_url,
|
||||||
|
)
|
||||||
|
})
|
||||||
|
})
|
||||||
|
});
|
||||||
|
let bangumi_title = bangumi_title.to_string();
|
||||||
|
let bangumi_home_page_url =
|
||||||
|
my_bangumi_page_url.join(bangumi_home_page_url).ok()?;
|
||||||
|
let MikanBangumiHomepage {
|
||||||
|
mikan_bangumi_id, ..
|
||||||
|
} = extract_mikan_bangumi_id_from_homepage(&bangumi_home_page_url)?;
|
||||||
|
if let Some(origin_poster_src) = origin_poster_src.as_ref() {
|
||||||
|
tracing::trace!(
|
||||||
|
origin_poster_src = origin_poster_src.as_str(),
|
||||||
|
bangumi_title,
|
||||||
|
mikan_bangumi_id,
|
||||||
|
"bangumi info extracted"
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
tracing::warn!(
|
||||||
|
bangumi_title,
|
||||||
|
mikan_bangumi_id,
|
||||||
|
"bangumi info extracted, but failed to extract poster_src"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
let bangumi_expand_info_url = build_mikan_bangumi_expand_info_url(
|
||||||
|
mikan_base_url.clone(),
|
||||||
|
&mikan_bangumi_id,
|
||||||
|
);
|
||||||
|
Some((
|
||||||
|
bangumi_title,
|
||||||
|
mikan_bangumi_id,
|
||||||
|
bangumi_expand_info_url,
|
||||||
|
origin_poster_src,
|
||||||
|
))
|
||||||
} else {
|
} else {
|
||||||
None
|
None
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
} {
|
.collect_vec()
|
||||||
tracing::trace!(fansub_name, mikan_fansub_id, "subscribed fansub extracted");
|
};
|
||||||
let mikan_bangumi_id = mikan_bangumi_index.mikan_bangumi_id;
|
|
||||||
let bangumi_title = mikan_bangumi_index.bangumi_title;
|
|
||||||
let origin_poster_src = mikan_bangumi_index.origin_poster_src;
|
|
||||||
|
|
||||||
Some(MikanBangumiMeta {
|
for (idx, (bangumi_title, mikan_bangumi_id, bangumi_expand_info_url, origin_poster_src)) in
|
||||||
homepage: build_mikan_bangumi_homepage_url(
|
bangumi_items.iter().enumerate()
|
||||||
mikan_base_url.clone(),
|
{
|
||||||
&mikan_bangumi_id,
|
|
||||||
Some(&mikan_fansub_id),
|
if history.get(idx).is_some() {
|
||||||
),
|
continue;
|
||||||
bangumi_title: bangumi_title.to_string(),
|
} else if let Some((fansub_name, mikan_fansub_id)) = {
|
||||||
mikan_bangumi_id: mikan_bangumi_id.to_string(),
|
let bangumi_expand_info_content =
|
||||||
mikan_fansub_id: Some(mikan_fansub_id),
|
fetch_html(http_client, bangumi_expand_info_url.clone()).await?;
|
||||||
fansub: Some(fansub_name),
|
let bangumi_expand_info_fragment =
|
||||||
origin_poster_src: origin_poster_src.clone(),
|
Html::parse_fragment(&bangumi_expand_info_content);
|
||||||
})
|
bangumi_expand_info_fragment
|
||||||
} else {
|
.select(fansub_container_selector)
|
||||||
tracing::trace!("subscribed fansub not found");
|
.next()
|
||||||
None
|
.and_then(|fansub_info| {
|
||||||
|
if let (Some(fansub_name), Some(mikan_fansub_id)) = (
|
||||||
|
fansub_info
|
||||||
|
.select(fansub_title_selector)
|
||||||
|
.next()
|
||||||
|
.and_then(|ele| ele.attr("title"))
|
||||||
|
.map(String::from),
|
||||||
|
fansub_info
|
||||||
|
.select(fansub_id_selector)
|
||||||
|
.next()
|
||||||
|
.and_then(|ele| ele.attr("data-subtitlegroupid"))
|
||||||
|
.map(String::from),
|
||||||
|
) {
|
||||||
|
Some((fansub_name, mikan_fansub_id))
|
||||||
|
} else {
|
||||||
|
None
|
||||||
|
}
|
||||||
|
})
|
||||||
|
} {
|
||||||
|
tracing::trace!(fansub_name, mikan_fansub_id, "subscribed fansub extracted");
|
||||||
|
let item = MikanBangumiMeta {
|
||||||
|
homepage: build_mikan_bangumi_homepage(
|
||||||
|
mikan_base_url.clone(),
|
||||||
|
mikan_bangumi_id,
|
||||||
|
Some(&mikan_fansub_id),
|
||||||
|
),
|
||||||
|
bangumi_title: bangumi_title.to_string(),
|
||||||
|
mikan_bangumi_id: mikan_bangumi_id.to_string(),
|
||||||
|
mikan_fansub_id: Some(mikan_fansub_id),
|
||||||
|
fansub: Some(fansub_name),
|
||||||
|
origin_poster_src: origin_poster_src.clone(),
|
||||||
|
};
|
||||||
|
yield item;
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
mod test {
|
mod test {
|
||||||
#![allow(unused_variables)]
|
#![allow(unused_variables)]
|
||||||
use std::{fs, sync::Arc};
|
|
||||||
|
|
||||||
use futures::{TryStreamExt, pin_mut};
|
use futures::{TryStreamExt, pin_mut};
|
||||||
use http::header;
|
use http::header;
|
||||||
use rstest::{fixture, rstest};
|
use rstest::{fixture, rstest};
|
||||||
@ -551,12 +507,9 @@ mod test {
|
|||||||
use zune_image::{codecs::ImageFormat, image::Image};
|
use zune_image::{codecs::ImageFormat, image::Image};
|
||||||
|
|
||||||
use super::*;
|
use super::*;
|
||||||
use crate::{
|
use crate::test_utils::{
|
||||||
extract::mikan::MikanCredentialForm,
|
app::UnitTestAppContext, mikan::build_testing_mikan_client,
|
||||||
test_utils::{
|
tracing::try_init_testing_tracing,
|
||||||
app::UnitTestAppContext, mikan::build_testing_mikan_client,
|
|
||||||
tracing::try_init_testing_tracing,
|
|
||||||
},
|
|
||||||
};
|
};
|
||||||
|
|
||||||
#[fixture]
|
#[fixture]
|
||||||
@ -637,9 +590,7 @@ mod test {
|
|||||||
|
|
||||||
#[rstest]
|
#[rstest]
|
||||||
#[tokio::test]
|
#[tokio::test]
|
||||||
async fn test_extract_mikan_bangumi_meta_from_bangumi_homepage(
|
async fn test_extract_mikan_bangumi_meta_from_bangumi_homepage(before_each: ()) -> RecorderResult<()> {
|
||||||
before_each: (),
|
|
||||||
) -> RecorderResult<()> {
|
|
||||||
let mut mikan_server = mockito::Server::new_async().await;
|
let mut mikan_server = mockito::Server::new_async().await;
|
||||||
let mikan_base_url = Url::parse(&mikan_server.url())?;
|
let mikan_base_url = Url::parse(&mikan_server.url())?;
|
||||||
let mikan_client = build_testing_mikan_client(mikan_base_url.clone()).await?;
|
let mikan_client = build_testing_mikan_client(mikan_base_url.clone()).await?;
|
||||||
@ -675,217 +626,95 @@ mod test {
|
|||||||
}
|
}
|
||||||
|
|
||||||
#[rstest]
|
#[rstest]
|
||||||
#[test]
|
#[tokio::test]
|
||||||
fn test_extract_mikan_bangumi_indices_meta_from_season_flow_fragment(
|
async fn test_extract_mikan_bangumis_meta_from_my_bangumi_page(before_each: ()) -> RecorderResult<()> {
|
||||||
before_each: (),
|
let mut mikan_server = mockito::Server::new_async().await;
|
||||||
) -> RecorderResult<()> {
|
|
||||||
let fragment =
|
|
||||||
fs::read_to_string("tests/resources/mikan/BangumiCoverFlow-2025-spring.html")?;
|
|
||||||
|
|
||||||
let indices = extract_mikan_bangumi_indices_meta_from_season_flow_fragment(
|
let mikan_base_url = Url::parse(&mikan_server.url())?;
|
||||||
&fragment,
|
|
||||||
Url::parse("https://mikanani.me/")?,
|
let my_bangumi_page_url = mikan_base_url.join("/Home/MyBangumi")?;
|
||||||
|
|
||||||
|
let context = Arc::new(
|
||||||
|
UnitTestAppContext::builder()
|
||||||
|
.mikan(build_testing_mikan_client(mikan_base_url.clone()).await?)
|
||||||
|
.build(),
|
||||||
);
|
);
|
||||||
|
|
||||||
tracing::info!("indices: {:#?}", &indices[0]);
|
{
|
||||||
|
let my_bangumi_without_cookie_mock = mikan_server
|
||||||
|
.mock("GET", my_bangumi_page_url.path())
|
||||||
|
.match_header(header::COOKIE, mockito::Matcher::Missing)
|
||||||
|
.with_body_from_file("tests/resources/mikan/MyBangumi-noauth.htm")
|
||||||
|
.create_async()
|
||||||
|
.await;
|
||||||
|
|
||||||
assert_eq!(indices.len(), 49);
|
let bangumi_metas = extract_mikan_bangumis_meta_from_my_bangumi_page(
|
||||||
let first = &indices[0];
|
context.clone(),
|
||||||
assert_eq!(first.bangumi_title, "吉伊卡哇");
|
my_bangumi_page_url.clone(),
|
||||||
assert_eq!(first.mikan_bangumi_id, "3288");
|
None,
|
||||||
assert_eq!(
|
&[],
|
||||||
first.homepage.to_string(),
|
);
|
||||||
String::from("https://mikanani.me/Home/Bangumi/3288")
|
|
||||||
);
|
pin_mut!(bangumi_metas);
|
||||||
assert_eq!(
|
|
||||||
first
|
let bangumi_metas = bangumi_metas.try_collect::<Vec<_>>().await?;
|
||||||
.origin_poster_src
|
|
||||||
.as_ref()
|
assert!(bangumi_metas.is_empty());
|
||||||
.map(|s| s.to_string())
|
|
||||||
.unwrap_or_default(),
|
assert!(my_bangumi_without_cookie_mock.matched_async().await);
|
||||||
String::from("https://mikanani.me/images/Bangumi/202204/d8ef46c0.jpg")
|
}
|
||||||
);
|
{
|
||||||
|
let my_bangumi_with_cookie_mock = mikan_server
|
||||||
|
.mock("GET", my_bangumi_page_url.path())
|
||||||
|
.match_header(
|
||||||
|
header::COOKIE,
|
||||||
|
mockito::Matcher::AllOf(vec![
|
||||||
|
mockito::Matcher::Regex(String::from(".*\\.AspNetCore\\.Antiforgery.*")),
|
||||||
|
mockito::Matcher::Regex(String::from(
|
||||||
|
".*\\.AspNetCore\\.Identity\\.Application.*",
|
||||||
|
)),
|
||||||
|
]),
|
||||||
|
)
|
||||||
|
.with_body_from_file("tests/resources/mikan/MyBangumi.htm")
|
||||||
|
.create_async()
|
||||||
|
.await;
|
||||||
|
|
||||||
|
let expand_bangumi_mock = mikan_server
|
||||||
|
.mock("GET", "/ExpandBangumi")
|
||||||
|
.match_query(mockito::Matcher::Any)
|
||||||
|
.with_body_from_file("tests/resources/mikan/ExpandBangumi.htm")
|
||||||
|
.create_async()
|
||||||
|
.await;
|
||||||
|
|
||||||
|
let auth_secrecy = Some(MikanAuthSecrecy {
|
||||||
|
cookie: String::from(
|
||||||
|
"mikan-announcement=1; .AspNetCore.Antiforgery.abc=abc; \
|
||||||
|
.AspNetCore.Identity.Application=abc; ",
|
||||||
|
),
|
||||||
|
user_agent: Some(String::from(
|
||||||
|
"Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like \
|
||||||
|
Gecko) Chrome/133.0.0.0 Safari/537.36 Edg/133.0.0.0",
|
||||||
|
)),
|
||||||
|
});
|
||||||
|
|
||||||
|
let bangumi_metas = extract_mikan_bangumis_meta_from_my_bangumi_page(
|
||||||
|
context.clone(),
|
||||||
|
my_bangumi_page_url,
|
||||||
|
auth_secrecy,
|
||||||
|
&[],
|
||||||
|
);
|
||||||
|
pin_mut!(bangumi_metas);
|
||||||
|
let bangumi_metas = bangumi_metas.try_collect::<Vec<_>>().await?;
|
||||||
|
|
||||||
|
assert!(!bangumi_metas.is_empty());
|
||||||
|
|
||||||
|
assert!(bangumi_metas[0].origin_poster_src.is_some());
|
||||||
|
|
||||||
|
assert!(my_bangumi_with_cookie_mock.matched_async().await);
|
||||||
|
|
||||||
|
expand_bangumi_mock.expect(bangumi_metas.len());
|
||||||
|
}
|
||||||
|
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
#[rstest]
|
|
||||||
#[test]
|
|
||||||
fn test_extract_mikan_bangumi_indices_meta_from_season_flow_fragment_noauth(
|
|
||||||
before_each: (),
|
|
||||||
) -> RecorderResult<()> {
|
|
||||||
let fragment =
|
|
||||||
fs::read_to_string("tests/resources/mikan/BangumiCoverFlow-2025-spring-noauth.html")?;
|
|
||||||
|
|
||||||
let indices = extract_mikan_bangumi_indices_meta_from_season_flow_fragment(
|
|
||||||
&fragment,
|
|
||||||
Url::parse("https://mikanani.me/")?,
|
|
||||||
);
|
|
||||||
|
|
||||||
assert!(indices.is_empty());
|
|
||||||
|
|
||||||
Ok(())
|
|
||||||
}
|
|
||||||
|
|
||||||
#[rstest]
|
|
||||||
#[test]
|
|
||||||
fn test_extract_mikan_bangumi_meta_from_expand_subscribed_fragment(
|
|
||||||
before_each: (),
|
|
||||||
) -> RecorderResult<()> {
|
|
||||||
let origin_poster_src =
|
|
||||||
Url::parse("https://mikanani.me/images/Bangumi/202504/076c1094.jpg")?;
|
|
||||||
let bangumi_index = MikanBangumiIndexMeta {
|
|
||||||
homepage: Url::parse("https://mikanani.me/Home/Bangumi/3599")?,
|
|
||||||
origin_poster_src: Some(origin_poster_src.clone()),
|
|
||||||
bangumi_title: "夏日口袋".to_string(),
|
|
||||||
mikan_bangumi_id: "3599".to_string(),
|
|
||||||
};
|
|
||||||
|
|
||||||
let fragment = fs::read_to_string("tests/resources/mikan/ExpandBangumi-3599.html")?;
|
|
||||||
|
|
||||||
let bangumi = extract_mikan_bangumi_meta_from_expand_subscribed_fragment(
|
|
||||||
bangumi_index.clone(),
|
|
||||||
&fragment,
|
|
||||||
Url::parse("https://mikanani.me/")?,
|
|
||||||
)
|
|
||||||
.unwrap_or_else(|| {
|
|
||||||
panic!("bangumi should not be None");
|
|
||||||
});
|
|
||||||
|
|
||||||
assert_eq!(
|
|
||||||
bangumi.homepage,
|
|
||||||
Url::parse("https://mikanani.me/Home/Bangumi/3599#370")?
|
|
||||||
);
|
|
||||||
assert_eq!(bangumi.bangumi_title, bangumi_index.bangumi_title);
|
|
||||||
assert_eq!(bangumi.mikan_bangumi_id, bangumi_index.mikan_bangumi_id);
|
|
||||||
assert_eq!(bangumi.origin_poster_src, bangumi_index.origin_poster_src);
|
|
||||||
assert_eq!(bangumi.mikan_fansub_id, Some(String::from("370")));
|
|
||||||
assert_eq!(bangumi.fansub, Some(String::from("LoliHouse")));
|
|
||||||
|
|
||||||
Ok(())
|
|
||||||
}
|
|
||||||
|
|
||||||
#[rstest]
|
|
||||||
#[test]
|
|
||||||
fn test_extract_mikan_bangumi_meta_from_expand_subscribed_fragment_noauth(
|
|
||||||
before_each: (),
|
|
||||||
) -> RecorderResult<()> {
|
|
||||||
let origin_poster_src =
|
|
||||||
Url::parse("https://mikanani.me/images/Bangumi/202504/076c1094.jpg")?;
|
|
||||||
let bangumi_index = MikanBangumiIndexMeta {
|
|
||||||
homepage: Url::parse("https://mikanani.me/Home/Bangumi/3599")?,
|
|
||||||
origin_poster_src: Some(origin_poster_src.clone()),
|
|
||||||
bangumi_title: "夏日口袋".to_string(),
|
|
||||||
mikan_bangumi_id: "3599".to_string(),
|
|
||||||
};
|
|
||||||
|
|
||||||
let fragment = fs::read_to_string("tests/resources/mikan/ExpandBangumi-3599-noauth.html")?;
|
|
||||||
|
|
||||||
let bangumi = extract_mikan_bangumi_meta_from_expand_subscribed_fragment(
|
|
||||||
bangumi_index.clone(),
|
|
||||||
&fragment,
|
|
||||||
Url::parse("https://mikanani.me/")?,
|
|
||||||
);
|
|
||||||
|
|
||||||
assert!(bangumi.is_none());
|
|
||||||
|
|
||||||
Ok(())
|
|
||||||
}
|
|
||||||
|
|
||||||
// #[rstest]
|
|
||||||
// #[tokio::test]
|
|
||||||
// async fn test_extract_mikan_bangumis_meta_from_my_bangumi_page(
|
|
||||||
// before_each: (),
|
|
||||||
// ) -> RecorderResult<()> {
|
|
||||||
// let mut mikan_server = mockito::Server::new_async().await;
|
|
||||||
|
|
||||||
// let mikan_base_url = Url::parse(&mikan_server.url())?;
|
|
||||||
|
|
||||||
// let my_bangumi_page_url = mikan_base_url.join("/Home/MyBangumi")?;
|
|
||||||
|
|
||||||
// let context = Arc::new(
|
|
||||||
// UnitTestAppContext::builder()
|
|
||||||
//
|
|
||||||
// .mikan(build_testing_mikan_client(mikan_base_url.clone()).await?)
|
|
||||||
// .build(),
|
|
||||||
// );
|
|
||||||
|
|
||||||
// {
|
|
||||||
// let my_bangumi_without_cookie_mock = mikan_server
|
|
||||||
// .mock("GET", my_bangumi_page_url.path())
|
|
||||||
// .match_header(header::COOKIE, mockito::Matcher::Missing)
|
|
||||||
//
|
|
||||||
// .with_body_from_file("tests/resources/mikan/MyBangumi-noauth.htm")
|
|
||||||
// .create_async()
|
|
||||||
// .await;
|
|
||||||
|
|
||||||
// let bangumi_metas =
|
|
||||||
// extract_mikan_bangumis_meta_from_my_bangumi_page(
|
|
||||||
// context.clone(), my_bangumi_page_url.clone(),
|
|
||||||
// None,
|
|
||||||
// &[],
|
|
||||||
// );
|
|
||||||
|
|
||||||
// pin_mut!(bangumi_metas);
|
|
||||||
|
|
||||||
// let bangumi_metas = bangumi_metas.try_collect::<Vec<_>>().await?;
|
|
||||||
|
|
||||||
// assert!(bangumi_metas.is_empty());
|
|
||||||
|
|
||||||
// assert!(my_bangumi_without_cookie_mock.matched_async().await);
|
|
||||||
// }
|
|
||||||
// {
|
|
||||||
// let my_bangumi_with_cookie_mock = mikan_server
|
|
||||||
// .mock("GET", my_bangumi_page_url.path())
|
|
||||||
// .match_header(
|
|
||||||
// header::COOKIE,
|
|
||||||
// mockito::Matcher::AllOf(vec![
|
|
||||||
//
|
|
||||||
// mockito::Matcher::Regex(String::from(".*\\.AspNetCore\\.Antiforgery.*")),
|
|
||||||
// mockito::Matcher::Regex(String::from(
|
|
||||||
// ".*\\.AspNetCore\\.Identity\\.Application.*",
|
|
||||||
// )),
|
|
||||||
// ]),
|
|
||||||
// )
|
|
||||||
// .with_body_from_file("tests/resources/mikan/MyBangumi.htm")
|
|
||||||
// .create_async()
|
|
||||||
// .await;
|
|
||||||
|
|
||||||
// let expand_bangumi_mock = mikan_server
|
|
||||||
// .mock("GET", "/ExpandBangumi")
|
|
||||||
// .match_query(mockito::Matcher::Any)
|
|
||||||
//
|
|
||||||
// .with_body_from_file("tests/resources/mikan/ExpandBangumi.htm")
|
|
||||||
// .create_async()
|
|
||||||
// .await;
|
|
||||||
|
|
||||||
// let auth_secrecy = Some(MikanCredentialForm {
|
|
||||||
// username: String::from("test_username"),
|
|
||||||
// password: String::from("test_password"),
|
|
||||||
// user_agent: String::from(
|
|
||||||
// "Mozilla/5.0 (Windows NT 10.0; Win64; x64)
|
|
||||||
// AppleWebKit/537.36 (KHTML, like \ Gecko)
|
|
||||||
// Chrome/133.0.0.0 Safari/537.36 Edg/133.0.0.0", ),
|
|
||||||
// });
|
|
||||||
|
|
||||||
// let bangumi_metas =
|
|
||||||
// extract_mikan_bangumis_meta_from_my_bangumi_page(
|
|
||||||
// context.clone(), my_bangumi_page_url,
|
|
||||||
// auth_secrecy,
|
|
||||||
// &[],
|
|
||||||
// );
|
|
||||||
// pin_mut!(bangumi_metas);
|
|
||||||
// let bangumi_metas = bangumi_metas.try_collect::<Vec<_>>().await?;
|
|
||||||
|
|
||||||
// assert!(!bangumi_metas.is_empty());
|
|
||||||
|
|
||||||
// assert!(bangumi_metas[0].origin_poster_src.is_some());
|
|
||||||
|
|
||||||
// assert!(my_bangumi_with_cookie_mock.matched_async().await);
|
|
||||||
|
|
||||||
// expand_bangumi_mock.expect(bangumi_metas.len());
|
|
||||||
// }
|
|
||||||
|
|
||||||
// Ok(())
|
|
||||||
// }
|
|
||||||
}
|
}
|
||||||
|
|||||||
@ -14,7 +14,6 @@ pub use downloader;
|
|||||||
pub mod app;
|
pub mod app;
|
||||||
pub mod auth;
|
pub mod auth;
|
||||||
pub mod cache;
|
pub mod cache;
|
||||||
pub mod crypto;
|
|
||||||
pub mod database;
|
pub mod database;
|
||||||
pub mod errors;
|
pub mod errors;
|
||||||
pub mod extract;
|
pub mod extract;
|
||||||
|
|||||||
@ -2,10 +2,7 @@ use std::collections::HashSet;
|
|||||||
|
|
||||||
use async_trait::async_trait;
|
use async_trait::async_trait;
|
||||||
use sea_orm::{DeriveIden, Statement};
|
use sea_orm::{DeriveIden, Statement};
|
||||||
use sea_orm_migration::{
|
use sea_orm_migration::prelude::{extension::postgres::IntoTypeRef, *};
|
||||||
prelude::{extension::postgres::IntoTypeRef, *},
|
|
||||||
schema::timestamp_with_time_zone,
|
|
||||||
};
|
|
||||||
|
|
||||||
use crate::migrations::extension::postgres::Type;
|
use crate::migrations::extension::postgres::Type;
|
||||||
|
|
||||||
@ -33,7 +30,6 @@ pub enum Subscriptions {
|
|||||||
Category,
|
Category,
|
||||||
SourceUrl,
|
SourceUrl,
|
||||||
Enabled,
|
Enabled,
|
||||||
CredentialId,
|
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(DeriveIden)]
|
#[derive(DeriveIden)]
|
||||||
@ -138,18 +134,6 @@ pub enum Auth {
|
|||||||
AuthType,
|
AuthType,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(DeriveIden)]
|
|
||||||
pub enum Credential3rd {
|
|
||||||
Table,
|
|
||||||
Id,
|
|
||||||
SubscriberId,
|
|
||||||
CredentialType,
|
|
||||||
Cookies,
|
|
||||||
Username,
|
|
||||||
Password,
|
|
||||||
UserAgent,
|
|
||||||
}
|
|
||||||
|
|
||||||
macro_rules! create_postgres_enum_for_active_enum {
|
macro_rules! create_postgres_enum_for_active_enum {
|
||||||
($manager: expr, $active_enum: expr, $($enum_value:expr),+) => {
|
($manager: expr, $active_enum: expr, $($enum_value:expr),+) => {
|
||||||
{
|
{
|
||||||
@ -160,17 +144,6 @@ macro_rules! create_postgres_enum_for_active_enum {
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn timestamps_z(t: TableCreateStatement) -> TableCreateStatement {
|
|
||||||
let mut t = t;
|
|
||||||
t.col(timestamp_with_time_zone(GeneralIds::CreatedAt).default(Expr::current_timestamp()))
|
|
||||||
.col(timestamp_with_time_zone(GeneralIds::UpdatedAt).default(Expr::current_timestamp()))
|
|
||||||
.take()
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn table_auto_z<T: IntoIden + 'static>(name: T) -> TableCreateStatement {
|
|
||||||
timestamps_z(Table::create().table(name).if_not_exists().take())
|
|
||||||
}
|
|
||||||
|
|
||||||
#[async_trait]
|
#[async_trait]
|
||||||
pub trait CustomSchemaManagerExt {
|
pub trait CustomSchemaManagerExt {
|
||||||
async fn create_postgres_auto_update_ts_fn(&self, col_name: &str) -> Result<(), DbErr>;
|
async fn create_postgres_auto_update_ts_fn(&self, col_name: &str) -> Result<(), DbErr>;
|
||||||
|
|||||||
@ -3,7 +3,7 @@ use sea_orm_migration::{prelude::*, schema::*};
|
|||||||
|
|
||||||
use super::defs::{
|
use super::defs::{
|
||||||
Bangumi, CustomSchemaManagerExt, Episodes, GeneralIds, Subscribers, SubscriptionBangumi,
|
Bangumi, CustomSchemaManagerExt, Episodes, GeneralIds, Subscribers, SubscriptionBangumi,
|
||||||
SubscriptionEpisode, Subscriptions, table_auto_z,
|
SubscriptionEpisode, Subscriptions,
|
||||||
};
|
};
|
||||||
use crate::models::{
|
use crate::models::{
|
||||||
subscribers::SEED_SUBSCRIBER,
|
subscribers::SEED_SUBSCRIBER,
|
||||||
@ -22,7 +22,7 @@ impl MigrationTrait for Migration {
|
|||||||
|
|
||||||
manager
|
manager
|
||||||
.create_table(
|
.create_table(
|
||||||
table_auto_z(Subscribers::Table)
|
table_auto(Subscribers::Table)
|
||||||
.col(pk_auto(Subscribers::Id))
|
.col(pk_auto(Subscribers::Id))
|
||||||
.col(string(Subscribers::DisplayName))
|
.col(string(Subscribers::DisplayName))
|
||||||
.col(json_binary_null(Subscribers::BangumiConf))
|
.col(json_binary_null(Subscribers::BangumiConf))
|
||||||
@ -57,7 +57,7 @@ impl MigrationTrait for Migration {
|
|||||||
|
|
||||||
manager
|
manager
|
||||||
.create_table(
|
.create_table(
|
||||||
table_auto_z(Subscriptions::Table)
|
table_auto(Subscriptions::Table)
|
||||||
.col(pk_auto(Subscriptions::Id))
|
.col(pk_auto(Subscriptions::Id))
|
||||||
.col(string(Subscriptions::DisplayName))
|
.col(string(Subscriptions::DisplayName))
|
||||||
.col(integer(Subscriptions::SubscriberId))
|
.col(integer(Subscriptions::SubscriberId))
|
||||||
@ -89,7 +89,7 @@ impl MigrationTrait for Migration {
|
|||||||
|
|
||||||
manager
|
manager
|
||||||
.create_table(
|
.create_table(
|
||||||
table_auto_z(Bangumi::Table)
|
table_auto(Bangumi::Table)
|
||||||
.col(pk_auto(Bangumi::Id))
|
.col(pk_auto(Bangumi::Id))
|
||||||
.col(text_null(Bangumi::MikanBangumiId))
|
.col(text_null(Bangumi::MikanBangumiId))
|
||||||
.col(integer(Bangumi::SubscriberId))
|
.col(integer(Bangumi::SubscriberId))
|
||||||
@ -156,7 +156,7 @@ impl MigrationTrait for Migration {
|
|||||||
|
|
||||||
manager
|
manager
|
||||||
.create_table(
|
.create_table(
|
||||||
table_auto_z(SubscriptionBangumi::Table)
|
table_auto(SubscriptionBangumi::Table)
|
||||||
.col(pk_auto(SubscriptionBangumi::Id))
|
.col(pk_auto(SubscriptionBangumi::Id))
|
||||||
.col(integer(SubscriptionBangumi::SubscriberId))
|
.col(integer(SubscriptionBangumi::SubscriberId))
|
||||||
.col(integer(SubscriptionBangumi::SubscriptionId))
|
.col(integer(SubscriptionBangumi::SubscriptionId))
|
||||||
@ -206,7 +206,7 @@ impl MigrationTrait for Migration {
|
|||||||
|
|
||||||
manager
|
manager
|
||||||
.create_table(
|
.create_table(
|
||||||
table_auto_z(Episodes::Table)
|
table_auto(Episodes::Table)
|
||||||
.col(pk_auto(Episodes::Id))
|
.col(pk_auto(Episodes::Id))
|
||||||
.col(text_null(Episodes::MikanEpisodeId))
|
.col(text_null(Episodes::MikanEpisodeId))
|
||||||
.col(text(Episodes::RawName))
|
.col(text(Episodes::RawName))
|
||||||
@ -275,7 +275,7 @@ impl MigrationTrait for Migration {
|
|||||||
|
|
||||||
manager
|
manager
|
||||||
.create_table(
|
.create_table(
|
||||||
table_auto_z(SubscriptionEpisode::Table)
|
table_auto(SubscriptionEpisode::Table)
|
||||||
.col(pk_auto(SubscriptionEpisode::Id))
|
.col(pk_auto(SubscriptionEpisode::Id))
|
||||||
.col(integer(SubscriptionEpisode::SubscriptionId))
|
.col(integer(SubscriptionEpisode::SubscriptionId))
|
||||||
.col(integer(SubscriptionEpisode::EpisodeId))
|
.col(integer(SubscriptionEpisode::EpisodeId))
|
||||||
|
|||||||
@ -23,7 +23,7 @@ impl MigrationTrait for Migration {
|
|||||||
|
|
||||||
manager
|
manager
|
||||||
.create_table(
|
.create_table(
|
||||||
table_auto_z(Downloaders::Table)
|
table_auto(Downloaders::Table)
|
||||||
.col(pk_auto(Downloaders::Id))
|
.col(pk_auto(Downloaders::Id))
|
||||||
.col(text(Downloaders::Endpoint))
|
.col(text(Downloaders::Endpoint))
|
||||||
.col(string_null(Downloaders::Username))
|
.col(string_null(Downloaders::Username))
|
||||||
@ -78,7 +78,7 @@ impl MigrationTrait for Migration {
|
|||||||
|
|
||||||
manager
|
manager
|
||||||
.create_table(
|
.create_table(
|
||||||
table_auto_z(Downloads::Table)
|
table_auto(Downloads::Table)
|
||||||
.col(pk_auto(Downloads::Id))
|
.col(pk_auto(Downloads::Id))
|
||||||
.col(string(Downloads::RawName))
|
.col(string(Downloads::RawName))
|
||||||
.col(string(Downloads::DisplayName))
|
.col(string(Downloads::DisplayName))
|
||||||
|
|||||||
@ -1,6 +1,5 @@
|
|||||||
use sea_orm_migration::{prelude::*, schema::*};
|
use sea_orm_migration::{prelude::*, schema::*};
|
||||||
|
|
||||||
use super::defs::table_auto_z;
|
|
||||||
use crate::{
|
use crate::{
|
||||||
migrations::defs::{CustomSchemaManagerExt, Downloaders, GeneralIds, Subscribers},
|
migrations::defs::{CustomSchemaManagerExt, Downloaders, GeneralIds, Subscribers},
|
||||||
models::downloaders::{DownloaderCategory, DownloaderCategoryEnum},
|
models::downloaders::{DownloaderCategory, DownloaderCategoryEnum},
|
||||||
@ -21,7 +20,7 @@ impl MigrationTrait for Migration {
|
|||||||
|
|
||||||
manager
|
manager
|
||||||
.create_table(
|
.create_table(
|
||||||
table_auto_z(Downloaders::Table)
|
table_auto(Downloaders::Table)
|
||||||
.col(pk_auto(Downloaders::Id))
|
.col(pk_auto(Downloaders::Id))
|
||||||
.col(text(Downloaders::Endpoint))
|
.col(text(Downloaders::Endpoint))
|
||||||
.col(string_null(Downloaders::Username))
|
.col(string_null(Downloaders::Username))
|
||||||
|
|||||||
@ -1,7 +1,7 @@
|
|||||||
use async_trait::async_trait;
|
use async_trait::async_trait;
|
||||||
use sea_orm_migration::{prelude::*, schema::*};
|
use sea_orm_migration::{prelude::*, schema::*};
|
||||||
|
|
||||||
use super::defs::{Auth, table_auto_z};
|
use super::defs::Auth;
|
||||||
use crate::{
|
use crate::{
|
||||||
migrations::defs::{CustomSchemaManagerExt, GeneralIds, Subscribers},
|
migrations::defs::{CustomSchemaManagerExt, GeneralIds, Subscribers},
|
||||||
models::{
|
models::{
|
||||||
@ -26,7 +26,7 @@ impl MigrationTrait for Migration {
|
|||||||
|
|
||||||
manager
|
manager
|
||||||
.create_table(
|
.create_table(
|
||||||
table_auto_z(Auth::Table)
|
table_auto(Auth::Table)
|
||||||
.col(pk_auto(Auth::Id))
|
.col(pk_auto(Auth::Id))
|
||||||
.col(text(Auth::Pid))
|
.col(text(Auth::Pid))
|
||||||
.col(enumeration(
|
.col(enumeration(
|
||||||
|
|||||||
@ -1,107 +0,0 @@
|
|||||||
use async_trait::async_trait;
|
|
||||||
use sea_orm_migration::{
|
|
||||||
prelude::*,
|
|
||||||
schema::{string_null, *},
|
|
||||||
};
|
|
||||||
|
|
||||||
use super::defs::{CustomSchemaManagerExt, GeneralIds, table_auto_z};
|
|
||||||
use crate::{
|
|
||||||
migrations::defs::{Credential3rd, Subscribers, Subscriptions},
|
|
||||||
models::credential_3rd::{Credential3rdType, Credential3rdTypeEnum},
|
|
||||||
};
|
|
||||||
|
|
||||||
#[derive(DeriveMigrationName)]
|
|
||||||
pub struct Migration;
|
|
||||||
|
|
||||||
#[async_trait]
|
|
||||||
impl MigrationTrait for Migration {
|
|
||||||
async fn up(&self, manager: &SchemaManager) -> Result<(), DbErr> {
|
|
||||||
create_postgres_enum_for_active_enum!(
|
|
||||||
manager,
|
|
||||||
Credential3rdTypeEnum,
|
|
||||||
Credential3rdType::Mikan
|
|
||||||
)
|
|
||||||
.await?;
|
|
||||||
|
|
||||||
manager
|
|
||||||
.create_table(
|
|
||||||
table_auto_z(Credential3rd::Table)
|
|
||||||
.col(pk_auto(Credential3rd::Id))
|
|
||||||
.col(integer(Credential3rd::SubscriberId))
|
|
||||||
.col(string(Credential3rd::CredentialType))
|
|
||||||
.col(string_null(Credential3rd::Cookies))
|
|
||||||
.col(string_null(Credential3rd::Username))
|
|
||||||
.col(string_null(Credential3rd::Password))
|
|
||||||
.col(string_null(Credential3rd::UserAgent))
|
|
||||||
.foreign_key(
|
|
||||||
ForeignKey::create()
|
|
||||||
.name("fk_credential_3rd_subscriber_id")
|
|
||||||
.from(Credential3rd::Table, Credential3rd::SubscriberId)
|
|
||||||
.to(Subscribers::Table, Subscribers::Id)
|
|
||||||
.on_update(ForeignKeyAction::Cascade)
|
|
||||||
.on_delete(ForeignKeyAction::Cascade),
|
|
||||||
)
|
|
||||||
.to_owned(),
|
|
||||||
)
|
|
||||||
.await?;
|
|
||||||
|
|
||||||
manager
|
|
||||||
.create_index(
|
|
||||||
Index::create()
|
|
||||||
.name("idx_credential_3rd_credential_type")
|
|
||||||
.table(Credential3rd::Table)
|
|
||||||
.col(Credential3rd::CredentialType)
|
|
||||||
.to_owned(),
|
|
||||||
)
|
|
||||||
.await?;
|
|
||||||
|
|
||||||
manager
|
|
||||||
.create_postgres_auto_update_ts_trigger_for_col(
|
|
||||||
Credential3rd::Table,
|
|
||||||
GeneralIds::UpdatedAt,
|
|
||||||
)
|
|
||||||
.await?;
|
|
||||||
|
|
||||||
manager
|
|
||||||
.alter_table(
|
|
||||||
Table::alter()
|
|
||||||
.table(Subscriptions::Table)
|
|
||||||
.add_column_if_not_exists(integer_null(Subscriptions::CredentialId))
|
|
||||||
.add_foreign_key(
|
|
||||||
TableForeignKey::new()
|
|
||||||
.name("fk_subscriptions_credential_id")
|
|
||||||
.from_tbl(Subscriptions::Table)
|
|
||||||
.from_col(Subscriptions::CredentialId)
|
|
||||||
.to_tbl(Credential3rd::Table)
|
|
||||||
.to_col(Credential3rd::Id)
|
|
||||||
.on_update(ForeignKeyAction::Cascade)
|
|
||||||
.on_delete(ForeignKeyAction::SetNull),
|
|
||||||
)
|
|
||||||
.to_owned(),
|
|
||||||
)
|
|
||||||
.await?;
|
|
||||||
|
|
||||||
Ok(())
|
|
||||||
}
|
|
||||||
|
|
||||||
async fn down(&self, manager: &SchemaManager) -> Result<(), DbErr> {
|
|
||||||
manager
|
|
||||||
.alter_table(
|
|
||||||
Table::alter()
|
|
||||||
.table(Subscriptions::Table)
|
|
||||||
.drop_column(Subscriptions::CredentialId)
|
|
||||||
.to_owned(),
|
|
||||||
)
|
|
||||||
.await?;
|
|
||||||
|
|
||||||
manager
|
|
||||||
.drop_table(Table::drop().table(Credential3rd::Table).to_owned())
|
|
||||||
.await?;
|
|
||||||
|
|
||||||
manager
|
|
||||||
.drop_postgres_enum_for_active_enum(Credential3rdTypeEnum)
|
|
||||||
.await?;
|
|
||||||
|
|
||||||
Ok(())
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@ -7,7 +7,6 @@ pub mod m20220101_000001_init;
|
|||||||
pub mod m20240224_082543_add_downloads;
|
pub mod m20240224_082543_add_downloads;
|
||||||
pub mod m20240225_060853_subscriber_add_downloader;
|
pub mod m20240225_060853_subscriber_add_downloader;
|
||||||
pub mod m20241231_000001_auth;
|
pub mod m20241231_000001_auth;
|
||||||
pub mod m20250501_021523_credential_3rd;
|
|
||||||
|
|
||||||
pub struct Migrator;
|
pub struct Migrator;
|
||||||
|
|
||||||
@ -19,7 +18,6 @@ impl MigratorTrait for Migrator {
|
|||||||
Box::new(m20240224_082543_add_downloads::Migration),
|
Box::new(m20240224_082543_add_downloads::Migration),
|
||||||
Box::new(m20240225_060853_subscriber_add_downloader::Migration),
|
Box::new(m20240225_060853_subscriber_add_downloader::Migration),
|
||||||
Box::new(m20241231_000001_auth::Migration),
|
Box::new(m20241231_000001_auth::Migration),
|
||||||
Box::new(m20250501_021523_credential_3rd::Migration),
|
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -24,9 +24,9 @@ pub enum AuthType {
|
|||||||
#[sea_orm(table_name = "auth")]
|
#[sea_orm(table_name = "auth")]
|
||||||
pub struct Model {
|
pub struct Model {
|
||||||
#[sea_orm(default_expr = "Expr::current_timestamp()")]
|
#[sea_orm(default_expr = "Expr::current_timestamp()")]
|
||||||
pub created_at: DateTimeUtc,
|
pub created_at: DateTime,
|
||||||
#[sea_orm(default_expr = "Expr::current_timestamp()")]
|
#[sea_orm(default_expr = "Expr::current_timestamp()")]
|
||||||
pub updated_at: DateTimeUtc,
|
pub updated_at: DateTime,
|
||||||
#[sea_orm(primary_key)]
|
#[sea_orm(primary_key)]
|
||||||
pub id: i32,
|
pub id: i32,
|
||||||
#[sea_orm(unique)]
|
#[sea_orm(unique)]
|
||||||
|
|||||||
@ -30,9 +30,9 @@ pub struct BangumiExtra {
|
|||||||
#[sea_orm(table_name = "bangumi")]
|
#[sea_orm(table_name = "bangumi")]
|
||||||
pub struct Model {
|
pub struct Model {
|
||||||
#[sea_orm(default_expr = "Expr::current_timestamp()")]
|
#[sea_orm(default_expr = "Expr::current_timestamp()")]
|
||||||
pub created_at: DateTimeUtc,
|
pub created_at: DateTime,
|
||||||
#[sea_orm(default_expr = "Expr::current_timestamp()")]
|
#[sea_orm(default_expr = "Expr::current_timestamp()")]
|
||||||
pub updated_at: DateTimeUtc,
|
pub updated_at: DateTime,
|
||||||
#[sea_orm(primary_key)]
|
#[sea_orm(primary_key)]
|
||||||
pub id: i32,
|
pub id: i32,
|
||||||
pub mikan_bangumi_id: Option<String>,
|
pub mikan_bangumi_id: Option<String>,
|
||||||
|
|||||||
@ -1,143 +0,0 @@
|
|||||||
use std::sync::Arc;
|
|
||||||
|
|
||||||
use async_trait::async_trait;
|
|
||||||
use sea_orm::{ActiveValue, prelude::*};
|
|
||||||
use serde::{Deserialize, Serialize};
|
|
||||||
|
|
||||||
use crate::{
|
|
||||||
app::AppContextTrait,
|
|
||||||
crypto::UserPassCredential,
|
|
||||||
errors::{RecorderError, RecorderResult},
|
|
||||||
};
|
|
||||||
|
|
||||||
#[derive(
|
|
||||||
Debug, Clone, PartialEq, Eq, EnumIter, DeriveActiveEnum, DeriveDisplay, Serialize, Deserialize,
|
|
||||||
)]
|
|
||||||
#[sea_orm(
|
|
||||||
rs_type = "String",
|
|
||||||
db_type = "Enum",
|
|
||||||
enum_name = "credential_3rd_type"
|
|
||||||
)]
|
|
||||||
pub enum Credential3rdType {
|
|
||||||
#[sea_orm(string_value = "mikan")]
|
|
||||||
Mikan,
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize, DeriveEntityModel)]
|
|
||||||
#[sea_orm(table_name = "credential_3rd")]
|
|
||||||
pub struct Model {
|
|
||||||
#[sea_orm(default_expr = "Expr::current_timestamp()")]
|
|
||||||
pub created_at: DateTimeUtc,
|
|
||||||
#[sea_orm(default_expr = "Expr::current_timestamp()")]
|
|
||||||
pub updated_at: DateTimeUtc,
|
|
||||||
#[sea_orm(primary_key)]
|
|
||||||
pub id: i32,
|
|
||||||
pub subscriber_id: i32,
|
|
||||||
pub credential_type: Credential3rdType,
|
|
||||||
pub cookies: Option<String>,
|
|
||||||
pub username: Option<String>,
|
|
||||||
pub password: Option<String>,
|
|
||||||
pub user_agent: Option<String>,
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Copy, Clone, Debug, EnumIter, DeriveRelation)]
|
|
||||||
pub enum Relation {
|
|
||||||
#[sea_orm(
|
|
||||||
belongs_to = "super::subscribers::Entity",
|
|
||||||
from = "Column::SubscriberId",
|
|
||||||
to = "super::subscribers::Column::Id",
|
|
||||||
on_update = "Cascade",
|
|
||||||
on_delete = "Cascade"
|
|
||||||
)]
|
|
||||||
Subscriber,
|
|
||||||
#[sea_orm(has_many = "super::subscriptions::Entity")]
|
|
||||||
Subscription,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl Related<super::subscribers::Entity> for Entity {
|
|
||||||
fn to() -> RelationDef {
|
|
||||||
Relation::Subscriber.def()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl Related<super::subscriptions::Entity> for Entity {
|
|
||||||
fn to() -> RelationDef {
|
|
||||||
Relation::Subscription.def()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#[async_trait]
|
|
||||||
impl ActiveModelBehavior for ActiveModel {}
|
|
||||||
|
|
||||||
impl ActiveModel {
|
|
||||||
pub async fn try_encrypt(mut self, ctx: Arc<dyn AppContextTrait>) -> RecorderResult<Self> {
|
|
||||||
let crypto = ctx.crypto();
|
|
||||||
|
|
||||||
if let ActiveValue::Set(Some(username)) = self.username {
|
|
||||||
let username_enc = crypto.encrypt_credentials(&username)?;
|
|
||||||
self.username = ActiveValue::Set(Some(username_enc));
|
|
||||||
}
|
|
||||||
|
|
||||||
if let ActiveValue::Set(Some(password)) = self.password {
|
|
||||||
let password_enc = crypto.encrypt_credentials(&password)?;
|
|
||||||
self.password = ActiveValue::Set(Some(password_enc));
|
|
||||||
}
|
|
||||||
|
|
||||||
if let ActiveValue::Set(Some(cookies)) = self.cookies {
|
|
||||||
let cookies_enc = crypto.encrypt_credentials(&cookies)?;
|
|
||||||
self.cookies = ActiveValue::Set(Some(cookies_enc));
|
|
||||||
}
|
|
||||||
|
|
||||||
Ok(self)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl Model {
|
|
||||||
pub async fn find_by_id(
|
|
||||||
ctx: Arc<dyn AppContextTrait>,
|
|
||||||
id: i32,
|
|
||||||
) -> RecorderResult<Option<Self>> {
|
|
||||||
let db = ctx.db();
|
|
||||||
let credential = Entity::find_by_id(id).one(db).await?;
|
|
||||||
|
|
||||||
Ok(credential)
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn try_into_userpass_credential(
|
|
||||||
self,
|
|
||||||
ctx: Arc<dyn AppContextTrait>,
|
|
||||||
) -> RecorderResult<UserPassCredential> {
|
|
||||||
let crypto = ctx.crypto();
|
|
||||||
let username_enc = self
|
|
||||||
.username
|
|
||||||
.ok_or_else(|| RecorderError::Credential3rdError {
|
|
||||||
message: "UserPassCredential username is required".to_string(),
|
|
||||||
source: None.into(),
|
|
||||||
})?;
|
|
||||||
|
|
||||||
let username: String = crypto.decrypt_credentials(&username_enc)?;
|
|
||||||
|
|
||||||
let password_enc = self
|
|
||||||
.password
|
|
||||||
.ok_or_else(|| RecorderError::Credential3rdError {
|
|
||||||
message: "UserPassCredential password is required".to_string(),
|
|
||||||
source: None.into(),
|
|
||||||
})?;
|
|
||||||
|
|
||||||
let password: String = crypto.decrypt_credentials(&password_enc)?;
|
|
||||||
|
|
||||||
let cookies: Option<String> = if let Some(cookies_enc) = self.cookies {
|
|
||||||
let cookies = crypto.decrypt_credentials(&cookies_enc)?;
|
|
||||||
Some(cookies)
|
|
||||||
} else {
|
|
||||||
None
|
|
||||||
};
|
|
||||||
|
|
||||||
Ok(UserPassCredential {
|
|
||||||
username,
|
|
||||||
password,
|
|
||||||
cookies,
|
|
||||||
user_agent: self.user_agent,
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@ -23,9 +23,9 @@ pub enum DownloaderCategory {
|
|||||||
#[sea_orm(table_name = "downloaders")]
|
#[sea_orm(table_name = "downloaders")]
|
||||||
pub struct Model {
|
pub struct Model {
|
||||||
#[sea_orm(default_expr = "Expr::current_timestamp()")]
|
#[sea_orm(default_expr = "Expr::current_timestamp()")]
|
||||||
pub created_at: DateTimeUtc,
|
pub created_at: DateTime,
|
||||||
#[sea_orm(default_expr = "Expr::current_timestamp()")]
|
#[sea_orm(default_expr = "Expr::current_timestamp()")]
|
||||||
pub updated_at: DateTimeUtc,
|
pub updated_at: DateTime,
|
||||||
#[sea_orm(primary_key)]
|
#[sea_orm(primary_key)]
|
||||||
pub id: i32,
|
pub id: i32,
|
||||||
pub category: DownloaderCategory,
|
pub category: DownloaderCategory,
|
||||||
|
|||||||
@ -39,9 +39,9 @@ pub enum DownloadMime {
|
|||||||
#[sea_orm(table_name = "downloads")]
|
#[sea_orm(table_name = "downloads")]
|
||||||
pub struct Model {
|
pub struct Model {
|
||||||
#[sea_orm(default_expr = "Expr::current_timestamp()")]
|
#[sea_orm(default_expr = "Expr::current_timestamp()")]
|
||||||
pub created_at: DateTimeUtc,
|
pub created_at: DateTime,
|
||||||
#[sea_orm(default_expr = "Expr::current_timestamp()")]
|
#[sea_orm(default_expr = "Expr::current_timestamp()")]
|
||||||
pub updated_at: DateTimeUtc,
|
pub updated_at: DateTime,
|
||||||
#[sea_orm(primary_key)]
|
#[sea_orm(primary_key)]
|
||||||
pub id: i32,
|
pub id: i32,
|
||||||
pub raw_name: String,
|
pub raw_name: String,
|
||||||
|
|||||||
@ -9,7 +9,7 @@ use crate::{
|
|||||||
app::AppContextTrait,
|
app::AppContextTrait,
|
||||||
errors::RecorderResult,
|
errors::RecorderResult,
|
||||||
extract::{
|
extract::{
|
||||||
mikan::{MikanEpisodeMeta, build_mikan_episode_homepage_url},
|
mikan::{MikanEpisodeMeta, build_mikan_episode_homepage},
|
||||||
rawname::parse_episode_meta_from_raw_name,
|
rawname::parse_episode_meta_from_raw_name,
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
@ -28,9 +28,9 @@ pub struct EpisodeExtra {
|
|||||||
#[sea_orm(table_name = "episodes")]
|
#[sea_orm(table_name = "episodes")]
|
||||||
pub struct Model {
|
pub struct Model {
|
||||||
#[sea_orm(default_expr = "Expr::current_timestamp()")]
|
#[sea_orm(default_expr = "Expr::current_timestamp()")]
|
||||||
pub created_at: DateTimeUtc,
|
pub created_at: DateTime,
|
||||||
#[sea_orm(default_expr = "Expr::current_timestamp()")]
|
#[sea_orm(default_expr = "Expr::current_timestamp()")]
|
||||||
pub updated_at: DateTimeUtc,
|
pub updated_at: DateTime,
|
||||||
#[sea_orm(primary_key)]
|
#[sea_orm(primary_key)]
|
||||||
pub id: i32,
|
pub id: i32,
|
||||||
#[sea_orm(indexed)]
|
#[sea_orm(indexed)]
|
||||||
@ -200,10 +200,8 @@ impl ActiveModel {
|
|||||||
})
|
})
|
||||||
.ok()
|
.ok()
|
||||||
.unwrap_or_default();
|
.unwrap_or_default();
|
||||||
let homepage = build_mikan_episode_homepage_url(
|
let homepage =
|
||||||
ctx.mikan().base_url().clone(),
|
build_mikan_episode_homepage(ctx.mikan().base_url().clone(), &item.mikan_episode_id);
|
||||||
&item.mikan_episode_id,
|
|
||||||
);
|
|
||||||
|
|
||||||
Ok(Self {
|
Ok(Self {
|
||||||
mikan_episode_id: ActiveValue::Set(Some(item.mikan_episode_id)),
|
mikan_episode_id: ActiveValue::Set(Some(item.mikan_episode_id)),
|
||||||
|
|||||||
@ -1,6 +1,5 @@
|
|||||||
pub mod auth;
|
pub mod auth;
|
||||||
pub mod bangumi;
|
pub mod bangumi;
|
||||||
pub mod credential_3rd;
|
|
||||||
pub mod downloaders;
|
pub mod downloaders;
|
||||||
pub mod downloads;
|
pub mod downloads;
|
||||||
pub mod episodes;
|
pub mod episodes;
|
||||||
|
|||||||
@ -5,7 +5,7 @@ use serde::{Deserialize, Serialize};
|
|||||||
|
|
||||||
use crate::{
|
use crate::{
|
||||||
app::AppContextTrait,
|
app::AppContextTrait,
|
||||||
errors::app_error::{RecorderError, RecorderResult},
|
errors::app_error::{RecorderResult, RecorderError},
|
||||||
};
|
};
|
||||||
|
|
||||||
pub const SEED_SUBSCRIBER: &str = "konobangu";
|
pub const SEED_SUBSCRIBER: &str = "konobangu";
|
||||||
@ -21,9 +21,9 @@ pub struct SubscriberBangumiConfig {
|
|||||||
#[sea_orm(table_name = "subscribers")]
|
#[sea_orm(table_name = "subscribers")]
|
||||||
pub struct Model {
|
pub struct Model {
|
||||||
#[sea_orm(default_expr = "Expr::current_timestamp()")]
|
#[sea_orm(default_expr = "Expr::current_timestamp()")]
|
||||||
pub created_at: DateTimeUtc,
|
pub created_at: DateTime,
|
||||||
#[sea_orm(default_expr = "Expr::current_timestamp()")]
|
#[sea_orm(default_expr = "Expr::current_timestamp()")]
|
||||||
pub updated_at: DateTimeUtc,
|
pub updated_at: DateTime,
|
||||||
#[sea_orm(primary_key)]
|
#[sea_orm(primary_key)]
|
||||||
pub id: i32,
|
pub id: i32,
|
||||||
pub display_name: String,
|
pub display_name: String,
|
||||||
|
|||||||
@ -11,7 +11,7 @@ use crate::{
|
|||||||
errors::RecorderResult,
|
errors::RecorderResult,
|
||||||
extract::{
|
extract::{
|
||||||
mikan::{
|
mikan::{
|
||||||
build_mikan_bangumi_homepage_url, build_mikan_bangumi_rss_url,
|
build_mikan_bangumi_homepage, build_mikan_bangumi_rss_link,
|
||||||
extract_mikan_bangumi_meta_from_bangumi_homepage,
|
extract_mikan_bangumi_meta_from_bangumi_homepage,
|
||||||
extract_mikan_episode_meta_from_episode_homepage,
|
extract_mikan_episode_meta_from_episode_homepage,
|
||||||
extract_mikan_rss_channel_from_rss_link,
|
extract_mikan_rss_channel_from_rss_link,
|
||||||
@ -44,9 +44,9 @@ pub enum SubscriptionCategory {
|
|||||||
#[sea_orm(table_name = "subscriptions")]
|
#[sea_orm(table_name = "subscriptions")]
|
||||||
pub struct Model {
|
pub struct Model {
|
||||||
#[sea_orm(default_expr = "Expr::current_timestamp()")]
|
#[sea_orm(default_expr = "Expr::current_timestamp()")]
|
||||||
pub created_at: DateTimeUtc,
|
pub created_at: DateTime,
|
||||||
#[sea_orm(default_expr = "Expr::current_timestamp()")]
|
#[sea_orm(default_expr = "Expr::current_timestamp()")]
|
||||||
pub updated_at: DateTimeUtc,
|
pub updated_at: DateTime,
|
||||||
#[sea_orm(primary_key)]
|
#[sea_orm(primary_key)]
|
||||||
pub id: i32,
|
pub id: i32,
|
||||||
pub display_name: String,
|
pub display_name: String,
|
||||||
@ -54,7 +54,6 @@ pub struct Model {
|
|||||||
pub category: SubscriptionCategory,
|
pub category: SubscriptionCategory,
|
||||||
pub source_url: String,
|
pub source_url: String,
|
||||||
pub enabled: bool,
|
pub enabled: bool,
|
||||||
pub credential_id: Option<i32>,
|
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Copy, Clone, Debug, EnumIter, DeriveRelation)]
|
#[derive(Copy, Clone, Debug, EnumIter, DeriveRelation)]
|
||||||
@ -75,14 +74,6 @@ pub enum Relation {
|
|||||||
SubscriptionEpisode,
|
SubscriptionEpisode,
|
||||||
#[sea_orm(has_many = "super::subscription_bangumi::Entity")]
|
#[sea_orm(has_many = "super::subscription_bangumi::Entity")]
|
||||||
SubscriptionBangumi,
|
SubscriptionBangumi,
|
||||||
#[sea_orm(
|
|
||||||
belongs_to = "super::credential_3rd::Entity",
|
|
||||||
from = "Column::CredentialId",
|
|
||||||
to = "super::credential_3rd::Column::Id",
|
|
||||||
on_update = "Cascade",
|
|
||||||
on_delete = "SetNull"
|
|
||||||
)]
|
|
||||||
Credential3rd,
|
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Related<super::subscribers::Entity> for Entity {
|
impl Related<super::subscribers::Entity> for Entity {
|
||||||
@ -131,12 +122,6 @@ impl Related<super::episodes::Entity> for Entity {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Related<super::credential_3rd::Entity> for Entity {
|
|
||||||
fn to() -> RelationDef {
|
|
||||||
Relation::Credential3rd.def()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Copy, Clone, Debug, EnumIter, DeriveRelatedEntity)]
|
#[derive(Copy, Clone, Debug, EnumIter, DeriveRelatedEntity)]
|
||||||
pub enum RelatedEntity {
|
pub enum RelatedEntity {
|
||||||
#[sea_orm(entity = "super::subscribers::Entity")]
|
#[sea_orm(entity = "super::subscribers::Entity")]
|
||||||
@ -149,8 +134,6 @@ pub enum RelatedEntity {
|
|||||||
SubscriptionEpisode,
|
SubscriptionEpisode,
|
||||||
#[sea_orm(entity = "super::subscription_bangumi::Entity")]
|
#[sea_orm(entity = "super::subscription_bangumi::Entity")]
|
||||||
SubscriptionBangumi,
|
SubscriptionBangumi,
|
||||||
#[sea_orm(entity = "super::credential_3rd::Entity")]
|
|
||||||
Credential3rd,
|
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
|
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
|
||||||
@ -287,12 +270,12 @@ impl Model {
|
|||||||
for ((mikan_bangumi_id, mikan_fansub_id), new_ep_metas) in new_mikan_bangumi_groups
|
for ((mikan_bangumi_id, mikan_fansub_id), new_ep_metas) in new_mikan_bangumi_groups
|
||||||
{
|
{
|
||||||
let mikan_base_url = ctx.mikan().base_url();
|
let mikan_base_url = ctx.mikan().base_url();
|
||||||
let bgm_homepage = build_mikan_bangumi_homepage_url(
|
let bgm_homepage = build_mikan_bangumi_homepage(
|
||||||
mikan_base_url.clone(),
|
mikan_base_url.clone(),
|
||||||
&mikan_bangumi_id,
|
&mikan_bangumi_id,
|
||||||
Some(&mikan_fansub_id),
|
Some(&mikan_fansub_id),
|
||||||
);
|
);
|
||||||
let bgm_rss_link = build_mikan_bangumi_rss_url(
|
let bgm_rss_link = build_mikan_bangumi_rss_link(
|
||||||
mikan_base_url.clone(),
|
mikan_base_url.clone(),
|
||||||
&mikan_bangumi_id,
|
&mikan_bangumi_id,
|
||||||
Some(&mikan_fansub_id),
|
Some(&mikan_fansub_id),
|
||||||
|
|||||||
@ -1,4 +0,0 @@
|
|||||||
use serde::{Deserialize, Serialize};
|
|
||||||
|
|
||||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
|
||||||
pub struct TaskConfig {}
|
|
||||||
279
apps/recorder/src/tasks/core.rs
Normal file
279
apps/recorder/src/tasks/core.rs
Normal file
@ -0,0 +1,279 @@
|
|||||||
|
use std::{borrow::Cow, sync::Arc};
|
||||||
|
|
||||||
|
use async_stream::stream;
|
||||||
|
use futures::{Stream, StreamExt, pin_mut};
|
||||||
|
use serde::{Serialize, de::DeserializeOwned};
|
||||||
|
use tokio::sync::{RwLock, mpsc};
|
||||||
|
|
||||||
|
use crate::{
|
||||||
|
app::AppContextTrait,
|
||||||
|
errors::app_error::{RecorderError, RecorderResult},
|
||||||
|
models,
|
||||||
|
};
|
||||||
|
|
||||||
|
pub struct TaskMeta {
|
||||||
|
pub subscriber_id: i32,
|
||||||
|
pub task_id: i32,
|
||||||
|
pub task_kind: Cow<'static, str>,
|
||||||
|
}
|
||||||
|
|
||||||
|
pub struct ReplayChannel<T: Send + Sync + Clone + 'static> {
|
||||||
|
sender: mpsc::UnboundedSender<T>,
|
||||||
|
channels: Arc<RwLock<Vec<mpsc::UnboundedSender<T>>>>,
|
||||||
|
buffer: Arc<RwLock<Vec<T>>>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<T: Send + Sync + Clone + 'static> ReplayChannel<T> {
|
||||||
|
pub fn new(history: Vec<T>) -> Self {
|
||||||
|
let (tx, mut rx) = mpsc::unbounded_channel::<T>();
|
||||||
|
let channels = Arc::new(RwLock::new(Vec::<mpsc::UnboundedSender<T>>::new()));
|
||||||
|
let buffer = Arc::new(RwLock::new(history));
|
||||||
|
{
|
||||||
|
let channels = channels.clone();
|
||||||
|
let buffer = buffer.clone();
|
||||||
|
tokio::spawn(async move {
|
||||||
|
loop {
|
||||||
|
match rx.recv().await {
|
||||||
|
Some(value) => {
|
||||||
|
let mut w = buffer.write().await;
|
||||||
|
let senders = channels.read().await;
|
||||||
|
for s in senders.iter() {
|
||||||
|
if !s.is_closed() {
|
||||||
|
if let Err(err) = s.send(value.clone()) {
|
||||||
|
tracing::error!(err = %err, "replay-channel broadcast to other subscribers error");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
w.push(value);
|
||||||
|
}
|
||||||
|
None => {
|
||||||
|
drop(rx);
|
||||||
|
let mut cs = channels.write().await;
|
||||||
|
cs.clear();
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
Self {
|
||||||
|
sender: tx,
|
||||||
|
channels,
|
||||||
|
buffer,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn sender(&self) -> &mpsc::UnboundedSender<T> {
|
||||||
|
&self.sender
|
||||||
|
}
|
||||||
|
|
||||||
|
pub async fn receiver(&self) -> mpsc::UnboundedReceiver<T> {
|
||||||
|
let (tx, rx) = mpsc::unbounded_channel();
|
||||||
|
let items = self.buffer.read().await;
|
||||||
|
for item in items.iter() {
|
||||||
|
if let Err(err) = tx.send(item.clone()) {
|
||||||
|
tracing::error!(err = %err, "replay-channel send replay value to other subscribers error");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if !self.sender.is_closed() {
|
||||||
|
let mut sw = self.channels.write().await;
|
||||||
|
sw.push(tx);
|
||||||
|
}
|
||||||
|
rx
|
||||||
|
}
|
||||||
|
|
||||||
|
pub async fn close(&self) {
|
||||||
|
let mut senders = self.channels.write().await;
|
||||||
|
senders.clear();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub trait StreamTaskCoreTrait: Sized {
|
||||||
|
type Request: Serialize + DeserializeOwned;
|
||||||
|
type Item: Serialize + DeserializeOwned;
|
||||||
|
|
||||||
|
fn task_id(&self) -> i32;
|
||||||
|
|
||||||
|
fn task_kind(&self) -> &str;
|
||||||
|
|
||||||
|
fn new(meta: TaskMeta, request: Self::Request) -> Self;
|
||||||
|
|
||||||
|
fn request(&self) -> &Self::Request;
|
||||||
|
}
|
||||||
|
|
||||||
|
pub trait StreamTaskReplayLayoutTrait: StreamTaskCoreTrait {
|
||||||
|
fn history(&self) -> &[Arc<RecorderResult<Self::Item>>];
|
||||||
|
|
||||||
|
fn resume_from_model(
|
||||||
|
task: models::tasks::Model,
|
||||||
|
stream_items: Vec<models::task_stream_item::Model>,
|
||||||
|
) -> RecorderResult<Self>;
|
||||||
|
|
||||||
|
fn running_receiver(
|
||||||
|
&self,
|
||||||
|
) -> impl Future<Output = Option<mpsc::UnboundedReceiver<Arc<RecorderResult<Self::Item>>>>>;
|
||||||
|
|
||||||
|
#[allow(clippy::type_complexity)]
|
||||||
|
fn init_receiver(
|
||||||
|
&self,
|
||||||
|
) -> impl Future<
|
||||||
|
Output = (
|
||||||
|
mpsc::UnboundedSender<Arc<RecorderResult<Self::Item>>>,
|
||||||
|
mpsc::UnboundedReceiver<Arc<RecorderResult<Self::Item>>>,
|
||||||
|
),
|
||||||
|
>;
|
||||||
|
|
||||||
|
fn serialize_request(request: Self::Request) -> RecorderResult<serde_json::Value> {
|
||||||
|
serde_json::to_value(request).map_err(RecorderError::from)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn serialize_item(item: RecorderResult<Self::Item>) -> RecorderResult<serde_json::Value> {
|
||||||
|
serde_json::to_value(item).map_err(RecorderError::from)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn deserialize_request(request: serde_json::Value) -> RecorderResult<Self::Request> {
|
||||||
|
serde_json::from_value(request).map_err(RecorderError::from)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn deserialize_item(item: serde_json::Value) -> RecorderResult<RecorderResult<Self::Item>> {
|
||||||
|
serde_json::from_value(item).map_err(RecorderError::from)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub trait StreamTaskRunnerTrait: StreamTaskCoreTrait {
|
||||||
|
fn run(
|
||||||
|
context: Arc<dyn AppContextTrait>,
|
||||||
|
request: &Self::Request,
|
||||||
|
history: &[Arc<RecorderResult<Self::Item>>],
|
||||||
|
) -> impl Stream<Item = RecorderResult<Self::Item>>;
|
||||||
|
}
|
||||||
|
|
||||||
|
pub trait StreamTaskReplayRunnerTrait: StreamTaskRunnerTrait + StreamTaskReplayLayoutTrait {
|
||||||
|
fn run_shared(
|
||||||
|
&self,
|
||||||
|
context: Arc<dyn AppContextTrait>,
|
||||||
|
) -> impl Stream<Item = Arc<RecorderResult<Self::Item>>> {
|
||||||
|
stream! {
|
||||||
|
if let Some(mut receiver) = self.running_receiver().await {
|
||||||
|
while let Some(item) = receiver.recv().await {
|
||||||
|
yield item
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
let (tx, _) = self.init_receiver().await;
|
||||||
|
let stream = Self::run(context, self.request(), self.history());
|
||||||
|
|
||||||
|
pin_mut!(stream);
|
||||||
|
|
||||||
|
while let Some(item) = stream.next().await {
|
||||||
|
let item = Arc::new(item);
|
||||||
|
if let Err(err) = tx.send(item.clone()) {
|
||||||
|
tracing::error!(task_id = self.task_id(), task_kind = self.task_kind(), err = %err, "run shared send error");
|
||||||
|
}
|
||||||
|
yield item
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub struct StandardStreamTaskReplayLayout<Request, Item>
|
||||||
|
where
|
||||||
|
Request: Serialize + DeserializeOwned,
|
||||||
|
Item: Serialize + DeserializeOwned + Sync + Send + 'static,
|
||||||
|
{
|
||||||
|
pub meta: TaskMeta,
|
||||||
|
pub request: Request,
|
||||||
|
pub history: Vec<Arc<RecorderResult<Item>>>,
|
||||||
|
#[allow(clippy::type_complexity)]
|
||||||
|
pub channel: Arc<RwLock<Option<ReplayChannel<Arc<RecorderResult<Item>>>>>>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<Request, Item> StreamTaskCoreTrait for StandardStreamTaskReplayLayout<Request, Item>
|
||||||
|
where
|
||||||
|
Request: Serialize + DeserializeOwned,
|
||||||
|
Item: Serialize + DeserializeOwned + Sync + Send + 'static,
|
||||||
|
{
|
||||||
|
type Request = Request;
|
||||||
|
type Item = Item;
|
||||||
|
|
||||||
|
fn task_id(&self) -> i32 {
|
||||||
|
self.meta.task_id
|
||||||
|
}
|
||||||
|
|
||||||
|
fn request(&self) -> &Self::Request {
|
||||||
|
&self.request
|
||||||
|
}
|
||||||
|
|
||||||
|
fn task_kind(&self) -> &str {
|
||||||
|
&self.meta.task_kind
|
||||||
|
}
|
||||||
|
|
||||||
|
fn new(meta: TaskMeta, request: Self::Request) -> Self {
|
||||||
|
Self {
|
||||||
|
meta,
|
||||||
|
request,
|
||||||
|
history: vec![],
|
||||||
|
channel: Arc::new(RwLock::new(None)),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<Request, Item> StreamTaskReplayLayoutTrait for StandardStreamTaskReplayLayout<Request, Item>
|
||||||
|
where
|
||||||
|
Request: Serialize + DeserializeOwned,
|
||||||
|
Item: Serialize + DeserializeOwned + Sync + Send + 'static,
|
||||||
|
{
|
||||||
|
fn history(&self) -> &[Arc<RecorderResult<Self::Item>>] {
|
||||||
|
&self.history
|
||||||
|
}
|
||||||
|
|
||||||
|
fn resume_from_model(
|
||||||
|
task: models::tasks::Model,
|
||||||
|
stream_items: Vec<models::task_stream_item::Model>,
|
||||||
|
) -> RecorderResult<Self> {
|
||||||
|
Ok(Self {
|
||||||
|
meta: TaskMeta {
|
||||||
|
task_id: task.id,
|
||||||
|
subscriber_id: task.subscriber_id,
|
||||||
|
task_kind: Cow::Owned(task.task_type),
|
||||||
|
},
|
||||||
|
request: Self::deserialize_request(task.request_data)?,
|
||||||
|
history: stream_items
|
||||||
|
.into_iter()
|
||||||
|
.map(|m| Self::deserialize_item(m.item).map(Arc::new))
|
||||||
|
.collect::<RecorderResult<Vec<_>>>()?,
|
||||||
|
channel: Arc::new(RwLock::new(None)),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn running_receiver(
|
||||||
|
&self,
|
||||||
|
) -> Option<mpsc::UnboundedReceiver<Arc<RecorderResult<Self::Item>>>> {
|
||||||
|
if let Some(channel) = self.channel.read().await.as_ref() {
|
||||||
|
Some(channel.receiver().await)
|
||||||
|
} else {
|
||||||
|
None
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn init_receiver(
|
||||||
|
&self,
|
||||||
|
) -> (
|
||||||
|
mpsc::UnboundedSender<Arc<RecorderResult<Self::Item>>>,
|
||||||
|
mpsc::UnboundedReceiver<Arc<RecorderResult<Self::Item>>>,
|
||||||
|
) {
|
||||||
|
let channel = ReplayChannel::new(self.history.clone());
|
||||||
|
let rx = channel.receiver().await;
|
||||||
|
let sender = channel.sender().clone();
|
||||||
|
|
||||||
|
{
|
||||||
|
{
|
||||||
|
let mut w = self.channel.write().await;
|
||||||
|
*w = Some(channel);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
(sender, rx)
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -0,0 +1,37 @@
|
|||||||
|
use std::sync::Arc;
|
||||||
|
|
||||||
|
use futures::Stream;
|
||||||
|
use serde::{Deserialize, Serialize};
|
||||||
|
use url::Url;
|
||||||
|
|
||||||
|
use crate::{
|
||||||
|
app::AppContextTrait,
|
||||||
|
errors::RecorderResult,
|
||||||
|
extract::mikan::{MikanAuthSecrecy, MikanBangumiMeta, web_extract},
|
||||||
|
tasks::core::{StandardStreamTaskReplayLayout, StreamTaskRunnerTrait},
|
||||||
|
};
|
||||||
|
|
||||||
|
#[derive(Clone, Debug, Serialize, Deserialize)]
|
||||||
|
pub struct ExtractMikanBangumisMetaFromMyBangumiRequest {
|
||||||
|
pub my_bangumi_page_url: Url,
|
||||||
|
pub auth_secrecy: Option<MikanAuthSecrecy>,
|
||||||
|
}
|
||||||
|
|
||||||
|
pub type ExtractMikanBangumisMetaFromMyBangumiTask =
|
||||||
|
StandardStreamTaskReplayLayout<ExtractMikanBangumisMetaFromMyBangumiRequest, MikanBangumiMeta>;
|
||||||
|
|
||||||
|
impl StreamTaskRunnerTrait for ExtractMikanBangumisMetaFromMyBangumiTask {
|
||||||
|
fn run(
|
||||||
|
context: Arc<dyn AppContextTrait>,
|
||||||
|
request: &Self::Request,
|
||||||
|
history: &[Arc<RecorderResult<Self::Item>>],
|
||||||
|
) -> impl Stream<Item = RecorderResult<Self::Item>> {
|
||||||
|
let context = context.clone();
|
||||||
|
web_extract::extract_mikan_bangumis_meta_from_my_bangumi_page(
|
||||||
|
context,
|
||||||
|
request.my_bangumi_page_url.clone(),
|
||||||
|
request.auth_secrecy.clone(),
|
||||||
|
history,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -1,172 +0,0 @@
|
|||||||
use std::{ops::Deref, sync::Arc};
|
|
||||||
|
|
||||||
use apalis::prelude::*;
|
|
||||||
use apalis_sql::postgres::PostgresStorage;
|
|
||||||
use fetch::fetch_html;
|
|
||||||
use serde::{Deserialize, Serialize};
|
|
||||||
use snafu::OptionExt;
|
|
||||||
|
|
||||||
use crate::{
|
|
||||||
app::AppContextTrait,
|
|
||||||
errors::{RecorderError, RecorderResult},
|
|
||||||
extract::mikan::{
|
|
||||||
MikanBangumiMeta, MikanSeasonStr, build_mikan_season_flow_url,
|
|
||||||
extract_mikan_bangumi_indices_meta_from_season_flow_fragment,
|
|
||||||
web_extract::{
|
|
||||||
MikanBangumiIndexMeta, build_mikan_bangumi_expand_subscribed_fragment_url,
|
|
||||||
extract_mikan_bangumi_meta_from_expand_subscribed_fragment,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
};
|
|
||||||
|
|
||||||
const TASK_NAME: &str = "mikan_extract_season_subscription";
|
|
||||||
|
|
||||||
#[derive(Clone, Debug, Serialize, Deserialize)]
|
|
||||||
pub struct ExtractMikanSeasonSubscriptionTask {
|
|
||||||
pub task_id: i32,
|
|
||||||
pub year: i32,
|
|
||||||
pub season_str: MikanSeasonStr,
|
|
||||||
pub credential_id: i32,
|
|
||||||
pub subscription_id: i32,
|
|
||||||
pub subscriber_id: i32,
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Clone, Debug, Serialize, Deserialize)]
|
|
||||||
pub struct ExtractMikanSeasonSubscriptionFansubsTask {
|
|
||||||
pub task_id: i32,
|
|
||||||
pub year: i32,
|
|
||||||
pub season_str: MikanSeasonStr,
|
|
||||||
pub credential_id: i32,
|
|
||||||
pub subscription_id: i32,
|
|
||||||
pub subscriber_id: i32,
|
|
||||||
pub bangumi_indices: Vec<MikanBangumiIndexMeta>,
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Clone, Debug, Serialize, Deserialize)]
|
|
||||||
pub struct ExtractMikanSeasonSubscriptionTaskResult {
|
|
||||||
pub task_id: i32,
|
|
||||||
pub year: i32,
|
|
||||||
pub season_str: MikanSeasonStr,
|
|
||||||
pub credential_id: i32,
|
|
||||||
pub subscription_id: i32,
|
|
||||||
pub subscriber_id: i32,
|
|
||||||
pub bangumi_metas: Vec<MikanBangumiMeta>,
|
|
||||||
}
|
|
||||||
|
|
||||||
pub async fn extract_mikan_season_subscription(
|
|
||||||
job: ExtractMikanSeasonSubscriptionTask,
|
|
||||||
data: Data<Arc<dyn AppContextTrait>>,
|
|
||||||
) -> RecorderResult<GoTo<ExtractMikanSeasonSubscriptionFansubsTask>> {
|
|
||||||
let ctx = data.deref();
|
|
||||||
|
|
||||||
let mikan_client = ctx
|
|
||||||
.mikan()
|
|
||||||
.fork_with_credential(ctx.clone(), Some(job.credential_id))
|
|
||||||
.await?;
|
|
||||||
|
|
||||||
let mikan_base_url = mikan_client.base_url().clone();
|
|
||||||
|
|
||||||
let season_flow_fragment_url =
|
|
||||||
build_mikan_season_flow_url(mikan_base_url.clone(), job.year, job.season_str);
|
|
||||||
|
|
||||||
let season_flow_fragment = fetch_html(&mikan_client, season_flow_fragment_url.clone()).await?;
|
|
||||||
|
|
||||||
let mut bangumi_indices = extract_mikan_bangumi_indices_meta_from_season_flow_fragment(
|
|
||||||
&season_flow_fragment,
|
|
||||||
mikan_base_url.clone(),
|
|
||||||
);
|
|
||||||
|
|
||||||
if bangumi_indices.is_empty() && !mikan_client.has_login().await? {
|
|
||||||
mikan_client.login().await?;
|
|
||||||
let season_flow_fragment =
|
|
||||||
fetch_html(&mikan_client, season_flow_fragment_url.clone()).await?;
|
|
||||||
bangumi_indices = extract_mikan_bangumi_indices_meta_from_season_flow_fragment(
|
|
||||||
&season_flow_fragment,
|
|
||||||
mikan_base_url.clone(),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
Ok(GoTo::Next(ExtractMikanSeasonSubscriptionFansubsTask {
|
|
||||||
task_id: job.task_id,
|
|
||||||
year: job.year,
|
|
||||||
season_str: job.season_str,
|
|
||||||
credential_id: job.credential_id,
|
|
||||||
subscription_id: job.subscription_id,
|
|
||||||
subscriber_id: job.subscriber_id,
|
|
||||||
bangumi_indices,
|
|
||||||
}))
|
|
||||||
}
|
|
||||||
|
|
||||||
pub async fn extract_mikan_season_subscription_fansubs(
|
|
||||||
job: ExtractMikanSeasonSubscriptionFansubsTask,
|
|
||||||
data: Data<Arc<dyn AppContextTrait>>,
|
|
||||||
) -> RecorderResult<GoTo<ExtractMikanSeasonSubscriptionTaskResult>> {
|
|
||||||
let ctx = data.deref();
|
|
||||||
|
|
||||||
let mikan_client = ctx
|
|
||||||
.mikan()
|
|
||||||
.fork_with_credential(ctx.clone(), Some(job.credential_id))
|
|
||||||
.await?;
|
|
||||||
|
|
||||||
let bangumi_indices = job.bangumi_indices;
|
|
||||||
|
|
||||||
let mut bangumi_metas = vec![];
|
|
||||||
|
|
||||||
let mikan_base_url = mikan_client.base_url().clone();
|
|
||||||
|
|
||||||
for bangumi_index in bangumi_indices {
|
|
||||||
let bangumi_title = bangumi_index.bangumi_title.clone();
|
|
||||||
let bangumi_expand_subscribed_fragment_url =
|
|
||||||
build_mikan_bangumi_expand_subscribed_fragment_url(
|
|
||||||
mikan_base_url.clone(),
|
|
||||||
&bangumi_index.mikan_bangumi_id,
|
|
||||||
);
|
|
||||||
let bangumi_expand_subscribed_fragment =
|
|
||||||
fetch_html(&mikan_client, bangumi_expand_subscribed_fragment_url).await?;
|
|
||||||
|
|
||||||
let bangumi_meta = extract_mikan_bangumi_meta_from_expand_subscribed_fragment(
|
|
||||||
bangumi_index,
|
|
||||||
&bangumi_expand_subscribed_fragment,
|
|
||||||
mikan_base_url.clone(),
|
|
||||||
)
|
|
||||||
.with_whatever_context::<_, String, RecorderError>(|| {
|
|
||||||
format!(
|
|
||||||
"failed to extract mikan bangumi fansub of title = {}",
|
|
||||||
bangumi_title
|
|
||||||
)
|
|
||||||
})?;
|
|
||||||
|
|
||||||
bangumi_metas.push(bangumi_meta);
|
|
||||||
}
|
|
||||||
|
|
||||||
Ok(GoTo::Done(ExtractMikanSeasonSubscriptionTaskResult {
|
|
||||||
bangumi_metas,
|
|
||||||
credential_id: job.credential_id,
|
|
||||||
season_str: job.season_str,
|
|
||||||
subscriber_id: job.subscriber_id,
|
|
||||||
subscription_id: job.subscription_id,
|
|
||||||
task_id: job.task_id,
|
|
||||||
year: job.year,
|
|
||||||
}))
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn register_extract_mikan_season_subscription_task(
|
|
||||||
monitor: Monitor,
|
|
||||||
ctx: Arc<dyn AppContextTrait>,
|
|
||||||
) -> RecorderResult<(Monitor, PostgresStorage<StepRequest<serde_json::Value>>)> {
|
|
||||||
let pool = ctx.db().get_postgres_connection_pool().clone();
|
|
||||||
let storage = PostgresStorage::new(pool);
|
|
||||||
|
|
||||||
let steps = StepBuilder::new()
|
|
||||||
.step_fn(extract_mikan_season_subscription)
|
|
||||||
.step_fn(extract_mikan_season_subscription_fansubs);
|
|
||||||
|
|
||||||
let worker = WorkerBuilder::new(TASK_NAME)
|
|
||||||
.catch_panic()
|
|
||||||
.enable_tracing()
|
|
||||||
.data(ctx)
|
|
||||||
.backend(storage.clone())
|
|
||||||
.build_stepped(steps);
|
|
||||||
|
|
||||||
Ok((monitor.register(worker), storage))
|
|
||||||
}
|
|
||||||
@ -1,5 +1 @@
|
|||||||
mod extract_season_subscription;
|
pub mod extract_mikan_bangumis_meta_from_my_bangumi;
|
||||||
|
|
||||||
pub use extract_season_subscription::{
|
|
||||||
ExtractMikanSeasonSubscriptionTask, register_extract_mikan_season_subscription_task,
|
|
||||||
};
|
|
||||||
|
|||||||
@ -1,6 +1,4 @@
|
|||||||
pub mod config;
|
pub mod core;
|
||||||
pub mod mikan;
|
pub mod mikan;
|
||||||
pub mod service;
|
pub mod service;
|
||||||
|
pub mod registry;
|
||||||
pub use config::TaskConfig;
|
|
||||||
pub use service::TaskService;
|
|
||||||
|
|||||||
1
apps/recorder/src/tasks/registry.rs
Normal file
1
apps/recorder/src/tasks/registry.rs
Normal file
@ -0,0 +1 @@
|
|||||||
|
|
||||||
@ -1,41 +1,4 @@
|
|||||||
use std::{fmt::Debug, sync::Arc};
|
#[derive(Debug)]
|
||||||
|
pub struct TaskService {}
|
||||||
|
|
||||||
use apalis::prelude::*;
|
impl TaskService {}
|
||||||
use apalis_sql::postgres::PostgresStorage;
|
|
||||||
use tokio::sync::Mutex;
|
|
||||||
|
|
||||||
use super::{TaskConfig, mikan::register_extract_mikan_season_subscription_task};
|
|
||||||
use crate::{app::AppContextTrait, errors::RecorderResult};
|
|
||||||
|
|
||||||
pub struct TaskService {
|
|
||||||
config: TaskConfig,
|
|
||||||
#[allow(dead_code)]
|
|
||||||
monitor: Arc<Mutex<Monitor>>,
|
|
||||||
pub extract_mikan_season_subscription_task_storage:
|
|
||||||
PostgresStorage<StepRequest<serde_json::Value>>,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl TaskService {
|
|
||||||
pub async fn from_config_and_ctx(
|
|
||||||
config: TaskConfig,
|
|
||||||
ctx: Arc<dyn AppContextTrait>,
|
|
||||||
) -> RecorderResult<Self> {
|
|
||||||
let monitor = Monitor::new();
|
|
||||||
let (monitor, extract_mikan_season_subscription_task_storage) =
|
|
||||||
register_extract_mikan_season_subscription_task(monitor, ctx.clone())?;
|
|
||||||
|
|
||||||
Ok(Self {
|
|
||||||
config,
|
|
||||||
monitor: Arc::new(Mutex::new(monitor)),
|
|
||||||
extract_mikan_season_subscription_task_storage,
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl Debug for TaskService {
|
|
||||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
|
||||||
f.debug_struct("TaskService")
|
|
||||||
.field("config", &self.config)
|
|
||||||
.finish()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|||||||
@ -1,5 +1,3 @@
|
|||||||
use std::fmt::Debug;
|
|
||||||
|
|
||||||
use typed_builder::TypedBuilder;
|
use typed_builder::TypedBuilder;
|
||||||
|
|
||||||
use crate::app::AppContextTrait;
|
use crate::app::AppContextTrait;
|
||||||
@ -15,20 +13,12 @@ pub struct UnitTestAppContext {
|
|||||||
auth: Option<crate::auth::AuthService>,
|
auth: Option<crate::auth::AuthService>,
|
||||||
graphql: Option<crate::graphql::GraphQLService>,
|
graphql: Option<crate::graphql::GraphQLService>,
|
||||||
storage: Option<crate::storage::StorageService>,
|
storage: Option<crate::storage::StorageService>,
|
||||||
crypto: Option<crate::crypto::CryptoService>,
|
|
||||||
tasks: Option<crate::tasks::TaskService>,
|
|
||||||
#[builder(default = Some(String::from(env!("CARGO_MANIFEST_DIR"))))]
|
#[builder(default = Some(String::from(env!("CARGO_MANIFEST_DIR"))))]
|
||||||
working_dir: Option<String>,
|
working_dir: Option<String>,
|
||||||
#[builder(default = crate::app::Environment::Testing, setter(!strip_option))]
|
#[builder(default = crate::app::Environment::Testing, setter(!strip_option))]
|
||||||
environment: crate::app::Environment,
|
environment: crate::app::Environment,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Debug for UnitTestAppContext {
|
|
||||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
|
||||||
write!(f, "UnitTestAppContext")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl AppContextTrait for UnitTestAppContext {
|
impl AppContextTrait for UnitTestAppContext {
|
||||||
fn logger(&self) -> &crate::logger::LoggerService {
|
fn logger(&self) -> &crate::logger::LoggerService {
|
||||||
self.logger.as_ref().expect("should set logger")
|
self.logger.as_ref().expect("should set logger")
|
||||||
@ -69,12 +59,4 @@ impl AppContextTrait for UnitTestAppContext {
|
|||||||
fn working_dir(&self) -> &String {
|
fn working_dir(&self) -> &String {
|
||||||
self.working_dir.as_ref().expect("should set working_dir")
|
self.working_dir.as_ref().expect("should set working_dir")
|
||||||
}
|
}
|
||||||
|
|
||||||
fn crypto(&self) -> &crate::crypto::CryptoService {
|
|
||||||
self.crypto.as_ref().expect("should set crypto")
|
|
||||||
}
|
|
||||||
|
|
||||||
fn task(&self) -> &crate::tasks::TaskService {
|
|
||||||
self.tasks.as_ref().expect("should set tasks")
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|||||||
@ -5,8 +5,8 @@ use axum::{Extension, Router, extract::State, middleware::from_fn_with_state, ro
|
|||||||
|
|
||||||
use super::core::Controller;
|
use super::core::Controller;
|
||||||
use crate::{
|
use crate::{
|
||||||
app::{AppContextTrait, Environment},
|
app::AppContextTrait,
|
||||||
auth::{AuthUserInfo, auth_middleware},
|
auth::{AuthUserInfo, header_www_authenticate_middleware},
|
||||||
errors::RecorderResult,
|
errors::RecorderResult,
|
||||||
};
|
};
|
||||||
|
|
||||||
@ -25,51 +25,9 @@ async fn graphql_handler(
|
|||||||
graphql_service.schema.execute(req).await.into()
|
graphql_service.schema.execute(req).await.into()
|
||||||
}
|
}
|
||||||
|
|
||||||
// 检查是否是 introspection 查询
|
|
||||||
fn is_introspection_query(req: &async_graphql::Request) -> bool {
|
|
||||||
if let Some(operation) = &req.operation_name {
|
|
||||||
if operation.starts_with("__") {
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// 检查查询内容是否包含 introspection 字段
|
|
||||||
let query = req.query.as_str();
|
|
||||||
query.contains("__schema") || query.contains("__type") || query.contains("__typename")
|
|
||||||
}
|
|
||||||
|
|
||||||
async fn graphql_introspection_handler(
|
|
||||||
State(ctx): State<Arc<dyn AppContextTrait>>,
|
|
||||||
req: GraphQLRequest,
|
|
||||||
) -> GraphQLResponse {
|
|
||||||
let graphql_service = ctx.graphql();
|
|
||||||
let req = req.into_inner();
|
|
||||||
|
|
||||||
if !is_introspection_query(&req) {
|
|
||||||
return GraphQLResponse::from(async_graphql::Response::from_errors(vec![
|
|
||||||
async_graphql::ServerError::new(
|
|
||||||
"Only introspection queries are allowed on this endpoint",
|
|
||||||
None,
|
|
||||||
),
|
|
||||||
]));
|
|
||||||
}
|
|
||||||
|
|
||||||
graphql_service.schema.execute(req).await.into()
|
|
||||||
}
|
|
||||||
|
|
||||||
pub async fn create(ctx: Arc<dyn AppContextTrait>) -> RecorderResult<Controller> {
|
pub async fn create(ctx: Arc<dyn AppContextTrait>) -> RecorderResult<Controller> {
|
||||||
let mut introspection_handler = post(graphql_introspection_handler);
|
|
||||||
|
|
||||||
if !matches!(ctx.environment(), Environment::Development) {
|
|
||||||
introspection_handler =
|
|
||||||
introspection_handler.layer(from_fn_with_state(ctx.clone(), auth_middleware));
|
|
||||||
}
|
|
||||||
|
|
||||||
let router = Router::<Arc<dyn AppContextTrait>>::new()
|
let router = Router::<Arc<dyn AppContextTrait>>::new()
|
||||||
.route(
|
.route("/", post(graphql_handler))
|
||||||
"/",
|
.layer(from_fn_with_state(ctx, header_www_authenticate_middleware));
|
||||||
post(graphql_handler).layer(from_fn_with_state(ctx, auth_middleware)),
|
|
||||||
)
|
|
||||||
.route("/introspection", introspection_handler);
|
|
||||||
Ok(Controller::from_prefix(CONTROLLER_PREFIX, router))
|
Ok(Controller::from_prefix(CONTROLLER_PREFIX, router))
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,28 +0,0 @@
|
|||||||
<svg width="0" height="0">
|
|
||||||
<defs>
|
|
||||||
<clipPath id="clip-triangle-1">
|
|
||||||
<polygon points="0 15,80 15,100 0,120 15,1120 15,1120 370,0 370" />
|
|
||||||
</clipPath>
|
|
||||||
<clipPath id="clip-triangle-2">
|
|
||||||
<polygon points="0 15,310 15,330 0,350 15,1120 15,1120 370,0 370" />
|
|
||||||
</clipPath>
|
|
||||||
<clipPath id="clip-triangle-3">
|
|
||||||
<polygon points="0 15,540 15,560 0,580 15,1120 15,1120 370,0 370" />
|
|
||||||
</clipPath>
|
|
||||||
<clipPath id="clip-triangle-4">
|
|
||||||
<polygon points="0 15,770 15,790 0,810 15,1120 15,1120 370,0 370" />
|
|
||||||
</clipPath>
|
|
||||||
<clipPath id="clip-triangle-5">
|
|
||||||
<polygon points="0 15,995 15,1015 0,1035 15,1120 15,1120 370,0 370" />
|
|
||||||
</clipPath>
|
|
||||||
<linearGradient id="row-bg-gradient" x1="0%" y1="0%" x2="100%" y2="0%">
|
|
||||||
<stop offset="0%" stop-color="#e4dedf" />
|
|
||||||
<stop offset="100%" stop-color="#cbbcc4" />
|
|
||||||
</linearGradient>
|
|
||||||
</defs>
|
|
||||||
</svg>
|
|
||||||
|
|
||||||
<div class="sk-bangumi">
|
|
||||||
|
|
||||||
<div class="no-subscribe-bangumi"> >_< 您还没有订阅任何番组,快去<a href="/">首页</a>添加订阅吧</div>
|
|
||||||
</div>
|
|
||||||
|
Before Width: | Height: | Size: 1.1 KiB |
@ -1,840 +0,0 @@
|
|||||||
<svg width="0" height="0">
|
|
||||||
<defs>
|
|
||||||
<clipPath id="clip-triangle-1">
|
|
||||||
<polygon points="0 15,80 15,100 0,120 15,1120 15,1120 370,0 370" />
|
|
||||||
</clipPath>
|
|
||||||
<clipPath id="clip-triangle-2">
|
|
||||||
<polygon points="0 15,310 15,330 0,350 15,1120 15,1120 370,0 370" />
|
|
||||||
</clipPath>
|
|
||||||
<clipPath id="clip-triangle-3">
|
|
||||||
<polygon points="0 15,540 15,560 0,580 15,1120 15,1120 370,0 370" />
|
|
||||||
</clipPath>
|
|
||||||
<clipPath id="clip-triangle-4">
|
|
||||||
<polygon points="0 15,770 15,790 0,810 15,1120 15,1120 370,0 370" />
|
|
||||||
</clipPath>
|
|
||||||
<clipPath id="clip-triangle-5">
|
|
||||||
<polygon points="0 15,995 15,1015 0,1035 15,1120 15,1120 370,0 370" />
|
|
||||||
</clipPath>
|
|
||||||
<linearGradient id="row-bg-gradient" x1="0%" y1="0%" x2="100%" y2="0%">
|
|
||||||
<stop offset="0%" stop-color="#e4dedf" />
|
|
||||||
<stop offset="100%" stop-color="#cbbcc4" />
|
|
||||||
</linearGradient>
|
|
||||||
</defs>
|
|
||||||
</svg>
|
|
||||||
|
|
||||||
<div class="sk-bangumi">
|
|
||||||
<div class="mine an-box animated fadeIn">
|
|
||||||
<ul class="list-inline an-ul">
|
|
||||||
<li>
|
|
||||||
<span data-src="/images/Bangumi/202204/d8ef46c0.jpg?width=400&height=400&format=webp"
|
|
||||||
class="js-expand_bangumi b-lazy" data-bangumiid="3288" data-bangumiindex="1"
|
|
||||||
data-showsubscribed="true"></span>
|
|
||||||
<div class="an-info">
|
|
||||||
<div class="an-info-group">
|
|
||||||
<div class="date-text">2024/08/09 更新</div>
|
|
||||||
<a href="/Home/Bangumi/3288" target="_blank" class="an-text"
|
|
||||||
title="吉伊卡哇">吉伊卡哇</a>
|
|
||||||
</div>
|
|
||||||
<div class="an-info-icon active js-subscribe_bangumi" data-subtitlegroupid="" data-bangumiid="3288"
|
|
||||||
data-toggle="tooltip" data-placement="bottom" title="取消"><i class="fa fa-rss "></i></div>
|
|
||||||
</div>
|
|
||||||
</li>
|
|
||||||
|
|
||||||
<li>
|
|
||||||
<span data-src="/images/Bangumi/202407/997f06af.jpg?width=400&height=400&format=webp"
|
|
||||||
class="js-expand_bangumi b-lazy" data-bangumiid="3383" data-bangumiindex="2"
|
|
||||||
data-showsubscribed="true"></span>
|
|
||||||
<div class="an-info">
|
|
||||||
<div class="an-info-group">
|
|
||||||
<div class="date-text">2025/05/08 更新</div>
|
|
||||||
<a href="/Home/Bangumi/3383" target="_blank" class="an-text"
|
|
||||||
title="你与我最后的战场,亦或是世界起始的圣战 第二季">你与我最后的战场,亦或是世界起始的圣战
|
|
||||||
第二季</a>
|
|
||||||
</div>
|
|
||||||
<div class="an-info-icon active js-subscribe_bangumi" data-subtitlegroupid="" data-bangumiid="3383"
|
|
||||||
data-toggle="tooltip" data-placement="bottom" title="取消"><i class="fa fa-rss "></i></div>
|
|
||||||
</div>
|
|
||||||
</li>
|
|
||||||
|
|
||||||
<li>
|
|
||||||
<span data-src="/images/Bangumi/202501/c63dd1b9.jpg?width=400&height=400&format=webp"
|
|
||||||
class="js-expand_bangumi b-lazy" data-bangumiid="3515" data-bangumiindex="3"
|
|
||||||
data-showsubscribed="true"></span>
|
|
||||||
<div class="an-info">
|
|
||||||
<div class="an-info-group">
|
|
||||||
<div class="date-text">2025/04/20 更新</div>
|
|
||||||
<a href="/Home/Bangumi/3515" target="_blank" class="an-text"
|
|
||||||
title="几分钟的欢呼">几分钟的欢呼</a>
|
|
||||||
</div>
|
|
||||||
<div class="an-info-icon active js-subscribe_bangumi" data-subtitlegroupid="" data-bangumiid="3515"
|
|
||||||
data-toggle="tooltip" data-placement="bottom" title="取消"><i class="fa fa-rss "></i></div>
|
|
||||||
</div>
|
|
||||||
</li>
|
|
||||||
|
|
||||||
<li>
|
|
||||||
<span data-src="/images/Bangumi/202501/d5a4b73b.jpg?width=400&height=400&format=webp"
|
|
||||||
class="js-expand_bangumi b-lazy" data-bangumiid="3526" data-bangumiindex="4"
|
|
||||||
data-showsubscribed="true"></span>
|
|
||||||
<div class="num-node text-center">1</div>
|
|
||||||
<div class="an-info">
|
|
||||||
<div class="an-info-group">
|
|
||||||
<div class="date-text">2025/05/14 更新</div>
|
|
||||||
<a href="/Home/Bangumi/3526" target="_blank" class="an-text"
|
|
||||||
title="我家有个阿宅女忍者">我家有个阿宅女忍者</a>
|
|
||||||
</div>
|
|
||||||
<div class="an-info-icon active js-subscribe_bangumi" data-subtitlegroupid="" data-bangumiid="3526"
|
|
||||||
data-toggle="tooltip" data-placement="bottom" title="取消"><i class="fa fa-rss "></i></div>
|
|
||||||
</div>
|
|
||||||
</li>
|
|
||||||
|
|
||||||
<li>
|
|
||||||
<span data-src="/images/Bangumi/202501/2e430a10.jpg?width=400&height=400&format=webp"
|
|
||||||
class="js-expand_bangumi b-lazy" data-bangumiid="3530" data-bangumiindex="5"
|
|
||||||
data-showsubscribed="true"></span>
|
|
||||||
<div class="an-info">
|
|
||||||
<div class="an-info-group">
|
|
||||||
<div class="date-text">2025/05/12 更新</div>
|
|
||||||
<a href="/Home/Bangumi/3530" target="_blank" class="an-text"
|
|
||||||
title="药屋少女的呢喃 第二季">药屋少女的呢喃
|
|
||||||
第二季</a>
|
|
||||||
</div>
|
|
||||||
<div class="an-info-icon active js-subscribe_bangumi" data-subtitlegroupid="" data-bangumiid="3530"
|
|
||||||
data-toggle="tooltip" data-placement="bottom" title="取消"><i class="fa fa-rss "></i></div>
|
|
||||||
</div>
|
|
||||||
</li>
|
|
||||||
</ul>
|
|
||||||
</div>
|
|
||||||
<div class="row an-res-row-frame" style="display:none;"></div>
|
|
||||||
<div class="mine an-box animated fadeIn">
|
|
||||||
<ul class="list-inline an-ul">
|
|
||||||
<li>
|
|
||||||
<span data-src="/images/Bangumi/202501/424750fe.jpg?width=400&height=400&format=webp"
|
|
||||||
class="js-expand_bangumi b-lazy" data-bangumiid="3556" data-bangumiindex="1"
|
|
||||||
data-showsubscribed="true"></span>
|
|
||||||
<div class="num-node text-center">1</div>
|
|
||||||
<div class="an-info">
|
|
||||||
<div class="an-info-group">
|
|
||||||
<div class="date-text">2025/05/14 更新</div>
|
|
||||||
<a href="/Home/Bangumi/3556" target="_blank" class="an-text"
|
|
||||||
title="脱离了A级队伍的我,和从前的徒弟们前往迷宫深处。">脱离了A级队伍的我,和从前的徒弟们前往迷宫深处。</a>
|
|
||||||
</div>
|
|
||||||
<div class="an-info-icon active js-subscribe_bangumi" data-subtitlegroupid="" data-bangumiid="3556"
|
|
||||||
data-toggle="tooltip" data-placement="bottom" title="取消"><i class="fa fa-rss "></i></div>
|
|
||||||
</div>
|
|
||||||
</li>
|
|
||||||
|
|
||||||
<li>
|
|
||||||
<span data-src="/images/Bangumi/202504/c0832100.jpg?width=400&height=400&format=webp"
|
|
||||||
class="js-expand_bangumi b-lazy" data-bangumiid="3573" data-bangumiindex="2"
|
|
||||||
data-showsubscribed="true"></span>
|
|
||||||
<div class="an-info">
|
|
||||||
<div class="an-info-group">
|
|
||||||
<div class="date-text">2025/05/05 更新</div>
|
|
||||||
<a href="/Home/Bangumi/3573" target="_blank" class="an-text"
|
|
||||||
title="一起加油吧">一起加油吧</a>
|
|
||||||
</div>
|
|
||||||
<div class="an-info-icon active js-subscribe_bangumi" data-subtitlegroupid="" data-bangumiid="3573"
|
|
||||||
data-toggle="tooltip" data-placement="bottom" title="取消"><i class="fa fa-rss "></i></div>
|
|
||||||
</div>
|
|
||||||
</li>
|
|
||||||
|
|
||||||
<li>
|
|
||||||
<span data-src="/images/Bangumi/202504/f86c46e8.jpg?width=400&height=400&format=webp"
|
|
||||||
class="js-expand_bangumi b-lazy" data-bangumiid="3582" data-bangumiindex="3"
|
|
||||||
data-showsubscribed="true"></span>
|
|
||||||
<div class="num-node text-center">1</div>
|
|
||||||
<div class="an-info">
|
|
||||||
<div class="an-info-group">
|
|
||||||
<div class="date-text">2025/05/13 更新</div>
|
|
||||||
<a href="/Home/Bangumi/3582" target="_blank" class="an-text"
|
|
||||||
title="安妮·雪莉">安妮·雪莉</a>
|
|
||||||
</div>
|
|
||||||
<div class="an-info-icon active js-subscribe_bangumi" data-subtitlegroupid="" data-bangumiid="3582"
|
|
||||||
data-toggle="tooltip" data-placement="bottom" title="取消"><i class="fa fa-rss "></i></div>
|
|
||||||
</div>
|
|
||||||
</li>
|
|
||||||
|
|
||||||
<li>
|
|
||||||
<span data-src="/images/Bangumi/202504/3e3fe89e.jpg?width=400&height=400&format=webp"
|
|
||||||
class="js-expand_bangumi b-lazy" data-bangumiid="3583" data-bangumiindex="4"
|
|
||||||
data-showsubscribed="true"></span>
|
|
||||||
<div class="num-node text-center">6</div>
|
|
||||||
<div class="an-info">
|
|
||||||
<div class="an-info-group">
|
|
||||||
<div class="date-text">2025/05/14 更新</div>
|
|
||||||
<a href="/Home/Bangumi/3583" target="_blank" class="an-text"
|
|
||||||
title="直至魔女消逝">直至魔女消逝</a>
|
|
||||||
</div>
|
|
||||||
<div class="an-info-icon active js-subscribe_bangumi" data-subtitlegroupid="" data-bangumiid="3583"
|
|
||||||
data-toggle="tooltip" data-placement="bottom" title="取消"><i class="fa fa-rss "></i></div>
|
|
||||||
</div>
|
|
||||||
</li>
|
|
||||||
|
|
||||||
<li>
|
|
||||||
<span data-src="/images/Bangumi/202504/d92cb519.jpg?width=400&height=400&format=webp"
|
|
||||||
class="js-expand_bangumi b-lazy" data-bangumiid="3584" data-bangumiindex="5"
|
|
||||||
data-showsubscribed="true"></span>
|
|
||||||
<div class="num-node text-center">3</div>
|
|
||||||
<div class="an-info">
|
|
||||||
<div class="an-info-group">
|
|
||||||
<div class="date-text">2025/05/14 更新</div>
|
|
||||||
<a href="/Home/Bangumi/3584" target="_blank" class="an-text"
|
|
||||||
title="忍者与杀手二人组的日常生活">忍者与杀手二人组的日常生活</a>
|
|
||||||
</div>
|
|
||||||
<div class="an-info-icon active js-subscribe_bangumi" data-subtitlegroupid="" data-bangumiid="3584"
|
|
||||||
data-toggle="tooltip" data-placement="bottom" title="取消"><i class="fa fa-rss "></i></div>
|
|
||||||
</div>
|
|
||||||
</li>
|
|
||||||
</ul>
|
|
||||||
</div>
|
|
||||||
<div class="row an-res-row-frame" style="display:none;"></div>
|
|
||||||
<div class="mine an-box animated fadeIn">
|
|
||||||
<ul class="list-inline an-ul">
|
|
||||||
<li>
|
|
||||||
<span data-src="/images/Bangumi/202504/e33a7226.jpg?width=400&height=400&format=webp"
|
|
||||||
class="js-expand_bangumi b-lazy" data-bangumiid="3585" data-bangumiindex="1"
|
|
||||||
data-showsubscribed="true"></span>
|
|
||||||
<div class="num-node text-center">2</div>
|
|
||||||
<div class="an-info">
|
|
||||||
<div class="an-info-group">
|
|
||||||
<div class="date-text">2025/05/14 更新</div>
|
|
||||||
<a href="/Home/Bangumi/3585" target="_blank" class="an-text"
|
|
||||||
title="机动战士高达 GQuuuuuuX">机动战士高达
|
|
||||||
GQuuuuuuX</a>
|
|
||||||
</div>
|
|
||||||
<div class="an-info-icon active js-subscribe_bangumi" data-subtitlegroupid="" data-bangumiid="3585"
|
|
||||||
data-toggle="tooltip" data-placement="bottom" title="取消"><i class="fa fa-rss "></i></div>
|
|
||||||
</div>
|
|
||||||
</li>
|
|
||||||
|
|
||||||
<li>
|
|
||||||
<span data-src="/images/Bangumi/202504/a5d55f1e.jpg?width=400&height=400&format=webp"
|
|
||||||
class="js-expand_bangumi b-lazy" data-bangumiid="3586" data-bangumiindex="2"
|
|
||||||
data-showsubscribed="true"></span>
|
|
||||||
<div class="num-node text-center">3</div>
|
|
||||||
<div class="an-info">
|
|
||||||
<div class="an-info-group">
|
|
||||||
<div class="date-text">2025/05/13 更新</div>
|
|
||||||
<a href="/Home/Bangumi/3586" target="_blank" class="an-text"
|
|
||||||
title="打了300年的史莱姆,不知不觉就练到了满级 ~其二~">打了300年的史莱姆,不知不觉就练到了满级
|
|
||||||
~其二~</a>
|
|
||||||
</div>
|
|
||||||
<div class="an-info-icon active js-subscribe_bangumi" data-subtitlegroupid="" data-bangumiid="3586"
|
|
||||||
data-toggle="tooltip" data-placement="bottom" title="取消"><i class="fa fa-rss "></i></div>
|
|
||||||
</div>
|
|
||||||
</li>
|
|
||||||
|
|
||||||
<li>
|
|
||||||
<span data-src="/images/Bangumi/202504/ff5c2429.jpg?width=400&height=400&format=webp"
|
|
||||||
class="js-expand_bangumi b-lazy" data-bangumiid="3587" data-bangumiindex="3"
|
|
||||||
data-showsubscribed="true"></span>
|
|
||||||
<div class="num-node text-center">3</div>
|
|
||||||
<div class="an-info">
|
|
||||||
<div class="an-info-group">
|
|
||||||
<div class="date-text">2025/05/13 更新</div>
|
|
||||||
<a href="/Home/Bangumi/3587" target="_blank" class="an-text"
|
|
||||||
title="魔女守护者">魔女守护者</a>
|
|
||||||
</div>
|
|
||||||
<div class="an-info-icon active js-subscribe_bangumi" data-subtitlegroupid="" data-bangumiid="3587"
|
|
||||||
data-toggle="tooltip" data-placement="bottom" title="取消"><i class="fa fa-rss "></i></div>
|
|
||||||
</div>
|
|
||||||
</li>
|
|
||||||
|
|
||||||
<li>
|
|
||||||
<span data-src="/images/Bangumi/202504/81ebb5a9.jpg?width=400&height=400&format=webp"
|
|
||||||
class="js-expand_bangumi b-lazy" data-bangumiid="3591" data-bangumiindex="4"
|
|
||||||
data-showsubscribed="true"></span>
|
|
||||||
<div class="num-node text-center">2</div>
|
|
||||||
<div class="an-info">
|
|
||||||
<div class="an-info-group">
|
|
||||||
<div class="date-text">2025/05/13 更新</div>
|
|
||||||
<a href="/Home/Bangumi/3591" target="_blank" class="an-text"
|
|
||||||
title="乡下大叔成为剑圣">乡下大叔成为剑圣</a>
|
|
||||||
</div>
|
|
||||||
<div class="an-info-icon active js-subscribe_bangumi" data-subtitlegroupid="" data-bangumiid="3591"
|
|
||||||
data-toggle="tooltip" data-placement="bottom" title="取消"><i class="fa fa-rss "></i></div>
|
|
||||||
</div>
|
|
||||||
</li>
|
|
||||||
|
|
||||||
<li>
|
|
||||||
<span data-src="/images/Bangumi/202504/d233a952.jpg?width=400&height=400&format=webp"
|
|
||||||
class="js-expand_bangumi b-lazy" data-bangumiid="3592" data-bangumiindex="5"
|
|
||||||
data-showsubscribed="true"></span>
|
|
||||||
<div class="num-node text-center">1</div>
|
|
||||||
<div class="an-info">
|
|
||||||
<div class="an-info-group">
|
|
||||||
<div class="date-text">2025/05/13 更新</div>
|
|
||||||
<a href="/Home/Bangumi/3592" target="_blank" class="an-text"
|
|
||||||
title="我是星际国家的恶德领主!">我是星际国家的恶德领主!</a>
|
|
||||||
</div>
|
|
||||||
<div class="an-info-icon active js-subscribe_bangumi" data-subtitlegroupid="" data-bangumiid="3592"
|
|
||||||
data-toggle="tooltip" data-placement="bottom" title="取消"><i class="fa fa-rss "></i></div>
|
|
||||||
</div>
|
|
||||||
</li>
|
|
||||||
</ul>
|
|
||||||
</div>
|
|
||||||
<div class="row an-res-row-frame" style="display:none;"></div>
|
|
||||||
<div class="mine an-box animated fadeIn">
|
|
||||||
<ul class="list-inline an-ul">
|
|
||||||
<li>
|
|
||||||
<span data-src="/images/Bangumi/202504/44e224ed.jpg?width=400&height=400&format=webp"
|
|
||||||
class="js-expand_bangumi b-lazy" data-bangumiid="3593" data-bangumiindex="1"
|
|
||||||
data-showsubscribed="true"></span>
|
|
||||||
<div class="an-info">
|
|
||||||
<div class="an-info-group">
|
|
||||||
<div class="date-text">2025/05/09 更新</div>
|
|
||||||
<a href="/Home/Bangumi/3593" target="_blank" class="an-text"
|
|
||||||
title="瞬间治疗却被视为无用而被流放的天才治疗师,以暗黑治疗师的身份幸福地生活着">瞬间治疗却被视为无用而被流放的天才治疗师,以暗黑治疗师的身份幸福地生活着</a>
|
|
||||||
</div>
|
|
||||||
<div class="an-info-icon active js-subscribe_bangumi" data-subtitlegroupid="" data-bangumiid="3593"
|
|
||||||
data-toggle="tooltip" data-placement="bottom" title="取消"><i class="fa fa-rss "></i></div>
|
|
||||||
</div>
|
|
||||||
</li>
|
|
||||||
|
|
||||||
<li>
|
|
||||||
<span data-src="/images/Bangumi/202504/85659a01.jpg?width=400&height=400&format=webp"
|
|
||||||
class="js-expand_bangumi b-lazy" data-bangumiid="3594" data-bangumiindex="2"
|
|
||||||
data-showsubscribed="true"></span>
|
|
||||||
<div class="num-node text-center">1</div>
|
|
||||||
<div class="an-info">
|
|
||||||
<div class="an-info-group">
|
|
||||||
<div class="date-text">2025/05/13 更新</div>
|
|
||||||
<a href="/Home/Bangumi/3594" target="_blank" class="an-text"
|
|
||||||
title="外星人姆姆">外星人姆姆</a>
|
|
||||||
</div>
|
|
||||||
<div class="an-info-icon active js-subscribe_bangumi" data-subtitlegroupid="" data-bangumiid="3594"
|
|
||||||
data-toggle="tooltip" data-placement="bottom" title="取消"><i class="fa fa-rss "></i></div>
|
|
||||||
</div>
|
|
||||||
</li>
|
|
||||||
|
|
||||||
<li>
|
|
||||||
<span data-src="/images/Bangumi/202504/95afd8e9.jpg?width=400&height=400&format=webp"
|
|
||||||
class="js-expand_bangumi b-lazy" data-bangumiid="3598" data-bangumiindex="3"
|
|
||||||
data-showsubscribed="true"></span>
|
|
||||||
<div class="num-node text-center">2</div>
|
|
||||||
<div class="an-info">
|
|
||||||
<div class="an-info-group">
|
|
||||||
<div class="date-text">2025/05/14 更新</div>
|
|
||||||
<a href="/Home/Bangumi/3598" target="_blank" class="an-text"
|
|
||||||
title="随兴旅-That's Journey-">随兴旅-That's
|
|
||||||
Journey-</a>
|
|
||||||
</div>
|
|
||||||
<div class="an-info-icon active js-subscribe_bangumi" data-subtitlegroupid="" data-bangumiid="3598"
|
|
||||||
data-toggle="tooltip" data-placement="bottom" title="取消"><i class="fa fa-rss "></i></div>
|
|
||||||
</div>
|
|
||||||
</li>
|
|
||||||
|
|
||||||
<li>
|
|
||||||
<span data-src="/images/Bangumi/202504/076c1094.jpg?width=400&height=400&format=webp"
|
|
||||||
class="js-expand_bangumi b-lazy" data-bangumiid="3599" data-bangumiindex="4"
|
|
||||||
data-showsubscribed="true"></span>
|
|
||||||
<div class="num-node text-center">1</div>
|
|
||||||
<div class="an-info">
|
|
||||||
<div class="an-info-group">
|
|
||||||
<div class="date-text">2025/05/13 更新</div>
|
|
||||||
<a href="/Home/Bangumi/3599" target="_blank" class="an-text"
|
|
||||||
title="夏日口袋">夏日口袋</a>
|
|
||||||
</div>
|
|
||||||
<div class="an-info-icon active js-subscribe_bangumi" data-subtitlegroupid="" data-bangumiid="3599"
|
|
||||||
data-toggle="tooltip" data-placement="bottom" title="取消"><i class="fa fa-rss "></i></div>
|
|
||||||
</div>
|
|
||||||
</li>
|
|
||||||
|
|
||||||
<li>
|
|
||||||
<span data-src="/images/Bangumi/202504/18802e7d.jpg?width=400&height=400&format=webp"
|
|
||||||
class="js-expand_bangumi b-lazy" data-bangumiid="3600" data-bangumiindex="5"
|
|
||||||
data-showsubscribed="true"></span>
|
|
||||||
<div class="num-node text-center">3</div>
|
|
||||||
<div class="an-info">
|
|
||||||
<div class="an-info-group">
|
|
||||||
<div class="date-text">2025/05/14 更新</div>
|
|
||||||
<a href="/Home/Bangumi/3600" target="_blank" class="an-text"
|
|
||||||
title="末日后酒店">末日后酒店</a>
|
|
||||||
</div>
|
|
||||||
<div class="an-info-icon active js-subscribe_bangumi" data-subtitlegroupid="" data-bangumiid="3600"
|
|
||||||
data-toggle="tooltip" data-placement="bottom" title="取消"><i class="fa fa-rss "></i></div>
|
|
||||||
</div>
|
|
||||||
</li>
|
|
||||||
</ul>
|
|
||||||
</div>
|
|
||||||
<div class="row an-res-row-frame" style="display:none;"></div>
|
|
||||||
<div class="mine an-box animated fadeIn">
|
|
||||||
<ul class="list-inline an-ul">
|
|
||||||
<li>
|
|
||||||
<span data-src="/images/Bangumi/202504/2e4096c4.jpg?width=400&height=400&format=webp"
|
|
||||||
class="js-expand_bangumi b-lazy" data-bangumiid="3601" data-bangumiindex="1"
|
|
||||||
data-showsubscribed="true"></span>
|
|
||||||
<div class="an-info">
|
|
||||||
<div class="an-info-group">
|
|
||||||
<div class="date-text">2025/05/13 更新</div>
|
|
||||||
<a href="/Home/Bangumi/3601" target="_blank" class="an-text"
|
|
||||||
title="中禅寺老师的灵怪讲义实录 老师会把谜题全都解开的。">中禅寺老师的灵怪讲义实录
|
|
||||||
老师会把谜题全都解开的。</a>
|
|
||||||
</div>
|
|
||||||
<div class="an-info-icon active js-subscribe_bangumi" data-subtitlegroupid="" data-bangumiid="3601"
|
|
||||||
data-toggle="tooltip" data-placement="bottom" title="取消"><i class="fa fa-rss "></i></div>
|
|
||||||
</div>
|
|
||||||
</li>
|
|
||||||
|
|
||||||
<li>
|
|
||||||
<span data-src="/images/Bangumi/202504/2e97d6d8.jpg?width=400&height=400&format=webp"
|
|
||||||
class="js-expand_bangumi b-lazy" data-bangumiid="3602" data-bangumiindex="2"
|
|
||||||
data-showsubscribed="true"></span>
|
|
||||||
<div class="num-node text-center">2</div>
|
|
||||||
<div class="an-info">
|
|
||||||
<div class="an-info-group">
|
|
||||||
<div class="date-text">2025/05/13 更新</div>
|
|
||||||
<a href="/Home/Bangumi/3602" target="_blank" class="an-text"
|
|
||||||
title="男女之间的友情存在吗?(不,不存在!!)">男女之间的友情存在吗?(不,不存在!!)</a>
|
|
||||||
</div>
|
|
||||||
<div class="an-info-icon active js-subscribe_bangumi" data-subtitlegroupid="" data-bangumiid="3602"
|
|
||||||
data-toggle="tooltip" data-placement="bottom" title="取消"><i class="fa fa-rss "></i></div>
|
|
||||||
</div>
|
|
||||||
</li>
|
|
||||||
|
|
||||||
<li>
|
|
||||||
<span data-src="/images/Bangumi/202504/e30f1861.jpg?width=400&height=400&format=webp"
|
|
||||||
class="js-expand_bangumi b-lazy" data-bangumiid="3603" data-bangumiindex="3"
|
|
||||||
data-showsubscribed="true"></span>
|
|
||||||
<div class="num-node text-center">1</div>
|
|
||||||
<div class="an-info">
|
|
||||||
<div class="an-info-group">
|
|
||||||
<div class="date-text">2025/05/13 更新</div>
|
|
||||||
<a href="/Home/Bangumi/3603" target="_blank" class="an-text"
|
|
||||||
title="拜托请穿上,鹰峰同学">拜托请穿上,鹰峰同学</a>
|
|
||||||
</div>
|
|
||||||
<div class="an-info-icon active js-subscribe_bangumi" data-subtitlegroupid="" data-bangumiid="3603"
|
|
||||||
data-toggle="tooltip" data-placement="bottom" title="取消"><i class="fa fa-rss "></i></div>
|
|
||||||
</div>
|
|
||||||
</li>
|
|
||||||
|
|
||||||
<li>
|
|
||||||
<span data-src="/images/Bangumi/202504/a2f98c24.jpg?width=400&height=400&format=webp"
|
|
||||||
class="js-expand_bangumi b-lazy" data-bangumiid="3604" data-bangumiindex="4"
|
|
||||||
data-showsubscribed="true"></span>
|
|
||||||
<div class="an-info">
|
|
||||||
<div class="an-info-group">
|
|
||||||
<div class="date-text">2025/05/12 更新</div>
|
|
||||||
<a href="/Home/Bangumi/3604" target="_blank" class="an-text"
|
|
||||||
title="赛马娘 芦毛灰姑娘">赛马娘
|
|
||||||
芦毛灰姑娘</a>
|
|
||||||
</div>
|
|
||||||
<div class="an-info-icon active js-subscribe_bangumi" data-subtitlegroupid="" data-bangumiid="3604"
|
|
||||||
data-toggle="tooltip" data-placement="bottom" title="取消"><i class="fa fa-rss "></i></div>
|
|
||||||
</div>
|
|
||||||
</li>
|
|
||||||
|
|
||||||
<li>
|
|
||||||
<span data-src="/images/Bangumi/202504/80d05785.jpg?width=400&height=400&format=webp"
|
|
||||||
class="js-expand_bangumi b-lazy" data-bangumiid="3605" data-bangumiindex="5"
|
|
||||||
data-showsubscribed="true"></span>
|
|
||||||
<div class="num-node text-center">1</div>
|
|
||||||
<div class="an-info">
|
|
||||||
<div class="an-info-group">
|
|
||||||
<div class="date-text">2025/05/13 更新</div>
|
|
||||||
<a href="/Home/Bangumi/3605" target="_blank" class="an-text"
|
|
||||||
title="推理要在晚餐后">推理要在晚餐后</a>
|
|
||||||
</div>
|
|
||||||
<div class="an-info-icon active js-subscribe_bangumi" data-subtitlegroupid="" data-bangumiid="3605"
|
|
||||||
data-toggle="tooltip" data-placement="bottom" title="取消"><i class="fa fa-rss "></i></div>
|
|
||||||
</div>
|
|
||||||
</li>
|
|
||||||
</ul>
|
|
||||||
</div>
|
|
||||||
<div class="row an-res-row-frame" style="display:none;"></div>
|
|
||||||
<div class="mine an-box animated fadeIn">
|
|
||||||
<ul class="list-inline an-ul">
|
|
||||||
<li>
|
|
||||||
<span data-src="/images/Bangumi/202504/6f86415a.jpg?width=400&height=400&format=webp"
|
|
||||||
class="js-expand_bangumi b-lazy" data-bangumiid="3606" data-bangumiindex="1"
|
|
||||||
data-showsubscribed="true"></span>
|
|
||||||
<div class="num-node text-center">1</div>
|
|
||||||
<div class="an-info">
|
|
||||||
<div class="an-info-group">
|
|
||||||
<div class="date-text">2025/05/13 更新</div>
|
|
||||||
<a href="/Home/Bangumi/3606" target="_blank" class="an-text"
|
|
||||||
title="摇滚乃是淑女的爱好">摇滚乃是淑女的爱好</a>
|
|
||||||
</div>
|
|
||||||
<div class="an-info-icon active js-subscribe_bangumi" data-subtitlegroupid="" data-bangumiid="3606"
|
|
||||||
data-toggle="tooltip" data-placement="bottom" title="取消"><i class="fa fa-rss "></i></div>
|
|
||||||
</div>
|
|
||||||
</li>
|
|
||||||
|
|
||||||
<li>
|
|
||||||
<span data-src="/images/Bangumi/202504/d731d60f.jpg?width=400&height=400&format=webp"
|
|
||||||
class="js-expand_bangumi b-lazy" data-bangumiid="3607" data-bangumiindex="2"
|
|
||||||
data-showsubscribed="true"></span>
|
|
||||||
<div class="num-node text-center">2</div>
|
|
||||||
<div class="an-info">
|
|
||||||
<div class="an-info-group">
|
|
||||||
<div class="date-text">2025/05/13 更新</div>
|
|
||||||
<a href="/Home/Bangumi/3607" target="_blank" class="an-text"
|
|
||||||
title="九龙大众浪漫">九龙大众浪漫</a>
|
|
||||||
</div>
|
|
||||||
<div class="an-info-icon active js-subscribe_bangumi" data-subtitlegroupid="" data-bangumiid="3607"
|
|
||||||
data-toggle="tooltip" data-placement="bottom" title="取消"><i class="fa fa-rss "></i></div>
|
|
||||||
</div>
|
|
||||||
</li>
|
|
||||||
|
|
||||||
<li>
|
|
||||||
<span data-src="/images/Bangumi/202504/0bf31724.jpg?width=400&height=400&format=webp"
|
|
||||||
class="js-expand_bangumi b-lazy" data-bangumiid="3609" data-bangumiindex="3"
|
|
||||||
data-showsubscribed="true"></span>
|
|
||||||
<div class="an-info">
|
|
||||||
<div class="an-info-group">
|
|
||||||
<div class="date-text">2025/05/10 更新</div>
|
|
||||||
<a href="/Home/Bangumi/3609" target="_blank" class="an-text"
|
|
||||||
title="圣女因太过完美不够可爱而被废除婚约并卖到邻国">圣女因太过完美不够可爱而被废除婚约并卖到邻国</a>
|
|
||||||
</div>
|
|
||||||
<div class="an-info-icon active js-subscribe_bangumi" data-subtitlegroupid="" data-bangumiid="3609"
|
|
||||||
data-toggle="tooltip" data-placement="bottom" title="取消"><i class="fa fa-rss "></i></div>
|
|
||||||
</div>
|
|
||||||
</li>
|
|
||||||
|
|
||||||
<li>
|
|
||||||
<span data-src="/images/Bangumi/202504/b41afc59.jpg?width=400&height=400&format=webp"
|
|
||||||
class="js-expand_bangumi b-lazy" data-bangumiid="3611" data-bangumiindex="4"
|
|
||||||
data-showsubscribed="true"></span>
|
|
||||||
<div class="an-info">
|
|
||||||
<div class="an-info-group">
|
|
||||||
<div class="date-text">2025/05/11 更新</div>
|
|
||||||
<a href="/Home/Bangumi/3611" target="_blank" class="an-text"
|
|
||||||
title="炎炎消防队 叁之章">炎炎消防队
|
|
||||||
叁之章</a>
|
|
||||||
</div>
|
|
||||||
<div class="an-info-icon active js-subscribe_bangumi" data-subtitlegroupid="" data-bangumiid="3611"
|
|
||||||
data-toggle="tooltip" data-placement="bottom" title="取消"><i class="fa fa-rss "></i></div>
|
|
||||||
</div>
|
|
||||||
</li>
|
|
||||||
|
|
||||||
<li>
|
|
||||||
<span data-src="/images/Bangumi/202504/fc6eaf43.jpg?width=400&height=400&format=webp"
|
|
||||||
class="js-expand_bangumi b-lazy" data-bangumiid="3612" data-bangumiindex="5"
|
|
||||||
data-showsubscribed="true"></span>
|
|
||||||
<div class="an-info">
|
|
||||||
<div class="an-info-group">
|
|
||||||
<div class="date-text">2025/05/12 更新</div>
|
|
||||||
<a href="/Home/Bangumi/3612" target="_blank" class="an-text"
|
|
||||||
title="测不准的阿波连同学 第二季">测不准的阿波连同学
|
|
||||||
第二季</a>
|
|
||||||
</div>
|
|
||||||
<div class="an-info-icon active js-subscribe_bangumi" data-subtitlegroupid="" data-bangumiid="3612"
|
|
||||||
data-toggle="tooltip" data-placement="bottom" title="取消"><i class="fa fa-rss "></i></div>
|
|
||||||
</div>
|
|
||||||
</li>
|
|
||||||
</ul>
|
|
||||||
</div>
|
|
||||||
<div class="row an-res-row-frame" style="display:none;"></div>
|
|
||||||
<div class="mine an-box animated fadeIn">
|
|
||||||
<ul class="list-inline an-ul">
|
|
||||||
<li>
|
|
||||||
<span data-src="/images/Bangumi/202504/3c0bd08c.jpg?width=400&height=400&format=webp"
|
|
||||||
class="js-expand_bangumi b-lazy" data-bangumiid="3613" data-bangumiindex="1"
|
|
||||||
data-showsubscribed="true"></span>
|
|
||||||
<div class="num-node text-center">1</div>
|
|
||||||
<div class="an-info">
|
|
||||||
<div class="an-info-group">
|
|
||||||
<div class="date-text">2025/05/14 更新</div>
|
|
||||||
<a href="/Home/Bangumi/3613" target="_blank" class="an-text"
|
|
||||||
title="阳光马达棒球场!">阳光马达棒球场!</a>
|
|
||||||
</div>
|
|
||||||
<div class="an-info-icon active js-subscribe_bangumi" data-subtitlegroupid="" data-bangumiid="3613"
|
|
||||||
data-toggle="tooltip" data-placement="bottom" title="取消"><i class="fa fa-rss "></i></div>
|
|
||||||
</div>
|
|
||||||
</li>
|
|
||||||
|
|
||||||
<li>
|
|
||||||
<span data-src="/images/Bangumi/202504/18f01f9e.jpg?width=400&height=400&format=webp"
|
|
||||||
class="js-expand_bangumi b-lazy" data-bangumiid="3614" data-bangumiindex="2"
|
|
||||||
data-showsubscribed="true"></span>
|
|
||||||
<div class="num-node text-center">14</div>
|
|
||||||
<div class="an-info">
|
|
||||||
<div class="an-info-group">
|
|
||||||
<div class="date-text">2025/05/14 更新</div>
|
|
||||||
<a href="/Home/Bangumi/3614" target="_blank" class="an-text"
|
|
||||||
title="紫云寺家的兄弟姐妹">紫云寺家的兄弟姐妹</a>
|
|
||||||
</div>
|
|
||||||
<div class="an-info-icon active js-subscribe_bangumi" data-subtitlegroupid="" data-bangumiid="3614"
|
|
||||||
data-toggle="tooltip" data-placement="bottom" title="取消"><i class="fa fa-rss "></i></div>
|
|
||||||
</div>
|
|
||||||
</li>
|
|
||||||
|
|
||||||
<li>
|
|
||||||
<span data-src="/images/Bangumi/202504/0c4c9df3.jpg?width=400&height=400&format=webp"
|
|
||||||
class="js-expand_bangumi b-lazy" data-bangumiid="3615" data-bangumiindex="3"
|
|
||||||
data-showsubscribed="true"></span>
|
|
||||||
<div class="an-info">
|
|
||||||
<div class="an-info-group">
|
|
||||||
<div class="date-text">2025/05/11 更新</div>
|
|
||||||
<a href="/Home/Bangumi/3615" target="_blank" class="an-text"
|
|
||||||
title="终末起点">终末起点</a>
|
|
||||||
</div>
|
|
||||||
<div class="an-info-icon active js-subscribe_bangumi" data-subtitlegroupid="" data-bangumiid="3615"
|
|
||||||
data-toggle="tooltip" data-placement="bottom" title="取消"><i class="fa fa-rss "></i></div>
|
|
||||||
</div>
|
|
||||||
</li>
|
|
||||||
|
|
||||||
<li>
|
|
||||||
<span data-src="/images/Bangumi/202504/096c19d0.jpg?width=400&height=400&format=webp"
|
|
||||||
class="js-expand_bangumi b-lazy" data-bangumiid="3617" data-bangumiindex="4"
|
|
||||||
data-showsubscribed="true"></span>
|
|
||||||
<div class="an-info">
|
|
||||||
<div class="an-info-group">
|
|
||||||
<div class="date-text">2025/05/12 更新</div>
|
|
||||||
<a href="/Home/Bangumi/3617" target="_blank" class="an-text"
|
|
||||||
title="爱有些沉重的黑暗精灵从异世界追过来了">爱有些沉重的黑暗精灵从异世界追过来了</a>
|
|
||||||
</div>
|
|
||||||
<div class="an-info-icon active js-subscribe_bangumi" data-subtitlegroupid="" data-bangumiid="3617"
|
|
||||||
data-toggle="tooltip" data-placement="bottom" title="取消"><i class="fa fa-rss "></i></div>
|
|
||||||
</div>
|
|
||||||
</li>
|
|
||||||
|
|
||||||
<li>
|
|
||||||
<span data-src="/images/Bangumi/202504/143326f9.jpg?width=400&height=400&format=webp"
|
|
||||||
class="js-expand_bangumi b-lazy" data-bangumiid="3618" data-bangumiindex="5"
|
|
||||||
data-showsubscribed="true"></span>
|
|
||||||
<div class="an-info">
|
|
||||||
<div class="an-info-group">
|
|
||||||
<div class="date-text">2025/05/10 更新</div>
|
|
||||||
<a href="/Home/Bangumi/3618" target="_blank" class="an-text"
|
|
||||||
title="神统记">神统记</a>
|
|
||||||
</div>
|
|
||||||
<div class="an-info-icon active js-subscribe_bangumi" data-subtitlegroupid="" data-bangumiid="3618"
|
|
||||||
data-toggle="tooltip" data-placement="bottom" title="取消"><i class="fa fa-rss "></i></div>
|
|
||||||
</div>
|
|
||||||
</li>
|
|
||||||
</ul>
|
|
||||||
</div>
|
|
||||||
<div class="row an-res-row-frame" style="display:none;"></div>
|
|
||||||
<div class="mine an-box animated fadeIn">
|
|
||||||
<ul class="list-inline an-ul">
|
|
||||||
<li>
|
|
||||||
<span data-src="/images/Bangumi/202504/c59f3187.jpg?width=400&height=400&format=webp"
|
|
||||||
class="js-expand_bangumi b-lazy" data-bangumiid="3620" data-bangumiindex="1"
|
|
||||||
data-showsubscribed="true"></span>
|
|
||||||
<div class="num-node text-center">1</div>
|
|
||||||
<div class="an-info">
|
|
||||||
<div class="an-info-group">
|
|
||||||
<div class="date-text">2025/05/13 更新</div>
|
|
||||||
<a href="/Home/Bangumi/3620" target="_blank" class="an-text"
|
|
||||||
title="记忆缝线">记忆缝线</a>
|
|
||||||
</div>
|
|
||||||
<div class="an-info-icon active js-subscribe_bangumi" data-subtitlegroupid="" data-bangumiid="3620"
|
|
||||||
data-toggle="tooltip" data-placement="bottom" title="取消"><i class="fa fa-rss "></i></div>
|
|
||||||
</div>
|
|
||||||
</li>
|
|
||||||
|
|
||||||
<li>
|
|
||||||
<span data-src="/images/Bangumi/202504/2b2ea518.jpg?width=400&height=400&format=webp"
|
|
||||||
class="js-expand_bangumi b-lazy" data-bangumiid="3621" data-bangumiindex="2"
|
|
||||||
data-showsubscribed="true"></span>
|
|
||||||
<div class="an-info">
|
|
||||||
<div class="an-info-group">
|
|
||||||
<div class="date-text">2025/05/12 更新</div>
|
|
||||||
<a href="/Home/Bangumi/3621" target="_blank" class="an-text"
|
|
||||||
title="快藏起来!玛琪娜同学!!">快藏起来!玛琪娜同学!!</a>
|
|
||||||
</div>
|
|
||||||
<div class="an-info-icon active js-subscribe_bangumi" data-subtitlegroupid="" data-bangumiid="3621"
|
|
||||||
data-toggle="tooltip" data-placement="bottom" title="取消"><i class="fa fa-rss "></i></div>
|
|
||||||
</div>
|
|
||||||
</li>
|
|
||||||
|
|
||||||
<li>
|
|
||||||
<span data-src="/images/Bangumi/202504/fddd0557.jpg?width=400&height=400&format=webp"
|
|
||||||
class="js-expand_bangumi b-lazy" data-bangumiid="3623" data-bangumiindex="3"
|
|
||||||
data-showsubscribed="true"></span>
|
|
||||||
<div class="num-node text-center">1</div>
|
|
||||||
<div class="an-info">
|
|
||||||
<div class="an-info-group">
|
|
||||||
<div class="date-text">2025/05/13 更新</div>
|
|
||||||
<a href="/Home/Bangumi/3623" target="_blank" class="an-text"
|
|
||||||
title="时光流逝,饭菜依旧美味">时光流逝,饭菜依旧美味</a>
|
|
||||||
</div>
|
|
||||||
<div class="an-info-icon active js-subscribe_bangumi" data-subtitlegroupid="" data-bangumiid="3623"
|
|
||||||
data-toggle="tooltip" data-placement="bottom" title="取消"><i class="fa fa-rss "></i></div>
|
|
||||||
</div>
|
|
||||||
</li>
|
|
||||||
|
|
||||||
<li>
|
|
||||||
<span data-src="/images/Bangumi/202504/cf6e83ba.jpg?width=400&height=400&format=webp"
|
|
||||||
class="js-expand_bangumi b-lazy" data-bangumiid="3624" data-bangumiindex="4"
|
|
||||||
data-showsubscribed="true"></span>
|
|
||||||
<div class="an-info">
|
|
||||||
<div class="an-info-group">
|
|
||||||
<div class="date-text">2025/05/13 更新</div>
|
|
||||||
<a href="/Home/Bangumi/3624" target="_blank" class="an-text"
|
|
||||||
title="拉撒路">拉撒路</a>
|
|
||||||
</div>
|
|
||||||
<div class="an-info-icon active js-subscribe_bangumi" data-subtitlegroupid="" data-bangumiid="3624"
|
|
||||||
data-toggle="tooltip" data-placement="bottom" title="取消"><i class="fa fa-rss "></i></div>
|
|
||||||
</div>
|
|
||||||
</li>
|
|
||||||
|
|
||||||
<li>
|
|
||||||
<span data-src="/images/Bangumi/202504/17a31d67.jpg?width=400&height=400&format=webp"
|
|
||||||
class="js-expand_bangumi b-lazy" data-bangumiid="3627" data-bangumiindex="5"
|
|
||||||
data-showsubscribed="true"></span>
|
|
||||||
<div class="num-node text-center">1</div>
|
|
||||||
<div class="an-info">
|
|
||||||
<div class="an-info-group">
|
|
||||||
<div class="date-text">2025/05/13 更新</div>
|
|
||||||
<a href="/Home/Bangumi/3627" target="_blank" class="an-text"
|
|
||||||
title="搞笑漫画日和GO">搞笑漫画日和GO</a>
|
|
||||||
</div>
|
|
||||||
<div class="an-info-icon active js-subscribe_bangumi" data-subtitlegroupid="" data-bangumiid="3627"
|
|
||||||
data-toggle="tooltip" data-placement="bottom" title="取消"><i class="fa fa-rss "></i></div>
|
|
||||||
</div>
|
|
||||||
</li>
|
|
||||||
</ul>
|
|
||||||
</div>
|
|
||||||
<div class="row an-res-row-frame" style="display:none;"></div>
|
|
||||||
<div class="mine an-box animated fadeIn">
|
|
||||||
<ul class="list-inline an-ul">
|
|
||||||
<li>
|
|
||||||
<span data-src="/images/Bangumi/202504/ca36fa9e.jpg?width=400&height=400&format=webp"
|
|
||||||
class="js-expand_bangumi b-lazy" data-bangumiid="3628" data-bangumiindex="1"
|
|
||||||
data-showsubscribed="true"></span>
|
|
||||||
<div class="an-info">
|
|
||||||
<div class="an-info-group">
|
|
||||||
<div class="date-text">2025/05/12 更新</div>
|
|
||||||
<a href="/Home/Bangumi/3628" target="_blank" class="an-text"
|
|
||||||
title="mono女孩">mono女孩</a>
|
|
||||||
</div>
|
|
||||||
<div class="an-info-icon active js-subscribe_bangumi" data-subtitlegroupid="" data-bangumiid="3628"
|
|
||||||
data-toggle="tooltip" data-placement="bottom" title="取消"><i class="fa fa-rss "></i></div>
|
|
||||||
</div>
|
|
||||||
</li>
|
|
||||||
|
|
||||||
<li>
|
|
||||||
<span data-src="/images/Bangumi/202504/af9a6139.jpg?width=400&height=400&format=webp"
|
|
||||||
class="js-expand_bangumi b-lazy" data-bangumiid="3632" data-bangumiindex="2"
|
|
||||||
data-showsubscribed="true"></span>
|
|
||||||
<div class="an-info">
|
|
||||||
<div class="an-info-group">
|
|
||||||
<div class="date-text">2025/05/11 更新</div>
|
|
||||||
<a href="/Home/Bangumi/3632" target="_blank" class="an-text"
|
|
||||||
title="干杂活我乃最强~关于原英雄队伍的杂役人员,实际上除了战斗能力外全是SSS的故事~">干杂活我乃最强~关于原英雄队伍的杂役人员,实际上除了战斗能力外全是SSS的故事~</a>
|
|
||||||
</div>
|
|
||||||
<div class="an-info-icon active js-subscribe_bangumi" data-subtitlegroupid="" data-bangumiid="3632"
|
|
||||||
data-toggle="tooltip" data-placement="bottom" title="取消"><i class="fa fa-rss "></i></div>
|
|
||||||
</div>
|
|
||||||
</li>
|
|
||||||
|
|
||||||
<li>
|
|
||||||
<span data-src="/images/Bangumi/202504/7ea16a61.jpg?width=400&height=400&format=webp"
|
|
||||||
class="js-expand_bangumi b-lazy" data-bangumiid="3633" data-bangumiindex="3"
|
|
||||||
data-showsubscribed="true"></span>
|
|
||||||
<div class="an-info">
|
|
||||||
<div class="an-info-group">
|
|
||||||
<div class="date-text">2025/04/03 更新</div>
|
|
||||||
<a href="/Home/Bangumi/3633" target="_blank" class="an-text"
|
|
||||||
title="进入花园">进入花园</a>
|
|
||||||
</div>
|
|
||||||
<div class="an-info-icon active js-subscribe_bangumi" data-subtitlegroupid="" data-bangumiid="3633"
|
|
||||||
data-toggle="tooltip" data-placement="bottom" title="取消"><i class="fa fa-rss "></i></div>
|
|
||||||
</div>
|
|
||||||
</li>
|
|
||||||
|
|
||||||
<li>
|
|
||||||
<span data-src="/images/Bangumi/202504/3400d7c6.jpg?width=400&height=400&format=webp"
|
|
||||||
class="js-expand_bangumi b-lazy" data-bangumiid="3635" data-bangumiindex="4"
|
|
||||||
data-showsubscribed="true"></span>
|
|
||||||
<div class="num-node text-center">3</div>
|
|
||||||
<div class="an-info">
|
|
||||||
<div class="an-info-group">
|
|
||||||
<div class="date-text">2025/05/13 更新</div>
|
|
||||||
<a href="/Home/Bangumi/3635" target="_blank" class="an-text"
|
|
||||||
title="小市民系列 第二季">小市民系列
|
|
||||||
第二季</a>
|
|
||||||
</div>
|
|
||||||
<div class="an-info-icon active js-subscribe_bangumi" data-subtitlegroupid="" data-bangumiid="3635"
|
|
||||||
data-toggle="tooltip" data-placement="bottom" title="取消"><i class="fa fa-rss "></i></div>
|
|
||||||
</div>
|
|
||||||
</li>
|
|
||||||
|
|
||||||
<li>
|
|
||||||
<span data-src="/images/Bangumi/202504/62a30c3f.jpg?width=400&height=400&format=webp"
|
|
||||||
class="js-expand_bangumi b-lazy" data-bangumiid="3636" data-bangumiindex="5"
|
|
||||||
data-showsubscribed="true"></span>
|
|
||||||
<div class="an-info">
|
|
||||||
<div class="an-info-group">
|
|
||||||
<div class="date-text">2025/04/05 更新</div>
|
|
||||||
<a href="/Home/Bangumi/3636" target="_blank" class="an-text"
|
|
||||||
title="The Star Seekers 星を追う少年たち">The
|
|
||||||
Star Seekers 星を追う少年たち</a>
|
|
||||||
</div>
|
|
||||||
<div class="an-info-icon active js-subscribe_bangumi" data-subtitlegroupid="" data-bangumiid="3636"
|
|
||||||
data-toggle="tooltip" data-placement="bottom" title="取消"><i class="fa fa-rss "></i></div>
|
|
||||||
</div>
|
|
||||||
</li>
|
|
||||||
</ul>
|
|
||||||
</div>
|
|
||||||
<div class="row an-res-row-frame" style="display:none;"></div>
|
|
||||||
<div class="mine an-box animated fadeIn">
|
|
||||||
<ul class="list-inline an-ul">
|
|
||||||
<li>
|
|
||||||
<span data-src="/images/Bangumi/202504/ffce0184.jpg?width=400&height=400&format=webp"
|
|
||||||
class="js-expand_bangumi b-lazy" data-bangumiid="3641" data-bangumiindex="1"
|
|
||||||
data-showsubscribed="true"></span>
|
|
||||||
<div class="num-node text-center">1</div>
|
|
||||||
<div class="an-info">
|
|
||||||
<div class="an-info-group">
|
|
||||||
<div class="date-text">2025/05/13 更新</div>
|
|
||||||
<a href="/Home/Bangumi/3641" target="_blank" class="an-text"
|
|
||||||
title="再见,地球 第二季">再见,地球
|
|
||||||
第二季</a>
|
|
||||||
</div>
|
|
||||||
<div class="an-info-icon active js-subscribe_bangumi" data-subtitlegroupid="" data-bangumiid="3641"
|
|
||||||
data-toggle="tooltip" data-placement="bottom" title="取消"><i class="fa fa-rss "></i></div>
|
|
||||||
</div>
|
|
||||||
</li>
|
|
||||||
|
|
||||||
<li>
|
|
||||||
<span data-src="/images/Bangumi/202504/716c2ff5.jpg?width=400&height=400&format=webp"
|
|
||||||
class="js-expand_bangumi b-lazy" data-bangumiid="3642" data-bangumiindex="2"
|
|
||||||
data-showsubscribed="true"></span>
|
|
||||||
<div class="an-info">
|
|
||||||
<div class="an-info-group">
|
|
||||||
<div class="date-text">2025/05/11 更新</div>
|
|
||||||
<a href="/Home/Bangumi/3642" target="_blank" class="an-text"
|
|
||||||
title="每日男公关">每日男公关</a>
|
|
||||||
</div>
|
|
||||||
<div class="an-info-icon active js-subscribe_bangumi" data-subtitlegroupid="" data-bangumiid="3642"
|
|
||||||
data-toggle="tooltip" data-placement="bottom" title="取消"><i class="fa fa-rss "></i></div>
|
|
||||||
</div>
|
|
||||||
</li>
|
|
||||||
|
|
||||||
<li>
|
|
||||||
<span data-src="/images/Bangumi/202504/052595b4.jpg?width=400&height=400&format=webp"
|
|
||||||
class="js-expand_bangumi b-lazy" data-bangumiid="3644" data-bangumiindex="3"
|
|
||||||
data-showsubscribed="true"></span>
|
|
||||||
<div class="an-info">
|
|
||||||
<div class="an-info-group">
|
|
||||||
<div class="date-text">2025/05/11 更新</div>
|
|
||||||
<a href="/Home/Bangumi/3644" target="_blank" class="an-text"
|
|
||||||
title="莉可丽丝:友谊是时间的窃贼">莉可丽丝:友谊是时间的窃贼</a>
|
|
||||||
</div>
|
|
||||||
<div class="an-info-icon active js-subscribe_bangumi" data-subtitlegroupid="" data-bangumiid="3644"
|
|
||||||
data-toggle="tooltip" data-placement="bottom" title="取消"><i class="fa fa-rss "></i></div>
|
|
||||||
</div>
|
|
||||||
</li>
|
|
||||||
|
|
||||||
<li>
|
|
||||||
<span data-src="/images/Bangumi/202504/be7f8aef.jpg?width=400&height=400&format=webp"
|
|
||||||
class="js-expand_bangumi b-lazy" data-bangumiid="3646" data-bangumiindex="4"
|
|
||||||
data-showsubscribed="true"></span>
|
|
||||||
<div class="an-info">
|
|
||||||
<div class="an-info-group">
|
|
||||||
<div class="date-text">2025/05/12 更新</div>
|
|
||||||
<a href="/Home/Bangumi/3646" target="_blank" class="an-text"
|
|
||||||
title="战队大失格 第二季">战队大失格
|
|
||||||
第二季</a>
|
|
||||||
</div>
|
|
||||||
<div class="an-info-icon active js-subscribe_bangumi" data-subtitlegroupid="" data-bangumiid="3646"
|
|
||||||
data-toggle="tooltip" data-placement="bottom" title="取消"><i class="fa fa-rss "></i></div>
|
|
||||||
</div>
|
|
||||||
</li>
|
|
||||||
</ul>
|
|
||||||
</div>
|
|
||||||
<div class="row an-res-row-frame" style="display:none;"></div>
|
|
||||||
</div>
|
|
||||||
|
Before Width: | Height: | Size: 54 KiB |
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
1466
apps/recorder/tests/resources/mikan/ExpandBangumi.htm
Normal file
1466
apps/recorder/tests/resources/mikan/ExpandBangumi.htm
Normal file
File diff suppressed because it is too large
Load Diff
641
apps/recorder/tests/resources/mikan/MyBangumi-noauth.htm
Normal file
641
apps/recorder/tests/resources/mikan/MyBangumi-noauth.htm
Normal file
@ -0,0 +1,641 @@
|
|||||||
|
<!DOCTYPE html PUBLIC "-//W3C//DTD HTML 4.01 Transitional//EN" "http://www.w3.org/TR/html4/loose.dtd">
|
||||||
|
<html>
|
||||||
|
<head>
|
||||||
|
|
||||||
|
<meta http-equiv="Content-Type" content="text/html; charset=UTF-8">
|
||||||
|
<meta name="keywords" content="新番,动漫,动漫下載,新番下载,animation,bangumi,动画,蜜柑计划,Mikan Project" />
|
||||||
|
<meta name="description" content="蜜柑计划:新一代的动漫下载站" />
|
||||||
|
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||||
|
|
||||||
|
<!-- 若用户有Google Chrome Frame,那么ie浏览时让IE使用chrome内核 -->
|
||||||
|
<meta http-equiv="X-UA-Compatible" content="IE=edge,chrome=1" />
|
||||||
|
|
||||||
|
<!-- 若是双核浏览器,默认webkit渲染(chrome) -->
|
||||||
|
<meta name="renderer" content="webkit">
|
||||||
|
<title>Mikan Project - 我的番组</title>
|
||||||
|
|
||||||
|
<!-- here put import css lib -->
|
||||||
|
|
||||||
|
|
||||||
|
<link rel="stylesheet" href="/lib/bootstrap/dist/css/bootstrap.min.css?v=7s5uDGW3AHqw6xtJmNNtr-OBRJUlgkNJEo78P4b0yRw" />
|
||||||
|
<link rel="stylesheet" href="/lib/font-awesome/css/font-awesome.min.css?v=3dkvEK0WLHRJ7_Csr0BZjAWxERc5WH7bdeUya2aXxdU" />
|
||||||
|
<link rel="stylesheet" href="/css/thirdparty.min.css?v=c2SZy6n-55iljz60XCAALXejEZvjc43kgwamU5DAYUU" />
|
||||||
|
<link rel="stylesheet" href="/css/animate.min.css?v=w_eXqGX0NdMPQ0LZNhdQ8B-DQMYAxelvLoIP39dzmus" />
|
||||||
|
<link rel="stylesheet" href="/css/mikan.min.css?v=aupBMgBgKRB5chTb5fl8lvHpN3OqX67_gKg3lXZewRw" />
|
||||||
|
|
||||||
|
<script src="/lib/jquery/dist/jquery.min.js?v=BbhdlvQf_xTY9gja0Dq3HiwQF8LaCRTXxZKRutelT44"></script>
|
||||||
|
<script src="/lib/bootstrap/dist/js/bootstrap.min.js?v=KXn5puMvxCw-dAYznun-drMdG1IFl3agK0p_pqT9KAo"></script>
|
||||||
|
<script src="/js/thirdparty.min.js?v=NsK_w5fw7Nm4ZPm4eZDgsivasZNgT6ArhIjmj-bRnR0"></script>
|
||||||
|
<script src="/js/darkreader.min.js?v=Lr_8XODLEDSPtT6LqaeLKzREs4jocJUzV8HvQPItIic"></script>
|
||||||
|
<script src="/js/ScrollMagic.min.js?v=1xuIM3UJWEZX_wWN9zrA8W7CWukfsMaEqb759CeHo3U"></script>
|
||||||
|
<script src="/js/jquery.ScrollMagic.min.js?v=SyygQh9gWWfvyS13QwI0SKGAQyHDachlaigiK4X59iw"></script>
|
||||||
|
|
||||||
|
|
||||||
|
<link rel="icon" href="/images/favicon.ico?v=2" />
|
||||||
|
<link rel="apple-touch-icon" href="\Images\apple-touch-icon.png">
|
||||||
|
<link rel="apple-touch-icon" sizes="152x152" href="\Images\apple-touch-icon-152x152.png">
|
||||||
|
<link rel="apple-touch-icon" sizes="180x180" href="\Images\apple-touch-icon-180x180.png">
|
||||||
|
<link rel="apple-touch-icon" sizes="144x144" href="\Images\apple-touch-icon-144x144.png">
|
||||||
|
|
||||||
|
<script>
|
||||||
|
(function (i, s, o, g, r, a, m) {
|
||||||
|
i['GoogleAnalyticsObject'] = r; i[r] = i[r] || function () {
|
||||||
|
(i[r].q = i[r].q || []).push(arguments)
|
||||||
|
}, i[r].l = 1 * new Date(); a = s.createElement(o),
|
||||||
|
m = s.getElementsByTagName(o)[0]; a.async = 1; a.src = g; m.parentNode.insertBefore(a, m)
|
||||||
|
})(window, document, 'script', '//www.google-analytics.com/analytics.js', 'ga');
|
||||||
|
|
||||||
|
ga('create', 'UA-8911610-8', 'auto');
|
||||||
|
ga('send', 'pageview');
|
||||||
|
</script>
|
||||||
|
</head>
|
||||||
|
<body class="main">
|
||||||
|
<div id="sk-header" class="hidden-xs hidden-sm">
|
||||||
|
<div id="sk-top-nav" class="container">
|
||||||
|
<a id="logo" href="/" style="width:205px;"><img id="mikan-pic" src="/images/mikan-pic.png" /><img src="/images/mikan-text.svg" style="height:30px;" /></a>
|
||||||
|
<div id="nav-list">
|
||||||
|
<ul class="list-inline nav-ul">
|
||||||
|
<li class="">
|
||||||
|
<div class="sk-col"><a href="/"><i class="fa fa-home fa-lg"></i>主页</a></div>
|
||||||
|
</li>
|
||||||
|
<li class="active">
|
||||||
|
<div class="sk-col"><a href="/Home/MyBangumi"><i class="fa fa-rss fa-lg"></i>订阅</a></div>
|
||||||
|
</li>
|
||||||
|
<li class="">
|
||||||
|
<div class="sk-col"><a href="/Home/Classic"><i class="fa fa-slack fa-lg"></i>列表</a></div>
|
||||||
|
</li>
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
<div class="search-form">
|
||||||
|
<form method="get" action="/Home/Search">
|
||||||
|
<div class="form-group has-feedback">
|
||||||
|
<label for="search" class="sr-only">搜索</label>
|
||||||
|
<input type="text" class="form-control input-sm" name="searchstr" id="header-search" placeholder="搜索">
|
||||||
|
<span class="glyphicon glyphicon-search form-control-feedback"></span>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
<section id="login">
|
||||||
|
<div id="user-login" class="pull-right">
|
||||||
|
<a href="/Account/Register" class="text-right">注册</a>
|
||||||
|
<a onclick="ToggleActive(this)" class="text-right" data-toggle="popover-x" data-target="#login-popover" data-placement="bottom bottom-right" rel="popover">登录</a>
|
||||||
|
<form action="/Account/Login?ReturnUrl=%2FHome%2FMyBangumi" class="form-vertical" method="post"> <div id="login-popover" class="popover popover-default">
|
||||||
|
<div class="arrow"></div>
|
||||||
|
<div id="login-popover-conent">
|
||||||
|
<div id="login-popover-input">
|
||||||
|
<div id="login-popover-div-username">
|
||||||
|
<img src="/images/user-name_login_icon.png" />
|
||||||
|
<input type="text" placeholder="用户名" id="login-popover-input-username" name="UserName" />
|
||||||
|
</div>
|
||||||
|
<div id="login-popover-div-password">
|
||||||
|
<img src="/images/password_login_icon.png" style="margin-left:3px;" />
|
||||||
|
<input type="password" placeholder="密码" id="login-popover-input-password" name="Password" />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<button id="login-popover-submit" type="submit" class="btn">登 录</button>
|
||||||
|
<div class="checkbox" id="login-popover-password">
|
||||||
|
<label id="login-popover-remember-password"><input type="checkbox" value="true" name="RememberMe"><input type="hidden" value="false" name="RememberMe">记住密码</label>
|
||||||
|
<div id="login-popover-forget-password" class="pull-right"><a href="/Account/ForgotPassword" class="forget-password">忘记密码</a></div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<a id="login-popover-create-account">还没有账号?赶紧来注册一个吧~</a>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<input name="__RequestVerificationToken" type="hidden" value="CfDJ8MyNMqFNaC9JmJW13PvY-93GWOqu6SxfgR0S2fsWVSk41_SQEknnNNxKBZ6pMaRs2yMRKWarMeS2av33qkf82qO9KD9P_gm2ovTbvQyHwNtlRoBJBF1JSt_ZAdLZnvfTjfJ8q_Ih0ADcAt3lwJXdPxQ" /></form> </div>
|
||||||
|
<script>
|
||||||
|
var AdvancedSubscriptionEnabled = false;
|
||||||
|
</script>
|
||||||
|
</section>
|
||||||
|
</div>
|
||||||
|
<div class="ribbon">
|
||||||
|
<span class="ribbon-color1"></span>
|
||||||
|
<span class="ribbon-color2"></span>
|
||||||
|
<span class="ribbon-color3"></span>
|
||||||
|
<span class="ribbon-color4"></span>
|
||||||
|
<span class="ribbon-color5"></span>
|
||||||
|
<span class="ribbon-color6"></span>
|
||||||
|
<span class="ribbon-color7"></span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="m-home-nav hidden-lg hidden-md" id="sk-mobile-header">
|
||||||
|
<div class="m-home-tool-left clickable" data-toggle="modal" data-target="#modal-nav">
|
||||||
|
<i class="fa fa-bars" aria-hidden="true"></i>
|
||||||
|
</div>
|
||||||
|
<div class="m-home-tool-left"></div>
|
||||||
|
<div style="text-align: center; height:100%;flex:1;">
|
||||||
|
<a href="/" style="text-decoration:none">
|
||||||
|
<img src="/images/mikan-pic.png" style="height: 3rem;margin-top: 0.5rem;">
|
||||||
|
<img src="/images/mikan-text.png" style="height: 1.5rem;margin-top: 0.5rem;">
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
<div class="m-home-tool-right clickable" data-toggle="modal" data-target="#modal-login">
|
||||||
|
<i class="fa fa-user" aria-hidden="true" style="margin-right: 1rem;"></i>
|
||||||
|
</div>
|
||||||
|
<div class="m-home-tool-right clickable" onclick="ShowNavSearch()">
|
||||||
|
<i class="fa fa-search" aria-hidden="true"></i>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="m-nav-search" style="width: 100%;">
|
||||||
|
<div style="flex: 1;">
|
||||||
|
<form method="get" action="/Home/Search">
|
||||||
|
<div class="input-group">
|
||||||
|
<span class="input-group-addon" id="sizing-addon1" style="border: none;background-color: white;">
|
||||||
|
<i class="fa fa-search" aria-hidden="true"></i>
|
||||||
|
</span>
|
||||||
|
<input type="text" class="form-control" placeholder="搜索" name="searchstr" aria-describedby="sizing-addon1" style="border: none;font-size:16px;">
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
<div style="width: 4rem;" onclick="HideNavSearch()">
|
||||||
|
<span style="font-size: 1.25rem;">取消</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
|
||||||
|
<div id="sk-container" class="container hidden-sm hidden-xs">
|
||||||
|
<section class="main-content">
|
||||||
|
|
||||||
|
<div id="an-list">
|
||||||
|
<div id="an-list-nav">
|
||||||
|
<div class="sk-col my-rss">我的字幕组订阅更新</div>
|
||||||
|
<div class="sk-col my-rss-date js-episode-update active" data-predate="1" data-enddate="1" data-maximumitems="10">昨天至今</div>
|
||||||
|
<div class="sk-col my-rss-date js-episode-update" data-predate="0" data-enddate="1" data-maximumitems="10">今天</div>
|
||||||
|
<div class="sk-col my-rss-date js-episode-update" data-predate="1" data-enddate="0" data-maximumitems="10">昨天</div>
|
||||||
|
<div class="sk-col my-rss-date js-episode-update" data-predate="2" data-enddate="1" data-maximumitems="10">近三天</div>
|
||||||
|
<div class="sk-col my-rss-date js-episode-update" data-predate="-1" data-enddate="-1" data-maximumitems="10">OVA/剧场版 (beta)</div>
|
||||||
|
|
||||||
|
<div class="sk-col my-rss-date indent-btn active" onclick="ToggleEpisodeUpdates(this)"><i class="fa fa-angle-down fa-2x"></i></div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div id="an-episode-updates">
|
||||||
|
|
||||||
|
<div class="no-episode-update">
|
||||||
|
<img src="/images/mikan-pic.png" style="height:150px;" /><img src="/images/no-episode-update.png" style="height:60px;margin-left:30px;" />
|
||||||
|
</div>
|
||||||
|
|
||||||
|
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
<div style="margin-top: 10px; margin-bottom: -10px;">
|
||||||
|
|
||||||
|
|
||||||
|
<div style="width:100%; margin-right: auto; margin-left: auto;" class="hidden-xs hidden-sm">
|
||||||
|
<a href="https://shop119340084.taobao.com/?mm_sycmid=1_150417_dba461f2e2f73a9ea2a8fa11f33a1aee" onclick="ga('send', 'event', 'sswj_lg', 'clicked', 'ad');">
|
||||||
|
<img src="/images/SSWJ/sswj8_lg.jpg" style='height: 100%; width: 100%; object-fit: contain' />
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div style="width:100%; margin-right: auto; margin-left: auto;" class="hidden-lg hidden-md">
|
||||||
|
<a href="https://m.tb.cn/h.g0X5kru9wgYTRsp?mm_sycmid=1_150416_5914d148315f48d5297c751b84bac595" onclick="ga('send', 'event', 'sswj_sm', 'clicked', 'ad');">
|
||||||
|
<img src="/images/SSWJ/sswj8_sm.jpg" style='height: 100%; width: 100%; object-fit: contain' />
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="date-select-row row" style="padding-left:35px">
|
||||||
|
|
||||||
|
<ul class="navbar-nav date-select">
|
||||||
|
<li class="sk-col dropdown date-btn">
|
||||||
|
<div class="dropdown-toggle btn btn-default dropdown-custom" data-toggle="dropdown">
|
||||||
|
<div class="sk-col glyphicon glyphicon-calendar"></div>
|
||||||
|
<div class="sk-col date-text"> 2025 冬季番组 <span class="caret"></span> </div>
|
||||||
|
</div>
|
||||||
|
<ul class="dropdown-menu" role="menu">
|
||||||
|
<li class="dropdown-submenu">
|
||||||
|
<a class="default-cursor">2025</a>
|
||||||
|
<ul class="dropdown-menu">
|
||||||
|
<li><a href="javaScript:void(0);" onclick="UpdateBangumiCoverFlow(this, false)" data-year="2025" data-season="冬">冬季番组</a></li>
|
||||||
|
</ul>
|
||||||
|
</li>
|
||||||
|
<li class="dropdown-submenu">
|
||||||
|
<a class="default-cursor">2024</a>
|
||||||
|
<ul class="dropdown-menu">
|
||||||
|
<li><a href="javaScript:void(0);" onclick="UpdateBangumiCoverFlow(this, false)" data-year="2024" data-season="秋">秋季番组</a></li>
|
||||||
|
<li><a href="javaScript:void(0);" onclick="UpdateBangumiCoverFlow(this, false)" data-year="2024" data-season="夏">夏季番组</a></li>
|
||||||
|
<li><a href="javaScript:void(0);" onclick="UpdateBangumiCoverFlow(this, false)" data-year="2024" data-season="春">春季番组</a></li>
|
||||||
|
<li><a href="javaScript:void(0);" onclick="UpdateBangumiCoverFlow(this, false)" data-year="2024" data-season="冬">冬季番组</a></li>
|
||||||
|
</ul>
|
||||||
|
</li>
|
||||||
|
<li class="dropdown-submenu">
|
||||||
|
<a class="default-cursor">2023</a>
|
||||||
|
<ul class="dropdown-menu">
|
||||||
|
<li><a href="javaScript:void(0);" onclick="UpdateBangumiCoverFlow(this, false)" data-year="2023" data-season="秋">秋季番组</a></li>
|
||||||
|
<li><a href="javaScript:void(0);" onclick="UpdateBangumiCoverFlow(this, false)" data-year="2023" data-season="夏">夏季番组</a></li>
|
||||||
|
<li><a href="javaScript:void(0);" onclick="UpdateBangumiCoverFlow(this, false)" data-year="2023" data-season="春">春季番组</a></li>
|
||||||
|
<li><a href="javaScript:void(0);" onclick="UpdateBangumiCoverFlow(this, false)" data-year="2023" data-season="冬">冬季番组</a></li>
|
||||||
|
</ul>
|
||||||
|
</li>
|
||||||
|
<li class="dropdown-submenu">
|
||||||
|
<a class="default-cursor">2022</a>
|
||||||
|
<ul class="dropdown-menu">
|
||||||
|
<li><a href="javaScript:void(0);" onclick="UpdateBangumiCoverFlow(this, false)" data-year="2022" data-season="秋">秋季番组</a></li>
|
||||||
|
<li><a href="javaScript:void(0);" onclick="UpdateBangumiCoverFlow(this, false)" data-year="2022" data-season="夏">夏季番组</a></li>
|
||||||
|
<li><a href="javaScript:void(0);" onclick="UpdateBangumiCoverFlow(this, false)" data-year="2022" data-season="春">春季番组</a></li>
|
||||||
|
<li><a href="javaScript:void(0);" onclick="UpdateBangumiCoverFlow(this, false)" data-year="2022" data-season="冬">冬季番组</a></li>
|
||||||
|
</ul>
|
||||||
|
</li>
|
||||||
|
<li class="dropdown-submenu">
|
||||||
|
<a class="default-cursor">2021</a>
|
||||||
|
<ul class="dropdown-menu">
|
||||||
|
<li><a href="javaScript:void(0);" onclick="UpdateBangumiCoverFlow(this, false)" data-year="2021" data-season="秋">秋季番组</a></li>
|
||||||
|
<li><a href="javaScript:void(0);" onclick="UpdateBangumiCoverFlow(this, false)" data-year="2021" data-season="夏">夏季番组</a></li>
|
||||||
|
<li><a href="javaScript:void(0);" onclick="UpdateBangumiCoverFlow(this, false)" data-year="2021" data-season="春">春季番组</a></li>
|
||||||
|
<li><a href="javaScript:void(0);" onclick="UpdateBangumiCoverFlow(this, false)" data-year="2021" data-season="冬">冬季番组</a></li>
|
||||||
|
</ul>
|
||||||
|
</li>
|
||||||
|
<li class="dropdown-submenu">
|
||||||
|
<a class="default-cursor">2020</a>
|
||||||
|
<ul class="dropdown-menu">
|
||||||
|
<li><a href="javaScript:void(0);" onclick="UpdateBangumiCoverFlow(this, false)" data-year="2020" data-season="秋">秋季番组</a></li>
|
||||||
|
<li><a href="javaScript:void(0);" onclick="UpdateBangumiCoverFlow(this, false)" data-year="2020" data-season="夏">夏季番组</a></li>
|
||||||
|
<li><a href="javaScript:void(0);" onclick="UpdateBangumiCoverFlow(this, false)" data-year="2020" data-season="春">春季番组</a></li>
|
||||||
|
<li><a href="javaScript:void(0);" onclick="UpdateBangumiCoverFlow(this, false)" data-year="2020" data-season="冬">冬季番组</a></li>
|
||||||
|
</ul>
|
||||||
|
</li>
|
||||||
|
<li class="dropdown-submenu">
|
||||||
|
<a class="default-cursor">2019</a>
|
||||||
|
<ul class="dropdown-menu">
|
||||||
|
<li><a href="javaScript:void(0);" onclick="UpdateBangumiCoverFlow(this, false)" data-year="2019" data-season="秋">秋季番组</a></li>
|
||||||
|
<li><a href="javaScript:void(0);" onclick="UpdateBangumiCoverFlow(this, false)" data-year="2019" data-season="夏">夏季番组</a></li>
|
||||||
|
<li><a href="javaScript:void(0);" onclick="UpdateBangumiCoverFlow(this, false)" data-year="2019" data-season="春">春季番组</a></li>
|
||||||
|
<li><a href="javaScript:void(0);" onclick="UpdateBangumiCoverFlow(this, false)" data-year="2019" data-season="冬">冬季番组</a></li>
|
||||||
|
</ul>
|
||||||
|
</li>
|
||||||
|
<li class="dropdown-submenu">
|
||||||
|
<a class="default-cursor">2018</a>
|
||||||
|
<ul class="dropdown-menu">
|
||||||
|
<li><a href="javaScript:void(0);" onclick="UpdateBangumiCoverFlow(this, false)" data-year="2018" data-season="秋">秋季番组</a></li>
|
||||||
|
<li><a href="javaScript:void(0);" onclick="UpdateBangumiCoverFlow(this, false)" data-year="2018" data-season="夏">夏季番组</a></li>
|
||||||
|
<li><a href="javaScript:void(0);" onclick="UpdateBangumiCoverFlow(this, false)" data-year="2018" data-season="春">春季番组</a></li>
|
||||||
|
<li><a href="javaScript:void(0);" onclick="UpdateBangumiCoverFlow(this, false)" data-year="2018" data-season="冬">冬季番组</a></li>
|
||||||
|
</ul>
|
||||||
|
</li>
|
||||||
|
<li class="dropdown-submenu">
|
||||||
|
<a class="default-cursor">2017</a>
|
||||||
|
<ul class="dropdown-menu">
|
||||||
|
<li><a href="javaScript:void(0);" onclick="UpdateBangumiCoverFlow(this, false)" data-year="2017" data-season="秋">秋季番组</a></li>
|
||||||
|
<li><a href="javaScript:void(0);" onclick="UpdateBangumiCoverFlow(this, false)" data-year="2017" data-season="夏">夏季番组</a></li>
|
||||||
|
<li><a href="javaScript:void(0);" onclick="UpdateBangumiCoverFlow(this, false)" data-year="2017" data-season="春">春季番组</a></li>
|
||||||
|
<li><a href="javaScript:void(0);" onclick="UpdateBangumiCoverFlow(this, false)" data-year="2017" data-season="冬">冬季番组</a></li>
|
||||||
|
</ul>
|
||||||
|
</li>
|
||||||
|
<li class="dropdown-submenu">
|
||||||
|
<a class="default-cursor">2016</a>
|
||||||
|
<ul class="dropdown-menu">
|
||||||
|
<li><a href="javaScript:void(0);" onclick="UpdateBangumiCoverFlow(this, false)" data-year="2016" data-season="秋">秋季番组</a></li>
|
||||||
|
<li><a href="javaScript:void(0);" onclick="UpdateBangumiCoverFlow(this, false)" data-year="2016" data-season="夏">夏季番组</a></li>
|
||||||
|
<li><a href="javaScript:void(0);" onclick="UpdateBangumiCoverFlow(this, false)" data-year="2016" data-season="春">春季番组</a></li>
|
||||||
|
<li><a href="javaScript:void(0);" onclick="UpdateBangumiCoverFlow(this, false)" data-year="2016" data-season="冬">冬季番组</a></li>
|
||||||
|
</ul>
|
||||||
|
</li>
|
||||||
|
<li class="dropdown-submenu">
|
||||||
|
<a class="default-cursor">2015</a>
|
||||||
|
<ul class="dropdown-menu">
|
||||||
|
<li><a href="javaScript:void(0);" onclick="UpdateBangumiCoverFlow(this, false)" data-year="2015" data-season="秋">秋季番组</a></li>
|
||||||
|
<li><a href="javaScript:void(0);" onclick="UpdateBangumiCoverFlow(this, false)" data-year="2015" data-season="夏">夏季番组</a></li>
|
||||||
|
<li><a href="javaScript:void(0);" onclick="UpdateBangumiCoverFlow(this, false)" data-year="2015" data-season="春">春季番组</a></li>
|
||||||
|
<li><a href="javaScript:void(0);" onclick="UpdateBangumiCoverFlow(this, false)" data-year="2015" data-season="冬">冬季番组</a></li>
|
||||||
|
</ul>
|
||||||
|
</li>
|
||||||
|
<li class="dropdown-submenu">
|
||||||
|
<a class="default-cursor">2014</a>
|
||||||
|
<ul class="dropdown-menu">
|
||||||
|
<li><a href="javaScript:void(0);" onclick="UpdateBangumiCoverFlow(this, false)" data-year="2014" data-season="秋">秋季番组</a></li>
|
||||||
|
<li><a href="javaScript:void(0);" onclick="UpdateBangumiCoverFlow(this, false)" data-year="2014" data-season="夏">夏季番组</a></li>
|
||||||
|
<li><a href="javaScript:void(0);" onclick="UpdateBangumiCoverFlow(this, false)" data-year="2014" data-season="春">春季番组</a></li>
|
||||||
|
<li><a href="javaScript:void(0);" onclick="UpdateBangumiCoverFlow(this, false)" data-year="2014" data-season="冬">冬季番组</a></li>
|
||||||
|
</ul>
|
||||||
|
</li>
|
||||||
|
<li class="dropdown-submenu">
|
||||||
|
<a class="default-cursor">2013</a>
|
||||||
|
<ul class="dropdown-menu">
|
||||||
|
<li><a href="javaScript:void(0);" onclick="UpdateBangumiCoverFlow(this, false)" data-year="2013" data-season="秋">秋季番组</a></li>
|
||||||
|
</ul>
|
||||||
|
</li>
|
||||||
|
</ul>
|
||||||
|
</li>
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div id="sk-body">
|
||||||
|
|
||||||
|
<svg width="0" height="0">
|
||||||
|
<defs>
|
||||||
|
<clipPath id="clip-triangle-1">
|
||||||
|
<polygon points="0 15,80 15,100 0,120 15,1120 15,1120 370,0 370"/>
|
||||||
|
</clipPath>
|
||||||
|
<clipPath id="clip-triangle-2">
|
||||||
|
<polygon points="0 15,310 15,330 0,350 15,1120 15,1120 370,0 370"/>
|
||||||
|
</clipPath>
|
||||||
|
<clipPath id="clip-triangle-3">
|
||||||
|
<polygon points="0 15,540 15,560 0,580 15,1120 15,1120 370,0 370"/>
|
||||||
|
</clipPath>
|
||||||
|
<clipPath id="clip-triangle-4">
|
||||||
|
<polygon points="0 15,770 15,790 0,810 15,1120 15,1120 370,0 370"/>
|
||||||
|
</clipPath>
|
||||||
|
<clipPath id="clip-triangle-5">
|
||||||
|
<polygon points="0 15,995 15,1015 0,1035 15,1120 15,1120 370,0 370"/>
|
||||||
|
</clipPath>
|
||||||
|
<linearGradient id="row-bg-gradient" x1="0%" y1="0%" x2="100%" y2="0%">
|
||||||
|
<stop offset="0%" stop-color="#e4dedf" />
|
||||||
|
<stop offset="100%" stop-color="#cbbcc4" />
|
||||||
|
</linearGradient>
|
||||||
|
</defs>
|
||||||
|
</svg>
|
||||||
|
|
||||||
|
<div class="sk-bangumi">
|
||||||
|
|
||||||
|
<div class="no-subscribe-bangumi"> >_< 您还没有订阅任何番组,快去<a href="/">首页</a>添加订阅吧</div></div>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div id="sk-mobile-container" class="m-home hidden-lg hidden-md">
|
||||||
|
<div style="height:16rem;">
|
||||||
|
<!-- Indicators -->
|
||||||
|
<div id="myCarousel" class="carousel slide carousel-fade" data-ride="carousel" style="padding-top:3.9rem;">
|
||||||
|
<!-- Wrapper for slides -->
|
||||||
|
<div class="carousel-inner" role="listbox">
|
||||||
|
<div class="item active carousel-bg" style="background-image:url('/images/Promotion/202501/2025_01_bangumi_3519.jpg?format=webp');" onclick="window.open('/Home/Bangumi/3519', '_self');">
|
||||||
|
</div>
|
||||||
|
<div class="item carousel-bg" style="background-image: url('/images/Promotion/202501/2025_01_bangumi_3530.jpg?format=webp');" onclick="window.open('/Home/Bangumi/3530', '_self');">
|
||||||
|
</div>
|
||||||
|
<div class="item carousel-bg" style="background-image: url('/images/Promotion/202501/2025_01_bangumi_3518.jpg?format=webp');" onclick="window.open('/Home/Bangumi/3518', '_self');">
|
||||||
|
</div>
|
||||||
|
<div class="item carousel-bg" style="background-image: url('/images/Promotion/202501/2025_01_bangumi_3539.jpg?format=webp');" onclick="window.open('/Home/Bangumi/3539', '_self');">
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="carousel-indicators" style="z-index:2;">
|
||||||
|
<img src="./../images/mikan-subscribe.png" style="border-radius: 5rem;width: 5rem;height: 5rem;margin-top: -2.5rem;">
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
<div class="m-home-subscribe">
|
||||||
|
<img src="./../images/mikan-subscribe.png" style="border-radius: 5rem;width: 5rem;height: 5rem;margin-top: -2.5rem;">
|
||||||
|
<div class="m-title">
|
||||||
|
<span class="title">我的订阅更新</span>
|
||||||
|
<div>
|
||||||
|
<div class="dropdown material-dropdown">
|
||||||
|
<button class="dropdown-toggle material-dropdown__btn" data-toggle="dropdown">
|
||||||
|
<span style="color: #3bc0c3;font-size: 1.25rem;" class="js-mobile-episode-update-date">昨天至今</span>
|
||||||
|
<span><i class="fa fa-angle-down" aria-hidden="true"></i></span>
|
||||||
|
</button>
|
||||||
|
<ul class="dropdown-menu material-dropdown-menu">
|
||||||
|
<li><a href="javascript:void(0);" class="material-dropdown-menu__link js-mobile-episode-update" data-predate="1" data-enddate="1" data-maximumitems="6">昨天至今</a></li>
|
||||||
|
<li><a href="javascript:void(0);" class="material-dropdown-menu__link js-mobile-episode-update" data-predate="0" data-enddate="1" data-maximumitems="6">今天</a></li>
|
||||||
|
<li><a href="javascript:void(0);" class="material-dropdown-menu__link js-mobile-episode-update" data-predate="1" data-enddate="0" data-maximumitems="6">昨天</a></li>
|
||||||
|
<li><a href="javascript:void(0);" class="material-dropdown-menu__link js-mobile-episode-update" data-predate="2" data-enddate="1" data-maximumitems="6">近三天</a></li>
|
||||||
|
<li><a href="javascript:void(0);" class="material-dropdown-menu__link js-mobile-episode-update" data-predate="6" data-enddate="1" data-maximumitems="6">最近一周</a></li>
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="m-subscribe-list">
|
||||||
|
<div style="height: 3rem;color: #888;border: none;font-size: 1.25rem;">
|
||||||
|
暂无更新,添加更多订阅吧!
|
||||||
|
</div>
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
<div style="margin-top: 0px; margin-bottom: 10px;">
|
||||||
|
|
||||||
|
|
||||||
|
<div style="width:100%; margin-right: auto; margin-left: auto;" class="hidden-xs hidden-sm">
|
||||||
|
<a href="https://shop119340084.taobao.com/?mm_sycmid=1_150417_dba461f2e2f73a9ea2a8fa11f33a1aee" onclick="ga('send', 'event', 'sswj_lg', 'clicked', 'ad');">
|
||||||
|
<img src="/images/SSWJ/sswj7_lg.jpg" style='height: 100%; width: 100%; object-fit: contain' />
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div style="width:100%; margin-right: auto; margin-left: auto;" class="hidden-lg hidden-md">
|
||||||
|
<a href="https://m.tb.cn/h.g0X5kru9wgYTRsp?mm_sycmid=1_150416_5914d148315f48d5297c751b84bac595" onclick="ga('send', 'event', 'sswj_sm', 'clicked', 'ad');">
|
||||||
|
<img src="/images/SSWJ/sswj7_sm.jpg" style='height: 100%; width: 100%; object-fit: contain' />
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="m-home-week">
|
||||||
|
|
||||||
|
<div class="m-home-week-item" style="background-color:white;">
|
||||||
|
<div class="title">
|
||||||
|
<span class="monday">我的订阅</span>
|
||||||
|
</div>
|
||||||
|
<div class="detail">
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<link rel="stylesheet" href="/css/bootstrap-toggle-round.css?v=ZHx5lTKQuvWMGeQuWNqkSQvuRt2u8x7w0URRg4MhfUo" />
|
||||||
|
<div class="modal modal-fullscreen fade" id="modal-nav" tabindex="-1" role="dialog" aria-labelledby="myModalLabel" aria-hidden="true" style="background-color:#3bc0c3;">
|
||||||
|
<div class="modal-dialog">
|
||||||
|
<div class="modal-content">
|
||||||
|
<div class="modal-body" style="margin: auto;width:100%;">
|
||||||
|
<div class="m-tool">
|
||||||
|
<span class="m-close clickable"><i class="fa fa-times" aria-hidden="true" data-toggle="modal" data-target="#modal-nav"></i></span>
|
||||||
|
<div class="m-tool-toolbar">
|
||||||
|
<img src="/images/mikan-pic.png" style="width: 3rem;">
|
||||||
|
<img src="/images/mikan-text.png" style="width: 7rem;">
|
||||||
|
</div>
|
||||||
|
<div class="m-tool-list">
|
||||||
|
<ul>
|
||||||
|
<li><a href="/" class="link">主页</a></li>
|
||||||
|
<li class="m-tool-search-change"><a href="/Home/MyBangumi" class="link">订阅</a></li>
|
||||||
|
<li onclick="tool.clickSearch()" class="m-tool-search-change">
|
||||||
|
<i class="fa fa-search" aria-hidden="true"></i> 搜索站内
|
||||||
|
</li>
|
||||||
|
<li class="m-tool-search-input">
|
||||||
|
<form method="get" action="/Home/Search">
|
||||||
|
<div style="display: flex;height: 100%;">
|
||||||
|
<input type="text" class="form-control" name="searchstr" style="font-size:16px;" />
|
||||||
|
<span style="width: 5rem;" onclick="tool.resetSearch()">取消</span>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
</li>
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="modal modal-fullscreen fade" id="modal-login" tabindex="-1" role="dialog" aria-labelledby="myModalLabel" aria-hidden="true" style="background-color:#edf1f2;">
|
||||||
|
<div class="modal-dialog">
|
||||||
|
<div class="modal-content">
|
||||||
|
<div class="modal-body" style="margin: auto;width:100%;height:85vh;">
|
||||||
|
<div class="m-login">
|
||||||
|
<span class="m-left clickable"><i class="fa fa-angle-left" aria-hidden="true" data-toggle="modal" data-target="#modal-login"></i></span>
|
||||||
|
|
||||||
|
<div class="m-tool-title">
|
||||||
|
登陆mikan账号
|
||||||
|
</div>
|
||||||
|
<div style="text-align: center;margin-top: 2rem;">
|
||||||
|
<img src="/images/mikan-pic.png" style="width: 6rem;">
|
||||||
|
</div>
|
||||||
|
<form action="/Account/Login?ReturnUrl=%2FHome%2FMyBangumi" method="post"> <div>
|
||||||
|
<input type="text" class="form-control" aria-label="..." placeholder="用户名" name="UserName">
|
||||||
|
<input type="password" class="form-control" aria-label="..." placeholder="密码" name="Password">
|
||||||
|
</div>
|
||||||
|
<button class="form-control" type="submit">登录</button>
|
||||||
|
<input name="__RequestVerificationToken" type="hidden" value="CfDJ8MyNMqFNaC9JmJW13PvY-93GWOqu6SxfgR0S2fsWVSk41_SQEknnNNxKBZ6pMaRs2yMRKWarMeS2av33qkf82qO9KD9P_gm2ovTbvQyHwNtlRoBJBF1JSt_ZAdLZnvfTjfJ8q_Ih0ADcAt3lwJXdPxQ" /></form> <div class="m-goto-registry">
|
||||||
|
<a href="/Account/Register" class="w-other-c" style="color:#3bc0c3">立即注册</a>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<footer class="footer hidden-xs hidden-sm">
|
||||||
|
<div id="sk-footer" class="container text-center">
|
||||||
|
<div>Powered by Mikan Project <a href="/Home/Contact" target="_blank">联系我们</a></div>
|
||||||
|
<div>Cooperate by PlaymateCat@Lisa</div>
|
||||||
|
</div>
|
||||||
|
</footer>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
var tool = {};
|
||||||
|
(function () {
|
||||||
|
|
||||||
|
var inputPEl = $('.m-tool-search-input');
|
||||||
|
var inputEl = inputPEl.find('input');
|
||||||
|
var changeEl = $('.m-tool-search-change');
|
||||||
|
inputPEl.hide();
|
||||||
|
tool.clickSearch = clickSearch;
|
||||||
|
tool.resetSearch = resetSearch;
|
||||||
|
|
||||||
|
function clickSearch() {
|
||||||
|
changeEl.hide();
|
||||||
|
inputPEl.show();
|
||||||
|
inputEl.focus();
|
||||||
|
}
|
||||||
|
|
||||||
|
function resetSearch() {
|
||||||
|
changeEl.show();
|
||||||
|
inputPEl.hide();
|
||||||
|
inputEl.val('');
|
||||||
|
}
|
||||||
|
})();
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
var pageUtil;
|
||||||
|
|
||||||
|
(function () {
|
||||||
|
pageUtil = {
|
||||||
|
isMobile: isMobile
|
||||||
|
};
|
||||||
|
|
||||||
|
function isMobile() {
|
||||||
|
var check = false;
|
||||||
|
(function (a) {
|
||||||
|
if (/(android|bb\d+|meego).+mobile|avantgo|bada\/|blackberry|blazer|compal|elaine|fennec|hiptop|iemobile|ip(hone|od)|iris|kindle|lge |maemo|midp|mmp|mobile.+firefox|netfront|opera m(ob|in)i|palm( os)?|phone|p(ixi|re)\/|plucker|pocket|psp|series(4|6)0|symbian|treo|up\.(browser|link)|vodafone|wap|windows ce|xda|xiino/i.test(a) || /1207|6310|6590|3gso|4thp|50[1-6]i|770s|802s|a wa|abac|ac(er|oo|s\-)|ai(ko|rn)|al(av|ca|co)|amoi|an(ex|ny|yw)|aptu|ar(ch|go)|as(te|us)|attw|au(di|\-m|r |s )|avan|be(ck|ll|nq)|bi(lb|rd)|bl(ac|az)|br(e|v)w|bumb|bw\-(n|u)|c55\/|capi|ccwa|cdm\-|cell|chtm|cldc|cmd\-|co(mp|nd)|craw|da(it|ll|ng)|dbte|dc\-s|devi|dica|dmob|do(c|p)o|ds(12|\-d)|el(49|ai)|em(l2|ul)|er(ic|k0)|esl8|ez([4-7]0|os|wa|ze)|fetc|fly(\-|_)|g1 u|g560|gene|gf\-5|g\-mo|go(\.w|od)|gr(ad|un)|haie|hcit|hd\-(m|p|t)|hei\-|hi(pt|ta)|hp( i|ip)|hs\-c|ht(c(\-| |_|a|g|p|s|t)|tp)|hu(aw|tc)|i\-(20|go|ma)|i230|iac( |\-|\/)|ibro|idea|ig01|ikom|im1k|inno|ipaq|iris|ja(t|v)a|jbro|jemu|jigs|kddi|keji|kgt( |\/)|klon|kpt |kwc\-|kyo(c|k)|le(no|xi)|lg( g|\/(k|l|u)|50|54|\-[a-w])|libw|lynx|m1\-w|m3ga|m50\/|ma(te|ui|xo)|mc(01|21|ca)|m\-cr|me(rc|ri)|mi(o8|oa|ts)|mmef|mo(01|02|bi|de|do|t(\-| |o|v)|zz)|mt(50|p1|v )|mwbp|mywa|n10[0-2]|n20[2-3]|n30(0|2)|n50(0|2|5)|n7(0(0|1)|10)|ne((c|m)\-|on|tf|wf|wg|wt)|nok(6|i)|nzph|o2im|op(ti|wv)|oran|owg1|p800|pan(a|d|t)|pdxg|pg(13|\-([1-8]|c))|phil|pire|pl(ay|uc)|pn\-2|po(ck|rt|se)|prox|psio|pt\-g|qa\-a|qc(07|12|21|32|60|\-[2-7]|i\-)|qtek|r380|r600|raks|rim9|ro(ve|zo)|s55\/|sa(ge|ma|mm|ms|ny|va)|sc(01|h\-|oo|p\-)|sdk\/|se(c(\-|0|1)|47|mc|nd|ri)|sgh\-|shar|sie(\-|m)|sk\-0|sl(45|id)|sm(al|ar|b3|it|t5)|so(ft|ny)|sp(01|h\-|v\-|v )|sy(01|mb)|t2(18|50)|t6(00|10|18)|ta(gt|lk)|tcl\-|tdg\-|tel(i|m)|tim\-|t\-mo|to(pl|sh)|ts(70|m\-|m3|m5)|tx\-9|up(\.b|g1|si)|utst|v400|v750|veri|vi(rg|te)|vk(40|5[0-3]|\-v)|vm40|voda|vulc|vx(52|53|60|61|70|80|81|83|85|98)|w3c(\-| )|webc|whit|wi(g |nc|nw)|wmlb|wonu|x700|yas\-|your|zeto|zte\-/i.test(a.substr(0, 4))) check = true;
|
||||||
|
})(navigator.userAgent || navigator.vendor || window.opera);
|
||||||
|
return check;
|
||||||
|
}
|
||||||
|
})();
|
||||||
|
|
||||||
|
//detect if page is mobile
|
||||||
|
if (pageUtil.isMobile()) {
|
||||||
|
document.getElementsByTagName('html')[0].style['font-size'] = window.innerWidth / 32 + 'px';
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
</body>
|
||||||
|
|
||||||
|
<!-- here put your own javascript -->
|
||||||
|
|
||||||
|
|
||||||
|
<script src="/js/mikan.min.js?v=7USd_hfRE7KH46vQBdF29boa3ENWKMVFRTyD9a8XEDg"></script>
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
</html>
|
||||||
3180
apps/recorder/tests/resources/mikan/MyBangumi.htm
Normal file
3180
apps/recorder/tests/resources/mikan/MyBangumi.htm
Normal file
File diff suppressed because it is too large
Load Diff
@ -10,11 +10,11 @@
|
|||||||
"cssVariables": true
|
"cssVariables": true
|
||||||
},
|
},
|
||||||
"aliases": {
|
"aliases": {
|
||||||
"components": "@/components",
|
"components": "@/views/components",
|
||||||
"utils": "@/presentation/utils",
|
"utils": "@/views/utils",
|
||||||
"ui": "@/components/ui",
|
"ui": "@/views/components/ui",
|
||||||
"lib": "@/presentation/lib",
|
"lib": "@/views/lib",
|
||||||
"hooks": "@/presentation/hooks"
|
"hooks": "@/views/hooks"
|
||||||
},
|
},
|
||||||
"iconLibrary": "lucide"
|
"iconLibrary": "lucide"
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,17 +0,0 @@
|
|||||||
import type { CodegenConfig } from '@graphql-codegen/cli';
|
|
||||||
|
|
||||||
const config: CodegenConfig = {
|
|
||||||
schema: 'http://127.0.0.1:5001/api/graphql/introspection',
|
|
||||||
documents: ['src/**/*.{ts,tsx}'],
|
|
||||||
generates: {
|
|
||||||
'./src/infra/graphql/gql/': {
|
|
||||||
plugins: [],
|
|
||||||
preset: 'client',
|
|
||||||
presetConfig: {
|
|
||||||
gqlTagName: 'gql',
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
};
|
|
||||||
|
|
||||||
export default config;
|
|
||||||
@ -6,9 +6,7 @@
|
|||||||
"scripts": {
|
"scripts": {
|
||||||
"build": "rsbuild build",
|
"build": "rsbuild build",
|
||||||
"dev": "rsbuild dev",
|
"dev": "rsbuild dev",
|
||||||
"preview": "rsbuild preview",
|
"preview": "rsbuild preview"
|
||||||
"codegen": "graphql-codegen --config graphql-codegen.ts",
|
|
||||||
"codegen-watch": "graphql-codegen --config graphql-codegen.ts --watch"
|
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@abraham/reflection": "^0.13.0",
|
"@abraham/reflection": "^0.13.0",
|
||||||
@ -47,9 +45,7 @@
|
|||||||
"@radix-ui/react-toggle-group": "^1.1.9",
|
"@radix-ui/react-toggle-group": "^1.1.9",
|
||||||
"@radix-ui/react-tooltip": "^1.2.6",
|
"@radix-ui/react-tooltip": "^1.2.6",
|
||||||
"@rsbuild/plugin-react": "^1.2.0",
|
"@rsbuild/plugin-react": "^1.2.0",
|
||||||
"@tanstack/react-query": "^5.75.6",
|
|
||||||
"@tanstack/react-router": "^1.112.13",
|
"@tanstack/react-router": "^1.112.13",
|
||||||
"@tanstack/react-table": "^8.21.3",
|
|
||||||
"@tanstack/router-devtools": "^1.112.13",
|
"@tanstack/router-devtools": "^1.112.13",
|
||||||
"arktype": "^2.1.6",
|
"arktype": "^2.1.6",
|
||||||
"chart.js": "^4.4.8",
|
"chart.js": "^4.4.8",
|
||||||
@ -63,10 +59,11 @@
|
|||||||
"input-otp": "^1.4.2",
|
"input-otp": "^1.4.2",
|
||||||
"jotai": "^2.12.3",
|
"jotai": "^2.12.3",
|
||||||
"jotai-signal": "^0.9.0",
|
"jotai-signal": "^0.9.0",
|
||||||
"lucide-react": "^0.509.0",
|
"lucide-react": "^0.508.0",
|
||||||
|
"next-themes": "^0.4.6",
|
||||||
"oidc-client-rx": "0.1.0-alpha.9",
|
"oidc-client-rx": "0.1.0-alpha.9",
|
||||||
"react": "^19.1.0",
|
"react": "^19.1.0",
|
||||||
"react-day-picker": "9.6.0",
|
"react-day-picker": "8.10.1",
|
||||||
"react-dom": "^19.1.0",
|
"react-dom": "^19.1.0",
|
||||||
"react-hook-form": "^7.56.3",
|
"react-hook-form": "^7.56.3",
|
||||||
"react-resizable-panels": "^3.0.1",
|
"react-resizable-panels": "^3.0.1",
|
||||||
@ -81,11 +78,6 @@
|
|||||||
"zod": "^3.24.4"
|
"zod": "^3.24.4"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@graphql-codegen/cli": "^5.0.6",
|
|
||||||
"@graphql-codegen/client-preset": "^4.8.1",
|
|
||||||
"@graphql-codegen/typescript": "^4.1.6",
|
|
||||||
"@graphql-typed-document-node/core": "^3.2.0",
|
|
||||||
"@parcel/watcher": "^2.5.1",
|
|
||||||
"@rsbuild/core": "^1.2.15",
|
"@rsbuild/core": "^1.2.15",
|
||||||
"@tailwindcss/postcss": "^4.0.9",
|
"@tailwindcss/postcss": "^4.0.9",
|
||||||
"@tanstack/react-router": "^1.112.0",
|
"@tanstack/react-router": "^1.112.0",
|
||||||
|
|||||||
@ -1,67 +0,0 @@
|
|||||||
import { Column } from "@tanstack/react-table";
|
|
||||||
import { ArrowDown, ArrowUp, ChevronsUpDown, EyeOff } from "lucide-react";
|
|
||||||
|
|
||||||
import { Button } from "@/components/ui/button";
|
|
||||||
import {
|
|
||||||
DropdownMenu,
|
|
||||||
DropdownMenuContent,
|
|
||||||
DropdownMenuItem,
|
|
||||||
DropdownMenuSeparator,
|
|
||||||
DropdownMenuTrigger,
|
|
||||||
} from "@/components/ui/dropdown-menu";
|
|
||||||
import { cn } from "@/presentation/utils";
|
|
||||||
import { HTMLAttributes } from "react";
|
|
||||||
|
|
||||||
interface DataTableColumnHeaderProps<TData, TValue>
|
|
||||||
extends HTMLAttributes<HTMLDivElement> {
|
|
||||||
column: Column<TData, TValue>;
|
|
||||||
title: string;
|
|
||||||
}
|
|
||||||
|
|
||||||
export function DataTableColumnHeader<TData, TValue>({
|
|
||||||
column,
|
|
||||||
title,
|
|
||||||
className,
|
|
||||||
}: DataTableColumnHeaderProps<TData, TValue>) {
|
|
||||||
if (!column.getCanSort()) {
|
|
||||||
return <div className={cn(className)}>{title}</div>;
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div className={cn("flex items-center space-x-2", className)}>
|
|
||||||
<DropdownMenu>
|
|
||||||
<DropdownMenuTrigger asChild>
|
|
||||||
<Button
|
|
||||||
variant="ghost"
|
|
||||||
size="sm"
|
|
||||||
className="-ml-3 h-8 data-[state=open]:bg-accent"
|
|
||||||
>
|
|
||||||
<span>{title}</span>
|
|
||||||
{column.getIsSorted() === "desc" ? (
|
|
||||||
<ArrowDown />
|
|
||||||
) : column.getIsSorted() === "asc" ? (
|
|
||||||
<ArrowUp />
|
|
||||||
) : (
|
|
||||||
<ChevronsUpDown />
|
|
||||||
)}
|
|
||||||
</Button>
|
|
||||||
</DropdownMenuTrigger>
|
|
||||||
<DropdownMenuContent align="start">
|
|
||||||
<DropdownMenuItem onClick={() => column.toggleSorting(false)}>
|
|
||||||
<ArrowUp className="h-3.5 w-3.5 text-muted-foreground/70" />
|
|
||||||
Asc
|
|
||||||
</DropdownMenuItem>
|
|
||||||
<DropdownMenuItem onClick={() => column.toggleSorting(true)}>
|
|
||||||
<ArrowDown className="h-3.5 w-3.5 text-muted-foreground/70" />
|
|
||||||
Desc
|
|
||||||
</DropdownMenuItem>
|
|
||||||
<DropdownMenuSeparator />
|
|
||||||
<DropdownMenuItem onClick={() => column.toggleVisibility(false)}>
|
|
||||||
<EyeOff className="h-3.5 w-3.5 text-muted-foreground/70" />
|
|
||||||
Hide
|
|
||||||
</DropdownMenuItem>
|
|
||||||
</DropdownMenuContent>
|
|
||||||
</DropdownMenu>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
@ -1,109 +0,0 @@
|
|||||||
import { Table } from "@tanstack/react-table";
|
|
||||||
import {
|
|
||||||
ChevronLeft,
|
|
||||||
ChevronRight,
|
|
||||||
ChevronsLeft,
|
|
||||||
ChevronsRight,
|
|
||||||
} from "lucide-react";
|
|
||||||
|
|
||||||
import { Button } from "@/components/ui/button";
|
|
||||||
import {
|
|
||||||
Select,
|
|
||||||
SelectContent,
|
|
||||||
SelectItem,
|
|
||||||
SelectTrigger,
|
|
||||||
SelectValue,
|
|
||||||
} from "@/components/ui/select";
|
|
||||||
import { useMemo } from "react";
|
|
||||||
|
|
||||||
interface DataTablePaginationProps<TData> {
|
|
||||||
table: Table<TData>;
|
|
||||||
showSelectedRowCount?: boolean;
|
|
||||||
}
|
|
||||||
|
|
||||||
export function DataTablePagination<TData>({
|
|
||||||
table,
|
|
||||||
showSelectedRowCount = false,
|
|
||||||
}: DataTablePaginationProps<TData>) {
|
|
||||||
const renderRowsPerPage = () => {
|
|
||||||
return (
|
|
||||||
<div className="flex items-center space-x-2">
|
|
||||||
<p className="text-sm font-medium">Rows per page</p>
|
|
||||||
<Select
|
|
||||||
value={`${table.getState().pagination.pageSize}`}
|
|
||||||
onValueChange={(value) => {
|
|
||||||
table.setPageSize(Number(value));
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<SelectTrigger className="h-8 w-[70px]">
|
|
||||||
<SelectValue placeholder={table.getState().pagination.pageSize} />
|
|
||||||
</SelectTrigger>
|
|
||||||
<SelectContent side="top">
|
|
||||||
{[10, 20, 30, 40, 50].map((pageSize) => (
|
|
||||||
<SelectItem key={pageSize} value={`${pageSize}`}>
|
|
||||||
{pageSize}
|
|
||||||
</SelectItem>
|
|
||||||
))}
|
|
||||||
</SelectContent>
|
|
||||||
</Select>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
return (
|
|
||||||
<div className="flex items-center justify-between px-2">
|
|
||||||
{showSelectedRowCount ? (
|
|
||||||
<div className="flex-1 text-sm text-muted-foreground">
|
|
||||||
{table.getFilteredSelectedRowModel().rows.length} of{" "}
|
|
||||||
{table.getFilteredRowModel().rows.length} row(s) selected.
|
|
||||||
</div>
|
|
||||||
) : (
|
|
||||||
<div className="flex-1 text-sm items-center">{renderRowsPerPage()}</div>
|
|
||||||
)}
|
|
||||||
<div className="flex items-center space-x-6 lg:space-x-8">
|
|
||||||
{showSelectedRowCount && renderRowsPerPage()}
|
|
||||||
<div className="flex w-[100px] items-center justify-center text-sm font-medium">
|
|
||||||
Page {table.getState().pagination.pageIndex + 1} of{" "}
|
|
||||||
{table.getPageCount()}
|
|
||||||
</div>
|
|
||||||
<div className="flex items-center space-x-2">
|
|
||||||
<Button
|
|
||||||
variant="outline"
|
|
||||||
className="hidden h-8 w-8 p-0 lg:flex"
|
|
||||||
onClick={() => table.setPageIndex(0)}
|
|
||||||
disabled={!table.getCanPreviousPage()}
|
|
||||||
>
|
|
||||||
<span className="sr-only">Go to first page</span>
|
|
||||||
<ChevronsLeft />
|
|
||||||
</Button>
|
|
||||||
<Button
|
|
||||||
variant="outline"
|
|
||||||
className="h-8 w-8 p-0"
|
|
||||||
onClick={() => table.previousPage()}
|
|
||||||
disabled={!table.getCanPreviousPage()}
|
|
||||||
>
|
|
||||||
<span className="sr-only">Go to previous page</span>
|
|
||||||
<ChevronLeft />
|
|
||||||
</Button>
|
|
||||||
<Button
|
|
||||||
variant="outline"
|
|
||||||
className="h-8 w-8 p-0"
|
|
||||||
onClick={() => table.nextPage()}
|
|
||||||
disabled={!table.getCanNextPage()}
|
|
||||||
>
|
|
||||||
<span className="sr-only">Go to next page</span>
|
|
||||||
<ChevronRight />
|
|
||||||
</Button>
|
|
||||||
<Button
|
|
||||||
variant="outline"
|
|
||||||
className="hidden h-8 w-8 p-0 lg:flex"
|
|
||||||
onClick={() => table.setPageIndex(table.getPageCount() - 1)}
|
|
||||||
disabled={!table.getCanNextPage()}
|
|
||||||
>
|
|
||||||
<span className="sr-only">Go to last page</span>
|
|
||||||
<ChevronsRight />
|
|
||||||
</Button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
@ -1,70 +0,0 @@
|
|||||||
'use client';
|
|
||||||
|
|
||||||
import type { Row } from '@tanstack/react-table';
|
|
||||||
import { MoreHorizontal } from 'lucide-react';
|
|
||||||
|
|
||||||
import { Button } from '@/components/ui/button';
|
|
||||||
import {
|
|
||||||
DropdownMenu,
|
|
||||||
DropdownMenuContent,
|
|
||||||
DropdownMenuItem,
|
|
||||||
DropdownMenuSeparator,
|
|
||||||
DropdownMenuShortcut,
|
|
||||||
DropdownMenuTrigger,
|
|
||||||
} from '@/components/ui/dropdown-menu';
|
|
||||||
|
|
||||||
import { useMemo } from 'react';
|
|
||||||
|
|
||||||
interface DataTableRowActionsProps<DataView, Id> {
|
|
||||||
row: Row<DataView>;
|
|
||||||
getId: (row: Row<DataView>) => Id;
|
|
||||||
showDetail?: boolean;
|
|
||||||
showEdit?: boolean;
|
|
||||||
showDelete?: boolean;
|
|
||||||
onDetail?: (id: Id) => void;
|
|
||||||
onDelete?: (id: Id) => void;
|
|
||||||
onEdit?: (id: Id) => void;
|
|
||||||
}
|
|
||||||
|
|
||||||
export function DataTableRowActions<DataView, Id>({
|
|
||||||
row,
|
|
||||||
getId,
|
|
||||||
showDetail,
|
|
||||||
showDelete,
|
|
||||||
showEdit,
|
|
||||||
onDetail,
|
|
||||||
onDelete,
|
|
||||||
onEdit,
|
|
||||||
}: DataTableRowActionsProps<DataView, Id>) {
|
|
||||||
const id = useMemo(() => getId(row), [getId, row]);
|
|
||||||
return (
|
|
||||||
<DropdownMenu>
|
|
||||||
<DropdownMenuTrigger asChild>
|
|
||||||
<Button
|
|
||||||
variant="ghost"
|
|
||||||
className="flex h-8 w-8 p-0 data-[state=open]:bg-muted"
|
|
||||||
>
|
|
||||||
<MoreHorizontal />
|
|
||||||
<span className="sr-only">Open menu</span>
|
|
||||||
</Button>
|
|
||||||
</DropdownMenuTrigger>
|
|
||||||
<DropdownMenuContent align="end" className="w-[160px]">
|
|
||||||
{showDetail && (
|
|
||||||
<DropdownMenuItem onClick={() => onDetail?.(id)}>
|
|
||||||
Detail
|
|
||||||
</DropdownMenuItem>
|
|
||||||
)}
|
|
||||||
{showEdit && (
|
|
||||||
<DropdownMenuItem onClick={() => onEdit?.(id)}>Edit</DropdownMenuItem>
|
|
||||||
)}
|
|
||||||
{(showDetail || showEdit) && showDelete && <DropdownMenuSeparator />}
|
|
||||||
{showDelete && (
|
|
||||||
<DropdownMenuItem onClick={() => onDelete?.(id)}>
|
|
||||||
Delete
|
|
||||||
<DropdownMenuShortcut>⌘⌫</DropdownMenuShortcut>
|
|
||||||
</DropdownMenuItem>
|
|
||||||
)}
|
|
||||||
</DropdownMenuContent>
|
|
||||||
</DropdownMenu>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
@ -1,59 +0,0 @@
|
|||||||
"use client";
|
|
||||||
|
|
||||||
import { DropdownMenuTrigger } from "@radix-ui/react-dropdown-menu";
|
|
||||||
import { Table } from "@tanstack/react-table";
|
|
||||||
import { Settings2 } from "lucide-react";
|
|
||||||
|
|
||||||
import { Button } from "@/components/ui/button";
|
|
||||||
import {
|
|
||||||
DropdownMenu,
|
|
||||||
DropdownMenuCheckboxItem,
|
|
||||||
DropdownMenuContent,
|
|
||||||
DropdownMenuLabel,
|
|
||||||
DropdownMenuSeparator,
|
|
||||||
} from "@/components/ui/dropdown-menu";
|
|
||||||
|
|
||||||
interface DataTableViewOptionsProps<TData> {
|
|
||||||
table: Table<TData>;
|
|
||||||
}
|
|
||||||
|
|
||||||
export function DataTableViewOptions<TData>({
|
|
||||||
table,
|
|
||||||
}: DataTableViewOptionsProps<TData>) {
|
|
||||||
return (
|
|
||||||
<DropdownMenu>
|
|
||||||
<DropdownMenuTrigger asChild>
|
|
||||||
<Button
|
|
||||||
variant="outline"
|
|
||||||
size="sm"
|
|
||||||
className="ml-auto hidden h-8 lg:flex"
|
|
||||||
>
|
|
||||||
<Settings2 />
|
|
||||||
Columns
|
|
||||||
</Button>
|
|
||||||
</DropdownMenuTrigger>
|
|
||||||
<DropdownMenuContent align="end" className="w-[150px]">
|
|
||||||
<DropdownMenuLabel>Toggle columns</DropdownMenuLabel>
|
|
||||||
<DropdownMenuSeparator />
|
|
||||||
{table
|
|
||||||
.getAllColumns()
|
|
||||||
.filter(
|
|
||||||
(column) =>
|
|
||||||
typeof column.accessorFn !== "undefined" && column.getCanHide()
|
|
||||||
)
|
|
||||||
.map((column) => {
|
|
||||||
return (
|
|
||||||
<DropdownMenuCheckboxItem
|
|
||||||
key={column.id}
|
|
||||||
className="capitalize"
|
|
||||||
checked={column.getIsVisible()}
|
|
||||||
onCheckedChange={(value) => column.toggleVisibility(!!value)}
|
|
||||||
>
|
|
||||||
{column.id}
|
|
||||||
</DropdownMenuCheckboxItem>
|
|
||||||
);
|
|
||||||
})}
|
|
||||||
</DropdownMenuContent>
|
|
||||||
</DropdownMenu>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
@ -1,34 +0,0 @@
|
|||||||
import { Alert, AlertDescription, AlertTitle } from "@/components/ui/alert";
|
|
||||||
import { Button } from "@/components/ui/button";
|
|
||||||
import { AlertCircle } from "lucide-react";
|
|
||||||
|
|
||||||
export interface QueryErrorViewProps {
|
|
||||||
title?: string;
|
|
||||||
message: string;
|
|
||||||
onRetry?: () => void;
|
|
||||||
}
|
|
||||||
|
|
||||||
export function QueryErrorView({
|
|
||||||
title = "Error",
|
|
||||||
message,
|
|
||||||
onRetry,
|
|
||||||
}: QueryErrorViewProps) {
|
|
||||||
return (
|
|
||||||
<div className="container mx-auto flex h-[50vh] items-center justify-center">
|
|
||||||
<div className="w-full max-w-md">
|
|
||||||
<Alert variant="destructive">
|
|
||||||
<AlertCircle className="h-4 w-4" />
|
|
||||||
<AlertTitle>{title}</AlertTitle>
|
|
||||||
<AlertDescription>{message}</AlertDescription>
|
|
||||||
{onRetry && (
|
|
||||||
<div className="mt-4">
|
|
||||||
<Button variant="outline" onClick={() => onRetry()}>
|
|
||||||
Retry
|
|
||||||
</Button>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
</Alert>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
@ -1,87 +0,0 @@
|
|||||||
/* eslint-disable */
|
|
||||||
import { ResultOf, DocumentTypeDecoration, TypedDocumentNode } from '@graphql-typed-document-node/core';
|
|
||||||
import { FragmentDefinitionNode } from 'graphql';
|
|
||||||
import { Incremental } from './graphql';
|
|
||||||
|
|
||||||
|
|
||||||
export type FragmentType<TDocumentType extends DocumentTypeDecoration<any, any>> = TDocumentType extends DocumentTypeDecoration<
|
|
||||||
infer TType,
|
|
||||||
any
|
|
||||||
>
|
|
||||||
? [TType] extends [{ ' $fragmentName'?: infer TKey }]
|
|
||||||
? TKey extends string
|
|
||||||
? { ' $fragmentRefs'?: { [key in TKey]: TType } }
|
|
||||||
: never
|
|
||||||
: never
|
|
||||||
: never;
|
|
||||||
|
|
||||||
// return non-nullable if `fragmentType` is non-nullable
|
|
||||||
export function useFragment<TType>(
|
|
||||||
_documentNode: DocumentTypeDecoration<TType, any>,
|
|
||||||
fragmentType: FragmentType<DocumentTypeDecoration<TType, any>>
|
|
||||||
): TType;
|
|
||||||
// return nullable if `fragmentType` is undefined
|
|
||||||
export function useFragment<TType>(
|
|
||||||
_documentNode: DocumentTypeDecoration<TType, any>,
|
|
||||||
fragmentType: FragmentType<DocumentTypeDecoration<TType, any>> | undefined
|
|
||||||
): TType | undefined;
|
|
||||||
// return nullable if `fragmentType` is nullable
|
|
||||||
export function useFragment<TType>(
|
|
||||||
_documentNode: DocumentTypeDecoration<TType, any>,
|
|
||||||
fragmentType: FragmentType<DocumentTypeDecoration<TType, any>> | null
|
|
||||||
): TType | null;
|
|
||||||
// return nullable if `fragmentType` is nullable or undefined
|
|
||||||
export function useFragment<TType>(
|
|
||||||
_documentNode: DocumentTypeDecoration<TType, any>,
|
|
||||||
fragmentType: FragmentType<DocumentTypeDecoration<TType, any>> | null | undefined
|
|
||||||
): TType | null | undefined;
|
|
||||||
// return array of non-nullable if `fragmentType` is array of non-nullable
|
|
||||||
export function useFragment<TType>(
|
|
||||||
_documentNode: DocumentTypeDecoration<TType, any>,
|
|
||||||
fragmentType: Array<FragmentType<DocumentTypeDecoration<TType, any>>>
|
|
||||||
): Array<TType>;
|
|
||||||
// return array of nullable if `fragmentType` is array of nullable
|
|
||||||
export function useFragment<TType>(
|
|
||||||
_documentNode: DocumentTypeDecoration<TType, any>,
|
|
||||||
fragmentType: Array<FragmentType<DocumentTypeDecoration<TType, any>>> | null | undefined
|
|
||||||
): Array<TType> | null | undefined;
|
|
||||||
// return readonly array of non-nullable if `fragmentType` is array of non-nullable
|
|
||||||
export function useFragment<TType>(
|
|
||||||
_documentNode: DocumentTypeDecoration<TType, any>,
|
|
||||||
fragmentType: ReadonlyArray<FragmentType<DocumentTypeDecoration<TType, any>>>
|
|
||||||
): ReadonlyArray<TType>;
|
|
||||||
// return readonly array of nullable if `fragmentType` is array of nullable
|
|
||||||
export function useFragment<TType>(
|
|
||||||
_documentNode: DocumentTypeDecoration<TType, any>,
|
|
||||||
fragmentType: ReadonlyArray<FragmentType<DocumentTypeDecoration<TType, any>>> | null | undefined
|
|
||||||
): ReadonlyArray<TType> | null | undefined;
|
|
||||||
export function useFragment<TType>(
|
|
||||||
_documentNode: DocumentTypeDecoration<TType, any>,
|
|
||||||
fragmentType: FragmentType<DocumentTypeDecoration<TType, any>> | Array<FragmentType<DocumentTypeDecoration<TType, any>>> | ReadonlyArray<FragmentType<DocumentTypeDecoration<TType, any>>> | null | undefined
|
|
||||||
): TType | Array<TType> | ReadonlyArray<TType> | null | undefined {
|
|
||||||
return fragmentType as any;
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
export function makeFragmentData<
|
|
||||||
F extends DocumentTypeDecoration<any, any>,
|
|
||||||
FT extends ResultOf<F>
|
|
||||||
>(data: FT, _fragment: F): FragmentType<F> {
|
|
||||||
return data as FragmentType<F>;
|
|
||||||
}
|
|
||||||
export function isFragmentReady<TQuery, TFrag>(
|
|
||||||
queryNode: DocumentTypeDecoration<TQuery, any>,
|
|
||||||
fragmentNode: TypedDocumentNode<TFrag>,
|
|
||||||
data: FragmentType<TypedDocumentNode<Incremental<TFrag>, any>> | null | undefined
|
|
||||||
): data is FragmentType<typeof fragmentNode> {
|
|
||||||
const deferredFields = (queryNode as { __meta__?: { deferredFields: Record<string, (keyof TFrag)[]> } }).__meta__
|
|
||||||
?.deferredFields;
|
|
||||||
|
|
||||||
if (!deferredFields) return true;
|
|
||||||
|
|
||||||
const fragDef = fragmentNode.definitions[0] as FragmentDefinitionNode | undefined;
|
|
||||||
const fragName = fragDef?.name?.value;
|
|
||||||
|
|
||||||
const fields = (fragName && deferredFields[fragName]) || [];
|
|
||||||
return fields.length > 0 && fields.every(field => data && field in data);
|
|
||||||
}
|
|
||||||
@ -1,70 +0,0 @@
|
|||||||
/* eslint-disable */
|
|
||||||
import * as types from './graphql';
|
|
||||||
import { TypedDocumentNode as DocumentNode } from '@graphql-typed-document-node/core';
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Map of all GraphQL operations in the project.
|
|
||||||
*
|
|
||||||
* This map has several performance disadvantages:
|
|
||||||
* 1. It is not tree-shakeable, so it will include all operations in the project.
|
|
||||||
* 2. It is not minifiable, so the string of a GraphQL query will be multiple times inside the bundle.
|
|
||||||
* 3. It does not support dead code elimination, so it will add unused operations.
|
|
||||||
*
|
|
||||||
* Therefore it is highly recommended to use the babel or swc plugin for production.
|
|
||||||
* Learn more about it here: https://the-guild.dev/graphql/codegen/plugins/presets/preset-client#reducing-bundle-size
|
|
||||||
*/
|
|
||||||
type Documents = {
|
|
||||||
"\n query GetSubscriptions(\n $page: PageInput!,\n $filters: SubscriptionsFilterInput!,\n $orderBy: SubscriptionsOrderInput!\n) {\n subscriptions(\n pagination: {\n page: $page\n }\n filters: $filters\n orderBy: $orderBy\n ) {\n nodes {\n id\n createdAt\n updatedAt\n displayName\n category\n sourceUrl\n enabled\n }\n paginationInfo {\n total\n pages\n }\n }\n }\n": typeof types.GetSubscriptionsDocument,
|
|
||||||
"\n mutation UpdateSubscriptions(\n $data: SubscriptionsUpdateInput!,\n $filters: SubscriptionsFilterInput!,\n ) {\n subscriptionsUpdate (\n data: $data\n filter: $filters\n ) {\n id\n createdAt\n updatedAt\n displayName\n category\n sourceUrl\n enabled\n }\n}\n": typeof types.UpdateSubscriptionsDocument,
|
|
||||||
"\n mutation DeleteSubscriptions($filters: SubscriptionsFilterInput) {\n subscriptionsDelete(filter: $filters)\n }\n": typeof types.DeleteSubscriptionsDocument,
|
|
||||||
"\nquery GetSubscriptionDetail ($id: Int!) {\n subscriptions(filters: { id: {\n eq: $id\n } }) {\n nodes {\n id\n displayName\n createdAt\n updatedAt\n category\n sourceUrl\n enabled\n bangumi {\n nodes {\n createdAt\n updatedAt\n id\n mikanBangumiId\n displayName\n rawName\n season\n seasonRaw\n fansub\n mikanFansubId\n rssLink\n posterLink\n savePath\n deleted\n homepage\n }\n }\n }\n }\n}\n": typeof types.GetSubscriptionDetailDocument,
|
|
||||||
"\n mutation CreateSubscription($input: SubscriptionsInsertInput!) {\n subscriptionsCreateOne(data: $input) {\n id\n displayName\n sourceUrl\n enabled\n category\n }\n }\n": typeof types.CreateSubscriptionDocument,
|
|
||||||
};
|
|
||||||
const documents: Documents = {
|
|
||||||
"\n query GetSubscriptions(\n $page: PageInput!,\n $filters: SubscriptionsFilterInput!,\n $orderBy: SubscriptionsOrderInput!\n) {\n subscriptions(\n pagination: {\n page: $page\n }\n filters: $filters\n orderBy: $orderBy\n ) {\n nodes {\n id\n createdAt\n updatedAt\n displayName\n category\n sourceUrl\n enabled\n }\n paginationInfo {\n total\n pages\n }\n }\n }\n": types.GetSubscriptionsDocument,
|
|
||||||
"\n mutation UpdateSubscriptions(\n $data: SubscriptionsUpdateInput!,\n $filters: SubscriptionsFilterInput!,\n ) {\n subscriptionsUpdate (\n data: $data\n filter: $filters\n ) {\n id\n createdAt\n updatedAt\n displayName\n category\n sourceUrl\n enabled\n }\n}\n": types.UpdateSubscriptionsDocument,
|
|
||||||
"\n mutation DeleteSubscriptions($filters: SubscriptionsFilterInput) {\n subscriptionsDelete(filter: $filters)\n }\n": types.DeleteSubscriptionsDocument,
|
|
||||||
"\nquery GetSubscriptionDetail ($id: Int!) {\n subscriptions(filters: { id: {\n eq: $id\n } }) {\n nodes {\n id\n displayName\n createdAt\n updatedAt\n category\n sourceUrl\n enabled\n bangumi {\n nodes {\n createdAt\n updatedAt\n id\n mikanBangumiId\n displayName\n rawName\n season\n seasonRaw\n fansub\n mikanFansubId\n rssLink\n posterLink\n savePath\n deleted\n homepage\n }\n }\n }\n }\n}\n": types.GetSubscriptionDetailDocument,
|
|
||||||
"\n mutation CreateSubscription($input: SubscriptionsInsertInput!) {\n subscriptionsCreateOne(data: $input) {\n id\n displayName\n sourceUrl\n enabled\n category\n }\n }\n": types.CreateSubscriptionDocument,
|
|
||||||
};
|
|
||||||
|
|
||||||
/**
|
|
||||||
* The gql function is used to parse GraphQL queries into a document that can be used by GraphQL clients.
|
|
||||||
*
|
|
||||||
*
|
|
||||||
* @example
|
|
||||||
* ```ts
|
|
||||||
* const query = gql(`query GetUser($id: ID!) { user(id: $id) { name } }`);
|
|
||||||
* ```
|
|
||||||
*
|
|
||||||
* The query argument is unknown!
|
|
||||||
* Please regenerate the types.
|
|
||||||
*/
|
|
||||||
export function gql(source: string): unknown;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* The gql function is used to parse GraphQL queries into a document that can be used by GraphQL clients.
|
|
||||||
*/
|
|
||||||
export function gql(source: "\n query GetSubscriptions(\n $page: PageInput!,\n $filters: SubscriptionsFilterInput!,\n $orderBy: SubscriptionsOrderInput!\n) {\n subscriptions(\n pagination: {\n page: $page\n }\n filters: $filters\n orderBy: $orderBy\n ) {\n nodes {\n id\n createdAt\n updatedAt\n displayName\n category\n sourceUrl\n enabled\n }\n paginationInfo {\n total\n pages\n }\n }\n }\n"): (typeof documents)["\n query GetSubscriptions(\n $page: PageInput!,\n $filters: SubscriptionsFilterInput!,\n $orderBy: SubscriptionsOrderInput!\n) {\n subscriptions(\n pagination: {\n page: $page\n }\n filters: $filters\n orderBy: $orderBy\n ) {\n nodes {\n id\n createdAt\n updatedAt\n displayName\n category\n sourceUrl\n enabled\n }\n paginationInfo {\n total\n pages\n }\n }\n }\n"];
|
|
||||||
/**
|
|
||||||
* The gql function is used to parse GraphQL queries into a document that can be used by GraphQL clients.
|
|
||||||
*/
|
|
||||||
export function gql(source: "\n mutation UpdateSubscriptions(\n $data: SubscriptionsUpdateInput!,\n $filters: SubscriptionsFilterInput!,\n ) {\n subscriptionsUpdate (\n data: $data\n filter: $filters\n ) {\n id\n createdAt\n updatedAt\n displayName\n category\n sourceUrl\n enabled\n }\n}\n"): (typeof documents)["\n mutation UpdateSubscriptions(\n $data: SubscriptionsUpdateInput!,\n $filters: SubscriptionsFilterInput!,\n ) {\n subscriptionsUpdate (\n data: $data\n filter: $filters\n ) {\n id\n createdAt\n updatedAt\n displayName\n category\n sourceUrl\n enabled\n }\n}\n"];
|
|
||||||
/**
|
|
||||||
* The gql function is used to parse GraphQL queries into a document that can be used by GraphQL clients.
|
|
||||||
*/
|
|
||||||
export function gql(source: "\n mutation DeleteSubscriptions($filters: SubscriptionsFilterInput) {\n subscriptionsDelete(filter: $filters)\n }\n"): (typeof documents)["\n mutation DeleteSubscriptions($filters: SubscriptionsFilterInput) {\n subscriptionsDelete(filter: $filters)\n }\n"];
|
|
||||||
/**
|
|
||||||
* The gql function is used to parse GraphQL queries into a document that can be used by GraphQL clients.
|
|
||||||
*/
|
|
||||||
export function gql(source: "\nquery GetSubscriptionDetail ($id: Int!) {\n subscriptions(filters: { id: {\n eq: $id\n } }) {\n nodes {\n id\n displayName\n createdAt\n updatedAt\n category\n sourceUrl\n enabled\n bangumi {\n nodes {\n createdAt\n updatedAt\n id\n mikanBangumiId\n displayName\n rawName\n season\n seasonRaw\n fansub\n mikanFansubId\n rssLink\n posterLink\n savePath\n deleted\n homepage\n }\n }\n }\n }\n}\n"): (typeof documents)["\nquery GetSubscriptionDetail ($id: Int!) {\n subscriptions(filters: { id: {\n eq: $id\n } }) {\n nodes {\n id\n displayName\n createdAt\n updatedAt\n category\n sourceUrl\n enabled\n bangumi {\n nodes {\n createdAt\n updatedAt\n id\n mikanBangumiId\n displayName\n rawName\n season\n seasonRaw\n fansub\n mikanFansubId\n rssLink\n posterLink\n savePath\n deleted\n homepage\n }\n }\n }\n }\n}\n"];
|
|
||||||
/**
|
|
||||||
* The gql function is used to parse GraphQL queries into a document that can be used by GraphQL clients.
|
|
||||||
*/
|
|
||||||
export function gql(source: "\n mutation CreateSubscription($input: SubscriptionsInsertInput!) {\n subscriptionsCreateOne(data: $input) {\n id\n displayName\n sourceUrl\n enabled\n category\n }\n }\n"): (typeof documents)["\n mutation CreateSubscription($input: SubscriptionsInsertInput!) {\n subscriptionsCreateOne(data: $input) {\n id\n displayName\n sourceUrl\n enabled\n category\n }\n }\n"];
|
|
||||||
|
|
||||||
export function gql(source: string) {
|
|
||||||
return (documents as any)[source] ?? {};
|
|
||||||
}
|
|
||||||
|
|
||||||
export type DocumentType<TDocumentNode extends DocumentNode<any, any>> = TDocumentNode extends DocumentNode< infer TType, any> ? TType : never;
|
|
||||||
File diff suppressed because it is too large
Load Diff
@ -1,2 +0,0 @@
|
|||||||
export * from "./fragment-masking";
|
|
||||||
export * from "./gql";
|
|
||||||
@ -1,10 +0,0 @@
|
|||||||
import type { Injector } from '@outposts/injection-js';
|
|
||||||
import { IntlService } from './intl.service';
|
|
||||||
|
|
||||||
export function intlContextFromInjector(injector: Injector) {
|
|
||||||
const intlService = injector.get(IntlService);
|
|
||||||
|
|
||||||
return {
|
|
||||||
intlService,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
@ -1,9 +0,0 @@
|
|||||||
import { useInjector } from 'oidc-client-rx/adapters/react';
|
|
||||||
import { useMemo } from 'react';
|
|
||||||
import { intlContextFromInjector } from './context';
|
|
||||||
|
|
||||||
export function useIntl() {
|
|
||||||
const injector = useInjector();
|
|
||||||
|
|
||||||
return useMemo(() => intlContextFromInjector(injector), [injector]);
|
|
||||||
}
|
|
||||||
@ -1,27 +0,0 @@
|
|||||||
import { inject } from '@outposts/injection-js';
|
|
||||||
import { DOCUMENT } from '../platform/injection';
|
|
||||||
|
|
||||||
export class IntlService {
|
|
||||||
document = inject(DOCUMENT);
|
|
||||||
|
|
||||||
formatTimestamp(timestamp: number, options?: Intl.DateTimeFormatOptions) {
|
|
||||||
const defaultOptions: Intl.DateTimeFormatOptions = {
|
|
||||||
year: 'numeric',
|
|
||||||
month: '2-digit',
|
|
||||||
day: '2-digit',
|
|
||||||
hour: '2-digit',
|
|
||||||
minute: '2-digit',
|
|
||||||
second: '2-digit',
|
|
||||||
hour12: false,
|
|
||||||
...options,
|
|
||||||
};
|
|
||||||
|
|
||||||
return new Intl.DateTimeFormat(
|
|
||||||
this.document.defaultView?.navigator.language,
|
|
||||||
{
|
|
||||||
...defaultOptions,
|
|
||||||
...options,
|
|
||||||
}
|
|
||||||
).format(new Date(timestamp));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@ -1,4 +1,4 @@
|
|||||||
import type { ProLinkProps } from '@/components/ui/pro-link';
|
import type { ProLinkProps } from '@/views/components/ui/pro-link';
|
||||||
import type { Injector } from '@outposts/injection-js';
|
import type { Injector } from '@outposts/injection-js';
|
||||||
import type { LucideIcon } from 'lucide-react';
|
import type { LucideIcon } from 'lucide-react';
|
||||||
|
|
||||||
|
|||||||
@ -1,5 +1,5 @@
|
|||||||
import { guardRouteIndexAsNotFound } from '@/components/layout/app-not-found';
|
|
||||||
import type { RouteStateDataOption } from '@/infra/routes/traits';
|
import type { RouteStateDataOption } from '@/infra/routes/traits';
|
||||||
|
import { guardRouteIndexAsNotFound } from '@/views/components/layout/app-not-found';
|
||||||
import { Outlet } from '@tanstack/react-router';
|
import { Outlet } from '@tanstack/react-router';
|
||||||
|
|
||||||
export interface BuildVirtualBranchRouteOptions {
|
export interface BuildVirtualBranchRouteOptions {
|
||||||
|
|||||||
@ -1,7 +1,3 @@
|
|||||||
import type { Injector } from '@outposts/injection-js';
|
|
||||||
import { atomWithObservable } from 'jotai/utils';
|
|
||||||
import { useInjector } from 'oidc-client-rx/adapters/react';
|
|
||||||
import { useMemo } from 'react';
|
|
||||||
import { ThemeService } from './theme.service';
|
import { ThemeService } from './theme.service';
|
||||||
|
|
||||||
export function provideStyles() {
|
export function provideStyles() {
|
||||||
@ -12,37 +8,3 @@ export function provideStyles() {
|
|||||||
},
|
},
|
||||||
];
|
];
|
||||||
}
|
}
|
||||||
|
|
||||||
export function themeContextFromInjector(injector: Injector) {
|
|
||||||
const themeService = injector.get(ThemeService);
|
|
||||||
const systemColorSchema$ = atomWithObservable(
|
|
||||||
() => themeService.systemColorSchema$
|
|
||||||
);
|
|
||||||
return {
|
|
||||||
themeService,
|
|
||||||
systemColorSchema$,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
export function setupThemeContext(injector: Injector) {
|
|
||||||
const { themeService } = themeContextFromInjector(injector);
|
|
||||||
themeService.setup();
|
|
||||||
}
|
|
||||||
|
|
||||||
export function useTheme() {
|
|
||||||
const injector = useInjector();
|
|
||||||
|
|
||||||
const { themeService } = useMemo(() => {
|
|
||||||
return themeContextFromInjector(injector);
|
|
||||||
}, [injector]);
|
|
||||||
|
|
||||||
const colorTheme = useMemo(
|
|
||||||
() => atomWithObservable(() => themeService.colorSchema$),
|
|
||||||
[themeService]
|
|
||||||
);
|
|
||||||
|
|
||||||
return {
|
|
||||||
themeService,
|
|
||||||
colorTheme,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|||||||
@ -1,17 +1,7 @@
|
|||||||
import { DOCUMENT } from '@/infra/platform/injection';
|
import { DOCUMENT } from '@/infra/platform/injection';
|
||||||
import { LocalStorageService } from '@/infra/storage/web-storage.service';
|
import { LocalStorageService } from '@/infra/storage/web-storage.service';
|
||||||
import { Injectable, inject } from '@outposts/injection-js';
|
import { Injectable, inject } from '@outposts/injection-js';
|
||||||
import {
|
|
||||||
BehaviorSubject,
|
|
||||||
ReplaySubject,
|
|
||||||
combineLatest,
|
|
||||||
distinctUntilChanged,
|
|
||||||
filter,
|
|
||||||
fromEvent,
|
|
||||||
map,
|
|
||||||
shareReplay,
|
|
||||||
startWith,
|
|
||||||
} from 'rxjs';
|
|
||||||
export type PreferColorSchemaType = 'dark' | 'light' | 'system';
|
export type PreferColorSchemaType = 'dark' | 'light' | 'system';
|
||||||
export type PreferColorSchemaClass = 'dark' | 'light';
|
export type PreferColorSchemaClass = 'dark' | 'light';
|
||||||
|
|
||||||
@ -19,71 +9,6 @@ export type PreferColorSchemaClass = 'dark' | 'light';
|
|||||||
export class ThemeService {
|
export class ThemeService {
|
||||||
document = inject(DOCUMENT);
|
document = inject(DOCUMENT);
|
||||||
localStorage = inject(LocalStorageService);
|
localStorage = inject(LocalStorageService);
|
||||||
systemColorSchema$ = new BehaviorSubject(this.systemColorSchema);
|
|
||||||
storageColorSchema$ = new BehaviorSubject(
|
|
||||||
this.getColorSchemaType(this.localStorage.getItem('prefers-color-scheme'))
|
|
||||||
);
|
|
||||||
colorSchema$ = new BehaviorSubject(
|
|
||||||
this.getColorSchemaByType(
|
|
||||||
this.storageColorSchema$.value,
|
|
||||||
this.systemColorSchema$.value
|
|
||||||
)
|
|
||||||
);
|
|
||||||
|
|
||||||
setup() {
|
|
||||||
const mediaQuery = this.document.defaultView?.matchMedia(
|
|
||||||
'(prefers-color-scheme: dark)'
|
|
||||||
);
|
|
||||||
|
|
||||||
if (mediaQuery) {
|
|
||||||
fromEvent(mediaQuery, 'change')
|
|
||||||
.pipe(
|
|
||||||
map(() => (mediaQuery.matches ? 'dark' : 'light')),
|
|
||||||
startWith(this.systemColorSchema),
|
|
||||||
distinctUntilChanged()
|
|
||||||
)
|
|
||||||
.subscribe(this.systemColorSchema$);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (this.document.defaultView?.localStorage) {
|
|
||||||
fromEvent(this.document.defaultView, 'storage')
|
|
||||||
.pipe(
|
|
||||||
filter(
|
|
||||||
(e): e is StorageEvent =>
|
|
||||||
(e as StorageEvent)?.key === 'prefers-color-scheme'
|
|
||||||
),
|
|
||||||
map((event) => this.getColorSchemaType(event.newValue)),
|
|
||||||
distinctUntilChanged()
|
|
||||||
)
|
|
||||||
.subscribe(this.storageColorSchema$);
|
|
||||||
}
|
|
||||||
|
|
||||||
combineLatest({
|
|
||||||
system: this.systemColorSchema$,
|
|
||||||
storage: this.storageColorSchema$,
|
|
||||||
})
|
|
||||||
.pipe(
|
|
||||||
map(({ system, storage }) => this.getColorSchemaByType(storage, system))
|
|
||||||
)
|
|
||||||
.subscribe(this.colorSchema$);
|
|
||||||
}
|
|
||||||
|
|
||||||
private getColorSchemaType(themeType: string | null): PreferColorSchemaType {
|
|
||||||
if (themeType === 'dark' || themeType === 'light') {
|
|
||||||
return themeType as PreferColorSchemaType;
|
|
||||||
}
|
|
||||||
return 'system';
|
|
||||||
}
|
|
||||||
|
|
||||||
private getColorSchemaByType(
|
|
||||||
themeType: PreferColorSchemaType,
|
|
||||||
systemColorSchema: PreferColorSchemaClass
|
|
||||||
): PreferColorSchemaClass {
|
|
||||||
if (themeType === 'dark' || themeType === 'light') {
|
|
||||||
return themeType;
|
|
||||||
}
|
|
||||||
return systemColorSchema;
|
|
||||||
}
|
|
||||||
|
|
||||||
get systemColorSchema(): PreferColorSchemaClass {
|
get systemColorSchema(): PreferColorSchemaClass {
|
||||||
return this.document.defaultView?.matchMedia('(prefers-color-scheme: dark)')
|
return this.document.defaultView?.matchMedia('(prefers-color-scheme: dark)')
|
||||||
@ -92,16 +17,24 @@ export class ThemeService {
|
|||||||
: 'light';
|
: 'light';
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private getColorSchemaByType(
|
||||||
|
themeType: PreferColorSchemaType
|
||||||
|
): PreferColorSchemaClass {
|
||||||
|
this.document.documentElement.classList.remove('dark', 'light');
|
||||||
|
if (themeType === 'dark' || themeType === 'light') {
|
||||||
|
return themeType;
|
||||||
|
}
|
||||||
|
return this.systemColorSchema;
|
||||||
|
}
|
||||||
|
|
||||||
get colorSchema() {
|
get colorSchema() {
|
||||||
return this.colorSchema$.value;
|
const theme = this.localStorage.getItem('prefers-color-scheme');
|
||||||
|
return this.getColorSchemaByType(theme as PreferColorSchemaType);
|
||||||
}
|
}
|
||||||
|
|
||||||
set colorSchema(themeType: PreferColorSchemaType) {
|
set colorSchema(themeType: PreferColorSchemaType) {
|
||||||
this.localStorage.setItem('prefers-color-scheme', themeType);
|
this.localStorage.setItem('prefers-color-scheme', themeType);
|
||||||
const themeClass = this.getColorSchemaByType(
|
const themeClass = this.getColorSchemaByType(themeType);
|
||||||
themeType,
|
|
||||||
this.systemColorSchema
|
|
||||||
);
|
|
||||||
this.document.documentElement.classList.remove('dark', 'light');
|
this.document.documentElement.classList.remove('dark', 'light');
|
||||||
this.document.documentElement.classList.add(themeClass);
|
this.document.documentElement.classList.add(themeClass);
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,10 +1,10 @@
|
|||||||
import '@abraham/reflection';
|
import '@abraham/reflection';
|
||||||
import { provideAuth, setupAuthContext } from '@/app/auth/context';
|
import { provideAuth, setupAuthContext } from '@/app/auth/context';
|
||||||
import { AppNotFoundComponent } from '@/components/layout/app-not-found';
|
|
||||||
import { providePlatform } from '@/infra/platform/context';
|
import { providePlatform } from '@/infra/platform/context';
|
||||||
import { provideStorages } from '@/infra/storage/context';
|
import { provideStorages } from '@/infra/storage/context';
|
||||||
import { provideStyles } from '@/infra/styles/context';
|
import { provideStyles } from '@/infra/styles/context';
|
||||||
import { routeTree } from '@/presentation/routeTree.gen';
|
import { AppNotFoundComponent } from '@/views/components/layout/app-not-found';
|
||||||
|
import { routeTree } from '@/views/routeTree.gen';
|
||||||
import { type Injector, ReflectiveInjector } from '@outposts/injection-js';
|
import { type Injector, ReflectiveInjector } from '@outposts/injection-js';
|
||||||
import { RouterProvider, createRouter } from '@tanstack/react-router';
|
import { RouterProvider, createRouter } from '@tanstack/react-router';
|
||||||
import {
|
import {
|
||||||
|
|||||||
@ -1,40 +0,0 @@
|
|||||||
import { useEffect } from 'react';
|
|
||||||
import { useStateRef } from './use-state-ref.ts';
|
|
||||||
export interface UseDebouncedSkeletonProps {
|
|
||||||
minSkeletonDuration?: number;
|
|
||||||
loading?: boolean;
|
|
||||||
}
|
|
||||||
|
|
||||||
export function useDebouncedSkeleton({
|
|
||||||
minSkeletonDuration = 100,
|
|
||||||
loading,
|
|
||||||
}: UseDebouncedSkeletonProps) {
|
|
||||||
const [showSkeleton, setShowSkeleton, showSkeletonRef] = useStateRef(loading);
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
if (loading && !showSkeleton) {
|
|
||||||
setShowSkeleton(true);
|
|
||||||
}
|
|
||||||
if (!loading && showSkeleton) {
|
|
||||||
const timeout = setTimeout(() => {
|
|
||||||
if (showSkeletonRef.current) {
|
|
||||||
setShowSkeleton(false);
|
|
||||||
}
|
|
||||||
}, minSkeletonDuration);
|
|
||||||
|
|
||||||
return () => {
|
|
||||||
clearTimeout(timeout);
|
|
||||||
};
|
|
||||||
}
|
|
||||||
}, [
|
|
||||||
loading,
|
|
||||||
showSkeleton,
|
|
||||||
setShowSkeleton,
|
|
||||||
minSkeletonDuration,
|
|
||||||
showSkeletonRef,
|
|
||||||
]);
|
|
||||||
|
|
||||||
return {
|
|
||||||
showSkeleton,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
@ -1,18 +0,0 @@
|
|||||||
import { useCallback, useInsertionEffect, useRef } from 'react';
|
|
||||||
|
|
||||||
export function useEvent<
|
|
||||||
const T extends (
|
|
||||||
...args: // eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
||||||
any[]
|
|
||||||
) => void,
|
|
||||||
>(fn: T): T {
|
|
||||||
const ref = useRef<T | null>(fn);
|
|
||||||
useInsertionEffect(() => {
|
|
||||||
ref.current = fn;
|
|
||||||
}, [fn]);
|
|
||||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
||||||
return useCallback((...args: any) => {
|
|
||||||
const latestFn = ref.current!;
|
|
||||||
return latestFn(...args);
|
|
||||||
}, []) as unknown as T;
|
|
||||||
}
|
|
||||||
@ -1,28 +0,0 @@
|
|||||||
import {
|
|
||||||
type Dispatch,
|
|
||||||
type RefObject,
|
|
||||||
type SetStateAction,
|
|
||||||
useCallback,
|
|
||||||
useRef,
|
|
||||||
useState,
|
|
||||||
} from 'react';
|
|
||||||
|
|
||||||
export function useStateRef<T>(
|
|
||||||
initialValue: T
|
|
||||||
): [T, Dispatch<SetStateAction<T>>, RefObject<T>] {
|
|
||||||
const [state, _setState] = useState(initialValue);
|
|
||||||
const ref = useRef(initialValue);
|
|
||||||
|
|
||||||
const setState = useCallback((value: T | ((prev: T) => T)) => {
|
|
||||||
let nextValue: T;
|
|
||||||
if (typeof value === 'function') {
|
|
||||||
nextValue = (value as (prev: T) => T)(ref.current);
|
|
||||||
} else {
|
|
||||||
nextValue = value;
|
|
||||||
}
|
|
||||||
ref.current = nextValue;
|
|
||||||
_setState(nextValue);
|
|
||||||
}, []);
|
|
||||||
|
|
||||||
return [state, setState, ref] as const;
|
|
||||||
}
|
|
||||||
@ -1,663 +0,0 @@
|
|||||||
/* eslint-disable */
|
|
||||||
|
|
||||||
// @ts-nocheck
|
|
||||||
|
|
||||||
// noinspection JSUnusedGlobalSymbols
|
|
||||||
|
|
||||||
// This file was automatically generated by TanStack Router.
|
|
||||||
// You should NOT make any changes in this file as it will be overwritten.
|
|
||||||
// Additionally, you should also exclude this file from your linter and/or formatter to prevent it from being checked or modified.
|
|
||||||
|
|
||||||
// Import Routes
|
|
||||||
|
|
||||||
import { Route as R404Import } from './routes/404.tsx';
|
|
||||||
import { Route as rootRoute } from './routes/__root.tsx';
|
|
||||||
import { Route as AppExploreExploreImport } from './routes/_app/_explore/explore.tsx';
|
|
||||||
import { Route as AppExploreFeedImport } from './routes/_app/_explore/feed.tsx';
|
|
||||||
import { Route as AppBangumiManageImport } from './routes/_app/bangumi/manage.tsx';
|
|
||||||
import { Route as AppBangumiRouteImport } from './routes/_app/bangumi/route.tsx';
|
|
||||||
import { Route as AppPlaygroundGraphqlApiImport } from './routes/_app/playground/graphql-api.tsx';
|
|
||||||
import { Route as AppPlaygroundRouteImport } from './routes/_app/playground/route.tsx';
|
|
||||||
import { Route as AppRouteImport } from './routes/_app/route.tsx';
|
|
||||||
import { Route as AppSettingsDownloaderImport } from './routes/_app/settings/downloader.tsx';
|
|
||||||
import { Route as AppSettingsRouteImport } from './routes/_app/settings/route.tsx';
|
|
||||||
import { Route as AppSubscriptionsCreateImport } from './routes/_app/subscriptions/create.tsx';
|
|
||||||
import { Route as AppSubscriptionsDetailSubscriptionIdImport } from './routes/_app/subscriptions/detail.$subscriptionId.tsx';
|
|
||||||
import { Route as AppSubscriptionsEditSubscriptionIdImport } from './routes/_app/subscriptions/edit.$subscriptionId.tsx';
|
|
||||||
import { Route as AppSubscriptionsManageImport } from './routes/_app/subscriptions/manage.tsx';
|
|
||||||
import { Route as AppSubscriptionsRouteImport } from './routes/_app/subscriptions/route.tsx';
|
|
||||||
import { Route as AboutImport } from './routes/about.tsx';
|
|
||||||
import { Route as AuthOidcCallbackImport } from './routes/auth/oidc/callback.tsx';
|
|
||||||
import { Route as AuthSignInImport } from './routes/auth/sign-in.tsx';
|
|
||||||
import { Route as AuthSignUpImport } from './routes/auth/sign-up.tsx';
|
|
||||||
import { Route as IndexImport } from './routes/index.tsx';
|
|
||||||
|
|
||||||
// Create/Update Routes
|
|
||||||
|
|
||||||
const AboutRoute = AboutImport.update({
|
|
||||||
id: '/about',
|
|
||||||
path: '/about',
|
|
||||||
getParentRoute: () => rootRoute,
|
|
||||||
} as any);
|
|
||||||
|
|
||||||
const R404Route = R404Import.update({
|
|
||||||
id: '/404',
|
|
||||||
path: '/404',
|
|
||||||
getParentRoute: () => rootRoute,
|
|
||||||
} as any);
|
|
||||||
|
|
||||||
const AppRouteRoute = AppRouteImport.update({
|
|
||||||
id: '/_app',
|
|
||||||
getParentRoute: () => rootRoute,
|
|
||||||
} as any);
|
|
||||||
|
|
||||||
const IndexRoute = IndexImport.update({
|
|
||||||
id: '/',
|
|
||||||
path: '/',
|
|
||||||
getParentRoute: () => rootRoute,
|
|
||||||
} as any);
|
|
||||||
|
|
||||||
const AuthSignUpRoute = AuthSignUpImport.update({
|
|
||||||
id: '/auth/sign-up',
|
|
||||||
path: '/auth/sign-up',
|
|
||||||
getParentRoute: () => rootRoute,
|
|
||||||
} as any);
|
|
||||||
|
|
||||||
const AuthSignInRoute = AuthSignInImport.update({
|
|
||||||
id: '/auth/sign-in',
|
|
||||||
path: '/auth/sign-in',
|
|
||||||
getParentRoute: () => rootRoute,
|
|
||||||
} as any);
|
|
||||||
|
|
||||||
const AppSubscriptionsRouteRoute = AppSubscriptionsRouteImport.update({
|
|
||||||
id: '/subscriptions',
|
|
||||||
path: '/subscriptions',
|
|
||||||
getParentRoute: () => AppRouteRoute,
|
|
||||||
} as any);
|
|
||||||
|
|
||||||
const AppSettingsRouteRoute = AppSettingsRouteImport.update({
|
|
||||||
id: '/settings',
|
|
||||||
path: '/settings',
|
|
||||||
getParentRoute: () => AppRouteRoute,
|
|
||||||
} as any);
|
|
||||||
|
|
||||||
const AppPlaygroundRouteRoute = AppPlaygroundRouteImport.update({
|
|
||||||
id: '/playground',
|
|
||||||
path: '/playground',
|
|
||||||
getParentRoute: () => AppRouteRoute,
|
|
||||||
} as any);
|
|
||||||
|
|
||||||
const AppBangumiRouteRoute = AppBangumiRouteImport.update({
|
|
||||||
id: '/bangumi',
|
|
||||||
path: '/bangumi',
|
|
||||||
getParentRoute: () => AppRouteRoute,
|
|
||||||
} as any);
|
|
||||||
|
|
||||||
const AuthOidcCallbackRoute = AuthOidcCallbackImport.update({
|
|
||||||
id: '/auth/oidc/callback',
|
|
||||||
path: '/auth/oidc/callback',
|
|
||||||
getParentRoute: () => rootRoute,
|
|
||||||
} as any);
|
|
||||||
|
|
||||||
const AppSubscriptionsManageRoute = AppSubscriptionsManageImport.update({
|
|
||||||
id: '/manage',
|
|
||||||
path: '/manage',
|
|
||||||
getParentRoute: () => AppSubscriptionsRouteRoute,
|
|
||||||
} as any);
|
|
||||||
|
|
||||||
const AppSubscriptionsCreateRoute = AppSubscriptionsCreateImport.update({
|
|
||||||
id: '/create',
|
|
||||||
path: '/create',
|
|
||||||
getParentRoute: () => AppSubscriptionsRouteRoute,
|
|
||||||
} as any);
|
|
||||||
|
|
||||||
const AppSettingsDownloaderRoute = AppSettingsDownloaderImport.update({
|
|
||||||
id: '/downloader',
|
|
||||||
path: '/downloader',
|
|
||||||
getParentRoute: () => AppSettingsRouteRoute,
|
|
||||||
} as any);
|
|
||||||
|
|
||||||
const AppPlaygroundGraphqlApiRoute = AppPlaygroundGraphqlApiImport.update({
|
|
||||||
id: '/graphql-api',
|
|
||||||
path: '/graphql-api',
|
|
||||||
getParentRoute: () => AppPlaygroundRouteRoute,
|
|
||||||
} as any).lazy(() =>
|
|
||||||
import('./routes/_app/playground/graphql-api.lazy.tsx').then((d) => d.Route)
|
|
||||||
);
|
|
||||||
|
|
||||||
const AppBangumiManageRoute = AppBangumiManageImport.update({
|
|
||||||
id: '/manage',
|
|
||||||
path: '/manage',
|
|
||||||
getParentRoute: () => AppBangumiRouteRoute,
|
|
||||||
} as any);
|
|
||||||
|
|
||||||
const AppExploreFeedRoute = AppExploreFeedImport.update({
|
|
||||||
id: '/_explore/feed',
|
|
||||||
path: '/feed',
|
|
||||||
getParentRoute: () => AppRouteRoute,
|
|
||||||
} as any);
|
|
||||||
|
|
||||||
const AppExploreExploreRoute = AppExploreExploreImport.update({
|
|
||||||
id: '/_explore/explore',
|
|
||||||
path: '/explore',
|
|
||||||
getParentRoute: () => AppRouteRoute,
|
|
||||||
} as any);
|
|
||||||
|
|
||||||
const AppSubscriptionsEditSubscriptionIdRoute =
|
|
||||||
AppSubscriptionsEditSubscriptionIdImport.update({
|
|
||||||
id: '/edit/$subscriptionId',
|
|
||||||
path: '/edit/$subscriptionId',
|
|
||||||
getParentRoute: () => AppSubscriptionsRouteRoute,
|
|
||||||
} as any);
|
|
||||||
|
|
||||||
const AppSubscriptionsDetailSubscriptionIdRoute =
|
|
||||||
AppSubscriptionsDetailSubscriptionIdImport.update({
|
|
||||||
id: '/detail/$subscriptionId',
|
|
||||||
path: '/detail/$subscriptionId',
|
|
||||||
getParentRoute: () => AppSubscriptionsRouteRoute,
|
|
||||||
} as any);
|
|
||||||
|
|
||||||
// Populate the FileRoutesByPath interface
|
|
||||||
|
|
||||||
declare module '@tanstack/react-router' {
|
|
||||||
interface FileRoutesByPath {
|
|
||||||
'/': {
|
|
||||||
id: '/';
|
|
||||||
path: '/';
|
|
||||||
fullPath: '/';
|
|
||||||
preLoaderRoute: typeof IndexImport;
|
|
||||||
parentRoute: typeof rootRoute;
|
|
||||||
};
|
|
||||||
'/_app': {
|
|
||||||
id: '/_app';
|
|
||||||
path: '';
|
|
||||||
fullPath: '';
|
|
||||||
preLoaderRoute: typeof AppRouteImport;
|
|
||||||
parentRoute: typeof rootRoute;
|
|
||||||
};
|
|
||||||
'/404': {
|
|
||||||
id: '/404';
|
|
||||||
path: '/404';
|
|
||||||
fullPath: '/404';
|
|
||||||
preLoaderRoute: typeof R404Import;
|
|
||||||
parentRoute: typeof rootRoute;
|
|
||||||
};
|
|
||||||
'/about': {
|
|
||||||
id: '/about';
|
|
||||||
path: '/about';
|
|
||||||
fullPath: '/about';
|
|
||||||
preLoaderRoute: typeof AboutImport;
|
|
||||||
parentRoute: typeof rootRoute;
|
|
||||||
};
|
|
||||||
'/_app/bangumi': {
|
|
||||||
id: '/_app/bangumi';
|
|
||||||
path: '/bangumi';
|
|
||||||
fullPath: '/bangumi';
|
|
||||||
preLoaderRoute: typeof AppBangumiRouteImport;
|
|
||||||
parentRoute: typeof AppRouteImport;
|
|
||||||
};
|
|
||||||
'/_app/playground': {
|
|
||||||
id: '/_app/playground';
|
|
||||||
path: '/playground';
|
|
||||||
fullPath: '/playground';
|
|
||||||
preLoaderRoute: typeof AppPlaygroundRouteImport;
|
|
||||||
parentRoute: typeof AppRouteImport;
|
|
||||||
};
|
|
||||||
'/_app/settings': {
|
|
||||||
id: '/_app/settings';
|
|
||||||
path: '/settings';
|
|
||||||
fullPath: '/settings';
|
|
||||||
preLoaderRoute: typeof AppSettingsRouteImport;
|
|
||||||
parentRoute: typeof AppRouteImport;
|
|
||||||
};
|
|
||||||
'/_app/subscriptions': {
|
|
||||||
id: '/_app/subscriptions';
|
|
||||||
path: '/subscriptions';
|
|
||||||
fullPath: '/subscriptions';
|
|
||||||
preLoaderRoute: typeof AppSubscriptionsRouteImport;
|
|
||||||
parentRoute: typeof AppRouteImport;
|
|
||||||
};
|
|
||||||
'/auth/sign-in': {
|
|
||||||
id: '/auth/sign-in';
|
|
||||||
path: '/auth/sign-in';
|
|
||||||
fullPath: '/auth/sign-in';
|
|
||||||
preLoaderRoute: typeof AuthSignInImport;
|
|
||||||
parentRoute: typeof rootRoute;
|
|
||||||
};
|
|
||||||
'/auth/sign-up': {
|
|
||||||
id: '/auth/sign-up';
|
|
||||||
path: '/auth/sign-up';
|
|
||||||
fullPath: '/auth/sign-up';
|
|
||||||
preLoaderRoute: typeof AuthSignUpImport;
|
|
||||||
parentRoute: typeof rootRoute;
|
|
||||||
};
|
|
||||||
'/_app/_explore/explore': {
|
|
||||||
id: '/_app/_explore/explore';
|
|
||||||
path: '/explore';
|
|
||||||
fullPath: '/explore';
|
|
||||||
preLoaderRoute: typeof AppExploreExploreImport;
|
|
||||||
parentRoute: typeof AppRouteImport;
|
|
||||||
};
|
|
||||||
'/_app/_explore/feed': {
|
|
||||||
id: '/_app/_explore/feed';
|
|
||||||
path: '/feed';
|
|
||||||
fullPath: '/feed';
|
|
||||||
preLoaderRoute: typeof AppExploreFeedImport;
|
|
||||||
parentRoute: typeof AppRouteImport;
|
|
||||||
};
|
|
||||||
'/_app/bangumi/manage': {
|
|
||||||
id: '/_app/bangumi/manage';
|
|
||||||
path: '/manage';
|
|
||||||
fullPath: '/bangumi/manage';
|
|
||||||
preLoaderRoute: typeof AppBangumiManageImport;
|
|
||||||
parentRoute: typeof AppBangumiRouteImport;
|
|
||||||
};
|
|
||||||
'/_app/playground/graphql-api': {
|
|
||||||
id: '/_app/playground/graphql-api';
|
|
||||||
path: '/graphql-api';
|
|
||||||
fullPath: '/playground/graphql-api';
|
|
||||||
preLoaderRoute: typeof AppPlaygroundGraphqlApiImport;
|
|
||||||
parentRoute: typeof AppPlaygroundRouteImport;
|
|
||||||
};
|
|
||||||
'/_app/settings/downloader': {
|
|
||||||
id: '/_app/settings/downloader';
|
|
||||||
path: '/downloader';
|
|
||||||
fullPath: '/settings/downloader';
|
|
||||||
preLoaderRoute: typeof AppSettingsDownloaderImport;
|
|
||||||
parentRoute: typeof AppSettingsRouteImport;
|
|
||||||
};
|
|
||||||
'/_app/subscriptions/create': {
|
|
||||||
id: '/_app/subscriptions/create';
|
|
||||||
path: '/create';
|
|
||||||
fullPath: '/subscriptions/create';
|
|
||||||
preLoaderRoute: typeof AppSubscriptionsCreateImport;
|
|
||||||
parentRoute: typeof AppSubscriptionsRouteImport;
|
|
||||||
};
|
|
||||||
'/_app/subscriptions/manage': {
|
|
||||||
id: '/_app/subscriptions/manage';
|
|
||||||
path: '/manage';
|
|
||||||
fullPath: '/subscriptions/manage';
|
|
||||||
preLoaderRoute: typeof AppSubscriptionsManageImport;
|
|
||||||
parentRoute: typeof AppSubscriptionsRouteImport;
|
|
||||||
};
|
|
||||||
'/auth/oidc/callback': {
|
|
||||||
id: '/auth/oidc/callback';
|
|
||||||
path: '/auth/oidc/callback';
|
|
||||||
fullPath: '/auth/oidc/callback';
|
|
||||||
preLoaderRoute: typeof AuthOidcCallbackImport;
|
|
||||||
parentRoute: typeof rootRoute;
|
|
||||||
};
|
|
||||||
'/_app/subscriptions/detail/$subscriptionId': {
|
|
||||||
id: '/_app/subscriptions/detail/$subscriptionId';
|
|
||||||
path: '/detail/$subscriptionId';
|
|
||||||
fullPath: '/subscriptions/detail/$subscriptionId';
|
|
||||||
preLoaderRoute: typeof AppSubscriptionsDetailSubscriptionIdImport;
|
|
||||||
parentRoute: typeof AppSubscriptionsRouteImport;
|
|
||||||
};
|
|
||||||
'/_app/subscriptions/edit/$subscriptionId': {
|
|
||||||
id: '/_app/subscriptions/edit/$subscriptionId';
|
|
||||||
path: '/edit/$subscriptionId';
|
|
||||||
fullPath: '/subscriptions/edit/$subscriptionId';
|
|
||||||
preLoaderRoute: typeof AppSubscriptionsEditSubscriptionIdImport;
|
|
||||||
parentRoute: typeof AppSubscriptionsRouteImport;
|
|
||||||
};
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Create and export the route tree
|
|
||||||
|
|
||||||
interface AppBangumiRouteRouteChildren {
|
|
||||||
AppBangumiManageRoute: typeof AppBangumiManageRoute;
|
|
||||||
}
|
|
||||||
|
|
||||||
const AppBangumiRouteRouteChildren: AppBangumiRouteRouteChildren = {
|
|
||||||
AppBangumiManageRoute: AppBangumiManageRoute,
|
|
||||||
};
|
|
||||||
|
|
||||||
const AppBangumiRouteRouteWithChildren = AppBangumiRouteRoute._addFileChildren(
|
|
||||||
AppBangumiRouteRouteChildren
|
|
||||||
);
|
|
||||||
|
|
||||||
interface AppPlaygroundRouteRouteChildren {
|
|
||||||
AppPlaygroundGraphqlApiRoute: typeof AppPlaygroundGraphqlApiRoute;
|
|
||||||
}
|
|
||||||
|
|
||||||
const AppPlaygroundRouteRouteChildren: AppPlaygroundRouteRouteChildren = {
|
|
||||||
AppPlaygroundGraphqlApiRoute: AppPlaygroundGraphqlApiRoute,
|
|
||||||
};
|
|
||||||
|
|
||||||
const AppPlaygroundRouteRouteWithChildren =
|
|
||||||
AppPlaygroundRouteRoute._addFileChildren(AppPlaygroundRouteRouteChildren);
|
|
||||||
|
|
||||||
interface AppSettingsRouteRouteChildren {
|
|
||||||
AppSettingsDownloaderRoute: typeof AppSettingsDownloaderRoute;
|
|
||||||
}
|
|
||||||
|
|
||||||
const AppSettingsRouteRouteChildren: AppSettingsRouteRouteChildren = {
|
|
||||||
AppSettingsDownloaderRoute: AppSettingsDownloaderRoute,
|
|
||||||
};
|
|
||||||
|
|
||||||
const AppSettingsRouteRouteWithChildren =
|
|
||||||
AppSettingsRouteRoute._addFileChildren(AppSettingsRouteRouteChildren);
|
|
||||||
|
|
||||||
interface AppSubscriptionsRouteRouteChildren {
|
|
||||||
AppSubscriptionsCreateRoute: typeof AppSubscriptionsCreateRoute;
|
|
||||||
AppSubscriptionsManageRoute: typeof AppSubscriptionsManageRoute;
|
|
||||||
AppSubscriptionsDetailSubscriptionIdRoute: typeof AppSubscriptionsDetailSubscriptionIdRoute;
|
|
||||||
AppSubscriptionsEditSubscriptionIdRoute: typeof AppSubscriptionsEditSubscriptionIdRoute;
|
|
||||||
}
|
|
||||||
|
|
||||||
const AppSubscriptionsRouteRouteChildren: AppSubscriptionsRouteRouteChildren = {
|
|
||||||
AppSubscriptionsCreateRoute: AppSubscriptionsCreateRoute,
|
|
||||||
AppSubscriptionsManageRoute: AppSubscriptionsManageRoute,
|
|
||||||
AppSubscriptionsDetailSubscriptionIdRoute:
|
|
||||||
AppSubscriptionsDetailSubscriptionIdRoute,
|
|
||||||
AppSubscriptionsEditSubscriptionIdRoute:
|
|
||||||
AppSubscriptionsEditSubscriptionIdRoute,
|
|
||||||
};
|
|
||||||
|
|
||||||
const AppSubscriptionsRouteRouteWithChildren =
|
|
||||||
AppSubscriptionsRouteRoute._addFileChildren(
|
|
||||||
AppSubscriptionsRouteRouteChildren
|
|
||||||
);
|
|
||||||
|
|
||||||
interface AppRouteRouteChildren {
|
|
||||||
AppBangumiRouteRoute: typeof AppBangumiRouteRouteWithChildren;
|
|
||||||
AppPlaygroundRouteRoute: typeof AppPlaygroundRouteRouteWithChildren;
|
|
||||||
AppSettingsRouteRoute: typeof AppSettingsRouteRouteWithChildren;
|
|
||||||
AppSubscriptionsRouteRoute: typeof AppSubscriptionsRouteRouteWithChildren;
|
|
||||||
AppExploreExploreRoute: typeof AppExploreExploreRoute;
|
|
||||||
AppExploreFeedRoute: typeof AppExploreFeedRoute;
|
|
||||||
}
|
|
||||||
|
|
||||||
const AppRouteRouteChildren: AppRouteRouteChildren = {
|
|
||||||
AppBangumiRouteRoute: AppBangumiRouteRouteWithChildren,
|
|
||||||
AppPlaygroundRouteRoute: AppPlaygroundRouteRouteWithChildren,
|
|
||||||
AppSettingsRouteRoute: AppSettingsRouteRouteWithChildren,
|
|
||||||
AppSubscriptionsRouteRoute: AppSubscriptionsRouteRouteWithChildren,
|
|
||||||
AppExploreExploreRoute: AppExploreExploreRoute,
|
|
||||||
AppExploreFeedRoute: AppExploreFeedRoute,
|
|
||||||
};
|
|
||||||
|
|
||||||
const AppRouteRouteWithChildren = AppRouteRoute._addFileChildren(
|
|
||||||
AppRouteRouteChildren
|
|
||||||
);
|
|
||||||
|
|
||||||
export interface FileRoutesByFullPath {
|
|
||||||
'/': typeof IndexRoute;
|
|
||||||
'': typeof AppRouteRouteWithChildren;
|
|
||||||
'/404': typeof R404Route;
|
|
||||||
'/about': typeof AboutRoute;
|
|
||||||
'/bangumi': typeof AppBangumiRouteRouteWithChildren;
|
|
||||||
'/playground': typeof AppPlaygroundRouteRouteWithChildren;
|
|
||||||
'/settings': typeof AppSettingsRouteRouteWithChildren;
|
|
||||||
'/subscriptions': typeof AppSubscriptionsRouteRouteWithChildren;
|
|
||||||
'/auth/sign-in': typeof AuthSignInRoute;
|
|
||||||
'/auth/sign-up': typeof AuthSignUpRoute;
|
|
||||||
'/explore': typeof AppExploreExploreRoute;
|
|
||||||
'/feed': typeof AppExploreFeedRoute;
|
|
||||||
'/bangumi/manage': typeof AppBangumiManageRoute;
|
|
||||||
'/playground/graphql-api': typeof AppPlaygroundGraphqlApiRoute;
|
|
||||||
'/settings/downloader': typeof AppSettingsDownloaderRoute;
|
|
||||||
'/subscriptions/create': typeof AppSubscriptionsCreateRoute;
|
|
||||||
'/subscriptions/manage': typeof AppSubscriptionsManageRoute;
|
|
||||||
'/auth/oidc/callback': typeof AuthOidcCallbackRoute;
|
|
||||||
'/subscriptions/detail/$subscriptionId': typeof AppSubscriptionsDetailSubscriptionIdRoute;
|
|
||||||
'/subscriptions/edit/$subscriptionId': typeof AppSubscriptionsEditSubscriptionIdRoute;
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface FileRoutesByTo {
|
|
||||||
'/': typeof IndexRoute;
|
|
||||||
'': typeof AppRouteRouteWithChildren;
|
|
||||||
'/404': typeof R404Route;
|
|
||||||
'/about': typeof AboutRoute;
|
|
||||||
'/bangumi': typeof AppBangumiRouteRouteWithChildren;
|
|
||||||
'/playground': typeof AppPlaygroundRouteRouteWithChildren;
|
|
||||||
'/settings': typeof AppSettingsRouteRouteWithChildren;
|
|
||||||
'/subscriptions': typeof AppSubscriptionsRouteRouteWithChildren;
|
|
||||||
'/auth/sign-in': typeof AuthSignInRoute;
|
|
||||||
'/auth/sign-up': typeof AuthSignUpRoute;
|
|
||||||
'/explore': typeof AppExploreExploreRoute;
|
|
||||||
'/feed': typeof AppExploreFeedRoute;
|
|
||||||
'/bangumi/manage': typeof AppBangumiManageRoute;
|
|
||||||
'/playground/graphql-api': typeof AppPlaygroundGraphqlApiRoute;
|
|
||||||
'/settings/downloader': typeof AppSettingsDownloaderRoute;
|
|
||||||
'/subscriptions/create': typeof AppSubscriptionsCreateRoute;
|
|
||||||
'/subscriptions/manage': typeof AppSubscriptionsManageRoute;
|
|
||||||
'/auth/oidc/callback': typeof AuthOidcCallbackRoute;
|
|
||||||
'/subscriptions/detail/$subscriptionId': typeof AppSubscriptionsDetailSubscriptionIdRoute;
|
|
||||||
'/subscriptions/edit/$subscriptionId': typeof AppSubscriptionsEditSubscriptionIdRoute;
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface FileRoutesById {
|
|
||||||
__root__: typeof rootRoute;
|
|
||||||
'/': typeof IndexRoute;
|
|
||||||
'/_app': typeof AppRouteRouteWithChildren;
|
|
||||||
'/404': typeof R404Route;
|
|
||||||
'/about': typeof AboutRoute;
|
|
||||||
'/_app/bangumi': typeof AppBangumiRouteRouteWithChildren;
|
|
||||||
'/_app/playground': typeof AppPlaygroundRouteRouteWithChildren;
|
|
||||||
'/_app/settings': typeof AppSettingsRouteRouteWithChildren;
|
|
||||||
'/_app/subscriptions': typeof AppSubscriptionsRouteRouteWithChildren;
|
|
||||||
'/auth/sign-in': typeof AuthSignInRoute;
|
|
||||||
'/auth/sign-up': typeof AuthSignUpRoute;
|
|
||||||
'/_app/_explore/explore': typeof AppExploreExploreRoute;
|
|
||||||
'/_app/_explore/feed': typeof AppExploreFeedRoute;
|
|
||||||
'/_app/bangumi/manage': typeof AppBangumiManageRoute;
|
|
||||||
'/_app/playground/graphql-api': typeof AppPlaygroundGraphqlApiRoute;
|
|
||||||
'/_app/settings/downloader': typeof AppSettingsDownloaderRoute;
|
|
||||||
'/_app/subscriptions/create': typeof AppSubscriptionsCreateRoute;
|
|
||||||
'/_app/subscriptions/manage': typeof AppSubscriptionsManageRoute;
|
|
||||||
'/auth/oidc/callback': typeof AuthOidcCallbackRoute;
|
|
||||||
'/_app/subscriptions/detail/$subscriptionId': typeof AppSubscriptionsDetailSubscriptionIdRoute;
|
|
||||||
'/_app/subscriptions/edit/$subscriptionId': typeof AppSubscriptionsEditSubscriptionIdRoute;
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface FileRouteTypes {
|
|
||||||
fileRoutesByFullPath: FileRoutesByFullPath;
|
|
||||||
fullPaths:
|
|
||||||
| '/'
|
|
||||||
| ''
|
|
||||||
| '/404'
|
|
||||||
| '/about'
|
|
||||||
| '/bangumi'
|
|
||||||
| '/playground'
|
|
||||||
| '/settings'
|
|
||||||
| '/subscriptions'
|
|
||||||
| '/auth/sign-in'
|
|
||||||
| '/auth/sign-up'
|
|
||||||
| '/explore'
|
|
||||||
| '/feed'
|
|
||||||
| '/bangumi/manage'
|
|
||||||
| '/playground/graphql-api'
|
|
||||||
| '/settings/downloader'
|
|
||||||
| '/subscriptions/create'
|
|
||||||
| '/subscriptions/manage'
|
|
||||||
| '/auth/oidc/callback'
|
|
||||||
| '/subscriptions/detail/$subscriptionId'
|
|
||||||
| '/subscriptions/edit/$subscriptionId';
|
|
||||||
fileRoutesByTo: FileRoutesByTo;
|
|
||||||
to:
|
|
||||||
| '/'
|
|
||||||
| ''
|
|
||||||
| '/404'
|
|
||||||
| '/about'
|
|
||||||
| '/bangumi'
|
|
||||||
| '/playground'
|
|
||||||
| '/settings'
|
|
||||||
| '/subscriptions'
|
|
||||||
| '/auth/sign-in'
|
|
||||||
| '/auth/sign-up'
|
|
||||||
| '/explore'
|
|
||||||
| '/feed'
|
|
||||||
| '/bangumi/manage'
|
|
||||||
| '/playground/graphql-api'
|
|
||||||
| '/settings/downloader'
|
|
||||||
| '/subscriptions/create'
|
|
||||||
| '/subscriptions/manage'
|
|
||||||
| '/auth/oidc/callback'
|
|
||||||
| '/subscriptions/detail/$subscriptionId'
|
|
||||||
| '/subscriptions/edit/$subscriptionId';
|
|
||||||
id:
|
|
||||||
| '__root__'
|
|
||||||
| '/'
|
|
||||||
| '/_app'
|
|
||||||
| '/404'
|
|
||||||
| '/about'
|
|
||||||
| '/_app/bangumi'
|
|
||||||
| '/_app/playground'
|
|
||||||
| '/_app/settings'
|
|
||||||
| '/_app/subscriptions'
|
|
||||||
| '/auth/sign-in'
|
|
||||||
| '/auth/sign-up'
|
|
||||||
| '/_app/_explore/explore'
|
|
||||||
| '/_app/_explore/feed'
|
|
||||||
| '/_app/bangumi/manage'
|
|
||||||
| '/_app/playground/graphql-api'
|
|
||||||
| '/_app/settings/downloader'
|
|
||||||
| '/_app/subscriptions/create'
|
|
||||||
| '/_app/subscriptions/manage'
|
|
||||||
| '/auth/oidc/callback'
|
|
||||||
| '/_app/subscriptions/detail/$subscriptionId'
|
|
||||||
| '/_app/subscriptions/edit/$subscriptionId';
|
|
||||||
fileRoutesById: FileRoutesById;
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface RootRouteChildren {
|
|
||||||
IndexRoute: typeof IndexRoute;
|
|
||||||
AppRouteRoute: typeof AppRouteRouteWithChildren;
|
|
||||||
R404Route: typeof R404Route;
|
|
||||||
AboutRoute: typeof AboutRoute;
|
|
||||||
AuthSignInRoute: typeof AuthSignInRoute;
|
|
||||||
AuthSignUpRoute: typeof AuthSignUpRoute;
|
|
||||||
AuthOidcCallbackRoute: typeof AuthOidcCallbackRoute;
|
|
||||||
}
|
|
||||||
|
|
||||||
const rootRouteChildren: RootRouteChildren = {
|
|
||||||
IndexRoute: IndexRoute,
|
|
||||||
AppRouteRoute: AppRouteRouteWithChildren,
|
|
||||||
R404Route: R404Route,
|
|
||||||
AboutRoute: AboutRoute,
|
|
||||||
AuthSignInRoute: AuthSignInRoute,
|
|
||||||
AuthSignUpRoute: AuthSignUpRoute,
|
|
||||||
AuthOidcCallbackRoute: AuthOidcCallbackRoute,
|
|
||||||
};
|
|
||||||
|
|
||||||
export const routeTree = rootRoute
|
|
||||||
._addFileChildren(rootRouteChildren)
|
|
||||||
._addFileTypes<FileRouteTypes>();
|
|
||||||
|
|
||||||
/* ROUTE_MANIFEST_START
|
|
||||||
{
|
|
||||||
"routes": {
|
|
||||||
"__root__": {
|
|
||||||
"filePath": "__root.tsx",
|
|
||||||
"children": [
|
|
||||||
"/",
|
|
||||||
"/_app",
|
|
||||||
"/404",
|
|
||||||
"/about",
|
|
||||||
"/auth/sign-in",
|
|
||||||
"/auth/sign-up",
|
|
||||||
"/auth/oidc/callback"
|
|
||||||
]
|
|
||||||
},
|
|
||||||
"/": {
|
|
||||||
"filePath": "index.tsx"
|
|
||||||
},
|
|
||||||
"/_app": {
|
|
||||||
"filePath": "_app/route.tsx",
|
|
||||||
"children": [
|
|
||||||
"/_app/bangumi",
|
|
||||||
"/_app/playground",
|
|
||||||
"/_app/settings",
|
|
||||||
"/_app/subscriptions",
|
|
||||||
"/_app/_explore/explore",
|
|
||||||
"/_app/_explore/feed"
|
|
||||||
]
|
|
||||||
},
|
|
||||||
"/404": {
|
|
||||||
"filePath": "404.tsx"
|
|
||||||
},
|
|
||||||
"/about": {
|
|
||||||
"filePath": "about.tsx"
|
|
||||||
},
|
|
||||||
"/_app/bangumi": {
|
|
||||||
"filePath": "_app/bangumi/route.tsx",
|
|
||||||
"parent": "/_app",
|
|
||||||
"children": [
|
|
||||||
"/_app/bangumi/manage"
|
|
||||||
]
|
|
||||||
},
|
|
||||||
"/_app/playground": {
|
|
||||||
"filePath": "_app/playground/route.tsx",
|
|
||||||
"parent": "/_app",
|
|
||||||
"children": [
|
|
||||||
"/_app/playground/graphql-api"
|
|
||||||
]
|
|
||||||
},
|
|
||||||
"/_app/settings": {
|
|
||||||
"filePath": "_app/settings/route.tsx",
|
|
||||||
"parent": "/_app",
|
|
||||||
"children": [
|
|
||||||
"/_app/settings/downloader"
|
|
||||||
]
|
|
||||||
},
|
|
||||||
"/_app/subscriptions": {
|
|
||||||
"filePath": "_app/subscriptions/route.tsx",
|
|
||||||
"parent": "/_app",
|
|
||||||
"children": [
|
|
||||||
"/_app/subscriptions/create",
|
|
||||||
"/_app/subscriptions/manage",
|
|
||||||
"/_app/subscriptions/detail/$subscriptionId",
|
|
||||||
"/_app/subscriptions/edit/$subscriptionId"
|
|
||||||
]
|
|
||||||
},
|
|
||||||
"/auth/sign-in": {
|
|
||||||
"filePath": "auth/sign-in.tsx"
|
|
||||||
},
|
|
||||||
"/auth/sign-up": {
|
|
||||||
"filePath": "auth/sign-up.tsx"
|
|
||||||
},
|
|
||||||
"/_app/_explore/explore": {
|
|
||||||
"filePath": "_app/_explore/explore.tsx",
|
|
||||||
"parent": "/_app"
|
|
||||||
},
|
|
||||||
"/_app/_explore/feed": {
|
|
||||||
"filePath": "_app/_explore/feed.tsx",
|
|
||||||
"parent": "/_app"
|
|
||||||
},
|
|
||||||
"/_app/bangumi/manage": {
|
|
||||||
"filePath": "_app/bangumi/manage.tsx",
|
|
||||||
"parent": "/_app/bangumi"
|
|
||||||
},
|
|
||||||
"/_app/playground/graphql-api": {
|
|
||||||
"filePath": "_app/playground/graphql-api.tsx",
|
|
||||||
"parent": "/_app/playground"
|
|
||||||
},
|
|
||||||
"/_app/settings/downloader": {
|
|
||||||
"filePath": "_app/settings/downloader.tsx",
|
|
||||||
"parent": "/_app/settings"
|
|
||||||
},
|
|
||||||
"/_app/subscriptions/create": {
|
|
||||||
"filePath": "_app/subscriptions/create.tsx",
|
|
||||||
"parent": "/_app/subscriptions"
|
|
||||||
},
|
|
||||||
"/_app/subscriptions/manage": {
|
|
||||||
"filePath": "_app/subscriptions/manage.tsx",
|
|
||||||
"parent": "/_app/subscriptions"
|
|
||||||
},
|
|
||||||
"/auth/oidc/callback": {
|
|
||||||
"filePath": "auth/oidc/callback.tsx"
|
|
||||||
},
|
|
||||||
"/_app/subscriptions/detail/$subscriptionId": {
|
|
||||||
"filePath": "_app/subscriptions/detail.$subscriptionId.tsx",
|
|
||||||
"parent": "/_app/subscriptions"
|
|
||||||
},
|
|
||||||
"/_app/subscriptions/edit/$subscriptionId": {
|
|
||||||
"filePath": "_app/subscriptions/edit.$subscriptionId.tsx",
|
|
||||||
"parent": "/_app/subscriptions"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
ROUTE_MANIFEST_END */
|
|
||||||
@ -1,8 +0,0 @@
|
|||||||
import { buildVirtualBranchRouteOptions } from '@/infra/routes/utils';
|
|
||||||
import { createFileRoute } from '@tanstack/react-router';
|
|
||||||
|
|
||||||
export const Route = createFileRoute('/_app/bangumi')(
|
|
||||||
buildVirtualBranchRouteOptions({
|
|
||||||
title: 'Bangumi',
|
|
||||||
})
|
|
||||||
);
|
|
||||||
@ -1,108 +0,0 @@
|
|||||||
import { gql } from '@apollo/client';
|
|
||||||
|
|
||||||
import type {
|
|
||||||
GetSubscriptionDetailQuery,
|
|
||||||
GetSubscriptionsQuery,
|
|
||||||
} from '@/infra/graphql/gql/graphql';
|
|
||||||
|
|
||||||
export const GET_SUBSCRIPTIONS = gql`
|
|
||||||
query GetSubscriptions(
|
|
||||||
$page: PageInput!,
|
|
||||||
$filters: SubscriptionsFilterInput!,
|
|
||||||
$orderBy: SubscriptionsOrderInput!
|
|
||||||
) {
|
|
||||||
subscriptions(
|
|
||||||
pagination: {
|
|
||||||
page: $page
|
|
||||||
}
|
|
||||||
filters: $filters
|
|
||||||
orderBy: $orderBy
|
|
||||||
) {
|
|
||||||
nodes {
|
|
||||||
id
|
|
||||||
createdAt
|
|
||||||
updatedAt
|
|
||||||
displayName
|
|
||||||
category
|
|
||||||
sourceUrl
|
|
||||||
enabled
|
|
||||||
}
|
|
||||||
paginationInfo {
|
|
||||||
total
|
|
||||||
pages
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
`;
|
|
||||||
|
|
||||||
export type SubscriptionDto =
|
|
||||||
GetSubscriptionsQuery['subscriptions']['nodes'][number];
|
|
||||||
|
|
||||||
export const UPDATE_SUBSCRIPTIONS = gql`
|
|
||||||
mutation UpdateSubscriptions(
|
|
||||||
$data: SubscriptionsUpdateInput!,
|
|
||||||
$filters: SubscriptionsFilterInput!,
|
|
||||||
) {
|
|
||||||
subscriptionsUpdate (
|
|
||||||
data: $data
|
|
||||||
filter: $filters
|
|
||||||
) {
|
|
||||||
id
|
|
||||||
createdAt
|
|
||||||
updatedAt
|
|
||||||
displayName
|
|
||||||
category
|
|
||||||
sourceUrl
|
|
||||||
enabled
|
|
||||||
}
|
|
||||||
}
|
|
||||||
`;
|
|
||||||
|
|
||||||
export const DELETE_SUBSCRIPTIONS = gql`
|
|
||||||
mutation DeleteSubscriptions($filters: SubscriptionsFilterInput) {
|
|
||||||
subscriptionsDelete(filter: $filters)
|
|
||||||
}
|
|
||||||
`;
|
|
||||||
|
|
||||||
export const GET_SUBSCRIPTION_DETAIL = gql`
|
|
||||||
query GetSubscriptionDetail ($id: Int!) {
|
|
||||||
subscriptions(filters: { id: {
|
|
||||||
eq: $id
|
|
||||||
} }) {
|
|
||||||
nodes {
|
|
||||||
id
|
|
||||||
displayName
|
|
||||||
createdAt
|
|
||||||
updatedAt
|
|
||||||
category
|
|
||||||
sourceUrl
|
|
||||||
enabled
|
|
||||||
bangumi {
|
|
||||||
nodes {
|
|
||||||
createdAt
|
|
||||||
updatedAt
|
|
||||||
id
|
|
||||||
mikanBangumiId
|
|
||||||
displayName
|
|
||||||
rawName
|
|
||||||
season
|
|
||||||
seasonRaw
|
|
||||||
fansub
|
|
||||||
mikanFansubId
|
|
||||||
rssLink
|
|
||||||
posterLink
|
|
||||||
savePath
|
|
||||||
deleted
|
|
||||||
homepage
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
`;
|
|
||||||
|
|
||||||
export type SubscriptionDetailDto =
|
|
||||||
GetSubscriptionDetailQuery['subscriptions']['nodes'][number];
|
|
||||||
|
|
||||||
export type SubscriptionDetailBangumiDto =
|
|
||||||
SubscriptionDetailDto['bangumi']['nodes'][number];
|
|
||||||
@ -1,38 +0,0 @@
|
|||||||
import type { GetSubscriptionDetailQuery } from '@/infra/graphql/gql/graphql';
|
|
||||||
import { useQuery } from '@apollo/client';
|
|
||||||
import { createFileRoute } from '@tanstack/react-router';
|
|
||||||
import { GET_SUBSCRIPTION_DETAIL } from './-defs.ts';
|
|
||||||
|
|
||||||
export const Route = createFileRoute(
|
|
||||||
'/_app/subscriptions/detail/$subscriptionId'
|
|
||||||
)({
|
|
||||||
component: DetailRouteComponent,
|
|
||||||
});
|
|
||||||
|
|
||||||
function DetailRouteComponent() {
|
|
||||||
const { subscriptionId } = Route.useParams();
|
|
||||||
const { data, loading, error } = useQuery<GetSubscriptionDetailQuery>(
|
|
||||||
GET_SUBSCRIPTION_DETAIL,
|
|
||||||
{
|
|
||||||
variables: {
|
|
||||||
id: Number.parseInt(subscriptionId),
|
|
||||||
},
|
|
||||||
}
|
|
||||||
);
|
|
||||||
|
|
||||||
if (loading) {
|
|
||||||
return <div>Loading...</div>;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (error) {
|
|
||||||
return <div>Error: {error.message}</div>;
|
|
||||||
}
|
|
||||||
|
|
||||||
const detail = data?.subscriptions?.nodes?.[0];
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div
|
|
||||||
dangerouslySetInnerHTML={{ __html: JSON.stringify(detail, null, 2) }}
|
|
||||||
/>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
@ -1,289 +0,0 @@
|
|||||||
import { DataTablePagination } from '@/components/ui/data-table-pagination';
|
|
||||||
import { DataTableViewOptions } from '@/components/ui/data-table-view-options';
|
|
||||||
import { QueryErrorView } from '@/components/ui/query-error-view.tsx';
|
|
||||||
import { Skeleton } from '@/components/ui/skeleton.tsx';
|
|
||||||
import { Switch } from '@/components/ui/switch';
|
|
||||||
import {
|
|
||||||
Table,
|
|
||||||
TableBody,
|
|
||||||
TableCell,
|
|
||||||
TableHead,
|
|
||||||
TableHeader,
|
|
||||||
TableRow,
|
|
||||||
} from '@/components/ui/table';
|
|
||||||
import type {
|
|
||||||
GetSubscriptionsQuery,
|
|
||||||
SubscriptionsUpdateInput,
|
|
||||||
} from '@/infra/graphql/gql/graphql';
|
|
||||||
import type { RouteStateDataOption } from '@/infra/routes/traits';
|
|
||||||
import { useDebouncedSkeleton } from '@/presentation/hooks/use-debounded-skeleton.ts';
|
|
||||||
import { useEvent } from '@/presentation/hooks/use-event.ts';
|
|
||||||
import { useMutation, useQuery } from '@apollo/client';
|
|
||||||
import { createFileRoute } from '@tanstack/react-router';
|
|
||||||
import { useNavigate } from '@tanstack/react-router';
|
|
||||||
import {
|
|
||||||
type ColumnDef,
|
|
||||||
type PaginationState,
|
|
||||||
type Row,
|
|
||||||
type SortingState,
|
|
||||||
type VisibilityState,
|
|
||||||
flexRender,
|
|
||||||
getCoreRowModel,
|
|
||||||
getPaginationRowModel,
|
|
||||||
useReactTable,
|
|
||||||
} from '@tanstack/react-table';
|
|
||||||
import { useMemo, useState } from 'react';
|
|
||||||
import { toast } from 'sonner';
|
|
||||||
import { DataTableRowActions } from '../../../../components/ui/data-table-row-actions.tsx';
|
|
||||||
import {
|
|
||||||
DELETE_SUBSCRIPTIONS,
|
|
||||||
GET_SUBSCRIPTIONS,
|
|
||||||
type SubscriptionDto,
|
|
||||||
UPDATE_SUBSCRIPTIONS,
|
|
||||||
} from './-defs.ts';
|
|
||||||
|
|
||||||
export const Route = createFileRoute('/_app/subscriptions/manage')({
|
|
||||||
component: SubscriptionManageRouteComponent,
|
|
||||||
staticData: {
|
|
||||||
breadcrumb: { label: 'Manage' },
|
|
||||||
} satisfies RouteStateDataOption,
|
|
||||||
});
|
|
||||||
|
|
||||||
function SubscriptionManageRouteComponent() {
|
|
||||||
const navigate = useNavigate();
|
|
||||||
|
|
||||||
const [columnVisibility, setColumnVisibility] = useState<VisibilityState>({});
|
|
||||||
const [sorting, setSorting] = useState<SortingState>([]);
|
|
||||||
const [pagination, setPagination] = useState<PaginationState>({
|
|
||||||
pageIndex: 0,
|
|
||||||
pageSize: 10,
|
|
||||||
});
|
|
||||||
|
|
||||||
const { loading, error, data, refetch } = useQuery<GetSubscriptionsQuery>(
|
|
||||||
GET_SUBSCRIPTIONS,
|
|
||||||
{
|
|
||||||
variables: {
|
|
||||||
page: {
|
|
||||||
page: pagination.pageIndex,
|
|
||||||
limit: pagination.pageSize,
|
|
||||||
},
|
|
||||||
filters: {},
|
|
||||||
orderBy: {},
|
|
||||||
},
|
|
||||||
refetchWritePolicy: 'overwrite',
|
|
||||||
nextFetchPolicy: 'network-only',
|
|
||||||
}
|
|
||||||
);
|
|
||||||
const [updateSubscription] = useMutation(UPDATE_SUBSCRIPTIONS);
|
|
||||||
const [deleteSubscription] = useMutation(DELETE_SUBSCRIPTIONS);
|
|
||||||
const { showSkeleton } = useDebouncedSkeleton({ loading });
|
|
||||||
|
|
||||||
const subscriptions = data?.subscriptions;
|
|
||||||
|
|
||||||
const handleUpdateRecord = useEvent(
|
|
||||||
(row: Row<SubscriptionDto>) => async (data: SubscriptionsUpdateInput) => {
|
|
||||||
const result = await updateSubscription({
|
|
||||||
variables: {
|
|
||||||
data,
|
|
||||||
filters: {
|
|
||||||
id: {
|
|
||||||
eq: row.original.id,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
});
|
|
||||||
if (result.errors) {
|
|
||||||
toast.error(result.errors[0].message);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
const refetchResult = await refetch();
|
|
||||||
if (refetchResult.errors) {
|
|
||||||
toast.error(refetchResult.errors[0].message);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
toast.success('Subscription updated');
|
|
||||||
}
|
|
||||||
);
|
|
||||||
|
|
||||||
const handleDeleteRecord = useEvent(
|
|
||||||
(row: Row<SubscriptionDto>) => async () => {
|
|
||||||
const result = await deleteSubscription({
|
|
||||||
variables: { filters: { id: { eq: row.original.id } } },
|
|
||||||
});
|
|
||||||
if (result.errors) {
|
|
||||||
toast.error(result.errors[0].message);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
const refetchResult = await refetch();
|
|
||||||
if (refetchResult.errors) {
|
|
||||||
toast.error(refetchResult.errors[0].message);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
toast.success('Subscription deleted');
|
|
||||||
}
|
|
||||||
);
|
|
||||||
|
|
||||||
const columns = useMemo(() => {
|
|
||||||
const cs: ColumnDef<SubscriptionDto>[] = [
|
|
||||||
{
|
|
||||||
header: 'Enabled',
|
|
||||||
accessorKey: 'enabled',
|
|
||||||
cell: ({ row }) => {
|
|
||||||
const enabled = row.original.enabled;
|
|
||||||
return (
|
|
||||||
<div className="px-1">
|
|
||||||
<Switch
|
|
||||||
checked={enabled}
|
|
||||||
onCheckedChange={(enabled) =>
|
|
||||||
handleUpdateRecord(row)({ enabled: enabled })
|
|
||||||
}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
},
|
|
||||||
enableResizing: true,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
header: 'Name',
|
|
||||||
accessorKey: 'displayName',
|
|
||||||
cell: ({ row }) => {
|
|
||||||
const displayName = row.original.displayName;
|
|
||||||
return (
|
|
||||||
<div className="whitespace-normal break-words">{displayName}</div>
|
|
||||||
);
|
|
||||||
},
|
|
||||||
},
|
|
||||||
{
|
|
||||||
header: 'Category',
|
|
||||||
accessorKey: 'category',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
header: 'Source URL',
|
|
||||||
accessorKey: 'sourceUrl',
|
|
||||||
cell: ({ row }) => {
|
|
||||||
const sourceUrl = row.original.sourceUrl;
|
|
||||||
return (
|
|
||||||
<div className="whitespace-normal break-words">{sourceUrl}</div>
|
|
||||||
);
|
|
||||||
},
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: 'actions',
|
|
||||||
cell: ({ row }) => (
|
|
||||||
<DataTableRowActions
|
|
||||||
row={row}
|
|
||||||
getId={(row) => row.original.id}
|
|
||||||
showDetail
|
|
||||||
showEdit
|
|
||||||
showDelete
|
|
||||||
onDetail={() => {
|
|
||||||
navigate({
|
|
||||||
to: '/subscriptions/detail/$subscriptionId',
|
|
||||||
params: { subscriptionId: `${row.original.id}` },
|
|
||||||
});
|
|
||||||
}}
|
|
||||||
onEdit={() => {
|
|
||||||
navigate({
|
|
||||||
to: '/subscriptions/edit/$subscriptionId',
|
|
||||||
params: { subscriptionId: `${row.original.id}` },
|
|
||||||
});
|
|
||||||
}}
|
|
||||||
onDelete={handleDeleteRecord(row)}
|
|
||||||
/>
|
|
||||||
),
|
|
||||||
},
|
|
||||||
];
|
|
||||||
return cs;
|
|
||||||
}, [handleUpdateRecord, handleDeleteRecord, navigate]);
|
|
||||||
|
|
||||||
const table = useReactTable({
|
|
||||||
data: data?.subscriptions?.nodes ?? [],
|
|
||||||
columns,
|
|
||||||
getCoreRowModel: getCoreRowModel(),
|
|
||||||
getPaginationRowModel: getPaginationRowModel(),
|
|
||||||
onPaginationChange: setPagination,
|
|
||||||
onSortingChange: setSorting,
|
|
||||||
onColumnVisibilityChange: setColumnVisibility,
|
|
||||||
pageCount: subscriptions?.paginationInfo?.pages,
|
|
||||||
rowCount: subscriptions?.paginationInfo?.total,
|
|
||||||
state: {
|
|
||||||
pagination,
|
|
||||||
sorting,
|
|
||||||
columnVisibility,
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
if (error) {
|
|
||||||
return <QueryErrorView message={error.message} onRetry={refetch} />;
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div className="container mx-auto space-y-4 rounded-md">
|
|
||||||
<div className="flex items-center py-4">
|
|
||||||
<DataTableViewOptions table={table} />
|
|
||||||
</div>
|
|
||||||
<div className="rounded-md border">
|
|
||||||
<Table>
|
|
||||||
<TableHeader>
|
|
||||||
{table.getHeaderGroups().map((headerGroup) => (
|
|
||||||
<TableRow key={headerGroup.id}>
|
|
||||||
{headerGroup.headers.map((header) => {
|
|
||||||
return (
|
|
||||||
<TableHead key={header.id}>
|
|
||||||
{header.isPlaceholder
|
|
||||||
? null
|
|
||||||
: flexRender(
|
|
||||||
header.column.columnDef.header,
|
|
||||||
header.getContext()
|
|
||||||
)}
|
|
||||||
</TableHead>
|
|
||||||
);
|
|
||||||
})}
|
|
||||||
</TableRow>
|
|
||||||
))}
|
|
||||||
</TableHeader>
|
|
||||||
<TableBody>
|
|
||||||
{showSkeleton &&
|
|
||||||
Array.from(new Array(pagination.pageSize)).map((_, index) => (
|
|
||||||
<TableRow key={index}>
|
|
||||||
{table.getVisibleLeafColumns().map((column) => (
|
|
||||||
<TableCell key={column.id}>
|
|
||||||
<Skeleton className="h-8" />
|
|
||||||
</TableCell>
|
|
||||||
))}
|
|
||||||
</TableRow>
|
|
||||||
))}
|
|
||||||
{!showSkeleton &&
|
|
||||||
(table.getRowModel().rows?.length ? (
|
|
||||||
table.getRowModel().rows.map((row) => (
|
|
||||||
<TableRow
|
|
||||||
key={row.id}
|
|
||||||
data-state={row.getIsSelected() && 'selected'}
|
|
||||||
>
|
|
||||||
{row.getVisibleCells().map((cell) => (
|
|
||||||
<TableCell key={cell.id}>
|
|
||||||
{flexRender(
|
|
||||||
cell.column.columnDef.cell,
|
|
||||||
cell.getContext()
|
|
||||||
)}
|
|
||||||
</TableCell>
|
|
||||||
))}
|
|
||||||
</TableRow>
|
|
||||||
))
|
|
||||||
) : (
|
|
||||||
<TableRow>
|
|
||||||
<TableCell
|
|
||||||
colSpan={columns.length}
|
|
||||||
className="h-24 text-center"
|
|
||||||
>
|
|
||||||
No results.
|
|
||||||
</TableCell>
|
|
||||||
</TableRow>
|
|
||||||
))}
|
|
||||||
</TableBody>
|
|
||||||
</Table>
|
|
||||||
</div>
|
|
||||||
<DataTablePagination table={table} showSelectedRowCount={false} />
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
@ -2,9 +2,9 @@ import {
|
|||||||
SidebarMenu,
|
SidebarMenu,
|
||||||
SidebarMenuButton,
|
SidebarMenuButton,
|
||||||
SidebarMenuItem,
|
SidebarMenuItem,
|
||||||
} from '@/components/ui/sidebar';
|
} from '@/views/components/ui/sidebar';
|
||||||
|
|
||||||
import { Image } from '@/components/ui/image';
|
import { Image } from '@/views/components/ui/image';
|
||||||
|
|
||||||
export function AppIcon() {
|
export function AppIcon() {
|
||||||
return (
|
return (
|
||||||
@ -1,4 +1,6 @@
|
|||||||
import { AppSidebar } from '@/components/layout/app-sidebar';
|
import type { RouteStateDataOption } from '@/infra/routes/traits';
|
||||||
|
import type { RouteBreadcrumbItem } from '@/infra/routes/traits';
|
||||||
|
import { AppSidebar } from '@/views/components/layout/app-sidebar';
|
||||||
import {
|
import {
|
||||||
Breadcrumb,
|
Breadcrumb,
|
||||||
BreadcrumbItem,
|
BreadcrumbItem,
|
||||||
@ -6,16 +8,14 @@ import {
|
|||||||
BreadcrumbList,
|
BreadcrumbList,
|
||||||
BreadcrumbPage,
|
BreadcrumbPage,
|
||||||
BreadcrumbSeparator,
|
BreadcrumbSeparator,
|
||||||
} from '@/components/ui/breadcrumb';
|
} from '@/views/components/ui/breadcrumb';
|
||||||
import { Separator } from '@/components/ui/separator';
|
import { Separator } from '@/views/components/ui/separator';
|
||||||
import {
|
import {
|
||||||
SidebarInset,
|
SidebarInset,
|
||||||
SidebarProvider,
|
SidebarProvider,
|
||||||
SidebarTrigger,
|
SidebarTrigger,
|
||||||
} from '@/components/ui/sidebar';
|
} from '@/views/components/ui/sidebar';
|
||||||
import type { RouteStateDataOption } from '@/infra/routes/traits';
|
import { cn } from '@/views/utils';
|
||||||
import type { RouteBreadcrumbItem } from '@/infra/routes/traits';
|
|
||||||
import { cn } from '@/presentation/utils';
|
|
||||||
import { useMatches } from '@tanstack/react-router';
|
import { useMatches } from '@tanstack/react-router';
|
||||||
import {
|
import {
|
||||||
type DetailedHTMLProps,
|
type DetailedHTMLProps,
|
||||||
@ -1,11 +1,11 @@
|
|||||||
|
import { AppNavMainData } from '@/infra/routes/nav';
|
||||||
import {
|
import {
|
||||||
Sidebar,
|
Sidebar,
|
||||||
SidebarContent,
|
SidebarContent,
|
||||||
SidebarFooter,
|
SidebarFooter,
|
||||||
SidebarHeader,
|
SidebarHeader,
|
||||||
SidebarRail,
|
SidebarRail,
|
||||||
} from '@/components/ui/sidebar';
|
} from '@/views/components/ui/sidebar';
|
||||||
import { AppNavMainData } from '@/infra/routes/nav';
|
|
||||||
import type { ComponentPropsWithoutRef } from 'react';
|
import type { ComponentPropsWithoutRef } from 'react';
|
||||||
import { AppIcon } from './app-icon';
|
import { AppIcon } from './app-icon';
|
||||||
import { NavMain } from './nav-main';
|
import { NavMain } from './nav-main';
|
||||||
@ -6,7 +6,7 @@ import {
|
|||||||
Collapsible,
|
Collapsible,
|
||||||
CollapsibleContent,
|
CollapsibleContent,
|
||||||
CollapsibleTrigger,
|
CollapsibleTrigger,
|
||||||
} from '@/components/ui/collapsible';
|
} from '@/views/components/ui/collapsible';
|
||||||
import {
|
import {
|
||||||
SidebarGroup,
|
SidebarGroup,
|
||||||
SidebarGroupLabel,
|
SidebarGroupLabel,
|
||||||
@ -16,7 +16,7 @@ import {
|
|||||||
SidebarMenuSub,
|
SidebarMenuSub,
|
||||||
SidebarMenuSubButton,
|
SidebarMenuSubButton,
|
||||||
SidebarMenuSubItem,
|
SidebarMenuSubItem,
|
||||||
} from '@/components/ui/sidebar';
|
} from '@/views/components/ui/sidebar';
|
||||||
import { useMatches } from '@tanstack/react-router';
|
import { useMatches } from '@tanstack/react-router';
|
||||||
import { ProLink, type ProLinkProps } from '../ui/pro-link';
|
import { ProLink, type ProLinkProps } from '../ui/pro-link';
|
||||||
|
|
||||||
@ -16,7 +16,7 @@ import {
|
|||||||
DropdownMenuItem,
|
DropdownMenuItem,
|
||||||
DropdownMenuSeparator,
|
DropdownMenuSeparator,
|
||||||
DropdownMenuTrigger,
|
DropdownMenuTrigger,
|
||||||
} from '@/components/ui/dropdown-menu';
|
} from '@/views/components/ui/dropdown-menu';
|
||||||
import {
|
import {
|
||||||
SidebarGroup,
|
SidebarGroup,
|
||||||
SidebarGroupLabel,
|
SidebarGroupLabel,
|
||||||
@ -25,7 +25,7 @@ import {
|
|||||||
SidebarMenuButton,
|
SidebarMenuButton,
|
||||||
SidebarMenuItem,
|
SidebarMenuItem,
|
||||||
useSidebar,
|
useSidebar,
|
||||||
} from '@/components/ui/sidebar';
|
} from '@/views/components/ui/sidebar';
|
||||||
import type { ComponentProps } from 'react';
|
import type { ComponentProps } from 'react';
|
||||||
|
|
||||||
export function NavProjects({
|
export function NavProjects({
|
||||||
@ -9,7 +9,11 @@ import {
|
|||||||
Sparkles,
|
Sparkles,
|
||||||
} from 'lucide-react';
|
} from 'lucide-react';
|
||||||
|
|
||||||
import { Avatar, AvatarFallback, AvatarImage } from '@/components/ui/avatar';
|
import {
|
||||||
|
Avatar,
|
||||||
|
AvatarFallback,
|
||||||
|
AvatarImage,
|
||||||
|
} from '@/views/components/ui/avatar';
|
||||||
import {
|
import {
|
||||||
DropdownMenu,
|
DropdownMenu,
|
||||||
DropdownMenuContent,
|
DropdownMenuContent,
|
||||||
@ -18,13 +22,13 @@ import {
|
|||||||
DropdownMenuLabel,
|
DropdownMenuLabel,
|
||||||
DropdownMenuSeparator,
|
DropdownMenuSeparator,
|
||||||
DropdownMenuTrigger,
|
DropdownMenuTrigger,
|
||||||
} from '@/components/ui/dropdown-menu';
|
} from '@/views/components/ui/dropdown-menu';
|
||||||
import {
|
import {
|
||||||
SidebarMenu,
|
SidebarMenu,
|
||||||
SidebarMenuButton,
|
SidebarMenuButton,
|
||||||
SidebarMenuItem,
|
SidebarMenuItem,
|
||||||
useSidebar,
|
useSidebar,
|
||||||
} from '@/components/ui/sidebar';
|
} from '@/views/components/ui/sidebar';
|
||||||
|
|
||||||
export function NavUser({
|
export function NavUser({
|
||||||
user,
|
user,
|
||||||
@ -2,7 +2,7 @@ import * as AccordionPrimitive from "@radix-ui/react-accordion";
|
|||||||
import { ChevronDownIcon } from "lucide-react";
|
import { ChevronDownIcon } from "lucide-react";
|
||||||
import * as React from "react";
|
import * as React from "react";
|
||||||
|
|
||||||
import { cn } from "@/presentation/utils";
|
import { cn } from "@/views/utils";
|
||||||
|
|
||||||
function Accordion({
|
function Accordion({
|
||||||
...props
|
...props
|
||||||
@ -3,8 +3,8 @@
|
|||||||
import * as AlertDialogPrimitive from "@radix-ui/react-alert-dialog";
|
import * as AlertDialogPrimitive from "@radix-ui/react-alert-dialog";
|
||||||
import * as React from "react";
|
import * as React from "react";
|
||||||
|
|
||||||
import { buttonVariants } from "@/components/ui/button";
|
import { buttonVariants } from "@/views/components/ui/button";
|
||||||
import { cn } from "@/presentation/utils";
|
import { cn } from "@/views/utils";
|
||||||
|
|
||||||
function AlertDialog({
|
function AlertDialog({
|
||||||
...props
|
...props
|
||||||
@ -1,7 +1,7 @@
|
|||||||
import { type VariantProps, cva } from "class-variance-authority";
|
import { type VariantProps, cva } from "class-variance-authority";
|
||||||
import * as React from "react";
|
import * as React from "react";
|
||||||
|
|
||||||
import { cn } from "@/presentation/utils";
|
import { cn } from "@/views/utils";
|
||||||
|
|
||||||
const alertVariants = cva(
|
const alertVariants = cva(
|
||||||
"relative w-full rounded-lg border px-4 py-3 text-sm grid has-[>svg]:grid-cols-[calc(var(--spacing)*4)_1fr] grid-cols-[0_1fr] has-[>svg]:gap-x-3 gap-y-0.5 items-start [&>svg]:size-4 [&>svg]:translate-y-0.5 [&>svg]:text-current",
|
"relative w-full rounded-lg border px-4 py-3 text-sm grid has-[>svg]:grid-cols-[calc(var(--spacing)*4)_1fr] grid-cols-[0_1fr] has-[>svg]:gap-x-3 gap-y-0.5 items-start [&>svg]:size-4 [&>svg]:translate-y-0.5 [&>svg]:text-current",
|
||||||
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue
Block a user