mirror of
https://github.com/amtoaer/bili-sync.git
synced 2026-07-12 07:51:53 +08:00
Compare commits
5 Commits
feat_danma
...
copilot/je
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
b60577cbc2 | ||
|
|
3ac56f6c4e | ||
|
|
116ae79578 | ||
|
|
6d8bf14880 | ||
|
|
791dd57f23 |
1511
Cargo.lock
generated
1511
Cargo.lock
generated
File diff suppressed because it is too large
Load Diff
68
Cargo.toml
68
Cargo.toml
@@ -4,7 +4,7 @@ default-members = ["crates/bili_sync"]
|
||||
resolver = "2"
|
||||
|
||||
[workspace.package]
|
||||
version = "2.11.0"
|
||||
version = "2.11.1"
|
||||
authors = ["amtoaer <amtoaer@gmail.com>"]
|
||||
license = "MIT"
|
||||
description = "由 Rust & Tokio 驱动的哔哩哔哩同步工具"
|
||||
@@ -15,40 +15,40 @@ publish = false
|
||||
bili_sync_entity = { path = "crates/bili_sync_entity" }
|
||||
bili_sync_migration = { path = "crates/bili_sync_migration" }
|
||||
|
||||
anyhow = { version = "1.0.100", features = ["backtrace"] }
|
||||
arc-swap = { version = "1.8.0", features = ["serde"] }
|
||||
anyhow = { version = "1.0.102", features = ["backtrace"] }
|
||||
arc-swap = { version = "1.9.1", features = ["serde"] }
|
||||
async-stream = "0.3.6"
|
||||
async-tempfile = { version = "0.7.0", features = ["uuid"] }
|
||||
async-tempfile = { version = "0.8.0", features = ["uuid"] }
|
||||
async-trait = "0.1.89"
|
||||
axum = { version = "0.8.8", features = ["macros", "ws"] }
|
||||
axum = { version = "0.8.9", features = ["macros", "ws"] }
|
||||
base64 = "0.22.1"
|
||||
built = { version = "0.7.7", features = ["git2", "chrono"] }
|
||||
chrono = { version = "0.4.42", features = ["serde"] }
|
||||
clap = { version = "4.5.54", features = ["env", "string"] }
|
||||
built = { version = "0.8.1", features = ["git2", "chrono"] }
|
||||
chrono = { version = "0.4.45", features = ["serde"] }
|
||||
clap = { version = "4.6.1", features = ["env", "string"] }
|
||||
cookie = "0.18.1"
|
||||
croner = "3.0.1"
|
||||
dashmap = "6.1.0"
|
||||
dashmap = "6.2.1"
|
||||
derivative = "2.2.0"
|
||||
dirs = "6.0.0"
|
||||
dunce = "1.0.5"
|
||||
either = "1.15.0"
|
||||
either = "1.16.0"
|
||||
enum_dispatch = "0.3.13"
|
||||
float-ord = "0.3.2"
|
||||
futures = "0.3.31"
|
||||
git2 = { version = "0.20.3", features = [], default-features = false }
|
||||
handlebars = "6.4.0"
|
||||
futures = "0.3.32"
|
||||
git2 = { version = "0.21.0", features = [], default-features = false }
|
||||
handlebars = "6.4.1"
|
||||
hex = "0.4.3"
|
||||
itertools = "0.14.0"
|
||||
leaky-bucket = "1.1.2"
|
||||
md5 = "0.8.0"
|
||||
memchr = "2.7.6"
|
||||
once_cell = "1.21.3"
|
||||
memchr = "2.8.1"
|
||||
once_cell = "1.21.4"
|
||||
parking_lot = "0.12.5"
|
||||
prost = "0.14.1"
|
||||
quick-xml = { version = "0.38.4", features = ["async-tokio"] }
|
||||
rand = "0.9.2"
|
||||
regex = "1.12.2"
|
||||
reqwest = { version = "0.13.1", features = [
|
||||
prost = "0.14.4"
|
||||
quick-xml = { version = "0.40.1", features = ["async-tokio"] }
|
||||
rand = "0.10.1"
|
||||
regex = "1.12.3"
|
||||
reqwest = { version = "0.13.4", features = [
|
||||
"query",
|
||||
"form",
|
||||
"charset",
|
||||
@@ -59,32 +59,32 @@ reqwest = { version = "0.13.1", features = [
|
||||
"rustls-no-provider",
|
||||
"stream",
|
||||
], default-features = false }
|
||||
rsa = { version = "0.10.0-rc.9", features = ["sha2"] }
|
||||
rsa = { version = "0.10.0-rc.18", features = ["sha2"] }
|
||||
rust-embed-for-web = { git = "https://github.com/amtoaer/rust-embed-for-web", tag = "v1.0.0" }
|
||||
rustls = { version = "0.23.36", default-features = false, features = ["ring"] }
|
||||
sea-orm = { version = "1.1.19", features = [
|
||||
rustls = { version = "0.23.40", default-features = false, features = ["ring"] }
|
||||
sea-orm = { version = "1.1.20", features = [
|
||||
"macros",
|
||||
"runtime-tokio",
|
||||
"sqlx-sqlite",
|
||||
"sqlite-use-returning-for-3_35",
|
||||
] }
|
||||
sea-orm-migration = { version = "1.1.19", features = [] }
|
||||
sea-orm-migration = { version = "1.1.20", features = [] }
|
||||
serde = { version = "1.0.228", features = ["derive"] }
|
||||
serde_json = "1.0.148"
|
||||
serde_json = "1.0.150"
|
||||
serde_urlencoded = "0.7.1"
|
||||
strum = { version = "0.27.2", features = ["derive"] }
|
||||
sysinfo = "0.37.2"
|
||||
thiserror = "2.0.17"
|
||||
tokio = { version = "1.49.0", features = ["full"] }
|
||||
strum = { version = "0.28.0", features = ["derive"] }
|
||||
sysinfo = "0.39.3"
|
||||
thiserror = "2.0.18"
|
||||
tokio = { version = "1.52.3", features = ["full"] }
|
||||
tokio-cron-scheduler = "0.15.1"
|
||||
tokio-stream = { version = "0.1.18", features = ["sync"] }
|
||||
tokio-util = { version = "0.7.18", features = ["io", "rt"] }
|
||||
toml = "0.9.10"
|
||||
tower = "0.5.2"
|
||||
toml = "1.1.2"
|
||||
tower = "0.5.3"
|
||||
tracing = "0.1.44"
|
||||
tracing-subscriber = { version = "0.3.22", features = ["chrono", "json"] }
|
||||
ua_generator = { version = "0.5.42", default-features = false }
|
||||
uuid = { version = "1.19.0", features = ["v4"] }
|
||||
tracing-subscriber = { version = "0.3.23", features = ["chrono", "json"] }
|
||||
ua_generator = { version = "0.5.65", default-features = false }
|
||||
uuid = { version = "1.23.2", features = ["v4"] }
|
||||
validator = { version = "0.20.0", features = ["derive"] }
|
||||
|
||||
[workspace.metadata.release]
|
||||
|
||||
@@ -205,7 +205,7 @@ JNrRuoEUXpabUzGB8QIDAQAB
|
||||
let ts = chrono::Local::now().timestamp_millis() - 20000;
|
||||
let data = format!("refresh_{}", ts).into_bytes();
|
||||
let encrypted = key
|
||||
.encrypt(&mut rand::rng(), Oaep::new::<Sha256>(), &data)
|
||||
.encrypt(&mut rand::rng(), Oaep::<Sha256>::new(), &data)
|
||||
.expect("fail to encrypt");
|
||||
hex::encode(encrypted)
|
||||
}
|
||||
|
||||
@@ -2,10 +2,14 @@ use thiserror::Error;
|
||||
|
||||
#[derive(Error, Debug, Clone)]
|
||||
pub enum BiliError {
|
||||
#[error("response missing 'code' or 'message' field, full response: {0}")]
|
||||
#[error("response missing 'code' field, full response: {0}")]
|
||||
InvalidResponse(String),
|
||||
#[error("API returned error code {0}, full response: {1}")]
|
||||
ErrorResponse(i64, String),
|
||||
#[error("API returned error code {code}, full response: {response}")]
|
||||
ErrorResponse {
|
||||
code: i64,
|
||||
message: Option<String>,
|
||||
response: String,
|
||||
},
|
||||
#[error("risk control triggered by server, full response: {0}")]
|
||||
RiskControlOccurred(String),
|
||||
#[error("invalid HTTP response code {0}, reason: {1}")]
|
||||
@@ -21,4 +25,15 @@ impl BiliError {
|
||||
BiliError::RiskControlOccurred(_) | BiliError::VideoStreamsEmpty | BiliError::InvalidStatusCode(_, _)
|
||||
)
|
||||
}
|
||||
|
||||
pub fn is_common_error(&self) -> bool {
|
||||
if let BiliError::ErrorResponse { code, message, .. } = self {
|
||||
for pair in [(-503, "服务暂不可用"), (-504, "服务调用超时")] {
|
||||
if *code == pair.0 && message.as_ref().is_some_and(|m| m == pair.1) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
}
|
||||
false
|
||||
}
|
||||
}
|
||||
|
||||
@@ -60,10 +60,18 @@ impl Validate for serde_json::Value {
|
||||
let code = self["code"]
|
||||
.as_i64()
|
||||
.with_context(|| BiliError::InvalidResponse(self.to_string()))?;
|
||||
let message = self["message"].as_str().map(ToOwned::to_owned);
|
||||
if code == -352 || !self["data"]["v_voucher"].is_null() {
|
||||
bail!(BiliError::RiskControlOccurred(self.to_string()));
|
||||
}
|
||||
ensure!(code == 0, BiliError::ErrorResponse(code, self.to_string()));
|
||||
ensure!(
|
||||
code == 0,
|
||||
BiliError::ErrorResponse {
|
||||
code,
|
||||
message,
|
||||
response: self.to_string(),
|
||||
}
|
||||
);
|
||||
Ok(self)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -38,6 +38,8 @@ pub struct Config {
|
||||
pub page_name: String,
|
||||
#[serde(default)]
|
||||
pub notifiers: Option<Arc<Vec<Notifier>>>,
|
||||
#[serde(default)]
|
||||
pub ignore_common_errors: bool,
|
||||
#[serde(default = "default_favorite_path")]
|
||||
pub favorite_default_path: String,
|
||||
#[serde(default = "default_collection_path")]
|
||||
@@ -124,6 +126,7 @@ impl Default for Config {
|
||||
video_name: "{{title}}".to_owned(),
|
||||
page_name: "{{bvid}}".to_owned(),
|
||||
notifiers: None,
|
||||
ignore_common_errors: false,
|
||||
favorite_default_path: default_favorite_path(),
|
||||
collection_default_path: default_collection_path(),
|
||||
submission_default_path: default_submission_path(),
|
||||
|
||||
@@ -8,7 +8,7 @@ use async_tempfile::TempFile;
|
||||
use futures::TryStreamExt;
|
||||
use reqwest::{Method, StatusCode, header};
|
||||
use tokio::fs::{self};
|
||||
use tokio::io::{AsyncSeekExt, AsyncWriteExt};
|
||||
use tokio::io::{AsyncSeekExt, AsyncWriteExt, BufWriter};
|
||||
use tokio::process::Command;
|
||||
use tokio::task::JoinSet;
|
||||
use tokio_util::io::StreamReader;
|
||||
@@ -155,8 +155,9 @@ impl Downloader {
|
||||
.error_for_status_ext()?;
|
||||
let expected = resp.header_content_length();
|
||||
let mut stream_reader = StreamReader::new(resp.bytes_stream().map_err(std::io::Error::other));
|
||||
let received = tokio::io::copy(&mut stream_reader, file).await?;
|
||||
file.flush().await?;
|
||||
let mut file_writer = BufWriter::with_capacity(2 * 1024 * 1024, file);
|
||||
let received = tokio::io::copy(&mut stream_reader, &mut file_writer).await?;
|
||||
file_writer.flush().await?;
|
||||
if let Some(expected) = expected {
|
||||
ensure!(
|
||||
received == expected,
|
||||
@@ -244,8 +245,9 @@ impl Downloader {
|
||||
);
|
||||
}
|
||||
let mut stream_reader = StreamReader::new(resp.bytes_stream().map_err(std::io::Error::other));
|
||||
let received = tokio::io::copy(&mut stream_reader, &mut file_clone).await?;
|
||||
file_clone.flush().await?;
|
||||
let mut file_writer = BufWriter::with_capacity(2 * 1024 * 1024, file_clone);
|
||||
let received = tokio::io::copy(&mut stream_reader, &mut file_writer).await?;
|
||||
file_writer.flush().await?;
|
||||
ensure!(
|
||||
received == end - start + 1,
|
||||
"downloaded bytes mismatch: expected {}, got {}",
|
||||
|
||||
@@ -140,6 +140,7 @@ impl DownloadTaskManager {
|
||||
&initial_config,
|
||||
&cx.bili_client,
|
||||
format!("初始化视频下载任务失败:{:#}", err),
|
||||
&err,
|
||||
);
|
||||
None
|
||||
}
|
||||
@@ -193,6 +194,7 @@ impl DownloadTaskManager {
|
||||
&initial_config,
|
||||
&cx.bili_client,
|
||||
format!("重载视频下载任务失败:{:#}", err),
|
||||
&err,
|
||||
);
|
||||
None
|
||||
}
|
||||
@@ -234,6 +236,7 @@ impl DownloadTaskManager {
|
||||
&config,
|
||||
&cx.bili_client,
|
||||
format!("本轮凭据检查与刷新任务执行遇到错误:{:#}", e),
|
||||
&e,
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -285,6 +288,7 @@ impl DownloadTaskManager {
|
||||
&config,
|
||||
&cx.bili_client,
|
||||
format!("本轮视频下载任务执行遇到错误:{:#}", e),
|
||||
&e,
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -360,6 +364,7 @@ async fn download_video(
|
||||
config,
|
||||
&bili_client,
|
||||
format!("处理 {} 时遇到错误:{:#},跳过该视频源", display_name, e),
|
||||
&e,
|
||||
);
|
||||
if let Ok(e) = e.downcast::<BiliError>()
|
||||
&& e.is_risk_control_related()
|
||||
|
||||
@@ -1,4 +1,6 @@
|
||||
use crate::bilibili::BiliClient;
|
||||
use anyhow::Error;
|
||||
|
||||
use crate::bilibili::{BiliClient, BiliError};
|
||||
use crate::config::Config;
|
||||
use crate::notifier::{Message, NotifierAllExt};
|
||||
|
||||
@@ -12,8 +14,17 @@ pub fn notify(config: &Config, bili_client: &BiliClient, msg: impl Into<Message<
|
||||
}
|
||||
}
|
||||
|
||||
pub fn error_and_notify(config: &Config, bili_client: &BiliClient, msg: String) {
|
||||
pub fn error_and_notify(config: &Config, bili_client: &BiliClient, msg: String, error: &Error) {
|
||||
error!("{msg}");
|
||||
if config.ignore_common_errors
|
||||
&& error.chain().any(|cause| {
|
||||
cause
|
||||
.downcast_ref::<BiliError>()
|
||||
.is_some_and(BiliError::is_common_error)
|
||||
})
|
||||
{
|
||||
return;
|
||||
}
|
||||
if let Some(notifiers) = &config.notifiers
|
||||
&& !notifiers.is_empty()
|
||||
{
|
||||
|
||||
@@ -140,7 +140,7 @@ pub async fn fetch_video_details(
|
||||
"获取视频 {} - {} 的详细信息失败,错误为:{:#}",
|
||||
&video_model.bvid, &video_model.name, e
|
||||
);
|
||||
if let Some(BiliError::ErrorResponse(-404, _)) = e.downcast_ref::<BiliError>() {
|
||||
if let Some(BiliError::ErrorResponse { code: -404, .. }) = e.downcast_ref::<BiliError>() {
|
||||
let mut video_active_model: bili_sync_entity::video::ActiveModel = video_model.into();
|
||||
video_active_model.valid = Set(false);
|
||||
video_active_model.save(connection).await?;
|
||||
|
||||
@@ -21,7 +21,7 @@ export default defineConfig({
|
||||
nav: [
|
||||
{ text: "主页", link: "/" },
|
||||
{
|
||||
text: "v2.11.0",
|
||||
text: "v2.11.1",
|
||||
items: [
|
||||
{
|
||||
text: "程序更新",
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
# bili-sync 是什么?
|
||||
|
||||
> [!TIP]
|
||||
> 当前最新程序版本为 v2.11.0,文档将始终与最新程序版本保持一致。
|
||||
> 当前最新程序版本为 v2.11.1,文档将始终与最新程序版本保持一致。
|
||||
|
||||
bili-sync 是一款专为 NAS 用户编写的哔哩哔哩同步工具。
|
||||
|
||||
|
||||
@@ -1,3 +1,3 @@
|
||||
[toolchain]
|
||||
channel = "1.94.0"
|
||||
channel = "1.96.0"
|
||||
components = ["clippy"]
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "bili-sync-web",
|
||||
"version": "2.11.0",
|
||||
"version": "2.11.1",
|
||||
"devDependencies": {
|
||||
"@eslint/compat": "^1.4.1",
|
||||
"@eslint/js": "^9.39.2",
|
||||
|
||||
@@ -330,6 +330,7 @@ export interface Config {
|
||||
video_name: string;
|
||||
page_name: string;
|
||||
notifiers: Notifier[] | null;
|
||||
ignore_common_errors: boolean;
|
||||
favorite_default_path: string;
|
||||
collection_default_path: string;
|
||||
submission_default_path: string;
|
||||
|
||||
@@ -792,6 +792,17 @@
|
||||
<!-- 通知设置 -->
|
||||
<Tabs.Content value="notifiers" class="mt-6 space-y-6">
|
||||
<div class="space-y-4">
|
||||
<div class="flex items-center justify-between rounded-lg border p-4">
|
||||
<div class="space-y-1">
|
||||
<Label for="ignore-common-errors">忽略常见错误</Label>
|
||||
<p class="text-muted-foreground text-sm">
|
||||
b 站接口频繁出现 -503
|
||||
服务暂不可用,在通知器内通知容易刷屏,开启该选项以忽略此错误通知
|
||||
</p>
|
||||
</div>
|
||||
<Switch id="ignore-common-errors" bind:checked={formData.ignore_common_errors} />
|
||||
</div>
|
||||
|
||||
<div class="flex items-center justify-between">
|
||||
<div>
|
||||
<h3 class="text-lg font-semibold">通知器管理</h3>
|
||||
|
||||
Reference in New Issue
Block a user