Compare commits

..

10 Commits

Author SHA1 Message Date
ᴀᴍᴛᴏᴀᴇʀ
c4db12b154 fix: 修复类型错误导致的数值溢出 (#115) 2024-06-01 03:21:23 +08:00
ᴀᴍᴛᴏᴀᴇʀ
2ef99a20c9 feat: 支持自定义 NFO 文件中的视频时间,可选加入收藏夹的时间、视频发布的时间 (#114)
* feat: 支持自定义 NFO 文件中的视频时间,可选加入收藏夹的时间、视频发布的时间

* chore: 使用小写
2024-06-01 03:01:39 +08:00
ᴀᴍᴛᴏᴀᴇʀ
67de151234 ci: 使用较旧的 rust nightly 版本,避免语言变更导致的编译失败 (#113) 2024-06-01 01:51:19 +08:00
ᴀᴍᴛᴏᴀᴇʀ
73f97f937f feat: 每次执行前检查登录状态,避免凭据失效导致的非预期行为 (#112)
* feat: 每次执行前检查登录状态,避免凭据失效导致的非预期行为

* refactor: 减少代码长度
2024-06-01 01:46:15 +08:00
ky0utarou
8fee6fb97a Update README.md - compose中指定user,附加简要说明 (#102)
* Update README.md - compose中指定user

* Update README.md - compose中指定user的简要说明
2024-05-08 19:11:32 +08:00
ᴀᴍᴛᴏᴀᴇʀ
e5e5b07978 fix: 修复当目标文件已存在时 ffmpeg 卡住的问题 (#99) 2024-05-05 17:22:35 +08:00
ᴀᴍᴛᴏᴀᴇʀ
cd2bd9cbb3 chore: 减少并发下载量与 read_timeout 值 (#96)
* chore: 减少并发下载量与 read_timeout 值

* chore: 修正注释
2024-05-03 12:48:53 +08:00
ᴀᴍᴛᴏᴀᴇʀ
f044b18337 chore: 使用 tracing 替换 env_logger (#93) 2024-05-02 03:00:16 +08:00
amtoaer
d3bfca42f6 ci: 先安装依赖再 copy 二进制文件,确保使用 docker 缓存 2024-05-02 00:45:47 +08:00
ky0utarou
10ccb47790 ci: Dockerfile - 保留tzdata (#91)
* Dockerfile - keep tzdata for correct time

* Dockerfile - install tzdata only for correct logging time

refer to https://stackoverflow.com/a/68996528
2024-05-01 21:21:22 +08:00
14 changed files with 152 additions and 77 deletions

View File

@@ -26,7 +26,7 @@ jobs:
- name: Checkout repo
uses: actions/checkout@v4
- run: rustup toolchain install nightly && rustup default nightly && rustup component add rustfmt clippy
- run: rustup default nightly-2024-04-30 && rustup component add rustfmt clippy
- name: Cache dependencies
uses: swatinem/rust-cache@v2

70
Cargo.lock generated
View File

@@ -427,7 +427,6 @@ dependencies = [
"cookie 0.18.1",
"dirs",
"entity",
"env_logger",
"filenamify",
"float-ord",
"futures",
@@ -450,6 +449,8 @@ dependencies = [
"thiserror",
"tokio",
"toml",
"tracing",
"tracing-subscriber",
]
[[package]]
@@ -862,29 +863,6 @@ dependencies = [
"serde_json",
]
[[package]]
name = "env_filter"
version = "0.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a009aa4810eb158359dda09d0c87378e4bbb89b5a801f016885a4707ba24f7ea"
dependencies = [
"log",
"regex",
]
[[package]]
name = "env_logger"
version = "0.11.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "38b35839ba51819680ba087cd351788c9a3c476841207e0b8cee0b04722343b9"
dependencies = [
"anstream",
"anstyle",
"env_filter",
"humantime",
"log",
]
[[package]]
name = "equivalent"
version = "1.0.1"
@@ -1366,12 +1344,6 @@ version = "1.8.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d897f394bad6a705d5f4104762e116a75639e470d80901eed05a860a95cb1904"
[[package]]
name = "humantime"
version = "2.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9a3a5bfb195931eeb336b2a7b4d761daec841b97f947d34394601737a7bba5e4"
[[package]]
name = "hyper"
version = "1.2.0"
@@ -1700,6 +1672,16 @@ dependencies = [
"minimal-lexical",
]
[[package]]
name = "nu-ansi-term"
version = "0.46.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "77a8165726e8236064dbb45459242600304b42a5ea24ee2948e18e023bf7ba84"
dependencies = [
"overload",
"winapi",
]
[[package]]
name = "num-bigint"
version = "0.4.4"
@@ -1828,6 +1810,12 @@ dependencies = [
"syn 2.0.58",
]
[[package]]
name = "overload"
version = "0.1.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b15813163c1d831bf4a13c3610c05c0d03b39feb07f7e09fa234dac9b15aaf39"
[[package]]
name = "parking"
version = "2.2.0"
@@ -3446,6 +3434,18 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c06d3da6113f116aaee68e4d601191614c9053067f9ab7f6edbcb161237daa54"
dependencies = [
"once_cell",
"valuable",
]
[[package]]
name = "tracing-log"
version = "0.2.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ee855f1f400bd0e5c02d150ae5de3840039a3f54b025156404e34c23c03f47c3"
dependencies = [
"log",
"once_cell",
"tracing-core",
]
[[package]]
@@ -3454,13 +3454,17 @@ version = "0.3.18"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ad0f048c97dbd9faa9b7df56362b8ebcaa52adb06b498c050d2f4e32f90a7a8b"
dependencies = [
"chrono",
"matchers",
"nu-ansi-term",
"once_cell",
"regex",
"sharded-slab",
"smallvec",
"thread_local",
"tracing",
"tracing-core",
"tracing-log",
]
[[package]]
@@ -3552,6 +3556,12 @@ dependencies = [
"serde",
]
[[package]]
name = "valuable"
version = "0.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "830b7e5d4d90034032940e4ace0d9a9a057e7a45cd94e6c007832e39edb82f6d"
[[package]]
name = "value-bag"
version = "1.8.1"

View File

@@ -14,7 +14,6 @@ chrono = { version = "0.4.35", features = ["serde"] }
cookie = "0.18.0"
dirs = "5.0.1"
entity = { path = "entity" }
env_logger = "0.11.3"
filenamify = "0.1.0"
float-ord = "0.3.2"
futures = "0.3.30"
@@ -49,6 +48,8 @@ strum = { version = "0.26", features = ["derive"] }
thiserror = "1.0.58"
tokio = { version = "1", features = ["full"] }
toml = "0.8.12"
tracing = "0.1"
tracing-subscriber = { version = "0.3", features = ["chrono"] }
[workspace]
members = [".", "entity", "migration"]

View File

@@ -4,15 +4,12 @@ ARG TARGETPLATFORM
WORKDIR /app
COPY ./*-bili-sync-rs ./targets/
RUN apk update && apk add --no-cache \
ca-certificates \
tzdata \
ffmpeg \
&& cp /usr/share/zoneinfo/Asia/Shanghai /etc/localtime \
&& echo "Asia/Shanghai" > /etc/timezone \
&& apk del tzdata
ffmpeg
COPY ./*-bili-sync-rs ./targets/
RUN if [ "$TARGETPLATFORM" = "linux/amd64" ]; then \
mv ./targets/Linux-x86_64-bili-sync-rs ./bili-sync-rs; \

View File

@@ -144,6 +144,7 @@ services:
restart: unless-stopped
network_mode: bridge
tty: true # 该选项请仅在日志终端支持彩色输出时启用,否则日志中可能会出现乱码
# user: 1000:1000 # 非必需设置项,说明见下
hostname: bili-sync-rs
container_name: bili-sync-rs
volumes:
@@ -153,6 +154,9 @@ services:
logging:
driver: "local"
```
### user 的设置
- 可设置为宿主机适当用户的 uid 及 gid (`$uid:$gid`),使项目下载的文件的所有者与该处设置的用户保持一致,不设置默认为 root
- 执行 `id ${user}` 以获得 `user` 用户的 uid 及 gid ,了解更多可参阅 [Docker文档](https://docs.docker.com/engine/reference/run/#user)
## 路线图
@@ -177,4 +181,4 @@ services:
+ [bilibili-API-collect](https://github.com/SocialSisterYi/bilibili-API-collect) B 站的第三方接口文档
+ [bilibili-api](https://github.com/Nemo2011/bilibili-api) 使用 Python 调用接口的参考实现
+ [danmu2ass](https://github.com/gwy15/danmu2ass) 本项目弹幕下载功能的缝合来源
+ [danmu2ass](https://github.com/gwy15/danmu2ass) 本项目弹幕下载功能的缝合来源

View File

@@ -8,7 +8,7 @@ pub struct Model {
#[sea_orm(primary_key)]
pub id: i32,
pub video_id: i32,
pub cid: i32,
pub cid: i64,
pub pid: i32,
pub name: String,
pub width: Option<u32>,

View File

@@ -1,6 +1,6 @@
use std::sync::Arc;
use anyhow::Result;
use anyhow::{bail, Result};
use reqwest::{header, Method};
use crate::bilibili::Credential;
@@ -29,7 +29,7 @@ impl Client {
.default_headers(headers)
.gzip(true)
.connect_timeout(std::time::Duration::from_secs(10))
.read_timeout(std::time::Duration::from_secs(30))
.read_timeout(std::time::Duration::from_secs(10))
.build()
.unwrap(),
)
@@ -85,4 +85,13 @@ impl BiliClient {
CONFIG.credential.store(Some(Arc::new(new_credential)));
CONFIG.save()
}
/// 检查凭据是否已设置且有效
pub async fn is_login(&self) -> Result<()> {
let credential = CONFIG.credential.load();
let Some(credential) = credential.as_deref() else {
bail!("no credential found");
};
credential.is_login(&self.client).await
}
}

View File

@@ -38,6 +38,24 @@ impl Credential {
res["data"]["refresh"].as_bool().ok_or(anyhow!("check refresh failed"))
}
/// 需要使用一个需要鉴权的接口来检查是否登录
/// 此处使用查看用户状态数的接口,该接口返回内容少,请求成本低
pub async fn is_login(&self, client: &Client) -> Result<()> {
client
.request(
Method::GET,
"https://api.bilibili.com/x/web-interface/nav/stat",
Some(self),
)
.send()
.await?
.error_for_status()?
.json::<serde_json::Value>()
.await?
.validate()?;
Ok(())
}
pub async fn refresh(&self, client: &Client) -> Result<Self> {
let correspond_path = Self::get_correspond_path();
let csrf = self.get_refresh_csrf(client, correspond_path).await?;

View File

@@ -39,7 +39,7 @@ impl serde::Serialize for Tag {
}
#[derive(Debug, serde::Deserialize, Default)]
pub struct PageInfo {
pub cid: i32,
pub cid: i64,
pub page: i32,
#[serde(rename = "part")]
pub name: String,
@@ -92,7 +92,7 @@ impl<'a> Video<'a> {
pub async fn get_danmaku_writer(&self, page: &'a PageInfo) -> Result<DanmakuWriter> {
let tasks = FuturesUnordered::new();
for i in 1..=(page.duration + 359) / 360 {
tasks.push(self.get_danmaku_segment(page, i as i32));
tasks.push(self.get_danmaku_segment(page, i as i64));
}
let result: Vec<Vec<DanmakuElem>> = tasks.try_collect().await?;
let mut result: Vec<DanmakuElem> = result.into_iter().flatten().collect();
@@ -100,7 +100,7 @@ impl<'a> Video<'a> {
Ok(DanmakuWriter::new(page, result.into_iter().map(|x| x.into()).collect()))
}
async fn get_danmaku_segment(&self, page: &PageInfo, segment_idx: i32) -> Result<Vec<DanmakuElem>> {
async fn get_danmaku_segment(&self, page: &PageInfo, segment_idx: i64) -> Result<Vec<DanmakuElem>> {
let mut res = self
.client
.request(Method::GET, "http://api.bilibili.com/x/v2/dm/web/seg.so")

View File

@@ -19,7 +19,7 @@ pub static CONFIG: Lazy<Config> = Lazy::new(|| {
panic!("加载配置文件失败,错误为: {err}");
}
warn!("配置文件不存在,使用默认配置...");
Config::new()
Config::default()
});
// 放到外面,确保新的配置项被保存
info!("配置加载完毕,覆盖刷新原有配置");
@@ -44,16 +44,20 @@ pub struct Config {
pub page_name: Cow<'static, str>,
pub interval: u64,
pub upper_path: PathBuf,
#[serde(default)]
pub nfo_time_type: NFOTimeType,
}
#[derive(Serialize, Deserialize, Default)]
#[serde(rename_all = "lowercase")]
pub enum NFOTimeType {
#[default]
FavTime,
PubTime,
}
impl Default for Config {
fn default() -> Self {
Self::new()
}
}
impl Config {
fn new() -> Self {
Self {
credential: ArcSwapOption::from(Some(Arc::new(Credential::default()))),
filter_option: FilterOption::default(),
@@ -63,9 +67,12 @@ impl Config {
page_name: Cow::Borrowed("{{bvid}}"),
interval: 1200,
upper_path: CONFIG_DIR.join("upper_face"),
nfo_time_type: NFOTimeType::FavTime,
}
}
}
impl Config {
/// 简单的预检查
pub fn check(&self) {
let mut ok = true;

View File

@@ -157,8 +157,8 @@ pub async fn download_unprocessed_videos(
favorite_model.f_id, favorite_model.name
);
let unhandled_videos_pages = unhandled_videos_pages(&favorite_model, connection).await?;
// 对于视频,允许个同时下载(视频内还有分页、不同分页还有多种下载任务)
let semaphore = Semaphore::new(5);
// 对于视频,允许个同时下载(视频内还有分页、不同分页还有多种下载任务)
let semaphore = Semaphore::new(3);
let downloader = Downloader::new(bili_client.client.clone());
let mut uppers_mutex: HashMap<i64, (Mutex<()>, Mutex<()>)> = HashMap::new();
for (video_model, _) in &unhandled_videos_pages {
@@ -312,8 +312,8 @@ pub async fn dispatch_download_page(
if !should_run {
return Ok(());
}
// 对于视频的分页,允许同时下载三个同时下载(绝大部分是单页视频)
let child_semaphore = Semaphore::new(5);
// 对于视频的分页,允许个同时下载(绝大部分是单页视频)
let child_semaphore = Semaphore::new(2);
let mut tasks = pages
.into_iter()
.map(|page_model| download_page(bili_client, video_model, page_model, &child_semaphore, downloader))
@@ -657,7 +657,11 @@ async fn generate_nfo(serializer: NFOSerializer<'_>, nfo_path: PathBuf) -> Resul
if let Some(parent) = nfo_path.parent() {
fs::create_dir_all(parent).await?;
}
fs::write(nfo_path, serializer.generate_nfo().await?.as_bytes()).await?;
fs::write(
nfo_path,
serializer.generate_nfo(&CONFIG.nfo_time_type).await?.as_bytes(),
)
.await?;
Ok(())
}

View File

@@ -17,7 +17,7 @@ use serde_json::json;
use tokio::io::AsyncWriteExt;
use crate::bilibili::{FavoriteListInfo, PageInfo, VideoInfo};
use crate::config::CONFIG;
use crate::config::{NFOTimeType, CONFIG};
use crate::core::status::Status;
pub static TEMPLATE: Lazy<handlebars::Handlebars> = Lazy::new(|| {
@@ -274,7 +274,7 @@ pub async fn update_pages_model(pages: Vec<page::ActiveModel>, connection: &Data
/// serde xml 似乎不太好用,先这么裸着写
/// (真是又臭又长啊
impl<'a> NFOSerializer<'a> {
pub async fn generate_nfo(self) -> Result<String> {
pub async fn generate_nfo(self, nfo_time_type: &NFOTimeType) -> Result<String> {
let mut buffer = r#"<?xml version="1.0" encoding="utf-8" standalone="yes"?>
"#
.as_bytes()
@@ -283,6 +283,10 @@ impl<'a> NFOSerializer<'a> {
let mut writer = Writer::new_with_indent(&mut tokio_buffer, b' ', 4);
match self {
NFOSerializer(ModelWrapper::Video(v), NFOMode::MOVIE) => {
let nfo_time = match nfo_time_type {
NFOTimeType::FavTime => v.favtime,
NFOTimeType::PubTime => v.pubtime,
};
writer
.create_element("movie")
.write_inner_content_async::<_, _, Error>(|writer| async move {
@@ -316,7 +320,7 @@ impl<'a> NFOSerializer<'a> {
.unwrap();
writer
.create_element("year")
.write_text_content_async(BytesText::new(&v.favtime.format("%Y").to_string()))
.write_text_content_async(BytesText::new(&nfo_time.format("%Y").to_string()))
.await
.unwrap();
if let Some(tags) = &v.tags {
@@ -337,7 +341,7 @@ impl<'a> NFOSerializer<'a> {
.unwrap();
writer
.create_element("aired")
.write_text_content_async(BytesText::new(&v.favtime.format("%Y-%m-%d").to_string()))
.write_text_content_async(BytesText::new(&nfo_time.format("%Y-%m-%d").to_string()))
.await
.unwrap();
Ok(writer)
@@ -346,6 +350,10 @@ impl<'a> NFOSerializer<'a> {
.unwrap();
}
NFOSerializer(ModelWrapper::Video(v), NFOMode::TVSHOW) => {
let nfo_time = match nfo_time_type {
NFOTimeType::FavTime => v.favtime,
NFOTimeType::PubTime => v.pubtime,
};
writer
.create_element("tvshow")
.write_inner_content_async::<_, _, Error>(|writer| async move {
@@ -379,7 +387,7 @@ impl<'a> NFOSerializer<'a> {
.unwrap();
writer
.create_element("year")
.write_text_content_async(BytesText::new(&v.favtime.format("%Y").to_string()))
.write_text_content_async(BytesText::new(&nfo_time.format("%Y").to_string()))
.await
.unwrap();
if let Some(tags) = &v.tags {
@@ -400,7 +408,7 @@ impl<'a> NFOSerializer<'a> {
.unwrap();
writer
.create_element("aired")
.write_text_content_async(BytesText::new(&v.favtime.format("%Y-%m-%d").to_string()))
.write_text_content_async(BytesText::new(&nfo_time.format("%Y-%m-%d").to_string()))
.await
.unwrap();
Ok(writer)
@@ -490,8 +498,8 @@ mod tests {
chrono::NaiveTime::from_hms_opt(2, 2, 2).unwrap(),
),
pubtime: chrono::NaiveDateTime::new(
chrono::NaiveDate::from_ymd_opt(2022, 2, 2).unwrap(),
chrono::NaiveTime::from_hms_opt(2, 2, 2).unwrap(),
chrono::NaiveDate::from_ymd_opt(2033, 3, 3).unwrap(),
chrono::NaiveTime::from_hms_opt(3, 3, 3).unwrap(),
),
bvid: "bvid".to_string(),
tags: Some(serde_json::json!(["tag1", "tag2"])),
@@ -499,7 +507,7 @@ mod tests {
};
assert_eq!(
NFOSerializer(ModelWrapper::Video(&video), NFOMode::MOVIE)
.generate_nfo()
.generate_nfo(&NFOTimeType::PubTime)
.await
.unwrap(),
r#"<?xml version="1.0" encoding="utf-8" standalone="yes"?>
@@ -511,16 +519,16 @@ mod tests {
<name>1</name>
<role>upper_name</role>
</actor>
<year>2022</year>
<year>2033</year>
<genre>tag1</genre>
<genre>tag2</genre>
<uniqueid type="bilibili">bvid</uniqueid>
<aired>2022-02-02</aired>
<aired>2033-03-03</aired>
</movie>"#,
);
assert_eq!(
NFOSerializer(ModelWrapper::Video(&video), NFOMode::TVSHOW)
.generate_nfo()
.generate_nfo(&NFOTimeType::FavTime)
.await
.unwrap(),
r#"<?xml version="1.0" encoding="utf-8" standalone="yes"?>
@@ -541,7 +549,7 @@ mod tests {
);
assert_eq!(
NFOSerializer(ModelWrapper::Video(&video), NFOMode::UPPER)
.generate_nfo()
.generate_nfo(&NFOTimeType::FavTime)
.await
.unwrap(),
r#"<?xml version="1.0" encoding="utf-8" standalone="yes"?>
@@ -549,7 +557,7 @@ mod tests {
<plot/>
<outline/>
<lockdata>false</lockdata>
<dateadded>2022-02-02 02:02:02</dateadded>
<dateadded>2033-03-03 03:03:03</dateadded>
<title>1</title>
<sorttitle>1</sorttitle>
</person>"#,
@@ -561,7 +569,7 @@ mod tests {
};
assert_eq!(
NFOSerializer(ModelWrapper::Page(&page), NFOMode::EPOSODE)
.generate_nfo()
.generate_nfo(&NFOTimeType::FavTime)
.await
.unwrap(),
r#"<?xml version="1.0" encoding="utf-8" standalone="yes"?>

View File

@@ -40,6 +40,7 @@ impl Downloader {
audio_path.to_str().unwrap(),
"-c",
"copy",
"-y",
output_path.to_str().unwrap(),
])
.output()

View File

@@ -1,5 +1,5 @@
#[macro_use]
extern crate log;
extern crate tracing;
mod bilibili;
mod config;
@@ -8,8 +8,11 @@ mod database;
mod downloader;
mod error;
use env_logger::Env;
use std::time::Duration;
use once_cell::sync::Lazy;
use tokio::time;
use tracing_subscriber::util::SubscriberInitExt;
use crate::bilibili::BiliClient;
use crate::config::CONFIG;
@@ -18,7 +21,15 @@ use crate::database::{database_connection, migrate_database};
#[tokio::main]
async fn main() -> ! {
env_logger::init_from_env(Env::default().default_filter_or("None,bili_sync=info"));
let default_log_level = std::env::var("RUST_LOG").unwrap_or("None,bili_sync=info".to_owned());
tracing_subscriber::fmt::Subscriber::builder()
.with_env_filter(tracing_subscriber::EnvFilter::builder().parse_lossy(default_log_level))
.with_timer(tracing_subscriber::fmt::time::ChronoLocal::new(
"%Y-%m-%d %H:%M:%S%.3f".to_owned(),
))
.finish()
.try_init()
.expect("初始化日志失败");
Lazy::force(&SCAN_ONLY);
Lazy::force(&CONFIG);
let mut anchor = chrono::Local::now().date_naive();
@@ -26,10 +37,15 @@ async fn main() -> ! {
let connection = database_connection().await.unwrap();
migrate_database(&connection).await.unwrap();
loop {
if let Err(e) = bili_client.is_login().await {
error!("检查登录状态时遇到错误:{e},等待下一轮执行");
time::sleep(Duration::from_secs(CONFIG.interval)).await;
continue;
}
if anchor != chrono::Local::now().date_naive() {
if let Err(e) = bili_client.check_refresh().await {
error!("检查刷新 Credential 遇到错误:{e},等待下一轮执行");
tokio::time::sleep(std::time::Duration::from_secs(CONFIG.interval)).await;
time::sleep(Duration::from_secs(CONFIG.interval)).await;
continue;
}
anchor = chrono::Local::now().date_naive();
@@ -41,6 +57,6 @@ async fn main() -> ! {
}
}
info!("所有收藏夹处理完毕,等待下一轮执行");
tokio::time::sleep(std::time::Duration::from_secs(CONFIG.interval)).await;
time::sleep(Duration::from_secs(CONFIG.interval)).await;
}
}