mirror of
https://github.com/amtoaer/bili-sync.git
synced 2026-05-11 18:11:05 +08:00
Compare commits
36 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
16e14fc371 | ||
|
|
b4a5dee236 | ||
|
|
2b3e6f9547 | ||
|
|
f8b93d2c76 | ||
|
|
94462ca706 | ||
|
|
9cbefc26ab | ||
|
|
2bfd69c15e | ||
|
|
4765d6f50a | ||
|
|
bf306dfec3 | ||
|
|
a6425f11a2 | ||
|
|
395ef0013a | ||
|
|
ab0533210f | ||
|
|
3eb2f0b14d | ||
|
|
42272b1294 | ||
|
|
d1168f35f3 | ||
|
|
bc27778366 | ||
|
|
9c5f3452e9 | ||
|
|
d3b4559b2d | ||
|
|
59305c0bb4 | ||
|
|
32214d5d5f | ||
|
|
315ad13703 | ||
|
|
e12a9cda95 | ||
|
|
c995b3bf72 | ||
|
|
1467c262a1 | ||
|
|
7251802202 | ||
|
|
e1285ff49a | ||
|
|
e01a22136e | ||
|
|
eba69ff82a | ||
|
|
5af6fe5e6e | ||
|
|
9d8e398cbe | ||
|
|
7097b2a6b9 | ||
|
|
acf7359d56 | ||
|
|
7c514b2dcc | ||
|
|
2c4fa441e7 | ||
|
|
51672e8607 | ||
|
|
cc7f773300 |
@@ -1,24 +1,52 @@
|
|||||||
name: Build Binary And Release
|
name: Build Binary
|
||||||
|
|
||||||
on:
|
on:
|
||||||
push:
|
workflow_call:
|
||||||
tags:
|
|
||||||
- v*
|
|
||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
|
build-frontend:
|
||||||
|
name: Build frontend
|
||||||
|
runs-on: ubuntu-24.04
|
||||||
|
defaults:
|
||||||
|
run:
|
||||||
|
working-directory: web
|
||||||
|
steps:
|
||||||
|
- name: Checkout repo
|
||||||
|
uses: actions/checkout@v4
|
||||||
|
- name: Setup bun
|
||||||
|
uses: oven-sh/setup-bun@v2
|
||||||
|
with:
|
||||||
|
bun-version: latest
|
||||||
|
- name: Install dependencies
|
||||||
|
run: bun install --frozen-lockfile
|
||||||
|
- name: Cache dependencies
|
||||||
|
uses: actions/cache@v4
|
||||||
|
with:
|
||||||
|
path: ~/.bun/install/cache
|
||||||
|
key: ${{ runner.os }}-bun-${{ hashFiles('docs/bun.lockb') }}
|
||||||
|
restore-keys: |
|
||||||
|
${{ runner.os }}-bun-
|
||||||
|
- name: Build Frontend
|
||||||
|
run: bun run build
|
||||||
|
- name: Upload Web Build Artifact
|
||||||
|
uses: actions/upload-artifact@v4
|
||||||
|
with:
|
||||||
|
name: web-build
|
||||||
|
path: web/build
|
||||||
build:
|
build:
|
||||||
name: Release for ${{ matrix.platform.release_for }}
|
name: Build bili-sync-rs for ${{ matrix.platform.release_for }}
|
||||||
|
needs: build-frontend
|
||||||
runs-on: ${{ matrix.platform.os }}
|
runs-on: ${{ matrix.platform.os }}
|
||||||
strategy:
|
strategy:
|
||||||
matrix:
|
matrix:
|
||||||
platform:
|
platform:
|
||||||
- release_for: Linux-x86_64
|
- release_for: Linux-x86_64
|
||||||
os: ubuntu-20.04
|
os: ubuntu-24.04
|
||||||
target: x86_64-unknown-linux-musl
|
target: x86_64-unknown-linux-musl
|
||||||
bin: bili-sync-rs
|
bin: bili-sync-rs
|
||||||
name: bili-sync-rs-Linux-x86_64-musl.tar.gz
|
name: bili-sync-rs-Linux-x86_64-musl.tar.gz
|
||||||
- release_for: Linux-aarch64
|
- release_for: Linux-aarch64
|
||||||
os: ubuntu-20.04
|
os: ubuntu-24.04
|
||||||
target: aarch64-unknown-linux-musl
|
target: aarch64-unknown-linux-musl
|
||||||
bin: bili-sync-rs
|
bin: bili-sync-rs
|
||||||
name: bili-sync-rs-Linux-aarch64-musl.tar.gz
|
name: bili-sync-rs-Linux-aarch64-musl.tar.gz
|
||||||
@@ -37,10 +65,16 @@ jobs:
|
|||||||
target: x86_64-pc-windows-msvc
|
target: x86_64-pc-windows-msvc
|
||||||
bin: bili-sync-rs.exe
|
bin: bili-sync-rs.exe
|
||||||
name: bili-sync-rs-Windows-x86_64.zip
|
name: bili-sync-rs-Windows-x86_64.zip
|
||||||
|
|
||||||
steps:
|
steps:
|
||||||
- name: Checkout repo
|
- name: Checkout repo
|
||||||
uses: actions/checkout@v4
|
uses: actions/checkout@v4
|
||||||
|
with:
|
||||||
|
fetch-depth: 0
|
||||||
|
- name: Download Web Build Artifact
|
||||||
|
uses: actions/download-artifact@v4
|
||||||
|
with:
|
||||||
|
name: web-build
|
||||||
|
path: web/build
|
||||||
- name: Cache dependencies
|
- name: Cache dependencies
|
||||||
uses: Swatinem/rust-cache@v2
|
uses: Swatinem/rust-cache@v2
|
||||||
- name: Install musl-tools
|
- name: Install musl-tools
|
||||||
@@ -57,7 +91,6 @@ jobs:
|
|||||||
- name: Package as archive
|
- name: Package as archive
|
||||||
shell: bash
|
shell: bash
|
||||||
run: |
|
run: |
|
||||||
cp target/${{ matrix.platform.target }}/release/${{ matrix.platform.bin }} ${{ matrix.platform.release_for }}-${{ matrix.platform.bin }}
|
|
||||||
cd target/${{ matrix.platform.target }}/release
|
cd target/${{ matrix.platform.target }}/release
|
||||||
if [[ "${{ matrix.platform.target }}" == "x86_64-pc-windows-msvc" ]]; then
|
if [[ "${{ matrix.platform.target }}" == "x86_64-pc-windows-msvc" ]]; then
|
||||||
7z a ../../../${{ matrix.platform.name }} ${{ matrix.platform.bin }}
|
7z a ../../../${{ matrix.platform.name }} ${{ matrix.platform.bin }}
|
||||||
@@ -68,62 +101,5 @@ jobs:
|
|||||||
uses: actions/upload-artifact@v4
|
uses: actions/upload-artifact@v4
|
||||||
with:
|
with:
|
||||||
name: bili-sync-rs-${{ matrix.platform.release_for }}
|
name: bili-sync-rs-${{ matrix.platform.release_for }}
|
||||||
# contains raw binary and compressed archive
|
|
||||||
path: |
|
path: |
|
||||||
${{ github.workspace }}/${{ matrix.platform.release_for }}-${{ matrix.platform.bin }}
|
|
||||||
${{ github.workspace }}/${{ matrix.platform.name }}
|
${{ github.workspace }}/${{ matrix.platform.name }}
|
||||||
release:
|
|
||||||
name: Create GitHub Release & Docker Image
|
|
||||||
needs: build
|
|
||||||
runs-on: ubuntu-20.04
|
|
||||||
permissions:
|
|
||||||
contents: write
|
|
||||||
steps:
|
|
||||||
- name: Checkout repo
|
|
||||||
uses: actions/checkout@v4
|
|
||||||
- name: Download release artifact
|
|
||||||
uses: actions/download-artifact@v4
|
|
||||||
with:
|
|
||||||
merge-multiple: true
|
|
||||||
- name: Publish GitHub release
|
|
||||||
uses: softprops/action-gh-release@v2
|
|
||||||
with:
|
|
||||||
files: bili-sync-rs*
|
|
||||||
tag_name: ${{ github.ref_name }}
|
|
||||||
draft: true
|
|
||||||
- name: Docker Meta
|
|
||||||
id: meta
|
|
||||||
uses: docker/metadata-action@v5
|
|
||||||
with:
|
|
||||||
images: ${{ secrets.DOCKERHUB_USERNAME }}/bili-sync-rs
|
|
||||||
tags: |
|
|
||||||
type=raw,value=latest
|
|
||||||
type=raw,value=${{ github.ref_name }}
|
|
||||||
- name: Set up QEMU
|
|
||||||
uses: docker/setup-qemu-action@v3
|
|
||||||
- name: Set up Docker Buildx
|
|
||||||
uses: docker/setup-buildx-action@v3
|
|
||||||
- name: Login to DockerHub
|
|
||||||
uses: docker/login-action@v3
|
|
||||||
with:
|
|
||||||
username: ${{ secrets.DOCKERHUB_USERNAME }}
|
|
||||||
password: ${{ secrets.DOCKERHUB_TOKEN }}
|
|
||||||
- name: Build and push Docker image
|
|
||||||
uses: docker/build-push-action@v5
|
|
||||||
with:
|
|
||||||
context: .
|
|
||||||
file: Dockerfile
|
|
||||||
platforms: |
|
|
||||||
linux/amd64
|
|
||||||
linux/arm64
|
|
||||||
push: true
|
|
||||||
tags: ${{ steps.meta.outputs.tags }}
|
|
||||||
labels: ${{ steps.meta.outputs.labels }}
|
|
||||||
cache-from: type=gha, scope=${{ github.workflow }}
|
|
||||||
cache-to: type=gha, scope=${{ github.workflow }}
|
|
||||||
- name: Update DockerHub description
|
|
||||||
uses: peter-evans/dockerhub-description@v3
|
|
||||||
with:
|
|
||||||
username: ${{ secrets.DOCKERHUB_USERNAME }}
|
|
||||||
password: ${{ secrets.DOCKERHUB_TOKEN }}
|
|
||||||
repository: ${{ secrets.DOCKERHUB_USERNAME }}/bili-sync-rs
|
|
||||||
@@ -1,15 +1,16 @@
|
|||||||
name: Build Docs
|
name: Build Main Docs
|
||||||
|
|
||||||
on:
|
on:
|
||||||
push:
|
push:
|
||||||
|
branches:
|
||||||
|
- main
|
||||||
paths:
|
paths:
|
||||||
- 'docs/**'
|
- 'docs/**'
|
||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
doc:
|
doc:
|
||||||
if: ${{ github.ref == 'refs/heads/main' }}
|
|
||||||
name: Build documentation
|
name: Build documentation
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-24.04
|
||||||
defaults:
|
defaults:
|
||||||
run:
|
run:
|
||||||
working-directory: docs
|
working-directory: docs
|
||||||
10
.github/workflows/commit-build.yaml
vendored
Normal file
10
.github/workflows/commit-build.yaml
vendored
Normal file
@@ -0,0 +1,10 @@
|
|||||||
|
name: Build Main Binary
|
||||||
|
|
||||||
|
on:
|
||||||
|
push:
|
||||||
|
branches:
|
||||||
|
- main
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
build-binary:
|
||||||
|
uses: amtoaer/bili-sync/.github/workflows/build-binary.yaml@main
|
||||||
@@ -20,7 +20,7 @@ env:
|
|||||||
jobs:
|
jobs:
|
||||||
tests:
|
tests:
|
||||||
name: Run Clippy and tests
|
name: Run Clippy and tests
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-24.04
|
||||||
if: ${{ github.event_name == 'push' || !github.event.pull_request.draft }}
|
if: ${{ github.event_name == 'push' || !github.event.pull_request.draft }}
|
||||||
steps:
|
steps:
|
||||||
- name: Checkout repo
|
- name: Checkout repo
|
||||||
78
.github/workflows/release-build.yaml
vendored
Normal file
78
.github/workflows/release-build.yaml
vendored
Normal file
@@ -0,0 +1,78 @@
|
|||||||
|
name: Build Main Binary And Release
|
||||||
|
|
||||||
|
on:
|
||||||
|
push:
|
||||||
|
tags:
|
||||||
|
- v*
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
build-binary:
|
||||||
|
uses: amtoaer/bili-sync/.github/workflows/build-binary.yaml@main
|
||||||
|
github-release:
|
||||||
|
name: Create GitHub Release
|
||||||
|
needs: build-binary
|
||||||
|
runs-on: ubuntu-24.04
|
||||||
|
permissions:
|
||||||
|
contents: write
|
||||||
|
steps:
|
||||||
|
- name: Checkout repo
|
||||||
|
uses: actions/checkout@v4
|
||||||
|
- name: Download release artifact
|
||||||
|
uses: actions/download-artifact@v4
|
||||||
|
with:
|
||||||
|
merge-multiple: true
|
||||||
|
- name: Publish GitHub release
|
||||||
|
uses: softprops/action-gh-release@v2
|
||||||
|
with:
|
||||||
|
files: bili-sync-rs*
|
||||||
|
tag_name: ${{ github.ref_name }}
|
||||||
|
draft: true
|
||||||
|
docker-release:
|
||||||
|
name: Create Docker Image
|
||||||
|
needs: build-binary
|
||||||
|
runs-on: ubuntu-24.04
|
||||||
|
permissions:
|
||||||
|
contents: write
|
||||||
|
steps:
|
||||||
|
- name: Checkout repo
|
||||||
|
uses: actions/checkout@v4
|
||||||
|
- name: Download release artifact
|
||||||
|
uses: actions/download-artifact@v4
|
||||||
|
with:
|
||||||
|
merge-multiple: true
|
||||||
|
- name: Docker Meta
|
||||||
|
id: meta
|
||||||
|
uses: docker/metadata-action@v5
|
||||||
|
with:
|
||||||
|
images: ${{ secrets.DOCKERHUB_USERNAME }}/bili-sync-rs
|
||||||
|
tags: |
|
||||||
|
type=raw,value=latest
|
||||||
|
type=raw,value=${{ github.ref_name }}
|
||||||
|
- name: Set up QEMU
|
||||||
|
uses: docker/setup-qemu-action@v3
|
||||||
|
- name: Set up Docker Buildx
|
||||||
|
uses: docker/setup-buildx-action@v3
|
||||||
|
- name: Login to DockerHub
|
||||||
|
uses: docker/login-action@v3
|
||||||
|
with:
|
||||||
|
username: ${{ secrets.DOCKERHUB_USERNAME }}
|
||||||
|
password: ${{ secrets.DOCKERHUB_TOKEN }}
|
||||||
|
- name: Build and push Docker image
|
||||||
|
uses: docker/build-push-action@v5
|
||||||
|
with:
|
||||||
|
context: .
|
||||||
|
file: Dockerfile
|
||||||
|
platforms: |
|
||||||
|
linux/amd64
|
||||||
|
linux/arm64
|
||||||
|
push: true
|
||||||
|
tags: ${{ steps.meta.outputs.tags }}
|
||||||
|
labels: ${{ steps.meta.outputs.labels }}
|
||||||
|
cache-from: type=gha, scope=${{ github.workflow }}
|
||||||
|
cache-to: type=gha, scope=${{ github.workflow }}
|
||||||
|
- name: Update DockerHub description
|
||||||
|
uses: peter-evans/dockerhub-description@v3
|
||||||
|
with:
|
||||||
|
username: ${{ secrets.DOCKERHUB_USERNAME }}
|
||||||
|
password: ${{ secrets.DOCKERHUB_TOKEN }}
|
||||||
|
repository: ${{ secrets.DOCKERHUB_USERNAME }}/bili-sync-rs
|
||||||
478
Cargo.lock
generated
478
Cargo.lock
generated
@@ -114,13 +114,22 @@ dependencies = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "anyhow"
|
name = "anyhow"
|
||||||
version = "1.0.95"
|
version = "1.0.96"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "34ac096ce696dc2fcabef30516bb13c0a68a11d30131d3df6f04711467681b04"
|
checksum = "6b964d184e89d9b6b67dd2715bc8e74cf3107fb2b529990c90cf517326150bf4"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"backtrace",
|
"backtrace",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "arbitrary"
|
||||||
|
version = "1.4.1"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "dde20b3d026af13f561bdd0f15edf01fc734f0dafcedbaf42bba506a9517f223"
|
||||||
|
dependencies = [
|
||||||
|
"derive_arbitrary",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "arc-swap"
|
name = "arc-swap"
|
||||||
version = "1.7.1"
|
version = "1.7.1"
|
||||||
@@ -336,6 +345,72 @@ version = "1.2.0"
|
|||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "f1fdabc7756949593fe60f30ec81974b613357de856987752631dea1e3394c80"
|
checksum = "f1fdabc7756949593fe60f30ec81974b613357de856987752631dea1e3394c80"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "axum"
|
||||||
|
version = "0.8.1"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "6d6fd624c75e18b3b4c6b9caf42b1afe24437daaee904069137d8bab077be8b8"
|
||||||
|
dependencies = [
|
||||||
|
"axum-core",
|
||||||
|
"axum-macros",
|
||||||
|
"bytes",
|
||||||
|
"form_urlencoded",
|
||||||
|
"futures-util",
|
||||||
|
"http",
|
||||||
|
"http-body",
|
||||||
|
"http-body-util",
|
||||||
|
"hyper",
|
||||||
|
"hyper-util",
|
||||||
|
"itoa",
|
||||||
|
"matchit",
|
||||||
|
"memchr",
|
||||||
|
"mime",
|
||||||
|
"percent-encoding",
|
||||||
|
"pin-project-lite",
|
||||||
|
"rustversion",
|
||||||
|
"serde",
|
||||||
|
"serde_json",
|
||||||
|
"serde_path_to_error",
|
||||||
|
"serde_urlencoded",
|
||||||
|
"sync_wrapper",
|
||||||
|
"tokio",
|
||||||
|
"tower",
|
||||||
|
"tower-layer",
|
||||||
|
"tower-service",
|
||||||
|
"tracing",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "axum-core"
|
||||||
|
version = "0.5.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "df1362f362fd16024ae199c1970ce98f9661bf5ef94b9808fee734bc3698b733"
|
||||||
|
dependencies = [
|
||||||
|
"bytes",
|
||||||
|
"futures-util",
|
||||||
|
"http",
|
||||||
|
"http-body",
|
||||||
|
"http-body-util",
|
||||||
|
"mime",
|
||||||
|
"pin-project-lite",
|
||||||
|
"rustversion",
|
||||||
|
"sync_wrapper",
|
||||||
|
"tower-layer",
|
||||||
|
"tower-service",
|
||||||
|
"tracing",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "axum-macros"
|
||||||
|
version = "0.5.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "604fde5e028fea851ce1d8570bbdc034bec850d157f7569d10f347d06808c05c"
|
||||||
|
dependencies = [
|
||||||
|
"proc-macro2",
|
||||||
|
"quote",
|
||||||
|
"syn 2.0.96",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "backtrace"
|
name = "backtrace"
|
||||||
version = "0.3.71"
|
version = "0.3.71"
|
||||||
@@ -379,14 +454,16 @@ dependencies = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "bili_sync"
|
name = "bili_sync"
|
||||||
version = "2.3.0"
|
version = "2.4.1"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"anyhow",
|
"anyhow",
|
||||||
"arc-swap",
|
"arc-swap",
|
||||||
"assert_matches",
|
"assert_matches",
|
||||||
"async-stream",
|
"async-stream",
|
||||||
|
"axum",
|
||||||
"bili_sync_entity",
|
"bili_sync_entity",
|
||||||
"bili_sync_migration",
|
"bili_sync_migration",
|
||||||
|
"built",
|
||||||
"chrono",
|
"chrono",
|
||||||
"clap",
|
"clap",
|
||||||
"cookie",
|
"cookie",
|
||||||
@@ -400,6 +477,7 @@ dependencies = [
|
|||||||
"leaky-bucket",
|
"leaky-bucket",
|
||||||
"md5",
|
"md5",
|
||||||
"memchr",
|
"memchr",
|
||||||
|
"mime_guess",
|
||||||
"once_cell",
|
"once_cell",
|
||||||
"prost",
|
"prost",
|
||||||
"quick-xml",
|
"quick-xml",
|
||||||
@@ -407,21 +485,26 @@ dependencies = [
|
|||||||
"regex",
|
"regex",
|
||||||
"reqwest",
|
"reqwest",
|
||||||
"rsa",
|
"rsa",
|
||||||
|
"rust-embed",
|
||||||
"sea-orm",
|
"sea-orm",
|
||||||
"serde",
|
"serde",
|
||||||
"serde_json",
|
"serde_json",
|
||||||
"serde_urlencoded",
|
"serde_urlencoded",
|
||||||
"strum",
|
"strum 0.27.1",
|
||||||
"thiserror 2.0.11",
|
"thiserror 2.0.11",
|
||||||
"tokio",
|
"tokio",
|
||||||
|
"tokio-util",
|
||||||
"toml",
|
"toml",
|
||||||
|
"tower",
|
||||||
"tracing",
|
"tracing",
|
||||||
"tracing-subscriber",
|
"tracing-subscriber",
|
||||||
|
"utoipa",
|
||||||
|
"utoipa-swagger-ui",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "bili_sync_entity"
|
name = "bili_sync_entity"
|
||||||
version = "2.3.0"
|
version = "2.4.1"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"sea-orm",
|
"sea-orm",
|
||||||
"serde_json",
|
"serde_json",
|
||||||
@@ -429,7 +512,7 @@ dependencies = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "bili_sync_migration"
|
name = "bili_sync_migration"
|
||||||
version = "2.3.0"
|
version = "2.4.1"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"async-std",
|
"async-std",
|
||||||
"sea-orm-migration",
|
"sea-orm-migration",
|
||||||
@@ -512,10 +595,20 @@ dependencies = [
|
|||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "bumpalo"
|
name = "built"
|
||||||
version = "3.15.4"
|
version = "0.7.7"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "7ff69b9dd49fd426c69a0db9fc04dd934cdb6645ff000864d98f7e2af8830eaa"
|
checksum = "56ed6191a7e78c36abdb16ab65341eefd73d64d303fffccdbb00d51e4205967b"
|
||||||
|
dependencies = [
|
||||||
|
"chrono",
|
||||||
|
"git2",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "bumpalo"
|
||||||
|
version = "3.17.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "1628fb46dfa0b37568d12e5edd512553eccf6a22a78e8bde00bb4aed84d5bdbf"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "bytecheck"
|
name = "bytecheck"
|
||||||
@@ -557,6 +650,8 @@ version = "1.2.9"
|
|||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "c8293772165d9345bdaaa39b45b2109591e63fe5e6fbc23c6ff930a048aa310b"
|
checksum = "c8293772165d9345bdaaa39b45b2109591e63fe5e6fbc23c6ff930a048aa310b"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
|
"jobserver",
|
||||||
|
"libc",
|
||||||
"shlex",
|
"shlex",
|
||||||
]
|
]
|
||||||
|
|
||||||
@@ -589,9 +684,9 @@ dependencies = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "clap"
|
name = "clap"
|
||||||
version = "4.5.26"
|
version = "4.5.30"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "a8eb5e908ef3a6efbe1ed62520fb7287959888c88485abe072543190ecc66783"
|
checksum = "92b7b18d71fad5313a1e320fa9897994228ce274b60faa4d694fe0ea89cd9e6d"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"clap_builder",
|
"clap_builder",
|
||||||
"clap_derive",
|
"clap_derive",
|
||||||
@@ -599,9 +694,9 @@ dependencies = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "clap_builder"
|
name = "clap_builder"
|
||||||
version = "4.5.26"
|
version = "4.5.30"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "96b01801b5fc6a0a232407abc821660c9c6d25a1cafc0d4f85f29fb8d9afc121"
|
checksum = "a35db2071778a7344791a4fb4f95308b5673d219dee3ae348b86642574ecc90c"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"anstream",
|
"anstream",
|
||||||
"anstyle",
|
"anstyle",
|
||||||
@@ -611,9 +706,9 @@ dependencies = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "clap_derive"
|
name = "clap_derive"
|
||||||
version = "4.5.24"
|
version = "4.5.28"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "54b755194d6389280185988721fffba69495eed5ee9feeee9a599b53db80318c"
|
checksum = "bf4ced95c6f4a675af3da73304b9ac4ed991640c36374e4b46795c49e17cf1ed"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"heck 0.5.0",
|
"heck 0.5.0",
|
||||||
"proc-macro2",
|
"proc-macro2",
|
||||||
@@ -802,6 +897,17 @@ dependencies = [
|
|||||||
"serde",
|
"serde",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "derive_arbitrary"
|
||||||
|
version = "1.4.1"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "30542c1ad912e0e3d22a1935c290e12e8a29d704a420177a31faad4a601a0800"
|
||||||
|
dependencies = [
|
||||||
|
"proc-macro2",
|
||||||
|
"quote",
|
||||||
|
"syn 2.0.96",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "derive_builder"
|
name = "derive_builder"
|
||||||
version = "0.20.2"
|
version = "0.20.2"
|
||||||
@@ -866,6 +972,17 @@ dependencies = [
|
|||||||
"windows-sys 0.59.0",
|
"windows-sys 0.59.0",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "displaydoc"
|
||||||
|
version = "0.2.5"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "97369cbbc041bc366949bc74d34658d6cda5621039731c6310521892a3a20ae0"
|
||||||
|
dependencies = [
|
||||||
|
"proc-macro2",
|
||||||
|
"quote",
|
||||||
|
"syn 2.0.96",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "dotenvy"
|
name = "dotenvy"
|
||||||
version = "0.15.7"
|
version = "0.15.7"
|
||||||
@@ -1183,6 +1300,19 @@ 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 = "4271d37baee1b8c7e4b708028c57d816cf9d2434acb33a549475f78c181f6253"
|
checksum = "4271d37baee1b8c7e4b708028c57d816cf9d2434acb33a549475f78c181f6253"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "git2"
|
||||||
|
version = "0.20.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "3fda788993cc341f69012feba8bf45c0ba4f3291fcc08e214b4d5a7332d88aff"
|
||||||
|
dependencies = [
|
||||||
|
"bitflags 2.5.0",
|
||||||
|
"libc",
|
||||||
|
"libgit2-sys",
|
||||||
|
"log",
|
||||||
|
"url",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "glob"
|
name = "glob"
|
||||||
version = "0.3.1"
|
version = "0.3.1"
|
||||||
@@ -1222,9 +1352,9 @@ dependencies = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "handlebars"
|
name = "handlebars"
|
||||||
version = "6.3.0"
|
version = "6.3.1"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "3d6b224b95c1e668ac0270325ad563b2eef1469fbbb8959bc7c692c844b813d9"
|
checksum = "d752747ddabc4c1a70dd28e72f2e3c218a816773e0d7faf67433f1acfa6cba7c"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"derive_builder",
|
"derive_builder",
|
||||||
"log",
|
"log",
|
||||||
@@ -1245,6 +1375,12 @@ dependencies = [
|
|||||||
"ahash",
|
"ahash",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "hashbrown"
|
||||||
|
version = "0.14.5"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "e5274423e17b7c9fc20b6e7e208532f9b19825d82dfd615708b70edd83df41f1"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "hashbrown"
|
name = "hashbrown"
|
||||||
version = "0.15.2"
|
version = "0.15.2"
|
||||||
@@ -1356,6 +1492,12 @@ version = "1.8.0"
|
|||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "d897f394bad6a705d5f4104762e116a75639e470d80901eed05a860a95cb1904"
|
checksum = "d897f394bad6a705d5f4104762e116a75639e470d80901eed05a860a95cb1904"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "httpdate"
|
||||||
|
version = "1.0.3"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "df3b46402a9d5adb4c86a0cf463f42e19994e3ee891101b1841f30a545cb49a9"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "hyper"
|
name = "hyper"
|
||||||
version = "1.5.2"
|
version = "1.5.2"
|
||||||
@@ -1369,6 +1511,7 @@ dependencies = [
|
|||||||
"http",
|
"http",
|
||||||
"http-body",
|
"http-body",
|
||||||
"httparse",
|
"httparse",
|
||||||
|
"httpdate",
|
||||||
"itoa",
|
"itoa",
|
||||||
"pin-project-lite",
|
"pin-project-lite",
|
||||||
"smallvec",
|
"smallvec",
|
||||||
@@ -1470,6 +1613,7 @@ checksum = "62f822373a4fe84d4bb149bf54e584a7f4abec90e072ed49cda0edea5b95471f"
|
|||||||
dependencies = [
|
dependencies = [
|
||||||
"equivalent",
|
"equivalent",
|
||||||
"hashbrown 0.15.2",
|
"hashbrown 0.15.2",
|
||||||
|
"serde",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
@@ -1504,6 +1648,15 @@ version = "1.0.11"
|
|||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "49f1f14873335454500d59611f1cf4a4b0f786f9ac11f4312a78e4cf2566695b"
|
checksum = "49f1f14873335454500d59611f1cf4a4b0f786f9ac11f4312a78e4cf2566695b"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "jobserver"
|
||||||
|
version = "0.1.32"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "48d1dbcbbeb6a7fec7e059840aa538bd62aaccf972c7346c4d9d2059312853d0"
|
||||||
|
dependencies = [
|
||||||
|
"libc",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "js-sys"
|
name = "js-sys"
|
||||||
version = "0.3.69"
|
version = "0.3.69"
|
||||||
@@ -1548,6 +1701,18 @@ version = "0.2.169"
|
|||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "b5aba8db14291edd000dfcc4d620c7ebfb122c613afb886ca8803fa4e128a20a"
|
checksum = "b5aba8db14291edd000dfcc4d620c7ebfb122c613afb886ca8803fa4e128a20a"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "libgit2-sys"
|
||||||
|
version = "0.18.0+1.9.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "e1a117465e7e1597e8febea8bb0c410f1c7fb93b1e1cddf34363f8390367ffec"
|
||||||
|
dependencies = [
|
||||||
|
"cc",
|
||||||
|
"libc",
|
||||||
|
"libz-sys",
|
||||||
|
"pkg-config",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "libm"
|
name = "libm"
|
||||||
version = "0.2.8"
|
version = "0.2.8"
|
||||||
@@ -1575,6 +1740,18 @@ dependencies = [
|
|||||||
"vcpkg",
|
"vcpkg",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "libz-sys"
|
||||||
|
version = "1.1.21"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "df9b68e50e6e0b26f672573834882eb57759f6db9b3be2ea3c35c91188bb4eaa"
|
||||||
|
dependencies = [
|
||||||
|
"cc",
|
||||||
|
"libc",
|
||||||
|
"pkg-config",
|
||||||
|
"vcpkg",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "linux-raw-sys"
|
name = "linux-raw-sys"
|
||||||
version = "0.4.13"
|
version = "0.4.13"
|
||||||
@@ -1591,6 +1768,12 @@ dependencies = [
|
|||||||
"scopeguard",
|
"scopeguard",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "lockfree-object-pool"
|
||||||
|
version = "0.1.6"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "9374ef4228402d4b7e403e5838cb880d9ee663314b0a900d5a6aabf0c213552e"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "log"
|
name = "log"
|
||||||
version = "0.4.22"
|
version = "0.4.22"
|
||||||
@@ -1609,6 +1792,12 @@ dependencies = [
|
|||||||
"regex-automata 0.1.10",
|
"regex-automata 0.1.10",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "matchit"
|
||||||
|
version = "0.8.4"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "47e1ffaa40ddd1f3ed91f717a33c8c0ee23fff369e3aa8772b9605cc1d22f4c3"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "md-5"
|
name = "md-5"
|
||||||
version = "0.10.6"
|
version = "0.10.6"
|
||||||
@@ -1637,6 +1826,16 @@ version = "0.3.17"
|
|||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "6877bb514081ee2a7ff5ef9de3281f14a4dd4bceac4c09388074a6b5df8a139a"
|
checksum = "6877bb514081ee2a7ff5ef9de3281f14a4dd4bceac4c09388074a6b5df8a139a"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "mime_guess"
|
||||||
|
version = "2.0.5"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "f7c44f8e672c00fe5308fa235f821cb4198414e1c77935c1ab6948d3fd78550e"
|
||||||
|
dependencies = [
|
||||||
|
"mime",
|
||||||
|
"unicase",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "miniz_oxide"
|
name = "miniz_oxide"
|
||||||
version = "0.7.2"
|
version = "0.7.2"
|
||||||
@@ -1758,9 +1957,9 @@ dependencies = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "once_cell"
|
name = "once_cell"
|
||||||
version = "1.20.2"
|
version = "1.20.3"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "1261fe7e33c73b354eab43b1273a57c8f967d0391e80353e51f764ac02cf6775"
|
checksum = "945462a4b81e43c4e3ba96bd7b49d834c6f61198356aa858733bc4acf3cbe62e"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "option-ext"
|
name = "option-ext"
|
||||||
@@ -2030,9 +2229,9 @@ dependencies = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "prost"
|
name = "prost"
|
||||||
version = "0.13.4"
|
version = "0.13.5"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "2c0fef6c4230e4ccf618a35c59d7ede15dea37de8427500f50aff708806e42ec"
|
checksum = "2796faa41db3ec313a31f7624d9286acf277b52de526150b7e69f3debf891ee5"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"bytes",
|
"bytes",
|
||||||
"prost-derive",
|
"prost-derive",
|
||||||
@@ -2040,9 +2239,9 @@ dependencies = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "prost-derive"
|
name = "prost-derive"
|
||||||
version = "0.13.4"
|
version = "0.13.5"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "157c5a9d7ea5c2ed2d9fb8f495b64759f7816c7eaea54ba3978f0d63000162e3"
|
checksum = "8a56d757972c98b346a9b766e3f02746cde6dd1cd1d1d563472929fdd74bec4d"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"anyhow",
|
"anyhow",
|
||||||
"itertools",
|
"itertools",
|
||||||
@@ -2377,6 +2576,40 @@ dependencies = [
|
|||||||
"zeroize",
|
"zeroize",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "rust-embed"
|
||||||
|
version = "8.5.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "fa66af4a4fdd5e7ebc276f115e895611a34739a9c1c01028383d612d550953c0"
|
||||||
|
dependencies = [
|
||||||
|
"rust-embed-impl",
|
||||||
|
"rust-embed-utils",
|
||||||
|
"walkdir",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "rust-embed-impl"
|
||||||
|
version = "8.5.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "6125dbc8867951125eec87294137f4e9c2c96566e61bf72c45095a7c77761478"
|
||||||
|
dependencies = [
|
||||||
|
"proc-macro2",
|
||||||
|
"quote",
|
||||||
|
"rust-embed-utils",
|
||||||
|
"syn 2.0.96",
|
||||||
|
"walkdir",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "rust-embed-utils"
|
||||||
|
version = "8.5.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "2e5347777e9aacb56039b0e1f28785929a8a3b709e87482e7442c72e7c12529d"
|
||||||
|
dependencies = [
|
||||||
|
"sha2",
|
||||||
|
"walkdir",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "rust_decimal"
|
name = "rust_decimal"
|
||||||
version = "1.35.0"
|
version = "1.35.0"
|
||||||
@@ -2471,6 +2704,15 @@ version = "1.0.17"
|
|||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "e86697c916019a8588c99b5fac3cead74ec0b4b819707a682fd4d23fa0ce1ba1"
|
checksum = "e86697c916019a8588c99b5fac3cead74ec0b4b819707a682fd4d23fa0ce1ba1"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "same-file"
|
||||||
|
version = "1.0.6"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "93fc1dc3aaa9bfed95e02e6eadabb4baf7e3078b0bd1b4d7b6b0b68378900502"
|
||||||
|
dependencies = [
|
||||||
|
"winapi-util",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "scopeguard"
|
name = "scopeguard"
|
||||||
version = "1.2.0"
|
version = "1.2.0"
|
||||||
@@ -2492,9 +2734,9 @@ dependencies = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "sea-orm"
|
name = "sea-orm"
|
||||||
version = "1.1.4"
|
version = "1.1.5"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "1a93194430b419da0801f404baf3b986399d6a2a4f43bc79bc96dea83f92ca43"
|
checksum = "00733e5418e8ae3758cdb988c3654174e716230cc53ee2cb884207cf86a23029"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"async-stream",
|
"async-stream",
|
||||||
"async-trait",
|
"async-trait",
|
||||||
@@ -2510,7 +2752,7 @@ dependencies = [
|
|||||||
"serde",
|
"serde",
|
||||||
"serde_json",
|
"serde_json",
|
||||||
"sqlx",
|
"sqlx",
|
||||||
"strum",
|
"strum 0.26.3",
|
||||||
"thiserror 1.0.63",
|
"thiserror 1.0.63",
|
||||||
"time",
|
"time",
|
||||||
"tracing",
|
"tracing",
|
||||||
@@ -2520,9 +2762,9 @@ dependencies = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "sea-orm-cli"
|
name = "sea-orm-cli"
|
||||||
version = "1.1.4"
|
version = "1.1.5"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "0e6e0e741bfdf434e6f6aadab156ba4d439e78c9449048698d98fa377871224a"
|
checksum = "0646647444d3a0366e30f26ff39f1656cc062b3dbf1f2e3d70cd9dc244b62cf7"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"chrono",
|
"chrono",
|
||||||
"clap",
|
"clap",
|
||||||
@@ -2537,9 +2779,9 @@ dependencies = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "sea-orm-macros"
|
name = "sea-orm-macros"
|
||||||
version = "1.1.4"
|
version = "1.1.5"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "d19e8f22fb474a8a622eb516c46885a080535d8d559386188f525977eaad32b3"
|
checksum = "a98408f82fb4875d41ef469a79944a7da29767c7b3e4028e22188a3dd613b10f"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"heck 0.4.1",
|
"heck 0.4.1",
|
||||||
"proc-macro2",
|
"proc-macro2",
|
||||||
@@ -2551,9 +2793,9 @@ dependencies = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "sea-orm-migration"
|
name = "sea-orm-migration"
|
||||||
version = "1.1.4"
|
version = "1.1.5"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "c0bb76ba314552ce15e3a24778cf9c116fc1225fa406e48b0a36e5a3cdbc1e21"
|
checksum = "b97ed0bea0d92241722718e239d899c051066a5fb259ced9986b9f60e488e076"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"async-trait",
|
"async-trait",
|
||||||
"clap",
|
"clap",
|
||||||
@@ -2644,18 +2886,18 @@ checksum = "1c107b6f4780854c8b126e228ea8869f4d7b71260f962fefb57b996b8959ba6b"
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "serde"
|
name = "serde"
|
||||||
version = "1.0.217"
|
version = "1.0.218"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "02fc4265df13d6fa1d00ecff087228cc0a2b5f3c0e87e258d8b94a156e984c70"
|
checksum = "e8dfc9d19bdbf6d17e22319da49161d5d0108e4188e8b680aef6299eed22df60"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"serde_derive",
|
"serde_derive",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "serde_derive"
|
name = "serde_derive"
|
||||||
version = "1.0.217"
|
version = "1.0.218"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "5a9bf7cf98d04a2b28aead066b7496853d4779c9cc183c440dbac457641e19a0"
|
checksum = "f09503e191f4e797cb8aac08e9a4a4695c5edf6a2e70e376d961ddd5c969f82b"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"proc-macro2",
|
"proc-macro2",
|
||||||
"quote",
|
"quote",
|
||||||
@@ -2664,9 +2906,9 @@ dependencies = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "serde_json"
|
name = "serde_json"
|
||||||
version = "1.0.135"
|
version = "1.0.139"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "2b0d7ba2887406110130a978386c4e1befb98c674b4fba677954e4db976630d9"
|
checksum = "44f86c3acccc9c65b153fe1b85a3be07fe5515274ec9f0653b4a0875731c72a6"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"itoa",
|
"itoa",
|
||||||
"memchr",
|
"memchr",
|
||||||
@@ -2675,10 +2917,20 @@ dependencies = [
|
|||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "serde_spanned"
|
name = "serde_path_to_error"
|
||||||
version = "0.6.7"
|
version = "0.1.16"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "eb5b1b31579f3811bf615c144393417496f152e12ac8b7663bf664f4a815306d"
|
checksum = "af99884400da37c88f5e9146b7f1fd0fbcae8f6eec4e9da38b67d05486f814a6"
|
||||||
|
dependencies = [
|
||||||
|
"itoa",
|
||||||
|
"serde",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "serde_spanned"
|
||||||
|
version = "0.6.8"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "87607cb1398ed59d48732e575a4c28a7a8ebf2454b964fe3f224f2afc07909e1"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"serde",
|
"serde",
|
||||||
]
|
]
|
||||||
@@ -2751,6 +3003,12 @@ dependencies = [
|
|||||||
"rand_core",
|
"rand_core",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "simd-adler32"
|
||||||
|
version = "0.3.7"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "d66dc143e6b11c1eddc06d5c423cfc97062865baf299914ab64caa38182078fe"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "simdutf8"
|
name = "simdutf8"
|
||||||
version = "0.1.4"
|
version = "0.1.4"
|
||||||
@@ -3041,15 +3299,21 @@ name = "strum"
|
|||||||
version = "0.26.3"
|
version = "0.26.3"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "8fec0f0aef304996cf250b31b5a10dee7980c85da9d759361292b8bca5a18f06"
|
checksum = "8fec0f0aef304996cf250b31b5a10dee7980c85da9d759361292b8bca5a18f06"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "strum"
|
||||||
|
version = "0.27.1"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "f64def088c51c9510a8579e3c5d67c65349dcf755e5479ad3d010aa6454e2c32"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"strum_macros",
|
"strum_macros",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "strum_macros"
|
name = "strum_macros"
|
||||||
version = "0.26.4"
|
version = "0.27.1"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "4c6bee85a5a24955dc440386795aa378cd9cf82acd5f764469152d2270e581be"
|
checksum = "c77a8c5abcaf0f9ce05d62342b7d298c346515365c36b673df4ebe3ced01fde8"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"heck 0.5.0",
|
"heck 0.5.0",
|
||||||
"proc-macro2",
|
"proc-macro2",
|
||||||
@@ -3274,27 +3538,29 @@ dependencies = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "tokio-util"
|
name = "tokio-util"
|
||||||
version = "0.7.11"
|
version = "0.7.13"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "9cf6b47b3771c49ac75ad09a6162f53ad4b8088b76ac60e8ec1455b31a189fe1"
|
checksum = "d7fcaa8d55a2bdd6b83ace262b016eca0d79ee02818c5c1bcdf0305114081078"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"bytes",
|
"bytes",
|
||||||
"futures-core",
|
"futures-core",
|
||||||
"futures-sink",
|
"futures-sink",
|
||||||
|
"futures-util",
|
||||||
|
"hashbrown 0.14.5",
|
||||||
"pin-project-lite",
|
"pin-project-lite",
|
||||||
"tokio",
|
"tokio",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "toml"
|
name = "toml"
|
||||||
version = "0.8.19"
|
version = "0.8.20"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "a1ed1f98e3fdc28d6d910e6737ae6ab1a93bf1985935a1193e68f93eeb68d24e"
|
checksum = "cd87a5cdd6ffab733b2f74bc4fd7ee5fff6634124999ac278c35fc78c6120148"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"serde",
|
"serde",
|
||||||
"serde_spanned",
|
"serde_spanned",
|
||||||
"toml_datetime",
|
"toml_datetime",
|
||||||
"toml_edit 0.22.22",
|
"toml_edit 0.22.24",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
@@ -3319,15 +3585,15 @@ dependencies = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "toml_edit"
|
name = "toml_edit"
|
||||||
version = "0.22.22"
|
version = "0.22.24"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "4ae48d6208a266e853d946088ed816055e556cc6028c5e8e2b84d9fa5dd7c7f5"
|
checksum = "17b4795ff5edd201c7cd6dca065ae59972ce77d1b80fa0a84d94950ece7d1474"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"indexmap",
|
"indexmap",
|
||||||
"serde",
|
"serde",
|
||||||
"serde_spanned",
|
"serde_spanned",
|
||||||
"toml_datetime",
|
"toml_datetime",
|
||||||
"winnow 0.6.24",
|
"winnow 0.7.3",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
@@ -3343,6 +3609,7 @@ dependencies = [
|
|||||||
"tokio",
|
"tokio",
|
||||||
"tower-layer",
|
"tower-layer",
|
||||||
"tower-service",
|
"tower-service",
|
||||||
|
"tracing",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
@@ -3438,6 +3705,12 @@ version = "0.1.6"
|
|||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "ed646292ffc8188ef8ea4d1e0e0150fb15a5c2e12ad9b8fc191ae7a8a7f3c4b9"
|
checksum = "ed646292ffc8188ef8ea4d1e0e0150fb15a5c2e12ad9b8fc191ae7a8a7f3c4b9"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "unicase"
|
||||||
|
version = "2.8.1"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "75b844d17643ee918803943289730bec8aac480150456169e647ed0b576ba539"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "unicode-bidi"
|
name = "unicode-bidi"
|
||||||
version = "0.3.15"
|
version = "0.3.15"
|
||||||
@@ -3482,6 +3755,55 @@ version = "0.2.1"
|
|||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "711b9620af191e0cdc7468a8d14e709c3dcdb115b36f838e601583af800a370a"
|
checksum = "711b9620af191e0cdc7468a8d14e709c3dcdb115b36f838e601583af800a370a"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "utoipa"
|
||||||
|
version = "5.3.1"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "435c6f69ef38c9017b4b4eea965dfb91e71e53d869e896db40d1cf2441dd75c0"
|
||||||
|
dependencies = [
|
||||||
|
"indexmap",
|
||||||
|
"serde",
|
||||||
|
"serde_json",
|
||||||
|
"utoipa-gen",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "utoipa-gen"
|
||||||
|
version = "5.3.1"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "a77d306bc75294fd52f3e99b13ece67c02c1a2789190a6f31d32f736624326f7"
|
||||||
|
dependencies = [
|
||||||
|
"proc-macro2",
|
||||||
|
"quote",
|
||||||
|
"regex",
|
||||||
|
"syn 2.0.96",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "utoipa-swagger-ui"
|
||||||
|
version = "9.0.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "161166ec520c50144922a625d8bc4925cc801b2dda958ab69878527c0e5c5d61"
|
||||||
|
dependencies = [
|
||||||
|
"axum",
|
||||||
|
"base64",
|
||||||
|
"mime_guess",
|
||||||
|
"regex",
|
||||||
|
"rust-embed",
|
||||||
|
"serde",
|
||||||
|
"serde_json",
|
||||||
|
"url",
|
||||||
|
"utoipa",
|
||||||
|
"utoipa-swagger-ui-vendored",
|
||||||
|
"zip",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "utoipa-swagger-ui-vendored"
|
||||||
|
version = "0.1.2"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "e2eebbbfe4093922c2b6734d7c679ebfebd704a0d7e56dfcb0d05818ce28977d"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "uuid"
|
name = "uuid"
|
||||||
version = "1.8.0"
|
version = "1.8.0"
|
||||||
@@ -3515,6 +3837,16 @@ version = "0.9.4"
|
|||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "49874b5167b65d7193b8aba1567f5c7d93d001cafc34600cee003eda787e483f"
|
checksum = "49874b5167b65d7193b8aba1567f5c7d93d001cafc34600cee003eda787e483f"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "walkdir"
|
||||||
|
version = "2.5.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "29790946404f91d9c5d06f9874efddea1dc06c5efe94541a7d6863108e3a5e4b"
|
||||||
|
dependencies = [
|
||||||
|
"same-file",
|
||||||
|
"winapi-util",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "want"
|
name = "want"
|
||||||
version = "0.3.1"
|
version = "0.3.1"
|
||||||
@@ -3660,6 +3992,15 @@ 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 = "ac3b87c63620426dd9b991e5ce0329eff545bccbbb34f3be09ff6fb6ab51b7b6"
|
checksum = "ac3b87c63620426dd9b991e5ce0329eff545bccbbb34f3be09ff6fb6ab51b7b6"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "winapi-util"
|
||||||
|
version = "0.1.9"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "cf221c93e13a30d793f7645a0e7762c55d169dbb0a49671918a2319d289b10bb"
|
||||||
|
dependencies = [
|
||||||
|
"windows-sys 0.59.0",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "winapi-x86_64-pc-windows-gnu"
|
name = "winapi-x86_64-pc-windows-gnu"
|
||||||
version = "0.4.0"
|
version = "0.4.0"
|
||||||
@@ -3864,9 +4205,9 @@ dependencies = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "winnow"
|
name = "winnow"
|
||||||
version = "0.6.24"
|
version = "0.7.3"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "c8d71a593cc5c42ad7876e2c1fda56f314f3754c084128833e64f1345ff8a03a"
|
checksum = "0e7f4ea97f6f78012141bcdb6a216b2609f0979ada50b20ca5b52dde2eac2bb1"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"memchr",
|
"memchr",
|
||||||
]
|
]
|
||||||
@@ -3891,3 +4232,34 @@ name = "zeroize"
|
|||||||
version = "1.7.0"
|
version = "1.7.0"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "525b4ec142c6b68a2d10f01f7bbf6755599ca3f81ea53b8431b7dd348f5fdb2d"
|
checksum = "525b4ec142c6b68a2d10f01f7bbf6755599ca3f81ea53b8431b7dd348f5fdb2d"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "zip"
|
||||||
|
version = "2.1.1"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "1dd56a4d5921bc2f99947ac5b3abe5f510b1be7376fdc5e9fce4a23c6a93e87c"
|
||||||
|
dependencies = [
|
||||||
|
"arbitrary",
|
||||||
|
"crc32fast",
|
||||||
|
"crossbeam-utils",
|
||||||
|
"displaydoc",
|
||||||
|
"flate2",
|
||||||
|
"indexmap",
|
||||||
|
"memchr",
|
||||||
|
"thiserror 1.0.63",
|
||||||
|
"zopfli",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "zopfli"
|
||||||
|
version = "0.8.1"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "e5019f391bac5cf252e93bbcc53d039ffd62c7bfb7c150414d61369afe57e946"
|
||||||
|
dependencies = [
|
||||||
|
"bumpalo",
|
||||||
|
"crc32fast",
|
||||||
|
"lockfree-object-pool",
|
||||||
|
"log",
|
||||||
|
"once_cell",
|
||||||
|
"simd-adler32",
|
||||||
|
]
|
||||||
|
|||||||
62
Cargo.toml
62
Cargo.toml
@@ -4,66 +4,74 @@ default-members = ["crates/bili_sync"]
|
|||||||
resolver = "2"
|
resolver = "2"
|
||||||
|
|
||||||
[workspace.package]
|
[workspace.package]
|
||||||
version = "2.3.0"
|
version = "2.4.1"
|
||||||
authors = ["amtoaer <amtoaer@gmail.com>"]
|
authors = ["amtoaer <amtoaer@gmail.com>"]
|
||||||
license = "MIT"
|
license = "MIT"
|
||||||
description = "由 Rust & Tokio 驱动的哔哩哔哩同步工具"
|
description = "由 Rust & Tokio 驱动的哔哩哔哩同步工具"
|
||||||
edition = "2021"
|
edition = "2024"
|
||||||
publish = false
|
publish = false
|
||||||
|
|
||||||
[workspace.dependencies]
|
[workspace.dependencies]
|
||||||
bili_sync_entity = { path = "crates/bili_sync_entity" }
|
bili_sync_entity = { path = "crates/bili_sync_entity" }
|
||||||
bili_sync_migration = { path = "crates/bili_sync_migration" }
|
bili_sync_migration = { path = "crates/bili_sync_migration" }
|
||||||
|
|
||||||
anyhow = { version = "1.0.95", features = ["backtrace"] }
|
anyhow = { version = "1.0.96", features = ["backtrace"] }
|
||||||
arc-swap = { version = "1.7.1", features = ["serde"] }
|
arc-swap = { version = "1.7.1", features = ["serde"] }
|
||||||
assert_matches = "1.5"
|
assert_matches = "1.5.0"
|
||||||
async-std = { version = "1.13.0", features = ["attributes", "tokio1"] }
|
async-std = { version = "1.13.0", features = ["attributes", "tokio1"] }
|
||||||
async-stream = "0.3.6"
|
async-stream = "0.3.6"
|
||||||
async-trait = "0.1.85"
|
async-trait = "0.1.86"
|
||||||
|
axum = { version = "0.8.1", features = ["macros"] }
|
||||||
|
built = { version = "0.7.7", features = ["git2", "chrono"] }
|
||||||
chrono = { version = "0.4.39", features = ["serde"] }
|
chrono = { version = "0.4.39", features = ["serde"] }
|
||||||
clap = { version = "4.5.26", features = ["env"] }
|
clap = { version = "4.5.30", features = ["env", "string"] }
|
||||||
cookie = "0.18.1"
|
cookie = "0.18.1"
|
||||||
cow-utils = "0.1.3"
|
cow-utils = "0.1.3"
|
||||||
dirs = "6.0.0"
|
dirs = "6.0.0"
|
||||||
enum_dispatch = "0.3.13"
|
enum_dispatch = "0.3.13"
|
||||||
float-ord = "0.3.2"
|
float-ord = "0.3.2"
|
||||||
futures = "0.3.31"
|
futures = "0.3.31"
|
||||||
handlebars = "6.3.0"
|
handlebars = "6.3.1"
|
||||||
hex = "0.4.3"
|
hex = "0.4.3"
|
||||||
leaky-bucket = "1.1.2"
|
leaky-bucket = "1.1.2"
|
||||||
md5 = "0.7.0"
|
md5 = "0.7.0"
|
||||||
memchr = "2.7.4"
|
memchr = "2.7.4"
|
||||||
once_cell = "1.20.2"
|
mime_guess = "2.0.5"
|
||||||
prost = "0.13.4"
|
once_cell = "1.20.3"
|
||||||
|
prost = "0.13.5"
|
||||||
quick-xml = { version = "0.37.2", features = ["async-tokio"] }
|
quick-xml = { version = "0.37.2", features = ["async-tokio"] }
|
||||||
rand = "0.8.5"
|
rand = "0.8.5"
|
||||||
regex = "1.11.1"
|
regex = "1.11.1"
|
||||||
reqwest = { version = "0.12.12", features = [
|
reqwest = { version = "0.12.12", features = [
|
||||||
"charset",
|
"charset",
|
||||||
"cookies",
|
"cookies",
|
||||||
"gzip",
|
"gzip",
|
||||||
"http2",
|
"http2",
|
||||||
"json",
|
"json",
|
||||||
"rustls-tls",
|
"rustls-tls",
|
||||||
"stream",
|
"stream",
|
||||||
], default-features = false }
|
], default-features = false }
|
||||||
rsa = { version = "0.9.7", features = ["sha2"] }
|
rsa = { version = "0.9.7", features = ["sha2"] }
|
||||||
sea-orm = { version = "1.1.4", features = [
|
rust-embed = "8.5.0"
|
||||||
"macros",
|
sea-orm = { version = "1.1.5", features = [
|
||||||
"runtime-tokio-rustls",
|
"macros",
|
||||||
"sqlx-sqlite",
|
"runtime-tokio-rustls",
|
||||||
|
"sqlx-sqlite",
|
||||||
] }
|
] }
|
||||||
sea-orm-migration = { version = "1.1.4", features = [] }
|
sea-orm-migration = { version = "1.1.5", features = [] }
|
||||||
serde = { version = "1.0.217", features = ["derive"] }
|
serde = { version = "1.0.218", features = ["derive"] }
|
||||||
serde_json = "1.0.135"
|
serde_json = "1.0.139"
|
||||||
serde_urlencoded = "0.7.1"
|
serde_urlencoded = "0.7.1"
|
||||||
strum = { version = "0.26.3", features = ["derive"] }
|
strum = { version = "0.27.1", features = ["derive"] }
|
||||||
thiserror = "2.0.11"
|
thiserror = "2.0.11"
|
||||||
tokio = { version = "1.43.0", features = ["full"] }
|
tokio = { version = "1.43.0", features = ["full"] }
|
||||||
toml = "0.8.19"
|
tokio-util = { version = "0.7.13", features = ["io", "rt"] }
|
||||||
|
toml = "0.8.20"
|
||||||
|
tower = "0.5.2"
|
||||||
tracing = "0.1.41"
|
tracing = "0.1.41"
|
||||||
tracing-subscriber = { version = "0.3.19", features = ["chrono"] }
|
tracing-subscriber = { version = "0.3.19", features = ["chrono"] }
|
||||||
|
utoipa = { version = "5.3.1", features = ["axum_extras"] }
|
||||||
|
utoipa-swagger-ui = { version = "9.0.0", features = ["axum", "vendored"] }
|
||||||
|
|
||||||
[workspace.metadata.release]
|
[workspace.metadata.release]
|
||||||
release = false
|
release = false
|
||||||
@@ -72,8 +80,8 @@ tag-prefix = ""
|
|||||||
pre-release-commit-message = "chore: 发布 bili-sync {{version}}"
|
pre-release-commit-message = "chore: 发布 bili-sync {{version}}"
|
||||||
publish = false
|
publish = false
|
||||||
pre-release-replacements = [
|
pre-release-replacements = [
|
||||||
{ file = "../../docs/.vitepress/config.mts", search = "\"v[0-9\\.]+\"", replace = "\"v{{version}}\"", exactly = 1 },
|
{ file = "../../docs/.vitepress/config.mts", search = "\"v[0-9\\.]+\"", replace = "\"v{{version}}\"", exactly = 1 },
|
||||||
{ file = "../../docs/introduction.md", search = " v[0-9\\.]+,", replace = " v{{version}},", exactly = 1 },
|
{ file = "../../docs/introduction.md", search = " v[0-9\\.]+,", replace = " v{{version}},", exactly = 1 },
|
||||||
]
|
]
|
||||||
|
|
||||||
[profile.release]
|
[profile.release]
|
||||||
|
|||||||
@@ -9,12 +9,12 @@ RUN apk update && apk add --no-cache \
|
|||||||
tzdata \
|
tzdata \
|
||||||
ffmpeg
|
ffmpeg
|
||||||
|
|
||||||
COPY ./*-bili-sync-rs ./targets/
|
COPY ./bili-sync-rs-Linux-*.tar.gz ./targets/
|
||||||
|
|
||||||
RUN if [ "$TARGETPLATFORM" = "linux/amd64" ]; then \
|
RUN if [ "$TARGETPLATFORM" = "linux/amd64" ]; then \
|
||||||
mv ./targets/Linux-x86_64-bili-sync-rs ./bili-sync-rs; \
|
tar xzvf ./targets/bili-sync-rs-Linux-x86_64-musl.tar.gz -C ./; \
|
||||||
else \
|
else \
|
||||||
mv ./targets/Linux-aarch64-bili-sync-rs ./bili-sync-rs; \
|
tar xzvf ./targets/bili-sync-rs-Linux-aarch64-musl.tar.gz -C ./; \
|
||||||
fi
|
fi
|
||||||
|
|
||||||
RUN rm -rf ./targets && chmod +x ./bili-sync-rs
|
RUN rm -rf ./targets && chmod +x ./bili-sync-rs
|
||||||
|
|||||||
25
Justfile
25
Justfile
@@ -1,21 +1,24 @@
|
|||||||
clean:
|
clean:
|
||||||
rm -rf ./*-bili-sync-rs
|
rm -rf ./bili-sync-rs-Linux*.tar.gz
|
||||||
|
|
||||||
build:
|
build-frontend:
|
||||||
|
cd ./web && bun run build && cd ..
|
||||||
|
|
||||||
|
build: build-frontend
|
||||||
cargo build --target x86_64-unknown-linux-musl --release
|
cargo build --target x86_64-unknown-linux-musl --release
|
||||||
|
|
||||||
|
build-debug: build-frontend
|
||||||
|
cargo build --target x86_64-unknown-linux-musl
|
||||||
|
|
||||||
build-docker: build
|
build-docker: build
|
||||||
cp target/x86_64-unknown-linux-musl/release/bili-sync-rs ./Linux-x86_64-bili-sync-rs
|
tar czvf ./bili-sync-rs-Linux-x86_64-musl.tar.gz -C ./target/x86_64-unknown-linux-musl/release/ ./bili-sync-rs
|
||||||
docker build . -t bili-sync-rs-local --build-arg="TARGETPLATFORM=linux/amd64"
|
docker build . -t bili-sync-rs-local --build-arg="TARGETPLATFORM=linux/amd64"
|
||||||
just clean
|
just clean
|
||||||
|
|
||||||
copy-config:
|
build-docker-debug: build-debug
|
||||||
rm -rf ~/.config/bili-sync
|
tar czvf ./bili-sync-rs-Linux-x86_64-musl.tar.gz -C ./target/x86_64-unknown-linux-musl/debug/ ./bili-sync-rs
|
||||||
cp -r ~/.config/nas/bili-sync-rs ~/.config/bili-sync
|
docker build . -t bili-sync-rs-local --build-arg="TARGETPLATFORM=linux/amd64"
|
||||||
sed -i -e 's/\/Bilibilis/\/Test_Bilibilis/g' -e 's/.config\/nas/.config\/test_nas/g' ~/.config/bili-sync/config.toml
|
just clean
|
||||||
|
|
||||||
run:
|
debug: build-frontend
|
||||||
cargo run
|
cargo run
|
||||||
|
|
||||||
debug: copy-config
|
|
||||||
just run
|
|
||||||
@@ -7,11 +7,13 @@ license = { workspace = true }
|
|||||||
description = { workspace = true }
|
description = { workspace = true }
|
||||||
publish = { workspace = true }
|
publish = { workspace = true }
|
||||||
readme = "../../README.md"
|
readme = "../../README.md"
|
||||||
|
build = "build.rs"
|
||||||
|
|
||||||
[dependencies]
|
[dependencies]
|
||||||
anyhow = { workspace = true }
|
anyhow = { workspace = true }
|
||||||
arc-swap = { workspace = true }
|
arc-swap = { workspace = true }
|
||||||
async-stream = { workspace = true }
|
async-stream = { workspace = true }
|
||||||
|
axum = { workspace = true }
|
||||||
bili_sync_entity = { workspace = true }
|
bili_sync_entity = { workspace = true }
|
||||||
bili_sync_migration = { workspace = true }
|
bili_sync_migration = { workspace = true }
|
||||||
chrono = { workspace = true }
|
chrono = { workspace = true }
|
||||||
@@ -27,6 +29,7 @@ hex = { workspace = true }
|
|||||||
leaky-bucket = { workspace = true }
|
leaky-bucket = { workspace = true }
|
||||||
md5 = { workspace = true }
|
md5 = { workspace = true }
|
||||||
memchr = { workspace = true }
|
memchr = { workspace = true }
|
||||||
|
mime_guess = { workspace = true }
|
||||||
once_cell = { workspace = true }
|
once_cell = { workspace = true }
|
||||||
prost = { workspace = true }
|
prost = { workspace = true }
|
||||||
quick-xml = { workspace = true }
|
quick-xml = { workspace = true }
|
||||||
@@ -34,6 +37,7 @@ rand = { workspace = true }
|
|||||||
regex = { workspace = true }
|
regex = { workspace = true }
|
||||||
reqwest = { workspace = true }
|
reqwest = { workspace = true }
|
||||||
rsa = { workspace = true }
|
rsa = { workspace = true }
|
||||||
|
rust-embed = { workspace = true }
|
||||||
sea-orm = { workspace = true }
|
sea-orm = { workspace = true }
|
||||||
serde = { workspace = true }
|
serde = { workspace = true }
|
||||||
serde_json = { workspace = true }
|
serde_json = { workspace = true }
|
||||||
@@ -41,13 +45,20 @@ serde_urlencoded = { workspace = true }
|
|||||||
strum = { workspace = true }
|
strum = { workspace = true }
|
||||||
thiserror = { workspace = true }
|
thiserror = { workspace = true }
|
||||||
tokio = { workspace = true }
|
tokio = { workspace = true }
|
||||||
|
tokio-util = { workspace = true }
|
||||||
toml = { workspace = true }
|
toml = { workspace = true }
|
||||||
|
tower = { workspace = true }
|
||||||
tracing = { workspace = true }
|
tracing = { workspace = true }
|
||||||
tracing-subscriber = { workspace = true }
|
tracing-subscriber = { workspace = true }
|
||||||
|
utoipa = { workspace = true }
|
||||||
|
utoipa-swagger-ui = { workspace = true }
|
||||||
|
|
||||||
[dev-dependencies]
|
[dev-dependencies]
|
||||||
assert_matches = { workspace = true }
|
assert_matches = { workspace = true }
|
||||||
|
|
||||||
|
[build-dependencies]
|
||||||
|
built = { workspace = true }
|
||||||
|
|
||||||
[package.metadata.release]
|
[package.metadata.release]
|
||||||
release = true
|
release = true
|
||||||
|
|
||||||
|
|||||||
3
crates/bili_sync/build.rs
Normal file
3
crates/bili_sync/build.rs
Normal file
@@ -0,0 +1,3 @@
|
|||||||
|
fn main() {
|
||||||
|
built::write_built_file().expect("Failed to acquire build-time information");
|
||||||
|
}
|
||||||
@@ -4,15 +4,15 @@ use std::pin::Pin;
|
|||||||
use anyhow::{Context, Result};
|
use anyhow::{Context, Result};
|
||||||
use bili_sync_entity::*;
|
use bili_sync_entity::*;
|
||||||
use futures::Stream;
|
use futures::Stream;
|
||||||
|
use sea_orm::ActiveValue::Set;
|
||||||
use sea_orm::entity::prelude::*;
|
use sea_orm::entity::prelude::*;
|
||||||
use sea_orm::sea_query::{OnConflict, SimpleExpr};
|
use sea_orm::sea_query::{OnConflict, SimpleExpr};
|
||||||
use sea_orm::ActiveValue::Set;
|
|
||||||
use sea_orm::{DatabaseConnection, Unchanged};
|
use sea_orm::{DatabaseConnection, Unchanged};
|
||||||
|
|
||||||
use crate::adapter::{VideoListModel, VideoListModelEnum, _ActiveModel};
|
use crate::adapter::{_ActiveModel, VideoSource, VideoSourceEnum};
|
||||||
use crate::bilibili::{BiliClient, Collection, CollectionItem, CollectionType, VideoInfo};
|
use crate::bilibili::{BiliClient, Collection, CollectionItem, CollectionType, VideoInfo};
|
||||||
|
|
||||||
impl VideoListModel for collection::Model {
|
impl VideoSource for collection::Model {
|
||||||
fn filter_expr(&self) -> SimpleExpr {
|
fn filter_expr(&self) -> SimpleExpr {
|
||||||
video::Column::CollectionId.eq(self.id)
|
video::Column::CollectionId.eq(self.id)
|
||||||
}
|
}
|
||||||
@@ -76,7 +76,10 @@ pub(super) async fn collection_from<'a>(
|
|||||||
path: &Path,
|
path: &Path,
|
||||||
bili_client: &'a BiliClient,
|
bili_client: &'a BiliClient,
|
||||||
connection: &DatabaseConnection,
|
connection: &DatabaseConnection,
|
||||||
) -> Result<(VideoListModelEnum, Pin<Box<dyn Stream<Item = Result<VideoInfo>> + 'a>>)> {
|
) -> Result<(
|
||||||
|
VideoSourceEnum,
|
||||||
|
Pin<Box<dyn Stream<Item = Result<VideoInfo>> + 'a + Send>>,
|
||||||
|
)> {
|
||||||
let collection = Collection::new(bili_client, collection_item);
|
let collection = Collection::new(bili_client, collection_item);
|
||||||
let collection_info = collection.get_info().await?;
|
let collection_info = collection.get_info().await?;
|
||||||
collection::Entity::insert(collection::ActiveModel {
|
collection::Entity::insert(collection::ActiveModel {
|
||||||
|
|||||||
@@ -4,15 +4,15 @@ use std::pin::Pin;
|
|||||||
use anyhow::{Context, Result};
|
use anyhow::{Context, Result};
|
||||||
use bili_sync_entity::*;
|
use bili_sync_entity::*;
|
||||||
use futures::Stream;
|
use futures::Stream;
|
||||||
|
use sea_orm::ActiveValue::Set;
|
||||||
use sea_orm::entity::prelude::*;
|
use sea_orm::entity::prelude::*;
|
||||||
use sea_orm::sea_query::{OnConflict, SimpleExpr};
|
use sea_orm::sea_query::{OnConflict, SimpleExpr};
|
||||||
use sea_orm::ActiveValue::Set;
|
|
||||||
use sea_orm::{DatabaseConnection, Unchanged};
|
use sea_orm::{DatabaseConnection, Unchanged};
|
||||||
|
|
||||||
use crate::adapter::{VideoListModel, VideoListModelEnum, _ActiveModel};
|
use crate::adapter::{_ActiveModel, VideoSource, VideoSourceEnum};
|
||||||
use crate::bilibili::{BiliClient, FavoriteList, VideoInfo};
|
use crate::bilibili::{BiliClient, FavoriteList, VideoInfo};
|
||||||
|
|
||||||
impl VideoListModel for favorite::Model {
|
impl VideoSource for favorite::Model {
|
||||||
fn filter_expr(&self) -> SimpleExpr {
|
fn filter_expr(&self) -> SimpleExpr {
|
||||||
video::Column::FavoriteId.eq(self.id)
|
video::Column::FavoriteId.eq(self.id)
|
||||||
}
|
}
|
||||||
@@ -67,7 +67,10 @@ pub(super) async fn favorite_from<'a>(
|
|||||||
path: &Path,
|
path: &Path,
|
||||||
bili_client: &'a BiliClient,
|
bili_client: &'a BiliClient,
|
||||||
connection: &DatabaseConnection,
|
connection: &DatabaseConnection,
|
||||||
) -> Result<(VideoListModelEnum, Pin<Box<dyn Stream<Item = Result<VideoInfo>> + 'a>>)> {
|
) -> Result<(
|
||||||
|
VideoSourceEnum,
|
||||||
|
Pin<Box<dyn Stream<Item = Result<VideoInfo>> + 'a + Send>>,
|
||||||
|
)> {
|
||||||
let favorite = FavoriteList::new(bili_client, fid.to_owned());
|
let favorite = FavoriteList::new(bili_client, fid.to_owned());
|
||||||
let favorite_info = favorite.get_info().await?;
|
let favorite_info = favorite.get_info().await?;
|
||||||
favorite::Entity::insert(favorite::ActiveModel {
|
favorite::Entity::insert(favorite::ActiveModel {
|
||||||
|
|||||||
@@ -9,9 +9,9 @@ use std::pin::Pin;
|
|||||||
use anyhow::Result;
|
use anyhow::Result;
|
||||||
use enum_dispatch::enum_dispatch;
|
use enum_dispatch::enum_dispatch;
|
||||||
use futures::Stream;
|
use futures::Stream;
|
||||||
|
use sea_orm::DatabaseConnection;
|
||||||
use sea_orm::entity::prelude::*;
|
use sea_orm::entity::prelude::*;
|
||||||
use sea_orm::sea_query::SimpleExpr;
|
use sea_orm::sea_query::SimpleExpr;
|
||||||
use sea_orm::DatabaseConnection;
|
|
||||||
|
|
||||||
#[rustfmt::skip]
|
#[rustfmt::skip]
|
||||||
use bili_sync_entity::collection::Model as Collection;
|
use bili_sync_entity::collection::Model as Collection;
|
||||||
@@ -26,15 +26,15 @@ use crate::adapter::watch_later::watch_later_from;
|
|||||||
use crate::bilibili::{BiliClient, CollectionItem, VideoInfo};
|
use crate::bilibili::{BiliClient, CollectionItem, VideoInfo};
|
||||||
|
|
||||||
#[enum_dispatch]
|
#[enum_dispatch]
|
||||||
pub enum VideoListModelEnum {
|
pub enum VideoSourceEnum {
|
||||||
Favorite,
|
Favorite,
|
||||||
Collection,
|
Collection,
|
||||||
Submission,
|
Submission,
|
||||||
WatchLater,
|
WatchLater,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[enum_dispatch(VideoListModelEnum)]
|
#[enum_dispatch(VideoSourceEnum)]
|
||||||
pub trait VideoListModel {
|
pub trait VideoSource {
|
||||||
/// 获取特定视频列表的筛选条件
|
/// 获取特定视频列表的筛选条件
|
||||||
fn filter_expr(&self) -> SimpleExpr;
|
fn filter_expr(&self) -> SimpleExpr;
|
||||||
|
|
||||||
@@ -48,7 +48,7 @@ pub trait VideoListModel {
|
|||||||
fn get_latest_row_at(&self) -> DateTime;
|
fn get_latest_row_at(&self) -> DateTime;
|
||||||
|
|
||||||
/// 更新视频 model 中记录的最新时间,此处返回需要更新的 ActiveModel,接着调用 save 方法执行保存
|
/// 更新视频 model 中记录的最新时间,此处返回需要更新的 ActiveModel,接着调用 save 方法执行保存
|
||||||
/// 不同 VideoListModel 返回的类型不同,为了 VideoListModel 的 object safety 不能使用 impl Trait
|
/// 不同 VideoSource 返回的类型不同,为了 VideoSource 的 object safety 不能使用 impl Trait
|
||||||
/// Box<dyn ActiveModelTrait> 又提示 ActiveModelTrait 没有 object safety,因此手写一个 Enum 静态分发
|
/// Box<dyn ActiveModelTrait> 又提示 ActiveModelTrait 没有 object safety,因此手写一个 Enum 静态分发
|
||||||
fn update_latest_row_at(&self, datetime: DateTime) -> _ActiveModel;
|
fn update_latest_row_at(&self, datetime: DateTime) -> _ActiveModel;
|
||||||
|
|
||||||
@@ -71,7 +71,7 @@ pub trait VideoListModel {
|
|||||||
fn log_download_video_end(&self);
|
fn log_download_video_end(&self);
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Clone, Copy)]
|
#[derive(Clone, Copy, Debug)]
|
||||||
pub enum Args<'a> {
|
pub enum Args<'a> {
|
||||||
Favorite { fid: &'a str },
|
Favorite { fid: &'a str },
|
||||||
Collection { collection_item: &'a CollectionItem },
|
Collection { collection_item: &'a CollectionItem },
|
||||||
@@ -79,12 +79,15 @@ pub enum Args<'a> {
|
|||||||
Submission { upper_id: &'a str },
|
Submission { upper_id: &'a str },
|
||||||
}
|
}
|
||||||
|
|
||||||
pub async fn video_list_from<'a>(
|
pub async fn video_source_from<'a>(
|
||||||
args: Args<'a>,
|
args: Args<'a>,
|
||||||
path: &Path,
|
path: &Path,
|
||||||
bili_client: &'a BiliClient,
|
bili_client: &'a BiliClient,
|
||||||
connection: &DatabaseConnection,
|
connection: &DatabaseConnection,
|
||||||
) -> Result<(VideoListModelEnum, Pin<Box<dyn Stream<Item = Result<VideoInfo>> + 'a>>)> {
|
) -> Result<(
|
||||||
|
VideoSourceEnum,
|
||||||
|
Pin<Box<dyn Stream<Item = Result<VideoInfo>> + 'a + Send>>,
|
||||||
|
)> {
|
||||||
match args {
|
match args {
|
||||||
Args::Favorite { fid } => favorite_from(fid, path, bili_client, connection).await,
|
Args::Favorite { fid } => favorite_from(fid, path, bili_client, connection).await,
|
||||||
Args::Collection { collection_item } => collection_from(collection_item, path, bili_client, connection).await,
|
Args::Collection { collection_item } => collection_from(collection_item, path, bili_client, connection).await,
|
||||||
|
|||||||
@@ -4,15 +4,15 @@ use std::pin::Pin;
|
|||||||
use anyhow::{Context, Result};
|
use anyhow::{Context, Result};
|
||||||
use bili_sync_entity::*;
|
use bili_sync_entity::*;
|
||||||
use futures::Stream;
|
use futures::Stream;
|
||||||
|
use sea_orm::ActiveValue::Set;
|
||||||
use sea_orm::entity::prelude::*;
|
use sea_orm::entity::prelude::*;
|
||||||
use sea_orm::sea_query::{OnConflict, SimpleExpr};
|
use sea_orm::sea_query::{OnConflict, SimpleExpr};
|
||||||
use sea_orm::ActiveValue::Set;
|
|
||||||
use sea_orm::{DatabaseConnection, Unchanged};
|
use sea_orm::{DatabaseConnection, Unchanged};
|
||||||
|
|
||||||
use crate::adapter::{VideoListModel, VideoListModelEnum, _ActiveModel};
|
use crate::adapter::{_ActiveModel, VideoSource, VideoSourceEnum};
|
||||||
use crate::bilibili::{BiliClient, Submission, VideoInfo};
|
use crate::bilibili::{BiliClient, Submission, VideoInfo};
|
||||||
|
|
||||||
impl VideoListModel for submission::Model {
|
impl VideoSource for submission::Model {
|
||||||
fn filter_expr(&self) -> SimpleExpr {
|
fn filter_expr(&self) -> SimpleExpr {
|
||||||
video::Column::SubmissionId.eq(self.id)
|
video::Column::SubmissionId.eq(self.id)
|
||||||
}
|
}
|
||||||
@@ -67,7 +67,10 @@ pub(super) async fn submission_from<'a>(
|
|||||||
path: &Path,
|
path: &Path,
|
||||||
bili_client: &'a BiliClient,
|
bili_client: &'a BiliClient,
|
||||||
connection: &DatabaseConnection,
|
connection: &DatabaseConnection,
|
||||||
) -> Result<(VideoListModelEnum, Pin<Box<dyn Stream<Item = Result<VideoInfo>> + 'a>>)> {
|
) -> Result<(
|
||||||
|
VideoSourceEnum,
|
||||||
|
Pin<Box<dyn Stream<Item = Result<VideoInfo>> + 'a + Send>>,
|
||||||
|
)> {
|
||||||
let submission = Submission::new(bili_client, upper_id.to_owned());
|
let submission = Submission::new(bili_client, upper_id.to_owned());
|
||||||
let upper = submission.get_info().await?;
|
let upper = submission.get_info().await?;
|
||||||
submission::Entity::insert(submission::ActiveModel {
|
submission::Entity::insert(submission::ActiveModel {
|
||||||
|
|||||||
@@ -4,15 +4,15 @@ use std::pin::Pin;
|
|||||||
use anyhow::{Context, Result};
|
use anyhow::{Context, Result};
|
||||||
use bili_sync_entity::*;
|
use bili_sync_entity::*;
|
||||||
use futures::Stream;
|
use futures::Stream;
|
||||||
|
use sea_orm::ActiveValue::Set;
|
||||||
use sea_orm::entity::prelude::*;
|
use sea_orm::entity::prelude::*;
|
||||||
use sea_orm::sea_query::{OnConflict, SimpleExpr};
|
use sea_orm::sea_query::{OnConflict, SimpleExpr};
|
||||||
use sea_orm::ActiveValue::Set;
|
|
||||||
use sea_orm::{DatabaseConnection, Unchanged};
|
use sea_orm::{DatabaseConnection, Unchanged};
|
||||||
|
|
||||||
use crate::adapter::{VideoListModel, VideoListModelEnum, _ActiveModel};
|
use crate::adapter::{_ActiveModel, VideoSource, VideoSourceEnum};
|
||||||
use crate::bilibili::{BiliClient, VideoInfo, WatchLater};
|
use crate::bilibili::{BiliClient, VideoInfo, WatchLater};
|
||||||
|
|
||||||
impl VideoListModel for watch_later::Model {
|
impl VideoSource for watch_later::Model {
|
||||||
fn filter_expr(&self) -> SimpleExpr {
|
fn filter_expr(&self) -> SimpleExpr {
|
||||||
video::Column::WatchLaterId.eq(self.id)
|
video::Column::WatchLaterId.eq(self.id)
|
||||||
}
|
}
|
||||||
@@ -66,7 +66,10 @@ pub(super) async fn watch_later_from<'a>(
|
|||||||
path: &Path,
|
path: &Path,
|
||||||
bili_client: &'a BiliClient,
|
bili_client: &'a BiliClient,
|
||||||
connection: &DatabaseConnection,
|
connection: &DatabaseConnection,
|
||||||
) -> Result<(VideoListModelEnum, Pin<Box<dyn Stream<Item = Result<VideoInfo>> + 'a>>)> {
|
) -> Result<(
|
||||||
|
VideoSourceEnum,
|
||||||
|
Pin<Box<dyn Stream<Item = Result<VideoInfo>> + 'a + Send>>,
|
||||||
|
)> {
|
||||||
let watch_later = WatchLater::new(bili_client);
|
let watch_later = WatchLater::new(bili_client);
|
||||||
watch_later::Entity::insert(watch_later::ActiveModel {
|
watch_later::Entity::insert(watch_later::ActiveModel {
|
||||||
id: Set(1),
|
id: Set(1),
|
||||||
|
|||||||
40
crates/bili_sync/src/api/auth.rs
Normal file
40
crates/bili_sync/src/api/auth.rs
Normal file
@@ -0,0 +1,40 @@
|
|||||||
|
use axum::extract::Request;
|
||||||
|
use axum::http::HeaderMap;
|
||||||
|
use axum::middleware::Next;
|
||||||
|
use axum::response::{IntoResponse, Response};
|
||||||
|
use reqwest::StatusCode;
|
||||||
|
use utoipa::Modify;
|
||||||
|
use utoipa::openapi::security::{ApiKey, ApiKeyValue, SecurityScheme};
|
||||||
|
|
||||||
|
use crate::api::wrapper::ApiResponse;
|
||||||
|
use crate::config::CONFIG;
|
||||||
|
|
||||||
|
pub async fn auth(headers: HeaderMap, request: Request, next: Next) -> Result<Response, StatusCode> {
|
||||||
|
if request.uri().path().starts_with("/api/") && get_token(&headers) != CONFIG.auth_token {
|
||||||
|
return Ok(ApiResponse::unauthorized(()).into_response());
|
||||||
|
}
|
||||||
|
Ok(next.run(request).await)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn get_token(headers: &HeaderMap) -> Option<String> {
|
||||||
|
headers
|
||||||
|
.get("Authorization")
|
||||||
|
.and_then(|v| v.to_str().ok())
|
||||||
|
.map(Into::into)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(super) struct OpenAPIAuth;
|
||||||
|
|
||||||
|
impl Modify for OpenAPIAuth {
|
||||||
|
fn modify(&self, openapi: &mut utoipa::openapi::OpenApi) {
|
||||||
|
if let Some(schema) = openapi.components.as_mut() {
|
||||||
|
schema.add_security_scheme(
|
||||||
|
"Token",
|
||||||
|
SecurityScheme::ApiKey(ApiKey::Header(ApiKeyValue::with_description(
|
||||||
|
"Authorization",
|
||||||
|
"与配置文件中的 auth_token 相同",
|
||||||
|
))),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
7
crates/bili_sync/src/api/error.rs
Normal file
7
crates/bili_sync/src/api/error.rs
Normal file
@@ -0,0 +1,7 @@
|
|||||||
|
use thiserror::Error;
|
||||||
|
|
||||||
|
#[derive(Error, Debug)]
|
||||||
|
pub enum InnerApiError {
|
||||||
|
#[error("Primary key not found: {0}")]
|
||||||
|
NotFound(i32),
|
||||||
|
}
|
||||||
252
crates/bili_sync/src/api/handler.rs
Normal file
252
crates/bili_sync/src/api/handler.rs
Normal file
@@ -0,0 +1,252 @@
|
|||||||
|
use std::sync::Arc;
|
||||||
|
|
||||||
|
use anyhow::{Result, anyhow};
|
||||||
|
use axum::extract::{Extension, Path, Query};
|
||||||
|
use bili_sync_entity::*;
|
||||||
|
use bili_sync_migration::{Expr, OnConflict};
|
||||||
|
use sea_orm::{
|
||||||
|
ColumnTrait, DatabaseConnection, EntityTrait, IntoActiveModel, PaginatorTrait, QueryFilter, QueryOrder,
|
||||||
|
QuerySelect, Set, TransactionTrait, Unchanged,
|
||||||
|
};
|
||||||
|
use utoipa::OpenApi;
|
||||||
|
|
||||||
|
use crate::api::auth::OpenAPIAuth;
|
||||||
|
use crate::api::error::InnerApiError;
|
||||||
|
use crate::api::request::VideosRequest;
|
||||||
|
use crate::api::response::{
|
||||||
|
PageInfo, ResetVideoResponse, VideoInfo, VideoResponse, VideoSource, VideoSourcesResponse, VideosResponse,
|
||||||
|
};
|
||||||
|
use crate::api::wrapper::{ApiError, ApiResponse};
|
||||||
|
use crate::utils::status::{PageStatus, VideoStatus};
|
||||||
|
|
||||||
|
#[derive(OpenApi)]
|
||||||
|
#[openapi(
|
||||||
|
paths(get_video_sources, get_videos, get_video, reset_video),
|
||||||
|
modifiers(&OpenAPIAuth),
|
||||||
|
security(
|
||||||
|
("Token" = []),
|
||||||
|
)
|
||||||
|
)]
|
||||||
|
pub struct ApiDoc;
|
||||||
|
|
||||||
|
/// 列出所有视频来源
|
||||||
|
#[utoipa::path(
|
||||||
|
get,
|
||||||
|
path = "/api/video-sources",
|
||||||
|
responses(
|
||||||
|
(status = 200, body = ApiResponse<VideoSourcesResponse>),
|
||||||
|
)
|
||||||
|
)]
|
||||||
|
pub async fn get_video_sources(
|
||||||
|
Extension(db): Extension<Arc<DatabaseConnection>>,
|
||||||
|
) -> Result<ApiResponse<VideoSourcesResponse>, ApiError> {
|
||||||
|
Ok(ApiResponse::ok(VideoSourcesResponse {
|
||||||
|
collection: collection::Entity::find()
|
||||||
|
.select_only()
|
||||||
|
.columns([collection::Column::Id, collection::Column::Name])
|
||||||
|
.into_model::<VideoSource>()
|
||||||
|
.all(db.as_ref())
|
||||||
|
.await?,
|
||||||
|
favorite: favorite::Entity::find()
|
||||||
|
.select_only()
|
||||||
|
.columns([favorite::Column::Id, favorite::Column::Name])
|
||||||
|
.into_model::<VideoSource>()
|
||||||
|
.all(db.as_ref())
|
||||||
|
.await?,
|
||||||
|
submission: submission::Entity::find()
|
||||||
|
.select_only()
|
||||||
|
.column(submission::Column::Id)
|
||||||
|
.column_as(submission::Column::UpperName, "name")
|
||||||
|
.into_model::<VideoSource>()
|
||||||
|
.all(db.as_ref())
|
||||||
|
.await?,
|
||||||
|
watch_later: watch_later::Entity::find()
|
||||||
|
.select_only()
|
||||||
|
.column(watch_later::Column::Id)
|
||||||
|
.column_as(Expr::value("稍后再看"), "name")
|
||||||
|
.into_model::<VideoSource>()
|
||||||
|
.all(db.as_ref())
|
||||||
|
.await?,
|
||||||
|
}))
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 列出视频的基本信息,支持根据视频来源筛选、名称查找和分页
|
||||||
|
#[utoipa::path(
|
||||||
|
get,
|
||||||
|
path = "/api/videos",
|
||||||
|
params(
|
||||||
|
VideosRequest,
|
||||||
|
),
|
||||||
|
responses(
|
||||||
|
(status = 200, body = ApiResponse<VideosResponse>),
|
||||||
|
)
|
||||||
|
)]
|
||||||
|
pub async fn get_videos(
|
||||||
|
Extension(db): Extension<Arc<DatabaseConnection>>,
|
||||||
|
Query(params): Query<VideosRequest>,
|
||||||
|
) -> Result<ApiResponse<VideosResponse>, ApiError> {
|
||||||
|
let mut query = video::Entity::find();
|
||||||
|
for (field, column) in [
|
||||||
|
(params.collection, video::Column::CollectionId),
|
||||||
|
(params.favorite, video::Column::FavoriteId),
|
||||||
|
(params.submission, video::Column::SubmissionId),
|
||||||
|
(params.watch_later, video::Column::WatchLaterId),
|
||||||
|
] {
|
||||||
|
if let Some(id) = field {
|
||||||
|
query = query.filter(column.eq(id));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if let Some(query_word) = params.query {
|
||||||
|
query = query.filter(video::Column::Name.contains(query_word));
|
||||||
|
}
|
||||||
|
let total_count = query.clone().count(db.as_ref()).await?;
|
||||||
|
let (page, page_size) = if let (Some(page), Some(page_size)) = (params.page, params.page_size) {
|
||||||
|
(page, page_size)
|
||||||
|
} else {
|
||||||
|
(1, 10)
|
||||||
|
};
|
||||||
|
Ok(ApiResponse::ok(VideosResponse {
|
||||||
|
videos: query
|
||||||
|
.order_by_desc(video::Column::Id)
|
||||||
|
.select_only()
|
||||||
|
.columns([
|
||||||
|
video::Column::Id,
|
||||||
|
video::Column::Name,
|
||||||
|
video::Column::UpperName,
|
||||||
|
video::Column::DownloadStatus,
|
||||||
|
])
|
||||||
|
.into_tuple::<(i32, String, String, u32)>()
|
||||||
|
.paginate(db.as_ref(), page_size)
|
||||||
|
.fetch_page(page)
|
||||||
|
.await?
|
||||||
|
.into_iter()
|
||||||
|
.map(VideoInfo::from)
|
||||||
|
.collect(),
|
||||||
|
total_count,
|
||||||
|
}))
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 获取视频详细信息,包括关联的所有 page
|
||||||
|
#[utoipa::path(
|
||||||
|
get,
|
||||||
|
path = "/api/videos/{id}",
|
||||||
|
responses(
|
||||||
|
(status = 200, body = ApiResponse<VideoResponse>),
|
||||||
|
)
|
||||||
|
)]
|
||||||
|
pub async fn get_video(
|
||||||
|
Path(id): Path<i32>,
|
||||||
|
Extension(db): Extension<Arc<DatabaseConnection>>,
|
||||||
|
) -> Result<ApiResponse<VideoResponse>, ApiError> {
|
||||||
|
let video_info = video::Entity::find_by_id(id)
|
||||||
|
.select_only()
|
||||||
|
.columns([
|
||||||
|
video::Column::Id,
|
||||||
|
video::Column::Name,
|
||||||
|
video::Column::UpperName,
|
||||||
|
video::Column::DownloadStatus,
|
||||||
|
])
|
||||||
|
.into_tuple::<(i32, String, String, u32)>()
|
||||||
|
.one(db.as_ref())
|
||||||
|
.await?
|
||||||
|
.map(VideoInfo::from);
|
||||||
|
let Some(video_info) = video_info else {
|
||||||
|
return Err(InnerApiError::NotFound(id).into());
|
||||||
|
};
|
||||||
|
let pages = page::Entity::find()
|
||||||
|
.filter(page::Column::VideoId.eq(id))
|
||||||
|
.order_by_asc(page::Column::Pid)
|
||||||
|
.select_only()
|
||||||
|
.columns([
|
||||||
|
page::Column::Id,
|
||||||
|
page::Column::Pid,
|
||||||
|
page::Column::Name,
|
||||||
|
page::Column::DownloadStatus,
|
||||||
|
])
|
||||||
|
.into_tuple::<(i32, i32, String, u32)>()
|
||||||
|
.all(db.as_ref())
|
||||||
|
.await?
|
||||||
|
.into_iter()
|
||||||
|
.map(PageInfo::from)
|
||||||
|
.collect();
|
||||||
|
Ok(ApiResponse::ok(VideoResponse {
|
||||||
|
video: video_info,
|
||||||
|
pages,
|
||||||
|
}))
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 将某个视频与其所有分页的失败状态清空为未下载状态,这样在下次下载任务中会触发重试
|
||||||
|
#[utoipa::path(
|
||||||
|
post,
|
||||||
|
path = "/api/videos/{id}/reset",
|
||||||
|
responses(
|
||||||
|
(status = 200, body = ApiResponse<ResetVideoResponse> ),
|
||||||
|
)
|
||||||
|
)]
|
||||||
|
pub async fn reset_video(
|
||||||
|
Path(id): Path<i32>,
|
||||||
|
Extension(db): Extension<Arc<DatabaseConnection>>,
|
||||||
|
) -> Result<ApiResponse<ResetVideoResponse>, ApiError> {
|
||||||
|
let txn = db.begin().await?;
|
||||||
|
let video_status: Option<u32> = video::Entity::find_by_id(id)
|
||||||
|
.select_only()
|
||||||
|
.column(video::Column::DownloadStatus)
|
||||||
|
.into_tuple()
|
||||||
|
.one(&txn)
|
||||||
|
.await?;
|
||||||
|
let Some(video_status) = video_status else {
|
||||||
|
return Err(anyhow!(InnerApiError::NotFound(id)).into());
|
||||||
|
};
|
||||||
|
let resetted_pages_model: Vec<_> = page::Entity::find()
|
||||||
|
.filter(page::Column::VideoId.eq(id))
|
||||||
|
.all(&txn)
|
||||||
|
.await?
|
||||||
|
.into_iter()
|
||||||
|
.filter_map(|mut model| {
|
||||||
|
let mut page_status = PageStatus::from(model.download_status);
|
||||||
|
if page_status.reset_failed() {
|
||||||
|
model.download_status = page_status.into();
|
||||||
|
Some(model)
|
||||||
|
} else {
|
||||||
|
None
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.collect();
|
||||||
|
let mut video_status = VideoStatus::from(video_status);
|
||||||
|
let mut should_update_video = video_status.reset_failed();
|
||||||
|
if !resetted_pages_model.is_empty() {
|
||||||
|
// 视频状态标志的第 5 位表示是否有分 P 下载失败,如果有需要重置的分页,需要同时重置视频的该状态
|
||||||
|
video_status.set(4, 0);
|
||||||
|
should_update_video = true;
|
||||||
|
}
|
||||||
|
if should_update_video {
|
||||||
|
video::Entity::update(video::ActiveModel {
|
||||||
|
id: Unchanged(id),
|
||||||
|
download_status: Set(video_status.into()),
|
||||||
|
..Default::default()
|
||||||
|
})
|
||||||
|
.exec(&txn)
|
||||||
|
.await?;
|
||||||
|
}
|
||||||
|
let resetted_pages_id: Vec<_> = resetted_pages_model.iter().map(|model| model.id).collect();
|
||||||
|
let resetted_pages_model: Vec<page::ActiveModel> = resetted_pages_model
|
||||||
|
.into_iter()
|
||||||
|
.map(|model| model.into_active_model())
|
||||||
|
.collect();
|
||||||
|
for page_trunk in resetted_pages_model.chunks(50) {
|
||||||
|
page::Entity::insert_many(page_trunk.to_vec())
|
||||||
|
.on_conflict(
|
||||||
|
OnConflict::column(page::Column::Id)
|
||||||
|
.update_column(page::Column::DownloadStatus)
|
||||||
|
.to_owned(),
|
||||||
|
)
|
||||||
|
.exec(&txn)
|
||||||
|
.await?;
|
||||||
|
}
|
||||||
|
txn.commit().await?;
|
||||||
|
Ok(ApiResponse::ok(ResetVideoResponse {
|
||||||
|
resetted: should_update_video,
|
||||||
|
video: id,
|
||||||
|
pages: resetted_pages_id,
|
||||||
|
}))
|
||||||
|
}
|
||||||
7
crates/bili_sync/src/api/mod.rs
Normal file
7
crates/bili_sync/src/api/mod.rs
Normal file
@@ -0,0 +1,7 @@
|
|||||||
|
pub mod auth;
|
||||||
|
pub mod handler;
|
||||||
|
|
||||||
|
mod error;
|
||||||
|
mod request;
|
||||||
|
mod response;
|
||||||
|
mod wrapper;
|
||||||
13
crates/bili_sync/src/api/request.rs
Normal file
13
crates/bili_sync/src/api/request.rs
Normal file
@@ -0,0 +1,13 @@
|
|||||||
|
use serde::Deserialize;
|
||||||
|
use utoipa::IntoParams;
|
||||||
|
|
||||||
|
#[derive(Deserialize, IntoParams)]
|
||||||
|
pub struct VideosRequest {
|
||||||
|
pub collection: Option<i32>,
|
||||||
|
pub favorite: Option<i32>,
|
||||||
|
pub submission: Option<i32>,
|
||||||
|
pub watch_later: Option<i32>,
|
||||||
|
pub query: Option<String>,
|
||||||
|
pub page: Option<u64>,
|
||||||
|
pub page_size: Option<u64>,
|
||||||
|
}
|
||||||
76
crates/bili_sync/src/api/response.rs
Normal file
76
crates/bili_sync/src/api/response.rs
Normal file
@@ -0,0 +1,76 @@
|
|||||||
|
use sea_orm::FromQueryResult;
|
||||||
|
use serde::Serialize;
|
||||||
|
use utoipa::ToSchema;
|
||||||
|
|
||||||
|
use crate::utils::status::{PageStatus, VideoStatus};
|
||||||
|
|
||||||
|
#[derive(Serialize, ToSchema)]
|
||||||
|
pub struct VideoSourcesResponse {
|
||||||
|
pub collection: Vec<VideoSource>,
|
||||||
|
pub favorite: Vec<VideoSource>,
|
||||||
|
pub submission: Vec<VideoSource>,
|
||||||
|
pub watch_later: Vec<VideoSource>,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Serialize, ToSchema)]
|
||||||
|
pub struct VideosResponse {
|
||||||
|
pub videos: Vec<VideoInfo>,
|
||||||
|
pub total_count: u64,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Serialize, ToSchema)]
|
||||||
|
pub struct VideoResponse {
|
||||||
|
pub video: VideoInfo,
|
||||||
|
pub pages: Vec<PageInfo>,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Serialize, ToSchema)]
|
||||||
|
pub struct ResetVideoResponse {
|
||||||
|
pub resetted: bool,
|
||||||
|
pub video: i32,
|
||||||
|
pub pages: Vec<i32>,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(FromQueryResult, Serialize, ToSchema)]
|
||||||
|
pub struct VideoSource {
|
||||||
|
id: i32,
|
||||||
|
name: String,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Serialize, ToSchema)]
|
||||||
|
pub struct PageInfo {
|
||||||
|
pub id: i32,
|
||||||
|
pub pid: i32,
|
||||||
|
pub name: String,
|
||||||
|
pub download_status: [u32; 5],
|
||||||
|
}
|
||||||
|
|
||||||
|
impl From<(i32, i32, String, u32)> for PageInfo {
|
||||||
|
fn from((id, pid, name, download_status): (i32, i32, String, u32)) -> Self {
|
||||||
|
Self {
|
||||||
|
id,
|
||||||
|
pid,
|
||||||
|
name,
|
||||||
|
download_status: PageStatus::from(download_status).into(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Serialize, ToSchema)]
|
||||||
|
pub struct VideoInfo {
|
||||||
|
pub id: i32,
|
||||||
|
pub name: String,
|
||||||
|
pub upper_name: String,
|
||||||
|
pub download_status: [u32; 5],
|
||||||
|
}
|
||||||
|
|
||||||
|
impl From<(i32, String, String, u32)> for VideoInfo {
|
||||||
|
fn from((id, name, upper_name, download_status): (i32, String, String, u32)) -> Self {
|
||||||
|
Self {
|
||||||
|
id,
|
||||||
|
name,
|
||||||
|
upper_name,
|
||||||
|
download_status: VideoStatus::from(download_status).into(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
64
crates/bili_sync/src/api/wrapper.rs
Normal file
64
crates/bili_sync/src/api/wrapper.rs
Normal file
@@ -0,0 +1,64 @@
|
|||||||
|
use anyhow::Error;
|
||||||
|
use axum::Json;
|
||||||
|
use axum::response::IntoResponse;
|
||||||
|
use reqwest::StatusCode;
|
||||||
|
use serde::Serialize;
|
||||||
|
use utoipa::ToSchema;
|
||||||
|
|
||||||
|
use crate::api::error::InnerApiError;
|
||||||
|
|
||||||
|
#[derive(ToSchema, Serialize)]
|
||||||
|
pub struct ApiResponse<T: Serialize> {
|
||||||
|
status_code: u16,
|
||||||
|
data: T,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<T: Serialize> ApiResponse<T> {
|
||||||
|
pub fn ok(data: T) -> Self {
|
||||||
|
Self { status_code: 200, data }
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn unauthorized(data: T) -> Self {
|
||||||
|
Self { status_code: 401, data }
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn not_found(data: T) -> Self {
|
||||||
|
Self { status_code: 404, data }
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn internal_server_error(data: T) -> Self {
|
||||||
|
Self { status_code: 500, data }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<T: Serialize> IntoResponse for ApiResponse<T> {
|
||||||
|
fn into_response(self) -> axum::response::Response {
|
||||||
|
(
|
||||||
|
StatusCode::from_u16(self.status_code).expect("invalid Http Status Code"),
|
||||||
|
Json(self),
|
||||||
|
)
|
||||||
|
.into_response()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub struct ApiError(Error);
|
||||||
|
|
||||||
|
impl<E> From<E> for ApiError
|
||||||
|
where
|
||||||
|
E: Into<anyhow::Error>,
|
||||||
|
{
|
||||||
|
fn from(value: E) -> Self {
|
||||||
|
Self(value.into())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl IntoResponse for ApiError {
|
||||||
|
fn into_response(self) -> axum::response::Response {
|
||||||
|
if let Some(inner_error) = self.0.downcast_ref::<InnerApiError>() {
|
||||||
|
match inner_error {
|
||||||
|
InnerApiError::NotFound(_) => return ApiResponse::not_found(self.0.to_string()).into_response(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
ApiResponse::internal_server_error(self.0.to_string()).into_response()
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,4 +1,4 @@
|
|||||||
use anyhow::{bail, Context, Result};
|
use anyhow::{Context, Result, bail};
|
||||||
use serde::{Deserialize, Serialize};
|
use serde::{Deserialize, Serialize};
|
||||||
|
|
||||||
use crate::bilibili::error::BiliError;
|
use crate::bilibili::error::BiliError;
|
||||||
@@ -98,7 +98,7 @@ impl Default for FilterOption {
|
|||||||
pub enum Stream {
|
pub enum Stream {
|
||||||
Flv(String),
|
Flv(String),
|
||||||
Html5Mp4(String),
|
Html5Mp4(String),
|
||||||
EpositeTryMp4(String),
|
EpisodeTryMp4(String),
|
||||||
DashVideo {
|
DashVideo {
|
||||||
url: String,
|
url: String,
|
||||||
quality: VideoQuality,
|
quality: VideoQuality,
|
||||||
@@ -116,7 +116,7 @@ impl Stream {
|
|||||||
match self {
|
match self {
|
||||||
Self::Flv(url) => url,
|
Self::Flv(url) => url,
|
||||||
Self::Html5Mp4(url) => url,
|
Self::Html5Mp4(url) => url,
|
||||||
Self::EpositeTryMp4(url) => url,
|
Self::EpisodeTryMp4(url) => url,
|
||||||
Self::DashVideo { url, .. } => url,
|
Self::DashVideo { url, .. } => url,
|
||||||
Self::DashAudio { url, .. } => url,
|
Self::DashAudio { url, .. } => url,
|
||||||
}
|
}
|
||||||
@@ -172,7 +172,7 @@ impl PageAnalyzer {
|
|||||||
)]);
|
)]);
|
||||||
}
|
}
|
||||||
if self.is_episode_try_mp4_stream() {
|
if self.is_episode_try_mp4_stream() {
|
||||||
return Ok(vec![Stream::EpositeTryMp4(
|
return Ok(vec![Stream::EpisodeTryMp4(
|
||||||
self.info["durl"][0]["url"]
|
self.info["durl"][0]["url"]
|
||||||
.as_str()
|
.as_str()
|
||||||
.context("invalid episode try mp4 stream")?
|
.context("invalid episode try mp4 stream")?
|
||||||
@@ -313,27 +313,31 @@ mod tests {
|
|||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn test_quality_order() {
|
fn test_quality_order() {
|
||||||
assert!([
|
assert!(
|
||||||
VideoQuality::Quality360p,
|
[
|
||||||
VideoQuality::Quality480p,
|
VideoQuality::Quality360p,
|
||||||
VideoQuality::Quality720p,
|
VideoQuality::Quality480p,
|
||||||
VideoQuality::Quality1080p,
|
VideoQuality::Quality720p,
|
||||||
VideoQuality::Quality1080pPLUS,
|
VideoQuality::Quality1080p,
|
||||||
VideoQuality::Quality1080p60,
|
VideoQuality::Quality1080pPLUS,
|
||||||
VideoQuality::Quality4k,
|
VideoQuality::Quality1080p60,
|
||||||
VideoQuality::QualityHdr,
|
VideoQuality::Quality4k,
|
||||||
VideoQuality::QualityDolby,
|
VideoQuality::QualityHdr,
|
||||||
VideoQuality::Quality8k
|
VideoQuality::QualityDolby,
|
||||||
]
|
VideoQuality::Quality8k
|
||||||
.is_sorted());
|
]
|
||||||
assert!([
|
.is_sorted()
|
||||||
AudioQuality::Quality64k,
|
);
|
||||||
AudioQuality::Quality132k,
|
assert!(
|
||||||
AudioQuality::Quality192k,
|
[
|
||||||
AudioQuality::QualityDolby,
|
AudioQuality::Quality64k,
|
||||||
AudioQuality::QualityHiRES,
|
AudioQuality::Quality132k,
|
||||||
]
|
AudioQuality::Quality192k,
|
||||||
.is_sorted());
|
AudioQuality::QualityDolby,
|
||||||
|
AudioQuality::QualityHiRES,
|
||||||
|
]
|
||||||
|
.is_sorted()
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
#[ignore = "only for manual test"]
|
#[ignore = "only for manual test"]
|
||||||
|
|||||||
@@ -3,11 +3,11 @@ use std::time::Duration;
|
|||||||
|
|
||||||
use anyhow::{Context, Result};
|
use anyhow::{Context, Result};
|
||||||
use leaky_bucket::RateLimiter;
|
use leaky_bucket::RateLimiter;
|
||||||
use reqwest::{header, Method};
|
use reqwest::{Method, header};
|
||||||
|
|
||||||
use crate::bilibili::credential::WbiImg;
|
|
||||||
use crate::bilibili::Credential;
|
use crate::bilibili::Credential;
|
||||||
use crate::config::{RateLimit, CONFIG};
|
use crate::bilibili::credential::WbiImg;
|
||||||
|
use crate::config::{CONFIG, RateLimit};
|
||||||
|
|
||||||
// 一个对 reqwest::Client 的简单封装,用于 Bilibili 请求
|
// 一个对 reqwest::Client 的简单封装,用于 Bilibili 请求
|
||||||
#[derive(Clone)]
|
#[derive(Clone)]
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
use std::fmt::{Display, Formatter};
|
use std::fmt::{Display, Formatter};
|
||||||
|
|
||||||
use anyhow::{anyhow, Context, Result};
|
use anyhow::{Context, Result, anyhow};
|
||||||
use async_stream::try_stream;
|
use async_stream::try_stream;
|
||||||
use futures::Stream;
|
use futures::Stream;
|
||||||
use reqwest::Method;
|
use reqwest::Method;
|
||||||
@@ -8,7 +8,7 @@ use serde::Deserialize;
|
|||||||
use serde_json::Value;
|
use serde_json::Value;
|
||||||
|
|
||||||
use crate::bilibili::credential::encoded_query;
|
use crate::bilibili::credential::encoded_query;
|
||||||
use crate::bilibili::{BiliClient, Validate, VideoInfo, MIXIN_KEY};
|
use crate::bilibili::{BiliClient, MIXIN_KEY, Validate, VideoInfo};
|
||||||
|
|
||||||
#[derive(PartialEq, Eq, Hash, Clone, Debug)]
|
#[derive(PartialEq, Eq, Hash, Clone, Debug)]
|
||||||
pub enum CollectionType {
|
pub enum CollectionType {
|
||||||
|
|||||||
@@ -1,11 +1,11 @@
|
|||||||
use std::borrow::Cow;
|
use std::borrow::Cow;
|
||||||
use std::collections::HashSet;
|
use std::collections::HashSet;
|
||||||
|
|
||||||
use anyhow::{bail, ensure, Context, Result};
|
use anyhow::{Context, Result, bail, ensure};
|
||||||
use cookie::Cookie;
|
use cookie::Cookie;
|
||||||
use cow_utils::CowUtils;
|
use cow_utils::CowUtils;
|
||||||
use regex::Regex;
|
use regex::Regex;
|
||||||
use reqwest::{header, Method};
|
use reqwest::{Method, header};
|
||||||
use rsa::pkcs8::DecodePublicKey;
|
use rsa::pkcs8::DecodePublicKey;
|
||||||
use rsa::sha2::Sha256;
|
use rsa::sha2::Sha256;
|
||||||
use rsa::{Oaep, RsaPublicKey};
|
use rsa::{Oaep, RsaPublicKey};
|
||||||
@@ -55,6 +55,7 @@ impl Credential {
|
|||||||
.request(Method::GET, "https://api.bilibili.com/x/web-interface/nav", Some(self))
|
.request(Method::GET, "https://api.bilibili.com/x/web-interface/nav", Some(self))
|
||||||
.send()
|
.send()
|
||||||
.await?
|
.await?
|
||||||
|
.error_for_status()?
|
||||||
.json::<serde_json::Value>()
|
.json::<serde_json::Value>()
|
||||||
.await?
|
.await?
|
||||||
.validate()?;
|
.validate()?;
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
use crate::bilibili::danmaku::canvas::CanvasConfig;
|
|
||||||
use crate::bilibili::danmaku::Danmu;
|
use crate::bilibili::danmaku::Danmu;
|
||||||
|
use crate::bilibili::danmaku::canvas::CanvasConfig;
|
||||||
|
|
||||||
pub enum Collision {
|
pub enum Collision {
|
||||||
// 会越来越远
|
// 会越来越远
|
||||||
|
|||||||
@@ -5,10 +5,10 @@ use anyhow::Result;
|
|||||||
use float_ord::FloatOrd;
|
use float_ord::FloatOrd;
|
||||||
use lane::Lane;
|
use lane::Lane;
|
||||||
|
|
||||||
|
use crate::bilibili::PageInfo;
|
||||||
use crate::bilibili::danmaku::canvas::lane::Collision;
|
use crate::bilibili::danmaku::canvas::lane::Collision;
|
||||||
use crate::bilibili::danmaku::danmu::DanmuType;
|
use crate::bilibili::danmaku::danmu::DanmuType;
|
||||||
use crate::bilibili::danmaku::{Danmu, DrawEffect, Drawable};
|
use crate::bilibili::danmaku::{Danmu, DrawEffect, Drawable};
|
||||||
use crate::bilibili::PageInfo;
|
|
||||||
|
|
||||||
#[derive(Debug, serde::Deserialize, serde::Serialize)]
|
#[derive(Debug, serde::Deserialize, serde::Serialize)]
|
||||||
pub struct DanmakuOption {
|
pub struct DanmakuOption {
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
//! 一个弹幕实例,但是没有位置信息
|
//! 一个弹幕实例,但是没有位置信息
|
||||||
use anyhow::{bail, Result};
|
use anyhow::{Result, bail};
|
||||||
|
|
||||||
use crate::bilibili::danmaku::canvas::CanvasConfig;
|
use crate::bilibili::danmaku::canvas::CanvasConfig;
|
||||||
|
|
||||||
|
|||||||
@@ -3,9 +3,9 @@ use std::path::PathBuf;
|
|||||||
use anyhow::Result;
|
use anyhow::Result;
|
||||||
use tokio::fs::{self, File};
|
use tokio::fs::{self, File};
|
||||||
|
|
||||||
|
use crate::bilibili::PageInfo;
|
||||||
use crate::bilibili::danmaku::canvas::CanvasConfig;
|
use crate::bilibili::danmaku::canvas::CanvasConfig;
|
||||||
use crate::bilibili::danmaku::{AssWriter, Danmu};
|
use crate::bilibili::danmaku::{AssWriter, Danmu};
|
||||||
use crate::bilibili::PageInfo;
|
|
||||||
use crate::config::CONFIG;
|
use crate::config::CONFIG;
|
||||||
|
|
||||||
pub struct DanmakuWriter<'a> {
|
pub struct DanmakuWriter<'a> {
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
use anyhow::{anyhow, Context, Result};
|
use anyhow::{Context, Result, anyhow};
|
||||||
use async_stream::try_stream;
|
use async_stream::try_stream;
|
||||||
use futures::Stream;
|
use futures::Stream;
|
||||||
use serde_json::Value;
|
use serde_json::Value;
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
use std::sync::Arc;
|
use std::sync::Arc;
|
||||||
|
|
||||||
pub use analyzer::{BestStream, FilterOption};
|
pub use analyzer::{BestStream, FilterOption};
|
||||||
use anyhow::{bail, ensure, Result};
|
use anyhow::{Result, bail, ensure};
|
||||||
use arc_swap::ArcSwapOption;
|
use arc_swap::ArcSwapOption;
|
||||||
use chrono::serde::ts_seconds;
|
use chrono::serde::ts_seconds;
|
||||||
use chrono::{DateTime, Utc};
|
use chrono::{DateTime, Utc};
|
||||||
@@ -25,6 +25,7 @@ mod danmaku;
|
|||||||
mod error;
|
mod error;
|
||||||
mod favorite_list;
|
mod favorite_list;
|
||||||
mod submission;
|
mod submission;
|
||||||
|
mod subtitle;
|
||||||
mod video;
|
mod video;
|
||||||
mod watch_later;
|
mod watch_later;
|
||||||
|
|
||||||
@@ -197,4 +198,26 @@ mod tests {
|
|||||||
assert!(videos.iter().all(|v| matches!(v, VideoInfo::Submission { .. })));
|
assert!(videos.iter().all(|v| matches!(v, VideoInfo::Submission { .. })));
|
||||||
assert!(videos.iter().rev().is_sorted_by_key(|v| v.release_datetime()));
|
assert!(videos.iter().rev().is_sorted_by_key(|v| v.release_datetime()));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[ignore = "only for manual test"]
|
||||||
|
#[tokio::test]
|
||||||
|
async fn test_subtitle_parse() -> Result<()> {
|
||||||
|
let bili_client = BiliClient::new();
|
||||||
|
let Ok(Some(mixin_key)) = bili_client.wbi_img().await.map(|wbi_img| wbi_img.into()) else {
|
||||||
|
panic!("获取 mixin key 失败");
|
||||||
|
};
|
||||||
|
set_global_mixin_key(mixin_key);
|
||||||
|
let video = Video::new(&bili_client, "BV1gLfnY8E6D".to_string());
|
||||||
|
let pages = video.get_pages().await?;
|
||||||
|
println!("pages: {:?}", pages);
|
||||||
|
let subtitles = video.get_subtitles(&pages[0]).await?;
|
||||||
|
for subtitle in subtitles {
|
||||||
|
println!(
|
||||||
|
"{}: {}",
|
||||||
|
subtitle.lan,
|
||||||
|
subtitle.body.to_string().chars().take(200).collect::<String>()
|
||||||
|
);
|
||||||
|
}
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
use anyhow::{anyhow, Context, Result};
|
use anyhow::{Context, Result, anyhow};
|
||||||
use arc_swap::access::Access;
|
use arc_swap::access::Access;
|
||||||
use async_stream::try_stream;
|
use async_stream::try_stream;
|
||||||
use futures::Stream;
|
use futures::Stream;
|
||||||
@@ -7,7 +7,7 @@ use serde_json::Value;
|
|||||||
|
|
||||||
use crate::bilibili::credential::encoded_query;
|
use crate::bilibili::credential::encoded_query;
|
||||||
use crate::bilibili::favorite_list::Upper;
|
use crate::bilibili::favorite_list::Upper;
|
||||||
use crate::bilibili::{BiliClient, Validate, VideoInfo, MIXIN_KEY};
|
use crate::bilibili::{BiliClient, MIXIN_KEY, Validate, VideoInfo};
|
||||||
pub struct Submission<'a> {
|
pub struct Submission<'a> {
|
||||||
client: &'a BiliClient,
|
client: &'a BiliClient,
|
||||||
upper_id: String,
|
upper_id: String,
|
||||||
|
|||||||
75
crates/bili_sync/src/bilibili/subtitle.rs
Normal file
75
crates/bili_sync/src/bilibili/subtitle.rs
Normal file
@@ -0,0 +1,75 @@
|
|||||||
|
use std::fmt::Display;
|
||||||
|
|
||||||
|
#[derive(Debug, serde::Deserialize)]
|
||||||
|
pub struct SubTitlesInfo {
|
||||||
|
pub subtitles: Vec<SubTitleInfo>,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, serde::Deserialize)]
|
||||||
|
pub struct SubTitleInfo {
|
||||||
|
pub lan: String,
|
||||||
|
pub subtitle_url: String,
|
||||||
|
}
|
||||||
|
|
||||||
|
pub struct SubTitle {
|
||||||
|
pub lan: String,
|
||||||
|
pub body: SubTitleBody,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, serde::Deserialize)]
|
||||||
|
pub struct SubTitleBody(pub Vec<SubTitleItem>);
|
||||||
|
|
||||||
|
#[derive(Debug, serde::Deserialize)]
|
||||||
|
pub struct SubTitleItem {
|
||||||
|
from: f64,
|
||||||
|
to: f64,
|
||||||
|
content: String,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl SubTitleInfo {
|
||||||
|
pub fn is_ai_sub(&self) -> bool {
|
||||||
|
// ai: aisubtitle.hdslb.com/bfs/ai_subtitle/xxxx
|
||||||
|
// 非 ai: aisubtitle.hdslb.com/bfs/subtitle/xxxx
|
||||||
|
self.subtitle_url.contains("ai_subtitle")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Display for SubTitleBody {
|
||||||
|
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||||
|
for (idx, item) in self.0.iter().enumerate() {
|
||||||
|
writeln!(f, "{}", idx)?;
|
||||||
|
writeln!(f, "{} --> {}", format_time(item.from), format_time(item.to))?;
|
||||||
|
writeln!(f, "{}", item.content)?;
|
||||||
|
writeln!(f)?;
|
||||||
|
}
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn format_time(time: f64) -> String {
|
||||||
|
let (second, millisecond) = (time.trunc(), (time.fract() * 1e3) as u32);
|
||||||
|
let (hour, minute, second) = (
|
||||||
|
(second / 3600.0) as u32,
|
||||||
|
((second % 3600.0) / 60.0) as u32,
|
||||||
|
(second % 60.0) as u32,
|
||||||
|
);
|
||||||
|
format!("{:02}:{:02}:{:02},{:03}", hour, minute, second, millisecond)
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
mod tests {
|
||||||
|
#[test]
|
||||||
|
fn test_format_time() {
|
||||||
|
// float 解析会有精度问题,但误差几毫秒应该不太关键
|
||||||
|
// 想再健壮一点就得手写 serde_json 解析拆分秒和毫秒,然后分别处理了
|
||||||
|
let testcases = [
|
||||||
|
(0.0, "00:00:00,000"),
|
||||||
|
(1.5, "00:00:01,500"),
|
||||||
|
(206.45, "00:03:26,449"),
|
||||||
|
(360001.23, "100:00:01,229"),
|
||||||
|
];
|
||||||
|
for (time, expect) in testcases.iter() {
|
||||||
|
assert_eq!(super::format_time(*time), *expect);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,6 +1,6 @@
|
|||||||
use anyhow::{ensure, Result};
|
use anyhow::{Result, ensure};
|
||||||
use futures::stream::FuturesUnordered;
|
|
||||||
use futures::TryStreamExt;
|
use futures::TryStreamExt;
|
||||||
|
use futures::stream::FuturesUnordered;
|
||||||
use prost::Message;
|
use prost::Message;
|
||||||
use reqwest::Method;
|
use reqwest::Method;
|
||||||
|
|
||||||
@@ -8,7 +8,8 @@ use crate::bilibili::analyzer::PageAnalyzer;
|
|||||||
use crate::bilibili::client::BiliClient;
|
use crate::bilibili::client::BiliClient;
|
||||||
use crate::bilibili::credential::encoded_query;
|
use crate::bilibili::credential::encoded_query;
|
||||||
use crate::bilibili::danmaku::{DanmakuElem, DanmakuWriter, DmSegMobileReply};
|
use crate::bilibili::danmaku::{DanmakuElem, DanmakuWriter, DmSegMobileReply};
|
||||||
use crate::bilibili::{Validate, VideoInfo, MIXIN_KEY};
|
use crate::bilibili::subtitle::{SubTitle, SubTitleBody, SubTitleInfo, SubTitlesInfo};
|
||||||
|
use crate::bilibili::{MIXIN_KEY, Validate, VideoInfo};
|
||||||
|
|
||||||
static MASK_CODE: u64 = 2251799813685247;
|
static MASK_CODE: u64 = 2251799813685247;
|
||||||
static XOR_CODE: u64 = 23442827791579;
|
static XOR_CODE: u64 = 23442827791579;
|
||||||
@@ -164,6 +165,46 @@ impl<'a> Video<'a> {
|
|||||||
.validate()?;
|
.validate()?;
|
||||||
Ok(PageAnalyzer::new(res["data"].take()))
|
Ok(PageAnalyzer::new(res["data"].take()))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub async fn get_subtitles(&self, page: &PageInfo) -> Result<Vec<SubTitle>> {
|
||||||
|
let mut res = self
|
||||||
|
.client
|
||||||
|
.request(Method::GET, "https://api.bilibili.com/x/player/wbi/v2")
|
||||||
|
.await
|
||||||
|
.query(&encoded_query(
|
||||||
|
vec![("cid", &page.cid.to_string()), ("bvid", &self.bvid), ("aid", &self.aid)],
|
||||||
|
MIXIN_KEY.load().as_deref(),
|
||||||
|
))
|
||||||
|
.send()
|
||||||
|
.await?
|
||||||
|
.error_for_status()?
|
||||||
|
.json::<serde_json::Value>()
|
||||||
|
.await?
|
||||||
|
.validate()?;
|
||||||
|
// 接口返回的信息,包含了一系列的字幕,每个字幕包含了字幕的语言和 json 下载地址
|
||||||
|
let subtitles_info: SubTitlesInfo = serde_json::from_value(res["data"]["subtitle"].take())?;
|
||||||
|
let tasks = subtitles_info
|
||||||
|
.subtitles
|
||||||
|
.into_iter()
|
||||||
|
.filter(|v| !v.is_ai_sub())
|
||||||
|
.map(|v| self.get_subtitle(v))
|
||||||
|
.collect::<FuturesUnordered<_>>();
|
||||||
|
tasks.try_collect().await
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn get_subtitle(&self, info: SubTitleInfo) -> Result<SubTitle> {
|
||||||
|
let mut res = self
|
||||||
|
.client
|
||||||
|
.client // 这里可以直接使用 inner_client,因为该请求不需要鉴权
|
||||||
|
.request(Method::GET, format!("https:{}", &info.subtitle_url).as_str(), None)
|
||||||
|
.send()
|
||||||
|
.await?
|
||||||
|
.error_for_status()?
|
||||||
|
.json::<serde_json::Value>()
|
||||||
|
.await?;
|
||||||
|
let body: SubTitleBody = serde_json::from_value(res["body"].take())?;
|
||||||
|
Ok(SubTitle { lan: info.lan, body })
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fn bvid_to_aid(bvid: &str) -> u64 {
|
fn bvid_to_aid(bvid: &str) -> u64 {
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
use anyhow::{anyhow, Context, Result};
|
use anyhow::{Context, Result, anyhow};
|
||||||
use async_stream::try_stream;
|
use async_stream::try_stream;
|
||||||
use futures::Stream;
|
use futures::Stream;
|
||||||
use serde_json::Value;
|
use serde_json::Value;
|
||||||
|
|||||||
@@ -1,7 +1,9 @@
|
|||||||
|
use std::borrow::Cow;
|
||||||
|
|
||||||
use clap::Parser;
|
use clap::Parser;
|
||||||
|
|
||||||
#[derive(Parser)]
|
#[derive(Parser)]
|
||||||
#[command(version, about, long_about = None)]
|
#[command(name = "Bili-Sync", version = detail_version(), about, long_about = None)]
|
||||||
pub struct Args {
|
pub struct Args {
|
||||||
#[arg(short, long, env = "SCAN_ONLY")]
|
#[arg(short, long, env = "SCAN_ONLY")]
|
||||||
pub scan_only: bool,
|
pub scan_only: bool,
|
||||||
@@ -9,3 +11,31 @@ pub struct Args {
|
|||||||
#[arg(short, long, default_value = "None,bili_sync=info", env = "RUST_LOG")]
|
#[arg(short, long, default_value = "None,bili_sync=info", env = "RUST_LOG")]
|
||||||
pub log_level: String,
|
pub log_level: String,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
mod built_info {
|
||||||
|
include!(concat!(env!("OUT_DIR"), "/built.rs"));
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn version() -> Cow<'static, str> {
|
||||||
|
if let (Some(git_version), Some(git_dirty)) = (built_info::GIT_VERSION, built_info::GIT_DIRTY) {
|
||||||
|
Cow::Owned(format!("{}{}", git_version, if git_dirty { "-dirty" } else { "" }))
|
||||||
|
} else {
|
||||||
|
Cow::Borrowed(built_info::PKG_VERSION)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn detail_version() -> String {
|
||||||
|
format!(
|
||||||
|
"{}
|
||||||
|
Architecture: {}-{}
|
||||||
|
Author: {}
|
||||||
|
Built Time: {}
|
||||||
|
Rustc Version: {}",
|
||||||
|
version(),
|
||||||
|
built_info::CFG_OS,
|
||||||
|
built_info::CFG_TARGET_ARCH,
|
||||||
|
built_info::PKG_AUTHORS,
|
||||||
|
built_info::BUILT_TIME_UTC,
|
||||||
|
built_info::RUSTC_VERSION,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|||||||
@@ -4,9 +4,9 @@ use clap::Parser;
|
|||||||
use handlebars::handlebars_helper;
|
use handlebars::handlebars_helper;
|
||||||
use once_cell::sync::Lazy;
|
use once_cell::sync::Lazy;
|
||||||
|
|
||||||
|
use crate::config::Config;
|
||||||
use crate::config::clap::Args;
|
use crate::config::clap::Args;
|
||||||
use crate::config::item::PathSafeTemplate;
|
use crate::config::item::PathSafeTemplate;
|
||||||
use crate::config::Config;
|
|
||||||
|
|
||||||
/// 全局的 CONFIG,可以从中读取配置信息
|
/// 全局的 CONFIG,可以从中读取配置信息
|
||||||
pub static CONFIG: Lazy<Config> = Lazy::new(load_config);
|
pub static CONFIG: Lazy<Config> = Lazy::new(load_config);
|
||||||
@@ -40,6 +40,7 @@ pub static CONFIG_DIR: Lazy<PathBuf> =
|
|||||||
|
|
||||||
#[cfg(not(test))]
|
#[cfg(not(test))]
|
||||||
fn load_config() -> Config {
|
fn load_config() -> Config {
|
||||||
|
info!("开始加载配置文件..");
|
||||||
let config = Config::load().unwrap_or_else(|err| {
|
let config = Config::load().unwrap_or_else(|err| {
|
||||||
if err
|
if err
|
||||||
.downcast_ref::<std::io::Error>()
|
.downcast_ref::<std::io::Error>()
|
||||||
@@ -47,12 +48,11 @@ fn load_config() -> Config {
|
|||||||
{
|
{
|
||||||
panic!("加载配置文件失败,错误为: {err}");
|
panic!("加载配置文件失败,错误为: {err}");
|
||||||
}
|
}
|
||||||
warn!("配置文件不存在,使用默认配置...");
|
warn!("配置文件不存在,使用默认配置..");
|
||||||
let default_config = Config::default();
|
Config::default()
|
||||||
default_config.save().expect("保存默认配置时遇到错误");
|
|
||||||
info!("已将默认配置写入 {}", CONFIG_DIR.join("config.toml").display());
|
|
||||||
default_config
|
|
||||||
});
|
});
|
||||||
|
info!("配置文件加载完毕,覆盖刷新原有配置");
|
||||||
|
config.save().expect("保存默认配置时遇到错误");
|
||||||
info!("检查配置文件..");
|
info!("检查配置文件..");
|
||||||
config.check();
|
config.check();
|
||||||
info!("配置文件检查通过");
|
info!("配置文件检查通过");
|
||||||
@@ -60,7 +60,6 @@ fn load_config() -> Config {
|
|||||||
}
|
}
|
||||||
|
|
||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
|
|
||||||
fn load_config() -> Config {
|
fn load_config() -> Config {
|
||||||
let credential = match (
|
let credential = match (
|
||||||
std::env::var("TEST_SESSDATA"),
|
std::env::var("TEST_SESSDATA"),
|
||||||
|
|||||||
@@ -114,7 +114,7 @@ where
|
|||||||
_ => {
|
_ => {
|
||||||
return Err(serde::de::Error::custom(
|
return Err(serde::de::Error::custom(
|
||||||
"invalid collection type, should be series or season",
|
"invalid collection type, should be series or season",
|
||||||
))
|
));
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
CollectionItem {
|
CollectionItem {
|
||||||
@@ -126,7 +126,7 @@ where
|
|||||||
_ => {
|
_ => {
|
||||||
return Err(serde::de::Error::custom(
|
return Err(serde::de::Error::custom(
|
||||||
"invalid collection key, should be series:mid:sid or season:mid:sid",
|
"invalid collection key, should be series:mid:sid or season:mid:sid",
|
||||||
))
|
));
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
collection_list.insert(collection_item, value);
|
collection_list.insert(collection_item, value);
|
||||||
|
|||||||
@@ -5,23 +5,45 @@ use std::sync::Arc;
|
|||||||
|
|
||||||
use anyhow::Result;
|
use anyhow::Result;
|
||||||
use arc_swap::ArcSwapOption;
|
use arc_swap::ArcSwapOption;
|
||||||
|
use rand::seq::SliceRandom;
|
||||||
use serde::{Deserialize, Serialize};
|
use serde::{Deserialize, Serialize};
|
||||||
|
|
||||||
mod clap;
|
mod clap;
|
||||||
mod global;
|
mod global;
|
||||||
mod item;
|
mod item;
|
||||||
|
|
||||||
|
use crate::adapter::Args;
|
||||||
use crate::bilibili::{CollectionItem, Credential, DanmakuOption, FilterOption};
|
use crate::bilibili::{CollectionItem, Credential, DanmakuOption, FilterOption};
|
||||||
|
pub use crate::config::clap::version;
|
||||||
pub use crate::config::global::{ARGS, CONFIG, CONFIG_DIR, TEMPLATE};
|
pub use crate::config::global::{ARGS, CONFIG, CONFIG_DIR, TEMPLATE};
|
||||||
use crate::config::item::{deserialize_collection_list, serialize_collection_list, ConcurrentLimit};
|
use crate::config::item::{ConcurrentLimit, deserialize_collection_list, serialize_collection_list};
|
||||||
pub use crate::config::item::{NFOTimeType, PathSafeTemplate, RateLimit, WatchLaterConfig};
|
pub use crate::config::item::{NFOTimeType, PathSafeTemplate, RateLimit, WatchLaterConfig};
|
||||||
|
|
||||||
fn default_time_format() -> String {
|
fn default_time_format() -> String {
|
||||||
"%Y-%m-%d".to_string()
|
"%Y-%m-%d".to_string()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// 默认的 auth_token 实现,生成随机 16 位字符串
|
||||||
|
fn default_auth_token() -> Option<String> {
|
||||||
|
let byte_choices = b"abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789!@#$%^&*()_+-=";
|
||||||
|
let mut rng = rand::thread_rng();
|
||||||
|
Some(
|
||||||
|
(0..16)
|
||||||
|
.map(|_| *(byte_choices.choose(&mut rng).expect("choose byte failed")) as char)
|
||||||
|
.collect(),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn default_bind_address() -> String {
|
||||||
|
"0.0.0.0:12345".to_string()
|
||||||
|
}
|
||||||
|
|
||||||
#[derive(Serialize, Deserialize)]
|
#[derive(Serialize, Deserialize)]
|
||||||
pub struct Config {
|
pub struct Config {
|
||||||
|
#[serde(default = "default_auth_token")]
|
||||||
|
pub auth_token: Option<String>,
|
||||||
|
#[serde(default = "default_bind_address")]
|
||||||
|
pub bind_address: String,
|
||||||
pub credential: ArcSwapOption<Credential>,
|
pub credential: ArcSwapOption<Credential>,
|
||||||
pub filter_option: FilterOption,
|
pub filter_option: FilterOption,
|
||||||
#[serde(default)]
|
#[serde(default)]
|
||||||
@@ -52,6 +74,8 @@ pub struct Config {
|
|||||||
impl Default for Config {
|
impl Default for Config {
|
||||||
fn default() -> Self {
|
fn default() -> Self {
|
||||||
Self {
|
Self {
|
||||||
|
auth_token: default_auth_token(),
|
||||||
|
bind_address: default_bind_address(),
|
||||||
credential: ArcSwapOption::from(Some(Arc::new(Credential::default()))),
|
credential: ArcSwapOption::from(Some(Arc::new(Credential::default()))),
|
||||||
filter_option: FilterOption::default(),
|
filter_option: FilterOption::default(),
|
||||||
danmaku_option: DanmakuOption::default(),
|
danmaku_option: DanmakuOption::default(),
|
||||||
@@ -85,23 +109,35 @@ impl Config {
|
|||||||
Ok(toml::from_str(&config_content)?)
|
Ok(toml::from_str(&config_content)?)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub fn as_video_sources(&self) -> Vec<(Args<'_>, &PathBuf)> {
|
||||||
|
let mut params = Vec::new();
|
||||||
|
self.favorite_list
|
||||||
|
.iter()
|
||||||
|
.for_each(|(fid, path)| params.push((Args::Favorite { fid }, path)));
|
||||||
|
self.collection_list
|
||||||
|
.iter()
|
||||||
|
.for_each(|(collection_item, path)| params.push((Args::Collection { collection_item }, path)));
|
||||||
|
if self.watch_later.enabled {
|
||||||
|
params.push((Args::WatchLater, &self.watch_later.path));
|
||||||
|
}
|
||||||
|
self.submission_list
|
||||||
|
.iter()
|
||||||
|
.for_each(|(upper_id, path)| params.push((Args::Submission { upper_id }, path)));
|
||||||
|
params
|
||||||
|
}
|
||||||
|
|
||||||
#[cfg(not(test))]
|
#[cfg(not(test))]
|
||||||
pub fn check(&self) {
|
pub fn check(&self) {
|
||||||
let mut ok = true;
|
let mut ok = true;
|
||||||
if self.favorite_list.is_empty() && self.collection_list.is_empty() && !self.watch_later.enabled {
|
let video_sources = self.as_video_sources();
|
||||||
|
if video_sources.is_empty() {
|
||||||
ok = false;
|
ok = false;
|
||||||
error!("没有配置任何需要扫描的内容,程序空转没有意义");
|
error!("没有配置任何需要扫描的内容,程序空转没有意义");
|
||||||
}
|
}
|
||||||
if self.watch_later.enabled && !self.watch_later.path.is_absolute() {
|
for (args, path) in video_sources {
|
||||||
error!(
|
|
||||||
"稍后再看保存的路径应为绝对路径,检测到:{}",
|
|
||||||
self.watch_later.path.display()
|
|
||||||
);
|
|
||||||
}
|
|
||||||
for path in self.favorite_list.values() {
|
|
||||||
if !path.is_absolute() {
|
if !path.is_absolute() {
|
||||||
ok = false;
|
ok = false;
|
||||||
error!("收藏夹保存的路径应为绝对路径,检测到: {}", path.display());
|
error!("{:?} 保存的路径应为绝对路径,检测到: {}", args, path.display());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if !self.upper_path.is_absolute() {
|
if !self.upper_path.is_absolute() {
|
||||||
|
|||||||
@@ -8,7 +8,7 @@ fn database_url() -> String {
|
|||||||
format!("sqlite://{}?mode=rwc", CONFIG_DIR.join("data.sqlite").to_string_lossy())
|
format!("sqlite://{}?mode=rwc", CONFIG_DIR.join("data.sqlite").to_string_lossy())
|
||||||
}
|
}
|
||||||
|
|
||||||
pub async fn database_connection() -> Result<DatabaseConnection> {
|
async fn database_connection() -> Result<DatabaseConnection> {
|
||||||
let mut option = ConnectOptions::new(database_url());
|
let mut option = ConnectOptions::new(database_url());
|
||||||
option
|
option
|
||||||
.max_connections(100)
|
.max_connections(100)
|
||||||
@@ -17,9 +17,15 @@ pub async fn database_connection() -> Result<DatabaseConnection> {
|
|||||||
Ok(Database::connect(option).await?)
|
Ok(Database::connect(option).await?)
|
||||||
}
|
}
|
||||||
|
|
||||||
pub async fn migrate_database() -> Result<()> {
|
async fn migrate_database() -> Result<()> {
|
||||||
// 注意此处使用内部构造的 DatabaseConnection,而不是通过 database_connection() 获取
|
// 注意此处使用内部构造的 DatabaseConnection,而不是通过 database_connection() 获取
|
||||||
// 这是因为使用多个连接的 Connection 会导致奇怪的迁移顺序问题,而使用默认的连接选项不会
|
// 这是因为使用多个连接的 Connection 会导致奇怪的迁移顺序问题,而使用默认的连接选项不会
|
||||||
let connection = Database::connect(database_url()).await?;
|
let connection = Database::connect(database_url()).await?;
|
||||||
Ok(Migrator::up(&connection, None).await?)
|
Ok(Migrator::up(&connection, None).await?)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// 进行数据库迁移并获取数据库连接,供外部使用
|
||||||
|
pub async fn setup_database() -> DatabaseConnection {
|
||||||
|
migrate_database().await.expect("数据库迁移失败");
|
||||||
|
database_connection().await.expect("获取数据库连接失败")
|
||||||
|
}
|
||||||
|
|||||||
@@ -1,11 +1,12 @@
|
|||||||
use core::str;
|
use core::str;
|
||||||
use std::path::Path;
|
use std::path::Path;
|
||||||
|
|
||||||
use anyhow::{bail, ensure, Result};
|
use anyhow::{Result, bail, ensure};
|
||||||
use futures::StreamExt;
|
use futures::TryStreamExt;
|
||||||
use reqwest::Method;
|
use reqwest::Method;
|
||||||
use tokio::fs::{self, File};
|
use tokio::fs::{self, File};
|
||||||
use tokio::io::AsyncWriteExt;
|
use tokio::io::AsyncWriteExt;
|
||||||
|
use tokio_util::io::StreamReader;
|
||||||
|
|
||||||
use crate::bilibili::Client;
|
use crate::bilibili::Client;
|
||||||
pub struct Downloader {
|
pub struct Downloader {
|
||||||
@@ -25,15 +26,15 @@ impl Downloader {
|
|||||||
fs::create_dir_all(parent).await?;
|
fs::create_dir_all(parent).await?;
|
||||||
}
|
}
|
||||||
let mut file = File::create(path).await?;
|
let mut file = File::create(path).await?;
|
||||||
let resp = self.client.request(Method::GET, url, None).send().await?;
|
let resp = self
|
||||||
|
.client
|
||||||
|
.request(Method::GET, url, None)
|
||||||
|
.send()
|
||||||
|
.await?
|
||||||
|
.error_for_status()?;
|
||||||
let expected = resp.content_length().unwrap_or_default();
|
let expected = resp.content_length().unwrap_or_default();
|
||||||
let mut received = 0u64;
|
let mut stream_reader = StreamReader::new(resp.bytes_stream().map_err(std::io::Error::other));
|
||||||
let mut stream = resp.bytes_stream();
|
let received = tokio::io::copy(&mut stream_reader, &mut file).await?;
|
||||||
while let Some(bytes) = stream.next().await {
|
|
||||||
let bytes = bytes?;
|
|
||||||
received += bytes.len() as u64;
|
|
||||||
file.write_all(&bytes).await?;
|
|
||||||
}
|
|
||||||
file.flush().await?;
|
file.flush().await?;
|
||||||
ensure!(
|
ensure!(
|
||||||
received >= expected,
|
received >= expected,
|
||||||
|
|||||||
@@ -1,3 +1,6 @@
|
|||||||
|
use std::io;
|
||||||
|
|
||||||
|
use anyhow::Result;
|
||||||
use thiserror::Error;
|
use thiserror::Error;
|
||||||
|
|
||||||
#[derive(Error, Debug)]
|
#[derive(Error, Debug)]
|
||||||
@@ -7,3 +10,41 @@ pub struct DownloadAbortError();
|
|||||||
#[derive(Error, Debug)]
|
#[derive(Error, Debug)]
|
||||||
#[error("Process page error")]
|
#[error("Process page error")]
|
||||||
pub struct ProcessPageError();
|
pub struct ProcessPageError();
|
||||||
|
|
||||||
|
pub enum ExecutionStatus {
|
||||||
|
Skipped,
|
||||||
|
Succeeded,
|
||||||
|
Ignored(anyhow::Error),
|
||||||
|
Failed(anyhow::Error),
|
||||||
|
// 任务可以返回该状态固定自己的 status
|
||||||
|
FixedFailed(u32, anyhow::Error),
|
||||||
|
}
|
||||||
|
|
||||||
|
// 目前 stable rust 似乎不支持自定义类型使用 ? 运算符,只能先在返回值使用 Result,再这样套层娃
|
||||||
|
impl From<Result<ExecutionStatus>> for ExecutionStatus {
|
||||||
|
fn from(res: Result<ExecutionStatus>) -> Self {
|
||||||
|
match res {
|
||||||
|
Ok(status) => status,
|
||||||
|
Err(err) => {
|
||||||
|
if let Some(error) = err.downcast_ref::<io::Error>() {
|
||||||
|
let error_kind = error.kind();
|
||||||
|
if error_kind == io::ErrorKind::PermissionDenied
|
||||||
|
|| (error_kind == io::ErrorKind::Other
|
||||||
|
&& error.get_ref().is_some_and(|e| {
|
||||||
|
e.downcast_ref::<reqwest::Error>()
|
||||||
|
.is_some_and(|e| e.is_decode() || e.is_body() || e.is_timeout())
|
||||||
|
}))
|
||||||
|
{
|
||||||
|
return ExecutionStatus::Ignored(err);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if let Some(error) = err.downcast_ref::<reqwest::Error>() {
|
||||||
|
if error.is_decode() || error.is_body() || error.is_timeout() {
|
||||||
|
return ExecutionStatus::Ignored(err);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
ExecutionStatus::Failed(err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@@ -2,82 +2,81 @@
|
|||||||
extern crate tracing;
|
extern crate tracing;
|
||||||
|
|
||||||
mod adapter;
|
mod adapter;
|
||||||
|
mod api;
|
||||||
mod bilibili;
|
mod bilibili;
|
||||||
mod config;
|
mod config;
|
||||||
mod database;
|
mod database;
|
||||||
mod downloader;
|
mod downloader;
|
||||||
mod error;
|
mod error;
|
||||||
|
mod task;
|
||||||
mod utils;
|
mod utils;
|
||||||
mod workflow;
|
mod workflow;
|
||||||
|
|
||||||
use std::path::PathBuf;
|
use std::fmt::Debug;
|
||||||
|
use std::future::Future;
|
||||||
|
use std::sync::Arc;
|
||||||
|
|
||||||
use once_cell::sync::Lazy;
|
use once_cell::sync::Lazy;
|
||||||
use tokio::time;
|
use task::{http_server, video_downloader};
|
||||||
|
use tokio_util::sync::CancellationToken;
|
||||||
|
use tokio_util::task::TaskTracker;
|
||||||
|
|
||||||
use crate::adapter::Args;
|
|
||||||
use crate::bilibili::BiliClient;
|
|
||||||
use crate::config::{ARGS, CONFIG};
|
use crate::config::{ARGS, CONFIG};
|
||||||
use crate::database::{database_connection, migrate_database};
|
use crate::database::setup_database;
|
||||||
use crate::utils::init_logger;
|
use crate::utils::init_logger;
|
||||||
use crate::workflow::process_video_list;
|
use crate::utils::signal::terminate;
|
||||||
|
|
||||||
#[tokio::main]
|
#[tokio::main]
|
||||||
async fn main() {
|
async fn main() {
|
||||||
init_logger(&ARGS.log_level);
|
init();
|
||||||
Lazy::force(&CONFIG);
|
let connection = Arc::new(setup_database().await);
|
||||||
migrate_database().await.expect("数据库迁移失败");
|
let token = CancellationToken::new();
|
||||||
let connection = database_connection().await.expect("获取数据库连接失败");
|
let tracker = TaskTracker::new();
|
||||||
let mut anchor = chrono::Local::now().date_naive();
|
|
||||||
let bili_client = BiliClient::new();
|
spawn_task("HTTP 服务", http_server(connection.clone()), &tracker, token.clone());
|
||||||
let params = build_params();
|
spawn_task("定时下载", video_downloader(connection), &tracker, token.clone());
|
||||||
loop {
|
|
||||||
'inner: {
|
tracker.close();
|
||||||
match bili_client.wbi_img().await.map(|wbi_img| wbi_img.into()) {
|
handle_shutdown(tracker, token).await
|
||||||
Ok(Some(mixin_key)) => bilibili::set_global_mixin_key(mixin_key),
|
|
||||||
Ok(_) => {
|
|
||||||
error!("解析 mixin key 失败,等待下一轮执行");
|
|
||||||
break 'inner;
|
|
||||||
}
|
|
||||||
Err(e) => {
|
|
||||||
error!("获取 mixin key 遇到错误:{e},等待下一轮执行");
|
|
||||||
break 'inner;
|
|
||||||
}
|
|
||||||
};
|
|
||||||
if anchor != chrono::Local::now().date_naive() {
|
|
||||||
if let Err(e) = bili_client.check_refresh().await {
|
|
||||||
error!("检查刷新 Credential 遇到错误:{e},等待下一轮执行");
|
|
||||||
break 'inner;
|
|
||||||
}
|
|
||||||
anchor = chrono::Local::now().date_naive();
|
|
||||||
}
|
|
||||||
for (args, path) in ¶ms {
|
|
||||||
if let Err(e) = process_video_list(*args, &bili_client, path, &connection).await {
|
|
||||||
error!("处理过程遇到错误:{e}");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
info!("本轮任务执行完毕,等待下一轮执行");
|
|
||||||
}
|
|
||||||
time::sleep(time::Duration::from_secs(CONFIG.interval)).await;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
fn build_params() -> Vec<(Args<'static>, &'static PathBuf)> {
|
fn spawn_task(
|
||||||
let mut params = Vec::new();
|
task_name: &'static str,
|
||||||
CONFIG
|
task: impl Future<Output = impl Debug> + Send + 'static,
|
||||||
.favorite_list
|
tracker: &TaskTracker,
|
||||||
.iter()
|
token: CancellationToken,
|
||||||
.for_each(|(fid, path)| params.push((Args::Favorite { fid }, path)));
|
) {
|
||||||
CONFIG
|
tracker.spawn(async move {
|
||||||
.collection_list
|
tokio::select! {
|
||||||
.iter()
|
res = task => {
|
||||||
.for_each(|(collection_item, path)| params.push((Args::Collection { collection_item }, path)));
|
error!("「{}」异常结束,返回结果为:「{:?}」,取消其它仍在执行的任务..", task_name, res);
|
||||||
if CONFIG.watch_later.enabled {
|
token.cancel();
|
||||||
params.push((Args::WatchLater, &CONFIG.watch_later.path));
|
},
|
||||||
}
|
_ = token.cancelled() => {
|
||||||
CONFIG
|
info!("「{}」接收到取消信号,终止运行..", task_name);
|
||||||
.submission_list
|
}
|
||||||
.iter()
|
}
|
||||||
.for_each(|(upper_id, path)| params.push((Args::Submission { upper_id }, path)));
|
});
|
||||||
params
|
}
|
||||||
|
|
||||||
|
/// 初始化日志系统,打印欢迎信息,加载配置文件
|
||||||
|
fn init() {
|
||||||
|
init_logger(&ARGS.log_level);
|
||||||
|
info!("欢迎使用 Bili-Sync,当前程序版本:{}", config::version());
|
||||||
|
info!("项目地址:https://github.com/amtoaer/bili-sync");
|
||||||
|
Lazy::force(&CONFIG);
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn handle_shutdown(tracker: TaskTracker, token: CancellationToken) {
|
||||||
|
tokio::select! {
|
||||||
|
_ = tracker.wait() => {
|
||||||
|
error!("所有任务均已终止,程序退出")
|
||||||
|
}
|
||||||
|
_ = terminate() => {
|
||||||
|
info!("接收到终止信号,正在终止任务..");
|
||||||
|
token.cancel();
|
||||||
|
tracker.wait().await;
|
||||||
|
info!("所有任务均已终止,程序退出");
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
61
crates/bili_sync/src/task/http_server.rs
Normal file
61
crates/bili_sync/src/task/http_server.rs
Normal file
@@ -0,0 +1,61 @@
|
|||||||
|
use std::sync::Arc;
|
||||||
|
|
||||||
|
use anyhow::{Context, Result};
|
||||||
|
use axum::extract::Request;
|
||||||
|
use axum::http::{Uri, header};
|
||||||
|
use axum::response::IntoResponse;
|
||||||
|
use axum::routing::{get, post};
|
||||||
|
use axum::{Extension, Router, ServiceExt, middleware};
|
||||||
|
use reqwest::StatusCode;
|
||||||
|
use rust_embed::Embed;
|
||||||
|
use sea_orm::DatabaseConnection;
|
||||||
|
use utoipa::OpenApi;
|
||||||
|
use utoipa_swagger_ui::{Config, SwaggerUi};
|
||||||
|
|
||||||
|
use crate::api::auth;
|
||||||
|
use crate::api::handler::{ApiDoc, get_video, get_video_sources, get_videos, reset_video};
|
||||||
|
use crate::config::CONFIG;
|
||||||
|
|
||||||
|
#[derive(Embed)]
|
||||||
|
#[folder = "../../web/build"]
|
||||||
|
struct Asset;
|
||||||
|
|
||||||
|
pub async fn http_server(database_connection: Arc<DatabaseConnection>) -> Result<()> {
|
||||||
|
let app = Router::new()
|
||||||
|
.route("/api/video-sources", get(get_video_sources))
|
||||||
|
.route("/api/videos", get(get_videos))
|
||||||
|
.route("/api/videos/{id}", get(get_video))
|
||||||
|
.route("/api/videos/{id}/reset", post(reset_video))
|
||||||
|
.merge(
|
||||||
|
SwaggerUi::new("/swagger-ui/")
|
||||||
|
.url("/api-docs/openapi.json", ApiDoc::openapi())
|
||||||
|
.config(
|
||||||
|
Config::default()
|
||||||
|
.try_it_out_enabled(true)
|
||||||
|
.persist_authorization(true)
|
||||||
|
.validator_url("none"),
|
||||||
|
),
|
||||||
|
)
|
||||||
|
.fallback_service(get(frontend_files))
|
||||||
|
.layer(Extension(database_connection))
|
||||||
|
.layer(middleware::from_fn(auth::auth));
|
||||||
|
let listener = tokio::net::TcpListener::bind(&CONFIG.bind_address)
|
||||||
|
.await
|
||||||
|
.context("bind address failed")?;
|
||||||
|
info!("开始运行管理页: http://{}", CONFIG.bind_address);
|
||||||
|
Ok(axum::serve(listener, ServiceExt::<Request>::into_make_service(app)).await?)
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn frontend_files(uri: Uri) -> impl IntoResponse {
|
||||||
|
let mut path = uri.path().trim_start_matches('/');
|
||||||
|
if path.is_empty() {
|
||||||
|
path = "index.html";
|
||||||
|
}
|
||||||
|
match Asset::get(path) {
|
||||||
|
Some(content) => {
|
||||||
|
let mime = mime_guess::from_path(path).first_or_octet_stream();
|
||||||
|
([(header::CONTENT_TYPE, mime.as_ref())], content.data).into_response()
|
||||||
|
}
|
||||||
|
None => (StatusCode::NOT_FOUND, "404 Not Found").into_response(),
|
||||||
|
}
|
||||||
|
}
|
||||||
5
crates/bili_sync/src/task/mod.rs
Normal file
5
crates/bili_sync/src/task/mod.rs
Normal file
@@ -0,0 +1,5 @@
|
|||||||
|
mod http_server;
|
||||||
|
mod video_downloader;
|
||||||
|
|
||||||
|
pub use http_server::http_server;
|
||||||
|
pub use video_downloader::video_downloader;
|
||||||
45
crates/bili_sync/src/task/video_downloader.rs
Normal file
45
crates/bili_sync/src/task/video_downloader.rs
Normal file
@@ -0,0 +1,45 @@
|
|||||||
|
use std::sync::Arc;
|
||||||
|
|
||||||
|
use sea_orm::DatabaseConnection;
|
||||||
|
use tokio::time;
|
||||||
|
|
||||||
|
use crate::bilibili::{self, BiliClient};
|
||||||
|
use crate::config::CONFIG;
|
||||||
|
use crate::workflow::process_video_source;
|
||||||
|
|
||||||
|
/// 启动周期下载视频的任务
|
||||||
|
pub async fn video_downloader(connection: Arc<DatabaseConnection>) {
|
||||||
|
let mut anchor = chrono::Local::now().date_naive();
|
||||||
|
let bili_client = BiliClient::new();
|
||||||
|
let video_sources = CONFIG.as_video_sources();
|
||||||
|
loop {
|
||||||
|
info!("开始执行本轮视频下载任务..");
|
||||||
|
'inner: {
|
||||||
|
match bili_client.wbi_img().await.map(|wbi_img| wbi_img.into()) {
|
||||||
|
Ok(Some(mixin_key)) => bilibili::set_global_mixin_key(mixin_key),
|
||||||
|
Ok(_) => {
|
||||||
|
error!("解析 mixin key 失败,等待下一轮执行");
|
||||||
|
break 'inner;
|
||||||
|
}
|
||||||
|
Err(e) => {
|
||||||
|
error!("获取 mixin key 遇到错误:{:#},等待下一轮执行", e);
|
||||||
|
break 'inner;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
if anchor != chrono::Local::now().date_naive() {
|
||||||
|
if let Err(e) = bili_client.check_refresh().await {
|
||||||
|
error!("检查刷新 Credential 遇到错误:{:#},等待下一轮执行", e);
|
||||||
|
break 'inner;
|
||||||
|
}
|
||||||
|
anchor = chrono::Local::now().date_naive();
|
||||||
|
}
|
||||||
|
for (args, path) in &video_sources {
|
||||||
|
if let Err(e) = process_video_source(*args, &bili_client, path, &connection).await {
|
||||||
|
error!("处理过程遇到错误:{:#}", e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
info!("本轮任务执行完毕,等待下一轮执行");
|
||||||
|
}
|
||||||
|
time::sleep(time::Duration::from_secs(CONFIG.interval)).await;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -3,6 +3,7 @@ pub mod filenamify;
|
|||||||
pub mod format_arg;
|
pub mod format_arg;
|
||||||
pub mod model;
|
pub mod model;
|
||||||
pub mod nfo;
|
pub mod nfo;
|
||||||
|
pub mod signal;
|
||||||
pub mod status;
|
pub mod status;
|
||||||
|
|
||||||
use tracing_subscriber::util::SubscriberInitExt;
|
use tracing_subscriber::util::SubscriberInitExt;
|
||||||
|
|||||||
@@ -1,10 +1,10 @@
|
|||||||
use anyhow::{Context, Result};
|
use anyhow::{Context, Result};
|
||||||
use bili_sync_entity::*;
|
use bili_sync_entity::*;
|
||||||
|
use sea_orm::DatabaseTransaction;
|
||||||
use sea_orm::entity::prelude::*;
|
use sea_orm::entity::prelude::*;
|
||||||
use sea_orm::sea_query::{OnConflict, SimpleExpr};
|
use sea_orm::sea_query::{OnConflict, SimpleExpr};
|
||||||
use sea_orm::DatabaseTransaction;
|
|
||||||
|
|
||||||
use crate::adapter::{VideoListModel, VideoListModelEnum};
|
use crate::adapter::{VideoSource, VideoSourceEnum};
|
||||||
use crate::bilibili::{PageInfo, VideoInfo};
|
use crate::bilibili::{PageInfo, VideoInfo};
|
||||||
use crate::utils::status::STATUS_COMPLETED;
|
use crate::utils::status::STATUS_COMPLETED;
|
||||||
|
|
||||||
@@ -50,14 +50,14 @@ pub async fn filter_unhandled_video_pages(
|
|||||||
/// 尝试创建 Video Model,如果发生冲突则忽略
|
/// 尝试创建 Video Model,如果发生冲突则忽略
|
||||||
pub async fn create_videos(
|
pub async fn create_videos(
|
||||||
videos_info: Vec<VideoInfo>,
|
videos_info: Vec<VideoInfo>,
|
||||||
video_list_model: &VideoListModelEnum,
|
video_source: &VideoSourceEnum,
|
||||||
connection: &DatabaseConnection,
|
connection: &DatabaseConnection,
|
||||||
) -> Result<()> {
|
) -> Result<()> {
|
||||||
let video_models = videos_info
|
let video_models = videos_info
|
||||||
.into_iter()
|
.into_iter()
|
||||||
.map(|v| {
|
.map(|v| {
|
||||||
let mut model = v.into_simple_model();
|
let mut model = v.into_simple_model();
|
||||||
video_list_model.set_relation_id(&mut model);
|
video_source.set_relation_id(&mut model);
|
||||||
model
|
model
|
||||||
})
|
})
|
||||||
.collect::<Vec<_>>();
|
.collect::<Vec<_>>();
|
||||||
|
|||||||
@@ -1,8 +1,8 @@
|
|||||||
use anyhow::Result;
|
use anyhow::Result;
|
||||||
use bili_sync_entity::*;
|
use bili_sync_entity::*;
|
||||||
|
use quick_xml::Error;
|
||||||
use quick_xml::events::{BytesCData, BytesText};
|
use quick_xml::events::{BytesCData, BytesText};
|
||||||
use quick_xml::writer::Writer;
|
use quick_xml::writer::Writer;
|
||||||
use quick_xml::Error;
|
|
||||||
use tokio::io::AsyncWriteExt;
|
use tokio::io::AsyncWriteExt;
|
||||||
|
|
||||||
use crate::config::NFOTimeType;
|
use crate::config::NFOTimeType;
|
||||||
@@ -43,7 +43,7 @@ impl NFOSerializer<'_> {
|
|||||||
.write_inner_content_async::<_, _, Error>(|writer| async move {
|
.write_inner_content_async::<_, _, Error>(|writer| async move {
|
||||||
writer
|
writer
|
||||||
.create_element("plot")
|
.create_element("plot")
|
||||||
.write_cdata_content_async(BytesCData::new(&v.intro))
|
.write_cdata_content_async(BytesCData::new(Self::format_plot(v)))
|
||||||
.await?;
|
.await?;
|
||||||
writer.create_element("outline").write_empty_async().await?;
|
writer.create_element("outline").write_empty_async().await?;
|
||||||
writer
|
writer
|
||||||
@@ -100,7 +100,7 @@ impl NFOSerializer<'_> {
|
|||||||
.write_inner_content_async::<_, _, Error>(|writer| async move {
|
.write_inner_content_async::<_, _, Error>(|writer| async move {
|
||||||
writer
|
writer
|
||||||
.create_element("plot")
|
.create_element("plot")
|
||||||
.write_cdata_content_async(BytesCData::new(&v.intro))
|
.write_cdata_content_async(BytesCData::new(Self::format_plot(v)))
|
||||||
.await?;
|
.await?;
|
||||||
writer.create_element("outline").write_empty_async().await?;
|
writer.create_element("outline").write_empty_async().await?;
|
||||||
writer
|
writer
|
||||||
@@ -202,6 +202,14 @@ impl NFOSerializer<'_> {
|
|||||||
tokio_buffer.flush().await?;
|
tokio_buffer.flush().await?;
|
||||||
Ok(String::from_utf8(buffer)?)
|
Ok(String::from_utf8(buffer)?)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[inline]
|
||||||
|
fn format_plot(model: &video::Model) -> String {
|
||||||
|
format!(
|
||||||
|
r#"原始视频:<a href="https://www.bilibili.com/video/{}/">{}</a><br/><br/>{}"#,
|
||||||
|
model.bvid, model.bvid, model.intro
|
||||||
|
)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
@@ -223,7 +231,7 @@ mod tests {
|
|||||||
chrono::NaiveDate::from_ymd_opt(2033, 3, 3).unwrap(),
|
chrono::NaiveDate::from_ymd_opt(2033, 3, 3).unwrap(),
|
||||||
chrono::NaiveTime::from_hms_opt(3, 3, 3).unwrap(),
|
chrono::NaiveTime::from_hms_opt(3, 3, 3).unwrap(),
|
||||||
),
|
),
|
||||||
bvid: "bvid".to_string(),
|
bvid: "BV1nWcSeeEkV".to_string(),
|
||||||
tags: Some(serde_json::json!(["tag1", "tag2"])),
|
tags: Some(serde_json::json!(["tag1", "tag2"])),
|
||||||
..Default::default()
|
..Default::default()
|
||||||
};
|
};
|
||||||
@@ -234,7 +242,7 @@ mod tests {
|
|||||||
.unwrap(),
|
.unwrap(),
|
||||||
r#"<?xml version="1.0" encoding="utf-8" standalone="yes"?>
|
r#"<?xml version="1.0" encoding="utf-8" standalone="yes"?>
|
||||||
<movie>
|
<movie>
|
||||||
<plot><![CDATA[intro]]></plot>
|
<plot><![CDATA[原始视频:<a href="https://www.bilibili.com/video/BV1nWcSeeEkV/">BV1nWcSeeEkV</a><br/><br/>intro]]></plot>
|
||||||
<outline/>
|
<outline/>
|
||||||
<title>name</title>
|
<title>name</title>
|
||||||
<actor>
|
<actor>
|
||||||
@@ -244,7 +252,7 @@ mod tests {
|
|||||||
<year>2033</year>
|
<year>2033</year>
|
||||||
<genre>tag1</genre>
|
<genre>tag1</genre>
|
||||||
<genre>tag2</genre>
|
<genre>tag2</genre>
|
||||||
<uniqueid type="bilibili">bvid</uniqueid>
|
<uniqueid type="bilibili">BV1nWcSeeEkV</uniqueid>
|
||||||
<aired>2033-03-03</aired>
|
<aired>2033-03-03</aired>
|
||||||
</movie>"#,
|
</movie>"#,
|
||||||
);
|
);
|
||||||
@@ -255,7 +263,7 @@ mod tests {
|
|||||||
.unwrap(),
|
.unwrap(),
|
||||||
r#"<?xml version="1.0" encoding="utf-8" standalone="yes"?>
|
r#"<?xml version="1.0" encoding="utf-8" standalone="yes"?>
|
||||||
<tvshow>
|
<tvshow>
|
||||||
<plot><![CDATA[intro]]></plot>
|
<plot><![CDATA[原始视频:<a href="https://www.bilibili.com/video/BV1nWcSeeEkV/">BV1nWcSeeEkV</a><br/><br/>intro]]></plot>
|
||||||
<outline/>
|
<outline/>
|
||||||
<title>name</title>
|
<title>name</title>
|
||||||
<actor>
|
<actor>
|
||||||
@@ -265,7 +273,7 @@ mod tests {
|
|||||||
<year>2022</year>
|
<year>2022</year>
|
||||||
<genre>tag1</genre>
|
<genre>tag1</genre>
|
||||||
<genre>tag2</genre>
|
<genre>tag2</genre>
|
||||||
<uniqueid type="bilibili">bvid</uniqueid>
|
<uniqueid type="bilibili">BV1nWcSeeEkV</uniqueid>
|
||||||
<aired>2022-02-02</aired>
|
<aired>2022-02-02</aired>
|
||||||
</tvshow>"#,
|
</tvshow>"#,
|
||||||
);
|
);
|
||||||
|
|||||||
21
crates/bili_sync/src/utils/signal.rs
Normal file
21
crates/bili_sync/src/utils/signal.rs
Normal file
@@ -0,0 +1,21 @@
|
|||||||
|
use std::io;
|
||||||
|
|
||||||
|
use tokio::signal;
|
||||||
|
|
||||||
|
#[cfg(target_family = "windows")]
|
||||||
|
pub async fn terminate() -> io::Result<()> {
|
||||||
|
signal::ctrl_c().await
|
||||||
|
}
|
||||||
|
|
||||||
|
/// ctrl + c 发送的是 SIGINT 信号,docker stop 发送的是 SIGTERM 信号,都需要处理
|
||||||
|
#[cfg(target_family = "unix")]
|
||||||
|
pub async fn terminate() -> io::Result<()> {
|
||||||
|
use tokio::select;
|
||||||
|
|
||||||
|
let mut term = signal::unix::signal(signal::unix::SignalKind::terminate())?;
|
||||||
|
let mut int = signal::unix::signal(signal::unix::SignalKind::interrupt())?;
|
||||||
|
select! {
|
||||||
|
_ = term.recv() => Ok(()),
|
||||||
|
_ = int.recv() => Ok(()),
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,7 +1,7 @@
|
|||||||
use anyhow::Result;
|
use crate::error::ExecutionStatus;
|
||||||
|
|
||||||
static STATUS_MAX_RETRY: u32 = 0b100;
|
pub(super) static STATUS_MAX_RETRY: u32 = 0b100;
|
||||||
static STATUS_OK: u32 = 0b111;
|
pub static STATUS_OK: u32 = 0b111;
|
||||||
pub static STATUS_COMPLETED: u32 = 1 << 31;
|
pub static STATUS_COMPLETED: u32 = 1 << 31;
|
||||||
|
|
||||||
/// 用来表示下载的状态,不想写太多列了,所以仅使用一个 u32 表示。
|
/// 用来表示下载的状态,不想写太多列了,所以仅使用一个 u32 表示。
|
||||||
@@ -10,12 +10,68 @@ pub static STATUS_COMPLETED: u32 = 1 << 31;
|
|||||||
/// 如果子任务执行成功,将状态设置为 0b111,该值定义为 STATUS_OK。
|
/// 如果子任务执行成功,将状态设置为 0b111,该值定义为 STATUS_OK。
|
||||||
/// 子任务达到最大失败次数或者执行成功时,认为该子任务已经完成。
|
/// 子任务达到最大失败次数或者执行成功时,认为该子任务已经完成。
|
||||||
/// 当所有子任务都已经完成时,为最高位打上标记 1,表示整个下载任务已经完成。
|
/// 当所有子任务都已经完成时,为最高位打上标记 1,表示整个下载任务已经完成。
|
||||||
#[derive(Clone)]
|
#[derive(Clone, Copy, Default)]
|
||||||
pub struct Status(u32);
|
pub struct Status<const N: usize>(u32);
|
||||||
|
|
||||||
impl Status {
|
impl<const N: usize> Status<N> {
|
||||||
fn new(status: u32) -> Self {
|
// 获取最高位的完成标记
|
||||||
Self(status)
|
pub fn get_completed(&self) -> bool {
|
||||||
|
self.0 >> 31 == 1
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 依次检查所有子任务是否还应该继续执行,返回一个 bool 数组
|
||||||
|
pub fn should_run(&self) -> [bool; N] {
|
||||||
|
let mut result = [false; N];
|
||||||
|
for (i, item) in result.iter_mut().enumerate() {
|
||||||
|
*item = self.check_continue(i);
|
||||||
|
}
|
||||||
|
result
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 重置所有失败的状态,将状态设置为 0b000,返回值表示 status 是否发生了变化
|
||||||
|
pub fn reset_failed(&mut self) -> bool {
|
||||||
|
let mut changed = false;
|
||||||
|
for i in 0..N {
|
||||||
|
let status = self.get_status(i);
|
||||||
|
if !(status < STATUS_MAX_RETRY || status == STATUS_OK) {
|
||||||
|
self.set_status(i, 0);
|
||||||
|
changed = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// 理论上 changed 可以直接从上面的循环中得到,因为 completed 标志位的改变是由子任务状态的改变引起的,子任务没有改变则 completed 也不会改变
|
||||||
|
// 但考虑特殊情况,新版本引入了一个新的子任务项,此时会出现明明有子任务未执行,但 completed 标记位仍然为 true 的情况
|
||||||
|
// 当然可以在新版本迁移文件中全局重置 completed 标记位,但这样影响范围太大感觉不太好
|
||||||
|
// 在后面进行这部分额外判断可以兼容这种情况,在由用户手动触发的 reset_failed 调用中修正 completed 标记位
|
||||||
|
if self.should_run().into_iter().any(|x| x) {
|
||||||
|
changed |= self.get_completed();
|
||||||
|
self.set_completed(false);
|
||||||
|
}
|
||||||
|
changed
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 覆盖某个子任务的状态
|
||||||
|
pub fn set(&mut self, offset: usize, status: u32) {
|
||||||
|
assert!(status < 0b1000, "status should be less than 0b1000");
|
||||||
|
self.set_status(offset, status);
|
||||||
|
if self.should_run().into_iter().all(|x| !x) {
|
||||||
|
self.set_completed(true);
|
||||||
|
} else {
|
||||||
|
self.set_completed(false);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 根据任务结果更新状态,任务结果是一个 Result 数组,需要与子任务一一对应
|
||||||
|
/// 如果所有子任务都已经完成,那么打上最高位的完成标记
|
||||||
|
pub fn update_status(&mut self, result: &[ExecutionStatus]) {
|
||||||
|
assert!(result.len() == N, "result length should be equal to N");
|
||||||
|
for (i, res) in result.iter().enumerate() {
|
||||||
|
self.set_result(res, i);
|
||||||
|
}
|
||||||
|
if self.should_run().into_iter().all(|x| !x) {
|
||||||
|
self.set_completed(true);
|
||||||
|
} else {
|
||||||
|
self.set_completed(false);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// 设置最高位的完成标记
|
/// 设置最高位的完成标记
|
||||||
@@ -27,15 +83,14 @@ impl Status {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// 获取最高位的完成标记
|
|
||||||
fn get_completed(&self) -> bool {
|
|
||||||
self.0 >> 31 == 1
|
|
||||||
}
|
|
||||||
|
|
||||||
/// 获取某个子任务的状态
|
/// 获取某个子任务的状态
|
||||||
fn get_status(&self, offset: usize) -> u32 {
|
fn get_status(&self, offset: usize) -> u32 {
|
||||||
let helper = !0u32;
|
(self.0 >> (offset * 3)) & 0b111
|
||||||
(self.0 & (helper << (offset * 3)) & (helper >> (32 - 3 * offset - 3))) >> (offset * 3)
|
}
|
||||||
|
|
||||||
|
/// 设置某个子任务的状态
|
||||||
|
fn set_status(&mut self, offset: usize, status: u32) {
|
||||||
|
self.0 = (self.0 & !(0b111 << (offset * 3))) | (status << (offset * 3));
|
||||||
}
|
}
|
||||||
|
|
||||||
// 将某个子任务的状态加一(在任务失败时使用)
|
// 将某个子任务的状态加一(在任务失败时使用)
|
||||||
@@ -53,95 +108,63 @@ impl Status {
|
|||||||
self.get_status(offset) < STATUS_MAX_RETRY
|
self.get_status(offset) < STATUS_MAX_RETRY
|
||||||
}
|
}
|
||||||
|
|
||||||
/// 依次检查所有子任务是否还应该继续执行,返回一个 bool 数组
|
|
||||||
fn should_run(&self, size: usize) -> Vec<bool> {
|
|
||||||
(0..size).map(|x| self.check_continue(x)).collect()
|
|
||||||
}
|
|
||||||
|
|
||||||
/// 根据子任务执行结果更新子任务的状态
|
/// 根据子任务执行结果更新子任务的状态
|
||||||
/// 如果 Result 是 Ok,那么认为任务执行成功,将状态设置为 STATUS_OK
|
fn set_result(&mut self, result: &ExecutionStatus, offset: usize) {
|
||||||
/// 如果 Result 是 Err,那么认为任务执行失败,将状态加一
|
// 如果任务返回 FixedFailed 状态,那么无论之前的状态如何,都将状态设置为 FixedFailed 的状态
|
||||||
fn set_result(&mut self, result: &Result<()>, offset: usize) {
|
if let ExecutionStatus::FixedFailed(status, _) = result {
|
||||||
if self.get_status(offset) < STATUS_MAX_RETRY {
|
assert!(*status < 0b1000, "status should be less than 0b1000");
|
||||||
|
self.set_status(offset, *status);
|
||||||
|
} else if self.get_status(offset) < STATUS_MAX_RETRY {
|
||||||
match result {
|
match result {
|
||||||
Ok(_) => self.set_ok(offset),
|
ExecutionStatus::Succeeded | ExecutionStatus::Skipped => self.set_ok(offset),
|
||||||
Err(_) => self.plus_one(offset),
|
ExecutionStatus::Failed(_) => self.plus_one(offset),
|
||||||
|
_ => {}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// 根据任务结果更新状态,任务结果是一个 Result 数组,需要与子任务一一对应
|
|
||||||
/// 如果所有子任务都已经完成,那么打上最高位的完成标记
|
|
||||||
fn update_status(&mut self, result: &[Result<()>]) {
|
|
||||||
for (i, res) in result.iter().enumerate() {
|
|
||||||
self.set_result(res, i);
|
|
||||||
}
|
|
||||||
if self.should_run(result.len()).iter().all(|x| !x) {
|
|
||||||
// 所有任务都成功或者由于尝试次数过多失败,为 status 最高位打上标记,将来不再重试
|
|
||||||
self.set_completed(true)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
impl From<Status> for u32 {
|
impl<const N: usize> From<u32> for Status<N> {
|
||||||
fn from(status: Status) -> Self {
|
fn from(status: u32) -> Self {
|
||||||
|
Status(status)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<const N: usize> From<Status<N>> for u32 {
|
||||||
|
fn from(status: Status<N>) -> Self {
|
||||||
status.0
|
status.0
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
impl<const N: usize> From<Status<N>> for [u32; N] {
|
||||||
|
fn from(status: Status<N>) -> Self {
|
||||||
|
let mut result = [0; N];
|
||||||
|
for (i, item) in result.iter_mut().enumerate() {
|
||||||
|
*item = status.get_status(i);
|
||||||
|
}
|
||||||
|
result
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<const N: usize> From<[u32; N]> for Status<N> {
|
||||||
|
fn from(status: [u32; N]) -> Self {
|
||||||
|
let mut result = Status::<N>::default();
|
||||||
|
for (i, item) in status.iter().enumerate() {
|
||||||
|
assert!(*item < 0b1000, "status should be less than 0b1000");
|
||||||
|
result.set_status(i, *item);
|
||||||
|
}
|
||||||
|
if result.should_run().iter().all(|x| !x) {
|
||||||
|
result.set_completed(true);
|
||||||
|
}
|
||||||
|
result
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/// 包含五个子任务,从前到后依次是:视频封面、视频信息、Up 主头像、Up 主信息、分 P 下载
|
/// 包含五个子任务,从前到后依次是:视频封面、视频信息、Up 主头像、Up 主信息、分 P 下载
|
||||||
#[derive(Clone)]
|
pub type VideoStatus = Status<5>;
|
||||||
pub struct VideoStatus(Status);
|
|
||||||
|
|
||||||
impl VideoStatus {
|
/// 包含五个子任务,从前到后分别是:视频封面、视频内容、视频信息、视频弹幕、视频字幕
|
||||||
pub fn new(status: u32) -> Self {
|
pub type PageStatus = Status<5>;
|
||||||
Self(Status::new(status))
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn should_run(&self) -> Vec<bool> {
|
|
||||||
self.0.should_run(5)
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn update_status(&mut self, result: &[Result<()>]) {
|
|
||||||
assert!(result.len() == 5, "VideoStatus should have 5 status");
|
|
||||||
self.0.update_status(result)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl From<VideoStatus> for u32 {
|
|
||||||
fn from(status: VideoStatus) -> Self {
|
|
||||||
status.0.into()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// 包含四个子任务,从前到后分别是:视频封面、视频内容、视频信息、视频弹幕
|
|
||||||
#[derive(Clone)]
|
|
||||||
pub struct PageStatus(Status);
|
|
||||||
|
|
||||||
impl PageStatus {
|
|
||||||
pub fn new(status: u32) -> Self {
|
|
||||||
Self(Status::new(status))
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn should_run(&self) -> Vec<bool> {
|
|
||||||
self.0.should_run(4)
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn update_status(&mut self, result: &[Result<()>]) {
|
|
||||||
assert!(result.len() == 4, "PageStatus should have 4 status");
|
|
||||||
self.0.update_status(result)
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn get_completed(&self) -> bool {
|
|
||||||
self.0.get_completed()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl From<PageStatus> for u32 {
|
|
||||||
fn from(status: PageStatus) -> Self {
|
|
||||||
status.0.into()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
mod test {
|
mod test {
|
||||||
@@ -150,16 +173,90 @@ mod test {
|
|||||||
use super::*;
|
use super::*;
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn test_status() {
|
fn test_status_update() {
|
||||||
let mut status = Status::new(0);
|
let mut status = Status::<3>::default();
|
||||||
assert_eq!(status.should_run(3), vec![true, true, true]);
|
assert_eq!(status.should_run(), [true, true, true]);
|
||||||
for count in 1..=3 {
|
for _ in 0..3 {
|
||||||
status.update_status(&[Err(anyhow!("")), Ok(()), Ok(())]);
|
status.update_status(&[
|
||||||
assert_eq!(status.should_run(3), vec![true, false, false]);
|
ExecutionStatus::Failed(anyhow!("")),
|
||||||
assert_eq!(u32::from(status.clone()), 0b111_111_000 + count);
|
ExecutionStatus::Succeeded,
|
||||||
|
ExecutionStatus::Succeeded,
|
||||||
|
]);
|
||||||
|
assert_eq!(status.should_run(), [true, false, false]);
|
||||||
}
|
}
|
||||||
status.update_status(&[Err(anyhow!("")), Ok(()), Ok(())]);
|
status.update_status(&[
|
||||||
assert_eq!(status.should_run(3), vec![false, false, false]);
|
ExecutionStatus::Failed(anyhow!("")),
|
||||||
assert_eq!(u32::from(status), 0b111_111_100 | STATUS_COMPLETED);
|
ExecutionStatus::Succeeded,
|
||||||
|
ExecutionStatus::Succeeded,
|
||||||
|
]);
|
||||||
|
assert_eq!(status.should_run(), [false, false, false]);
|
||||||
|
assert!(status.get_completed());
|
||||||
|
status.update_status(&[
|
||||||
|
ExecutionStatus::FixedFailed(1, anyhow!("")),
|
||||||
|
ExecutionStatus::FixedFailed(4, anyhow!("")),
|
||||||
|
ExecutionStatus::FixedFailed(7, anyhow!("")),
|
||||||
|
]);
|
||||||
|
assert_eq!(status.should_run(), [true, false, false]);
|
||||||
|
assert!(!status.get_completed());
|
||||||
|
assert_eq!(<[u32; 3]>::from(status), [1, 4, 7]);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_status_convert() {
|
||||||
|
let testcases = [[0, 0, 1], [1, 2, 3], [3, 1, 2], [3, 0, 7]];
|
||||||
|
for testcase in testcases.iter() {
|
||||||
|
let status = Status::<3>::from(testcase.clone());
|
||||||
|
assert_eq!(<[u32; 3]>::from(status), *testcase);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
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])];
|
||||||
|
for (before, after) in testcases.iter() {
|
||||||
|
let mut status = Status::<3>::from(before.clone());
|
||||||
|
status.update_status(&[
|
||||||
|
ExecutionStatus::Failed(anyhow!("")),
|
||||||
|
ExecutionStatus::Succeeded,
|
||||||
|
ExecutionStatus::Succeeded,
|
||||||
|
]);
|
||||||
|
assert_eq!(<[u32; 3]>::from(status), *after);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_status_reset_failed() {
|
||||||
|
// 重置一个已经失败的任务
|
||||||
|
let mut status = Status::<3>::from([3, 4, 7]);
|
||||||
|
assert!(!status.get_completed());
|
||||||
|
assert!(status.reset_failed());
|
||||||
|
assert!(!status.get_completed());
|
||||||
|
assert_eq!(<[u32; 3]>::from(status), [3, 0, 7]);
|
||||||
|
// 没有内容需要重置,但 completed 标记位是错误的(模拟新增一个子任务状态的情况),此时 reset_failed 会修正 completed 标记位
|
||||||
|
status.set_completed(true);
|
||||||
|
assert!(status.get_completed());
|
||||||
|
assert!(status.reset_failed());
|
||||||
|
assert!(!status.get_completed());
|
||||||
|
// 重置一个已经成功的任务,没有改变状态,也不会修改标记位
|
||||||
|
let mut status = Status::<3>::from([7, 7, 7]);
|
||||||
|
assert!(status.get_completed());
|
||||||
|
assert!(!status.reset_failed());
|
||||||
|
assert!(status.get_completed());
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_status_set() {
|
||||||
|
// 设置子状态,从 completed 到 uncompleted
|
||||||
|
let mut status = Status::<5>::from([7, 7, 7, 7, 7]);
|
||||||
|
assert!(status.get_completed());
|
||||||
|
status.set(4, 0);
|
||||||
|
assert!(!status.get_completed());
|
||||||
|
assert_eq!(<[u32; 5]>::from(status), [7, 7, 7, 7, 0]);
|
||||||
|
// 设置子状态,从 uncompleted 到 completed
|
||||||
|
let mut status = Status::<5>::from([4, 7, 7, 7, 0]);
|
||||||
|
assert!(!status.get_completed());
|
||||||
|
status.set(4, 7);
|
||||||
|
assert!(status.get_completed());
|
||||||
|
assert_eq!(<[u32; 5]>::from(status), [4, 7, 7, 7, 7]);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,60 +1,60 @@
|
|||||||
use std::collections::HashMap;
|
use std::collections::HashSet;
|
||||||
use std::path::{Path, PathBuf};
|
use std::path::{Path, PathBuf};
|
||||||
use std::pin::Pin;
|
use std::pin::Pin;
|
||||||
|
|
||||||
use anyhow::{anyhow, bail, Context, Result};
|
use anyhow::{Context, Result, anyhow, bail};
|
||||||
use bili_sync_entity::*;
|
use bili_sync_entity::*;
|
||||||
use futures::stream::{FuturesOrdered, FuturesUnordered};
|
use futures::stream::{FuturesOrdered, FuturesUnordered};
|
||||||
use futures::{Future, Stream, StreamExt};
|
use futures::{Future, Stream, StreamExt, TryStreamExt};
|
||||||
use sea_orm::entity::prelude::*;
|
|
||||||
use sea_orm::ActiveValue::Set;
|
use sea_orm::ActiveValue::Set;
|
||||||
use sea_orm::TransactionTrait;
|
use sea_orm::TransactionTrait;
|
||||||
|
use sea_orm::entity::prelude::*;
|
||||||
use tokio::fs;
|
use tokio::fs;
|
||||||
use tokio::sync::{Mutex, Semaphore};
|
use tokio::sync::Semaphore;
|
||||||
|
|
||||||
use crate::adapter::{video_list_from, Args, VideoListModel, VideoListModelEnum};
|
use crate::adapter::{Args, VideoSource, VideoSourceEnum, video_source_from};
|
||||||
use crate::bilibili::{BestStream, BiliClient, BiliError, Dimension, PageInfo, Video, VideoInfo};
|
use crate::bilibili::{BestStream, BiliClient, BiliError, Dimension, PageInfo, Video, VideoInfo};
|
||||||
use crate::config::{PathSafeTemplate, ARGS, CONFIG, TEMPLATE};
|
use crate::config::{ARGS, CONFIG, PathSafeTemplate, TEMPLATE};
|
||||||
use crate::downloader::Downloader;
|
use crate::downloader::Downloader;
|
||||||
use crate::error::{DownloadAbortError, ProcessPageError};
|
use crate::error::{DownloadAbortError, ExecutionStatus, ProcessPageError};
|
||||||
use crate::utils::format_arg::{page_format_args, video_format_args};
|
use crate::utils::format_arg::{page_format_args, video_format_args};
|
||||||
use crate::utils::model::{
|
use crate::utils::model::{
|
||||||
create_pages, create_videos, filter_unfilled_videos, filter_unhandled_video_pages, update_pages_model,
|
create_pages, create_videos, filter_unfilled_videos, filter_unhandled_video_pages, update_pages_model,
|
||||||
update_videos_model,
|
update_videos_model,
|
||||||
};
|
};
|
||||||
use crate::utils::nfo::{ModelWrapper, NFOMode, NFOSerializer};
|
use crate::utils::nfo::{ModelWrapper, NFOMode, NFOSerializer};
|
||||||
use crate::utils::status::{PageStatus, VideoStatus};
|
use crate::utils::status::{PageStatus, STATUS_OK, VideoStatus};
|
||||||
|
|
||||||
/// 完整地处理某个视频列表
|
/// 完整地处理某个视频来源
|
||||||
pub async fn process_video_list(
|
pub async fn process_video_source(
|
||||||
args: Args<'_>,
|
args: Args<'_>,
|
||||||
bili_client: &BiliClient,
|
bili_client: &BiliClient,
|
||||||
path: &Path,
|
path: &Path,
|
||||||
connection: &DatabaseConnection,
|
connection: &DatabaseConnection,
|
||||||
) -> Result<()> {
|
) -> Result<()> {
|
||||||
// 从参数中获取视频列表的 Model 与视频流
|
// 从参数中获取视频列表的 Model 与视频流
|
||||||
let (video_list_model, video_streams) = video_list_from(args, path, bili_client, connection).await?;
|
let (video_source, video_streams) = video_source_from(args, path, bili_client, connection).await?;
|
||||||
// 从视频流中获取新视频的简要信息,写入数据库
|
// 从视频流中获取新视频的简要信息,写入数据库
|
||||||
refresh_video_list(&video_list_model, video_streams, connection).await?;
|
refresh_video_source(&video_source, video_streams, connection).await?;
|
||||||
// 单独请求视频详情接口,获取视频的详情信息与所有的分页,写入数据库
|
// 单独请求视频详情接口,获取视频的详情信息与所有的分页,写入数据库
|
||||||
fetch_video_details(bili_client, &video_list_model, connection).await?;
|
fetch_video_details(bili_client, &video_source, connection).await?;
|
||||||
if ARGS.scan_only {
|
if ARGS.scan_only {
|
||||||
warn!("已开启仅扫描模式,跳过视频下载..");
|
warn!("已开启仅扫描模式,跳过视频下载..");
|
||||||
} else {
|
} else {
|
||||||
// 从数据库中查找所有未下载的视频与分页,下载并处理
|
// 从数据库中查找所有未下载的视频与分页,下载并处理
|
||||||
download_unprocessed_videos(bili_client, &video_list_model, connection).await?;
|
download_unprocessed_videos(bili_client, &video_source, connection).await?;
|
||||||
}
|
}
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
/// 请求接口,获取视频列表中所有新添加的视频信息,将其写入数据库
|
/// 请求接口,获取视频列表中所有新添加的视频信息,将其写入数据库
|
||||||
pub async fn refresh_video_list<'a>(
|
pub async fn refresh_video_source<'a>(
|
||||||
video_list_model: &VideoListModelEnum,
|
video_source: &VideoSourceEnum,
|
||||||
video_streams: Pin<Box<dyn Stream<Item = Result<VideoInfo>> + 'a>>,
|
video_streams: Pin<Box<dyn Stream<Item = Result<VideoInfo>> + 'a + Send>>,
|
||||||
connection: &DatabaseConnection,
|
connection: &DatabaseConnection,
|
||||||
) -> Result<()> {
|
) -> Result<()> {
|
||||||
video_list_model.log_refresh_video_start();
|
video_source.log_refresh_video_start();
|
||||||
let latest_row_at = video_list_model.get_latest_row_at().and_utc();
|
let latest_row_at = video_source.get_latest_row_at().and_utc();
|
||||||
let mut max_datetime = latest_row_at;
|
let mut max_datetime = latest_row_at;
|
||||||
let mut error = Ok(());
|
let mut error = Ok(());
|
||||||
let mut video_streams = video_streams
|
let mut video_streams = video_streams
|
||||||
@@ -81,35 +81,35 @@ pub async fn refresh_video_list<'a>(
|
|||||||
let mut count = 0;
|
let mut count = 0;
|
||||||
while let Some(videos_info) = video_streams.next().await {
|
while let Some(videos_info) = video_streams.next().await {
|
||||||
count += videos_info.len();
|
count += videos_info.len();
|
||||||
create_videos(videos_info, video_list_model, connection).await?;
|
create_videos(videos_info, video_source, connection).await?;
|
||||||
}
|
}
|
||||||
// 如果获取视频分页过程中发生了错误,直接在此处返回,不更新 latest_row_at
|
// 如果获取视频分页过程中发生了错误,直接在此处返回,不更新 latest_row_at
|
||||||
error?;
|
error?;
|
||||||
if max_datetime != latest_row_at {
|
if max_datetime != latest_row_at {
|
||||||
video_list_model
|
video_source
|
||||||
.update_latest_row_at(max_datetime.naive_utc())
|
.update_latest_row_at(max_datetime.naive_utc())
|
||||||
.save(connection)
|
.save(connection)
|
||||||
.await?;
|
.await?;
|
||||||
}
|
}
|
||||||
video_list_model.log_refresh_video_end(count);
|
video_source.log_refresh_video_end(count);
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
/// 筛选出所有未获取到全部信息的视频,尝试补充其详细信息
|
/// 筛选出所有未获取到全部信息的视频,尝试补充其详细信息
|
||||||
pub async fn fetch_video_details(
|
pub async fn fetch_video_details(
|
||||||
bili_client: &BiliClient,
|
bili_client: &BiliClient,
|
||||||
video_list_model: &VideoListModelEnum,
|
video_source: &VideoSourceEnum,
|
||||||
connection: &DatabaseConnection,
|
connection: &DatabaseConnection,
|
||||||
) -> Result<()> {
|
) -> Result<()> {
|
||||||
video_list_model.log_fetch_video_start();
|
video_source.log_fetch_video_start();
|
||||||
let videos_model = filter_unfilled_videos(video_list_model.filter_expr(), connection).await?;
|
let videos_model = filter_unfilled_videos(video_source.filter_expr(), connection).await?;
|
||||||
for video_model in videos_model {
|
for video_model in videos_model {
|
||||||
let video = Video::new(bili_client, video_model.bvid.clone());
|
let video = Video::new(bili_client, video_model.bvid.clone());
|
||||||
let info: Result<_> = async { Ok((video.get_tags().await?, video.get_view_info().await?)) }.await;
|
let info: Result<_> = async { Ok((video.get_tags().await?, video.get_view_info().await?)) }.await;
|
||||||
match info {
|
match info {
|
||||||
Err(e) => {
|
Err(e) => {
|
||||||
error!(
|
error!(
|
||||||
"获取视频 {} - {} 的详细信息失败,错误为:{}",
|
"获取视频 {} - {} 的详细信息失败,错误为:{:#}",
|
||||||
&video_model.bvid, &video_model.name, e
|
&video_model.bvid, &video_model.name, e
|
||||||
);
|
);
|
||||||
if let Some(BiliError::RequestFailed(-404, _)) = e.downcast_ref::<BiliError>() {
|
if let Some(BiliError::RequestFailed(-404, _)) = e.downcast_ref::<BiliError>() {
|
||||||
@@ -128,7 +128,7 @@ pub async fn fetch_video_details(
|
|||||||
// 将分页信息写入数据库
|
// 将分页信息写入数据库
|
||||||
create_pages(pages, &video_model, &txn).await?;
|
create_pages(pages, &video_model, &txn).await?;
|
||||||
let mut video_active_model = view_info.into_detail_model(video_model);
|
let mut video_active_model = view_info.into_detail_model(video_model);
|
||||||
video_list_model.set_relation_id(&mut video_active_model);
|
video_source.set_relation_id(&mut video_active_model);
|
||||||
video_active_model.single_page = Set(Some(pages_len == 1));
|
video_active_model.single_page = Set(Some(pages_len == 1));
|
||||||
video_active_model.tags = Set(Some(serde_json::to_value(tags)?));
|
video_active_model.tags = Set(Some(serde_json::to_value(tags)?));
|
||||||
video_active_model.save(&txn).await?;
|
video_active_model.save(&txn).await?;
|
||||||
@@ -136,39 +136,35 @@ pub async fn fetch_video_details(
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
video_list_model.log_fetch_video_end();
|
video_source.log_fetch_video_end();
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
/// 下载所有未处理成功的视频
|
/// 下载所有未处理成功的视频
|
||||||
pub async fn download_unprocessed_videos(
|
pub async fn download_unprocessed_videos(
|
||||||
bili_client: &BiliClient,
|
bili_client: &BiliClient,
|
||||||
video_list_model: &VideoListModelEnum,
|
video_source: &VideoSourceEnum,
|
||||||
connection: &DatabaseConnection,
|
connection: &DatabaseConnection,
|
||||||
) -> Result<()> {
|
) -> Result<()> {
|
||||||
video_list_model.log_download_video_start();
|
video_source.log_download_video_start();
|
||||||
let semaphore = Semaphore::new(CONFIG.concurrent_limit.video);
|
let semaphore = Semaphore::new(CONFIG.concurrent_limit.video);
|
||||||
let downloader = Downloader::new(bili_client.client.clone());
|
let downloader = Downloader::new(bili_client.client.clone());
|
||||||
let mut uppers_mutex: HashMap<i64, (Mutex<()>, Mutex<()>)> = HashMap::new();
|
let unhandled_videos_pages = filter_unhandled_video_pages(video_source.filter_expr(), connection).await?;
|
||||||
let unhandled_videos_pages = filter_unhandled_video_pages(video_list_model.filter_expr(), connection).await?;
|
let mut assigned_upper = HashSet::new();
|
||||||
for (video_model, _) in &unhandled_videos_pages {
|
|
||||||
uppers_mutex
|
|
||||||
.entry(video_model.upper_id)
|
|
||||||
.or_insert_with(|| (Mutex::new(()), Mutex::new(())));
|
|
||||||
}
|
|
||||||
let tasks = unhandled_videos_pages
|
let tasks = unhandled_videos_pages
|
||||||
.into_iter()
|
.into_iter()
|
||||||
.map(|(video_model, pages_model)| {
|
.map(|(video_model, pages_model)| {
|
||||||
let upper_mutex = uppers_mutex.get(&video_model.upper_id).expect("upper mutex not found");
|
let should_download_upper = !assigned_upper.contains(&video_model.upper_id);
|
||||||
|
assigned_upper.insert(video_model.upper_id);
|
||||||
download_video_pages(
|
download_video_pages(
|
||||||
bili_client,
|
bili_client,
|
||||||
video_list_model,
|
video_source,
|
||||||
video_model,
|
video_model,
|
||||||
pages_model,
|
pages_model,
|
||||||
connection,
|
connection,
|
||||||
&semaphore,
|
&semaphore,
|
||||||
&downloader,
|
&downloader,
|
||||||
upper_mutex,
|
should_download_upper,
|
||||||
)
|
)
|
||||||
})
|
})
|
||||||
.collect::<FuturesUnordered<_>>();
|
.collect::<FuturesUnordered<_>>();
|
||||||
@@ -194,25 +190,25 @@ pub async fn download_unprocessed_videos(
|
|||||||
if download_aborted {
|
if download_aborted {
|
||||||
error!("下载触发风控,已终止所有任务,等待下一轮执行");
|
error!("下载触发风控,已终止所有任务,等待下一轮执行");
|
||||||
}
|
}
|
||||||
video_list_model.log_download_video_end();
|
video_source.log_download_video_end();
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
#[allow(clippy::too_many_arguments)]
|
#[allow(clippy::too_many_arguments)]
|
||||||
pub async fn download_video_pages(
|
pub async fn download_video_pages(
|
||||||
bili_client: &BiliClient,
|
bili_client: &BiliClient,
|
||||||
video_list_model: &VideoListModelEnum,
|
video_source: &VideoSourceEnum,
|
||||||
video_model: video::Model,
|
video_model: video::Model,
|
||||||
pages: Vec<page::Model>,
|
pages: Vec<page::Model>,
|
||||||
connection: &DatabaseConnection,
|
connection: &DatabaseConnection,
|
||||||
semaphore: &Semaphore,
|
semaphore: &Semaphore,
|
||||||
downloader: &Downloader,
|
downloader: &Downloader,
|
||||||
upper_mutex: &(Mutex<()>, Mutex<()>),
|
should_download_upper: bool,
|
||||||
) -> Result<video::ActiveModel> {
|
) -> Result<video::ActiveModel> {
|
||||||
let _permit = semaphore.acquire().await.context("acquire semaphore failed")?;
|
let _permit = semaphore.acquire().await.context("acquire semaphore failed")?;
|
||||||
let mut status = VideoStatus::new(video_model.download_status);
|
let mut status = VideoStatus::from(video_model.download_status);
|
||||||
let seprate_status = status.should_run();
|
let separate_status = status.should_run();
|
||||||
let base_path = video_list_model
|
let base_path = video_source
|
||||||
.path()
|
.path()
|
||||||
.join(TEMPLATE.path_safe_render("video", &video_format_args(&video_model))?);
|
.join(TEMPLATE.path_safe_render("video", &video_format_args(&video_model))?);
|
||||||
let upper_id = video_model.upper_id.to_string();
|
let upper_id = video_model.upper_id.to_string();
|
||||||
@@ -223,10 +219,10 @@ pub async fn download_video_pages(
|
|||||||
let is_single_page = video_model.single_page.context("single_page is null")?;
|
let is_single_page = video_model.single_page.context("single_page is null")?;
|
||||||
// 对于单页视频,page 的下载已经足够
|
// 对于单页视频,page 的下载已经足够
|
||||||
// 对于多页视频,page 下载仅包含了分集内容,需要额外补上视频的 poster 的 tvshow.nfo
|
// 对于多页视频,page 下载仅包含了分集内容,需要额外补上视频的 poster 的 tvshow.nfo
|
||||||
let tasks: Vec<Pin<Box<dyn Future<Output = Result<()>>>>> = vec![
|
let tasks: Vec<Pin<Box<dyn Future<Output = Result<ExecutionStatus>> + Send>>> = vec![
|
||||||
// 下载视频封面
|
// 下载视频封面
|
||||||
Box::pin(fetch_video_poster(
|
Box::pin(fetch_video_poster(
|
||||||
seprate_status[0] && !is_single_page,
|
separate_status[0] && !is_single_page,
|
||||||
&video_model,
|
&video_model,
|
||||||
downloader,
|
downloader,
|
||||||
base_path.join("poster.jpg"),
|
base_path.join("poster.jpg"),
|
||||||
@@ -234,28 +230,26 @@ pub async fn download_video_pages(
|
|||||||
)),
|
)),
|
||||||
// 生成视频信息的 nfo
|
// 生成视频信息的 nfo
|
||||||
Box::pin(generate_video_nfo(
|
Box::pin(generate_video_nfo(
|
||||||
seprate_status[1] && !is_single_page,
|
separate_status[1] && !is_single_page,
|
||||||
&video_model,
|
&video_model,
|
||||||
base_path.join("tvshow.nfo"),
|
base_path.join("tvshow.nfo"),
|
||||||
)),
|
)),
|
||||||
// 下载 Up 主头像
|
// 下载 Up 主头像
|
||||||
Box::pin(fetch_upper_face(
|
Box::pin(fetch_upper_face(
|
||||||
seprate_status[2],
|
separate_status[2] && should_download_upper,
|
||||||
&video_model,
|
&video_model,
|
||||||
downloader,
|
downloader,
|
||||||
&upper_mutex.0,
|
|
||||||
base_upper_path.join("folder.jpg"),
|
base_upper_path.join("folder.jpg"),
|
||||||
)),
|
)),
|
||||||
// 生成 Up 主信息的 nfo
|
// 生成 Up 主信息的 nfo
|
||||||
Box::pin(generate_upper_nfo(
|
Box::pin(generate_upper_nfo(
|
||||||
seprate_status[3],
|
separate_status[3] && should_download_upper,
|
||||||
&video_model,
|
&video_model,
|
||||||
&upper_mutex.1,
|
|
||||||
base_upper_path.join("person.nfo"),
|
base_upper_path.join("person.nfo"),
|
||||||
)),
|
)),
|
||||||
// 分发并执行分 P 下载的任务
|
// 分发并执行分 P 下载的任务
|
||||||
Box::pin(dispatch_download_page(
|
Box::pin(dispatch_download_page(
|
||||||
seprate_status[4],
|
separate_status[4],
|
||||||
bili_client,
|
bili_client,
|
||||||
&video_model,
|
&video_model,
|
||||||
pages,
|
pages,
|
||||||
@@ -265,17 +259,26 @@ pub async fn download_video_pages(
|
|||||||
)),
|
)),
|
||||||
];
|
];
|
||||||
let tasks: FuturesOrdered<_> = tasks.into_iter().collect();
|
let tasks: FuturesOrdered<_> = tasks.into_iter().collect();
|
||||||
let results: Vec<Result<()>> = tasks.collect().await;
|
let results: Vec<ExecutionStatus> = tasks.collect::<Vec<_>>().await.into_iter().map(Into::into).collect();
|
||||||
status.update_status(&results);
|
status.update_status(&results);
|
||||||
results
|
results
|
||||||
.iter()
|
.iter()
|
||||||
.take(4)
|
.take(4)
|
||||||
.zip(["封面", "详情", "作者头像", "作者详情"])
|
.zip(["封面", "详情", "作者头像", "作者详情"])
|
||||||
.for_each(|(res, task_name)| match res {
|
.for_each(|(res, task_name)| match res {
|
||||||
Ok(_) => info!("处理视频「{}」{}成功", &video_model.name, task_name),
|
ExecutionStatus::Skipped => info!("处理视频「{}」{}已成功过,跳过", &video_model.name, task_name),
|
||||||
Err(e) => error!("处理视频「{}」{}失败: {}", &video_model.name, task_name, e),
|
ExecutionStatus::Succeeded => info!("处理视频「{}」{}成功", &video_model.name, task_name),
|
||||||
|
ExecutionStatus::Ignored(e) => {
|
||||||
|
error!(
|
||||||
|
"处理视频「{}」{}出现常见错误,已忽略: {:#}",
|
||||||
|
&video_model.name, task_name, e
|
||||||
|
)
|
||||||
|
}
|
||||||
|
ExecutionStatus::Failed(e) | ExecutionStatus::FixedFailed(_, e) => {
|
||||||
|
error!("处理视频「{}」{}失败: {:#}", &video_model.name, task_name, e)
|
||||||
|
}
|
||||||
});
|
});
|
||||||
if let Err(e) = results.into_iter().nth(4).context("page download result not found")? {
|
if let ExecutionStatus::Failed(e) = results.into_iter().nth(4).context("page download result not found")? {
|
||||||
if e.downcast_ref::<DownloadAbortError>().is_some() {
|
if e.downcast_ref::<DownloadAbortError>().is_some() {
|
||||||
return Err(e);
|
return Err(e);
|
||||||
}
|
}
|
||||||
@@ -295,9 +298,9 @@ pub async fn dispatch_download_page(
|
|||||||
connection: &DatabaseConnection,
|
connection: &DatabaseConnection,
|
||||||
downloader: &Downloader,
|
downloader: &Downloader,
|
||||||
base_path: &Path,
|
base_path: &Path,
|
||||||
) -> Result<()> {
|
) -> Result<ExecutionStatus> {
|
||||||
if !should_run {
|
if !should_run {
|
||||||
return Ok(());
|
return Ok(ExecutionStatus::Skipped);
|
||||||
}
|
}
|
||||||
let child_semaphore = Semaphore::new(CONFIG.concurrent_limit.page);
|
let child_semaphore = Semaphore::new(CONFIG.concurrent_limit.page);
|
||||||
let tasks = pages
|
let tasks = pages
|
||||||
@@ -313,18 +316,19 @@ pub async fn dispatch_download_page(
|
|||||||
)
|
)
|
||||||
})
|
})
|
||||||
.collect::<FuturesUnordered<_>>();
|
.collect::<FuturesUnordered<_>>();
|
||||||
let (mut download_aborted, mut error_occurred) = (false, false);
|
let (mut download_aborted, mut target_status) = (false, STATUS_OK);
|
||||||
let mut stream = tasks
|
let mut stream = tasks
|
||||||
.take_while(|res| {
|
.take_while(|res| {
|
||||||
match res {
|
match res {
|
||||||
Ok(model) => {
|
Ok(model) => {
|
||||||
// 当前函数返回的是所有分页的下载状态,只要有任何一个分页返回新的下载状态标识位是 false,当前函数就应该认为是失败的
|
// 该视频的所有分页的下载状态都会在此返回,需要根据这些状态确认视频层“分 P 下载”子任务的状态
|
||||||
if model
|
// 在过去的实现中,此处仅仅根据 page_download_status 的最高标志位来判断,如果最高标志位是 true 则认为完成
|
||||||
.download_status
|
// 这样会导致即使分页中有失败到 MAX_RETRY 的情况,视频层的分 P 下载状态也会被认为是 Succeeded,不够准确
|
||||||
.try_as_ref()
|
// 新版本实现会将此处取值为所有子任务状态的最小值,这样只有所有分页的子任务全部成功时才会认为视频层的分 P 下载状态是 Succeeded
|
||||||
.is_none_or(|status| !PageStatus::new(*status).get_completed())
|
let page_download_status = model.download_status.try_as_ref().expect("download_status must be set");
|
||||||
{
|
let separate_status: [u32; 5] = PageStatus::from(*page_download_status).into();
|
||||||
error_occurred = true;
|
for status in separate_status {
|
||||||
|
target_status = target_status.min(status);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
Err(e) => {
|
Err(e) => {
|
||||||
@@ -345,14 +349,10 @@ pub async fn dispatch_download_page(
|
|||||||
error!("下载视频「{}」的分页时触发风控,将异常向上传递..", &video_model.name);
|
error!("下载视频「{}」的分页时触发风控,将异常向上传递..", &video_model.name);
|
||||||
bail!(DownloadAbortError());
|
bail!(DownloadAbortError());
|
||||||
}
|
}
|
||||||
if error_occurred {
|
if target_status != STATUS_OK {
|
||||||
error!(
|
return Ok(ExecutionStatus::FixedFailed(target_status, ProcessPageError().into()));
|
||||||
"下载视频「{}」的分页时出现错误,将在下一轮尝试重新处理",
|
|
||||||
&video_model.name
|
|
||||||
);
|
|
||||||
bail!(ProcessPageError());
|
|
||||||
}
|
}
|
||||||
Ok(())
|
Ok(ExecutionStatus::Succeeded)
|
||||||
}
|
}
|
||||||
|
|
||||||
/// 下载某个分页,未发生风控且正常运行时返回 Ok(Page::ActiveModel),其中 status 字段存储了新的下载状态,发生风控时返回 DownloadAbortError
|
/// 下载某个分页,未发生风控且正常运行时返回 Ok(Page::ActiveModel),其中 status 字段存储了新的下载状态,发生风控时返回 DownloadAbortError
|
||||||
@@ -365,17 +365,18 @@ pub async fn download_page(
|
|||||||
base_path: &Path,
|
base_path: &Path,
|
||||||
) -> Result<page::ActiveModel> {
|
) -> Result<page::ActiveModel> {
|
||||||
let _permit = semaphore.acquire().await.context("acquire semaphore failed")?;
|
let _permit = semaphore.acquire().await.context("acquire semaphore failed")?;
|
||||||
let mut status = PageStatus::new(page_model.download_status);
|
let mut status = PageStatus::from(page_model.download_status);
|
||||||
let seprate_status = status.should_run();
|
let separate_status = status.should_run();
|
||||||
let is_single_page = video_model.single_page.context("single_page is null")?;
|
let is_single_page = video_model.single_page.context("single_page is null")?;
|
||||||
let base_name = TEMPLATE.path_safe_render("page", &page_format_args(video_model, &page_model))?;
|
let base_name = TEMPLATE.path_safe_render("page", &page_format_args(video_model, &page_model))?;
|
||||||
let (poster_path, video_path, nfo_path, danmaku_path, fanart_path) = if is_single_page {
|
let (poster_path, video_path, nfo_path, danmaku_path, fanart_path, subtitle_path) = if is_single_page {
|
||||||
(
|
(
|
||||||
base_path.join(format!("{}-poster.jpg", &base_name)),
|
base_path.join(format!("{}-poster.jpg", &base_name)),
|
||||||
base_path.join(format!("{}.mp4", &base_name)),
|
base_path.join(format!("{}.mp4", &base_name)),
|
||||||
base_path.join(format!("{}.nfo", &base_name)),
|
base_path.join(format!("{}.nfo", &base_name)),
|
||||||
base_path.join(format!("{}.zh-CN.default.ass", &base_name)),
|
base_path.join(format!("{}.zh-CN.default.ass", &base_name)),
|
||||||
Some(base_path.join(format!("{}-fanart.jpg", &base_name))),
|
Some(base_path.join(format!("{}-fanart.jpg", &base_name))),
|
||||||
|
base_path.join(format!("{}.srt", &base_name)),
|
||||||
)
|
)
|
||||||
} else {
|
} else {
|
||||||
(
|
(
|
||||||
@@ -393,6 +394,9 @@ pub async fn download_page(
|
|||||||
.join(format!("{} - S01E{:0>2}.zh-CN.default.ass", &base_name, page_model.pid)),
|
.join(format!("{} - S01E{:0>2}.zh-CN.default.ass", &base_name, page_model.pid)),
|
||||||
// 对于多页视频,会在上一步 fetch_video_poster 中获取剧集的 fanart,无需在此处下载单集的
|
// 对于多页视频,会在上一步 fetch_video_poster 中获取剧集的 fanart,无需在此处下载单集的
|
||||||
None,
|
None,
|
||||||
|
base_path
|
||||||
|
.join("Season 1")
|
||||||
|
.join(format!("{} - S01E{:0>2}.srt", &base_name, page_model.pid)),
|
||||||
)
|
)
|
||||||
};
|
};
|
||||||
let dimension = match (page_model.width, page_model.height) {
|
let dimension = match (page_model.width, page_model.height) {
|
||||||
@@ -409,9 +413,9 @@ pub async fn download_page(
|
|||||||
dimension,
|
dimension,
|
||||||
..Default::default()
|
..Default::default()
|
||||||
};
|
};
|
||||||
let tasks: Vec<Pin<Box<dyn Future<Output = Result<()>>>>> = vec![
|
let tasks: Vec<Pin<Box<dyn Future<Output = Result<ExecutionStatus>> + Send>>> = vec![
|
||||||
Box::pin(fetch_page_poster(
|
Box::pin(fetch_page_poster(
|
||||||
seprate_status[0],
|
separate_status[0],
|
||||||
video_model,
|
video_model,
|
||||||
&page_model,
|
&page_model,
|
||||||
downloader,
|
downloader,
|
||||||
@@ -419,40 +423,62 @@ pub async fn download_page(
|
|||||||
fanart_path,
|
fanart_path,
|
||||||
)),
|
)),
|
||||||
Box::pin(fetch_page_video(
|
Box::pin(fetch_page_video(
|
||||||
seprate_status[1],
|
separate_status[1],
|
||||||
bili_client,
|
bili_client,
|
||||||
video_model,
|
video_model,
|
||||||
downloader,
|
downloader,
|
||||||
&page_info,
|
&page_info,
|
||||||
&video_path,
|
&video_path,
|
||||||
)),
|
)),
|
||||||
Box::pin(generate_page_nfo(seprate_status[2], video_model, &page_model, nfo_path)),
|
Box::pin(generate_page_nfo(
|
||||||
|
separate_status[2],
|
||||||
|
video_model,
|
||||||
|
&page_model,
|
||||||
|
nfo_path,
|
||||||
|
)),
|
||||||
Box::pin(fetch_page_danmaku(
|
Box::pin(fetch_page_danmaku(
|
||||||
seprate_status[3],
|
separate_status[3],
|
||||||
bili_client,
|
bili_client,
|
||||||
video_model,
|
video_model,
|
||||||
&page_info,
|
&page_info,
|
||||||
danmaku_path,
|
danmaku_path,
|
||||||
)),
|
)),
|
||||||
|
Box::pin(fetch_page_subtitle(
|
||||||
|
separate_status[4],
|
||||||
|
bili_client,
|
||||||
|
video_model,
|
||||||
|
&page_info,
|
||||||
|
&subtitle_path,
|
||||||
|
)),
|
||||||
];
|
];
|
||||||
let tasks: FuturesOrdered<_> = tasks.into_iter().collect();
|
let tasks: FuturesOrdered<_> = tasks.into_iter().collect();
|
||||||
let results: Vec<Result<()>> = tasks.collect().await;
|
let results: Vec<ExecutionStatus> = tasks.collect::<Vec<_>>().await.into_iter().map(Into::into).collect();
|
||||||
status.update_status(&results);
|
status.update_status(&results);
|
||||||
results
|
results
|
||||||
.iter()
|
.iter()
|
||||||
.zip(["封面", "视频", "详情", "弹幕"])
|
.zip(["封面", "视频", "详情", "弹幕", "字幕"])
|
||||||
.for_each(|(res, task_name)| match res {
|
.for_each(|(res, task_name)| match res {
|
||||||
Ok(_) => info!(
|
ExecutionStatus::Skipped => info!(
|
||||||
|
"处理视频「{}」第 {} 页{}已成功过,跳过",
|
||||||
|
&video_model.name, page_model.pid, task_name
|
||||||
|
),
|
||||||
|
ExecutionStatus::Succeeded => info!(
|
||||||
"处理视频「{}」第 {} 页{}成功",
|
"处理视频「{}」第 {} 页{}成功",
|
||||||
&video_model.name, page_model.pid, task_name
|
&video_model.name, page_model.pid, task_name
|
||||||
),
|
),
|
||||||
Err(e) => error!(
|
ExecutionStatus::Ignored(e) => {
|
||||||
"处理视频「{}」第 {} 页{}失败: {}",
|
error!(
|
||||||
|
"处理视频「{}」第 {} 页{}出现常见错误,已忽略: {:#}",
|
||||||
|
&video_model.name, page_model.pid, task_name, e
|
||||||
|
)
|
||||||
|
}
|
||||||
|
ExecutionStatus::Failed(e) | ExecutionStatus::FixedFailed(_, e) => error!(
|
||||||
|
"处理视频「{}」第 {} 页{}失败: {:#}",
|
||||||
&video_model.name, page_model.pid, task_name, e
|
&video_model.name, page_model.pid, task_name, e
|
||||||
),
|
),
|
||||||
});
|
});
|
||||||
// 如果下载视频时触发风控,直接返回 DownloadAbortError
|
// 如果下载视频时触发风控,直接返回 DownloadAbortError
|
||||||
if let Err(e) = results.into_iter().nth(1).context("video download result not found")? {
|
if let ExecutionStatus::Failed(e) = results.into_iter().nth(1).context("video download result not found")? {
|
||||||
if let Ok(BiliError::RiskControlOccurred) = e.downcast::<BiliError>() {
|
if let Ok(BiliError::RiskControlOccurred) = e.downcast::<BiliError>() {
|
||||||
bail!(DownloadAbortError());
|
bail!(DownloadAbortError());
|
||||||
}
|
}
|
||||||
@@ -470,9 +496,9 @@ pub async fn fetch_page_poster(
|
|||||||
downloader: &Downloader,
|
downloader: &Downloader,
|
||||||
poster_path: PathBuf,
|
poster_path: PathBuf,
|
||||||
fanart_path: Option<PathBuf>,
|
fanart_path: Option<PathBuf>,
|
||||||
) -> Result<()> {
|
) -> Result<ExecutionStatus> {
|
||||||
if !should_run {
|
if !should_run {
|
||||||
return Ok(());
|
return Ok(ExecutionStatus::Skipped);
|
||||||
}
|
}
|
||||||
let single_page = video_model.single_page.context("single_page is null")?;
|
let single_page = video_model.single_page.context("single_page is null")?;
|
||||||
let url = if single_page {
|
let url = if single_page {
|
||||||
@@ -489,7 +515,7 @@ pub async fn fetch_page_poster(
|
|||||||
if let Some(fanart_path) = fanart_path {
|
if let Some(fanart_path) = fanart_path {
|
||||||
fs::copy(&poster_path, &fanart_path).await?;
|
fs::copy(&poster_path, &fanart_path).await?;
|
||||||
}
|
}
|
||||||
Ok(())
|
Ok(ExecutionStatus::Succeeded)
|
||||||
}
|
}
|
||||||
|
|
||||||
pub async fn fetch_page_video(
|
pub async fn fetch_page_video(
|
||||||
@@ -499,9 +525,9 @@ pub async fn fetch_page_video(
|
|||||||
downloader: &Downloader,
|
downloader: &Downloader,
|
||||||
page_info: &PageInfo,
|
page_info: &PageInfo,
|
||||||
page_path: &Path,
|
page_path: &Path,
|
||||||
) -> Result<()> {
|
) -> Result<ExecutionStatus> {
|
||||||
if !should_run {
|
if !should_run {
|
||||||
return Ok(());
|
return Ok(ExecutionStatus::Skipped);
|
||||||
}
|
}
|
||||||
let bili_video = Video::new(bili_client, video_model.bvid.clone());
|
let bili_video = Video::new(bili_client, video_model.bvid.clone());
|
||||||
let streams = bili_video
|
let streams = bili_video
|
||||||
@@ -509,11 +535,11 @@ pub async fn fetch_page_video(
|
|||||||
.await?
|
.await?
|
||||||
.best_stream(&CONFIG.filter_option)?;
|
.best_stream(&CONFIG.filter_option)?;
|
||||||
match streams {
|
match streams {
|
||||||
BestStream::Mixed(mix_stream) => downloader.fetch(mix_stream.url(), page_path).await,
|
BestStream::Mixed(mix_stream) => downloader.fetch(mix_stream.url(), page_path).await?,
|
||||||
BestStream::VideoAudio {
|
BestStream::VideoAudio {
|
||||||
video: video_stream,
|
video: video_stream,
|
||||||
audio: None,
|
audio: None,
|
||||||
} => downloader.fetch(video_stream.url(), page_path).await,
|
} => downloader.fetch(video_stream.url(), page_path).await?,
|
||||||
BestStream::VideoAudio {
|
BestStream::VideoAudio {
|
||||||
video: video_stream,
|
video: video_stream,
|
||||||
audio: Some(audio_stream),
|
audio: Some(audio_stream),
|
||||||
@@ -530,9 +556,10 @@ pub async fn fetch_page_video(
|
|||||||
.await;
|
.await;
|
||||||
let _ = fs::remove_file(tmp_video_path).await;
|
let _ = fs::remove_file(tmp_video_path).await;
|
||||||
let _ = fs::remove_file(tmp_audio_path).await;
|
let _ = fs::remove_file(tmp_audio_path).await;
|
||||||
res
|
res?
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
Ok(ExecutionStatus::Succeeded)
|
||||||
}
|
}
|
||||||
|
|
||||||
pub async fn fetch_page_danmaku(
|
pub async fn fetch_page_danmaku(
|
||||||
@@ -541,16 +568,40 @@ pub async fn fetch_page_danmaku(
|
|||||||
video_model: &video::Model,
|
video_model: &video::Model,
|
||||||
page_info: &PageInfo,
|
page_info: &PageInfo,
|
||||||
danmaku_path: PathBuf,
|
danmaku_path: PathBuf,
|
||||||
) -> Result<()> {
|
) -> Result<ExecutionStatus> {
|
||||||
if !should_run {
|
if !should_run {
|
||||||
return Ok(());
|
return Ok(ExecutionStatus::Skipped);
|
||||||
}
|
}
|
||||||
let bili_video = Video::new(bili_client, video_model.bvid.clone());
|
let bili_video = Video::new(bili_client, video_model.bvid.clone());
|
||||||
bili_video
|
bili_video
|
||||||
.get_danmaku_writer(page_info)
|
.get_danmaku_writer(page_info)
|
||||||
.await?
|
.await?
|
||||||
.write(danmaku_path)
|
.write(danmaku_path)
|
||||||
.await
|
.await?;
|
||||||
|
Ok(ExecutionStatus::Succeeded)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub async fn fetch_page_subtitle(
|
||||||
|
should_run: bool,
|
||||||
|
bili_client: &BiliClient,
|
||||||
|
video_model: &video::Model,
|
||||||
|
page_info: &PageInfo,
|
||||||
|
subtitle_path: &Path,
|
||||||
|
) -> Result<ExecutionStatus> {
|
||||||
|
if !should_run {
|
||||||
|
return Ok(ExecutionStatus::Skipped);
|
||||||
|
}
|
||||||
|
let bili_video = Video::new(bili_client, video_model.bvid.clone());
|
||||||
|
let subtitles = bili_video.get_subtitles(page_info).await?;
|
||||||
|
let tasks = subtitles
|
||||||
|
.into_iter()
|
||||||
|
.map(|subtitle| async move {
|
||||||
|
let path = subtitle_path.with_extension(format!("{}.srt", subtitle.lan));
|
||||||
|
tokio::fs::write(path, subtitle.body.to_string()).await
|
||||||
|
})
|
||||||
|
.collect::<FuturesUnordered<_>>();
|
||||||
|
tasks.try_collect::<Vec<()>>().await?;
|
||||||
|
Ok(ExecutionStatus::Succeeded)
|
||||||
}
|
}
|
||||||
|
|
||||||
pub async fn generate_page_nfo(
|
pub async fn generate_page_nfo(
|
||||||
@@ -558,9 +609,9 @@ pub async fn generate_page_nfo(
|
|||||||
video_model: &video::Model,
|
video_model: &video::Model,
|
||||||
page_model: &page::Model,
|
page_model: &page::Model,
|
||||||
nfo_path: PathBuf,
|
nfo_path: PathBuf,
|
||||||
) -> Result<()> {
|
) -> Result<ExecutionStatus> {
|
||||||
if !should_run {
|
if !should_run {
|
||||||
return Ok(());
|
return Ok(ExecutionStatus::Skipped);
|
||||||
}
|
}
|
||||||
let single_page = video_model.single_page.context("single_page is null")?;
|
let single_page = video_model.single_page.context("single_page is null")?;
|
||||||
let nfo_serializer = if single_page {
|
let nfo_serializer = if single_page {
|
||||||
@@ -568,7 +619,8 @@ pub async fn generate_page_nfo(
|
|||||||
} else {
|
} else {
|
||||||
NFOSerializer(ModelWrapper::Page(page_model), NFOMode::EPOSODE)
|
NFOSerializer(ModelWrapper::Page(page_model), NFOMode::EPOSODE)
|
||||||
};
|
};
|
||||||
generate_nfo(nfo_serializer, nfo_path).await
|
generate_nfo(nfo_serializer, nfo_path).await?;
|
||||||
|
Ok(ExecutionStatus::Succeeded)
|
||||||
}
|
}
|
||||||
|
|
||||||
pub async fn fetch_video_poster(
|
pub async fn fetch_video_poster(
|
||||||
@@ -577,56 +629,52 @@ pub async fn fetch_video_poster(
|
|||||||
downloader: &Downloader,
|
downloader: &Downloader,
|
||||||
poster_path: PathBuf,
|
poster_path: PathBuf,
|
||||||
fanart_path: PathBuf,
|
fanart_path: PathBuf,
|
||||||
) -> Result<()> {
|
) -> Result<ExecutionStatus> {
|
||||||
if !should_run {
|
if !should_run {
|
||||||
return Ok(());
|
return Ok(ExecutionStatus::Skipped);
|
||||||
}
|
}
|
||||||
downloader.fetch(&video_model.cover, &poster_path).await?;
|
downloader.fetch(&video_model.cover, &poster_path).await?;
|
||||||
fs::copy(&poster_path, &fanart_path).await?;
|
fs::copy(&poster_path, &fanart_path).await?;
|
||||||
Ok(())
|
Ok(ExecutionStatus::Succeeded)
|
||||||
}
|
}
|
||||||
|
|
||||||
pub async fn fetch_upper_face(
|
pub async fn fetch_upper_face(
|
||||||
should_run: bool,
|
should_run: bool,
|
||||||
video_model: &video::Model,
|
video_model: &video::Model,
|
||||||
downloader: &Downloader,
|
downloader: &Downloader,
|
||||||
upper_face_mutex: &Mutex<()>,
|
|
||||||
upper_face_path: PathBuf,
|
upper_face_path: PathBuf,
|
||||||
) -> Result<()> {
|
) -> Result<ExecutionStatus> {
|
||||||
if !should_run {
|
if !should_run {
|
||||||
return Ok(());
|
return Ok(ExecutionStatus::Skipped);
|
||||||
}
|
}
|
||||||
// 这个锁只是为了避免多个视频同时下载同一个 up 主的头像,不携带实际内容
|
downloader.fetch(&video_model.upper_face, &upper_face_path).await?;
|
||||||
let _ = upper_face_mutex.lock().await;
|
Ok(ExecutionStatus::Succeeded)
|
||||||
if !upper_face_path.exists() {
|
|
||||||
return downloader.fetch(&video_model.upper_face, &upper_face_path).await;
|
|
||||||
}
|
|
||||||
Ok(())
|
|
||||||
}
|
}
|
||||||
|
|
||||||
pub async fn generate_upper_nfo(
|
pub async fn generate_upper_nfo(
|
||||||
should_run: bool,
|
should_run: bool,
|
||||||
video_model: &video::Model,
|
video_model: &video::Model,
|
||||||
upper_nfo_mutex: &Mutex<()>,
|
|
||||||
nfo_path: PathBuf,
|
nfo_path: PathBuf,
|
||||||
) -> Result<()> {
|
) -> Result<ExecutionStatus> {
|
||||||
if !should_run {
|
if !should_run {
|
||||||
return Ok(());
|
return Ok(ExecutionStatus::Skipped);
|
||||||
}
|
}
|
||||||
let _ = upper_nfo_mutex.lock().await;
|
let nfo_serializer = NFOSerializer(ModelWrapper::Video(video_model), NFOMode::UPPER);
|
||||||
if !nfo_path.exists() {
|
generate_nfo(nfo_serializer, nfo_path).await?;
|
||||||
let nfo_serializer = NFOSerializer(ModelWrapper::Video(video_model), NFOMode::UPPER);
|
Ok(ExecutionStatus::Succeeded)
|
||||||
return generate_nfo(nfo_serializer, nfo_path).await;
|
|
||||||
}
|
|
||||||
Ok(())
|
|
||||||
}
|
}
|
||||||
|
|
||||||
pub async fn generate_video_nfo(should_run: bool, video_model: &video::Model, nfo_path: PathBuf) -> Result<()> {
|
pub async fn generate_video_nfo(
|
||||||
|
should_run: bool,
|
||||||
|
video_model: &video::Model,
|
||||||
|
nfo_path: PathBuf,
|
||||||
|
) -> Result<ExecutionStatus> {
|
||||||
if !should_run {
|
if !should_run {
|
||||||
return Ok(());
|
return Ok(ExecutionStatus::Skipped);
|
||||||
}
|
}
|
||||||
let nfo_serializer = NFOSerializer(ModelWrapper::Video(video_model), NFOMode::TVSHOW);
|
let nfo_serializer = NFOSerializer(ModelWrapper::Video(video_model), NFOMode::TVSHOW);
|
||||||
generate_nfo(nfo_serializer, nfo_path).await
|
generate_nfo(nfo_serializer, nfo_path).await?;
|
||||||
|
Ok(ExecutionStatus::Succeeded)
|
||||||
}
|
}
|
||||||
|
|
||||||
/// 创建 nfo_path 的父目录,然后写入 nfo 文件
|
/// 创建 nfo_path 的父目录,然后写入 nfo 文件
|
||||||
|
|||||||
@@ -7,7 +7,7 @@ pub struct Migration;
|
|||||||
#[async_trait::async_trait]
|
#[async_trait::async_trait]
|
||||||
impl MigrationTrait for Migration {
|
impl MigrationTrait for Migration {
|
||||||
async fn up(&self, manager: &SchemaManager) -> Result<(), DbErr> {
|
async fn up(&self, manager: &SchemaManager) -> Result<(), DbErr> {
|
||||||
// 为四张 video list 表添加 latest_row_at 字段,表示该列表处理到的最新时间
|
// 为四张 video source 表添加 latest_row_at 字段,表示该列表处理到的最新时间
|
||||||
manager
|
manager
|
||||||
.alter_table(
|
.alter_table(
|
||||||
Table::alter()
|
Table::alter()
|
||||||
|
|||||||
@@ -21,7 +21,7 @@ export default defineConfig({
|
|||||||
nav: [
|
nav: [
|
||||||
{ text: "主页", link: "/" },
|
{ text: "主页", link: "/" },
|
||||||
{
|
{
|
||||||
text: "v2.3.0",
|
text: "v2.4.1",
|
||||||
items: [
|
items: [
|
||||||
{
|
{
|
||||||
text: "程序更新",
|
text: "程序更新",
|
||||||
@@ -61,6 +61,13 @@ export default defineConfig({
|
|||||||
{ text: "获取投稿信息", link: "/submission" },
|
{ text: "获取投稿信息", link: "/submission" },
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
text: "其它",
|
||||||
|
items: [
|
||||||
|
{ text: "常见问题", link: "/question" },
|
||||||
|
{ text: "管理页", link: "/frontend" },
|
||||||
|
],
|
||||||
|
}
|
||||||
],
|
],
|
||||||
socialLinks: [
|
socialLinks: [
|
||||||
{ icon: "github", link: "https://github.com/amtoaer/bili-sync" },
|
{ icon: "github", link: "https://github.com/amtoaer/bili-sync" },
|
||||||
|
|||||||
BIN
docs/assets/frontend.webp
Normal file
BIN
docs/assets/frontend.webp
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 167 KiB |
BIN
docs/assets/swagger.webp
Normal file
BIN
docs/assets/swagger.webp
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 78 KiB |
@@ -49,6 +49,16 @@
|
|||||||
> [!CAUTION]
|
> [!CAUTION]
|
||||||
> **路径分隔符**在不同平台定义不同,Windows 下为 `\`,MacOS 和 Linux 下为 `/`。
|
> **路径分隔符**在不同平台定义不同,Windows 下为 `\`,MacOS 和 Linux 下为 `/`。
|
||||||
|
|
||||||
|
## `auth_token`
|
||||||
|
|
||||||
|
表示调用程序管理 API 需要的身份凭据,程序会检查 API 请求 Header 中是否包含正确的 `Authorization` 字段。
|
||||||
|
|
||||||
|
内置管理页前端提供了 `auth_token` 的输入框,填写后即可成功调用 API 使用管理页。
|
||||||
|
|
||||||
|
## `bind_address`
|
||||||
|
|
||||||
|
程序 Web Server 监听的地址,程序启动时会监听该地址,成功后可通过 `http://${bind_address}` 访问管理页。
|
||||||
|
|
||||||
## `interval`
|
## `interval`
|
||||||
|
|
||||||
表示程序每次执行扫描下载的间隔时间,单位为秒。
|
表示程序每次执行扫描下载的间隔时间,单位为秒。
|
||||||
|
|||||||
@@ -10,7 +10,7 @@
|
|||||||
- 每个视频都有唯一的 bvid,包含了封面、描述和标签信息,并包含了一个或多个分页;
|
- 每个视频都有唯一的 bvid,包含了封面、描述和标签信息,并包含了一个或多个分页;
|
||||||
- 每个分页都有一个唯一的 cid,包含了封面、视频、音频、弹幕。
|
- 每个分页都有一个唯一的 cid,包含了封面、视频、音频、弹幕。
|
||||||
|
|
||||||
为了描述方便,在后文会将收藏夹、稍后再看这类结构统称为 video list,将视频称为 video,将分页称为 page。不难看出这三者有着很明显的层级关系:**video list 包含若干 video,video 包含若干 page**。
|
为了描述方便,在后文会将收藏夹、稍后再看这类结构统称为 video source,将视频称为 video,将分页称为 page。不难看出这三者有着很明显的层级关系:**video source 包含若干 video,video 包含若干 page**。
|
||||||
|
|
||||||
一个非常容易混淆的点是视频合集/视频列表与多页视频的区别:
|
一个非常容易混淆的点是视频合集/视频列表与多页视频的区别:
|
||||||
|
|
||||||
@@ -21,7 +21,7 @@
|
|||||||
|
|
||||||
这两张图中,上图是视频合集,下图是多页视频。这两者在展示上区别较小,但在结构上有相当大的不同。结合上面对 b 站视频结构的介绍,这个区别可以简单总结为:
|
这两张图中,上图是视频合集,下图是多页视频。这两者在展示上区别较小,但在结构上有相当大的不同。结合上面对 b 站视频结构的介绍,这个区别可以简单总结为:
|
||||||
|
|
||||||
+ **视频合集是由多个仅包含单个 page 的 video 组成的 video list**;
|
+ **视频合集是由多个仅包含单个 page 的 video 组成的 video source**;
|
||||||
|
|
||||||
+ **多页视频是由多个 page 组成的 video**。
|
+ **多页视频是由多个 page 组成的 video**。
|
||||||
|
|
||||||
@@ -31,7 +31,7 @@
|
|||||||
|
|
||||||
EMBY 的一般结构是: `媒体库 - 文件夹 - 电影/电视剧 - 分季/分集`,方便起见,我采用了如下的对应关系:
|
EMBY 的一般结构是: `媒体库 - 文件夹 - 电影/电视剧 - 分季/分集`,方便起见,我采用了如下的对应关系:
|
||||||
|
|
||||||
1. **文件夹**:对应 b 站的 video list;
|
1. **文件夹**:对应 b 站的 video source;
|
||||||
2. **电视剧**: 对应 b 站的 video;
|
2. **电视剧**: 对应 b 站的 video;
|
||||||
3. **第一季的所有分集**:对应 b 站的 page。
|
3. **第一季的所有分集**:对应 b 站的 page。
|
||||||
|
|
||||||
@@ -54,11 +54,11 @@ EMBY 的一般结构是: `媒体库 - 文件夹 - 电影/电视剧 - 分季/
|
|||||||
> [!NOTE]
|
> [!NOTE]
|
||||||
> 可以[前往此处](https://github.com/amtoaer/bili-sync/tree/main/crates/bili_sync_entity/src/entities)实时查看当前版本的数据库表结构。
|
> 可以[前往此处](https://github.com/amtoaer/bili-sync/tree/main/crates/bili_sync_entity/src/entities)实时查看当前版本的数据库表结构。
|
||||||
|
|
||||||
既然拥有着明显的层级关系,那数据库表就很容易设计了。为了简化实现,程序没有额外考虑单个 video 被多个 video list 引用的情况(如一个视频同时在收藏夹和稍后再看中)。而是简单的将其设计为了不交叉的层级结构。
|
既然拥有着明显的层级关系,那数据库表就很容易设计了。为了简化实现,程序没有额外考虑单个 video 被多个 video source 引用的情况(如一个视频同时在收藏夹和稍后再看中)。而是简单的将其设计为了不交叉的层级结构。
|
||||||
|
|
||||||
### video list 表
|
### video source 表
|
||||||
|
|
||||||
从上面的介绍可以看出,video list 并不是一个具体的结构,而是拥有多种实现的抽象概念。我选择将其特化实现为多张表:
|
从上面的介绍可以看出,video source 并不是一个具体的结构,而是拥有多种实现的抽象概念。我选择将其特化实现为多张表:
|
||||||
|
|
||||||
1. favorite:收藏夹;
|
1. favorite:收藏夹;
|
||||||
2. watch_later:稍后再看;
|
2. watch_later:稍后再看;
|
||||||
@@ -67,9 +67,9 @@ EMBY 的一般结构是: `媒体库 - 文件夹 - 电影/电视剧 - 分季/
|
|||||||
|
|
||||||
### video 表
|
### video 表
|
||||||
|
|
||||||
video 表包含了 video 的基本信息,如 bvid、标题、封面、描述、标签等。此外,video 表还包含了与 video list 的关联。
|
video 表包含了 video 的基本信息,如 bvid、标题、封面、描述、标签等。此外,video 表还包含了与 video source 的关联。
|
||||||
|
|
||||||
具体来说,每一种 video list 都在 video 表中有一个对应的列,指向 video list 表中的 id,如 favorite_id、collection_id 等。接下来将这些键与 video 的 bvid 绑在一起建立唯一索引,就可以保证在同一个 video list 中不会有重复的 video。
|
具体来说,每一种 video source 都在 video 表中有一个对应的列,指向 video source 表中的 id,如 favorite_id、collection_id 等。接下来将这些键与 video 的 bvid 绑在一起建立唯一索引,就可以保证在同一个 video source 中不会有重复的 video。
|
||||||
|
|
||||||
### page 表
|
### page 表
|
||||||
|
|
||||||
@@ -81,20 +81,20 @@ page 表包含了 page 的基本信息,如 cid、标题、封面等。与 vide
|
|||||||
|
|
||||||
程序启动时会读取配置文件、迁移数据库、初始化日志等操作。如果发现需要的文件不存在,程序会自动创建。
|
程序启动时会读取配置文件、迁移数据库、初始化日志等操作。如果发现需要的文件不存在,程序会自动创建。
|
||||||
|
|
||||||
### 扫描 video list 获取新视频
|
### 扫描 video source 获取新视频
|
||||||
|
|
||||||
> [!WARNING]
|
> [!WARNING]
|
||||||
> b 站实现接口时为了节省资源,通过 video list 获取到的 video 列表通常是分页且不包含详细信息的。
|
> b 站实现接口时为了节省资源,通过 video source 获取到的 video 列表通常是分页且不包含详细信息的。
|
||||||
|
|
||||||
程序会扫描所有配置文件中包含的 video list,获取其中包含的 video 的简单信息并填充到数据库。在实现时需要避免频繁的全量扫描。
|
程序会扫描所有配置文件中包含的 video source,获取其中包含的 video 的简单信息并填充到数据库。在实现时需要避免频繁的全量扫描。
|
||||||
|
|
||||||
具体到 bili-sync 的实现中,每个 video list 都有一个 `latest_row_at` 列,用于记录处理过的最新视频时间。程序在请求接口时会设置按时间排序,确保新视频位于前面。排序依据的时间根据 video list 的类型而定:收藏夹按收藏时间,投稿按照投稿时间...
|
具体到 bili-sync 的实现中,每个 video source 都有一个 `latest_row_at` 列,用于记录处理过的最新视频时间。程序在请求接口时会设置按时间排序,确保新视频位于前面。排序依据的时间根据 video source 的类型而定:收藏夹按收藏时间,投稿按照投稿时间...
|
||||||
|
|
||||||
拉取过程会逐页请求,程序会不断将获取到的视频保存到数据库中,直到发现第一个小于等于 `latest_row_at` 的视频时停止。接着将 `latest_row_at` 更新为最新的视频时间。
|
拉取过程会逐页请求,程序会不断将获取到的视频保存到数据库中,直到发现第一个小于等于 `latest_row_at` 的视频时停止。接着将 `latest_row_at` 更新为最新的视频时间。
|
||||||
|
|
||||||
### 填充 video 详情
|
### 填充 video 详情
|
||||||
|
|
||||||
将新增视频的简单信息写入数据库后,下一步会填充 video 详情。正如上文所述:**通过 video list 获取到的 video 列表通常是不包含详细信息的**,因此需要额外的请求来填充这些信息。
|
将新增视频的简单信息写入数据库后,下一步会填充 video 详情。正如上文所述:**通过 video source 获取到的 video 列表通常是不包含详细信息的**,因此需要额外的请求来填充这些信息。
|
||||||
|
|
||||||
这一步会筛选出所有未完全填充信息的 video,逐个获取 video 的详细信息(如标签、包含的 page 等)并填充到数据库中。
|
这一步会筛选出所有未完全填充信息的 video,逐个获取 video 的详细信息(如标签、包含的 page 等)并填充到数据库中。
|
||||||
|
|
||||||
@@ -108,4 +108,4 @@ page 表包含了 page 的基本信息,如 cid、标题、封面等。与 vide
|
|||||||
|
|
||||||
如果某些部分下载失败,status 字段会记录这些部分的失败次数,程序会在下次下载时重试。如果重试次数超过了设定的阈值,那么视频会被标记为下载失败,后续直接忽略。
|
如果某些部分下载失败,status 字段会记录这些部分的失败次数,程序会在下次下载时重试。如果重试次数超过了设定的阈值,那么视频会被标记为下载失败,后续直接忽略。
|
||||||
|
|
||||||
此处程序对风控做了额外的处理,一般风控发生时接下来的所有请求都会失败,因此程序检测到风控时不会认为是某个视频下载失败,而是直接终止 video list 的全部下载任务,等待下次扫描时重试。
|
此处程序对风控做了额外的处理,一般风控发生时接下来的所有请求都会失败,因此程序检测到风控时不会认为是某个视频下载失败,而是直接终止 video source 的全部下载任务,等待下次扫描时重试。
|
||||||
13
docs/frontend.md
Normal file
13
docs/frontend.md
Normal file
@@ -0,0 +1,13 @@
|
|||||||
|
# 管理页
|
||||||
|
|
||||||
|
在 2.4.0 版本,bili-sync 提供了一个内置的管理页,可以使用浏览器访问,实现一些简单的预览和重置操作。
|
||||||
|
|
||||||
|
由于作者的前端水平有限,网页使用 90% AI + 10% 人工实现,问题会比较多,欢迎前端大能 PR(应该说比起 PR 缝缝补补不如直接重写 XD)。
|
||||||
|
|
||||||
|

|
||||||
|
|
||||||
|
# API
|
||||||
|
|
||||||
|
后端提供的 API 可以通过 `/swagger-ui/` 访问:
|
||||||
|
|
||||||
|

|
||||||
@@ -1,7 +1,7 @@
|
|||||||
# bili-sync 是什么?
|
# bili-sync 是什么?
|
||||||
|
|
||||||
> [!TIP]
|
> [!TIP]
|
||||||
> 当前最新程序版本为 v2.3.0,文档将始终与最新程序版本保持一致。
|
> 当前最新程序版本为 v2.4.1,文档将始终与最新程序版本保持一致。
|
||||||
|
|
||||||
bili-sync 是一款专为 NAS 用户编写的哔哩哔哩同步工具。
|
bili-sync 是一款专为 NAS 用户编写的哔哩哔哩同步工具。
|
||||||
|
|
||||||
|
|||||||
25
docs/question.md
Normal file
25
docs/question.md
Normal file
@@ -0,0 +1,25 @@
|
|||||||
|
# 常见问题
|
||||||
|
|
||||||
|
## 各种文件找不到问题,如运行后找不到初始 `config.toml`、提示成功下载但看不到视频文件等。
|
||||||
|
|
||||||
|
请检查挂载位置与配置文件填写是否正确,需要理解的是:
|
||||||
|
1. 容器挂载是把宿主机的 `/A` 挂载到容器内的 `/B`;
|
||||||
|
2. 程序运行在容器中,能够读取、写入的目录只能是 `/B`,因此配置文件内填写的路径只能与 `/B` 有关。
|
||||||
|
|
||||||
|
## 下载视频出现 Permission denied、Operation not permitted 等错误。
|
||||||
|
|
||||||
|
有两种可能的原因:
|
||||||
|
1. 容器运行时指定了 `user`(非 root),但配置文件并未正确填写挂载后的路径。此时目标路径只是一个普通的容器内路径,非 root 用户无法修改,导致执行出错;
|
||||||
|
2. 配置文件正确填写了挂载后的路径,此时出现权限错误说明你为容器指定的 `user` 无权写入宿主机上的原始路径。需检查宿主机原始路径的文件权限。
|
||||||
|
|
||||||
|
## 下载某个视频连续多次出现 `error decoding response body` 错误
|
||||||
|
|
||||||
|
这个问题我也出现过几次,目前还不清楚原因,但怀疑是 b 站服务器使用某种检测机制拒绝了响应。
|
||||||
|
|
||||||
|
bili-sync 在 2.4.0 版本引入了一个改动,不将此错误计入错误次数,允许其无限重试,我过去下载失败的某个视频使用这个策略在多次尝试后成功了。
|
||||||
|
|
||||||
|
尽管如此,该解决方案仍然比较玄学,需要将来能够查明具体原因再加以修复。
|
||||||
|
|
||||||
|
## 有些视频已经达到了最大重试次数还没有成功,我可以手动重试吗?
|
||||||
|
|
||||||
|
2.4.0 版本引入了一个简陋的[管理页](/frontend)来支持这个功能,你可以查询特定视频并点击重置,这样在下次下载任务触发时就会重试这个任务了。
|
||||||
@@ -33,6 +33,10 @@ services:
|
|||||||
user: 1000:1000
|
user: 1000:1000
|
||||||
hostname: bili-sync-rs
|
hostname: bili-sync-rs
|
||||||
container_name: bili-sync-rs
|
container_name: bili-sync-rs
|
||||||
|
# 程序默认绑定 0.0.0.0:12345 运行 http 服务
|
||||||
|
# 可同时修改 compose 文件与 config.toml 变更服务运行的端口
|
||||||
|
ports:
|
||||||
|
- 12345:12345
|
||||||
volumes:
|
volumes:
|
||||||
- ${你希望存储程序配置的目录}:/app/.config/bili-sync
|
- ${你希望存储程序配置的目录}:/app/.config/bili-sync
|
||||||
# 还需要有一些其它必要的挂载,包括 up 主信息位置、视频下载位置
|
# 还需要有一些其它必要的挂载,包括 up 主信息位置、视频下载位置
|
||||||
@@ -69,6 +73,8 @@ services:
|
|||||||
|
|
||||||
当前版本的默认示例文件如下:
|
当前版本的默认示例文件如下:
|
||||||
```toml
|
```toml
|
||||||
|
auth_token = "xxxxxxxx"
|
||||||
|
bind_address = "0.0.0.0:12345"
|
||||||
video_name = "{{title}}"
|
video_name = "{{title}}"
|
||||||
page_name = "{{bvid}}"
|
page_name = "{{bvid}}"
|
||||||
interval = 1200
|
interval = 1200
|
||||||
@@ -133,6 +139,16 @@ duration = 250
|
|||||||
|
|
||||||
虽然配置文件看起来很长,但绝大部分选项是不需要做修改的。一般来说,我们只需要关注其中的少数几个,以下逐条说明。
|
虽然配置文件看起来很长,但绝大部分选项是不需要做修改的。一般来说,我们只需要关注其中的少数几个,以下逐条说明。
|
||||||
|
|
||||||
|
### `auth_token`
|
||||||
|
|
||||||
|
表示调用程序管理 API 需要的身份凭据,程序会检查 API 请求 Header 中是否包含正确的 `Authorization` 字段。
|
||||||
|
|
||||||
|
内置管理页前端提供了 `auth_token` 的输入框,填写后即可成功调用 API 使用管理页。
|
||||||
|
|
||||||
|
### `bind_address`
|
||||||
|
|
||||||
|
程序 Web Server 监听的地址,程序启动时会监听该地址,成功后可通过 `http://${bind_address}` 访问管理页。
|
||||||
|
|
||||||
### `interval`
|
### `interval`
|
||||||
|
|
||||||
表示程序每次执行扫描下载的间隔时间,单位为秒。
|
表示程序每次执行扫描下载的间隔时间,单位为秒。
|
||||||
|
|||||||
23
web/.gitignore
vendored
Normal file
23
web/.gitignore
vendored
Normal file
@@ -0,0 +1,23 @@
|
|||||||
|
node_modules
|
||||||
|
|
||||||
|
# Output
|
||||||
|
.output
|
||||||
|
.vercel
|
||||||
|
.netlify
|
||||||
|
.wrangler
|
||||||
|
/.svelte-kit
|
||||||
|
/build
|
||||||
|
|
||||||
|
# OS
|
||||||
|
.DS_Store
|
||||||
|
Thumbs.db
|
||||||
|
|
||||||
|
# Env
|
||||||
|
.env
|
||||||
|
.env.*
|
||||||
|
!.env.example
|
||||||
|
!.env.test
|
||||||
|
|
||||||
|
# Vite
|
||||||
|
vite.config.js.timestamp-*
|
||||||
|
vite.config.ts.timestamp-*
|
||||||
1
web/.npmrc
Normal file
1
web/.npmrc
Normal file
@@ -0,0 +1 @@
|
|||||||
|
engine-strict=true
|
||||||
4
web/.prettierignore
Normal file
4
web/.prettierignore
Normal file
@@ -0,0 +1,4 @@
|
|||||||
|
# Package Managers
|
||||||
|
package-lock.json
|
||||||
|
pnpm-lock.yaml
|
||||||
|
yarn.lock
|
||||||
15
web/.prettierrc
Normal file
15
web/.prettierrc
Normal file
@@ -0,0 +1,15 @@
|
|||||||
|
{
|
||||||
|
"useTabs": true,
|
||||||
|
"singleQuote": true,
|
||||||
|
"trailingComma": "none",
|
||||||
|
"printWidth": 100,
|
||||||
|
"plugins": ["prettier-plugin-svelte", "prettier-plugin-tailwindcss"],
|
||||||
|
"overrides": [
|
||||||
|
{
|
||||||
|
"files": "*.svelte",
|
||||||
|
"options": {
|
||||||
|
"parser": "svelte"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
38
web/README.md
Normal file
38
web/README.md
Normal file
@@ -0,0 +1,38 @@
|
|||||||
|
# sv
|
||||||
|
|
||||||
|
Everything you need to build a Svelte project, powered by [`sv`](https://github.com/sveltejs/cli).
|
||||||
|
|
||||||
|
## Creating a project
|
||||||
|
|
||||||
|
If you're seeing this, you've probably already done this step. Congrats!
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# create a new project in the current directory
|
||||||
|
npx sv create
|
||||||
|
|
||||||
|
# create a new project in my-app
|
||||||
|
npx sv create my-app
|
||||||
|
```
|
||||||
|
|
||||||
|
## Developing
|
||||||
|
|
||||||
|
Once you've created a project and installed dependencies with `npm install` (or `pnpm install` or `yarn`), start a development server:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
npm run dev
|
||||||
|
|
||||||
|
# or start the server and open the app in a new browser tab
|
||||||
|
npm run dev -- --open
|
||||||
|
```
|
||||||
|
|
||||||
|
## Building
|
||||||
|
|
||||||
|
To create a production version of your app:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
npm run build
|
||||||
|
```
|
||||||
|
|
||||||
|
You can preview the production build with `npm run preview`.
|
||||||
|
|
||||||
|
> To deploy your app, you may need to install an [adapter](https://svelte.dev/docs/kit/adapters) for your target environment.
|
||||||
0
web/build/.gitkeep
Normal file
0
web/build/.gitkeep
Normal file
691
web/bun.lock
Normal file
691
web/bun.lock
Normal file
@@ -0,0 +1,691 @@
|
|||||||
|
{
|
||||||
|
"lockfileVersion": 1,
|
||||||
|
"workspaces": {
|
||||||
|
"": {
|
||||||
|
"name": "web",
|
||||||
|
"devDependencies": {
|
||||||
|
"@eslint/compat": "^1.2.5",
|
||||||
|
"@eslint/js": "^9.18.0",
|
||||||
|
"@sveltejs/adapter-static": "^3.0.8",
|
||||||
|
"@sveltejs/kit": "^2.16.0",
|
||||||
|
"@sveltejs/vite-plugin-svelte": "^5.0.0",
|
||||||
|
"autoprefixer": "^10.4.20",
|
||||||
|
"bits-ui": "0.22.0",
|
||||||
|
"clsx": "^2.1.1",
|
||||||
|
"eslint": "^9.18.0",
|
||||||
|
"eslint-config-prettier": "^10.0.1",
|
||||||
|
"eslint-plugin-svelte": "^2.46.1",
|
||||||
|
"globals": "^15.14.0",
|
||||||
|
"mode-watcher": "^0.5.1",
|
||||||
|
"prettier": "^3.4.2",
|
||||||
|
"prettier-plugin-svelte": "^3.3.3",
|
||||||
|
"prettier-plugin-tailwindcss": "^0.6.10",
|
||||||
|
"svelte": "^5.0.0",
|
||||||
|
"svelte-check": "^4.0.0",
|
||||||
|
"svelte-sonner": "^0.3.28",
|
||||||
|
"tailwind-merge": "^3.0.1",
|
||||||
|
"tailwind-variants": "^0.3.1",
|
||||||
|
"tailwindcss": "^3.4.17",
|
||||||
|
"tailwindcss-animate": "^1.0.7",
|
||||||
|
"typescript": "^5.0.0",
|
||||||
|
"typescript-eslint": "^8.20.0",
|
||||||
|
"vite": "^6.0.0",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
"packages": {
|
||||||
|
"@alloc/quick-lru": ["@alloc/quick-lru@5.2.0", "", {}, "sha512-UrcABB+4bUrFABwbluTIBErXwvbsU/V7TZWfmbgJfbkwiBuziS9gxdODUyuiecfdGQ85jglMW6juS3+z5TsKLw=="],
|
||||||
|
|
||||||
|
"@ampproject/remapping": ["@ampproject/remapping@2.3.0", "", { "dependencies": { "@jridgewell/gen-mapping": "^0.3.5", "@jridgewell/trace-mapping": "^0.3.24" } }, "sha512-30iZtAPgz+LTIYoeivqYo853f02jBYSd5uGnGpkFV0M3xOt9aN73erkgYAmZU43x4VfqcnLxW9Kpg3R5LC4YYw=="],
|
||||||
|
|
||||||
|
"@esbuild/aix-ppc64": ["@esbuild/aix-ppc64@0.24.2", "", { "os": "aix", "cpu": "ppc64" }, "sha512-thpVCb/rhxE/BnMLQ7GReQLLN8q9qbHmI55F4489/ByVg2aQaQ6kbcLb6FHkocZzQhxc4gx0sCk0tJkKBFzDhA=="],
|
||||||
|
|
||||||
|
"@esbuild/android-arm": ["@esbuild/android-arm@0.24.2", "", { "os": "android", "cpu": "arm" }, "sha512-tmwl4hJkCfNHwFB3nBa8z1Uy3ypZpxqxfTQOcHX+xRByyYgunVbZ9MzUUfb0RxaHIMnbHagwAxuTL+tnNM+1/Q=="],
|
||||||
|
|
||||||
|
"@esbuild/android-arm64": ["@esbuild/android-arm64@0.24.2", "", { "os": "android", "cpu": "arm64" }, "sha512-cNLgeqCqV8WxfcTIOeL4OAtSmL8JjcN6m09XIgro1Wi7cF4t/THaWEa7eL5CMoMBdjoHOTh/vwTO/o2TRXIyzg=="],
|
||||||
|
|
||||||
|
"@esbuild/android-x64": ["@esbuild/android-x64@0.24.2", "", { "os": "android", "cpu": "x64" }, "sha512-B6Q0YQDqMx9D7rvIcsXfmJfvUYLoP722bgfBlO5cGvNVb5V/+Y7nhBE3mHV9OpxBf4eAS2S68KZztiPaWq4XYw=="],
|
||||||
|
|
||||||
|
"@esbuild/darwin-arm64": ["@esbuild/darwin-arm64@0.24.2", "", { "os": "darwin", "cpu": "arm64" }, "sha512-kj3AnYWc+CekmZnS5IPu9D+HWtUI49hbnyqk0FLEJDbzCIQt7hg7ucF1SQAilhtYpIujfaHr6O0UHlzzSPdOeA=="],
|
||||||
|
|
||||||
|
"@esbuild/darwin-x64": ["@esbuild/darwin-x64@0.24.2", "", { "os": "darwin", "cpu": "x64" }, "sha512-WeSrmwwHaPkNR5H3yYfowhZcbriGqooyu3zI/3GGpF8AyUdsrrP0X6KumITGA9WOyiJavnGZUwPGvxvwfWPHIA=="],
|
||||||
|
|
||||||
|
"@esbuild/freebsd-arm64": ["@esbuild/freebsd-arm64@0.24.2", "", { "os": "freebsd", "cpu": "arm64" }, "sha512-UN8HXjtJ0k/Mj6a9+5u6+2eZ2ERD7Edt1Q9IZiB5UZAIdPnVKDoG7mdTVGhHJIeEml60JteamR3qhsr1r8gXvg=="],
|
||||||
|
|
||||||
|
"@esbuild/freebsd-x64": ["@esbuild/freebsd-x64@0.24.2", "", { "os": "freebsd", "cpu": "x64" }, "sha512-TvW7wE/89PYW+IevEJXZ5sF6gJRDY/14hyIGFXdIucxCsbRmLUcjseQu1SyTko+2idmCw94TgyaEZi9HUSOe3Q=="],
|
||||||
|
|
||||||
|
"@esbuild/linux-arm": ["@esbuild/linux-arm@0.24.2", "", { "os": "linux", "cpu": "arm" }, "sha512-n0WRM/gWIdU29J57hJyUdIsk0WarGd6To0s+Y+LwvlC55wt+GT/OgkwoXCXvIue1i1sSNWblHEig00GBWiJgfA=="],
|
||||||
|
|
||||||
|
"@esbuild/linux-arm64": ["@esbuild/linux-arm64@0.24.2", "", { "os": "linux", "cpu": "arm64" }, "sha512-7HnAD6074BW43YvvUmE/35Id9/NB7BeX5EoNkK9obndmZBUk8xmJJeU7DwmUeN7tkysslb2eSl6CTrYz6oEMQg=="],
|
||||||
|
|
||||||
|
"@esbuild/linux-ia32": ["@esbuild/linux-ia32@0.24.2", "", { "os": "linux", "cpu": "ia32" }, "sha512-sfv0tGPQhcZOgTKO3oBE9xpHuUqguHvSo4jl+wjnKwFpapx+vUDcawbwPNuBIAYdRAvIDBfZVvXprIj3HA+Ugw=="],
|
||||||
|
|
||||||
|
"@esbuild/linux-loong64": ["@esbuild/linux-loong64@0.24.2", "", { "os": "linux", "cpu": "none" }, "sha512-CN9AZr8kEndGooS35ntToZLTQLHEjtVB5n7dl8ZcTZMonJ7CCfStrYhrzF97eAecqVbVJ7APOEe18RPI4KLhwQ=="],
|
||||||
|
|
||||||
|
"@esbuild/linux-mips64el": ["@esbuild/linux-mips64el@0.24.2", "", { "os": "linux", "cpu": "none" }, "sha512-iMkk7qr/wl3exJATwkISxI7kTcmHKE+BlymIAbHO8xanq/TjHaaVThFF6ipWzPHryoFsesNQJPE/3wFJw4+huw=="],
|
||||||
|
|
||||||
|
"@esbuild/linux-ppc64": ["@esbuild/linux-ppc64@0.24.2", "", { "os": "linux", "cpu": "ppc64" }, "sha512-shsVrgCZ57Vr2L8mm39kO5PPIb+843FStGt7sGGoqiiWYconSxwTiuswC1VJZLCjNiMLAMh34jg4VSEQb+iEbw=="],
|
||||||
|
|
||||||
|
"@esbuild/linux-riscv64": ["@esbuild/linux-riscv64@0.24.2", "", { "os": "linux", "cpu": "none" }, "sha512-4eSFWnU9Hhd68fW16GD0TINewo1L6dRrB+oLNNbYyMUAeOD2yCK5KXGK1GH4qD/kT+bTEXjsyTCiJGHPZ3eM9Q=="],
|
||||||
|
|
||||||
|
"@esbuild/linux-s390x": ["@esbuild/linux-s390x@0.24.2", "", { "os": "linux", "cpu": "s390x" }, "sha512-S0Bh0A53b0YHL2XEXC20bHLuGMOhFDO6GN4b3YjRLK//Ep3ql3erpNcPlEFed93hsQAjAQDNsvcK+hV90FubSw=="],
|
||||||
|
|
||||||
|
"@esbuild/linux-x64": ["@esbuild/linux-x64@0.24.2", "", { "os": "linux", "cpu": "x64" }, "sha512-8Qi4nQcCTbLnK9WoMjdC9NiTG6/E38RNICU6sUNqK0QFxCYgoARqVqxdFmWkdonVsvGqWhmm7MO0jyTqLqwj0Q=="],
|
||||||
|
|
||||||
|
"@esbuild/netbsd-arm64": ["@esbuild/netbsd-arm64@0.24.2", "", { "os": "none", "cpu": "arm64" }, "sha512-wuLK/VztRRpMt9zyHSazyCVdCXlpHkKm34WUyinD2lzK07FAHTq0KQvZZlXikNWkDGoT6x3TD51jKQ7gMVpopw=="],
|
||||||
|
|
||||||
|
"@esbuild/netbsd-x64": ["@esbuild/netbsd-x64@0.24.2", "", { "os": "none", "cpu": "x64" }, "sha512-VefFaQUc4FMmJuAxmIHgUmfNiLXY438XrL4GDNV1Y1H/RW3qow68xTwjZKfj/+Plp9NANmzbH5R40Meudu8mmw=="],
|
||||||
|
|
||||||
|
"@esbuild/openbsd-arm64": ["@esbuild/openbsd-arm64@0.24.2", "", { "os": "openbsd", "cpu": "arm64" }, "sha512-YQbi46SBct6iKnszhSvdluqDmxCJA+Pu280Av9WICNwQmMxV7nLRHZfjQzwbPs3jeWnuAhE9Jy0NrnJ12Oz+0A=="],
|
||||||
|
|
||||||
|
"@esbuild/openbsd-x64": ["@esbuild/openbsd-x64@0.24.2", "", { "os": "openbsd", "cpu": "x64" }, "sha512-+iDS6zpNM6EnJyWv0bMGLWSWeXGN/HTaF/LXHXHwejGsVi+ooqDfMCCTerNFxEkM3wYVcExkeGXNqshc9iMaOA=="],
|
||||||
|
|
||||||
|
"@esbuild/sunos-x64": ["@esbuild/sunos-x64@0.24.2", "", { "os": "sunos", "cpu": "x64" }, "sha512-hTdsW27jcktEvpwNHJU4ZwWFGkz2zRJUz8pvddmXPtXDzVKTTINmlmga3ZzwcuMpUvLw7JkLy9QLKyGpD2Yxig=="],
|
||||||
|
|
||||||
|
"@esbuild/win32-arm64": ["@esbuild/win32-arm64@0.24.2", "", { "os": "win32", "cpu": "arm64" }, "sha512-LihEQ2BBKVFLOC9ZItT9iFprsE9tqjDjnbulhHoFxYQtQfai7qfluVODIYxt1PgdoyQkz23+01rzwNwYfutxUQ=="],
|
||||||
|
|
||||||
|
"@esbuild/win32-ia32": ["@esbuild/win32-ia32@0.24.2", "", { "os": "win32", "cpu": "ia32" }, "sha512-q+iGUwfs8tncmFC9pcnD5IvRHAzmbwQ3GPS5/ceCyHdjXubwQWI12MKWSNSMYLJMq23/IUCvJMS76PDqXe1fxA=="],
|
||||||
|
|
||||||
|
"@esbuild/win32-x64": ["@esbuild/win32-x64@0.24.2", "", { "os": "win32", "cpu": "x64" }, "sha512-7VTgWzgMGvup6aSqDPLiW5zHaxYJGTO4OokMjIlrCtf+VpEL+cXKtCvg723iguPYI5oaUNdS+/V7OU2gvXVWEg=="],
|
||||||
|
|
||||||
|
"@eslint-community/eslint-utils": ["@eslint-community/eslint-utils@4.4.1", "", { "dependencies": { "eslint-visitor-keys": "^3.4.3" }, "peerDependencies": { "eslint": "^6.0.0 || ^7.0.0 || >=8.0.0" } }, "sha512-s3O3waFUrMV8P/XaF/+ZTp1X9XBZW1a4B97ZnjQF2KYWaFD2A8KyFBsrsfSjEmjn3RGWAIuvlneuZm3CUK3jbA=="],
|
||||||
|
|
||||||
|
"@eslint-community/regexpp": ["@eslint-community/regexpp@4.12.1", "", {}, "sha512-CCZCDJuduB9OUkFkY2IgppNZMi2lBQgD2qzwXkEia16cge2pijY/aXi96CJMquDMn3nJdlPV1A5KrJEXwfLNzQ=="],
|
||||||
|
|
||||||
|
"@eslint/compat": ["@eslint/compat@1.2.6", "", { "peerDependencies": { "eslint": "^9.10.0" }, "optionalPeers": ["eslint"] }, "sha512-k7HNCqApoDHM6XzT30zGoETj+D+uUcZUb+IVAJmar3u6bvHf7hhHJcWx09QHj4/a2qrKZMWU0E16tvkiAdv06Q=="],
|
||||||
|
|
||||||
|
"@eslint/config-array": ["@eslint/config-array@0.19.2", "", { "dependencies": { "@eslint/object-schema": "^2.1.6", "debug": "^4.3.1", "minimatch": "^3.1.2" } }, "sha512-GNKqxfHG2ySmJOBSHg7LxeUx4xpuCoFjacmlCoYWEbaPXLwvfIjixRI12xCQZeULksQb23uiA8F40w5TojpV7w=="],
|
||||||
|
|
||||||
|
"@eslint/core": ["@eslint/core@0.11.0", "", { "dependencies": { "@types/json-schema": "^7.0.15" } }, "sha512-DWUB2pksgNEb6Bz2fggIy1wh6fGgZP4Xyy/Mt0QZPiloKKXerbqq9D3SBQTlCRYOrcRPu4vuz+CGjwdfqxnoWA=="],
|
||||||
|
|
||||||
|
"@eslint/eslintrc": ["@eslint/eslintrc@3.2.0", "", { "dependencies": { "ajv": "^6.12.4", "debug": "^4.3.2", "espree": "^10.0.1", "globals": "^14.0.0", "ignore": "^5.2.0", "import-fresh": "^3.2.1", "js-yaml": "^4.1.0", "minimatch": "^3.1.2", "strip-json-comments": "^3.1.1" } }, "sha512-grOjVNN8P3hjJn/eIETF1wwd12DdnwFDoyceUJLYYdkpbwq3nLi+4fqrTAONx7XDALqlL220wC/RHSC/QTI/0w=="],
|
||||||
|
|
||||||
|
"@eslint/js": ["@eslint/js@9.20.0", "", {}, "sha512-iZA07H9io9Wn836aVTytRaNqh00Sad+EamwOVJT12GTLw1VGMFV/4JaME+JjLtr9fiGaoWgYnS54wrfWsSs4oQ=="],
|
||||||
|
|
||||||
|
"@eslint/object-schema": ["@eslint/object-schema@2.1.6", "", {}, "sha512-RBMg5FRL0I0gs51M/guSAj5/e14VQ4tpZnQNWwuDT66P14I43ItmPfIZRhO9fUVIPOAQXU47atlywZ/czoqFPA=="],
|
||||||
|
|
||||||
|
"@eslint/plugin-kit": ["@eslint/plugin-kit@0.2.5", "", { "dependencies": { "@eslint/core": "^0.10.0", "levn": "^0.4.1" } }, "sha512-lB05FkqEdUg2AA0xEbUz0SnkXT1LcCTa438W4IWTUh4hdOnVbQyOJ81OrDXsJk/LSiJHubgGEFoR5EHq1NsH1A=="],
|
||||||
|
|
||||||
|
"@floating-ui/core": ["@floating-ui/core@1.6.9", "", { "dependencies": { "@floating-ui/utils": "^0.2.9" } }, "sha512-uMXCuQ3BItDUbAMhIXw7UPXRfAlOAvZzdK9BWpE60MCn+Svt3aLn9jsPTi/WNGlRUu2uI0v5S7JiIUsbsvh3fw=="],
|
||||||
|
|
||||||
|
"@floating-ui/dom": ["@floating-ui/dom@1.6.13", "", { "dependencies": { "@floating-ui/core": "^1.6.0", "@floating-ui/utils": "^0.2.9" } }, "sha512-umqzocjDgNRGTuO7Q8CU32dkHkECqI8ZdMZ5Swb6QAM0t5rnlrN3lGo1hdpscRd3WS8T6DKYK4ephgIH9iRh3w=="],
|
||||||
|
|
||||||
|
"@floating-ui/utils": ["@floating-ui/utils@0.2.9", "", {}, "sha512-MDWhGtE+eHw5JW7lq4qhc5yRLS11ERl1c7Z6Xd0a58DozHES6EnNNwUWbMiG4J9Cgj053Bhk8zvlhFYKVhULwg=="],
|
||||||
|
|
||||||
|
"@humanfs/core": ["@humanfs/core@0.19.1", "", {}, "sha512-5DyQ4+1JEUzejeK1JGICcideyfUbGixgS9jNgex5nqkW+cY7WZhxBigmieN5Qnw9ZosSNVC9KQKyb+GUaGyKUA=="],
|
||||||
|
|
||||||
|
"@humanfs/node": ["@humanfs/node@0.16.6", "", { "dependencies": { "@humanfs/core": "^0.19.1", "@humanwhocodes/retry": "^0.3.0" } }, "sha512-YuI2ZHQL78Q5HbhDiBA1X4LmYdXCKCMQIfw0pw7piHJwyREFebJUvrQN4cMssyES6x+vfUbx1CIpaQUKYdQZOw=="],
|
||||||
|
|
||||||
|
"@humanwhocodes/module-importer": ["@humanwhocodes/module-importer@1.0.1", "", {}, "sha512-bxveV4V8v5Yb4ncFTT3rPSgZBOpCkjfK0y4oVVVJwIuDVBRMDXrPyXRL988i5ap9m9bnyEEjWfm5WkBmtffLfA=="],
|
||||||
|
|
||||||
|
"@humanwhocodes/retry": ["@humanwhocodes/retry@0.4.1", "", {}, "sha512-c7hNEllBlenFTHBky65mhq8WD2kbN9Q6gk0bTk8lSBvc554jpXSkST1iePudpt7+A/AQvuHs9EMqjHDXMY1lrA=="],
|
||||||
|
|
||||||
|
"@internationalized/date": ["@internationalized/date@3.7.0", "", { "dependencies": { "@swc/helpers": "^0.5.0" } }, "sha512-VJ5WS3fcVx0bejE/YHfbDKR/yawZgKqn/if+oEeLqNwBtPzVB06olkfcnojTmEMX+gTpH+FlQ69SHNitJ8/erQ=="],
|
||||||
|
|
||||||
|
"@isaacs/cliui": ["@isaacs/cliui@8.0.2", "", { "dependencies": { "string-width": "^5.1.2", "string-width-cjs": "npm:string-width@^4.2.0", "strip-ansi": "^7.0.1", "strip-ansi-cjs": "npm:strip-ansi@^6.0.1", "wrap-ansi": "^8.1.0", "wrap-ansi-cjs": "npm:wrap-ansi@^7.0.0" } }, "sha512-O8jcjabXaleOG9DQ0+ARXWZBTfnP4WNAqzuiJK7ll44AmxGKv/J2M4TPjxjY3znBCfvBXFzucm1twdyFybFqEA=="],
|
||||||
|
|
||||||
|
"@jridgewell/gen-mapping": ["@jridgewell/gen-mapping@0.3.8", "", { "dependencies": { "@jridgewell/set-array": "^1.2.1", "@jridgewell/sourcemap-codec": "^1.4.10", "@jridgewell/trace-mapping": "^0.3.24" } }, "sha512-imAbBGkb+ebQyxKgzv5Hu2nmROxoDOXHh80evxdoXNOrvAnVx7zimzc1Oo5h9RlfV4vPXaE2iM5pOFbvOCClWA=="],
|
||||||
|
|
||||||
|
"@jridgewell/resolve-uri": ["@jridgewell/resolve-uri@3.1.2", "", {}, "sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw=="],
|
||||||
|
|
||||||
|
"@jridgewell/set-array": ["@jridgewell/set-array@1.2.1", "", {}, "sha512-R8gLRTZeyp03ymzP/6Lil/28tGeGEzhx1q2k703KGWRAI1VdvPIXdG70VJc2pAMw3NA6JKL5hhFu1sJX0Mnn/A=="],
|
||||||
|
|
||||||
|
"@jridgewell/sourcemap-codec": ["@jridgewell/sourcemap-codec@1.5.0", "", {}, "sha512-gv3ZRaISU3fjPAgNsriBRqGWQL6quFx04YMPW/zD8XMLsU32mhCCbfbO6KZFLjvYpCZ8zyDEgqsgf+PwPaM7GQ=="],
|
||||||
|
|
||||||
|
"@jridgewell/trace-mapping": ["@jridgewell/trace-mapping@0.3.25", "", { "dependencies": { "@jridgewell/resolve-uri": "^3.1.0", "@jridgewell/sourcemap-codec": "^1.4.14" } }, "sha512-vNk6aEwybGtawWmy/PzwnGDOjCkLWSD2wqvjGGAgOAwCGWySYXfYoxt00IJkTF+8Lb57DwOb3Aa0o9CApepiYQ=="],
|
||||||
|
|
||||||
|
"@melt-ui/svelte": ["@melt-ui/svelte@0.76.2", "", { "dependencies": { "@floating-ui/core": "^1.3.1", "@floating-ui/dom": "^1.4.5", "@internationalized/date": "^3.5.0", "dequal": "^2.0.3", "focus-trap": "^7.5.2", "nanoid": "^5.0.4" }, "peerDependencies": { "svelte": ">=3 <5" } }, "sha512-7SbOa11tXUS95T3fReL+dwDs5FyJtCEqrqG3inRziDws346SYLsxOQ6HmX+4BkIsQh1R8U3XNa+EMmdMt38lMA=="],
|
||||||
|
|
||||||
|
"@nodelib/fs.scandir": ["@nodelib/fs.scandir@2.1.5", "", { "dependencies": { "@nodelib/fs.stat": "2.0.5", "run-parallel": "^1.1.9" } }, "sha512-vq24Bq3ym5HEQm2NKCr3yXDwjc7vTsEThRDnkp2DK9p1uqLR+DHurm/NOTo0KG7HYHU7eppKZj3MyqYuMBf62g=="],
|
||||||
|
|
||||||
|
"@nodelib/fs.stat": ["@nodelib/fs.stat@2.0.5", "", {}, "sha512-RkhPPp2zrqDAQA/2jNhnztcPAlv64XdhIp7a7454A5ovI7Bukxgt7MX7udwAu3zg1DcpPU0rz3VV1SeaqvY4+A=="],
|
||||||
|
|
||||||
|
"@nodelib/fs.walk": ["@nodelib/fs.walk@1.2.8", "", { "dependencies": { "@nodelib/fs.scandir": "2.1.5", "fastq": "^1.6.0" } }, "sha512-oGB+UxlgWcgQkgwo8GcEGwemoTFt3FIO9ababBmaGwXIoBKZ+GTy0pP185beGg7Llih/NSHSV2XAs1lnznocSg=="],
|
||||||
|
|
||||||
|
"@pkgjs/parseargs": ["@pkgjs/parseargs@0.11.0", "", {}, "sha512-+1VkjdD0QBLPodGrJUeqarH8VAIvQODIbwh9XpP5Syisf7YoQgsJKPNFoqqLQlu+VQ/tVSshMR6loPMn8U+dPg=="],
|
||||||
|
|
||||||
|
"@polka/url": ["@polka/url@1.0.0-next.28", "", {}, "sha512-8LduaNlMZGwdZ6qWrKlfa+2M4gahzFkprZiAt2TF8uS0qQgBizKXpXURqvTJ4WtmupWxaLqjRb2UCTe72mu+Aw=="],
|
||||||
|
|
||||||
|
"@rollup/rollup-android-arm-eabi": ["@rollup/rollup-android-arm-eabi@4.34.7", "", { "os": "android", "cpu": "arm" }, "sha512-l6CtzHYo8D2TQ3J7qJNpp3Q1Iye56ssIAtqbM2H8axxCEEwvN7o8Ze9PuIapbxFL3OHrJU2JBX6FIIVnP/rYyw=="],
|
||||||
|
|
||||||
|
"@rollup/rollup-android-arm64": ["@rollup/rollup-android-arm64@4.34.7", "", { "os": "android", "cpu": "arm64" }, "sha512-KvyJpFUueUnSp53zhAa293QBYqwm94TgYTIfXyOTtidhm5V0LbLCJQRGkQClYiX3FXDQGSvPxOTD/6rPStMMDg=="],
|
||||||
|
|
||||||
|
"@rollup/rollup-darwin-arm64": ["@rollup/rollup-darwin-arm64@4.34.7", "", { "os": "darwin", "cpu": "arm64" }, "sha512-jq87CjmgL9YIKvs8ybtIC98s/M3HdbqXhllcy9EdLV0yMg1DpxES2gr65nNy7ObNo/vZ/MrOTxt0bE5LinL6mA=="],
|
||||||
|
|
||||||
|
"@rollup/rollup-darwin-x64": ["@rollup/rollup-darwin-x64@4.34.7", "", { "os": "darwin", "cpu": "x64" }, "sha512-rSI/m8OxBjsdnMMg0WEetu/w+LhLAcCDEiL66lmMX4R3oaml3eXz3Dxfvrxs1FbzPbJMaItQiksyMfv1hoIxnA=="],
|
||||||
|
|
||||||
|
"@rollup/rollup-freebsd-arm64": ["@rollup/rollup-freebsd-arm64@4.34.7", "", { "os": "freebsd", "cpu": "arm64" }, "sha512-oIoJRy3ZrdsXpFuWDtzsOOa/E/RbRWXVokpVrNnkS7npz8GEG++E1gYbzhYxhxHbO2om1T26BZjVmdIoyN2WtA=="],
|
||||||
|
|
||||||
|
"@rollup/rollup-freebsd-x64": ["@rollup/rollup-freebsd-x64@4.34.7", "", { "os": "freebsd", "cpu": "x64" }, "sha512-X++QSLm4NZfZ3VXGVwyHdRf58IBbCu9ammgJxuWZYLX0du6kZvdNqPwrjvDfwmi6wFdvfZ/s6K7ia0E5kI7m8Q=="],
|
||||||
|
|
||||||
|
"@rollup/rollup-linux-arm-gnueabihf": ["@rollup/rollup-linux-arm-gnueabihf@4.34.7", "", { "os": "linux", "cpu": "arm" }, "sha512-Z0TzhrsNqukTz3ISzrvyshQpFnFRfLunYiXxlCRvcrb3nvC5rVKI+ZXPFG/Aa4jhQa1gHgH3A0exHaRRN4VmdQ=="],
|
||||||
|
|
||||||
|
"@rollup/rollup-linux-arm-musleabihf": ["@rollup/rollup-linux-arm-musleabihf@4.34.7", "", { "os": "linux", "cpu": "arm" }, "sha512-nkznpyXekFAbvFBKBy4nNppSgneB1wwG1yx/hujN3wRnhnkrYVugMTCBXED4+Ni6thoWfQuHNYbFjgGH0MBXtw=="],
|
||||||
|
|
||||||
|
"@rollup/rollup-linux-arm64-gnu": ["@rollup/rollup-linux-arm64-gnu@4.34.7", "", { "os": "linux", "cpu": "arm64" }, "sha512-KCjlUkcKs6PjOcxolqrXglBDcfCuUCTVlX5BgzgoJHw+1rWH1MCkETLkLe5iLLS9dP5gKC7mp3y6x8c1oGBUtA=="],
|
||||||
|
|
||||||
|
"@rollup/rollup-linux-arm64-musl": ["@rollup/rollup-linux-arm64-musl@4.34.7", "", { "os": "linux", "cpu": "arm64" }, "sha512-uFLJFz6+utmpbR313TTx+NpPuAXbPz4BhTQzgaP0tozlLnGnQ6rCo6tLwaSa6b7l6gRErjLicXQ1iPiXzYotjw=="],
|
||||||
|
|
||||||
|
"@rollup/rollup-linux-loongarch64-gnu": ["@rollup/rollup-linux-loongarch64-gnu@4.34.7", "", { "os": "linux", "cpu": "none" }, "sha512-ws8pc68UcJJqCpneDFepnwlsMUFoWvPbWXT/XUrJ7rWUL9vLoIN3GAasgG+nCvq8xrE3pIrd+qLX/jotcLy0Qw=="],
|
||||||
|
|
||||||
|
"@rollup/rollup-linux-powerpc64le-gnu": ["@rollup/rollup-linux-powerpc64le-gnu@4.34.7", "", { "os": "linux", "cpu": "ppc64" }, "sha512-vrDk9JDa/BFkxcS2PbWpr0C/LiiSLxFbNOBgfbW6P8TBe9PPHx9Wqbvx2xgNi1TOAyQHQJ7RZFqBiEohm79r0w=="],
|
||||||
|
|
||||||
|
"@rollup/rollup-linux-riscv64-gnu": ["@rollup/rollup-linux-riscv64-gnu@4.34.7", "", { "os": "linux", "cpu": "none" }, "sha512-rB+ejFyjtmSo+g/a4eovDD1lHWHVqizN8P0Hm0RElkINpS0XOdpaXloqM4FBkF9ZWEzg6bezymbpLmeMldfLTw=="],
|
||||||
|
|
||||||
|
"@rollup/rollup-linux-s390x-gnu": ["@rollup/rollup-linux-s390x-gnu@4.34.7", "", { "os": "linux", "cpu": "s390x" }, "sha512-nNXNjo4As6dNqRn7OrsnHzwTgtypfRA3u3AKr0B3sOOo+HkedIbn8ZtFnB+4XyKJojIfqDKmbIzO1QydQ8c+Pw=="],
|
||||||
|
|
||||||
|
"@rollup/rollup-linux-x64-gnu": ["@rollup/rollup-linux-x64-gnu@4.34.7", "", { "os": "linux", "cpu": "x64" }, "sha512-9kPVf9ahnpOMSGlCxXGv980wXD0zRR3wyk8+33/MXQIpQEOpaNe7dEHm5LMfyRZRNt9lMEQuH0jUKj15MkM7QA=="],
|
||||||
|
|
||||||
|
"@rollup/rollup-linux-x64-musl": ["@rollup/rollup-linux-x64-musl@4.34.7", "", { "os": "linux", "cpu": "x64" }, "sha512-7wJPXRWTTPtTFDFezA8sle/1sdgxDjuMoRXEKtx97ViRxGGkVQYovem+Q8Pr/2HxiHp74SSRG+o6R0Yq0shPwQ=="],
|
||||||
|
|
||||||
|
"@rollup/rollup-win32-arm64-msvc": ["@rollup/rollup-win32-arm64-msvc@4.34.7", "", { "os": "win32", "cpu": "arm64" }, "sha512-MN7aaBC7mAjsiMEZcsJvwNsQVNZShgES/9SzWp1HC9Yjqb5OpexYnRjF7RmE4itbeesHMYYQiAtUAQaSKs2Rfw=="],
|
||||||
|
|
||||||
|
"@rollup/rollup-win32-ia32-msvc": ["@rollup/rollup-win32-ia32-msvc@4.34.7", "", { "os": "win32", "cpu": "ia32" }, "sha512-aeawEKYswsFu1LhDM9RIgToobquzdtSc4jSVqHV8uApz4FVvhFl/mKh92wc8WpFc6aYCothV/03UjY6y7yLgbg=="],
|
||||||
|
|
||||||
|
"@rollup/rollup-win32-x64-msvc": ["@rollup/rollup-win32-x64-msvc@4.34.7", "", { "os": "win32", "cpu": "x64" }, "sha512-4ZedScpxxIrVO7otcZ8kCX1mZArtH2Wfj3uFCxRJ9NO80gg1XV0U/b2f/MKaGwj2X3QopHfoWiDQ917FRpwY3w=="],
|
||||||
|
|
||||||
|
"@sveltejs/adapter-static": ["@sveltejs/adapter-static@3.0.8", "", { "peerDependencies": { "@sveltejs/kit": "^2.0.0" } }, "sha512-YaDrquRpZwfcXbnlDsSrBQNCChVOT9MGuSg+dMAyfsAa1SmiAhrA5jUYUiIMC59G92kIbY/AaQOWcBdq+lh+zg=="],
|
||||||
|
|
||||||
|
"@sveltejs/kit": ["@sveltejs/kit@2.17.2", "", { "dependencies": { "@types/cookie": "^0.6.0", "cookie": "^0.6.0", "devalue": "^5.1.0", "esm-env": "^1.2.2", "import-meta-resolve": "^4.1.0", "kleur": "^4.1.5", "magic-string": "^0.30.5", "mrmime": "^2.0.0", "sade": "^1.8.1", "set-cookie-parser": "^2.6.0", "sirv": "^3.0.0" }, "peerDependencies": { "@sveltejs/vite-plugin-svelte": "^3.0.0 || ^4.0.0-next.1 || ^5.0.0", "svelte": "^4.0.0 || ^5.0.0-next.0", "vite": "^5.0.3 || ^6.0.0" }, "bin": { "svelte-kit": "svelte-kit.js" } }, "sha512-Vypk02baf7qd3SOB1uUwUC/3Oka+srPo2J0a8YN3EfJypRshDkNx9HzNKjSmhOnGWwT+SSO06+N0mAb8iVTmTQ=="],
|
||||||
|
|
||||||
|
"@sveltejs/vite-plugin-svelte": ["@sveltejs/vite-plugin-svelte@5.0.3", "", { "dependencies": { "@sveltejs/vite-plugin-svelte-inspector": "^4.0.1", "debug": "^4.4.0", "deepmerge": "^4.3.1", "kleur": "^4.1.5", "magic-string": "^0.30.15", "vitefu": "^1.0.4" }, "peerDependencies": { "svelte": "^5.0.0", "vite": "^6.0.0" } }, "sha512-MCFS6CrQDu1yGwspm4qtli0e63vaPCehf6V7pIMP15AsWgMKrqDGCPFF/0kn4SP0ii4aySu4Pa62+fIRGFMjgw=="],
|
||||||
|
|
||||||
|
"@sveltejs/vite-plugin-svelte-inspector": ["@sveltejs/vite-plugin-svelte-inspector@4.0.1", "", { "dependencies": { "debug": "^4.3.7" }, "peerDependencies": { "@sveltejs/vite-plugin-svelte": "^5.0.0", "svelte": "^5.0.0", "vite": "^6.0.0" } }, "sha512-J/Nmb2Q2y7mck2hyCX4ckVHcR5tu2J+MtBEQqpDrrgELZ2uvraQcK/ioCV61AqkdXFgriksOKIceDcQmqnGhVw=="],
|
||||||
|
|
||||||
|
"@swc/helpers": ["@swc/helpers@0.5.15", "", { "dependencies": { "tslib": "^2.8.0" } }, "sha512-JQ5TuMi45Owi4/BIMAJBoSQoOJu12oOk/gADqlcUL9JEdHB8vyjUSsxqeNXnmXHjYKMi2WcYtezGEEhqUI/E2g=="],
|
||||||
|
|
||||||
|
"@types/cookie": ["@types/cookie@0.6.0", "", {}, "sha512-4Kh9a6B2bQciAhf7FSuMRRkUWecJgJu9nPnx3yzpsfXX/c50REIqpHY4C82bXP90qrLtXtkDxTZosYO3UpOwlA=="],
|
||||||
|
|
||||||
|
"@types/estree": ["@types/estree@1.0.6", "", {}, "sha512-AYnb1nQyY49te+VRAVgmzfcgjYS91mY5P0TKUDCLEM+gNnA+3T6rWITXRLYCpahpqSQbN5cE+gHpnPyXjHWxcw=="],
|
||||||
|
|
||||||
|
"@types/json-schema": ["@types/json-schema@7.0.15", "", {}, "sha512-5+fP8P8MFNC+AyZCDxrB2pkZFPGzqQWUzpSeuuVLvm8VMcorNYavBqoFcxK8bQz4Qsbn4oUEEem4wDLfcysGHA=="],
|
||||||
|
|
||||||
|
"@typescript-eslint/eslint-plugin": ["@typescript-eslint/eslint-plugin@8.24.0", "", { "dependencies": { "@eslint-community/regexpp": "^4.10.0", "@typescript-eslint/scope-manager": "8.24.0", "@typescript-eslint/type-utils": "8.24.0", "@typescript-eslint/utils": "8.24.0", "@typescript-eslint/visitor-keys": "8.24.0", "graphemer": "^1.4.0", "ignore": "^5.3.1", "natural-compare": "^1.4.0", "ts-api-utils": "^2.0.1" }, "peerDependencies": { "@typescript-eslint/parser": "^8.0.0 || ^8.0.0-alpha.0", "eslint": "^8.57.0 || ^9.0.0", "typescript": ">=4.8.4 <5.8.0" } }, "sha512-aFcXEJJCI4gUdXgoo/j9udUYIHgF23MFkg09LFz2dzEmU0+1Plk4rQWv/IYKvPHAtlkkGoB3m5e6oUp+JPsNaQ=="],
|
||||||
|
|
||||||
|
"@typescript-eslint/parser": ["@typescript-eslint/parser@8.24.0", "", { "dependencies": { "@typescript-eslint/scope-manager": "8.24.0", "@typescript-eslint/types": "8.24.0", "@typescript-eslint/typescript-estree": "8.24.0", "@typescript-eslint/visitor-keys": "8.24.0", "debug": "^4.3.4" }, "peerDependencies": { "eslint": "^8.57.0 || ^9.0.0", "typescript": ">=4.8.4 <5.8.0" } }, "sha512-MFDaO9CYiard9j9VepMNa9MTcqVvSny2N4hkY6roquzj8pdCBRENhErrteaQuu7Yjn1ppk0v1/ZF9CG3KIlrTA=="],
|
||||||
|
|
||||||
|
"@typescript-eslint/scope-manager": ["@typescript-eslint/scope-manager@8.24.0", "", { "dependencies": { "@typescript-eslint/types": "8.24.0", "@typescript-eslint/visitor-keys": "8.24.0" } }, "sha512-HZIX0UByphEtdVBKaQBgTDdn9z16l4aTUz8e8zPQnyxwHBtf5vtl1L+OhH+m1FGV9DrRmoDuYKqzVrvWDcDozw=="],
|
||||||
|
|
||||||
|
"@typescript-eslint/type-utils": ["@typescript-eslint/type-utils@8.24.0", "", { "dependencies": { "@typescript-eslint/typescript-estree": "8.24.0", "@typescript-eslint/utils": "8.24.0", "debug": "^4.3.4", "ts-api-utils": "^2.0.1" }, "peerDependencies": { "eslint": "^8.57.0 || ^9.0.0", "typescript": ">=4.8.4 <5.8.0" } }, "sha512-8fitJudrnY8aq0F1wMiPM1UUgiXQRJ5i8tFjq9kGfRajU+dbPyOuHbl0qRopLEidy0MwqgTHDt6CnSeXanNIwA=="],
|
||||||
|
|
||||||
|
"@typescript-eslint/types": ["@typescript-eslint/types@8.24.0", "", {}, "sha512-VacJCBTyje7HGAw7xp11q439A+zeGG0p0/p2zsZwpnMzjPB5WteaWqt4g2iysgGFafrqvyLWqq6ZPZAOCoefCw=="],
|
||||||
|
|
||||||
|
"@typescript-eslint/typescript-estree": ["@typescript-eslint/typescript-estree@8.24.0", "", { "dependencies": { "@typescript-eslint/types": "8.24.0", "@typescript-eslint/visitor-keys": "8.24.0", "debug": "^4.3.4", "fast-glob": "^3.3.2", "is-glob": "^4.0.3", "minimatch": "^9.0.4", "semver": "^7.6.0", "ts-api-utils": "^2.0.1" }, "peerDependencies": { "typescript": ">=4.8.4 <5.8.0" } }, "sha512-ITjYcP0+8kbsvT9bysygfIfb+hBj6koDsu37JZG7xrCiy3fPJyNmfVtaGsgTUSEuTzcvME5YI5uyL5LD1EV5ZQ=="],
|
||||||
|
|
||||||
|
"@typescript-eslint/utils": ["@typescript-eslint/utils@8.24.0", "", { "dependencies": { "@eslint-community/eslint-utils": "^4.4.0", "@typescript-eslint/scope-manager": "8.24.0", "@typescript-eslint/types": "8.24.0", "@typescript-eslint/typescript-estree": "8.24.0" }, "peerDependencies": { "eslint": "^8.57.0 || ^9.0.0", "typescript": ">=4.8.4 <5.8.0" } }, "sha512-07rLuUBElvvEb1ICnafYWr4hk8/U7X9RDCOqd9JcAMtjh/9oRmcfN4yGzbPVirgMR0+HLVHehmu19CWeh7fsmQ=="],
|
||||||
|
|
||||||
|
"@typescript-eslint/visitor-keys": ["@typescript-eslint/visitor-keys@8.24.0", "", { "dependencies": { "@typescript-eslint/types": "8.24.0", "eslint-visitor-keys": "^4.2.0" } }, "sha512-kArLq83QxGLbuHrTMoOEWO+l2MwsNS2TGISEdx8xgqpkbytB07XmlQyQdNDrCc1ecSqx0cnmhGvpX+VBwqqSkg=="],
|
||||||
|
|
||||||
|
"acorn": ["acorn@8.14.0", "", { "bin": { "acorn": "bin/acorn" } }, "sha512-cl669nCJTZBsL97OF4kUQm5g5hC2uihk0NxY3WENAC0TYdILVkAyHymAntgxGkl7K+t0cXIrH5siy5S4XkFycA=="],
|
||||||
|
|
||||||
|
"acorn-jsx": ["acorn-jsx@5.3.2", "", { "peerDependencies": { "acorn": "^6.0.0 || ^7.0.0 || ^8.0.0" } }, "sha512-rq9s+JNhf0IChjtDXxllJ7g41oZk5SlXtp0LHwyA5cejwn7vKmKp4pPri6YEePv2PU65sAsegbXtIinmDFDXgQ=="],
|
||||||
|
|
||||||
|
"acorn-typescript": ["acorn-typescript@1.4.13", "", { "peerDependencies": { "acorn": ">=8.9.0" } }, "sha512-xsc9Xv0xlVfwp2o7sQ+GCQ1PgbkdcpWdTzrwXxO3xDMTAywVS3oXVOcOHuRjAPkS4P9b+yc/qNF15460v+jp4Q=="],
|
||||||
|
|
||||||
|
"ajv": ["ajv@6.12.6", "", { "dependencies": { "fast-deep-equal": "^3.1.1", "fast-json-stable-stringify": "^2.0.0", "json-schema-traverse": "^0.4.1", "uri-js": "^4.2.2" } }, "sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g=="],
|
||||||
|
|
||||||
|
"ansi-regex": ["ansi-regex@6.1.0", "", {}, "sha512-7HSX4QQb4CspciLpVFwyRe79O3xsIZDDLER21kERQ71oaPodF8jL725AgJMFAYbooIqolJoRLuM81SpeUkpkvA=="],
|
||||||
|
|
||||||
|
"ansi-styles": ["ansi-styles@4.3.0", "", { "dependencies": { "color-convert": "^2.0.1" } }, "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg=="],
|
||||||
|
|
||||||
|
"any-promise": ["any-promise@1.3.0", "", {}, "sha512-7UvmKalWRt1wgjL1RrGxoSJW/0QZFIegpeGvZG9kjp8vrRu55XTHbwnqq2GpXm9uLbcuhxm3IqX9OB4MZR1b2A=="],
|
||||||
|
|
||||||
|
"anymatch": ["anymatch@3.1.3", "", { "dependencies": { "normalize-path": "^3.0.0", "picomatch": "^2.0.4" } }, "sha512-KMReFUr0B4t+D+OBkjR3KYqvocp2XaSzO55UcB6mgQMd3KbcE+mWTyvVV7D/zsdEbNnV6acZUutkiHQXvTr1Rw=="],
|
||||||
|
|
||||||
|
"arg": ["arg@5.0.2", "", {}, "sha512-PYjyFOLKQ9y57JvQ6QLo8dAgNqswh8M1RMJYdQduT6xbWSgK36P/Z/v+p888pM69jMMfS8Xd8F6I1kQ/I9HUGg=="],
|
||||||
|
|
||||||
|
"argparse": ["argparse@2.0.1", "", {}, "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q=="],
|
||||||
|
|
||||||
|
"aria-query": ["aria-query@5.3.2", "", {}, "sha512-COROpnaoap1E2F000S62r6A60uHZnmlvomhfyT2DlTcrY1OrBKn2UhH7qn5wTC9zMvD0AY7csdPSNwKP+7WiQw=="],
|
||||||
|
|
||||||
|
"autoprefixer": ["autoprefixer@10.4.20", "", { "dependencies": { "browserslist": "^4.23.3", "caniuse-lite": "^1.0.30001646", "fraction.js": "^4.3.7", "normalize-range": "^0.1.2", "picocolors": "^1.0.1", "postcss-value-parser": "^4.2.0" }, "peerDependencies": { "postcss": "^8.1.0" }, "bin": { "autoprefixer": "bin/autoprefixer" } }, "sha512-XY25y5xSv/wEoqzDyXXME4AFfkZI0P23z6Fs3YgymDnKJkCGOnkL0iTxCa85UTqaSgfcqyf3UA6+c7wUvx/16g=="],
|
||||||
|
|
||||||
|
"axobject-query": ["axobject-query@4.1.0", "", {}, "sha512-qIj0G9wZbMGNLjLmg1PT6v2mE9AH2zlnADJD/2tC6E00hgmhUOfEB6greHPAfLRSufHqROIUTkw6E+M3lH0PTQ=="],
|
||||||
|
|
||||||
|
"balanced-match": ["balanced-match@1.0.2", "", {}, "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw=="],
|
||||||
|
|
||||||
|
"binary-extensions": ["binary-extensions@2.3.0", "", {}, "sha512-Ceh+7ox5qe7LJuLHoY0feh3pHuUDHAcRUeyL2VYghZwfpkNIy/+8Ocg0a3UuSoYzavmylwuLWQOf3hl0jjMMIw=="],
|
||||||
|
|
||||||
|
"bits-ui": ["bits-ui@0.22.0", "", { "dependencies": { "@internationalized/date": "^3.5.1", "@melt-ui/svelte": "0.76.2", "nanoid": "^5.0.5" }, "peerDependencies": { "svelte": "^4.0.0 || ^5.0.0" } }, "sha512-r7Fw1HNgA4YxZBRcozl7oP0bheQ8EHh+kfMBZJgyFISix8t4p/nqDcHLmBgIiJ3T5XjYnJRorYDjIWaCfhb5fw=="],
|
||||||
|
|
||||||
|
"brace-expansion": ["brace-expansion@1.1.11", "", { "dependencies": { "balanced-match": "^1.0.0", "concat-map": "0.0.1" } }, "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA=="],
|
||||||
|
|
||||||
|
"braces": ["braces@3.0.3", "", { "dependencies": { "fill-range": "^7.1.1" } }, "sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA=="],
|
||||||
|
|
||||||
|
"browserslist": ["browserslist@4.24.4", "", { "dependencies": { "caniuse-lite": "^1.0.30001688", "electron-to-chromium": "^1.5.73", "node-releases": "^2.0.19", "update-browserslist-db": "^1.1.1" }, "bin": { "browserslist": "cli.js" } }, "sha512-KDi1Ny1gSePi1vm0q4oxSF8b4DR44GF4BbmS2YdhPLOEqd8pDviZOGH/GsmRwoWJ2+5Lr085X7naowMwKHDG1A=="],
|
||||||
|
|
||||||
|
"callsites": ["callsites@3.1.0", "", {}, "sha512-P8BjAsXvZS+VIDUI11hHCQEv74YT67YUi5JJFNWIqL235sBmjX4+qx9Muvls5ivyNENctx46xQLQ3aTuE7ssaQ=="],
|
||||||
|
|
||||||
|
"camelcase-css": ["camelcase-css@2.0.1", "", {}, "sha512-QOSvevhslijgYwRx6Rv7zKdMF8lbRmx+uQGx2+vDc+KI/eBnsy9kit5aj23AgGu3pa4t9AgwbnXWqS+iOY+2aA=="],
|
||||||
|
|
||||||
|
"caniuse-lite": ["caniuse-lite@1.0.30001699", "", {}, "sha512-b+uH5BakXZ9Do9iK+CkDmctUSEqZl+SP056vc5usa0PL+ev5OHw003rZXcnjNDv3L8P5j6rwT6C0BPKSikW08w=="],
|
||||||
|
|
||||||
|
"chalk": ["chalk@4.1.2", "", { "dependencies": { "ansi-styles": "^4.1.0", "supports-color": "^7.1.0" } }, "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA=="],
|
||||||
|
|
||||||
|
"chokidar": ["chokidar@4.0.3", "", { "dependencies": { "readdirp": "^4.0.1" } }, "sha512-Qgzu8kfBvo+cA4962jnP1KkS6Dop5NS6g7R5LFYJr4b8Ub94PPQXUksCw9PvXoeXPRRddRNC5C1JQUR2SMGtnA=="],
|
||||||
|
|
||||||
|
"clsx": ["clsx@2.1.1", "", {}, "sha512-eYm0QWBtUrBWZWG0d386OGAw16Z995PiOVo2B7bjWSbHedGl5e0ZWaq65kOGgUSNesEIDkB9ISbTg/JK9dhCZA=="],
|
||||||
|
|
||||||
|
"color-convert": ["color-convert@2.0.1", "", { "dependencies": { "color-name": "~1.1.4" } }, "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ=="],
|
||||||
|
|
||||||
|
"color-name": ["color-name@1.1.4", "", {}, "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA=="],
|
||||||
|
|
||||||
|
"commander": ["commander@4.1.1", "", {}, "sha512-NOKm8xhkzAjzFx8B2v5OAHT+u5pRQc2UCa2Vq9jYL/31o2wi9mxBA7LIFs3sV5VSC49z6pEhfbMULvShKj26WA=="],
|
||||||
|
|
||||||
|
"concat-map": ["concat-map@0.0.1", "", {}, "sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg=="],
|
||||||
|
|
||||||
|
"cookie": ["cookie@0.6.0", "", {}, "sha512-U71cyTamuh1CRNCfpGY6to28lxvNwPG4Guz/EVjgf3Jmzv0vlDp1atT9eS5dDjMYHucpHbWns6Lwf3BKz6svdw=="],
|
||||||
|
|
||||||
|
"cross-spawn": ["cross-spawn@7.0.6", "", { "dependencies": { "path-key": "^3.1.0", "shebang-command": "^2.0.0", "which": "^2.0.1" } }, "sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA=="],
|
||||||
|
|
||||||
|
"cssesc": ["cssesc@3.0.0", "", { "bin": { "cssesc": "bin/cssesc" } }, "sha512-/Tb/JcjK111nNScGob5MNtsntNM1aCNUDipB/TkwZFhyDrrE47SOx/18wF2bbjgc3ZzCSKW1T5nt5EbFoAz/Vg=="],
|
||||||
|
|
||||||
|
"debug": ["debug@4.4.0", "", { "dependencies": { "ms": "^2.1.3" } }, "sha512-6WTZ/IxCY/T6BALoZHaE4ctp9xm+Z5kY/pzYaCHRFeyVhojxlrm+46y68HA6hr0TcwEssoxNiDEUJQjfPZ/RYA=="],
|
||||||
|
|
||||||
|
"deep-is": ["deep-is@0.1.4", "", {}, "sha512-oIPzksmTg4/MriiaYGO+okXDT7ztn/w3Eptv/+gSIdMdKsJo0u4CfYNFJPy+4SKMuCqGw2wxnA+URMg3t8a/bQ=="],
|
||||||
|
|
||||||
|
"deepmerge": ["deepmerge@4.3.1", "", {}, "sha512-3sUqbMEc77XqpdNO7FRyRog+eW3ph+GYCbj+rK+uYyRMuwsVy0rMiVtPn+QJlKFvWP/1PYpapqYn0Me2knFn+A=="],
|
||||||
|
|
||||||
|
"dequal": ["dequal@2.0.3", "", {}, "sha512-0je+qPKHEMohvfRTCEo3CrPG6cAzAYgmzKyxRiYSSDkS6eGJdyVJm7WaYA5ECaAD9wLB2T4EEeymA5aFVcYXCA=="],
|
||||||
|
|
||||||
|
"devalue": ["devalue@5.1.1", "", {}, "sha512-maua5KUiapvEwiEAe+XnlZ3Rh0GD+qI1J/nb9vrJc3muPXvcF/8gXYTWF76+5DAqHyDUtOIImEuo0YKE9mshVw=="],
|
||||||
|
|
||||||
|
"didyoumean": ["didyoumean@1.2.2", "", {}, "sha512-gxtyfqMg7GKyhQmb056K7M3xszy/myH8w+B4RT+QXBQsvAOdc3XymqDDPHx1BgPgsdAA5SIifona89YtRATDzw=="],
|
||||||
|
|
||||||
|
"dlv": ["dlv@1.1.3", "", {}, "sha512-+HlytyjlPKnIG8XuRG8WvmBP8xs8P71y+SKKS6ZXWoEgLuePxtDoUEiH7WkdePWrQ5JBpE6aoVqfZfJUQkjXwA=="],
|
||||||
|
|
||||||
|
"eastasianwidth": ["eastasianwidth@0.2.0", "", {}, "sha512-I88TYZWc9XiYHRQ4/3c5rjjfgkjhLyW2luGIheGERbNQ6OY7yTybanSpDXZa8y7VUP9YmDcYa+eyq4ca7iLqWA=="],
|
||||||
|
|
||||||
|
"electron-to-chromium": ["electron-to-chromium@1.5.101", "", {}, "sha512-L0ISiQrP/56Acgu4/i/kfPwWSgrzYZUnQrC0+QPFuhqlLP1Ir7qzPPDVS9BcKIyWTRU8+o6CC8dKw38tSWhYIA=="],
|
||||||
|
|
||||||
|
"emoji-regex": ["emoji-regex@9.2.2", "", {}, "sha512-L18DaJsXSUk2+42pv8mLs5jJT2hqFkFE4j21wOmgbUqsZ2hL72NsUU785g9RXgo3s0ZNgVl42TiHp3ZtOv/Vyg=="],
|
||||||
|
|
||||||
|
"esbuild": ["esbuild@0.24.2", "", { "optionalDependencies": { "@esbuild/aix-ppc64": "0.24.2", "@esbuild/android-arm": "0.24.2", "@esbuild/android-arm64": "0.24.2", "@esbuild/android-x64": "0.24.2", "@esbuild/darwin-arm64": "0.24.2", "@esbuild/darwin-x64": "0.24.2", "@esbuild/freebsd-arm64": "0.24.2", "@esbuild/freebsd-x64": "0.24.2", "@esbuild/linux-arm": "0.24.2", "@esbuild/linux-arm64": "0.24.2", "@esbuild/linux-ia32": "0.24.2", "@esbuild/linux-loong64": "0.24.2", "@esbuild/linux-mips64el": "0.24.2", "@esbuild/linux-ppc64": "0.24.2", "@esbuild/linux-riscv64": "0.24.2", "@esbuild/linux-s390x": "0.24.2", "@esbuild/linux-x64": "0.24.2", "@esbuild/netbsd-arm64": "0.24.2", "@esbuild/netbsd-x64": "0.24.2", "@esbuild/openbsd-arm64": "0.24.2", "@esbuild/openbsd-x64": "0.24.2", "@esbuild/sunos-x64": "0.24.2", "@esbuild/win32-arm64": "0.24.2", "@esbuild/win32-ia32": "0.24.2", "@esbuild/win32-x64": "0.24.2" }, "bin": { "esbuild": "bin/esbuild" } }, "sha512-+9egpBW8I3CD5XPe0n6BfT5fxLzxrlDzqydF3aviG+9ni1lDC/OvMHcxqEFV0+LANZG5R1bFMWfUrjVsdwxJvA=="],
|
||||||
|
|
||||||
|
"escalade": ["escalade@3.2.0", "", {}, "sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA=="],
|
||||||
|
|
||||||
|
"escape-string-regexp": ["escape-string-regexp@4.0.0", "", {}, "sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA=="],
|
||||||
|
|
||||||
|
"eslint": ["eslint@9.20.1", "", { "dependencies": { "@eslint-community/eslint-utils": "^4.2.0", "@eslint-community/regexpp": "^4.12.1", "@eslint/config-array": "^0.19.0", "@eslint/core": "^0.11.0", "@eslint/eslintrc": "^3.2.0", "@eslint/js": "9.20.0", "@eslint/plugin-kit": "^0.2.5", "@humanfs/node": "^0.16.6", "@humanwhocodes/module-importer": "^1.0.1", "@humanwhocodes/retry": "^0.4.1", "@types/estree": "^1.0.6", "@types/json-schema": "^7.0.15", "ajv": "^6.12.4", "chalk": "^4.0.0", "cross-spawn": "^7.0.6", "debug": "^4.3.2", "escape-string-regexp": "^4.0.0", "eslint-scope": "^8.2.0", "eslint-visitor-keys": "^4.2.0", "espree": "^10.3.0", "esquery": "^1.5.0", "esutils": "^2.0.2", "fast-deep-equal": "^3.1.3", "file-entry-cache": "^8.0.0", "find-up": "^5.0.0", "glob-parent": "^6.0.2", "ignore": "^5.2.0", "imurmurhash": "^0.1.4", "is-glob": "^4.0.0", "json-stable-stringify-without-jsonify": "^1.0.1", "lodash.merge": "^4.6.2", "minimatch": "^3.1.2", "natural-compare": "^1.4.0", "optionator": "^0.9.3" }, "peerDependencies": { "jiti": "*" }, "optionalPeers": ["jiti"], "bin": { "eslint": "bin/eslint.js" } }, "sha512-m1mM33o6dBUjxl2qb6wv6nGNwCAsns1eKtaQ4l/NPHeTvhiUPbtdfMyktxN4B3fgHIgsYh1VT3V9txblpQHq+g=="],
|
||||||
|
|
||||||
|
"eslint-compat-utils": ["eslint-compat-utils@0.5.1", "", { "dependencies": { "semver": "^7.5.4" }, "peerDependencies": { "eslint": ">=6.0.0" } }, "sha512-3z3vFexKIEnjHE3zCMRo6fn/e44U7T1khUjg+Hp0ZQMCigh28rALD0nPFBcGZuiLC5rLZa2ubQHDRln09JfU2Q=="],
|
||||||
|
|
||||||
|
"eslint-config-prettier": ["eslint-config-prettier@10.0.1", "", { "peerDependencies": { "eslint": ">=7.0.0" }, "bin": { "eslint-config-prettier": "build/bin/cli.js" } }, "sha512-lZBts941cyJyeaooiKxAtzoPHTN+GbQTJFAIdQbRhA4/8whaAraEh47Whw/ZFfrjNSnlAxqfm9i0XVAEkULjCw=="],
|
||||||
|
|
||||||
|
"eslint-plugin-svelte": ["eslint-plugin-svelte@2.46.1", "", { "dependencies": { "@eslint-community/eslint-utils": "^4.4.0", "@jridgewell/sourcemap-codec": "^1.4.15", "eslint-compat-utils": "^0.5.1", "esutils": "^2.0.3", "known-css-properties": "^0.35.0", "postcss": "^8.4.38", "postcss-load-config": "^3.1.4", "postcss-safe-parser": "^6.0.0", "postcss-selector-parser": "^6.1.0", "semver": "^7.6.2", "svelte-eslint-parser": "^0.43.0" }, "peerDependencies": { "eslint": "^7.0.0 || ^8.0.0-0 || ^9.0.0-0", "svelte": "^3.37.0 || ^4.0.0 || ^5.0.0" }, "optionalPeers": ["svelte"] }, "sha512-7xYr2o4NID/f9OEYMqxsEQsCsj4KaMy4q5sANaKkAb6/QeCjYFxRmDm2S3YC3A3pl1kyPZ/syOx/i7LcWYSbIw=="],
|
||||||
|
|
||||||
|
"eslint-scope": ["eslint-scope@8.2.0", "", { "dependencies": { "esrecurse": "^4.3.0", "estraverse": "^5.2.0" } }, "sha512-PHlWUfG6lvPc3yvP5A4PNyBL1W8fkDUccmI21JUu/+GKZBoH/W5u6usENXUrWFRsyoW5ACUjFGgAFQp5gUlb/A=="],
|
||||||
|
|
||||||
|
"eslint-visitor-keys": ["eslint-visitor-keys@4.2.0", "", {}, "sha512-UyLnSehNt62FFhSwjZlHmeokpRK59rcz29j+F1/aDgbkbRTk7wIc9XzdoasMUbRNKDM0qQt/+BJ4BrpFeABemw=="],
|
||||||
|
|
||||||
|
"esm-env": ["esm-env@1.2.2", "", {}, "sha512-Epxrv+Nr/CaL4ZcFGPJIYLWFom+YeV1DqMLHJoEd9SYRxNbaFruBwfEX/kkHUJf55j2+TUbmDcmuilbP1TmXHA=="],
|
||||||
|
|
||||||
|
"espree": ["espree@10.3.0", "", { "dependencies": { "acorn": "^8.14.0", "acorn-jsx": "^5.3.2", "eslint-visitor-keys": "^4.2.0" } }, "sha512-0QYC8b24HWY8zjRnDTL6RiHfDbAWn63qb4LMj1Z4b076A4une81+z03Kg7l7mn/48PUTqoLptSXez8oknU8Clg=="],
|
||||||
|
|
||||||
|
"esquery": ["esquery@1.6.0", "", { "dependencies": { "estraverse": "^5.1.0" } }, "sha512-ca9pw9fomFcKPvFLXhBKUK90ZvGibiGOvRJNbjljY7s7uq/5YO4BOzcYtJqExdx99rF6aAcnRxHmcUHcz6sQsg=="],
|
||||||
|
|
||||||
|
"esrap": ["esrap@1.4.5", "", { "dependencies": { "@jridgewell/sourcemap-codec": "^1.4.15" } }, "sha512-CjNMjkBWWZeHn+VX+gS8YvFwJ5+NDhg8aWZBSFJPR8qQduDNjbJodA2WcwCm7uQa5Rjqj+nZvVmceg1RbHFB9g=="],
|
||||||
|
|
||||||
|
"esrecurse": ["esrecurse@4.3.0", "", { "dependencies": { "estraverse": "^5.2.0" } }, "sha512-KmfKL3b6G+RXvP8N1vr3Tq1kL/oCFgn2NYXEtqP8/L3pKapUA4G8cFVaoF3SU323CD4XypR/ffioHmkti6/Tag=="],
|
||||||
|
|
||||||
|
"estraverse": ["estraverse@5.3.0", "", {}, "sha512-MMdARuVEQziNTeJD8DgMqmhwR11BRQ/cBP+pLtYdSTnf3MIO8fFeiINEbX36ZdNlfU/7A9f3gUw49B3oQsvwBA=="],
|
||||||
|
|
||||||
|
"esutils": ["esutils@2.0.3", "", {}, "sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g=="],
|
||||||
|
|
||||||
|
"fast-deep-equal": ["fast-deep-equal@3.1.3", "", {}, "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q=="],
|
||||||
|
|
||||||
|
"fast-glob": ["fast-glob@3.3.3", "", { "dependencies": { "@nodelib/fs.stat": "^2.0.2", "@nodelib/fs.walk": "^1.2.3", "glob-parent": "^5.1.2", "merge2": "^1.3.0", "micromatch": "^4.0.8" } }, "sha512-7MptL8U0cqcFdzIzwOTHoilX9x5BrNqye7Z/LuC7kCMRio1EMSyqRK3BEAUD7sXRq4iT4AzTVuZdhgQ2TCvYLg=="],
|
||||||
|
|
||||||
|
"fast-json-stable-stringify": ["fast-json-stable-stringify@2.1.0", "", {}, "sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw=="],
|
||||||
|
|
||||||
|
"fast-levenshtein": ["fast-levenshtein@2.0.6", "", {}, "sha512-DCXu6Ifhqcks7TZKY3Hxp3y6qphY5SJZmrWMDrKcERSOXWQdMhU9Ig/PYrzyw/ul9jOIyh0N4M0tbC5hodg8dw=="],
|
||||||
|
|
||||||
|
"fastq": ["fastq@1.19.0", "", { "dependencies": { "reusify": "^1.0.4" } }, "sha512-7SFSRCNjBQIZH/xZR3iy5iQYR8aGBE0h3VG6/cwlbrpdciNYBMotQav8c1XI3HjHH+NikUpP53nPdlZSdWmFzA=="],
|
||||||
|
|
||||||
|
"fdir": ["fdir@6.4.3", "", { "peerDependencies": { "picomatch": "^3 || ^4" }, "optionalPeers": ["picomatch"] }, "sha512-PMXmW2y1hDDfTSRc9gaXIuCCRpuoz3Kaz8cUelp3smouvfT632ozg2vrT6lJsHKKOF59YLbOGfAWGUcKEfRMQw=="],
|
||||||
|
|
||||||
|
"file-entry-cache": ["file-entry-cache@8.0.0", "", { "dependencies": { "flat-cache": "^4.0.0" } }, "sha512-XXTUwCvisa5oacNGRP9SfNtYBNAMi+RPwBFmblZEF7N7swHYQS6/Zfk7SRwx4D5j3CH211YNRco1DEMNVfZCnQ=="],
|
||||||
|
|
||||||
|
"fill-range": ["fill-range@7.1.1", "", { "dependencies": { "to-regex-range": "^5.0.1" } }, "sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg=="],
|
||||||
|
|
||||||
|
"find-up": ["find-up@5.0.0", "", { "dependencies": { "locate-path": "^6.0.0", "path-exists": "^4.0.0" } }, "sha512-78/PXT1wlLLDgTzDs7sjq9hzz0vXD+zn+7wypEe4fXQxCmdmqfGsEPQxmiCSQI3ajFV91bVSsvNtrJRiW6nGng=="],
|
||||||
|
|
||||||
|
"flat-cache": ["flat-cache@4.0.1", "", { "dependencies": { "flatted": "^3.2.9", "keyv": "^4.5.4" } }, "sha512-f7ccFPK3SXFHpx15UIGyRJ/FJQctuKZ0zVuN3frBo4HnK3cay9VEW0R6yPYFHC0AgqhukPzKjq22t5DmAyqGyw=="],
|
||||||
|
|
||||||
|
"flatted": ["flatted@3.3.2", "", {}, "sha512-AiwGJM8YcNOaobumgtng+6NHuOqC3A7MixFeDafM3X9cIUM+xUXoS5Vfgf+OihAYe20fxqNM9yPBXJzRtZ/4eA=="],
|
||||||
|
|
||||||
|
"focus-trap": ["focus-trap@7.6.4", "", { "dependencies": { "tabbable": "^6.2.0" } }, "sha512-xx560wGBk7seZ6y933idtjJQc1l+ck+pI3sKvhKozdBV1dRZoKhkW5xoCaFv9tQiX5RH1xfSxjuNu6g+lmN/gw=="],
|
||||||
|
|
||||||
|
"foreground-child": ["foreground-child@3.3.0", "", { "dependencies": { "cross-spawn": "^7.0.0", "signal-exit": "^4.0.1" } }, "sha512-Ld2g8rrAyMYFXBhEqMz8ZAHBi4J4uS1i/CxGMDnjyFWddMXLVcDp051DZfu+t7+ab7Wv6SMqpWmyFIj5UbfFvg=="],
|
||||||
|
|
||||||
|
"fraction.js": ["fraction.js@4.3.7", "", {}, "sha512-ZsDfxO51wGAXREY55a7la9LScWpwv9RxIrYABrlvOFBlH/ShPnrtsXeuUIfXKKOVicNxQ+o8JTbJvjS4M89yew=="],
|
||||||
|
|
||||||
|
"fsevents": ["fsevents@2.3.3", "", { "os": "darwin" }, "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw=="],
|
||||||
|
|
||||||
|
"function-bind": ["function-bind@1.1.2", "", {}, "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA=="],
|
||||||
|
|
||||||
|
"glob": ["glob@10.4.5", "", { "dependencies": { "foreground-child": "^3.1.0", "jackspeak": "^3.1.2", "minimatch": "^9.0.4", "minipass": "^7.1.2", "package-json-from-dist": "^1.0.0", "path-scurry": "^1.11.1" }, "bin": { "glob": "dist/esm/bin.mjs" } }, "sha512-7Bv8RF0k6xjo7d4A/PxYLbUCfb6c+Vpd2/mB2yRDlew7Jb5hEXiCD9ibfO7wpk8i4sevK6DFny9h7EYbM3/sHg=="],
|
||||||
|
|
||||||
|
"glob-parent": ["glob-parent@6.0.2", "", { "dependencies": { "is-glob": "^4.0.3" } }, "sha512-XxwI8EOhVQgWp6iDL+3b0r86f4d6AX6zSU55HfB4ydCEuXLXc5FcYeOu+nnGftS4TEju/11rt4KJPTMgbfmv4A=="],
|
||||||
|
|
||||||
|
"globals": ["globals@15.15.0", "", {}, "sha512-7ACyT3wmyp3I61S4fG682L0VA2RGD9otkqGJIwNUMF1SWUombIIk+af1unuDYgMm082aHYwD+mzJvv9Iu8dsgg=="],
|
||||||
|
|
||||||
|
"graphemer": ["graphemer@1.4.0", "", {}, "sha512-EtKwoO6kxCL9WO5xipiHTZlSzBm7WLT627TqC/uVRd0HKmq8NXyebnNYxDoBi7wt8eTWrUrKXCOVaFq9x1kgag=="],
|
||||||
|
|
||||||
|
"has-flag": ["has-flag@4.0.0", "", {}, "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ=="],
|
||||||
|
|
||||||
|
"hasown": ["hasown@2.0.2", "", { "dependencies": { "function-bind": "^1.1.2" } }, "sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ=="],
|
||||||
|
|
||||||
|
"ignore": ["ignore@5.3.2", "", {}, "sha512-hsBTNUqQTDwkWtcdYI2i06Y/nUBEsNEDJKjWdigLvegy8kDuJAS8uRlpkkcQpyEXL0Z/pjDy5HBmMjRCJ2gq+g=="],
|
||||||
|
|
||||||
|
"import-fresh": ["import-fresh@3.3.1", "", { "dependencies": { "parent-module": "^1.0.0", "resolve-from": "^4.0.0" } }, "sha512-TR3KfrTZTYLPB6jUjfx6MF9WcWrHL9su5TObK4ZkYgBdWKPOFoSoQIdEuTuR82pmtxH2spWG9h6etwfr1pLBqQ=="],
|
||||||
|
|
||||||
|
"import-meta-resolve": ["import-meta-resolve@4.1.0", "", {}, "sha512-I6fiaX09Xivtk+THaMfAwnA3MVA5Big1WHF1Dfx9hFuvNIWpXnorlkzhcQf6ehrqQiiZECRt1poOAkPmer3ruw=="],
|
||||||
|
|
||||||
|
"imurmurhash": ["imurmurhash@0.1.4", "", {}, "sha512-JmXMZ6wuvDmLiHEml9ykzqO6lwFbof0GG4IkcGaENdCRDDmMVnny7s5HsIgHCbaq0w2MyPhDqkhTUgS2LU2PHA=="],
|
||||||
|
|
||||||
|
"is-binary-path": ["is-binary-path@2.1.0", "", { "dependencies": { "binary-extensions": "^2.0.0" } }, "sha512-ZMERYes6pDydyuGidse7OsHxtbI7WVeUEozgR/g7rd0xUimYNlvZRE/K2MgZTjWy725IfelLeVcEM97mmtRGXw=="],
|
||||||
|
|
||||||
|
"is-core-module": ["is-core-module@2.16.1", "", { "dependencies": { "hasown": "^2.0.2" } }, "sha512-UfoeMA6fIJ8wTYFEUjelnaGI67v6+N7qXJEvQuIGa99l4xsCruSYOVSQ0uPANn4dAzm8lkYPaKLrrijLq7x23w=="],
|
||||||
|
|
||||||
|
"is-extglob": ["is-extglob@2.1.1", "", {}, "sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ=="],
|
||||||
|
|
||||||
|
"is-fullwidth-code-point": ["is-fullwidth-code-point@3.0.0", "", {}, "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg=="],
|
||||||
|
|
||||||
|
"is-glob": ["is-glob@4.0.3", "", { "dependencies": { "is-extglob": "^2.1.1" } }, "sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg=="],
|
||||||
|
|
||||||
|
"is-number": ["is-number@7.0.0", "", {}, "sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng=="],
|
||||||
|
|
||||||
|
"is-reference": ["is-reference@3.0.3", "", { "dependencies": { "@types/estree": "^1.0.6" } }, "sha512-ixkJoqQvAP88E6wLydLGGqCJsrFUnqoH6HnaczB8XmDH1oaWU+xxdptvikTgaEhtZ53Ky6YXiBuUI2WXLMCwjw=="],
|
||||||
|
|
||||||
|
"isexe": ["isexe@2.0.0", "", {}, "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw=="],
|
||||||
|
|
||||||
|
"jackspeak": ["jackspeak@3.4.3", "", { "dependencies": { "@isaacs/cliui": "^8.0.2" }, "optionalDependencies": { "@pkgjs/parseargs": "^0.11.0" } }, "sha512-OGlZQpz2yfahA/Rd1Y8Cd9SIEsqvXkLVoSw/cgwhnhFMDbsQFeZYoJJ7bIZBS9BcamUW96asq/npPWugM+RQBw=="],
|
||||||
|
|
||||||
|
"jiti": ["jiti@1.21.7", "", { "bin": { "jiti": "bin/jiti.js" } }, "sha512-/imKNG4EbWNrVjoNC/1H5/9GFy+tqjGBHCaSsN+P2RnPqjsLmv6UD3Ej+Kj8nBWaRAwyk7kK5ZUc+OEatnTR3A=="],
|
||||||
|
|
||||||
|
"js-yaml": ["js-yaml@4.1.0", "", { "dependencies": { "argparse": "^2.0.1" }, "bin": { "js-yaml": "bin/js-yaml.js" } }, "sha512-wpxZs9NoxZaJESJGIZTyDEaYpl0FKSA+FB9aJiyemKhMwkxQg63h4T1KJgUGHpTqPDNRcmmYLugrRjJlBtWvRA=="],
|
||||||
|
|
||||||
|
"json-buffer": ["json-buffer@3.0.1", "", {}, "sha512-4bV5BfR2mqfQTJm+V5tPPdf+ZpuhiIvTuAB5g8kcrXOZpTT/QwwVRWBywX1ozr6lEuPdbHxwaJlm9G6mI2sfSQ=="],
|
||||||
|
|
||||||
|
"json-schema-traverse": ["json-schema-traverse@0.4.1", "", {}, "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg=="],
|
||||||
|
|
||||||
|
"json-stable-stringify-without-jsonify": ["json-stable-stringify-without-jsonify@1.0.1", "", {}, "sha512-Bdboy+l7tA3OGW6FjyFHWkP5LuByj1Tk33Ljyq0axyzdk9//JSi2u3fP1QSmd1KNwq6VOKYGlAu87CisVir6Pw=="],
|
||||||
|
|
||||||
|
"keyv": ["keyv@4.5.4", "", { "dependencies": { "json-buffer": "3.0.1" } }, "sha512-oxVHkHR/EJf2CNXnWxRLW6mg7JyCCUcG0DtEGmL2ctUo1PNTin1PUil+r/+4r5MpVgC/fn1kjsx7mjSujKqIpw=="],
|
||||||
|
|
||||||
|
"kleur": ["kleur@4.1.5", "", {}, "sha512-o+NO+8WrRiQEE4/7nwRJhN1HWpVmJm511pBHUxPLtp0BUISzlBplORYSmTclCnJvQq2tKu/sgl3xVpkc7ZWuQQ=="],
|
||||||
|
|
||||||
|
"known-css-properties": ["known-css-properties@0.35.0", "", {}, "sha512-a/RAk2BfKk+WFGhhOCAYqSiFLc34k8Mt/6NWRI4joER0EYUzXIcFivjjnoD3+XU1DggLn/tZc3DOAgke7l8a4A=="],
|
||||||
|
|
||||||
|
"levn": ["levn@0.4.1", "", { "dependencies": { "prelude-ls": "^1.2.1", "type-check": "~0.4.0" } }, "sha512-+bT2uH4E5LGE7h/n3evcS/sQlJXCpIp6ym8OWJ5eV6+67Dsql/LaaT7qJBAt2rzfoa/5QBGBhxDix1dMt2kQKQ=="],
|
||||||
|
|
||||||
|
"lilconfig": ["lilconfig@3.1.3", "", {}, "sha512-/vlFKAoH5Cgt3Ie+JLhRbwOsCQePABiU3tJ1egGvyQ+33R/vcwM2Zl2QR/LzjsBeItPt3oSVXapn+m4nQDvpzw=="],
|
||||||
|
|
||||||
|
"lines-and-columns": ["lines-and-columns@1.2.4", "", {}, "sha512-7ylylesZQ/PV29jhEDl3Ufjo6ZX7gCqJr5F7PKrqc93v7fzSymt1BpwEU8nAUXs8qzzvqhbjhK5QZg6Mt/HkBg=="],
|
||||||
|
|
||||||
|
"locate-character": ["locate-character@3.0.0", "", {}, "sha512-SW13ws7BjaeJ6p7Q6CO2nchbYEc3X3J6WrmTTDto7yMPqVSZTUyY5Tjbid+Ab8gLnATtygYtiDIJGQRRn2ZOiA=="],
|
||||||
|
|
||||||
|
"locate-path": ["locate-path@6.0.0", "", { "dependencies": { "p-locate": "^5.0.0" } }, "sha512-iPZK6eYjbxRu3uB4/WZ3EsEIMJFMqAoopl3R+zuq0UjcAm/MO6KCweDgPfP3elTztoKP3KtnVHxTn2NHBSDVUw=="],
|
||||||
|
|
||||||
|
"lodash.merge": ["lodash.merge@4.6.2", "", {}, "sha512-0KpjqXRVvrYyCsX1swR/XTK0va6VQkQM6MNo7PqW77ByjAhoARA8EfrP1N4+KlKj8YS0ZUCtRT/YUuhyYDujIQ=="],
|
||||||
|
|
||||||
|
"lru-cache": ["lru-cache@10.4.3", "", {}, "sha512-JNAzZcXrCt42VGLuYz0zfAzDfAvJWW6AfYlDBQyDV5DClI2m5sAmK+OIO7s59XfsRsWHp02jAJrRadPRGTt6SQ=="],
|
||||||
|
|
||||||
|
"magic-string": ["magic-string@0.30.17", "", { "dependencies": { "@jridgewell/sourcemap-codec": "^1.5.0" } }, "sha512-sNPKHvyjVf7gyjwS4xGTaW/mCnF8wnjtifKBEhxfZ7E/S8tQ0rssrwGNn6q8JH/ohItJfSQp9mBtQYuTlH5QnA=="],
|
||||||
|
|
||||||
|
"merge2": ["merge2@1.4.1", "", {}, "sha512-8q7VEgMJW4J8tcfVPy8g09NcQwZdbwFEqhe/WZkoIzjn/3TGDwtOCYtXGxA3O8tPzpczCCDgv+P2P5y00ZJOOg=="],
|
||||||
|
|
||||||
|
"micromatch": ["micromatch@4.0.8", "", { "dependencies": { "braces": "^3.0.3", "picomatch": "^2.3.1" } }, "sha512-PXwfBhYu0hBCPw8Dn0E+WDYb7af3dSLVWKi3HGv84IdF4TyFoC0ysxFd0Goxw7nSv4T/PzEJQxsYsEiFCKo2BA=="],
|
||||||
|
|
||||||
|
"minimatch": ["minimatch@3.1.2", "", { "dependencies": { "brace-expansion": "^1.1.7" } }, "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw=="],
|
||||||
|
|
||||||
|
"minipass": ["minipass@7.1.2", "", {}, "sha512-qOOzS1cBTWYF4BH8fVePDBOO9iptMnGUEZwNc/cMWnTV2nVLZ7VoNWEPHkYczZA0pdoA7dl6e7FL659nX9S2aw=="],
|
||||||
|
|
||||||
|
"mode-watcher": ["mode-watcher@0.5.1", "", { "peerDependencies": { "svelte": "^4.0.0 || ^5.0.0-next.1" } }, "sha512-adEC6T7TMX/kzQlaO/MtiQOSFekZfQu4MC+lXyoceQG+U5sKpJWZ4yKXqw846ExIuWJgedkOIPqAYYRk/xHm+w=="],
|
||||||
|
|
||||||
|
"mri": ["mri@1.2.0", "", {}, "sha512-tzzskb3bG8LvYGFF/mDTpq3jpI6Q9wc3LEmBaghu+DdCssd1FakN7Bc0hVNmEyGq1bq3RgfkCb3cmQLpNPOroA=="],
|
||||||
|
|
||||||
|
"mrmime": ["mrmime@2.0.0", "", {}, "sha512-eu38+hdgojoyq63s+yTpN4XMBdt5l8HhMhc4VKLO9KM5caLIBvUm4thi7fFaxyTmCKeNnXZ5pAlBwCUnhA09uw=="],
|
||||||
|
|
||||||
|
"ms": ["ms@2.1.3", "", {}, "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA=="],
|
||||||
|
|
||||||
|
"mz": ["mz@2.7.0", "", { "dependencies": { "any-promise": "^1.0.0", "object-assign": "^4.0.1", "thenify-all": "^1.0.0" } }, "sha512-z81GNO7nnYMEhrGh9LeymoE4+Yr0Wn5McHIZMK5cfQCl+NDX08sCZgUc9/6MHni9IWuFLm1Z3HTCXu2z9fN62Q=="],
|
||||||
|
|
||||||
|
"nanoid": ["nanoid@5.1.0", "", { "bin": { "nanoid": "bin/nanoid.js" } }, "sha512-zDAl/llz8Ue/EblwSYwdxGBYfj46IM1dhjVi8dyp9LQffoIGxJEAHj2oeZ4uNcgycSRcQ83CnfcZqEJzVDLcDw=="],
|
||||||
|
|
||||||
|
"natural-compare": ["natural-compare@1.4.0", "", {}, "sha512-OWND8ei3VtNC9h7V60qff3SVobHr996CTwgxubgyQYEpg290h9J0buyECNNJexkFm5sOajh5G116RYA1c8ZMSw=="],
|
||||||
|
|
||||||
|
"node-releases": ["node-releases@2.0.19", "", {}, "sha512-xxOWJsBKtzAq7DY0J+DTzuz58K8e7sJbdgwkbMWQe8UYB6ekmsQ45q0M/tJDsGaZmbC+l7n57UV8Hl5tHxO9uw=="],
|
||||||
|
|
||||||
|
"normalize-path": ["normalize-path@3.0.0", "", {}, "sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA=="],
|
||||||
|
|
||||||
|
"normalize-range": ["normalize-range@0.1.2", "", {}, "sha512-bdok/XvKII3nUpklnV6P2hxtMNrCboOjAcyBuQnWEhO665FwrSNRxU+AqpsyvO6LgGYPspN+lu5CLtw4jPRKNA=="],
|
||||||
|
|
||||||
|
"object-assign": ["object-assign@4.1.1", "", {}, "sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg=="],
|
||||||
|
|
||||||
|
"object-hash": ["object-hash@3.0.0", "", {}, "sha512-RSn9F68PjH9HqtltsSnqYC1XXoWe9Bju5+213R98cNGttag9q9yAOTzdbsqvIa7aNm5WffBZFpWYr2aWrklWAw=="],
|
||||||
|
|
||||||
|
"optionator": ["optionator@0.9.4", "", { "dependencies": { "deep-is": "^0.1.3", "fast-levenshtein": "^2.0.6", "levn": "^0.4.1", "prelude-ls": "^1.2.1", "type-check": "^0.4.0", "word-wrap": "^1.2.5" } }, "sha512-6IpQ7mKUxRcZNLIObR0hz7lxsapSSIYNZJwXPGeF0mTVqGKFIXj1DQcMoT22S3ROcLyY/rz0PWaWZ9ayWmad9g=="],
|
||||||
|
|
||||||
|
"p-limit": ["p-limit@3.1.0", "", { "dependencies": { "yocto-queue": "^0.1.0" } }, "sha512-TYOanM3wGwNGsZN2cVTYPArw454xnXj5qmWF1bEoAc4+cU/ol7GVh7odevjp1FNHduHc3KZMcFduxU5Xc6uJRQ=="],
|
||||||
|
|
||||||
|
"p-locate": ["p-locate@5.0.0", "", { "dependencies": { "p-limit": "^3.0.2" } }, "sha512-LaNjtRWUBY++zB5nE/NwcaoMylSPk+S+ZHNB1TzdbMJMny6dynpAGt7X/tl/QYq3TIeE6nxHppbo2LGymrG5Pw=="],
|
||||||
|
|
||||||
|
"package-json-from-dist": ["package-json-from-dist@1.0.1", "", {}, "sha512-UEZIS3/by4OC8vL3P2dTXRETpebLI2NiI5vIrjaD/5UtrkFX/tNbwjTSRAGC/+7CAo2pIcBaRgWmcBBHcsaCIw=="],
|
||||||
|
|
||||||
|
"parent-module": ["parent-module@1.0.1", "", { "dependencies": { "callsites": "^3.0.0" } }, "sha512-GQ2EWRpQV8/o+Aw8YqtfZZPfNRWZYkbidE9k5rpl/hC3vtHHBfGm2Ifi6qWV+coDGkrUKZAxE3Lot5kcsRlh+g=="],
|
||||||
|
|
||||||
|
"path-exists": ["path-exists@4.0.0", "", {}, "sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w=="],
|
||||||
|
|
||||||
|
"path-key": ["path-key@3.1.1", "", {}, "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q=="],
|
||||||
|
|
||||||
|
"path-parse": ["path-parse@1.0.7", "", {}, "sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw=="],
|
||||||
|
|
||||||
|
"path-scurry": ["path-scurry@1.11.1", "", { "dependencies": { "lru-cache": "^10.2.0", "minipass": "^5.0.0 || ^6.0.2 || ^7.0.0" } }, "sha512-Xa4Nw17FS9ApQFJ9umLiJS4orGjm7ZzwUrwamcGQuHSzDyth9boKDaycYdDcZDuqYATXw4HFXgaqWTctW/v1HA=="],
|
||||||
|
|
||||||
|
"picocolors": ["picocolors@1.1.1", "", {}, "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA=="],
|
||||||
|
|
||||||
|
"picomatch": ["picomatch@2.3.1", "", {}, "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA=="],
|
||||||
|
|
||||||
|
"pify": ["pify@2.3.0", "", {}, "sha512-udgsAY+fTnvv7kI7aaxbqwWNb0AHiB0qBO89PZKPkoTmGOgdbrHDKD+0B2X4uTfJ/FT1R09r9gTsjUjNJotuog=="],
|
||||||
|
|
||||||
|
"pirates": ["pirates@4.0.6", "", {}, "sha512-saLsH7WeYYPiD25LDuLRRY/i+6HaPYr6G1OUlN39otzkSTxKnubR9RTxS3/Kk50s1g2JTgFwWQDQyplC5/SHZg=="],
|
||||||
|
|
||||||
|
"postcss": ["postcss@8.5.2", "", { "dependencies": { "nanoid": "^3.3.8", "picocolors": "^1.1.1", "source-map-js": "^1.2.1" } }, "sha512-MjOadfU3Ys9KYoX0AdkBlFEF1Vx37uCCeN4ZHnmwm9FfpbsGWMZeBLMmmpY+6Ocqod7mkdZ0DT31OlbsFrLlkA=="],
|
||||||
|
|
||||||
|
"postcss-import": ["postcss-import@15.1.0", "", { "dependencies": { "postcss-value-parser": "^4.0.0", "read-cache": "^1.0.0", "resolve": "^1.1.7" }, "peerDependencies": { "postcss": "^8.0.0" } }, "sha512-hpr+J05B2FVYUAXHeK1YyI267J/dDDhMU6B6civm8hSY1jYJnBXxzKDKDswzJmtLHryrjhnDjqqp/49t8FALew=="],
|
||||||
|
|
||||||
|
"postcss-js": ["postcss-js@4.0.1", "", { "dependencies": { "camelcase-css": "^2.0.1" }, "peerDependencies": { "postcss": "^8.4.21" } }, "sha512-dDLF8pEO191hJMtlHFPRa8xsizHaM82MLfNkUHdUtVEV3tgTp5oj+8qbEqYM57SLfc74KSbw//4SeJma2LRVIw=="],
|
||||||
|
|
||||||
|
"postcss-load-config": ["postcss-load-config@3.1.4", "", { "dependencies": { "lilconfig": "^2.0.5", "yaml": "^1.10.2" }, "peerDependencies": { "postcss": ">=8.0.9", "ts-node": ">=9.0.0" }, "optionalPeers": ["postcss", "ts-node"] }, "sha512-6DiM4E7v4coTE4uzA8U//WhtPwyhiim3eyjEMFCnUpzbrkK9wJHgKDT2mR+HbtSrd/NubVaYTOpSpjUl8NQeRg=="],
|
||||||
|
|
||||||
|
"postcss-nested": ["postcss-nested@6.2.0", "", { "dependencies": { "postcss-selector-parser": "^6.1.1" }, "peerDependencies": { "postcss": "^8.2.14" } }, "sha512-HQbt28KulC5AJzG+cZtj9kvKB93CFCdLvog1WFLf1D+xmMvPGlBstkpTEZfK5+AN9hfJocyBFCNiqyS48bpgzQ=="],
|
||||||
|
|
||||||
|
"postcss-safe-parser": ["postcss-safe-parser@6.0.0", "", { "peerDependencies": { "postcss": "^8.3.3" } }, "sha512-FARHN8pwH+WiS2OPCxJI8FuRJpTVnn6ZNFiqAM2aeW2LwTHWWmWgIyKC6cUo0L8aeKiF/14MNvnpls6R2PBeMQ=="],
|
||||||
|
|
||||||
|
"postcss-scss": ["postcss-scss@4.0.9", "", { "peerDependencies": { "postcss": "^8.4.29" } }, "sha512-AjKOeiwAitL/MXxQW2DliT28EKukvvbEWx3LBmJIRN8KfBGZbRTxNYW0kSqi1COiTZ57nZ9NW06S6ux//N1c9A=="],
|
||||||
|
|
||||||
|
"postcss-selector-parser": ["postcss-selector-parser@6.1.2", "", { "dependencies": { "cssesc": "^3.0.0", "util-deprecate": "^1.0.2" } }, "sha512-Q8qQfPiZ+THO/3ZrOrO0cJJKfpYCagtMUkXbnEfmgUjwXg6z/WBeOyS9APBBPCTSiDV+s4SwQGu8yFsiMRIudg=="],
|
||||||
|
|
||||||
|
"postcss-value-parser": ["postcss-value-parser@4.2.0", "", {}, "sha512-1NNCs6uurfkVbeXG4S8JFT9t19m45ICnif8zWLd5oPSZ50QnwMfK+H3jv408d4jw/7Bttv5axS5IiHoLaVNHeQ=="],
|
||||||
|
|
||||||
|
"prelude-ls": ["prelude-ls@1.2.1", "", {}, "sha512-vkcDPrRZo1QZLbn5RLGPpg/WmIQ65qoWWhcGKf/b5eplkkarX0m9z8ppCat4mlOqUsWpyNuYgO3VRyrYHSzX5g=="],
|
||||||
|
|
||||||
|
"prettier": ["prettier@3.5.1", "", { "bin": { "prettier": "bin/prettier.cjs" } }, "sha512-hPpFQvHwL3Qv5AdRvBFMhnKo4tYxp0ReXiPn2bxkiohEX6mBeBwEpBSQTkD458RaaDKQMYSp4hX4UtfUTA5wDw=="],
|
||||||
|
|
||||||
|
"prettier-plugin-svelte": ["prettier-plugin-svelte@3.3.3", "", { "peerDependencies": { "prettier": "^3.0.0", "svelte": "^3.2.0 || ^4.0.0-next.0 || ^5.0.0-next.0" } }, "sha512-yViK9zqQ+H2qZD1w/bH7W8i+bVfKrD8GIFjkFe4Thl6kCT9SlAsXVNmt3jCvQOCsnOhcvYgsoVlRV/Eu6x5nNw=="],
|
||||||
|
|
||||||
|
"prettier-plugin-tailwindcss": ["prettier-plugin-tailwindcss@0.6.11", "", { "peerDependencies": { "@ianvs/prettier-plugin-sort-imports": "*", "@prettier/plugin-pug": "*", "@shopify/prettier-plugin-liquid": "*", "@trivago/prettier-plugin-sort-imports": "*", "@zackad/prettier-plugin-twig": "*", "prettier": "^3.0", "prettier-plugin-astro": "*", "prettier-plugin-css-order": "*", "prettier-plugin-import-sort": "*", "prettier-plugin-jsdoc": "*", "prettier-plugin-marko": "*", "prettier-plugin-multiline-arrays": "*", "prettier-plugin-organize-attributes": "*", "prettier-plugin-organize-imports": "*", "prettier-plugin-sort-imports": "*", "prettier-plugin-style-order": "*", "prettier-plugin-svelte": "*" }, "optionalPeers": ["@ianvs/prettier-plugin-sort-imports", "@prettier/plugin-pug", "@shopify/prettier-plugin-liquid", "@trivago/prettier-plugin-sort-imports", "@zackad/prettier-plugin-twig", "prettier-plugin-astro", "prettier-plugin-css-order", "prettier-plugin-import-sort", "prettier-plugin-jsdoc", "prettier-plugin-marko", "prettier-plugin-multiline-arrays", "prettier-plugin-organize-attributes", "prettier-plugin-organize-imports", "prettier-plugin-sort-imports", "prettier-plugin-style-order", "prettier-plugin-svelte"] }, "sha512-YxaYSIvZPAqhrrEpRtonnrXdghZg1irNg4qrjboCXrpybLWVs55cW2N3juhspVJiO0JBvYJT8SYsJpc8OQSnsA=="],
|
||||||
|
|
||||||
|
"punycode": ["punycode@2.3.1", "", {}, "sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg=="],
|
||||||
|
|
||||||
|
"queue-microtask": ["queue-microtask@1.2.3", "", {}, "sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A=="],
|
||||||
|
|
||||||
|
"read-cache": ["read-cache@1.0.0", "", { "dependencies": { "pify": "^2.3.0" } }, "sha512-Owdv/Ft7IjOgm/i0xvNDZ1LrRANRfew4b2prF3OWMQLxLfu3bS8FVhCsrSCMK4lR56Y9ya+AThoTpDCTxCmpRA=="],
|
||||||
|
|
||||||
|
"readdirp": ["readdirp@4.1.2", "", {}, "sha512-GDhwkLfywWL2s6vEjyhri+eXmfH6j1L7JE27WhqLeYzoh/A3DBaYGEj2H/HFZCn/kMfim73FXxEJTw06WtxQwg=="],
|
||||||
|
|
||||||
|
"resolve": ["resolve@1.22.10", "", { "dependencies": { "is-core-module": "^2.16.0", "path-parse": "^1.0.7", "supports-preserve-symlinks-flag": "^1.0.0" }, "bin": { "resolve": "bin/resolve" } }, "sha512-NPRy+/ncIMeDlTAsuqwKIiferiawhefFJtkNSW0qZJEqMEb+qBt/77B/jGeeek+F0uOeN05CDa6HXbbIgtVX4w=="],
|
||||||
|
|
||||||
|
"resolve-from": ["resolve-from@4.0.0", "", {}, "sha512-pb/MYmXstAkysRFx8piNI1tGFNQIFA3vkE3Gq4EuA1dF6gHp/+vgZqsCGJapvy8N3Q+4o7FwvquPJcnZ7RYy4g=="],
|
||||||
|
|
||||||
|
"reusify": ["reusify@1.0.4", "", {}, "sha512-U9nH88a3fc/ekCF1l0/UP1IosiuIjyTh7hBvXVMHYgVcfGvt897Xguj2UOLDeI5BG2m7/uwyaLVT6fbtCwTyzw=="],
|
||||||
|
|
||||||
|
"rollup": ["rollup@4.34.7", "", { "dependencies": { "@types/estree": "1.0.6" }, "optionalDependencies": { "@rollup/rollup-android-arm-eabi": "4.34.7", "@rollup/rollup-android-arm64": "4.34.7", "@rollup/rollup-darwin-arm64": "4.34.7", "@rollup/rollup-darwin-x64": "4.34.7", "@rollup/rollup-freebsd-arm64": "4.34.7", "@rollup/rollup-freebsd-x64": "4.34.7", "@rollup/rollup-linux-arm-gnueabihf": "4.34.7", "@rollup/rollup-linux-arm-musleabihf": "4.34.7", "@rollup/rollup-linux-arm64-gnu": "4.34.7", "@rollup/rollup-linux-arm64-musl": "4.34.7", "@rollup/rollup-linux-loongarch64-gnu": "4.34.7", "@rollup/rollup-linux-powerpc64le-gnu": "4.34.7", "@rollup/rollup-linux-riscv64-gnu": "4.34.7", "@rollup/rollup-linux-s390x-gnu": "4.34.7", "@rollup/rollup-linux-x64-gnu": "4.34.7", "@rollup/rollup-linux-x64-musl": "4.34.7", "@rollup/rollup-win32-arm64-msvc": "4.34.7", "@rollup/rollup-win32-ia32-msvc": "4.34.7", "@rollup/rollup-win32-x64-msvc": "4.34.7", "fsevents": "~2.3.2" }, "bin": { "rollup": "dist/bin/rollup" } }, "sha512-8qhyN0oZ4x0H6wmBgfKxJtxM7qS98YJ0k0kNh5ECVtuchIJ7z9IVVvzpmtQyT10PXKMtBxYr1wQ5Apg8RS8kXQ=="],
|
||||||
|
|
||||||
|
"run-parallel": ["run-parallel@1.2.0", "", { "dependencies": { "queue-microtask": "^1.2.2" } }, "sha512-5l4VyZR86LZ/lDxZTR6jqL8AFE2S0IFLMP26AbjsLVADxHdhB/c0GUsH+y39UfCi3dzz8OlQuPmnaJOMoDHQBA=="],
|
||||||
|
|
||||||
|
"sade": ["sade@1.8.1", "", { "dependencies": { "mri": "^1.1.0" } }, "sha512-xal3CZX1Xlo/k4ApwCFrHVACi9fBqJ7V+mwhBsuf/1IOKbBy098Fex+Wa/5QMubw09pSZ/u8EY8PWgevJsXp1A=="],
|
||||||
|
|
||||||
|
"semver": ["semver@7.7.1", "", { "bin": { "semver": "bin/semver.js" } }, "sha512-hlq8tAfn0m/61p4BVRcPzIGr6LKiMwo4VM6dGi6pt4qcRkmNzTcWq6eCEjEh+qXjkMDvPlOFFSGwQjoEa6gyMA=="],
|
||||||
|
|
||||||
|
"set-cookie-parser": ["set-cookie-parser@2.7.1", "", {}, "sha512-IOc8uWeOZgnb3ptbCURJWNjWUPcO3ZnTTdzsurqERrP6nPyv+paC55vJM0LpOlT2ne+Ix+9+CRG1MNLlyZ4GjQ=="],
|
||||||
|
|
||||||
|
"shebang-command": ["shebang-command@2.0.0", "", { "dependencies": { "shebang-regex": "^3.0.0" } }, "sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA=="],
|
||||||
|
|
||||||
|
"shebang-regex": ["shebang-regex@3.0.0", "", {}, "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A=="],
|
||||||
|
|
||||||
|
"signal-exit": ["signal-exit@4.1.0", "", {}, "sha512-bzyZ1e88w9O1iNJbKnOlvYTrWPDl46O1bG0D3XInv+9tkPrxrN8jUUTiFlDkkmKWgn1M6CfIA13SuGqOa9Korw=="],
|
||||||
|
|
||||||
|
"sirv": ["sirv@3.0.0", "", { "dependencies": { "@polka/url": "^1.0.0-next.24", "mrmime": "^2.0.0", "totalist": "^3.0.0" } }, "sha512-BPwJGUeDaDCHihkORDchNyyTvWFhcusy1XMmhEVTQTwGeybFbp8YEmB+njbPnth1FibULBSBVwCQni25XlCUDg=="],
|
||||||
|
|
||||||
|
"source-map-js": ["source-map-js@1.2.1", "", {}, "sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA=="],
|
||||||
|
|
||||||
|
"string-width": ["string-width@5.1.2", "", { "dependencies": { "eastasianwidth": "^0.2.0", "emoji-regex": "^9.2.2", "strip-ansi": "^7.0.1" } }, "sha512-HnLOCR3vjcY8beoNLtcjZ5/nxn2afmME6lhrDrebokqMap+XbeW8n9TXpPDOqdGK5qcI3oT0GKTW6wC7EMiVqA=="],
|
||||||
|
|
||||||
|
"string-width-cjs": ["string-width@4.2.3", "", { "dependencies": { "emoji-regex": "^8.0.0", "is-fullwidth-code-point": "^3.0.0", "strip-ansi": "^6.0.1" } }, "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g=="],
|
||||||
|
|
||||||
|
"strip-ansi": ["strip-ansi@7.1.0", "", { "dependencies": { "ansi-regex": "^6.0.1" } }, "sha512-iq6eVVI64nQQTRYq2KtEg2d2uU7LElhTJwsH4YzIHZshxlgZms/wIc4VoDQTlG/IvVIrBKG06CrZnp0qv7hkcQ=="],
|
||||||
|
|
||||||
|
"strip-ansi-cjs": ["strip-ansi@6.0.1", "", { "dependencies": { "ansi-regex": "^5.0.1" } }, "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A=="],
|
||||||
|
|
||||||
|
"strip-json-comments": ["strip-json-comments@3.1.1", "", {}, "sha512-6fPc+R4ihwqP6N/aIv2f1gMH8lOVtWQHoqC4yK6oSDVVocumAsfCqjkXnqiYMhmMwS/mEHLp7Vehlt3ql6lEig=="],
|
||||||
|
|
||||||
|
"sucrase": ["sucrase@3.35.0", "", { "dependencies": { "@jridgewell/gen-mapping": "^0.3.2", "commander": "^4.0.0", "glob": "^10.3.10", "lines-and-columns": "^1.1.6", "mz": "^2.7.0", "pirates": "^4.0.1", "ts-interface-checker": "^0.1.9" }, "bin": { "sucrase": "bin/sucrase", "sucrase-node": "bin/sucrase-node" } }, "sha512-8EbVDiu9iN/nESwxeSxDKe0dunta1GOlHufmSSXxMD2z2/tMZpDMpvXQGsc+ajGo8y2uYUmixaSRUc/QPoQ0GA=="],
|
||||||
|
|
||||||
|
"supports-color": ["supports-color@7.2.0", "", { "dependencies": { "has-flag": "^4.0.0" } }, "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw=="],
|
||||||
|
|
||||||
|
"supports-preserve-symlinks-flag": ["supports-preserve-symlinks-flag@1.0.0", "", {}, "sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w=="],
|
||||||
|
|
||||||
|
"svelte": ["svelte@5.20.1", "", { "dependencies": { "@ampproject/remapping": "^2.3.0", "@jridgewell/sourcemap-codec": "^1.5.0", "@types/estree": "^1.0.5", "acorn": "^8.12.1", "acorn-typescript": "^1.4.13", "aria-query": "^5.3.1", "axobject-query": "^4.1.0", "clsx": "^2.1.1", "esm-env": "^1.2.1", "esrap": "^1.4.3", "is-reference": "^3.0.3", "locate-character": "^3.0.0", "magic-string": "^0.30.11", "zimmerframe": "^1.1.2" } }, "sha512-aCARru2WTdzJl55Ws8SK27+kvQwd8tijl4kY7NoDUXUHtTHhxMa8Lf6QNZKmU7cuPu3jjFloDO1j5HgYJNIIWg=="],
|
||||||
|
|
||||||
|
"svelte-check": ["svelte-check@4.1.4", "", { "dependencies": { "@jridgewell/trace-mapping": "^0.3.25", "chokidar": "^4.0.1", "fdir": "^6.2.0", "picocolors": "^1.0.0", "sade": "^1.7.4" }, "peerDependencies": { "svelte": "^4.0.0 || ^5.0.0-next.0", "typescript": ">=5.0.0" }, "bin": { "svelte-check": "bin/svelte-check" } }, "sha512-v0j7yLbT29MezzaQJPEDwksybTE2Ups9rUxEXy92T06TiA0cbqcO8wAOwNUVkFW6B0hsYHA+oAX3BS8b/2oHtw=="],
|
||||||
|
|
||||||
|
"svelte-eslint-parser": ["svelte-eslint-parser@0.43.0", "", { "dependencies": { "eslint-scope": "^7.2.2", "eslint-visitor-keys": "^3.4.3", "espree": "^9.6.1", "postcss": "^8.4.39", "postcss-scss": "^4.0.9" }, "peerDependencies": { "svelte": "^3.37.0 || ^4.0.0 || ^5.0.0" }, "optionalPeers": ["svelte"] }, "sha512-GpU52uPKKcVnh8tKN5P4UZpJ/fUDndmq7wfsvoVXsyP+aY0anol7Yqo01fyrlaWGMFfm4av5DyrjlaXdLRJvGA=="],
|
||||||
|
|
||||||
|
"svelte-sonner": ["svelte-sonner@0.3.28", "", { "peerDependencies": { "svelte": "^3.0.0 || ^4.0.0 || ^5.0.0-next.1" } }, "sha512-K3AmlySeFifF/cKgsYNv5uXqMVNln0NBAacOYgmkQStLa/UoU0LhfAACU6Gr+YYC8bOCHdVmFNoKuDbMEsppJg=="],
|
||||||
|
|
||||||
|
"tabbable": ["tabbable@6.2.0", "", {}, "sha512-Cat63mxsVJlzYvN51JmVXIgNoUokrIaT2zLclCXjRd8boZ0004U4KCs/sToJ75C6sdlByWxpYnb5Boif1VSFew=="],
|
||||||
|
|
||||||
|
"tailwind-merge": ["tailwind-merge@3.0.1", "", {}, "sha512-AvzE8FmSoXC7nC+oU5GlQJbip2UO7tmOhOfQyOmPhrStOGXHU08j8mZEHZ4BmCqY5dWTCo4ClWkNyRNx1wpT0g=="],
|
||||||
|
|
||||||
|
"tailwind-variants": ["tailwind-variants@0.3.1", "", { "dependencies": { "tailwind-merge": "2.5.4" }, "peerDependencies": { "tailwindcss": "*" } }, "sha512-krn67M3FpPwElg4FsZrOQd0U26o7UDH/QOkK8RNaiCCrr052f6YJPBUfNKnPo/s/xRzNPtv1Mldlxsg8Tb46BQ=="],
|
||||||
|
|
||||||
|
"tailwindcss": ["tailwindcss@3.4.17", "", { "dependencies": { "@alloc/quick-lru": "^5.2.0", "arg": "^5.0.2", "chokidar": "^3.6.0", "didyoumean": "^1.2.2", "dlv": "^1.1.3", "fast-glob": "^3.3.2", "glob-parent": "^6.0.2", "is-glob": "^4.0.3", "jiti": "^1.21.6", "lilconfig": "^3.1.3", "micromatch": "^4.0.8", "normalize-path": "^3.0.0", "object-hash": "^3.0.0", "picocolors": "^1.1.1", "postcss": "^8.4.47", "postcss-import": "^15.1.0", "postcss-js": "^4.0.1", "postcss-load-config": "^4.0.2", "postcss-nested": "^6.2.0", "postcss-selector-parser": "^6.1.2", "resolve": "^1.22.8", "sucrase": "^3.35.0" }, "bin": { "tailwind": "lib/cli.js", "tailwindcss": "lib/cli.js" } }, "sha512-w33E2aCvSDP0tW9RZuNXadXlkHXqFzSkQew/aIa2i/Sj8fThxwovwlXHSPXTbAHwEIhBFXAedUhP2tueAKP8Og=="],
|
||||||
|
|
||||||
|
"tailwindcss-animate": ["tailwindcss-animate@1.0.7", "", { "peerDependencies": { "tailwindcss": ">=3.0.0 || insiders" } }, "sha512-bl6mpH3T7I3UFxuvDEXLxy/VuFxBk5bbzplh7tXI68mwMokNYd1t9qPBHlnyTwfa4JGC4zP516I1hYYtQ/vspA=="],
|
||||||
|
|
||||||
|
"thenify": ["thenify@3.3.1", "", { "dependencies": { "any-promise": "^1.0.0" } }, "sha512-RVZSIV5IG10Hk3enotrhvz0T9em6cyHBLkH/YAZuKqd8hRkKhSfCGIcP2KUY0EPxndzANBmNllzWPwak+bheSw=="],
|
||||||
|
|
||||||
|
"thenify-all": ["thenify-all@1.6.0", "", { "dependencies": { "thenify": ">= 3.1.0 < 4" } }, "sha512-RNxQH/qI8/t3thXJDwcstUO4zeqo64+Uy/+sNVRBx4Xn2OX+OZ9oP+iJnNFqplFra2ZUVeKCSa2oVWi3T4uVmA=="],
|
||||||
|
|
||||||
|
"to-regex-range": ["to-regex-range@5.0.1", "", { "dependencies": { "is-number": "^7.0.0" } }, "sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ=="],
|
||||||
|
|
||||||
|
"totalist": ["totalist@3.0.1", "", {}, "sha512-sf4i37nQ2LBx4m3wB74y+ubopq6W/dIzXg0FDGjsYnZHVa1Da8FH853wlL2gtUhg+xJXjfk3kUZS3BRoQeoQBQ=="],
|
||||||
|
|
||||||
|
"ts-api-utils": ["ts-api-utils@2.0.1", "", { "peerDependencies": { "typescript": ">=4.8.4" } }, "sha512-dnlgjFSVetynI8nzgJ+qF62efpglpWRk8isUEWZGWlJYySCTD6aKvbUDu+zbPeDakk3bg5H4XpitHukgfL1m9w=="],
|
||||||
|
|
||||||
|
"ts-interface-checker": ["ts-interface-checker@0.1.13", "", {}, "sha512-Y/arvbn+rrz3JCKl9C4kVNfTfSm2/mEp5FSz5EsZSANGPSlQrpRI5M4PKF+mJnE52jOO90PnPSc3Ur3bTQw0gA=="],
|
||||||
|
|
||||||
|
"tslib": ["tslib@2.8.1", "", {}, "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w=="],
|
||||||
|
|
||||||
|
"type-check": ["type-check@0.4.0", "", { "dependencies": { "prelude-ls": "^1.2.1" } }, "sha512-XleUoc9uwGXqjWwXaUTZAmzMcFZ5858QA2vvx1Ur5xIcixXIP+8LnFDgRplU30us6teqdlskFfu+ae4K79Ooew=="],
|
||||||
|
|
||||||
|
"typescript": ["typescript@5.7.3", "", { "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" } }, "sha512-84MVSjMEHP+FQRPy3pX9sTVV/INIex71s9TL2Gm5FG/WG1SqXeKyZ0k7/blY/4FdOzI12CBy1vGc4og/eus0fw=="],
|
||||||
|
|
||||||
|
"typescript-eslint": ["typescript-eslint@8.24.0", "", { "dependencies": { "@typescript-eslint/eslint-plugin": "8.24.0", "@typescript-eslint/parser": "8.24.0", "@typescript-eslint/utils": "8.24.0" }, "peerDependencies": { "eslint": "^8.57.0 || ^9.0.0", "typescript": ">=4.8.4 <5.8.0" } }, "sha512-/lmv4366en/qbB32Vz5+kCNZEMf6xYHwh1z48suBwZvAtnXKbP+YhGe8OLE2BqC67LMqKkCNLtjejdwsdW6uOQ=="],
|
||||||
|
|
||||||
|
"update-browserslist-db": ["update-browserslist-db@1.1.2", "", { "dependencies": { "escalade": "^3.2.0", "picocolors": "^1.1.1" }, "peerDependencies": { "browserslist": ">= 4.21.0" }, "bin": { "update-browserslist-db": "cli.js" } }, "sha512-PPypAm5qvlD7XMZC3BujecnaOxwhrtoFR+Dqkk5Aa/6DssiH0ibKoketaj9w8LP7Bont1rYeoV5plxD7RTEPRg=="],
|
||||||
|
|
||||||
|
"uri-js": ["uri-js@4.4.1", "", { "dependencies": { "punycode": "^2.1.0" } }, "sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg=="],
|
||||||
|
|
||||||
|
"util-deprecate": ["util-deprecate@1.0.2", "", {}, "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw=="],
|
||||||
|
|
||||||
|
"vite": ["vite@6.1.0", "", { "dependencies": { "esbuild": "^0.24.2", "postcss": "^8.5.1", "rollup": "^4.30.1" }, "optionalDependencies": { "fsevents": "~2.3.3" }, "peerDependencies": { "@types/node": "^18.0.0 || ^20.0.0 || >=22.0.0", "jiti": ">=1.21.0", "less": "*", "lightningcss": "^1.21.0", "sass": "*", "sass-embedded": "*", "stylus": "*", "sugarss": "*", "terser": "^5.16.0", "tsx": "^4.8.1", "yaml": "^2.4.2" }, "optionalPeers": ["@types/node", "jiti", "less", "lightningcss", "sass", "sass-embedded", "stylus", "sugarss", "terser", "tsx", "yaml"], "bin": { "vite": "bin/vite.js" } }, "sha512-RjjMipCKVoR4hVfPY6GQTgveinjNuyLw+qruksLDvA5ktI1150VmcMBKmQaEWJhg/j6Uaf6dNCNA0AfdzUb/hQ=="],
|
||||||
|
|
||||||
|
"vitefu": ["vitefu@1.0.5", "", { "peerDependencies": { "vite": "^3.0.0 || ^4.0.0 || ^5.0.0 || ^6.0.0" }, "optionalPeers": ["vite"] }, "sha512-h4Vflt9gxODPFNGPwp4zAMZRpZR7eslzwH2c5hn5kNZ5rhnKyRJ50U+yGCdc2IRaBs8O4haIgLNGrV5CrpMsCA=="],
|
||||||
|
|
||||||
|
"which": ["which@2.0.2", "", { "dependencies": { "isexe": "^2.0.0" }, "bin": { "node-which": "./bin/node-which" } }, "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA=="],
|
||||||
|
|
||||||
|
"word-wrap": ["word-wrap@1.2.5", "", {}, "sha512-BN22B5eaMMI9UMtjrGd5g5eCYPpCPDUy0FJXbYsaT5zYxjFOckS53SQDE3pWkVoWpHXVb3BrYcEN4Twa55B5cA=="],
|
||||||
|
|
||||||
|
"wrap-ansi": ["wrap-ansi@8.1.0", "", { "dependencies": { "ansi-styles": "^6.1.0", "string-width": "^5.0.1", "strip-ansi": "^7.0.1" } }, "sha512-si7QWI6zUMq56bESFvagtmzMdGOtoxfR+Sez11Mobfc7tm+VkUckk9bW2UeffTGVUbOksxmSw0AA2gs8g71NCQ=="],
|
||||||
|
|
||||||
|
"wrap-ansi-cjs": ["wrap-ansi@7.0.0", "", { "dependencies": { "ansi-styles": "^4.0.0", "string-width": "^4.1.0", "strip-ansi": "^6.0.0" } }, "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q=="],
|
||||||
|
|
||||||
|
"yaml": ["yaml@1.10.2", "", {}, "sha512-r3vXyErRCYJ7wg28yvBY5VSoAF8ZvlcW9/BwUzEtUsjvX/DKs24dIkuwjtuprwJJHsbyUbLApepYTR1BN4uHrg=="],
|
||||||
|
|
||||||
|
"yocto-queue": ["yocto-queue@0.1.0", "", {}, "sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q=="],
|
||||||
|
|
||||||
|
"zimmerframe": ["zimmerframe@1.1.2", "", {}, "sha512-rAbqEGa8ovJy4pyBxZM70hg4pE6gDgaQ0Sl9M3enG3I0d6H4XSAM3GeNGLKnsBpuijUow064sf7ww1nutC5/3w=="],
|
||||||
|
|
||||||
|
"@eslint-community/eslint-utils/eslint-visitor-keys": ["eslint-visitor-keys@3.4.3", "", {}, "sha512-wpc+LXeiyiisxPlEkUzU6svyS1frIO3Mgxj1fdy7Pm8Ygzguax2N3Fa/D/ag1WqbOprdI+uY6wMUl8/a2G+iag=="],
|
||||||
|
|
||||||
|
"@eslint/eslintrc/globals": ["globals@14.0.0", "", {}, "sha512-oahGvuMGQlPw/ivIYBjVSrWAfWLBeku5tpPE2fOPLi+WHffIWbuh2tCjhyQhTBPMf5E9jDEH4FOmTYgYwbKwtQ=="],
|
||||||
|
|
||||||
|
"@eslint/plugin-kit/@eslint/core": ["@eslint/core@0.10.0", "", { "dependencies": { "@types/json-schema": "^7.0.15" } }, "sha512-gFHJ+xBOo4G3WRlR1e/3G8A6/KZAH6zcE/hkLRCZTi/B9avAG365QhFA8uOGzTMqgTghpn7/fSnscW++dpMSAw=="],
|
||||||
|
|
||||||
|
"@humanfs/node/@humanwhocodes/retry": ["@humanwhocodes/retry@0.3.1", "", {}, "sha512-JBxkERygn7Bv/GbN5Rv8Ul6LVknS+5Bp6RgDC/O8gEBU/yeH5Ui5C/OlWrTb6qct7LjjfT6Re2NxB0ln0yYybA=="],
|
||||||
|
|
||||||
|
"@typescript-eslint/typescript-estree/minimatch": ["minimatch@9.0.5", "", { "dependencies": { "brace-expansion": "^2.0.1" } }, "sha512-G6T0ZX48xgozx7587koeX9Ys2NYy6Gmv//P89sEte9V9whIapMNF4idKxnW2QtCcLiTWlb/wfCabAtAFWhhBow=="],
|
||||||
|
|
||||||
|
"fast-glob/glob-parent": ["glob-parent@5.1.2", "", { "dependencies": { "is-glob": "^4.0.1" } }, "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow=="],
|
||||||
|
|
||||||
|
"glob/minimatch": ["minimatch@9.0.5", "", { "dependencies": { "brace-expansion": "^2.0.1" } }, "sha512-G6T0ZX48xgozx7587koeX9Ys2NYy6Gmv//P89sEte9V9whIapMNF4idKxnW2QtCcLiTWlb/wfCabAtAFWhhBow=="],
|
||||||
|
|
||||||
|
"postcss/nanoid": ["nanoid@3.3.8", "", { "bin": { "nanoid": "bin/nanoid.cjs" } }, "sha512-WNLf5Sd8oZxOm+TzppcYk8gVOgP+l58xNy58D0nbUnOxOWRWvlcCV4kUF7ltmI6PsrLl/BgKEyS4mqsGChFN0w=="],
|
||||||
|
|
||||||
|
"postcss-load-config/lilconfig": ["lilconfig@2.1.0", "", {}, "sha512-utWOt/GHzuUxnLKxB6dk81RoOeoNeHgbrXiuGk4yyF5qlRz+iIVWu56E2fqGHFrXz0QNUhLB/8nKqvRH66JKGQ=="],
|
||||||
|
|
||||||
|
"string-width-cjs/emoji-regex": ["emoji-regex@8.0.0", "", {}, "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A=="],
|
||||||
|
|
||||||
|
"string-width-cjs/strip-ansi": ["strip-ansi@6.0.1", "", { "dependencies": { "ansi-regex": "^5.0.1" } }, "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A=="],
|
||||||
|
|
||||||
|
"strip-ansi-cjs/ansi-regex": ["ansi-regex@5.0.1", "", {}, "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ=="],
|
||||||
|
|
||||||
|
"svelte-eslint-parser/eslint-scope": ["eslint-scope@7.2.2", "", { "dependencies": { "esrecurse": "^4.3.0", "estraverse": "^5.2.0" } }, "sha512-dOt21O7lTMhDM+X9mB4GX+DZrZtCUJPL/wlcTqxyrx5IvO0IYtILdtrQGQp+8n5S0gwSVmOf9NQrjMOgfQZlIg=="],
|
||||||
|
|
||||||
|
"svelte-eslint-parser/eslint-visitor-keys": ["eslint-visitor-keys@3.4.3", "", {}, "sha512-wpc+LXeiyiisxPlEkUzU6svyS1frIO3Mgxj1fdy7Pm8Ygzguax2N3Fa/D/ag1WqbOprdI+uY6wMUl8/a2G+iag=="],
|
||||||
|
|
||||||
|
"svelte-eslint-parser/espree": ["espree@9.6.1", "", { "dependencies": { "acorn": "^8.9.0", "acorn-jsx": "^5.3.2", "eslint-visitor-keys": "^3.4.1" } }, "sha512-oruZaFkjorTpF32kDSI5/75ViwGeZginGGy2NoOSg3Q9bnwlnmDm4HLnkl0RE3n+njDXR037aY1+x58Z/zFdwQ=="],
|
||||||
|
|
||||||
|
"tailwind-variants/tailwind-merge": ["tailwind-merge@2.5.4", "", {}, "sha512-0q8cfZHMu9nuYP/b5Shb7Y7Sh1B7Nnl5GqNr1U+n2p6+mybvRtayrQ+0042Z5byvTA8ihjlP8Odo8/VnHbZu4Q=="],
|
||||||
|
|
||||||
|
"tailwindcss/chokidar": ["chokidar@3.6.0", "", { "dependencies": { "anymatch": "~3.1.2", "braces": "~3.0.2", "glob-parent": "~5.1.2", "is-binary-path": "~2.1.0", "is-glob": "~4.0.1", "normalize-path": "~3.0.0", "readdirp": "~3.6.0" }, "optionalDependencies": { "fsevents": "~2.3.2" } }, "sha512-7VT13fmjotKpGipCW9JEQAusEPE+Ei8nl6/g4FBAmIm0GOOLMua9NDDo/DWp0ZAxCr3cPq5ZpBqmPAQgDda2Pw=="],
|
||||||
|
|
||||||
|
"tailwindcss/postcss-load-config": ["postcss-load-config@4.0.2", "", { "dependencies": { "lilconfig": "^3.0.0", "yaml": "^2.3.4" }, "peerDependencies": { "postcss": ">=8.0.9", "ts-node": ">=9.0.0" }, "optionalPeers": ["postcss", "ts-node"] }, "sha512-bSVhyJGL00wMVoPUzAVAnbEoWyqRxkjv64tUl427SKnPrENtq6hJwUojroMz2VB+Q1edmi4IfrAPpami5VVgMQ=="],
|
||||||
|
|
||||||
|
"wrap-ansi/ansi-styles": ["ansi-styles@6.2.1", "", {}, "sha512-bN798gFfQX+viw3R7yrGWRqnrN2oRkEkUjjl4JNn4E8GxxbjtG3FbrEIIY3l8/hrwUwIeCZvi4QuOTP4MErVug=="],
|
||||||
|
|
||||||
|
"wrap-ansi-cjs/string-width": ["string-width@4.2.3", "", { "dependencies": { "emoji-regex": "^8.0.0", "is-fullwidth-code-point": "^3.0.0", "strip-ansi": "^6.0.1" } }, "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g=="],
|
||||||
|
|
||||||
|
"wrap-ansi-cjs/strip-ansi": ["strip-ansi@6.0.1", "", { "dependencies": { "ansi-regex": "^5.0.1" } }, "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A=="],
|
||||||
|
|
||||||
|
"@typescript-eslint/typescript-estree/minimatch/brace-expansion": ["brace-expansion@2.0.1", "", { "dependencies": { "balanced-match": "^1.0.0" } }, "sha512-XnAIvQ8eM+kC6aULx6wuQiwVsnzsi9d3WxzV3FpWTGA19F621kwdbsAcFKXgKUHZWsy+mY6iL1sHTxWEFCytDA=="],
|
||||||
|
|
||||||
|
"glob/minimatch/brace-expansion": ["brace-expansion@2.0.1", "", { "dependencies": { "balanced-match": "^1.0.0" } }, "sha512-XnAIvQ8eM+kC6aULx6wuQiwVsnzsi9d3WxzV3FpWTGA19F621kwdbsAcFKXgKUHZWsy+mY6iL1sHTxWEFCytDA=="],
|
||||||
|
|
||||||
|
"string-width-cjs/strip-ansi/ansi-regex": ["ansi-regex@5.0.1", "", {}, "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ=="],
|
||||||
|
|
||||||
|
"tailwindcss/chokidar/glob-parent": ["glob-parent@5.1.2", "", { "dependencies": { "is-glob": "^4.0.1" } }, "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow=="],
|
||||||
|
|
||||||
|
"tailwindcss/chokidar/readdirp": ["readdirp@3.6.0", "", { "dependencies": { "picomatch": "^2.2.1" } }, "sha512-hOS089on8RduqdbhvQ5Z37A0ESjsqz6qnRcffsMU3495FuTdqSm+7bhJ29JvIOsBDEEnan5DPu9t3To9VRlMzA=="],
|
||||||
|
|
||||||
|
"tailwindcss/postcss-load-config/yaml": ["yaml@2.7.0", "", { "bin": { "yaml": "bin.mjs" } }, "sha512-+hSoy/QHluxmC9kCIJyL/uyFmLmc+e5CFR5Wa+bpIhIj85LVb9ZH2nVnqrHoSvKogwODv0ClqZkmiSSaIH5LTA=="],
|
||||||
|
|
||||||
|
"wrap-ansi-cjs/string-width/emoji-regex": ["emoji-regex@8.0.0", "", {}, "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A=="],
|
||||||
|
|
||||||
|
"wrap-ansi-cjs/strip-ansi/ansi-regex": ["ansi-regex@5.0.1", "", {}, "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ=="],
|
||||||
|
}
|
||||||
|
}
|
||||||
17
web/components.json
Normal file
17
web/components.json
Normal file
@@ -0,0 +1,17 @@
|
|||||||
|
{
|
||||||
|
"$schema": "https://next.shadcn-svelte.com/schema.json",
|
||||||
|
"style": "default",
|
||||||
|
"tailwind": {
|
||||||
|
"config": "tailwind.config.ts",
|
||||||
|
"css": "src/app.css",
|
||||||
|
"baseColor": "slate"
|
||||||
|
},
|
||||||
|
"aliases": {
|
||||||
|
"components": "$lib/components",
|
||||||
|
"utils": "$lib/utils",
|
||||||
|
"ui": "$lib/components/ui",
|
||||||
|
"hooks": "$lib/hooks"
|
||||||
|
},
|
||||||
|
"typescript": true,
|
||||||
|
"registry": "https://next.shadcn-svelte.com/registry"
|
||||||
|
}
|
||||||
34
web/eslint.config.js
Normal file
34
web/eslint.config.js
Normal file
@@ -0,0 +1,34 @@
|
|||||||
|
import prettier from 'eslint-config-prettier';
|
||||||
|
import js from '@eslint/js';
|
||||||
|
import { includeIgnoreFile } from '@eslint/compat';
|
||||||
|
import svelte from 'eslint-plugin-svelte';
|
||||||
|
import globals from 'globals';
|
||||||
|
import { fileURLToPath } from 'node:url';
|
||||||
|
import ts from 'typescript-eslint';
|
||||||
|
const gitignorePath = fileURLToPath(new URL('./.gitignore', import.meta.url));
|
||||||
|
|
||||||
|
export default ts.config(
|
||||||
|
includeIgnoreFile(gitignorePath),
|
||||||
|
js.configs.recommended,
|
||||||
|
...ts.configs.recommended,
|
||||||
|
...svelte.configs['flat/recommended'],
|
||||||
|
prettier,
|
||||||
|
...svelte.configs['flat/prettier'],
|
||||||
|
{
|
||||||
|
languageOptions: {
|
||||||
|
globals: {
|
||||||
|
...globals.browser,
|
||||||
|
...globals.node
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
files: ['**/*.svelte'],
|
||||||
|
|
||||||
|
languageOptions: {
|
||||||
|
parserOptions: {
|
||||||
|
parser: ts.parser
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
);
|
||||||
44
web/package.json
Normal file
44
web/package.json
Normal file
@@ -0,0 +1,44 @@
|
|||||||
|
{
|
||||||
|
"name": "web",
|
||||||
|
"private": true,
|
||||||
|
"version": "0.0.1",
|
||||||
|
"type": "module",
|
||||||
|
"scripts": {
|
||||||
|
"dev": "vite dev",
|
||||||
|
"build": "vite build",
|
||||||
|
"preview": "vite preview",
|
||||||
|
"prepare": "svelte-kit sync || echo ''",
|
||||||
|
"check": "svelte-kit sync && svelte-check --tsconfig ./tsconfig.json",
|
||||||
|
"check:watch": "svelte-kit sync && svelte-check --tsconfig ./tsconfig.json --watch",
|
||||||
|
"format": "prettier --write .",
|
||||||
|
"lint": "prettier --check . && eslint ."
|
||||||
|
},
|
||||||
|
"devDependencies": {
|
||||||
|
"@eslint/compat": "^1.2.5",
|
||||||
|
"@eslint/js": "^9.18.0",
|
||||||
|
"@sveltejs/adapter-static": "^3.0.8",
|
||||||
|
"@sveltejs/kit": "^2.16.0",
|
||||||
|
"@sveltejs/vite-plugin-svelte": "^5.0.0",
|
||||||
|
"autoprefixer": "^10.4.20",
|
||||||
|
"bits-ui": "0.22.0",
|
||||||
|
"clsx": "^2.1.1",
|
||||||
|
"eslint": "^9.18.0",
|
||||||
|
"eslint-config-prettier": "^10.0.1",
|
||||||
|
"eslint-plugin-svelte": "^2.46.1",
|
||||||
|
"globals": "^15.14.0",
|
||||||
|
"mode-watcher": "^0.5.1",
|
||||||
|
"prettier": "^3.4.2",
|
||||||
|
"prettier-plugin-svelte": "^3.3.3",
|
||||||
|
"prettier-plugin-tailwindcss": "^0.6.10",
|
||||||
|
"svelte": "^5.0.0",
|
||||||
|
"svelte-check": "^4.0.0",
|
||||||
|
"svelte-sonner": "^0.3.28",
|
||||||
|
"tailwind-merge": "^3.0.1",
|
||||||
|
"tailwind-variants": "^0.3.1",
|
||||||
|
"tailwindcss": "^3.4.17",
|
||||||
|
"tailwindcss-animate": "^1.0.7",
|
||||||
|
"typescript": "^5.0.0",
|
||||||
|
"typescript-eslint": "^8.20.0",
|
||||||
|
"vite": "^6.0.0"
|
||||||
|
}
|
||||||
|
}
|
||||||
6
web/postcss.config.js
Normal file
6
web/postcss.config.js
Normal file
@@ -0,0 +1,6 @@
|
|||||||
|
export default {
|
||||||
|
plugins: {
|
||||||
|
tailwindcss: {},
|
||||||
|
autoprefixer: {}
|
||||||
|
}
|
||||||
|
};
|
||||||
61
web/src/app.css
Normal file
61
web/src/app.css
Normal file
@@ -0,0 +1,61 @@
|
|||||||
|
@tailwind base;
|
||||||
|
@tailwind components;
|
||||||
|
@tailwind utilities;
|
||||||
|
|
||||||
|
@layer base {
|
||||||
|
:root {
|
||||||
|
--background: 0 0% 100%;
|
||||||
|
--foreground: 240 10% 3.9%;
|
||||||
|
--card: 0 0% 100%;
|
||||||
|
--card-foreground: 240 10% 3.9%;
|
||||||
|
--popover: 0 0% 100%;
|
||||||
|
;
|
||||||
|
--popover-foreground: 240 10% 3.9%;
|
||||||
|
--primary: 240 5.9% 10%;
|
||||||
|
--primary-foreground: 0 0% 98%;
|
||||||
|
--secondary: 240 4.8% 95.9%;
|
||||||
|
--secondary-foreground: 240 5.9% 10%;
|
||||||
|
--muted: 240 4.8% 95.9%;
|
||||||
|
--muted-foreground: 240 3.8% 46.1%;
|
||||||
|
--accent: 240 4.8% 95.9%;
|
||||||
|
--accent-foreground: 240 5.9% 10%;
|
||||||
|
--destructive: 0 72.22% 50.59%;
|
||||||
|
--destructive-foreground: 0 0% 98%;
|
||||||
|
--border: 240 5.9% 90%;
|
||||||
|
--input: 240 5.9% 90%;
|
||||||
|
--ring: 240 5.9% 10%;
|
||||||
|
--radius: 0.5rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.dark {
|
||||||
|
--background: 240 10% 3.9%;
|
||||||
|
--foreground: 0 0% 98%;
|
||||||
|
--card: 240 10% 3.9%;
|
||||||
|
--card-foreground: 0 0% 98%;
|
||||||
|
--popover: 240 10% 3.9%;
|
||||||
|
--popover-foreground: 0 0% 98%;
|
||||||
|
--primary: 0 0% 98%;
|
||||||
|
--primary-foreground: 240 5.9% 10%;
|
||||||
|
--secondary: 240 3.7% 15.9%;
|
||||||
|
--secondary-foreground: 0 0% 98%;
|
||||||
|
--muted: 240 3.7% 15.9%;
|
||||||
|
--muted-foreground: 240 5% 64.9%;
|
||||||
|
--accent: 240 3.7% 15.9%;
|
||||||
|
--accent-foreground: 0 0% 98%;
|
||||||
|
--destructive: 0 62.8% 30.6%;
|
||||||
|
--destructive-foreground: 0 0% 98%;
|
||||||
|
--border: 240 3.7% 15.9%;
|
||||||
|
--input: 240 3.7% 15.9%;
|
||||||
|
--ring: 240 4.9% 83.9%;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@layer base {
|
||||||
|
* {
|
||||||
|
@apply border-border;
|
||||||
|
}
|
||||||
|
|
||||||
|
body {
|
||||||
|
@apply bg-background text-foreground;
|
||||||
|
}
|
||||||
|
}
|
||||||
13
web/src/app.d.ts
vendored
Normal file
13
web/src/app.d.ts
vendored
Normal file
@@ -0,0 +1,13 @@
|
|||||||
|
// See https://svelte.dev/docs/kit/types#app.d.ts
|
||||||
|
// for information about these interfaces
|
||||||
|
declare global {
|
||||||
|
namespace App {
|
||||||
|
// interface Error {}
|
||||||
|
// interface Locals {}
|
||||||
|
// interface PageData {}
|
||||||
|
// interface PageState {}
|
||||||
|
// interface Platform {}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export {};
|
||||||
12
web/src/app.html
Normal file
12
web/src/app.html
Normal file
@@ -0,0 +1,12 @@
|
|||||||
|
<!doctype html>
|
||||||
|
<html lang="en">
|
||||||
|
<head>
|
||||||
|
<meta charset="utf-8" />
|
||||||
|
<link rel="icon" href="%sveltekit.assets%/favicon.png" />
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
||||||
|
%sveltekit.head%
|
||||||
|
</head>
|
||||||
|
<body data-sveltekit-preload-data="hover">
|
||||||
|
<div style="display: contents">%sveltekit.body%</div>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
56
web/src/lib/api.ts
Normal file
56
web/src/lib/api.ts
Normal file
@@ -0,0 +1,56 @@
|
|||||||
|
import type { VideoResponse, VideoInfo, VideosResponse, VideoSourcesResponse, ResetVideoResponse } from './types';
|
||||||
|
|
||||||
|
const BASE_URL = '/api';
|
||||||
|
|
||||||
|
export class ApiError extends Error {
|
||||||
|
constructor(message: string) {
|
||||||
|
super(message);
|
||||||
|
this.name = 'ApiError';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function fetchWithAuth(url: string, options: RequestInit = {}) {
|
||||||
|
const token = localStorage.getItem('auth_token');
|
||||||
|
const headers = {
|
||||||
|
...options.headers,
|
||||||
|
'Authorization': token || ''
|
||||||
|
};
|
||||||
|
|
||||||
|
const response = await fetch(url, { ...options, headers });
|
||||||
|
if (!response.ok) {
|
||||||
|
throw new ApiError(`API request failed: ${response.statusText}, body: ${await response.text()}`);
|
||||||
|
}
|
||||||
|
let { data } = await response.json();
|
||||||
|
return data;
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function getVideoSources(): Promise<VideoSourcesResponse> {
|
||||||
|
return fetchWithAuth(`${BASE_URL}/video-sources`);
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function listVideos(params: {
|
||||||
|
collection?: string;
|
||||||
|
favorite?: string;
|
||||||
|
submission?: string;
|
||||||
|
watch_later?: string;
|
||||||
|
query?: string;
|
||||||
|
page?: number;
|
||||||
|
page_size?: number;
|
||||||
|
}): Promise<VideosResponse> {
|
||||||
|
const searchParams = new URLSearchParams();
|
||||||
|
Object.entries(params).forEach(([key, value]) => {
|
||||||
|
if (value !== undefined) {
|
||||||
|
searchParams.append(key, value.toString());
|
||||||
|
}
|
||||||
|
});
|
||||||
|
return fetchWithAuth(`${BASE_URL}/videos?${searchParams.toString()}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
export async function getVideo(id: number): Promise<VideoResponse> {
|
||||||
|
return fetchWithAuth(`${BASE_URL}/videos/${id}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function resetVideo(id: number): Promise<ResetVideoResponse> {
|
||||||
|
return fetchWithAuth(`${BASE_URL}/videos/${id}/reset`, { method: 'POST' });
|
||||||
|
}
|
||||||
16
web/src/lib/components/Header.svelte
Normal file
16
web/src/lib/components/Header.svelte
Normal file
@@ -0,0 +1,16 @@
|
|||||||
|
<script lang="ts">
|
||||||
|
import { Input } from '$lib/components/ui/input';
|
||||||
|
let apiToken: string = localStorage.getItem('auth_token') || '';
|
||||||
|
function updateToken() {
|
||||||
|
localStorage.setItem('auth_token', apiToken);
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<header class="flex items-center justify-between bg-gray-100 p-4">
|
||||||
|
<h1 class="text-xl font-bold">bili-sync 管理页</h1>
|
||||||
|
<div>
|
||||||
|
<Input type="password" placeholder="API Token" bind:value={apiToken} on:change={updateToken} />
|
||||||
|
</div>
|
||||||
|
</header>
|
||||||
|
|
||||||
|
<slot />
|
||||||
119
web/src/lib/components/VideoItem.svelte
Normal file
119
web/src/lib/components/VideoItem.svelte
Normal file
@@ -0,0 +1,119 @@
|
|||||||
|
<script lang="ts">
|
||||||
|
import { onMount } from 'svelte';
|
||||||
|
import { Button } from '$lib/components/ui/button';
|
||||||
|
import { Badge } from '$lib/components/ui/badge';
|
||||||
|
import { getVideo, resetVideo } from '$lib/api';
|
||||||
|
import { toast } from 'svelte-sonner';
|
||||||
|
import type { VideoResponse, VideoInfo, ResetVideoResponse } from '$lib/types';
|
||||||
|
|
||||||
|
export let video: VideoInfo;
|
||||||
|
export let collapseSignal: boolean = false;
|
||||||
|
|
||||||
|
let showDetail = false;
|
||||||
|
let detail: VideoResponse | null = null;
|
||||||
|
let loading = false;
|
||||||
|
|
||||||
|
// 定义视频和页面各状态的名称映射
|
||||||
|
const videoStatusLabels = ['视频封面', '视频信息', 'Up 主头像', 'Up 主信息', '分 P 下载'];
|
||||||
|
const pageStatusLabels = ['视频封面', '视频内容', '视频信息', '视频弹幕', '视频字幕'];
|
||||||
|
|
||||||
|
let prevCollapseSignal = collapseSignal;
|
||||||
|
$: if (collapseSignal !== prevCollapseSignal) {
|
||||||
|
showDetail = false;
|
||||||
|
prevCollapseSignal = collapseSignal;
|
||||||
|
}
|
||||||
|
|
||||||
|
function getVariant(status: number): 'warning' | 'success' | 'destructive' {
|
||||||
|
if (status === 0) return 'warning';
|
||||||
|
if (status === 7) return 'success';
|
||||||
|
return 'destructive';
|
||||||
|
}
|
||||||
|
|
||||||
|
async function toggleDetail() {
|
||||||
|
showDetail = !showDetail;
|
||||||
|
if (showDetail && (!detail || detail.video.id !== video.id)) {
|
||||||
|
loading = true;
|
||||||
|
detail = await getVideo(video.id);
|
||||||
|
loading = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 修改重置函数:调用 resetVideo 后重新获取视频详情
|
||||||
|
async function resetVideoItem() {
|
||||||
|
loading = true;
|
||||||
|
try {
|
||||||
|
const res: ResetVideoResponse = await resetVideo(video.id);
|
||||||
|
// 重置后重新加载视频详情,并更新视频信息
|
||||||
|
const newDetail = await getVideo(video.id);
|
||||||
|
detail = newDetail;
|
||||||
|
video = newDetail.video;
|
||||||
|
// 根据返回的 resetted 显示提示
|
||||||
|
if (res.resetted) {
|
||||||
|
toast.success('重置成功', {
|
||||||
|
description: `已重置视频与视频的 ${res.pages.length} 条 page.`
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
toast.info('重置无效', {
|
||||||
|
description: '所有任务均成功,无需重置'
|
||||||
|
});
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error(error);
|
||||||
|
toast.error('重置失败', { description: `错误信息:${error}` });
|
||||||
|
}
|
||||||
|
loading = false;
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<div class="my-2 rounded border p-4">
|
||||||
|
<div class="flex items-center justify-between">
|
||||||
|
<div>
|
||||||
|
<h3>{video.name}</h3>
|
||||||
|
<div class="flex space-x-1">
|
||||||
|
{#each video.download_status as status, i}
|
||||||
|
<Badge variant={getVariant(status)}>
|
||||||
|
{videoStatusLabels[i]}: {status === 0
|
||||||
|
? '未开始'
|
||||||
|
: status === 7
|
||||||
|
? '已完成'
|
||||||
|
: `失败 ${status} 次`}
|
||||||
|
</Badge>
|
||||||
|
{/each}
|
||||||
|
</div>
|
||||||
|
<p class="text-sm text-gray-500">{video.upper_name}</p>
|
||||||
|
</div>
|
||||||
|
<div class="flex space-x-2">
|
||||||
|
<Button onclick={toggleDetail}>
|
||||||
|
{showDetail ? '收起' : '展开'}
|
||||||
|
</Button>
|
||||||
|
<Button onclick={resetVideoItem}>重置</Button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{#if showDetail}
|
||||||
|
{#if loading}
|
||||||
|
<p>加载详情...</p>
|
||||||
|
{:else if detail}
|
||||||
|
<div class="mt-2">
|
||||||
|
<h4 class="font-semibold">视频详情</h4>
|
||||||
|
<div>
|
||||||
|
{#each detail.pages as page}
|
||||||
|
<div class="border-t py-1">
|
||||||
|
<p>ID: {page.id} - 名称: {page.name}</p>
|
||||||
|
<div class="flex space-x-1">
|
||||||
|
{#each page.download_status as status, i}
|
||||||
|
<Badge variant={getVariant(status)}>
|
||||||
|
{pageStatusLabels[i]}: {status === 0
|
||||||
|
? '未开始'
|
||||||
|
: status === 7
|
||||||
|
? '已完成'
|
||||||
|
: `失败 ${status} 次`}
|
||||||
|
</Badge>
|
||||||
|
{/each}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{/each}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{/if}
|
||||||
|
{/if}
|
||||||
|
</div>
|
||||||
18
web/src/lib/components/ui/badge/badge.svelte
Normal file
18
web/src/lib/components/ui/badge/badge.svelte
Normal file
@@ -0,0 +1,18 @@
|
|||||||
|
<script lang="ts">
|
||||||
|
import { type Variant, badgeVariants } from "./index.js";
|
||||||
|
import { cn } from "$lib/utils.js";
|
||||||
|
|
||||||
|
let className: string | undefined | null = undefined;
|
||||||
|
export let href: string | undefined = undefined;
|
||||||
|
export let variant: Variant = "default";
|
||||||
|
export { className as class };
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<svelte:element
|
||||||
|
this={href ? "a" : "span"}
|
||||||
|
{href}
|
||||||
|
class={cn(badgeVariants({ variant, className }))}
|
||||||
|
{...$$restProps}
|
||||||
|
>
|
||||||
|
<slot />
|
||||||
|
</svelte:element>
|
||||||
23
web/src/lib/components/ui/badge/index.ts
Normal file
23
web/src/lib/components/ui/badge/index.ts
Normal file
@@ -0,0 +1,23 @@
|
|||||||
|
import { type VariantProps, tv } from "tailwind-variants";
|
||||||
|
export { default as Badge } from "./badge.svelte";
|
||||||
|
|
||||||
|
export const badgeVariants = tv({
|
||||||
|
base: "focus:ring-ring inline-flex select-none items-center rounded-full border px-2.5 py-0.5 text-xs font-semibold transition-colors focus:outline-none focus:ring-2 focus:ring-offset-2",
|
||||||
|
variants: {
|
||||||
|
variant: {
|
||||||
|
default: "bg-primary text-primary-foreground hover:bg-primary/80 border-transparent",
|
||||||
|
secondary:
|
||||||
|
"bg-secondary text-secondary-foreground hover:bg-secondary/80 border-transparent",
|
||||||
|
destructive:
|
||||||
|
"bg-destructive text-destructive-foreground hover:bg-destructive/80 border-transparent",
|
||||||
|
success: 'border-transparent bg-green-500 text-success-foreground',
|
||||||
|
warning: 'border-transparent bg-yellow-500 text-warning-foreground',
|
||||||
|
outline: "text-foreground",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
defaultVariants: {
|
||||||
|
variant: "default",
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
export type Variant = VariantProps<typeof badgeVariants>["variant"];
|
||||||
74
web/src/lib/components/ui/button/button.svelte
Normal file
74
web/src/lib/components/ui/button/button.svelte
Normal file
@@ -0,0 +1,74 @@
|
|||||||
|
<script lang="ts" module>
|
||||||
|
import type { WithElementRef } from "bits-ui";
|
||||||
|
import type { HTMLAnchorAttributes, HTMLButtonAttributes } from "svelte/elements";
|
||||||
|
import { type VariantProps, tv } from "tailwind-variants";
|
||||||
|
|
||||||
|
export const buttonVariants = tv({
|
||||||
|
base: "ring-offset-background focus-visible:ring-ring inline-flex items-center justify-center gap-2 whitespace-nowrap rounded-md text-sm font-medium transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-offset-2 disabled:pointer-events-none disabled:opacity-50 [&_svg]:pointer-events-none [&_svg]:size-4 [&_svg]:shrink-0",
|
||||||
|
variants: {
|
||||||
|
variant: {
|
||||||
|
default: "bg-primary text-primary-foreground hover:bg-primary/90",
|
||||||
|
destructive: "bg-destructive text-destructive-foreground hover:bg-destructive/90",
|
||||||
|
outline:
|
||||||
|
"border-input bg-background hover:bg-accent hover:text-accent-foreground border",
|
||||||
|
secondary: "bg-secondary text-secondary-foreground hover:bg-secondary/80",
|
||||||
|
ghost: "hover:bg-accent hover:text-accent-foreground",
|
||||||
|
link: "text-primary underline-offset-4 hover:underline",
|
||||||
|
},
|
||||||
|
size: {
|
||||||
|
default: "h-10 px-4 py-2",
|
||||||
|
sm: "h-9 rounded-md px-3",
|
||||||
|
lg: "h-11 rounded-md px-8",
|
||||||
|
icon: "h-10 w-10",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
defaultVariants: {
|
||||||
|
variant: "default",
|
||||||
|
size: "default",
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
export type ButtonVariant = VariantProps<typeof buttonVariants>["variant"];
|
||||||
|
export type ButtonSize = VariantProps<typeof buttonVariants>["size"];
|
||||||
|
|
||||||
|
export type ButtonProps = WithElementRef<HTMLButtonAttributes> &
|
||||||
|
WithElementRef<HTMLAnchorAttributes> & {
|
||||||
|
variant?: ButtonVariant;
|
||||||
|
size?: ButtonSize;
|
||||||
|
};
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<script lang="ts">
|
||||||
|
import { cn } from "$lib/utils.js";
|
||||||
|
|
||||||
|
let {
|
||||||
|
class: className,
|
||||||
|
variant = "default",
|
||||||
|
size = "default",
|
||||||
|
ref = $bindable(null),
|
||||||
|
href = undefined,
|
||||||
|
type = "button",
|
||||||
|
children,
|
||||||
|
...restProps
|
||||||
|
}: ButtonProps = $props();
|
||||||
|
</script>
|
||||||
|
|
||||||
|
{#if href}
|
||||||
|
<a
|
||||||
|
bind:this={ref}
|
||||||
|
class={cn(buttonVariants({ variant, size }), className)}
|
||||||
|
{href}
|
||||||
|
{...restProps}
|
||||||
|
>
|
||||||
|
{@render children?.()}
|
||||||
|
</a>
|
||||||
|
{:else}
|
||||||
|
<button
|
||||||
|
bind:this={ref}
|
||||||
|
class={cn(buttonVariants({ variant, size }), className)}
|
||||||
|
{type}
|
||||||
|
{...restProps}
|
||||||
|
>
|
||||||
|
{@render children?.()}
|
||||||
|
</button>
|
||||||
|
{/if}
|
||||||
17
web/src/lib/components/ui/button/index.ts
Normal file
17
web/src/lib/components/ui/button/index.ts
Normal file
@@ -0,0 +1,17 @@
|
|||||||
|
import Root, {
|
||||||
|
type ButtonProps,
|
||||||
|
type ButtonSize,
|
||||||
|
type ButtonVariant,
|
||||||
|
buttonVariants,
|
||||||
|
} from "./button.svelte";
|
||||||
|
|
||||||
|
export {
|
||||||
|
Root,
|
||||||
|
type ButtonProps as Props,
|
||||||
|
//
|
||||||
|
Root as Button,
|
||||||
|
buttonVariants,
|
||||||
|
type ButtonProps,
|
||||||
|
type ButtonSize,
|
||||||
|
type ButtonVariant,
|
||||||
|
};
|
||||||
29
web/src/lib/components/ui/input/index.ts
Normal file
29
web/src/lib/components/ui/input/index.ts
Normal file
@@ -0,0 +1,29 @@
|
|||||||
|
import Root from "./input.svelte";
|
||||||
|
|
||||||
|
export type FormInputEvent<T extends Event = Event> = T & {
|
||||||
|
currentTarget: EventTarget & HTMLInputElement;
|
||||||
|
};
|
||||||
|
export type InputEvents = {
|
||||||
|
blur: FormInputEvent<FocusEvent>;
|
||||||
|
change: FormInputEvent<Event>;
|
||||||
|
click: FormInputEvent<MouseEvent>;
|
||||||
|
focus: FormInputEvent<FocusEvent>;
|
||||||
|
focusin: FormInputEvent<FocusEvent>;
|
||||||
|
focusout: FormInputEvent<FocusEvent>;
|
||||||
|
keydown: FormInputEvent<KeyboardEvent>;
|
||||||
|
keypress: FormInputEvent<KeyboardEvent>;
|
||||||
|
keyup: FormInputEvent<KeyboardEvent>;
|
||||||
|
mouseover: FormInputEvent<MouseEvent>;
|
||||||
|
mouseenter: FormInputEvent<MouseEvent>;
|
||||||
|
mouseleave: FormInputEvent<MouseEvent>;
|
||||||
|
mousemove: FormInputEvent<MouseEvent>;
|
||||||
|
paste: FormInputEvent<ClipboardEvent>;
|
||||||
|
input: FormInputEvent<InputEvent>;
|
||||||
|
wheel: FormInputEvent<WheelEvent>;
|
||||||
|
};
|
||||||
|
|
||||||
|
export {
|
||||||
|
Root,
|
||||||
|
//
|
||||||
|
Root as Input,
|
||||||
|
};
|
||||||
42
web/src/lib/components/ui/input/input.svelte
Normal file
42
web/src/lib/components/ui/input/input.svelte
Normal file
@@ -0,0 +1,42 @@
|
|||||||
|
<script lang="ts">
|
||||||
|
import type { HTMLInputAttributes } from "svelte/elements";
|
||||||
|
import type { InputEvents } from "./index.js";
|
||||||
|
import { cn } from "$lib/utils.js";
|
||||||
|
|
||||||
|
type $$Props = HTMLInputAttributes;
|
||||||
|
type $$Events = InputEvents;
|
||||||
|
|
||||||
|
let className: $$Props["class"] = undefined;
|
||||||
|
export let value: $$Props["value"] = undefined;
|
||||||
|
export { className as class };
|
||||||
|
|
||||||
|
// Workaround for https://github.com/sveltejs/svelte/issues/9305
|
||||||
|
// Fixed in Svelte 5, but not backported to 4.x.
|
||||||
|
export let readonly: $$Props["readonly"] = undefined;
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<input
|
||||||
|
class={cn(
|
||||||
|
"border-input bg-background ring-offset-background placeholder:text-muted-foreground focus-visible:ring-ring flex h-10 w-full rounded-md border px-3 py-2 text-sm file:border-0 file:bg-transparent file:text-sm file:font-medium focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-offset-2 disabled:cursor-not-allowed disabled:opacity-50",
|
||||||
|
className
|
||||||
|
)}
|
||||||
|
bind:value
|
||||||
|
{readonly}
|
||||||
|
on:blur
|
||||||
|
on:change
|
||||||
|
on:click
|
||||||
|
on:focus
|
||||||
|
on:focusin
|
||||||
|
on:focusout
|
||||||
|
on:keydown
|
||||||
|
on:keypress
|
||||||
|
on:keyup
|
||||||
|
on:mouseover
|
||||||
|
on:mouseenter
|
||||||
|
on:mouseleave
|
||||||
|
on:mousemove
|
||||||
|
on:paste
|
||||||
|
on:input
|
||||||
|
on:wheel|passive
|
||||||
|
{...$$restProps}
|
||||||
|
/>
|
||||||
1
web/src/lib/components/ui/sonner/index.ts
Normal file
1
web/src/lib/components/ui/sonner/index.ts
Normal file
@@ -0,0 +1 @@
|
|||||||
|
export { default as Toaster } from "./sonner.svelte";
|
||||||
20
web/src/lib/components/ui/sonner/sonner.svelte
Normal file
20
web/src/lib/components/ui/sonner/sonner.svelte
Normal file
@@ -0,0 +1,20 @@
|
|||||||
|
<script lang="ts">
|
||||||
|
import { Toaster as Sonner, type ToasterProps as SonnerProps } from "svelte-sonner";
|
||||||
|
import { mode } from "mode-watcher";
|
||||||
|
|
||||||
|
type $$Props = SonnerProps;
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<Sonner
|
||||||
|
theme={$mode}
|
||||||
|
class="toaster group"
|
||||||
|
toastOptions={{
|
||||||
|
classes: {
|
||||||
|
toast: "group toast group-[.toaster]:bg-background group-[.toaster]:text-foreground group-[.toaster]:border-border group-[.toaster]:shadow-lg",
|
||||||
|
description: "group-[.toast]:text-muted-foreground",
|
||||||
|
actionButton: "group-[.toast]:bg-primary group-[.toast]:text-primary-foreground",
|
||||||
|
cancelButton: "group-[.toast]:bg-muted group-[.toast]:text-muted-foreground",
|
||||||
|
},
|
||||||
|
}}
|
||||||
|
{...$$restProps}
|
||||||
|
/>
|
||||||
18
web/src/lib/components/ui/tabs/index.ts
Normal file
18
web/src/lib/components/ui/tabs/index.ts
Normal file
@@ -0,0 +1,18 @@
|
|||||||
|
import { Tabs as TabsPrimitive } from "bits-ui";
|
||||||
|
import Content from "./tabs-content.svelte";
|
||||||
|
import List from "./tabs-list.svelte";
|
||||||
|
import Trigger from "./tabs-trigger.svelte";
|
||||||
|
|
||||||
|
const Root = TabsPrimitive.Root;
|
||||||
|
|
||||||
|
export {
|
||||||
|
Root,
|
||||||
|
Content,
|
||||||
|
List,
|
||||||
|
Trigger,
|
||||||
|
//
|
||||||
|
Root as Tabs,
|
||||||
|
Content as TabsContent,
|
||||||
|
List as TabsList,
|
||||||
|
Trigger as TabsTrigger,
|
||||||
|
};
|
||||||
21
web/src/lib/components/ui/tabs/tabs-content.svelte
Normal file
21
web/src/lib/components/ui/tabs/tabs-content.svelte
Normal file
@@ -0,0 +1,21 @@
|
|||||||
|
<script lang="ts">
|
||||||
|
import { Tabs as TabsPrimitive } from "bits-ui";
|
||||||
|
import { cn } from "$lib/utils.js";
|
||||||
|
|
||||||
|
type $$Props = TabsPrimitive.ContentProps;
|
||||||
|
|
||||||
|
let className: $$Props["class"] = undefined;
|
||||||
|
export let value: $$Props["value"];
|
||||||
|
export { className as class };
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<TabsPrimitive.Content
|
||||||
|
class={cn(
|
||||||
|
"ring-offset-background focus-visible:ring-ring mt-2 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-offset-2",
|
||||||
|
className
|
||||||
|
)}
|
||||||
|
{value}
|
||||||
|
{...$$restProps}
|
||||||
|
>
|
||||||
|
<slot />
|
||||||
|
</TabsPrimitive.Content>
|
||||||
19
web/src/lib/components/ui/tabs/tabs-list.svelte
Normal file
19
web/src/lib/components/ui/tabs/tabs-list.svelte
Normal file
@@ -0,0 +1,19 @@
|
|||||||
|
<script lang="ts">
|
||||||
|
import { Tabs as TabsPrimitive } from "bits-ui";
|
||||||
|
import { cn } from "$lib/utils.js";
|
||||||
|
|
||||||
|
type $$Props = TabsPrimitive.ListProps;
|
||||||
|
|
||||||
|
let className: $$Props["class"] = undefined;
|
||||||
|
export { className as class };
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<TabsPrimitive.List
|
||||||
|
class={cn(
|
||||||
|
"bg-muted text-muted-foreground inline-flex h-10 items-center justify-center rounded-md p-1",
|
||||||
|
className
|
||||||
|
)}
|
||||||
|
{...$$restProps}
|
||||||
|
>
|
||||||
|
<slot />
|
||||||
|
</TabsPrimitive.List>
|
||||||
23
web/src/lib/components/ui/tabs/tabs-trigger.svelte
Normal file
23
web/src/lib/components/ui/tabs/tabs-trigger.svelte
Normal file
@@ -0,0 +1,23 @@
|
|||||||
|
<script lang="ts">
|
||||||
|
import { Tabs as TabsPrimitive } from "bits-ui";
|
||||||
|
import { cn } from "$lib/utils.js";
|
||||||
|
|
||||||
|
type $$Props = TabsPrimitive.TriggerProps;
|
||||||
|
type $$Events = TabsPrimitive.TriggerEvents;
|
||||||
|
|
||||||
|
let className: $$Props["class"] = undefined;
|
||||||
|
export let value: $$Props["value"];
|
||||||
|
export { className as class };
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<TabsPrimitive.Trigger
|
||||||
|
class={cn(
|
||||||
|
"ring-offset-background focus-visible:ring-ring data-[state=active]:bg-background data-[state=active]:text-foreground inline-flex items-center justify-center whitespace-nowrap rounded-sm px-3 py-1.5 text-sm font-medium transition-all focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-offset-2 disabled:pointer-events-none disabled:opacity-50 data-[state=active]:shadow-sm",
|
||||||
|
className
|
||||||
|
)}
|
||||||
|
{value}
|
||||||
|
{...$$restProps}
|
||||||
|
on:click
|
||||||
|
>
|
||||||
|
<slot />
|
||||||
|
</TabsPrimitive.Trigger>
|
||||||
43
web/src/lib/types.ts
Normal file
43
web/src/lib/types.ts
Normal file
@@ -0,0 +1,43 @@
|
|||||||
|
export interface VideoSource {
|
||||||
|
id: number;
|
||||||
|
name: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface VideoSourcesResponse {
|
||||||
|
collection: VideoSource[];
|
||||||
|
favorite: VideoSource[];
|
||||||
|
submission: VideoSource[];
|
||||||
|
watch_later: VideoSource[];
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface VideoInfo {
|
||||||
|
id: number;
|
||||||
|
name: string;
|
||||||
|
upper_name: string;
|
||||||
|
download_status: number[];
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface VideosResponse {
|
||||||
|
videos: VideoInfo[];
|
||||||
|
total_count: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface PageInfo {
|
||||||
|
id: number;
|
||||||
|
pid: number;
|
||||||
|
name: string;
|
||||||
|
download_status: number[];
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface VideoResponse {
|
||||||
|
video: VideoInfo;
|
||||||
|
pages: PageInfo[];
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface ResetVideoResponse {
|
||||||
|
resetted: boolean;
|
||||||
|
video: number;
|
||||||
|
pages: number[];
|
||||||
|
}
|
||||||
|
|
||||||
|
export type VideoCategory = 'collection' | 'favorite' | 'submission' | 'watch_later';
|
||||||
6
web/src/lib/utils.ts
Normal file
6
web/src/lib/utils.ts
Normal file
@@ -0,0 +1,6 @@
|
|||||||
|
import { type ClassValue, clsx } from "clsx";
|
||||||
|
import { twMerge } from "tailwind-merge";
|
||||||
|
|
||||||
|
export function cn(...inputs: ClassValue[]) {
|
||||||
|
return twMerge(clsx(inputs));
|
||||||
|
}
|
||||||
9
web/src/routes/+layout.svelte
Normal file
9
web/src/routes/+layout.svelte
Normal file
@@ -0,0 +1,9 @@
|
|||||||
|
<script lang="ts">
|
||||||
|
import '../app.css';
|
||||||
|
import { Toaster } from '$lib/components/ui/sonner';
|
||||||
|
let { children } = $props();
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<Toaster />
|
||||||
|
|
||||||
|
{@render children()}
|
||||||
2
web/src/routes/+layout.ts
Normal file
2
web/src/routes/+layout.ts
Normal file
@@ -0,0 +1,2 @@
|
|||||||
|
export const ssr = false;
|
||||||
|
export const prerender = true;
|
||||||
185
web/src/routes/+page.svelte
Normal file
185
web/src/routes/+page.svelte
Normal file
@@ -0,0 +1,185 @@
|
|||||||
|
<script lang="ts">
|
||||||
|
import { onMount } from 'svelte';
|
||||||
|
import { Input } from '$lib/components/ui/input';
|
||||||
|
import { Button } from '$lib/components/ui/button';
|
||||||
|
import VideoItem from '$lib/components/VideoItem.svelte';
|
||||||
|
import { listVideos, getVideoSources } from '$lib/api';
|
||||||
|
import type { VideoInfo, VideoSourcesResponse } from '$lib/types';
|
||||||
|
import Header from '$lib/components/Header.svelte';
|
||||||
|
|
||||||
|
// API Token 管理
|
||||||
|
let apiToken: string = localStorage.getItem('auth_token') || '';
|
||||||
|
function updateToken() {
|
||||||
|
localStorage.setItem('auth_token', apiToken);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 定义分类列表
|
||||||
|
const categories: (keyof VideoSourcesResponse)[] = [
|
||||||
|
'collection',
|
||||||
|
'favorite',
|
||||||
|
'submission',
|
||||||
|
'watch_later'
|
||||||
|
];
|
||||||
|
let activeCategory: keyof VideoSourcesResponse = 'collection';
|
||||||
|
let searchQuery = '';
|
||||||
|
let videos: VideoInfo[] = [];
|
||||||
|
let total = 0;
|
||||||
|
let currentPage = 0;
|
||||||
|
const pageSize = 10;
|
||||||
|
|
||||||
|
// 视频列表模型及全局选中模型(只全局允许选中一个)
|
||||||
|
let videoListModels: VideoSourcesResponse = {
|
||||||
|
collection: [],
|
||||||
|
favorite: [],
|
||||||
|
submission: [],
|
||||||
|
watch_later: []
|
||||||
|
};
|
||||||
|
// 移除 per 分类选中,新增全局 selectedModel
|
||||||
|
let selectedModel: { category: keyof VideoSourcesResponse; id: number } | null = null;
|
||||||
|
// 控制侧边栏各分类的折叠状态,true 为折叠
|
||||||
|
let collapse: { [key in keyof VideoSourcesResponse]?: boolean } = {
|
||||||
|
collection: false,
|
||||||
|
favorite: false,
|
||||||
|
submission: false,
|
||||||
|
watch_later: false
|
||||||
|
};
|
||||||
|
|
||||||
|
// 新增:定义 collapse 信号,用于让每个 VideoItem 收起详情
|
||||||
|
let videoCollapseSignal = false;
|
||||||
|
|
||||||
|
// 加载视频列表模型
|
||||||
|
async function fetchVideoListModels() {
|
||||||
|
videoListModels = await getVideoSources();
|
||||||
|
// 默认选中第一个有数据的模型
|
||||||
|
for (const key of categories) {
|
||||||
|
if (videoListModels[key]?.length) {
|
||||||
|
selectedModel = { category: key, id: videoListModels[key][0].id };
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// 默认使用 activeCategory 对应的选中 id 加载视频
|
||||||
|
fetchVideos();
|
||||||
|
}
|
||||||
|
|
||||||
|
// 加载视频列表,根据当前 activeCategory 对应的 selectedModel 发起请求
|
||||||
|
async function fetchVideos() {
|
||||||
|
const params: any = {};
|
||||||
|
if (selectedModel && selectedModel.category === activeCategory) {
|
||||||
|
params[`${activeCategory}`] = selectedModel.id.toString();
|
||||||
|
}
|
||||||
|
if (searchQuery) params.query = searchQuery;
|
||||||
|
params.page_size = pageSize;
|
||||||
|
params.page = currentPage;
|
||||||
|
const listRes = await listVideos(params);
|
||||||
|
videos = listRes.videos;
|
||||||
|
total = listRes.total_count;
|
||||||
|
}
|
||||||
|
|
||||||
|
onMount(fetchVideoListModels);
|
||||||
|
|
||||||
|
$: activeCategory, currentPage, searchQuery, fetchVideos();
|
||||||
|
|
||||||
|
function onSearch() {
|
||||||
|
currentPage = 0;
|
||||||
|
fetchVideos();
|
||||||
|
}
|
||||||
|
|
||||||
|
function prevPage() {
|
||||||
|
if (currentPage > 0) {
|
||||||
|
currentPage -= 1;
|
||||||
|
videoCollapseSignal = !videoCollapseSignal;
|
||||||
|
fetchVideos();
|
||||||
|
// 平滑滚动到顶部
|
||||||
|
window.scrollTo({ top: 0, behavior: 'smooth' });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function nextPage() {
|
||||||
|
if ((currentPage + 1) * pageSize < total) {
|
||||||
|
currentPage += 1;
|
||||||
|
videoCollapseSignal = !videoCollapseSignal;
|
||||||
|
fetchVideos();
|
||||||
|
// 平滑滚动到顶部
|
||||||
|
window.scrollTo({ top: 0, behavior: 'smooth' });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 点击侧边栏项时更新 activeCategory 和全局选中模型 id
|
||||||
|
function selectModel(category: keyof VideoSourcesResponse, id: number) {
|
||||||
|
// 如果当前已选中的模型和点击的一致,则取消筛选
|
||||||
|
if (selectedModel && selectedModel.category === category && selectedModel.id === id) {
|
||||||
|
selectedModel = null;
|
||||||
|
} else {
|
||||||
|
selectedModel = { category, id };
|
||||||
|
}
|
||||||
|
activeCategory = category;
|
||||||
|
currentPage = 0;
|
||||||
|
videoCollapseSignal = !videoCollapseSignal;
|
||||||
|
fetchVideos();
|
||||||
|
window.scrollTo({ top: 0, behavior: 'smooth' });
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<svelte:head>
|
||||||
|
<title>bili-sync 管理页</title>
|
||||||
|
</svelte:head>
|
||||||
|
|
||||||
|
<Header>
|
||||||
|
<div class="flex">
|
||||||
|
<!-- 左侧侧边栏 -->
|
||||||
|
<aside class="w-1/4 border-r p-4">
|
||||||
|
<h2 class="mb-4 text-xl font-bold">视频来源</h2>
|
||||||
|
{#each categories as cat}
|
||||||
|
<div class="mb-4">
|
||||||
|
<!-- 点击标题切换折叠状态 -->
|
||||||
|
<button
|
||||||
|
class="w-full text-left font-semibold"
|
||||||
|
on:click={() => (collapse[cat] = !collapse[cat])}
|
||||||
|
>
|
||||||
|
{cat}
|
||||||
|
{collapse[cat] ? '▶' : '▼'}
|
||||||
|
</button>
|
||||||
|
{#if !collapse[cat]}
|
||||||
|
{#if videoListModels[cat]?.length}
|
||||||
|
<ul class="ml-4">
|
||||||
|
{#each videoListModels[cat] as model}
|
||||||
|
<li class="mb-1">
|
||||||
|
<button
|
||||||
|
class="w-full rounded px-2 py-1 text-left hover:bg-gray-100 {selectedModel &&
|
||||||
|
selectedModel.category === cat &&
|
||||||
|
selectedModel.id === model.id
|
||||||
|
? 'bg-gray-200'
|
||||||
|
: ''}"
|
||||||
|
on:click={() => selectModel(cat, model.id)}
|
||||||
|
>
|
||||||
|
{model.name}
|
||||||
|
</button>
|
||||||
|
</li>
|
||||||
|
{/each}
|
||||||
|
</ul>
|
||||||
|
{:else}
|
||||||
|
<p class="ml-4 text-gray-500">无数据</p>
|
||||||
|
{/if}
|
||||||
|
{/if}
|
||||||
|
</div>
|
||||||
|
{/each}
|
||||||
|
</aside>
|
||||||
|
|
||||||
|
<!-- 主内容区域 -->
|
||||||
|
<main class="flex-1 p-4">
|
||||||
|
<div class="mb-4">
|
||||||
|
<Input placeholder="搜索视频..." bind:value={searchQuery} on:change={onSearch} />
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
{#each videos as video}
|
||||||
|
<VideoItem {video} collapseSignal={videoCollapseSignal} />
|
||||||
|
{/each}
|
||||||
|
</div>
|
||||||
|
<div class="mt-4 flex items-center justify-between">
|
||||||
|
<Button onclick={prevPage} disabled={currentPage === 0}>上一页</Button>
|
||||||
|
<span>第 {currentPage + 1} 页,共 {Math.ceil(total / pageSize)} 页</span>
|
||||||
|
<Button onclick={nextPage} disabled={(currentPage + 1) * pageSize >= total}>下一页</Button>
|
||||||
|
</div>
|
||||||
|
</main>
|
||||||
|
</div>
|
||||||
|
</Header>
|
||||||
BIN
web/static/favicon.png
Normal file
BIN
web/static/favicon.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 4.3 KiB |
29
web/svelte.config.js
Normal file
29
web/svelte.config.js
Normal file
@@ -0,0 +1,29 @@
|
|||||||
|
import adapter from '@sveltejs/adapter-static';
|
||||||
|
import { vitePreprocess } from '@sveltejs/vite-plugin-svelte';
|
||||||
|
|
||||||
|
/** @type {import('@sveltejs/kit').Config} */
|
||||||
|
const config = {
|
||||||
|
// Consult https://svelte.dev/docs/kit/integrations
|
||||||
|
// for more information about preprocessors
|
||||||
|
preprocess: vitePreprocess(),
|
||||||
|
|
||||||
|
kit: {
|
||||||
|
alias: {
|
||||||
|
"@/*": "./path/to/lib/*",
|
||||||
|
},
|
||||||
|
// adapter-auto only supports some environments, see https://svelte.dev/docs/kit/adapter-auto for a list.
|
||||||
|
// If your environment is not supported, or you settled on a specific environment, switch out the adapter.
|
||||||
|
// See https://svelte.dev/docs/kit/adapters for more information about adapters.
|
||||||
|
adapter: adapter({
|
||||||
|
// default options are shown. On some platforms
|
||||||
|
// these options are set automatically — see below
|
||||||
|
pages: 'build',
|
||||||
|
assets: 'build',
|
||||||
|
fallback: undefined,
|
||||||
|
precompress: false,
|
||||||
|
strict: true
|
||||||
|
})
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
export default config;
|
||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user