mirror of
https://github.com/amtoaer/bili-sync.git
synced 2026-05-08 01:02:49 +08:00
Compare commits
11 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
0958893574 | ||
|
|
97aec74242 | ||
|
|
aa9d8c9e66 | ||
|
|
1ad82e513e | ||
|
|
be4f62d4e1 | ||
|
|
2bdfdd8b8f | ||
|
|
2366c36462 | ||
|
|
badaeed104 | ||
|
|
ee7ee4b883 | ||
|
|
2429f3b742 | ||
|
|
8d8218d515 |
2
.github/workflows/check.yaml
vendored
2
.github/workflows/check.yaml
vendored
@@ -26,7 +26,7 @@ jobs:
|
||||
- name: Checkout repo
|
||||
uses: actions/checkout@v4
|
||||
|
||||
- run: rustup toolchain install nightly --profile minimal
|
||||
- run: rustup toolchain install nightly && rustup default nightly && rustup component add rustfmt clippy
|
||||
|
||||
- name: Cache dependencies
|
||||
uses: swatinem/rust-cache@v2
|
||||
|
||||
49
Cargo.lock
generated
49
Cargo.lock
generated
@@ -393,6 +393,12 @@ version = "0.21.7"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "9d297deb1925b89f2ccc13d7635fa0714f12c87adce1c75356b39ca9b7178567"
|
||||
|
||||
[[package]]
|
||||
name = "base64"
|
||||
version = "0.22.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "9475866fec1451be56a3c2400fd081ff546538961565ccb5b7142cbd22bc7a51"
|
||||
|
||||
[[package]]
|
||||
name = "base64ct"
|
||||
version = "1.6.0"
|
||||
@@ -1513,15 +1519,6 @@ version = "2.9.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "8f518f335dce6725a761382244631d86cf0ccb2863413590b31338feb467f9c3"
|
||||
|
||||
[[package]]
|
||||
name = "itertools"
|
||||
version = "0.10.5"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "b0fd2260e829bddf4cb6ea802289de2f86d6a7a690192fbe91b3f46e0f2c8473"
|
||||
dependencies = [
|
||||
"either",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "itertools"
|
||||
version = "0.12.1"
|
||||
@@ -2098,7 +2095,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "19de2de2a00075bf566bee3bd4db014b11587e84184d3f7a791bc17f1a8e9e48"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"itertools 0.10.5",
|
||||
"itertools",
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn 2.0.58",
|
||||
@@ -2270,12 +2267,12 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "reqwest"
|
||||
version = "0.12.2"
|
||||
version = "0.12.4"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "2d66674f2b6fb864665eea7a3c1ac4e3dfacd2fda83cf6f935a612e01b0e3338"
|
||||
checksum = "566cafdd92868e0939d3fb961bd0dc25fcfaaed179291093b3d43e6b3150ea10"
|
||||
dependencies = [
|
||||
"async-compression",
|
||||
"base64",
|
||||
"base64 0.22.0",
|
||||
"bytes",
|
||||
"cookie 0.17.0",
|
||||
"cookie_store",
|
||||
@@ -2297,7 +2294,7 @@ dependencies = [
|
||||
"percent-encoding",
|
||||
"pin-project-lite",
|
||||
"rustls 0.22.3",
|
||||
"rustls-pemfile",
|
||||
"rustls-pemfile 2.1.2",
|
||||
"rustls-pki-types",
|
||||
"serde",
|
||||
"serde_json",
|
||||
@@ -2461,7 +2458,17 @@ version = "1.0.4"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "1c74cae0a4cf6ccbbf5f359f08efdf8ee7e1dc532573bf0db71968cb56b1448c"
|
||||
dependencies = [
|
||||
"base64",
|
||||
"base64 0.21.7",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "rustls-pemfile"
|
||||
version = "2.1.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "29993a25686778eb88d4189742cd713c9bce943bc54251a33509dc63cbacf73d"
|
||||
dependencies = [
|
||||
"base64 0.22.0",
|
||||
"rustls-pki-types",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -2858,7 +2865,7 @@ version = "0.2.3"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "ce81b7bd7c4493975347ef60d8c7e8b742d4694f4c49f93e0a12ea263938176c"
|
||||
dependencies = [
|
||||
"itertools 0.12.1",
|
||||
"itertools",
|
||||
"nom",
|
||||
"unicode_categories",
|
||||
]
|
||||
@@ -2907,7 +2914,7 @@ dependencies = [
|
||||
"percent-encoding",
|
||||
"rust_decimal",
|
||||
"rustls 0.21.10",
|
||||
"rustls-pemfile",
|
||||
"rustls-pemfile 1.0.4",
|
||||
"serde",
|
||||
"serde_json",
|
||||
"sha2",
|
||||
@@ -2969,7 +2976,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "1ed31390216d20e538e447a7a9b959e06ed9fc51c37b514b46eb758016ecd418"
|
||||
dependencies = [
|
||||
"atoi",
|
||||
"base64",
|
||||
"base64 0.21.7",
|
||||
"bigdecimal",
|
||||
"bitflags 2.5.0",
|
||||
"byteorder",
|
||||
@@ -3016,7 +3023,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "7c824eb80b894f926f89a0b9da0c7f435d27cdd35b8c655b114e58223918577e"
|
||||
dependencies = [
|
||||
"atoi",
|
||||
"base64",
|
||||
"base64 0.21.7",
|
||||
"bigdecimal",
|
||||
"bitflags 2.5.0",
|
||||
"byteorder",
|
||||
@@ -3887,9 +3894,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "winreg"
|
||||
version = "0.50.0"
|
||||
version = "0.52.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "524e57b2c537c0f9b1e69f1965311ec12182b4122e45035b1508cd24d2adadb1"
|
||||
checksum = "a277a57398d4bfa075df44f501a17cfdf8542d224f0d36095a2adc7aee4ef0a5"
|
||||
dependencies = [
|
||||
"cfg-if",
|
||||
"windows-sys 0.48.0",
|
||||
|
||||
@@ -25,7 +25,7 @@ prost = "0.12.4"
|
||||
quick-xml = { version = "0.31.0", features = ["async-tokio"] }
|
||||
rand = "0.8.5"
|
||||
regex = "1.10.3"
|
||||
reqwest = { version = "0.12.0", features = [
|
||||
reqwest = { version = "0.12.4", features = [
|
||||
"json",
|
||||
"stream",
|
||||
"cookies",
|
||||
|
||||
57
README.md
57
README.md
@@ -3,18 +3,18 @@
|
||||
## 简介
|
||||
|
||||
> [!NOTE]
|
||||
> 新版本已使用 Rust 重构,该文档是对新版本的说明。对于 v1.x 的 Python 版本,请前往 [v1.x](https://github.com/amtoaer/bili-sync/tree/v1.x) 分支查看。
|
||||
>
|
||||
> 目前新版本尚未进行 docker 打包,docker 版本相关问题请同样参考 [v1.x](https://github.com/amtoaer/bili-sync/tree/v1.x) 分支的 README 与 [v1.x 的 release 文档](https://github.com/amtoaer/bili-sync/releases)。
|
||||
> 此为 v2.x 版本文档,v1.x 版本文档请前往[此处](https://github.com/amtoaer/bili-sync/tree/v1.x)查看。
|
||||
|
||||
> [!CAUTION]
|
||||
> 当前新版本尚不稳定,可能会有未告知的不兼容更改,请优先使用 v1.x 的 Python 版本。
|
||||
|
||||
为 NAS 用户编写的 BILIBILI 收藏夹同步工具,可使用 EMBY 等媒体库工具浏览。
|
||||
|
||||
支持展示视频封面、名称、加入日期、标签、分页等。
|
||||
|
||||
|
||||
## 效果演示
|
||||
|
||||
**注:因为可能同时存在单页视频和多页视频,媒体库类型请选择“混合内容”。**
|
||||
|
||||
### 概览
|
||||

|
||||
### 详情
|
||||
@@ -26,13 +26,18 @@
|
||||
|
||||
## 配置文件说明
|
||||
|
||||
程序默认会将配置文件存储于 `~/.config/bili-sync/config.toml`,数据库文件存储于 `~/.config/bili-sync/data.sqlite`,如果发现不存在则新建并写入初始配置。
|
||||
> [!NOTE]
|
||||
> 在 Docker 环境中,`~` 会被展开为 `/app`。
|
||||
|
||||
配置文件加载时会进行简单校验,对于默认的空配置,校验将会报错,程序将会终止运行。
|
||||
程序默认会将配置文件存储于 `~/.config/bili-sync/config.toml`,数据库文件存储于 `~/.config/bili-sync/data.sqlite`,如果发现不存在会新建并写入默认配置。
|
||||
|
||||
配置文件加载时会进行简单校验,默认配置无法通过校验,程序会报错终止运行。
|
||||
|
||||
可以下载程序后直接运行程序,看到报错后参考报错信息对默认配置进行修改,修改正确后即可正常运行。
|
||||
|
||||
对于配置文件中的 `credential`,请参考[凭据获取流程](https://nemo2011.github.io/bilibili-api/#/get-credential)。
|
||||
|
||||
配置文件中的 `video_name` 和 `page_name` 支持使用模板,在执行时会被动态替换为对应的内容。
|
||||
配置文件中的 `video_name` 和 `page_name` 支持使用模板,模板的替换语法请参考示例。模板中的内容在执行时会被动态替换为对应的内容。
|
||||
|
||||
video_name 支持设置 bvid(视频编号)、title(视频标题)、upper_name(up 主名称)、upper_mid(up 主 id)。
|
||||
|
||||
@@ -71,11 +76,17 @@ page_name 除支持 video 的全部参数外,还支持 ptitle(分 P 标题
|
||||
## 配置文件示例
|
||||
|
||||
```toml
|
||||
# 视频所处文件夹的名称
|
||||
video_name = "{{title}}"
|
||||
# 视频分页文件的命名
|
||||
page_name = "{{bvid}}"
|
||||
# 扫描运行的间隔(单位:秒)
|
||||
interval = 1200
|
||||
# emby 演员信息的保存位置
|
||||
upper_path = "/home/amtoaer/.config/nas/emby/metadata/people/"
|
||||
|
||||
[credential]
|
||||
# Bilibili 的 Web 端身份凭据,需要凭据才能下载高清视频
|
||||
sessdata = ""
|
||||
bili_jct = ""
|
||||
buvid3 = ""
|
||||
@@ -83,6 +94,8 @@ dedeuserid = ""
|
||||
ac_time_value = ""
|
||||
|
||||
[filter_option]
|
||||
# 视频、音频流的筛选选项,程序会使用范围内质量最高的流
|
||||
# 注意设置范围过小可能导致无满足条件的流,推荐仅调整质量上限和编码优先级
|
||||
video_max_quality = "Quality8k"
|
||||
video_min_quality = "Quality360p"
|
||||
audio_max_quality = "QualityHiRES"
|
||||
@@ -98,6 +111,7 @@ no_hdr = false
|
||||
no_hires = false
|
||||
|
||||
[danmaku_option]
|
||||
# 弹幕的一些相关选项,如字体、字号、透明度、停留时间、是否加粗等
|
||||
duration = 12.0
|
||||
font = "黑体"
|
||||
font_size = 25
|
||||
@@ -112,9 +126,34 @@ outline = 0.8
|
||||
time_offset = 0.0
|
||||
|
||||
[favorite_list]
|
||||
# 收藏夹 ID = 存储的位置
|
||||
52642258 = "/home/amtoaer/HDDs/Videos/Bilibilis/混剪"
|
||||
```
|
||||
|
||||
## Docker Compose 文件示例
|
||||
|
||||
该项目为 `Linux/amd64` 与 `Linux/arm64` 提供了 Docker 版本镜像。
|
||||
|
||||
Docker 版包含该平台对应版本的可执行文件(位于`/app/bili-sync-rs`),并预装了 FFmpeg,其它用法与普通版本完全一致。(可查看 [用于构建镜像的 Dockerfile](./Dockerfile) )
|
||||
|
||||
以下是一个 Docker Compose 的编写示例:
|
||||
```yaml
|
||||
services:
|
||||
bili-sync-rs:
|
||||
image: amtoaer/bili-sync-rs:v2.0.0
|
||||
restart: unless-stopped
|
||||
network_mode: bridge
|
||||
tty: true # 该选项请仅在日志终端支持彩色输出时启用,否则日志中可能会出现乱码
|
||||
hostname: bili-sync-rs
|
||||
container_name: bili-sync-rs
|
||||
volumes:
|
||||
- /home/amtoaer/.config/nas/bili-sync-rs:/app/.config/bili-sync
|
||||
# 以及一些其它必要的挂载,确保此处的挂载与 bili-sync-rs 的配置相匹配
|
||||
# ...
|
||||
logging:
|
||||
driver: "local"
|
||||
```
|
||||
|
||||
## 路线图
|
||||
|
||||
- [x] 凭证认证
|
||||
@@ -129,7 +168,7 @@ time_offset = 0.0
|
||||
- [x] 更好的错误处理
|
||||
- [x] 更好的日志
|
||||
- [x] 请求过快出现风控的 workaround
|
||||
- [ ] 提供简单易用的打包(如 docker)
|
||||
- [x] 提供简单易用的打包(如 docker)
|
||||
- [ ] 支持 UP 主合集下载
|
||||
|
||||
## 参考与借鉴
|
||||
|
||||
@@ -1,5 +1,3 @@
|
||||
use std::sync::Arc;
|
||||
|
||||
use anyhow::{anyhow, bail, Result};
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
@@ -49,7 +47,7 @@ pub struct FilterOption {
|
||||
pub video_min_quality: VideoQuality,
|
||||
pub audio_max_quality: AudioQuality,
|
||||
pub audio_min_quality: AudioQuality,
|
||||
pub codecs: Arc<Vec<VideoCodecs>>,
|
||||
pub codecs: Vec<VideoCodecs>,
|
||||
pub no_dolby_video: bool,
|
||||
pub no_dolby_audio: bool,
|
||||
pub no_hdr: bool,
|
||||
@@ -63,7 +61,7 @@ impl Default for FilterOption {
|
||||
video_min_quality: VideoQuality::Quality360p,
|
||||
audio_max_quality: AudioQuality::QualityHiRES,
|
||||
audio_min_quality: AudioQuality::Quality64k,
|
||||
codecs: Arc::new(vec![VideoCodecs::AV1, VideoCodecs::HEV, VideoCodecs::AVC]),
|
||||
codecs: vec![VideoCodecs::AV1, VideoCodecs::HEV, VideoCodecs::AVC],
|
||||
no_dolby_video: false,
|
||||
no_dolby_audio: false,
|
||||
no_hdr: false,
|
||||
|
||||
@@ -28,6 +28,8 @@ impl Client {
|
||||
reqwest::Client::builder()
|
||||
.default_headers(headers)
|
||||
.gzip(true)
|
||||
.connect_timeout(std::time::Duration::from_secs(10))
|
||||
.read_timeout(std::time::Duration::from_secs(30))
|
||||
.build()
|
||||
.unwrap(),
|
||||
)
|
||||
|
||||
@@ -9,10 +9,10 @@ use rsa::sha2::Sha256;
|
||||
use rsa::{Oaep, RsaPublicKey};
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
use super::error::BiliError;
|
||||
use crate::bilibili::error::BiliError;
|
||||
use crate::bilibili::Client;
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
#[derive(Default, Debug, Clone, Serialize, Deserialize)]
|
||||
pub struct Credential {
|
||||
pub sessdata: String,
|
||||
pub bili_jct: String,
|
||||
@@ -22,16 +22,6 @@ pub struct Credential {
|
||||
}
|
||||
|
||||
impl Credential {
|
||||
const fn empty() -> Self {
|
||||
Self {
|
||||
sessdata: String::new(),
|
||||
bili_jct: String::new(),
|
||||
buvid3: String::new(),
|
||||
dedeuserid: String::new(),
|
||||
ac_time_value: String::new(),
|
||||
}
|
||||
}
|
||||
|
||||
/// 检查凭据是否有效
|
||||
pub async fn need_refresh(&self, client: &Client) -> Result<bool> {
|
||||
let res = client
|
||||
@@ -126,7 +116,7 @@ JNrRuoEUXpabUzGB8QIDAQAB
|
||||
let set_cookies = headers.get_all(header::SET_COOKIE);
|
||||
let mut credential = Self {
|
||||
buvid3: self.buvid3.clone(),
|
||||
..Self::empty()
|
||||
..Self::default()
|
||||
};
|
||||
let required_cookies = HashSet::from(["SESSDATA", "bili_jct", "DedeUserID"]);
|
||||
let cookies: Vec<Cookie> = set_cookies
|
||||
|
||||
@@ -5,8 +5,8 @@ use std::pin::Pin;
|
||||
use anyhow::Result;
|
||||
use tokio::io::{AsyncWrite, AsyncWriteExt, BufWriter};
|
||||
|
||||
use super::canvas::CanvasConfig;
|
||||
use crate::bilibili::danmaku::{DrawEffect, Drawable};
|
||||
use crate::bilibili::danmaku::canvas::CanvasConfig;
|
||||
use crate::bilibili::danmaku::{DanmakuOption, DrawEffect, Drawable};
|
||||
|
||||
struct TimePoint {
|
||||
t: f64,
|
||||
@@ -38,7 +38,7 @@ impl fmt::Display for AssEffect {
|
||||
}
|
||||
}
|
||||
|
||||
impl super::DanmakuOption {
|
||||
impl DanmakuOption {
|
||||
pub fn ass_styles(&self) -> Vec<String> {
|
||||
vec![
|
||||
// Name, Fontname, Fontsize, PrimaryColour, SecondaryColour, OutlineColour, BackColour, \
|
||||
@@ -196,6 +196,7 @@ fn escape_text(text: &str) -> Cow<str> {
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn time_point_fmt() {
|
||||
assert_eq!(format!("{}", TimePoint { t: 0.0 }), "0:00:00.00");
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
use super::CanvasConfig;
|
||||
use crate::bilibili::danmaku::canvas::CanvasConfig;
|
||||
use crate::bilibili::danmaku::Danmu;
|
||||
|
||||
pub enum Collision {
|
||||
|
||||
@@ -5,10 +5,9 @@ use anyhow::Result;
|
||||
use float_ord::FloatOrd;
|
||||
use lane::Lane;
|
||||
|
||||
use super::{Danmu, Drawable};
|
||||
use crate::bilibili::danmaku::canvas::lane::Collision;
|
||||
use crate::bilibili::danmaku::danmu::DanmuType;
|
||||
use crate::bilibili::danmaku::DrawEffect;
|
||||
use crate::bilibili::danmaku::{Danmu, DrawEffect, Drawable};
|
||||
use crate::bilibili::PageInfo;
|
||||
|
||||
#[derive(Debug, serde::Deserialize, serde::Serialize)]
|
||||
|
||||
@@ -1,7 +1,8 @@
|
||||
//! 一个弹幕实例,但是没有位置信息
|
||||
use anyhow::{bail, Result};
|
||||
|
||||
use super::canvas::CanvasConfig;
|
||||
use crate::bilibili::danmaku::canvas::CanvasConfig;
|
||||
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
|
||||
pub enum DanmuType {
|
||||
#[default]
|
||||
|
||||
@@ -3,8 +3,8 @@ use std::path::PathBuf;
|
||||
use anyhow::Result;
|
||||
use tokio::fs::{self, File};
|
||||
|
||||
use super::canvas::CanvasConfig;
|
||||
use super::{AssWriter, Danmu};
|
||||
use crate::bilibili::danmaku::canvas::CanvasConfig;
|
||||
use crate::bilibili::danmaku::{AssWriter, Danmu};
|
||||
use crate::bilibili::PageInfo;
|
||||
use crate::config::CONFIG;
|
||||
|
||||
|
||||
@@ -4,10 +4,9 @@ use futures::TryStreamExt;
|
||||
use prost::Message;
|
||||
use reqwest::Method;
|
||||
|
||||
use super::danmaku::{DanmakuElem, DanmakuWriter};
|
||||
use crate::bilibili::analyzer::PageAnalyzer;
|
||||
use crate::bilibili::client::BiliClient;
|
||||
use crate::bilibili::danmaku::DmSegMobileReply;
|
||||
use crate::bilibili::danmaku::{DanmakuElem, DanmakuWriter, DmSegMobileReply};
|
||||
use crate::bilibili::error::BiliError;
|
||||
|
||||
static MASK_CODE: u64 = 2251799813685247;
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
use std::borrow::Cow;
|
||||
use std::collections::HashMap;
|
||||
use std::path::PathBuf;
|
||||
use std::sync::Arc;
|
||||
|
||||
use anyhow::Result;
|
||||
use arc_swap::ArcSwapOption;
|
||||
@@ -11,7 +12,13 @@ use crate::bilibili::{Credential, DanmakuOption, FilterOption};
|
||||
|
||||
pub static CONFIG: Lazy<Config> = Lazy::new(|| {
|
||||
let config = Config::load().unwrap_or_else(|err| {
|
||||
warn!("加载配置失败,错误为: {err},将使用默认配置...");
|
||||
if err
|
||||
.downcast_ref::<std::io::Error>()
|
||||
.map_or(true, |e| e.kind() != std::io::ErrorKind::NotFound)
|
||||
{
|
||||
panic!("加载配置文件失败,错误为: {err}");
|
||||
}
|
||||
warn!("配置文件不存在,使用默认配置...");
|
||||
Config::new()
|
||||
});
|
||||
// 放到外面,确保新的配置项被保存
|
||||
@@ -48,7 +55,7 @@ impl Default for Config {
|
||||
impl Config {
|
||||
fn new() -> Self {
|
||||
Self {
|
||||
credential: ArcSwapOption::empty(),
|
||||
credential: ArcSwapOption::from(Some(Arc::new(Credential::default()))),
|
||||
filter_option: FilterOption::default(),
|
||||
danmaku_option: DanmakuOption::default(),
|
||||
favorite_list: HashMap::new(),
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
use std::collections::HashMap;
|
||||
use std::env::{args, var};
|
||||
use std::path::{Path, PathBuf};
|
||||
use std::pin::Pin;
|
||||
|
||||
@@ -7,6 +8,7 @@ use entity::{favorite, page, video};
|
||||
use filenamify::filenamify;
|
||||
use futures::stream::{FuturesOrdered, FuturesUnordered};
|
||||
use futures::{pin_mut, Future, StreamExt};
|
||||
use once_cell::sync::Lazy;
|
||||
use sea_orm::entity::prelude::*;
|
||||
use sea_orm::ActiveValue::Set;
|
||||
use sea_orm::TransactionTrait;
|
||||
@@ -14,18 +16,18 @@ use serde_json::json;
|
||||
use tokio::fs;
|
||||
use tokio::sync::{Mutex, Semaphore};
|
||||
|
||||
use super::status::{PageStatus, VideoStatus};
|
||||
use super::utils::{
|
||||
unhandled_videos_pages, update_pages_model, update_videos_model, ModelWrapper, NFOMode, NFOSerializer, TEMPLATE,
|
||||
};
|
||||
use crate::bilibili::{BestStream, BiliClient, BiliError, Dimension, FavoriteList, FilterOption, PageInfo, Video};
|
||||
use crate::bilibili::{BestStream, BiliClient, BiliError, Dimension, FavoriteList, PageInfo, Video};
|
||||
use crate::config::CONFIG;
|
||||
use crate::core::status::{PageStatus, VideoStatus};
|
||||
use crate::core::utils::{
|
||||
create_video_pages, create_videos, exist_labels, filter_unfilled_videos, handle_favorite_info, total_video_count,
|
||||
unhandled_videos_pages, update_pages_model, update_videos_model, ModelWrapper, NFOMode, NFOSerializer, TEMPLATE,
|
||||
};
|
||||
use crate::downloader::Downloader;
|
||||
use crate::error::{DownloadAbortError, ProcessPageError};
|
||||
|
||||
pub static SCAN_ONLY: Lazy<bool> = Lazy::new(|| var("SCAN_ONLY").is_ok() || args().any(|arg| arg == "--scan-only"));
|
||||
|
||||
/// 处理某个收藏夹,首先刷新收藏夹信息,然后下载收藏夹中未下载成功的视频
|
||||
pub async fn process_favorite_list(
|
||||
bili_client: &BiliClient,
|
||||
@@ -35,6 +37,10 @@ pub async fn process_favorite_list(
|
||||
) -> Result<()> {
|
||||
let favorite_model = refresh_favorite_list(bili_client, fid, path, connection).await?;
|
||||
let favorite_model = fetch_video_details(bili_client, favorite_model, connection).await?;
|
||||
if *SCAN_ONLY {
|
||||
warn!("已开启仅扫描模式,跳过视频下载...");
|
||||
return Ok(());
|
||||
}
|
||||
download_unprocessed_videos(bili_client, favorite_model, connection).await
|
||||
}
|
||||
|
||||
@@ -285,8 +291,8 @@ pub async fn download_video_pages(
|
||||
),
|
||||
});
|
||||
if let Err(e) = results.into_iter().nth(4).unwrap() {
|
||||
if let Ok(e) = e.downcast::<DownloadAbortError>() {
|
||||
return Err(e.into());
|
||||
if e.downcast_ref::<DownloadAbortError>().is_some() {
|
||||
return Err(e);
|
||||
}
|
||||
}
|
||||
let mut video_active_model: video::ActiveModel = video_model.into();
|
||||
@@ -327,6 +333,7 @@ pub async fn dispatch_download_page(
|
||||
}
|
||||
Err(e) => {
|
||||
if e.downcast_ref::<DownloadAbortError>().is_some() {
|
||||
should_error = true;
|
||||
is_break = true;
|
||||
break;
|
||||
}
|
||||
@@ -464,8 +471,8 @@ pub async fn download_page(
|
||||
});
|
||||
// 查看下载视频的状态,该状态会影响上层是否 break
|
||||
if let Err(e) = results.into_iter().nth(1).unwrap() {
|
||||
if let Ok(e) = e.downcast::<DownloadAbortError>() {
|
||||
return Err(e.into());
|
||||
if let Ok(BiliError::RiskControlOccurred) = e.downcast::<BiliError>() {
|
||||
bail!(DownloadAbortError());
|
||||
}
|
||||
}
|
||||
let mut page_active_model: page::ActiveModel = page_model.into();
|
||||
@@ -513,7 +520,7 @@ pub async fn fetch_page_video(
|
||||
let streams = bili_video
|
||||
.get_page_analyzer(page_info)
|
||||
.await?
|
||||
.best_stream(&FilterOption::default())?;
|
||||
.best_stream(&CONFIG.filter_option)?;
|
||||
match streams {
|
||||
BestStream::Mixed(mix_stream) => {
|
||||
downloader.fetch(mix_stream.url(), &page_path).await?;
|
||||
@@ -643,15 +650,38 @@ async fn generate_nfo(serializer: NFOSerializer<'_>, nfo_path: PathBuf) -> Resul
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use handlebars::handlebars_helper;
|
||||
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn test_template_usage() {
|
||||
let mut template = handlebars::Handlebars::new();
|
||||
let _ = template.register_template_string("video", "{{bvid}}");
|
||||
handlebars_helper!(truncate: |s: String, len: usize| {
|
||||
if s.chars().count() > len {
|
||||
s.chars().take(len).collect::<String>()
|
||||
} else {
|
||||
s.to_string()
|
||||
}
|
||||
});
|
||||
template.register_helper("truncate", Box::new(truncate));
|
||||
let _ = template.register_template_string("video", "test{{bvid}}test");
|
||||
let _ = template.register_template_string("test_truncate", "哈哈,{{ truncate title 30 }}");
|
||||
assert_eq!(
|
||||
template.render("video", &json!({"bvid": "BV1b5411h7g7"})).unwrap(),
|
||||
"BV1b5411h7g7"
|
||||
"testBV1b5411h7g7test"
|
||||
);
|
||||
assert_eq!(
|
||||
template
|
||||
.render(
|
||||
"test_truncate",
|
||||
&json!({"title": "你说得对,但是 Rust 是由 Mozilla 自主研发的一款全新的编译期格斗游戏。\
|
||||
编译将发生在一个被称作「Cargo」的构建系统中。在这里,被引用的指针将被授予「生命周期」之力,导引对象安全。\
|
||||
你将扮演一位名为「Rustacean」的神秘角色, 在与「Rustc」的搏斗中邂逅各种骨骼惊奇的傲娇报错。\
|
||||
征服她们、通过编译同时,逐步发掘「C++」程序崩溃的真相。"})
|
||||
)
|
||||
.unwrap(),
|
||||
"哈哈,你说得对,但是 Rust 是由 Mozilla 自主研发的一"
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -4,6 +4,7 @@ use std::path::Path;
|
||||
use anyhow::Result;
|
||||
use entity::*;
|
||||
use filenamify::filenamify;
|
||||
use handlebars::handlebars_helper;
|
||||
use migration::OnConflict;
|
||||
use once_cell::sync::Lazy;
|
||||
use quick_xml::events::{BytesCData, BytesText};
|
||||
@@ -15,12 +16,20 @@ use sea_orm::QuerySelect;
|
||||
use serde_json::json;
|
||||
use tokio::io::AsyncWriteExt;
|
||||
|
||||
use super::status::Status;
|
||||
use crate::bilibili::{FavoriteListInfo, PageInfo, VideoInfo};
|
||||
use crate::config::CONFIG;
|
||||
use crate::core::status::Status;
|
||||
|
||||
pub static TEMPLATE: Lazy<handlebars::Handlebars> = Lazy::new(|| {
|
||||
let mut handlebars = handlebars::Handlebars::new();
|
||||
handlebars_helper!(truncate: |s: String, len: usize| {
|
||||
if s.chars().count() > len {
|
||||
s.chars().take(len).collect::<String>()
|
||||
} else {
|
||||
s.to_string()
|
||||
}
|
||||
});
|
||||
handlebars.register_helper("truncate", Box::new(truncate));
|
||||
handlebars
|
||||
.register_template_string("video", &CONFIG.video_name)
|
||||
.unwrap();
|
||||
@@ -468,6 +477,7 @@ impl<'a> NFOSerializer<'a> {
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_generate_nfo() {
|
||||
let video = video::Model {
|
||||
|
||||
12
src/main.rs
12
src/main.rs
@@ -8,16 +8,18 @@ mod database;
|
||||
mod downloader;
|
||||
mod error;
|
||||
|
||||
use env_logger::Env;
|
||||
use once_cell::sync::Lazy;
|
||||
|
||||
use self::bilibili::BiliClient;
|
||||
use self::config::CONFIG;
|
||||
use self::core::command::process_favorite_list;
|
||||
use self::database::{database_connection, migrate_database};
|
||||
use crate::bilibili::BiliClient;
|
||||
use crate::config::CONFIG;
|
||||
use crate::core::command::{process_favorite_list, SCAN_ONLY};
|
||||
use crate::database::{database_connection, migrate_database};
|
||||
|
||||
#[tokio::main]
|
||||
async fn main() -> ! {
|
||||
env_logger::init();
|
||||
env_logger::init_from_env(Env::default().default_filter_or("None,bili_sync=info"));
|
||||
Lazy::force(&SCAN_ONLY);
|
||||
Lazy::force(&CONFIG);
|
||||
let mut anchor = chrono::Local::now().date_naive();
|
||||
let bili_client = BiliClient::new();
|
||||
|
||||
Reference in New Issue
Block a user