mirror of
https://github.com/amtoaer/bili-sync.git
synced 2026-05-12 02:21:17 +08:00
Compare commits
9 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
e51fed984b | ||
|
|
716c78b1e3 | ||
|
|
22bc6bb3e8 | ||
|
|
fedbd4cdb1 | ||
|
|
c1d9dc8b87 | ||
|
|
7f09a98d6c | ||
|
|
269647ac22 | ||
|
|
e0189c5b36 | ||
|
|
4c1abcf48c |
15
.github/workflows/build-binary.yaml
vendored
15
.github/workflows/build-binary.yaml
vendored
@@ -75,17 +75,18 @@ jobs:
|
|||||||
with:
|
with:
|
||||||
name: web-build
|
name: web-build
|
||||||
path: web/build
|
path: web/build
|
||||||
- name: Cache dependencies
|
- name: Read Toolchain Version
|
||||||
uses: Swatinem/rust-cache@v2
|
uses: SebRollen/toml-action@v1.2.0
|
||||||
- name: Install musl-tools
|
id: read_rust_toolchain
|
||||||
run: sudo apt-get update --yes && sudo apt-get install --yes musl-tools
|
with:
|
||||||
if: contains(matrix.platform.target, 'musl')
|
file: rust-toolchain.toml
|
||||||
|
field: toolchain.channel
|
||||||
- name: Build binary
|
- name: Build binary
|
||||||
uses: houseabsolute/actions-rust-cross@v0
|
uses: houseabsolute/actions-rust-cross@v1
|
||||||
with:
|
with:
|
||||||
command: build
|
command: build
|
||||||
target: ${{ matrix.platform.target }}
|
target: ${{ matrix.platform.target }}
|
||||||
toolchain: stable
|
toolchain: ${{ steps.read_rust_toolchain.outputs.value }}
|
||||||
args: "--locked --release"
|
args: "--locked --release"
|
||||||
strip: true
|
strip: true
|
||||||
- name: Package as archive
|
- name: Package as archive
|
||||||
|
|||||||
2
.github/workflows/pr-check.yaml
vendored
2
.github/workflows/pr-check.yaml
vendored
@@ -26,7 +26,7 @@ jobs:
|
|||||||
- name: Checkout repo
|
- name: Checkout repo
|
||||||
uses: actions/checkout@v4
|
uses: actions/checkout@v4
|
||||||
|
|
||||||
- run: rustup default stable && rustup component add clippy && rustup component add rustfmt --toolchain nightly
|
- run: rustup install && rustup component add rustfmt --toolchain nightly
|
||||||
|
|
||||||
- name: Cache dependencies
|
- name: Cache dependencies
|
||||||
uses: swatinem/rust-cache@v2
|
uses: swatinem/rust-cache@v2
|
||||||
|
|||||||
160
Cargo.lock
generated
160
Cargo.lock
generated
@@ -231,28 +231,6 @@ version = "1.5.0"
|
|||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "c08606f8c3cbf4ce6ec8e28fb0014a2c086708fe954eaa885384a6165172e7e8"
|
checksum = "c08606f8c3cbf4ce6ec8e28fb0014a2c086708fe954eaa885384a6165172e7e8"
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "aws-lc-rs"
|
|
||||||
version = "1.15.2"
|
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "6a88aab2464f1f25453baa7a07c84c5b7684e274054ba06817f382357f77a288"
|
|
||||||
dependencies = [
|
|
||||||
"aws-lc-sys",
|
|
||||||
"zeroize",
|
|
||||||
]
|
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "aws-lc-sys"
|
|
||||||
version = "0.35.0"
|
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "b45afffdee1e7c9126814751f88dddc747f41d91da16c9551a0f1e8a11e788a1"
|
|
||||||
dependencies = [
|
|
||||||
"cc",
|
|
||||||
"cmake",
|
|
||||||
"dunce",
|
|
||||||
"fs_extra",
|
|
||||||
]
|
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "axum"
|
name = "axum"
|
||||||
version = "0.8.8"
|
version = "0.8.8"
|
||||||
@@ -375,7 +353,7 @@ dependencies = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "bili_sync"
|
name = "bili_sync"
|
||||||
version = "2.10.1"
|
version = "2.10.3"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"anyhow",
|
"anyhow",
|
||||||
"arc-swap",
|
"arc-swap",
|
||||||
@@ -411,6 +389,7 @@ dependencies = [
|
|||||||
"reqwest",
|
"reqwest",
|
||||||
"rsa 0.10.0-rc.9",
|
"rsa 0.10.0-rc.9",
|
||||||
"rust-embed-for-web",
|
"rust-embed-for-web",
|
||||||
|
"rustls",
|
||||||
"sea-orm",
|
"sea-orm",
|
||||||
"serde",
|
"serde",
|
||||||
"serde_json",
|
"serde_json",
|
||||||
@@ -433,7 +412,7 @@ dependencies = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "bili_sync_entity"
|
name = "bili_sync_entity"
|
||||||
version = "2.10.1"
|
version = "2.10.3"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"derivative",
|
"derivative",
|
||||||
"regex",
|
"regex",
|
||||||
@@ -444,7 +423,7 @@ dependencies = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "bili_sync_migration"
|
name = "bili_sync_migration"
|
||||||
version = "2.10.1"
|
version = "2.10.3"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"sea-orm-migration",
|
"sea-orm-migration",
|
||||||
]
|
]
|
||||||
@@ -686,15 +665,6 @@ version = "0.7.6"
|
|||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "a1d728cc89cf3aee9ff92b05e62b19ee65a02b5702cff7d5a377e32c6ae29d8d"
|
checksum = "a1d728cc89cf3aee9ff92b05e62b19ee65a02b5702cff7d5a377e32c6ae29d8d"
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "cmake"
|
|
||||||
version = "0.1.57"
|
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "75443c44cd6b379beb8c5b45d85d0773baf31cce901fe7bb252f4eff3008ef7d"
|
|
||||||
dependencies = [
|
|
||||||
"cc",
|
|
||||||
]
|
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "colorchoice"
|
name = "colorchoice"
|
||||||
version = "1.0.4"
|
version = "1.0.4"
|
||||||
@@ -1117,12 +1087,6 @@ version = "0.15.7"
|
|||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "1aaf95b3e5c8f23aa320147307562d361db0ae0d51242340f558153b4eb2439b"
|
checksum = "1aaf95b3e5c8f23aa320147307562d361db0ae0d51242340f558153b4eb2439b"
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "dunce"
|
|
||||||
version = "1.0.5"
|
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "92773504d58c093f6de2459af4af33faa518c13451eb8f2b5698ed3d36e7c813"
|
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "either"
|
name = "either"
|
||||||
version = "1.15.0"
|
version = "1.15.0"
|
||||||
@@ -1251,12 +1215,6 @@ dependencies = [
|
|||||||
"percent-encoding",
|
"percent-encoding",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "fs_extra"
|
|
||||||
version = "1.3.0"
|
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "42703706b716c37f96a77aea830392ad231f44c9e9a67872fa5548707e11b11c"
|
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "funty"
|
name = "funty"
|
||||||
version = "2.0.0"
|
version = "2.0.0"
|
||||||
@@ -1380,10 +1338,8 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
|
|||||||
checksum = "335ff9f135e4384c8150d6f27c6daed433577f86b4750418338c01a1a2528592"
|
checksum = "335ff9f135e4384c8150d6f27c6daed433577f86b4750418338c01a1a2528592"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"cfg-if",
|
"cfg-if",
|
||||||
"js-sys",
|
|
||||||
"libc",
|
"libc",
|
||||||
"wasi",
|
"wasi",
|
||||||
"wasm-bindgen",
|
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
@@ -1393,11 +1349,9 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
|
|||||||
checksum = "899def5c37c4fd7b2664648c28120ecec138e4d395b459e5ca34f9cce2dd77fd"
|
checksum = "899def5c37c4fd7b2664648c28120ecec138e4d395b459e5ca34f9cce2dd77fd"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"cfg-if",
|
"cfg-if",
|
||||||
"js-sys",
|
|
||||||
"libc",
|
"libc",
|
||||||
"r-efi",
|
"r-efi",
|
||||||
"wasip2",
|
"wasip2",
|
||||||
"wasm-bindgen",
|
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
@@ -2013,12 +1967,6 @@ version = "0.4.29"
|
|||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "5e5032e24019045c762d3c0f28f5b6b8bbf38563a65908389bf7978758920897"
|
checksum = "5e5032e24019045c762d3c0f28f5b6b8bbf38563a65908389bf7978758920897"
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "lru-slab"
|
|
||||||
version = "0.1.2"
|
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "112b39cec0b298b6c1999fee3e31427f74f676e4cb9879ed1a121b43661a4154"
|
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "matchers"
|
name = "matchers"
|
||||||
version = "0.2.0"
|
version = "0.2.0"
|
||||||
@@ -2621,62 +2569,6 @@ dependencies = [
|
|||||||
"tokio",
|
"tokio",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "quinn"
|
|
||||||
version = "0.11.9"
|
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "b9e20a958963c291dc322d98411f541009df2ced7b5a4f2bd52337638cfccf20"
|
|
||||||
dependencies = [
|
|
||||||
"bytes",
|
|
||||||
"cfg_aliases",
|
|
||||||
"pin-project-lite",
|
|
||||||
"quinn-proto",
|
|
||||||
"quinn-udp",
|
|
||||||
"rustc-hash",
|
|
||||||
"rustls",
|
|
||||||
"socket2",
|
|
||||||
"thiserror 2.0.17",
|
|
||||||
"tokio",
|
|
||||||
"tracing",
|
|
||||||
"web-time",
|
|
||||||
]
|
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "quinn-proto"
|
|
||||||
version = "0.11.13"
|
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "f1906b49b0c3bc04b5fe5d86a77925ae6524a19b816ae38ce1e426255f1d8a31"
|
|
||||||
dependencies = [
|
|
||||||
"aws-lc-rs",
|
|
||||||
"bytes",
|
|
||||||
"getrandom 0.3.4",
|
|
||||||
"lru-slab",
|
|
||||||
"rand 0.9.2",
|
|
||||||
"ring",
|
|
||||||
"rustc-hash",
|
|
||||||
"rustls",
|
|
||||||
"rustls-pki-types",
|
|
||||||
"slab",
|
|
||||||
"thiserror 2.0.17",
|
|
||||||
"tinyvec",
|
|
||||||
"tracing",
|
|
||||||
"web-time",
|
|
||||||
]
|
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "quinn-udp"
|
|
||||||
version = "0.5.14"
|
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "addec6a0dcad8a8d96a771f815f0eaf55f9d1805756410b39f5fa81332574cbd"
|
|
||||||
dependencies = [
|
|
||||||
"cfg_aliases",
|
|
||||||
"libc",
|
|
||||||
"once_cell",
|
|
||||||
"socket2",
|
|
||||||
"tracing",
|
|
||||||
"windows-sys 0.60.2",
|
|
||||||
]
|
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "quote"
|
name = "quote"
|
||||||
version = "1.0.42"
|
version = "1.0.42"
|
||||||
@@ -2840,7 +2732,6 @@ dependencies = [
|
|||||||
"mime",
|
"mime",
|
||||||
"percent-encoding",
|
"percent-encoding",
|
||||||
"pin-project-lite",
|
"pin-project-lite",
|
||||||
"quinn",
|
|
||||||
"rustls",
|
"rustls",
|
||||||
"rustls-pki-types",
|
"rustls-pki-types",
|
||||||
"rustls-platform-verifier",
|
"rustls-platform-verifier",
|
||||||
@@ -3006,12 +2897,6 @@ version = "0.1.23"
|
|||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "d626bb9dae77e28219937af045c257c28bfd3f69333c512553507f5f9798cb76"
|
checksum = "d626bb9dae77e28219937af045c257c28bfd3f69333c512553507f5f9798cb76"
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "rustc-hash"
|
|
||||||
version = "2.1.1"
|
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "357703d41365b4b27c590e3ed91eabb1b663f07c4c084095e60cbed4362dff0d"
|
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "rustc_version"
|
name = "rustc_version"
|
||||||
version = "0.4.1"
|
version = "0.4.1"
|
||||||
@@ -3023,11 +2908,10 @@ dependencies = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "rustls"
|
name = "rustls"
|
||||||
version = "0.23.35"
|
version = "0.23.36"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "533f54bc6a7d4f647e46ad909549eda97bf5afc1585190ef692b4286b198bd8f"
|
checksum = "c665f33d38cea657d9614f766881e4d510e0eda4239891eea56b4cadcf01801b"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"aws-lc-rs",
|
|
||||||
"once_cell",
|
"once_cell",
|
||||||
"ring",
|
"ring",
|
||||||
"rustls-pki-types",
|
"rustls-pki-types",
|
||||||
@@ -3054,7 +2938,6 @@ version = "1.13.2"
|
|||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "21e6f2ab2928ca4291b86736a8bd920a277a399bba1589409d72154ff87c1282"
|
checksum = "21e6f2ab2928ca4291b86736a8bd920a277a399bba1589409d72154ff87c1282"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"web-time",
|
|
||||||
"zeroize",
|
"zeroize",
|
||||||
]
|
]
|
||||||
|
|
||||||
@@ -3091,7 +2974,6 @@ version = "0.103.8"
|
|||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "2ffdfa2f5286e2247234e03f680868ac2815974dc39e00ea15adc445d0aafe52"
|
checksum = "2ffdfa2f5286e2247234e03f680868ac2815974dc39e00ea15adc445d0aafe52"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"aws-lc-rs",
|
|
||||||
"ring",
|
"ring",
|
||||||
"rustls-pki-types",
|
"rustls-pki-types",
|
||||||
"untrusted",
|
"untrusted",
|
||||||
@@ -3610,7 +3492,6 @@ dependencies = [
|
|||||||
"once_cell",
|
"once_cell",
|
||||||
"percent-encoding",
|
"percent-encoding",
|
||||||
"rust_decimal",
|
"rust_decimal",
|
||||||
"rustls",
|
|
||||||
"serde",
|
"serde",
|
||||||
"serde_json",
|
"serde_json",
|
||||||
"sha2 0.10.9",
|
"sha2 0.10.9",
|
||||||
@@ -3622,7 +3503,6 @@ dependencies = [
|
|||||||
"tracing",
|
"tracing",
|
||||||
"url",
|
"url",
|
||||||
"uuid",
|
"uuid",
|
||||||
"webpki-roots 0.26.11",
|
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
@@ -4592,16 +4472,6 @@ dependencies = [
|
|||||||
"wasm-bindgen",
|
"wasm-bindgen",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "web-time"
|
|
||||||
version = "1.1.0"
|
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "5a6580f308b1fad9207618087a65c04e7a10bc77e02c8e84e9b00dd4b12fa0bb"
|
|
||||||
dependencies = [
|
|
||||||
"js-sys",
|
|
||||||
"wasm-bindgen",
|
|
||||||
]
|
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "webpki-root-certs"
|
name = "webpki-root-certs"
|
||||||
version = "1.0.5"
|
version = "1.0.5"
|
||||||
@@ -4611,24 +4481,6 @@ dependencies = [
|
|||||||
"rustls-pki-types",
|
"rustls-pki-types",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "webpki-roots"
|
|
||||||
version = "0.26.11"
|
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "521bc38abb08001b01866da9f51eb7c5d647a19260e00054a8c7fd5f9e57f7a9"
|
|
||||||
dependencies = [
|
|
||||||
"webpki-roots 1.0.5",
|
|
||||||
]
|
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "webpki-roots"
|
|
||||||
version = "1.0.5"
|
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "12bed680863276c63889429bfd6cab3b99943659923822de1c8a39c49e4d722c"
|
|
||||||
dependencies = [
|
|
||||||
"rustls-pki-types",
|
|
||||||
]
|
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "whoami"
|
name = "whoami"
|
||||||
version = "1.6.1"
|
version = "1.6.1"
|
||||||
|
|||||||
@@ -4,7 +4,7 @@ default-members = ["crates/bili_sync"]
|
|||||||
resolver = "2"
|
resolver = "2"
|
||||||
|
|
||||||
[workspace.package]
|
[workspace.package]
|
||||||
version = "2.10.1"
|
version = "2.10.3"
|
||||||
authors = ["amtoaer <amtoaer@gmail.com>"]
|
authors = ["amtoaer <amtoaer@gmail.com>"]
|
||||||
license = "MIT"
|
license = "MIT"
|
||||||
description = "由 Rust & Tokio 驱动的哔哩哔哩同步工具"
|
description = "由 Rust & Tokio 驱动的哔哩哔哩同步工具"
|
||||||
@@ -54,14 +54,15 @@ reqwest = { version = "0.13.1", features = [
|
|||||||
"gzip",
|
"gzip",
|
||||||
"http2",
|
"http2",
|
||||||
"json",
|
"json",
|
||||||
"rustls",
|
"rustls-no-provider",
|
||||||
"stream",
|
"stream",
|
||||||
], default-features = false }
|
], default-features = false }
|
||||||
rsa = { version = "0.10.0-rc.9", features = ["sha2"] }
|
rsa = { version = "0.10.0-rc.9", features = ["sha2"] }
|
||||||
rust-embed-for-web = { git = "https://github.com/amtoaer/rust-embed-for-web", tag = "v1.0.0" }
|
rust-embed-for-web = { git = "https://github.com/amtoaer/rust-embed-for-web", tag = "v1.0.0" }
|
||||||
|
rustls = { version = "0.23.36", default-features = false, features = ["ring"] }
|
||||||
sea-orm = { version = "1.1.19", features = [
|
sea-orm = { version = "1.1.19", features = [
|
||||||
"macros",
|
"macros",
|
||||||
"runtime-tokio-rustls",
|
"runtime-tokio",
|
||||||
"sqlx-sqlite",
|
"sqlx-sqlite",
|
||||||
"sqlite-use-returning-for-3_35",
|
"sqlite-use-returning-for-3_35",
|
||||||
] }
|
] }
|
||||||
|
|||||||
@@ -42,6 +42,7 @@ regex = { workspace = true }
|
|||||||
reqwest = { workspace = true }
|
reqwest = { workspace = true }
|
||||||
rsa = { workspace = true }
|
rsa = { workspace = true }
|
||||||
rust-embed-for-web = { workspace = true }
|
rust-embed-for-web = { workspace = true }
|
||||||
|
rustls = { workspace = true }
|
||||||
sea-orm = { workspace = true }
|
sea-orm = { workspace = true }
|
||||||
serde = { workspace = true }
|
serde = { workspace = true }
|
||||||
serde_json = { workspace = true }
|
serde_json = { workspace = true }
|
||||||
|
|||||||
@@ -1,9 +1,22 @@
|
|||||||
use std::borrow::Borrow;
|
use std::borrow::Borrow;
|
||||||
|
|
||||||
use itertools::Itertools;
|
use itertools::Itertools;
|
||||||
use sea_orm::{ConnectionTrait, DatabaseTransaction};
|
use sea_orm::{Condition, ConnectionTrait, DatabaseTransaction};
|
||||||
|
|
||||||
|
use crate::api::request::StatusFilter;
|
||||||
use crate::api::response::{PageInfo, SimplePageInfo, SimpleVideoInfo, VideoInfo};
|
use crate::api::response::{PageInfo, SimplePageInfo, SimpleVideoInfo, VideoInfo};
|
||||||
|
use crate::utils::status::VideoStatus;
|
||||||
|
|
||||||
|
impl StatusFilter {
|
||||||
|
pub fn to_video_query(&self) -> Condition {
|
||||||
|
let query_builder = VideoStatus::query_builder();
|
||||||
|
match self {
|
||||||
|
Self::Failed => query_builder.failed(),
|
||||||
|
Self::Succeeded => query_builder.succeeded(),
|
||||||
|
Self::Waiting => query_builder.waiting(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
pub trait VideoRecord {
|
pub trait VideoRecord {
|
||||||
fn as_id_status_tuple(&self) -> (i32, u32);
|
fn as_id_status_tuple(&self) -> (i32, u32);
|
||||||
|
|||||||
@@ -4,6 +4,14 @@ use validator::Validate;
|
|||||||
|
|
||||||
use crate::bilibili::CollectionType;
|
use crate::bilibili::CollectionType;
|
||||||
|
|
||||||
|
#[derive(Deserialize)]
|
||||||
|
#[serde(rename_all = "lowercase")]
|
||||||
|
pub enum StatusFilter {
|
||||||
|
Failed,
|
||||||
|
Succeeded,
|
||||||
|
Waiting,
|
||||||
|
}
|
||||||
|
|
||||||
#[derive(Deserialize)]
|
#[derive(Deserialize)]
|
||||||
pub struct VideosRequest {
|
pub struct VideosRequest {
|
||||||
pub collection: Option<i32>,
|
pub collection: Option<i32>,
|
||||||
@@ -11,6 +19,7 @@ pub struct VideosRequest {
|
|||||||
pub submission: Option<i32>,
|
pub submission: Option<i32>,
|
||||||
pub watch_later: Option<i32>,
|
pub watch_later: Option<i32>,
|
||||||
pub query: Option<String>,
|
pub query: Option<String>,
|
||||||
|
pub status_filter: Option<StatusFilter>,
|
||||||
pub page: Option<u64>,
|
pub page: Option<u64>,
|
||||||
pub page_size: Option<u64>,
|
pub page_size: Option<u64>,
|
||||||
}
|
}
|
||||||
@@ -28,6 +37,7 @@ pub struct ResetFilteredVideoStatusRequest {
|
|||||||
pub submission: Option<i32>,
|
pub submission: Option<i32>,
|
||||||
pub watch_later: Option<i32>,
|
pub watch_later: Option<i32>,
|
||||||
pub query: Option<String>,
|
pub query: Option<String>,
|
||||||
|
pub status_filter: Option<StatusFilter>,
|
||||||
#[serde(default)]
|
#[serde(default)]
|
||||||
pub force: bool,
|
pub force: bool,
|
||||||
}
|
}
|
||||||
@@ -64,6 +74,7 @@ pub struct UpdateFilteredVideoStatusRequest {
|
|||||||
pub submission: Option<i32>,
|
pub submission: Option<i32>,
|
||||||
pub watch_later: Option<i32>,
|
pub watch_later: Option<i32>,
|
||||||
pub query: Option<String>,
|
pub query: Option<String>,
|
||||||
|
pub status_filter: Option<StatusFilter>,
|
||||||
#[serde(default)]
|
#[serde(default)]
|
||||||
#[validate(nested)]
|
#[validate(nested)]
|
||||||
pub video_updates: Vec<StatusUpdate>,
|
pub video_updates: Vec<StatusUpdate>,
|
||||||
|
|||||||
@@ -62,6 +62,9 @@ pub async fn get_videos(
|
|||||||
.or(video::Column::Bvid.contains(query_word)),
|
.or(video::Column::Bvid.contains(query_word)),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
if let Some(status_filter) = params.status_filter {
|
||||||
|
query = query.filter(status_filter.to_video_query());
|
||||||
|
}
|
||||||
let total_count = query.clone().count(&db).await?;
|
let total_count = query.clone().count(&db).await?;
|
||||||
let (page, page_size) = if let (Some(page), Some(page_size)) = (params.page, params.page_size) {
|
let (page, page_size) = if let (Some(page), Some(page_size)) = (params.page, params.page_size) {
|
||||||
(page, page_size)
|
(page, page_size)
|
||||||
@@ -218,6 +221,9 @@ pub async fn reset_filtered_video_status(
|
|||||||
.or(video::Column::Bvid.contains(query_word)),
|
.or(video::Column::Bvid.contains(query_word)),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
if let Some(status_filter) = request.status_filter {
|
||||||
|
query = query.filter(status_filter.to_video_query());
|
||||||
|
}
|
||||||
let all_videos = query.into_partial_model::<SimpleVideoInfo>().all(&db).await?;
|
let all_videos = query.into_partial_model::<SimpleVideoInfo>().all(&db).await?;
|
||||||
let all_pages = page::Entity::find()
|
let all_pages = page::Entity::find()
|
||||||
.filter(page::Column::VideoId.is_in(all_videos.iter().map(|v| v.id)))
|
.filter(page::Column::VideoId.is_in(all_videos.iter().map(|v| v.id)))
|
||||||
@@ -351,6 +357,9 @@ pub async fn update_filtered_video_status(
|
|||||||
.or(video::Column::Bvid.contains(query_word)),
|
.or(video::Column::Bvid.contains(query_word)),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
if let Some(status_filter) = request.status_filter {
|
||||||
|
query = query.filter(status_filter.to_video_query());
|
||||||
|
}
|
||||||
let mut all_videos = query.into_partial_model::<SimpleVideoInfo>().all(&db).await?;
|
let mut all_videos = query.into_partial_model::<SimpleVideoInfo>().all(&db).await?;
|
||||||
let mut all_pages = page::Entity::find()
|
let mut all_pages = page::Entity::find()
|
||||||
.filter(page::Column::VideoId.is_in(all_videos.iter().map(|v| v.id)))
|
.filter(page::Column::VideoId.is_in(all_videos.iter().map(|v| v.id)))
|
||||||
|
|||||||
@@ -100,7 +100,7 @@ impl Default for FilterOption {
|
|||||||
video_min_quality: VideoQuality::Quality360p,
|
video_min_quality: VideoQuality::Quality360p,
|
||||||
audio_max_quality: AudioQuality::QualityHiRES,
|
audio_max_quality: AudioQuality::QualityHiRES,
|
||||||
audio_min_quality: AudioQuality::Quality64k,
|
audio_min_quality: AudioQuality::Quality64k,
|
||||||
codecs: vec![VideoCodecs::AV1, VideoCodecs::HEV, VideoCodecs::AVC],
|
codecs: vec![VideoCodecs::AVC, VideoCodecs::HEV, VideoCodecs::AV1],
|
||||||
no_dolby_video: false,
|
no_dolby_video: false,
|
||||||
no_dolby_audio: false,
|
no_dolby_audio: false,
|
||||||
no_hdr: false,
|
no_hdr: false,
|
||||||
|
|||||||
@@ -3,6 +3,7 @@ use std::time::Duration;
|
|||||||
|
|
||||||
use anyhow::{Result, bail};
|
use anyhow::{Result, bail};
|
||||||
use leaky_bucket::RateLimiter;
|
use leaky_bucket::RateLimiter;
|
||||||
|
use parking_lot::Once;
|
||||||
use reqwest::{Method, header};
|
use reqwest::{Method, header};
|
||||||
use ua_generator::ua;
|
use ua_generator::ua;
|
||||||
|
|
||||||
@@ -16,6 +17,12 @@ pub struct Client(reqwest::Client);
|
|||||||
|
|
||||||
impl Client {
|
impl Client {
|
||||||
pub fn new() -> Self {
|
pub fn new() -> Self {
|
||||||
|
static INIT: Once = Once::new();
|
||||||
|
INIT.call_once(|| {
|
||||||
|
rustls::crypto::ring::default_provider()
|
||||||
|
.install_default()
|
||||||
|
.expect("Failed to install rustls crypto provider");
|
||||||
|
});
|
||||||
// 正常访问 api 所必须的 header,作为默认 header 添加到每个请求中
|
// 正常访问 api 所必须的 header,作为默认 header 添加到每个请求中
|
||||||
let mut headers = header::HeaderMap::new();
|
let mut headers = header::HeaderMap::new();
|
||||||
headers.insert(
|
headers.insert(
|
||||||
|
|||||||
@@ -1,3 +1,10 @@
|
|||||||
|
use std::marker::PhantomData;
|
||||||
|
|
||||||
|
use bili_sync_entity::{page, video};
|
||||||
|
use bili_sync_migration::{ExprTrait, IntoCondition};
|
||||||
|
use sea_orm::sea_query::Expr;
|
||||||
|
use sea_orm::{ColumnTrait, Condition};
|
||||||
|
|
||||||
use crate::error::ExecutionStatus;
|
use crate::error::ExecutionStatus;
|
||||||
|
|
||||||
pub static STATUS_NOT_STARTED: u32 = 0b000;
|
pub static STATUS_NOT_STARTED: u32 = 0b000;
|
||||||
@@ -11,10 +18,17 @@ pub static STATUS_COMPLETED: u32 = 1 << 31;
|
|||||||
/// 如果子任务执行成功,将状态设置为 0b111,该值定义为 STATUS_OK。
|
/// 如果子任务执行成功,将状态设置为 0b111,该值定义为 STATUS_OK。
|
||||||
/// 子任务达到最大失败次数或者执行成功时,认为该子任务已经完成。
|
/// 子任务达到最大失败次数或者执行成功时,认为该子任务已经完成。
|
||||||
/// 当所有子任务都已经完成时,为最高位打上标记 1,表示整个下载任务已经完成。
|
/// 当所有子任务都已经完成时,为最高位打上标记 1,表示整个下载任务已经完成。
|
||||||
#[derive(Clone, Copy, Default)]
|
#[derive(Clone, Copy)]
|
||||||
pub struct Status<const N: usize>(u32);
|
pub struct Status<const N: usize, C>(u32, PhantomData<C>);
|
||||||
|
|
||||||
impl<const N: usize> Status<N> {
|
impl<const N: usize, C> Default for Status<N, C> {
|
||||||
|
fn default() -> Self {
|
||||||
|
Self(0, PhantomData)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<const N: usize, C> Status<N, C> {
|
||||||
|
pub(crate) const LEN: usize = N;
|
||||||
// 获取最高位的完成标记
|
// 获取最高位的完成标记
|
||||||
pub fn get_completed(&self) -> bool {
|
pub fn get_completed(&self) -> bool {
|
||||||
self.0 >> 31 == 1
|
self.0 >> 31 == 1
|
||||||
@@ -136,20 +150,20 @@ impl<const N: usize> Status<N> {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl<const N: usize> From<u32> for Status<N> {
|
impl<const N: usize, C> From<u32> for Status<N, C> {
|
||||||
fn from(status: u32) -> Self {
|
fn from(status: u32) -> Self {
|
||||||
Status(status)
|
Status(status, PhantomData)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl<const N: usize> From<Status<N>> for u32 {
|
impl<const N: usize, C> From<Status<N, C>> for u32 {
|
||||||
fn from(status: Status<N>) -> Self {
|
fn from(status: Status<N, C>) -> Self {
|
||||||
status.0
|
status.0
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl<const N: usize> From<Status<N>> for [u32; N] {
|
impl<const N: usize, C> From<Status<N, C>> for [u32; N] {
|
||||||
fn from(status: Status<N>) -> Self {
|
fn from(status: Status<N, C>) -> Self {
|
||||||
let mut result = [0; N];
|
let mut result = [0; N];
|
||||||
for (i, item) in result.iter_mut().enumerate() {
|
for (i, item) in result.iter_mut().enumerate() {
|
||||||
*item = status.get_status(i);
|
*item = status.get_status(i);
|
||||||
@@ -158,9 +172,9 @@ impl<const N: usize> From<Status<N>> for [u32; N] {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl<const N: usize> From<[u32; N]> for Status<N> {
|
impl<const N: usize, C> From<[u32; N]> for Status<N, C> {
|
||||||
fn from(status: [u32; N]) -> Self {
|
fn from(status: [u32; N]) -> Self {
|
||||||
let mut result = Status::<N>::default();
|
let mut result = Self::default();
|
||||||
for (i, item) in status.iter().enumerate() {
|
for (i, item) in status.iter().enumerate() {
|
||||||
assert!(*item < 0b1000, "status should be less than 0b1000");
|
assert!(*item < 0b1000, "status should be less than 0b1000");
|
||||||
result.set_status(i, *item);
|
result.set_status(i, *item);
|
||||||
@@ -173,10 +187,64 @@ impl<const N: usize> From<[u32; N]> for Status<N> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// 包含五个子任务,从前到后依次是:视频封面、视频信息、Up 主头像、Up 主信息、分页下载
|
/// 包含五个子任务,从前到后依次是:视频封面、视频信息、Up 主头像、Up 主信息、分页下载
|
||||||
pub type VideoStatus = Status<5>;
|
pub type VideoStatus = Status<5, video::Column>;
|
||||||
|
|
||||||
|
impl VideoStatus {
|
||||||
|
pub fn query_builder() -> StatusQueryBuilder<{ Self::LEN }, video::Column> {
|
||||||
|
StatusQueryBuilder::new(video::Column::DownloadStatus)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/// 包含五个子任务,从前到后分别是:视频封面、视频内容、视频信息、视频弹幕、视频字幕
|
/// 包含五个子任务,从前到后分别是:视频封面、视频内容、视频信息、视频弹幕、视频字幕
|
||||||
pub type PageStatus = Status<5>;
|
pub type PageStatus = Status<5, page::Column>;
|
||||||
|
|
||||||
|
impl PageStatus {
|
||||||
|
pub fn query_builder() -> StatusQueryBuilder<{ Self::LEN }, page::Column> {
|
||||||
|
StatusQueryBuilder::new(page::Column::DownloadStatus)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub struct StatusQueryBuilder<const N: usize, C: ColumnTrait> {
|
||||||
|
column: C,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<const N: usize, C: ColumnTrait> StatusQueryBuilder<N, C> {
|
||||||
|
fn new(column: C) -> Self {
|
||||||
|
Self { column }
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 完成状态:所有子任务的状态都是成功
|
||||||
|
pub fn succeeded(&self) -> Condition {
|
||||||
|
let mut condition = Condition::all();
|
||||||
|
for offset in 0..N as i32 {
|
||||||
|
condition = condition.add(Expr::col(self.column).right_shift(offset * 3).bit_and(7).eq(7))
|
||||||
|
}
|
||||||
|
condition
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 失败状态:存在任何失败的子任务
|
||||||
|
pub fn failed(&self) -> Condition {
|
||||||
|
let mut condition = Condition::any();
|
||||||
|
for offset in 0..N as i32 {
|
||||||
|
condition = condition.add(
|
||||||
|
Expr::col(self.column)
|
||||||
|
.right_shift(offset * 3)
|
||||||
|
.bit_and(7)
|
||||||
|
.is_not_in([0, 7]),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
condition
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 等待状态:所有子任务的状态都不是失败,且其中存在未开始
|
||||||
|
pub fn waiting(&self) -> Condition {
|
||||||
|
let mut condition = Condition::any();
|
||||||
|
for offset in 0..N as i32 {
|
||||||
|
condition = condition.add(Expr::col(self.column).right_shift(offset * 3).bit_and(7).eq(0))
|
||||||
|
}
|
||||||
|
condition.and(self.failed().not()).into_condition()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
mod tests {
|
mod tests {
|
||||||
@@ -186,7 +254,7 @@ mod tests {
|
|||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn test_status_update() {
|
fn test_status_update() {
|
||||||
let mut status = Status::<3>::default();
|
let mut status = Status::<3, video::Column>::default();
|
||||||
assert_eq!(status.should_run(), [true, true, true]);
|
assert_eq!(status.should_run(), [true, true, true]);
|
||||||
for _ in 0..3 {
|
for _ in 0..3 {
|
||||||
status.update_status(&[
|
status.update_status(&[
|
||||||
@@ -217,7 +285,7 @@ mod tests {
|
|||||||
fn test_status_convert() {
|
fn test_status_convert() {
|
||||||
let testcases = [[0, 0, 1], [1, 2, 3], [3, 1, 2], [3, 0, 7]];
|
let testcases = [[0, 0, 1], [1, 2, 3], [3, 1, 2], [3, 0, 7]];
|
||||||
for testcase in testcases.iter() {
|
for testcase in testcases.iter() {
|
||||||
let status = Status::<3>::from(testcase.clone());
|
let status = Status::<3, video::Column>::from(testcase.clone());
|
||||||
assert_eq!(<[u32; 3]>::from(status), *testcase);
|
assert_eq!(<[u32; 3]>::from(status), *testcase);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -226,7 +294,7 @@ mod tests {
|
|||||||
fn test_status_convert_and_update() {
|
fn test_status_convert_and_update() {
|
||||||
let testcases = [([0, 0, 1], [1, 7, 7]), ([3, 4, 3], [4, 4, 7]), ([3, 1, 7], [4, 7, 7])];
|
let testcases = [([0, 0, 1], [1, 7, 7]), ([3, 4, 3], [4, 4, 7]), ([3, 1, 7], [4, 7, 7])];
|
||||||
for (before, after) in testcases.iter() {
|
for (before, after) in testcases.iter() {
|
||||||
let mut status = Status::<3>::from(before.clone());
|
let mut status = Status::<3, video::Column>::from(before.clone());
|
||||||
status.update_status(&[
|
status.update_status(&[
|
||||||
ExecutionStatus::Failed(anyhow!("")),
|
ExecutionStatus::Failed(anyhow!("")),
|
||||||
ExecutionStatus::Succeeded,
|
ExecutionStatus::Succeeded,
|
||||||
@@ -239,7 +307,7 @@ mod tests {
|
|||||||
#[test]
|
#[test]
|
||||||
fn test_status_reset_failed() {
|
fn test_status_reset_failed() {
|
||||||
// 重置一个出现部分失败但还有重试次数的任务,将所有的失败状态重置为 0
|
// 重置一个出现部分失败但还有重试次数的任务,将所有的失败状态重置为 0
|
||||||
let mut status = Status::<3>::from([3, 4, 7]);
|
let mut status = Status::<3, video::Column>::from([3, 4, 7]);
|
||||||
assert!(!status.get_completed());
|
assert!(!status.get_completed());
|
||||||
assert!(status.reset_failed());
|
assert!(status.reset_failed());
|
||||||
assert!(!status.get_completed());
|
assert!(!status.get_completed());
|
||||||
@@ -253,12 +321,12 @@ mod tests {
|
|||||||
assert!(status.force_reset_failed());
|
assert!(status.force_reset_failed());
|
||||||
assert!(!status.get_completed());
|
assert!(!status.get_completed());
|
||||||
// 重置一个已经成功的任务,没有改变状态,也不会修改标记位
|
// 重置一个已经成功的任务,没有改变状态,也不会修改标记位
|
||||||
let mut status = Status::<3>::from([7, 7, 7]);
|
let mut status = Status::<3, video::Column>::from([7, 7, 7]);
|
||||||
assert!(status.get_completed());
|
assert!(status.get_completed());
|
||||||
assert!(!status.reset_failed());
|
assert!(!status.reset_failed());
|
||||||
assert!(status.get_completed());
|
assert!(status.get_completed());
|
||||||
// 重置一个全部失败的任务,修改状态并且修改标记位
|
// 重置一个全部失败的任务,修改状态并且修改标记位
|
||||||
let mut status = Status::<3>::from([4, 4, 4]);
|
let mut status = Status::<3, video::Column>::from([4, 4, 4]);
|
||||||
assert!(status.get_completed());
|
assert!(status.get_completed());
|
||||||
assert!(status.reset_failed());
|
assert!(status.reset_failed());
|
||||||
assert!(!status.get_completed());
|
assert!(!status.get_completed());
|
||||||
@@ -268,13 +336,13 @@ mod tests {
|
|||||||
#[test]
|
#[test]
|
||||||
fn test_status_set() {
|
fn test_status_set() {
|
||||||
// 设置子状态,从 completed 到 uncompleted
|
// 设置子状态,从 completed 到 uncompleted
|
||||||
let mut status = Status::<5>::from([7, 7, 7, 7, 7]);
|
let mut status = Status::<5, video::Column>::from([7, 7, 7, 7, 7]);
|
||||||
assert!(status.get_completed());
|
assert!(status.get_completed());
|
||||||
status.set(4, 0);
|
status.set(4, 0);
|
||||||
assert!(!status.get_completed());
|
assert!(!status.get_completed());
|
||||||
assert_eq!(<[u32; 5]>::from(status), [7, 7, 7, 7, 0]);
|
assert_eq!(<[u32; 5]>::from(status), [7, 7, 7, 7, 0]);
|
||||||
// 设置子状态,从 uncompleted 到 completed
|
// 设置子状态,从 uncompleted 到 completed
|
||||||
let mut status = Status::<5>::from([4, 7, 7, 7, 0]);
|
let mut status = Status::<5, video::Column>::from([4, 7, 7, 7, 0]);
|
||||||
assert!(!status.get_completed());
|
assert!(!status.get_completed());
|
||||||
status.set(4, 7);
|
status.set(4, 7);
|
||||||
assert!(status.get_completed());
|
assert!(status.get_completed());
|
||||||
|
|||||||
@@ -6,7 +6,7 @@ publish = { workspace = true }
|
|||||||
|
|
||||||
[dependencies]
|
[dependencies]
|
||||||
derivative = { workspace = true }
|
derivative = { workspace = true }
|
||||||
sea-orm = { workspace = true }
|
|
||||||
regex = { workspace = true }
|
regex = { workspace = true }
|
||||||
|
sea-orm = { workspace = true }
|
||||||
serde = { workspace = true }
|
serde = { workspace = true }
|
||||||
serde_json = { workspace = true }
|
serde_json = { workspace = true }
|
||||||
|
|||||||
@@ -21,7 +21,7 @@ export default defineConfig({
|
|||||||
nav: [
|
nav: [
|
||||||
{ text: "主页", link: "/" },
|
{ text: "主页", link: "/" },
|
||||||
{
|
{
|
||||||
text: "v2.10.1",
|
text: "v2.10.3",
|
||||||
items: [
|
items: [
|
||||||
{
|
{
|
||||||
text: "程序更新",
|
text: "程序更新",
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
# bili-sync 是什么?
|
# bili-sync 是什么?
|
||||||
|
|
||||||
> [!TIP]
|
> [!TIP]
|
||||||
> 当前最新程序版本为 v2.10.1,文档将始终与最新程序版本保持一致。
|
> 当前最新程序版本为 v2.10.3,文档将始终与最新程序版本保持一致。
|
||||||
|
|
||||||
bili-sync 是一款专为 NAS 用户编写的哔哩哔哩同步工具。
|
bili-sync 是一款专为 NAS 用户编写的哔哩哔哩同步工具。
|
||||||
|
|
||||||
|
|||||||
3
rust-toolchain.toml
Normal file
3
rust-toolchain.toml
Normal file
@@ -0,0 +1,3 @@
|
|||||||
|
[toolchain]
|
||||||
|
channel = "1.93.0"
|
||||||
|
components = ["clippy"]
|
||||||
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "bili-sync-web",
|
"name": "bili-sync-web",
|
||||||
"version": "2.10.1",
|
"version": "2.10.3",
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@eslint/compat": "^1.4.1",
|
"@eslint/compat": "^1.4.1",
|
||||||
"@eslint/js": "^9.39.2",
|
"@eslint/js": "^9.39.2",
|
||||||
|
|||||||
@@ -62,7 +62,7 @@
|
|||||||
</Button>
|
</Button>
|
||||||
{/snippet}
|
{/snippet}
|
||||||
</DropdownMenu.Trigger>
|
</DropdownMenu.Trigger>
|
||||||
<DropdownMenu.Content class="w-[200px]" align="end">
|
<DropdownMenu.Content class="w-50" align="end">
|
||||||
<DropdownMenu.Group>
|
<DropdownMenu.Group>
|
||||||
{#if filters}
|
{#if filters}
|
||||||
{#each Object.entries(filters) as [key, filter] (key)}
|
{#each Object.entries(filters) as [key, filter] (key)}
|
||||||
|
|||||||
93
web/src/lib/components/status-filter.svelte
Normal file
93
web/src/lib/components/status-filter.svelte
Normal file
@@ -0,0 +1,93 @@
|
|||||||
|
<script lang="ts">
|
||||||
|
import CheckCircleIcon from '@lucide/svelte/icons/check-circle';
|
||||||
|
import XCircleIcon from '@lucide/svelte/icons/x-circle';
|
||||||
|
import ClockIcon from '@lucide/svelte/icons/clock';
|
||||||
|
import ChevronDownIcon from '@lucide/svelte/icons/chevron-down';
|
||||||
|
import TrashIcon from '@lucide/svelte/icons/trash';
|
||||||
|
import * as DropdownMenu from '$lib/components/ui/dropdown-menu/index.js';
|
||||||
|
import { Button } from '$lib/components/ui/button/index.js';
|
||||||
|
import { type StatusFilterValue } from '$lib/stores/filter';
|
||||||
|
|
||||||
|
interface Props {
|
||||||
|
value: StatusFilterValue | null;
|
||||||
|
onSelect?: (value: StatusFilterValue) => void;
|
||||||
|
onRemove?: () => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
let { value = $bindable(null), onSelect, onRemove }: Props = $props();
|
||||||
|
|
||||||
|
let open = $state(false);
|
||||||
|
let triggerRef = $state<HTMLButtonElement>(null!);
|
||||||
|
|
||||||
|
function closeAndFocusTrigger() {
|
||||||
|
open = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
const statusOptions = [
|
||||||
|
{
|
||||||
|
value: 'failed' as const,
|
||||||
|
label: '仅失败',
|
||||||
|
icon: XCircleIcon
|
||||||
|
},
|
||||||
|
{
|
||||||
|
value: 'succeeded' as const,
|
||||||
|
label: '仅成功',
|
||||||
|
icon: CheckCircleIcon
|
||||||
|
},
|
||||||
|
{
|
||||||
|
value: 'waiting' as const,
|
||||||
|
label: '仅等待',
|
||||||
|
icon: ClockIcon
|
||||||
|
}
|
||||||
|
];
|
||||||
|
|
||||||
|
function handleSelect(selectedValue: StatusFilterValue) {
|
||||||
|
value = selectedValue;
|
||||||
|
onSelect?.(selectedValue);
|
||||||
|
closeAndFocusTrigger();
|
||||||
|
}
|
||||||
|
|
||||||
|
const currentOption = $derived(statusOptions.find((opt) => opt.value === value));
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<div class="inline-flex items-center gap-1">
|
||||||
|
<span class="bg-secondary text-secondary-foreground rounded-lg px-2 py-1 text-xs font-medium">
|
||||||
|
{currentOption ? currentOption.label : '未应用'}
|
||||||
|
</span>
|
||||||
|
|
||||||
|
<DropdownMenu.Root bind:open>
|
||||||
|
<DropdownMenu.Trigger bind:ref={triggerRef}>
|
||||||
|
{#snippet child({ props })}
|
||||||
|
<Button variant="ghost" size="sm" {...props} class="h-6 w-6 p-0">
|
||||||
|
<ChevronDownIcon class="h-3 w-3" />
|
||||||
|
</Button>
|
||||||
|
{/snippet}
|
||||||
|
</DropdownMenu.Trigger>
|
||||||
|
<DropdownMenu.Content class="w-50" align="end">
|
||||||
|
<DropdownMenu.Group>
|
||||||
|
<DropdownMenu.Label class="text-xs">视频状态</DropdownMenu.Label>
|
||||||
|
{#each statusOptions as option (option.value)}
|
||||||
|
<DropdownMenu.Item class="text-xs" onclick={() => handleSelect(option.value)}>
|
||||||
|
<option.icon class="mr-2 size-3" />
|
||||||
|
<span class:font-semibold={value === option.value}>
|
||||||
|
{option.label}
|
||||||
|
</span>
|
||||||
|
{#if value === option.value}
|
||||||
|
<CheckCircleIcon class="ml-auto size-3" />
|
||||||
|
{/if}
|
||||||
|
</DropdownMenu.Item>
|
||||||
|
{/each}
|
||||||
|
<DropdownMenu.Separator />
|
||||||
|
<DropdownMenu.Item
|
||||||
|
onclick={() => {
|
||||||
|
closeAndFocusTrigger();
|
||||||
|
onRemove?.();
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<TrashIcon class="mr-2 size-3" />
|
||||||
|
<span class="text-xs font-medium">移除筛选</span>
|
||||||
|
</DropdownMenu.Item>
|
||||||
|
</DropdownMenu.Group>
|
||||||
|
</DropdownMenu.Content>
|
||||||
|
</DropdownMenu.Root>
|
||||||
|
</div>
|
||||||
@@ -1,5 +1,7 @@
|
|||||||
import { writable } from 'svelte/store';
|
import { writable } from 'svelte/store';
|
||||||
|
|
||||||
|
export type StatusFilterValue = 'failed' | 'succeeded' | 'waiting' | null;
|
||||||
|
|
||||||
export interface AppState {
|
export interface AppState {
|
||||||
query: string;
|
query: string;
|
||||||
currentPage: number;
|
currentPage: number;
|
||||||
@@ -7,19 +9,21 @@ export interface AppState {
|
|||||||
type: string;
|
type: string;
|
||||||
id: string;
|
id: string;
|
||||||
} | null;
|
} | null;
|
||||||
|
statusFilter: StatusFilterValue | null;
|
||||||
}
|
}
|
||||||
|
|
||||||
export const appStateStore = writable<AppState>({
|
export const appStateStore = writable<AppState>({
|
||||||
query: '',
|
query: '',
|
||||||
currentPage: 0,
|
currentPage: 0,
|
||||||
videoSource: null
|
videoSource: null,
|
||||||
|
statusFilter: null
|
||||||
});
|
});
|
||||||
|
|
||||||
export const ToQuery = (state: AppState): string => {
|
export const ToQuery = (state: AppState): string => {
|
||||||
const { query, videoSource } = state;
|
const { query, videoSource, currentPage, statusFilter } = state;
|
||||||
const params = new URLSearchParams();
|
const params = new URLSearchParams();
|
||||||
if (state.currentPage > 0) {
|
if (currentPage > 0) {
|
||||||
params.set('page', String(state.currentPage));
|
params.set('page', String(currentPage));
|
||||||
}
|
}
|
||||||
if (query.trim()) {
|
if (query.trim()) {
|
||||||
params.set('query', query);
|
params.set('query', query);
|
||||||
@@ -27,6 +31,9 @@ export const ToQuery = (state: AppState): string => {
|
|||||||
if (videoSource && videoSource.type && videoSource.id) {
|
if (videoSource && videoSource.type && videoSource.id) {
|
||||||
params.set(videoSource.type, videoSource.id);
|
params.set(videoSource.type, videoSource.id);
|
||||||
}
|
}
|
||||||
|
if (statusFilter) {
|
||||||
|
params.set('status_filter', statusFilter);
|
||||||
|
}
|
||||||
const queryString = params.toString();
|
const queryString = params.toString();
|
||||||
return queryString ? `videos?${queryString}` : 'videos';
|
return queryString ? `videos?${queryString}` : 'videos';
|
||||||
};
|
};
|
||||||
@@ -40,6 +47,7 @@ export const ToFilterParams = (
|
|||||||
favorite?: number;
|
favorite?: number;
|
||||||
submission?: number;
|
submission?: number;
|
||||||
watch_later?: number;
|
watch_later?: number;
|
||||||
|
status_filter?: Exclude<StatusFilterValue, null>;
|
||||||
} => {
|
} => {
|
||||||
const params: {
|
const params: {
|
||||||
query?: string;
|
query?: string;
|
||||||
@@ -47,6 +55,7 @@ export const ToFilterParams = (
|
|||||||
favorite?: number;
|
favorite?: number;
|
||||||
submission?: number;
|
submission?: number;
|
||||||
watch_later?: number;
|
watch_later?: number;
|
||||||
|
status_filter?: Exclude<StatusFilterValue, null>;
|
||||||
} = {};
|
} = {};
|
||||||
|
|
||||||
if (state.query.trim()) {
|
if (state.query.trim()) {
|
||||||
@@ -57,13 +66,15 @@ export const ToFilterParams = (
|
|||||||
const { type, id } = state.videoSource;
|
const { type, id } = state.videoSource;
|
||||||
params[type as 'collection' | 'favorite' | 'submission' | 'watch_later'] = parseInt(id);
|
params[type as 'collection' | 'favorite' | 'submission' | 'watch_later'] = parseInt(id);
|
||||||
}
|
}
|
||||||
|
if (state.statusFilter) {
|
||||||
|
params.status_filter = state.statusFilter;
|
||||||
|
}
|
||||||
return params;
|
return params;
|
||||||
};
|
};
|
||||||
|
|
||||||
// 检查是否有活动的筛选条件
|
// 检查是否有活动的筛选条件
|
||||||
export const hasActiveFilters = (state: AppState): boolean => {
|
export const hasActiveFilters = (state: AppState): boolean => {
|
||||||
return !!(state.query.trim() || state.videoSource);
|
return !!(state.query.trim() || state.videoSource || state.statusFilter);
|
||||||
};
|
};
|
||||||
|
|
||||||
export const setQuery = (query: string) => {
|
export const setQuery = (query: string) => {
|
||||||
@@ -73,20 +84,6 @@ export const setQuery = (query: string) => {
|
|||||||
}));
|
}));
|
||||||
};
|
};
|
||||||
|
|
||||||
export const setVideoSourceFilter = (filter: { type: string; id: string }) => {
|
|
||||||
appStateStore.update((state) => ({
|
|
||||||
...state,
|
|
||||||
videoSource: filter
|
|
||||||
}));
|
|
||||||
};
|
|
||||||
|
|
||||||
export const clearVideoSourceFilter = () => {
|
|
||||||
appStateStore.update((state) => ({
|
|
||||||
...state,
|
|
||||||
videoSource: null
|
|
||||||
}));
|
|
||||||
};
|
|
||||||
|
|
||||||
export const setCurrentPage = (page: number) => {
|
export const setCurrentPage = (page: number) => {
|
||||||
appStateStore.update((state) => ({
|
appStateStore.update((state) => ({
|
||||||
...state,
|
...state,
|
||||||
@@ -94,6 +91,13 @@ export const setCurrentPage = (page: number) => {
|
|||||||
}));
|
}));
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export const setStatusFilter = (statusFilter: StatusFilterValue | null) => {
|
||||||
|
appStateStore.update((state) => ({
|
||||||
|
...state,
|
||||||
|
statusFilter
|
||||||
|
}));
|
||||||
|
};
|
||||||
|
|
||||||
export const resetCurrentPage = () => {
|
export const resetCurrentPage = () => {
|
||||||
appStateStore.update((state) => ({
|
appStateStore.update((state) => ({
|
||||||
...state,
|
...state,
|
||||||
@@ -104,19 +108,13 @@ export const resetCurrentPage = () => {
|
|||||||
export const setAll = (
|
export const setAll = (
|
||||||
query: string,
|
query: string,
|
||||||
currentPage: number,
|
currentPage: number,
|
||||||
videoSource: { type: string; id: string } | null
|
videoSource: { type: string; id: string } | null,
|
||||||
|
statusFilter: StatusFilterValue | null
|
||||||
) => {
|
) => {
|
||||||
appStateStore.set({
|
appStateStore.set({
|
||||||
query,
|
query,
|
||||||
currentPage,
|
currentPage,
|
||||||
videoSource
|
videoSource,
|
||||||
});
|
statusFilter
|
||||||
};
|
|
||||||
|
|
||||||
export const clearAll = () => {
|
|
||||||
appStateStore.set({
|
|
||||||
query: '',
|
|
||||||
currentPage: 0,
|
|
||||||
videoSource: null
|
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -9,6 +9,7 @@ export interface VideosRequest {
|
|||||||
submission?: number;
|
submission?: number;
|
||||||
watch_later?: number;
|
watch_later?: number;
|
||||||
query?: string;
|
query?: string;
|
||||||
|
failed_only?: boolean;
|
||||||
page?: number;
|
page?: number;
|
||||||
page_size?: number;
|
page_size?: number;
|
||||||
}
|
}
|
||||||
@@ -106,6 +107,8 @@ export interface UpdateFilteredVideoStatusRequest {
|
|||||||
submission?: number;
|
submission?: number;
|
||||||
watch_later?: number;
|
watch_later?: number;
|
||||||
query?: string;
|
query?: string;
|
||||||
|
// 仅更新下载失败
|
||||||
|
failed_only?: boolean;
|
||||||
video_updates?: StatusUpdate[];
|
video_updates?: StatusUpdate[];
|
||||||
page_updates?: StatusUpdate[];
|
page_updates?: StatusUpdate[];
|
||||||
}
|
}
|
||||||
@@ -120,6 +123,8 @@ export interface ResetFilteredVideoStatusRequest {
|
|||||||
submission?: number;
|
submission?: number;
|
||||||
watch_later?: number;
|
watch_later?: number;
|
||||||
query?: string;
|
query?: string;
|
||||||
|
// 仅重置下载失败
|
||||||
|
failed_only?: boolean;
|
||||||
force: boolean;
|
force: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -4,6 +4,7 @@
|
|||||||
import { Switch } from '$lib/components/ui/switch/index.js';
|
import { Switch } from '$lib/components/ui/switch/index.js';
|
||||||
import { Input } from '$lib/components/ui/input/index.js';
|
import { Input } from '$lib/components/ui/input/index.js';
|
||||||
import { Label } from '$lib/components/ui/label/index.js';
|
import { Label } from '$lib/components/ui/label/index.js';
|
||||||
|
import { Badge } from '$lib/components/ui/badge/index.js';
|
||||||
import * as Table from '$lib/components/ui/table/index.js';
|
import * as Table from '$lib/components/ui/table/index.js';
|
||||||
import * as Tabs from '$lib/components/ui/tabs/index.js';
|
import * as Tabs from '$lib/components/ui/tabs/index.js';
|
||||||
import * as Dialog from '$lib/components/ui/dialog/index.js';
|
import * as Dialog from '$lib/components/ui/dialog/index.js';
|
||||||
@@ -15,6 +16,8 @@
|
|||||||
import PlusIcon from '@lucide/svelte/icons/plus';
|
import PlusIcon from '@lucide/svelte/icons/plus';
|
||||||
import InfoIcon from '@lucide/svelte/icons/info';
|
import InfoIcon from '@lucide/svelte/icons/info';
|
||||||
import TrashIcon2 from '@lucide/svelte/icons/trash-2';
|
import TrashIcon2 from '@lucide/svelte/icons/trash-2';
|
||||||
|
import CheckCircleIcon from '@lucide/svelte/icons/check-circle';
|
||||||
|
import XCircleIcon from '@lucide/svelte/icons/x-circle';
|
||||||
import * as Tooltip from '$lib/components/ui/tooltip/index.js';
|
import * as Tooltip from '$lib/components/ui/tooltip/index.js';
|
||||||
import { toast } from 'svelte-sonner';
|
import { toast } from 'svelte-sonner';
|
||||||
import { setBreadcrumb } from '$lib/stores/breadcrumb';
|
import { setBreadcrumb } from '$lib/stores/breadcrumb';
|
||||||
@@ -315,10 +318,7 @@
|
|||||||
<Table.Head class="w-[20%]">名称</Table.Head>
|
<Table.Head class="w-[20%]">名称</Table.Head>
|
||||||
<Table.Head class="w-[30%]">下载路径</Table.Head>
|
<Table.Head class="w-[30%]">下载路径</Table.Head>
|
||||||
<Table.Head class="w-[15%]">过滤规则</Table.Head>
|
<Table.Head class="w-[15%]">过滤规则</Table.Head>
|
||||||
<Table.Head class="w-[10%]">启用状态</Table.Head>
|
<Table.Head class="w-[15%]">启用状态</Table.Head>
|
||||||
{#if key === 'submissions'}
|
|
||||||
<Table.Head class="w-[10%]">使用动态 API</Table.Head>
|
|
||||||
{/if}
|
|
||||||
<Table.Head class="w-[10%] text-right">操作</Table.Head>
|
<Table.Head class="w-[10%] text-right">操作</Table.Head>
|
||||||
</Table.Row>
|
</Table.Row>
|
||||||
</Table.Header>
|
</Table.Header>
|
||||||
@@ -327,73 +327,103 @@
|
|||||||
<Table.Row>
|
<Table.Row>
|
||||||
<Table.Cell class="font-medium">{source.name}</Table.Cell>
|
<Table.Cell class="font-medium">{source.name}</Table.Cell>
|
||||||
<Table.Cell>
|
<Table.Cell>
|
||||||
<code
|
<div
|
||||||
class="bg-muted text-muted-foreground inline-flex h-8 items-center rounded px-3 py-1 text-sm"
|
class="bg-secondary hover:bg-secondary/80 flex w-fit cursor-text items-center gap-2 rounded-md px-2.5 py-1.5 transition-colors"
|
||||||
>
|
>
|
||||||
{source.path || '未设置'}
|
<FolderIcon class="text-foreground/70 h-3.5 w-3.5 shrink-0" />
|
||||||
</code>
|
<span
|
||||||
|
class="text-foreground/70 font-mono text-xs font-medium select-text"
|
||||||
|
>
|
||||||
|
{source.path || '未设置'}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
</Table.Cell>
|
</Table.Cell>
|
||||||
<Table.Cell>
|
<Table.Cell>
|
||||||
{#if source.rule && source.rule.length > 0}
|
{#if source.rule && source.rule.length > 0}
|
||||||
<div class="flex items-center gap-1">
|
<Tooltip.Root disableHoverableContent={true}>
|
||||||
<Tooltip.Root>
|
<Tooltip.Trigger>
|
||||||
<Tooltip.Trigger>
|
<Badge
|
||||||
<span class="text-muted-foreground text-sm"
|
variant="secondary"
|
||||||
>{source.rule.length} 条规则</span
|
class="flex w-fit cursor-help items-center gap-1.5"
|
||||||
>
|
>
|
||||||
</Tooltip.Trigger>
|
{source.rule.length} 条规则
|
||||||
<Tooltip.Content>
|
</Badge>
|
||||||
<p class="text-xs">{source.ruleDisplay}</p>
|
</Tooltip.Trigger>
|
||||||
</Tooltip.Content>
|
<Tooltip.Content>
|
||||||
</Tooltip.Root>
|
<p class="text-xs">{source.ruleDisplay}</p>
|
||||||
</div>
|
</Tooltip.Content>
|
||||||
|
</Tooltip.Root>
|
||||||
{:else}
|
{:else}
|
||||||
<span class="text-muted-foreground text-sm">-</span>
|
<Badge variant="secondary" class="flex w-fit items-center gap-1.5">
|
||||||
|
-
|
||||||
|
</Badge>
|
||||||
{/if}
|
{/if}
|
||||||
</Table.Cell>
|
</Table.Cell>
|
||||||
<Table.Cell>
|
<Table.Cell>
|
||||||
<div class="flex h-8 items-center gap-2">
|
{#if source.enabled}
|
||||||
<Switch checked={source.enabled} disabled />
|
<Badge
|
||||||
</div>
|
class="flex w-fit items-center gap-1.5 bg-emerald-700 text-emerald-100"
|
||||||
</Table.Cell>
|
|
||||||
{#if key === 'submissions'}
|
|
||||||
<Table.Cell>
|
|
||||||
<div class="flex h-8 items-center gap-2">
|
|
||||||
{#if source.useDynamicApi !== null}
|
|
||||||
<Switch checked={source.useDynamicApi} disabled />
|
|
||||||
{/if}
|
|
||||||
</div>
|
|
||||||
</Table.Cell>
|
|
||||||
{/if}
|
|
||||||
<Table.Cell class="text-right">
|
|
||||||
<Button
|
|
||||||
size="sm"
|
|
||||||
variant="outline"
|
|
||||||
onclick={() => openEditDialog(key, source, index)}
|
|
||||||
class="h-8 w-8 p-0"
|
|
||||||
title="编辑"
|
|
||||||
>
|
|
||||||
<EditIcon class="h-3 w-3" />
|
|
||||||
</Button>
|
|
||||||
<Button
|
|
||||||
size="sm"
|
|
||||||
variant="outline"
|
|
||||||
onclick={() => openEvaluateRules(key, source)}
|
|
||||||
class="h-8 w-8 p-0"
|
|
||||||
title="重新评估规则"
|
|
||||||
>
|
|
||||||
<ListRestartIcon class="h-3 w-3" />
|
|
||||||
</Button>
|
|
||||||
{#if activeTab !== 'watch_later'}
|
|
||||||
<Button
|
|
||||||
size="sm"
|
|
||||||
variant="outline"
|
|
||||||
onclick={() => openRemoveDialog(key, source, index)}
|
|
||||||
class="h-8 w-8 p-0"
|
|
||||||
title="删除"
|
|
||||||
>
|
>
|
||||||
<TrashIcon2 class="h-3 w-3" />
|
<CheckCircleIcon class="h-3 w-3" />
|
||||||
</Button>
|
已启用{#if key === 'submissions' && source.useDynamicApi !== null}{source.useDynamicApi
|
||||||
|
? '(动态 API)'
|
||||||
|
: ''}{/if}
|
||||||
|
</Badge>
|
||||||
|
{:else}
|
||||||
|
<Badge class="flex w-fit items-center gap-1.5 bg-rose-700 text-rose-100 ">
|
||||||
|
<XCircleIcon class="h-3 w-3" />
|
||||||
|
已禁用
|
||||||
|
</Badge>
|
||||||
|
{/if}
|
||||||
|
</Table.Cell>
|
||||||
|
|
||||||
|
<Table.Cell class="text-right">
|
||||||
|
<Tooltip.Root disableHoverableContent={true}>
|
||||||
|
<Tooltip.Trigger>
|
||||||
|
<Button
|
||||||
|
size="sm"
|
||||||
|
variant="outline"
|
||||||
|
onclick={() => openEditDialog(key, source, index)}
|
||||||
|
class="h-8 w-8 p-0"
|
||||||
|
>
|
||||||
|
<EditIcon class="h-3 w-3" />
|
||||||
|
</Button>
|
||||||
|
</Tooltip.Trigger>
|
||||||
|
<Tooltip.Content>
|
||||||
|
<p class="text-xs">编辑</p>
|
||||||
|
</Tooltip.Content>
|
||||||
|
</Tooltip.Root>
|
||||||
|
<Tooltip.Root disableHoverableContent={true}>
|
||||||
|
<Tooltip.Trigger>
|
||||||
|
<Button
|
||||||
|
size="sm"
|
||||||
|
variant="outline"
|
||||||
|
onclick={() => openEvaluateRules(key, source)}
|
||||||
|
class="h-8 w-8 p-0"
|
||||||
|
>
|
||||||
|
<ListRestartIcon class="h-3 w-3" />
|
||||||
|
</Button>
|
||||||
|
</Tooltip.Trigger>
|
||||||
|
<Tooltip.Content>
|
||||||
|
<p class="text-xs">重新评估规则</p>
|
||||||
|
</Tooltip.Content>
|
||||||
|
</Tooltip.Root>
|
||||||
|
{#if activeTab !== 'watch_later'}
|
||||||
|
<Tooltip.Root disableHoverableContent={true}>
|
||||||
|
<Tooltip.Trigger>
|
||||||
|
<Button
|
||||||
|
size="sm"
|
||||||
|
variant="outline"
|
||||||
|
onclick={() => openRemoveDialog(key, source, index)}
|
||||||
|
class="h-8 w-8 p-0"
|
||||||
|
>
|
||||||
|
<TrashIcon2 class="h-3 w-3" />
|
||||||
|
</Button>
|
||||||
|
</Tooltip.Trigger>
|
||||||
|
<Tooltip.Content>
|
||||||
|
<p class="text-xs">删除</p>
|
||||||
|
</Tooltip.Content>
|
||||||
|
</Tooltip.Root>
|
||||||
{/if}
|
{/if}
|
||||||
</Table.Cell>
|
</Table.Cell>
|
||||||
</Table.Row>
|
</Table.Row>
|
||||||
@@ -438,7 +468,7 @@
|
|||||||
<!-- 编辑对话框 -->
|
<!-- 编辑对话框 -->
|
||||||
<Dialog.Root bind:open={showEditDialog}>
|
<Dialog.Root bind:open={showEditDialog}>
|
||||||
<Dialog.Content
|
<Dialog.Content
|
||||||
class="no-scrollbar max-h-[85vh] !max-w-[90vw] overflow-y-auto lg:!max-w-[70vw]"
|
class="no-scrollbar max-h-[85vh] max-w-[90vw]! overflow-y-auto lg:max-w-[70vw]!"
|
||||||
>
|
>
|
||||||
<Dialog.Title class="text-lg font-semibold">
|
<Dialog.Title class="text-lg font-semibold">
|
||||||
编辑视频源: {editingSource?.name || ''}
|
编辑视频源: {editingSource?.name || ''}
|
||||||
|
|||||||
@@ -26,14 +26,17 @@
|
|||||||
setAll,
|
setAll,
|
||||||
setCurrentPage,
|
setCurrentPage,
|
||||||
setQuery,
|
setQuery,
|
||||||
|
setStatusFilter,
|
||||||
ToQuery,
|
ToQuery,
|
||||||
ToFilterParams,
|
ToFilterParams,
|
||||||
hasActiveFilters
|
hasActiveFilters,
|
||||||
|
type StatusFilterValue
|
||||||
} from '$lib/stores/filter';
|
} from '$lib/stores/filter';
|
||||||
import { toast } from 'svelte-sonner';
|
import { toast } from 'svelte-sonner';
|
||||||
import DropdownFilter, { type Filter } from '$lib/components/dropdown-filter.svelte';
|
import DropdownFilter, { type Filter } from '$lib/components/dropdown-filter.svelte';
|
||||||
import SearchBar from '$lib/components/search-bar.svelte';
|
import SearchBar from '$lib/components/search-bar.svelte';
|
||||||
import FilteredStatusEditor from '$lib/components/filtered-status-editor.svelte';
|
import FilteredStatusEditor from '$lib/components/filtered-status-editor.svelte';
|
||||||
|
import StatusFilter from '$lib/components/status-filter.svelte';
|
||||||
|
|
||||||
const pageSize = 20;
|
const pageSize = 20;
|
||||||
|
|
||||||
@@ -61,9 +64,18 @@
|
|||||||
videoSource = { type: source.type, id: value };
|
videoSource = { type: source.type, id: value };
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
// 支持从 URL 里还原状态筛选
|
||||||
|
const statusFilterParam = searchParams.get('status_filter');
|
||||||
|
const statusFilter: StatusFilterValue | null =
|
||||||
|
statusFilterParam === 'failed' ||
|
||||||
|
statusFilterParam === 'succeeded' ||
|
||||||
|
statusFilterParam === 'waiting'
|
||||||
|
? statusFilterParam
|
||||||
|
: null;
|
||||||
return {
|
return {
|
||||||
query: searchParams.get('query') || '',
|
query: searchParams.get('query') || '',
|
||||||
videoSource,
|
videoSource,
|
||||||
|
statusFilter,
|
||||||
pageNum: parseInt(searchParams.get('page') || '0')
|
pageNum: parseInt(searchParams.get('page') || '0')
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
@@ -71,11 +83,12 @@
|
|||||||
async function loadVideos(
|
async function loadVideos(
|
||||||
query: string,
|
query: string,
|
||||||
pageNum: number = 0,
|
pageNum: number = 0,
|
||||||
filter?: { type: string; id: string } | null
|
filter?: { type: string; id: string } | null,
|
||||||
|
statusFilter: StatusFilterValue | null = null
|
||||||
) {
|
) {
|
||||||
loading = true;
|
loading = true;
|
||||||
try {
|
try {
|
||||||
const params: Record<string, string | number> = {
|
const params: Record<string, string | number | boolean> = {
|
||||||
page: pageNum,
|
page: pageNum,
|
||||||
page_size: pageSize
|
page_size: pageSize
|
||||||
};
|
};
|
||||||
@@ -85,6 +98,9 @@
|
|||||||
if (filter) {
|
if (filter) {
|
||||||
params[filter.type] = parseInt(filter.id);
|
params[filter.type] = parseInt(filter.id);
|
||||||
}
|
}
|
||||||
|
if (statusFilter) {
|
||||||
|
params.status_filter = statusFilter;
|
||||||
|
}
|
||||||
const result = await api.getVideos(params);
|
const result = await api.getVideos(params);
|
||||||
videosData = result.data;
|
videosData = result.data;
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
@@ -103,9 +119,9 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
async function handleSearchParamsChange(searchParams: URLSearchParams) {
|
async function handleSearchParamsChange(searchParams: URLSearchParams) {
|
||||||
const { query, videoSource, pageNum } = getApiParams(searchParams);
|
const { query, videoSource, pageNum, statusFilter } = getApiParams(searchParams);
|
||||||
setAll(query, pageNum, videoSource);
|
setAll(query, pageNum, videoSource, statusFilter);
|
||||||
loadVideos(query, pageNum, videoSource);
|
loadVideos(query, pageNum, videoSource, statusFilter);
|
||||||
}
|
}
|
||||||
|
|
||||||
async function handleResetVideo(id: number, forceReset: boolean) {
|
async function handleResetVideo(id: number, forceReset: boolean) {
|
||||||
@@ -116,8 +132,8 @@
|
|||||||
toast.success('重置成功', {
|
toast.success('重置成功', {
|
||||||
description: `视频「${data.video.name}」已重置`
|
description: `视频「${data.video.name}」已重置`
|
||||||
});
|
});
|
||||||
const { query, currentPage, videoSource } = $appStateStore;
|
const { query, currentPage, videoSource, statusFilter } = $appStateStore;
|
||||||
await loadVideos(query, currentPage, videoSource);
|
await loadVideos(query, currentPage, videoSource, statusFilter);
|
||||||
} else {
|
} else {
|
||||||
toast.info('重置无效', {
|
toast.info('重置无效', {
|
||||||
description: `视频「${data.video.name}」没有失败的状态,无需重置`
|
description: `视频「${data.video.name}」没有失败的状态,无需重置`
|
||||||
@@ -144,8 +160,8 @@
|
|||||||
description: `视频「${data.video.name}」已清空重置`
|
description: `视频「${data.video.name}」已清空重置`
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
const { query, currentPage, videoSource } = $appStateStore;
|
const { query, currentPage, videoSource, statusFilter } = $appStateStore;
|
||||||
await loadVideos(query, currentPage, videoSource);
|
await loadVideos(query, currentPage, videoSource, statusFilter);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('清空重置失败:', error);
|
console.error('清空重置失败:', error);
|
||||||
toast.error('清空重置失败', {
|
toast.error('清空重置失败', {
|
||||||
@@ -168,8 +184,8 @@
|
|||||||
toast.success('重置成功', {
|
toast.success('重置成功', {
|
||||||
description: `已重置 ${data.resetted_videos_count} 个视频和 ${data.resetted_pages_count} 个分页`
|
description: `已重置 ${data.resetted_videos_count} 个视频和 ${data.resetted_pages_count} 个分页`
|
||||||
});
|
});
|
||||||
const { query, currentPage, videoSource } = $appStateStore;
|
const { query, currentPage, videoSource, statusFilter } = $appStateStore;
|
||||||
await loadVideos(query, currentPage, videoSource);
|
await loadVideos(query, currentPage, videoSource, statusFilter);
|
||||||
} else {
|
} else {
|
||||||
toast.info('没有需要重置的视频');
|
toast.info('没有需要重置的视频');
|
||||||
}
|
}
|
||||||
@@ -199,8 +215,8 @@
|
|||||||
toast.success('更新成功', {
|
toast.success('更新成功', {
|
||||||
description: `已更新 ${data.updated_videos_count} 个视频和 ${data.updated_pages_count} 个分页`
|
description: `已更新 ${data.updated_videos_count} 个视频和 ${data.updated_pages_count} 个分页`
|
||||||
});
|
});
|
||||||
const { query, currentPage, videoSource } = $appStateStore;
|
const { query, currentPage, videoSource, statusFilter } = $appStateStore;
|
||||||
await loadVideos(query, currentPage, videoSource);
|
await loadVideos(query, currentPage, videoSource, statusFilter);
|
||||||
} else {
|
} else {
|
||||||
toast.info('没有视频被更新');
|
toast.info('没有视频被更新');
|
||||||
}
|
}
|
||||||
@@ -234,6 +250,14 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
if (state.statusFilter) {
|
||||||
|
const statusLabels = {
|
||||||
|
failed: '仅失败',
|
||||||
|
succeeded: '仅成功',
|
||||||
|
waiting: '仅等待'
|
||||||
|
};
|
||||||
|
parts.push(`状态:${statusLabels[state.statusFilter]}`);
|
||||||
|
}
|
||||||
return parts;
|
return parts;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -289,20 +313,40 @@
|
|||||||
goto(`/${ToQuery($appStateStore)}`);
|
goto(`/${ToQuery($appStateStore)}`);
|
||||||
}}
|
}}
|
||||||
></SearchBar>
|
></SearchBar>
|
||||||
<div class="flex items-center gap-2">
|
<div class="flex items-center gap-3">
|
||||||
<span class="text-muted-foreground text-sm">筛选:</span>
|
<!-- 状态筛选 -->
|
||||||
<DropdownFilter
|
<div class="flex items-center gap-1">
|
||||||
{filters}
|
<span class="text-muted-foreground text-xs">状态:</span>
|
||||||
selectedLabel={$appStateStore.videoSource}
|
<StatusFilter
|
||||||
onSelect={(type, id) => {
|
value={$appStateStore.statusFilter}
|
||||||
setAll('', 0, { type, id });
|
onSelect={(value) => {
|
||||||
goto(`/${ToQuery($appStateStore)}`);
|
setStatusFilter(value);
|
||||||
}}
|
resetCurrentPage();
|
||||||
onRemove={() => {
|
goto(`/${ToQuery($appStateStore)}`);
|
||||||
setAll('', 0, null);
|
}}
|
||||||
goto(`/${ToQuery($appStateStore)}`);
|
onRemove={() => {
|
||||||
}}
|
setStatusFilter(null);
|
||||||
/>
|
resetCurrentPage();
|
||||||
|
goto(`/${ToQuery($appStateStore)}`);
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<!-- 视频源筛选 -->
|
||||||
|
<div class="flex items-center gap-1">
|
||||||
|
<span class="text-muted-foreground text-xs">来源:</span>
|
||||||
|
<DropdownFilter
|
||||||
|
{filters}
|
||||||
|
selectedLabel={$appStateStore.videoSource}
|
||||||
|
onSelect={(type, id) => {
|
||||||
|
setAll('', 0, { type, id }, $appStateStore.statusFilter);
|
||||||
|
goto(`/${ToQuery($appStateStore)}`);
|
||||||
|
}}
|
||||||
|
onRemove={() => {
|
||||||
|
setAll('', 0, null, $appStateStore.statusFilter);
|
||||||
|
goto(`/${ToQuery($appStateStore)}`);
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user