mirror of
https://github.com/amtoaer/bili-sync.git
synced 2026-05-09 17:52:41 +08:00
Compare commits
6 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
25c7377b76 | ||
|
|
cd245caabc | ||
|
|
8d9266b2ee | ||
|
|
db62f5527a | ||
|
|
0958893574 | ||
|
|
97aec74242 |
10
Justfile
Normal file
10
Justfile
Normal file
@@ -0,0 +1,10 @@
|
||||
clean:
|
||||
rm -rf ./*-bili-sync-rs
|
||||
|
||||
build:
|
||||
cargo build --target x86_64-unknown-linux-musl --release
|
||||
|
||||
build-docker: build
|
||||
cp target/x86_64-unknown-linux-musl/release/bili-sync-rs ./Linux-x86_64-bili-sync-rs
|
||||
docker build . -t bili-sync-rs-local --build-arg="TARGETPLATFORM=linux/amd64"
|
||||
just clean
|
||||
Binary file not shown.
|
Before Width: | Height: | Size: 4.2 MiB After Width: | Height: | Size: 6.1 MiB |
37
scripts/2.0.3_add_fanart.py
Normal file
37
scripts/2.0.3_add_fanart.py
Normal file
@@ -0,0 +1,37 @@
|
||||
"""
|
||||
2.0.2 -> 2.0.3 时添加了将 poster 拷贝为 fanart 的行为
|
||||
该行为对已存在的视频不会生效,所以可以手动执行该脚本
|
||||
具体来说,该脚本会:
|
||||
1. 遍历命令行参数中所有存在的路径
|
||||
2. 找到路径中所有以 poster.jpg 结尾的文件
|
||||
3. 将 poster.jpg 替换为 fanart.jpg,拷贝到同一目录
|
||||
"""
|
||||
|
||||
import os
|
||||
import sys
|
||||
import shutil
|
||||
from pathlib import Path
|
||||
|
||||
|
||||
def main():
|
||||
if len(sys.argv) <= 1:
|
||||
print("用法: python 2.0.3_add_fanart.py <path1> <path2> ...")
|
||||
exit(1)
|
||||
paths = [Path(path) for path in sys.argv[1:]]
|
||||
for path in paths:
|
||||
if not path.exists():
|
||||
print(f"路径 {path} 不存在,跳过..")
|
||||
continue
|
||||
for root, _, files in os.walk(path):
|
||||
for file in files:
|
||||
if file.endswith("poster.jpg"):
|
||||
poster_path = Path(root) / file
|
||||
print(f"已找到 poster: {poster_path}")
|
||||
fanart_path = Path(root) / file.replace("poster.jpg", "fanart.jpg")
|
||||
shutil.copyfile(poster_path, fanart_path)
|
||||
print(f"已将 {poster_path} 拷贝至 {fanart_path}")
|
||||
print("操作完成")
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
@@ -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,
|
||||
|
||||
@@ -9,8 +9,7 @@ use rsa::sha2::Sha256;
|
||||
use rsa::{Oaep, RsaPublicKey};
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
use super::error::BiliError;
|
||||
use crate::bilibili::Client;
|
||||
use crate::bilibili::{Client, Validate};
|
||||
|
||||
#[derive(Default, Debug, Clone, Serialize, Deserialize)]
|
||||
pub struct Credential {
|
||||
@@ -34,14 +33,8 @@ impl Credential {
|
||||
.await?
|
||||
.error_for_status()?
|
||||
.json::<serde_json::Value>()
|
||||
.await?;
|
||||
let (code, msg) = match (res["code"].as_i64(), res["message"].as_str()) {
|
||||
(Some(code), Some(msg)) => (code, msg),
|
||||
_ => bail!("no code or message found"),
|
||||
};
|
||||
if code != 0 {
|
||||
bail!(BiliError::RequestFailed(code, msg.to_owned()));
|
||||
}
|
||||
.await?
|
||||
.validate()?;
|
||||
res["data"]["refresh"].as_bool().ok_or(anyhow!("check refresh failed"))
|
||||
}
|
||||
|
||||
@@ -105,14 +98,7 @@ JNrRuoEUXpabUzGB8QIDAQAB
|
||||
.error_for_status()?;
|
||||
// 必须在 .json 前取出 headers,否则 res 会被消耗
|
||||
let headers = std::mem::take(res.headers_mut());
|
||||
let res = res.json::<serde_json::Value>().await?;
|
||||
let (code, msg) = match (res["code"].as_i64(), res["message"].as_str()) {
|
||||
(Some(code), Some(msg)) => (code, msg),
|
||||
_ => bail!("no code or message found"),
|
||||
};
|
||||
if code != 0 {
|
||||
bail!(BiliError::RequestFailed(code, msg.to_owned()));
|
||||
}
|
||||
let res = res.json::<serde_json::Value>().await?.validate()?;
|
||||
let set_cookies = headers.get_all(header::SET_COOKIE);
|
||||
let mut credential = Self {
|
||||
buvid3: self.buvid3.clone(),
|
||||
@@ -144,7 +130,7 @@ JNrRuoEUXpabUzGB8QIDAQAB
|
||||
}
|
||||
|
||||
async fn confirm_refresh(&self, client: &Client, new_credential: &Credential) -> Result<()> {
|
||||
let res = client
|
||||
client
|
||||
.request(
|
||||
Method::POST,
|
||||
"https://passport.bilibili.com/x/passport-login/web/confirm/refresh",
|
||||
@@ -159,14 +145,8 @@ JNrRuoEUXpabUzGB8QIDAQAB
|
||||
.await?
|
||||
.error_for_status()?
|
||||
.json::<serde_json::Value>()
|
||||
.await?;
|
||||
let (code, msg) = match (res["code"].as_i64(), res["message"].as_str()) {
|
||||
(Some(code), Some(msg)) => (code, msg),
|
||||
_ => bail!("no code or message found"),
|
||||
};
|
||||
if code != 0 {
|
||||
bail!(BiliError::RequestFailed(code, msg.to_owned()));
|
||||
}
|
||||
.await?
|
||||
.validate()?;
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
|
||||
|
||||
@@ -1,12 +1,11 @@
|
||||
use anyhow::{bail, Result};
|
||||
use anyhow::Result;
|
||||
use async_stream::stream;
|
||||
use chrono::serde::ts_seconds;
|
||||
use chrono::{DateTime, Utc};
|
||||
use futures::Stream;
|
||||
use serde_json::Value;
|
||||
|
||||
use crate::bilibili::error::BiliError;
|
||||
use crate::bilibili::BiliClient;
|
||||
use crate::bilibili::{BiliClient, Validate};
|
||||
pub struct FavoriteList<'a> {
|
||||
client: &'a BiliClient,
|
||||
fid: String,
|
||||
@@ -56,20 +55,13 @@ impl<'a> FavoriteList<'a> {
|
||||
.await?
|
||||
.error_for_status()?
|
||||
.json::<serde_json::Value>()
|
||||
.await?;
|
||||
let (code, msg) = match (res["code"].as_i64(), res["message"].as_str()) {
|
||||
(Some(code), Some(msg)) => (code, msg),
|
||||
_ => bail!("no code or message found"),
|
||||
};
|
||||
if code != 0 {
|
||||
bail!(BiliError::RequestFailed(code, msg.to_owned()));
|
||||
}
|
||||
.await?
|
||||
.validate()?;
|
||||
Ok(serde_json::from_value(res["data"].take())?)
|
||||
}
|
||||
|
||||
async fn get_videos(&self, page: u32) -> Result<Value> {
|
||||
let res = self
|
||||
.client
|
||||
self.client
|
||||
.request(reqwest::Method::GET, "https://api.bilibili.com/x/v3/fav/resource/list")
|
||||
.query(&[
|
||||
("media_id", self.fid.as_str()),
|
||||
@@ -83,15 +75,8 @@ impl<'a> FavoriteList<'a> {
|
||||
.await?
|
||||
.error_for_status()?
|
||||
.json::<serde_json::Value>()
|
||||
.await?;
|
||||
let (code, msg) = match (res["code"].as_i64(), res["message"].as_str()) {
|
||||
(Some(code), Some(msg)) => (code, msg),
|
||||
_ => bail!("no code or message found"),
|
||||
};
|
||||
if code != 0 {
|
||||
bail!(BiliError::RequestFailed(code, msg.to_owned()));
|
||||
}
|
||||
Ok(res)
|
||||
.await?
|
||||
.validate()
|
||||
}
|
||||
|
||||
// 拿到收藏夹的所有权,返回一个收藏夹下的视频流
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
pub use analyzer::{BestStream, FilterOption};
|
||||
use anyhow::{bail, Result};
|
||||
pub use client::{BiliClient, Client};
|
||||
pub use credential::Credential;
|
||||
pub use danmaku::DanmakuOption;
|
||||
@@ -13,3 +14,24 @@ mod danmaku;
|
||||
mod error;
|
||||
mod favorite_list;
|
||||
mod video;
|
||||
|
||||
pub(crate) trait Validate {
|
||||
type Output;
|
||||
|
||||
fn validate(self) -> Result<Self::Output>;
|
||||
}
|
||||
|
||||
impl Validate for serde_json::Value {
|
||||
type Output = serde_json::Value;
|
||||
|
||||
fn validate(self) -> Result<Self::Output> {
|
||||
let (code, msg) = match (self["code"].as_i64(), self["message"].as_str()) {
|
||||
(Some(code), Some(msg)) => (code, msg),
|
||||
_ => bail!("no code or message found"),
|
||||
};
|
||||
if code != 0 {
|
||||
bail!(BiliError::RequestFailed(code, msg.to_owned()));
|
||||
}
|
||||
Ok(self)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -4,11 +4,10 @@ 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::error::BiliError;
|
||||
use crate::bilibili::danmaku::{DanmakuElem, DanmakuWriter, DmSegMobileReply};
|
||||
use crate::bilibili::Validate;
|
||||
|
||||
static MASK_CODE: u64 = 2251799813685247;
|
||||
static XOR_CODE: u64 = 23442827791579;
|
||||
@@ -71,14 +70,8 @@ impl<'a> Video<'a> {
|
||||
.await?
|
||||
.error_for_status()?
|
||||
.json::<serde_json::Value>()
|
||||
.await?;
|
||||
let (code, msg) = match (res["code"].as_i64(), res["message"].as_str()) {
|
||||
(Some(code), Some(msg)) => (code, msg),
|
||||
_ => bail!("no code or message found"),
|
||||
};
|
||||
if code != 0 {
|
||||
bail!(BiliError::RequestFailed(code, msg.to_owned()));
|
||||
}
|
||||
.await?
|
||||
.validate()?;
|
||||
Ok(serde_json::from_value(res["data"].take())?)
|
||||
}
|
||||
|
||||
@@ -91,14 +84,8 @@ impl<'a> Video<'a> {
|
||||
.await?
|
||||
.error_for_status()?
|
||||
.json::<serde_json::Value>()
|
||||
.await?;
|
||||
let (code, msg) = match (res["code"].as_i64(), res["message"].as_str()) {
|
||||
(Some(code), Some(msg)) => (code, msg),
|
||||
_ => bail!("no code or message found"),
|
||||
};
|
||||
if code != 0 {
|
||||
bail!(BiliError::RequestFailed(code, msg.to_owned()));
|
||||
}
|
||||
.await?
|
||||
.validate()?;
|
||||
Ok(serde_json::from_value(res["data"].take())?)
|
||||
}
|
||||
|
||||
@@ -149,14 +136,8 @@ impl<'a> Video<'a> {
|
||||
.await?
|
||||
.error_for_status()?
|
||||
.json::<serde_json::Value>()
|
||||
.await?;
|
||||
let (code, msg) = match (res["code"].as_i64(), res["message"].as_str()) {
|
||||
(Some(code), Some(msg)) => (code, msg),
|
||||
_ => bail!("no code or message found"),
|
||||
};
|
||||
if code != 0 {
|
||||
bail!(BiliError::RequestFailed(code, msg.to_owned()));
|
||||
}
|
||||
.await?
|
||||
.validate()?;
|
||||
Ok(PageAnalyzer::new(res["data"].take()))
|
||||
}
|
||||
}
|
||||
|
||||
@@ -16,14 +16,12 @@ 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};
|
||||
@@ -243,6 +241,7 @@ pub async fn download_video_pages(
|
||||
&video_model,
|
||||
downloader,
|
||||
base_path.join("poster.jpg"),
|
||||
base_path.join("fanart.jpg"),
|
||||
)),
|
||||
// 生成视频信息的 nfo
|
||||
Box::pin(generate_video_nfo(
|
||||
@@ -392,12 +391,13 @@ pub async fn download_page(
|
||||
"pid": page_model.pid,
|
||||
}),
|
||||
)?);
|
||||
let (poster_path, video_path, nfo_path, danmaku_path) = if is_single_page {
|
||||
let (poster_path, video_path, nfo_path, danmaku_path, fanart_path) = if is_single_page {
|
||||
(
|
||||
base_path.join(format!("{}-poster.jpg", &base_name)),
|
||||
base_path.join(format!("{}.mp4", &base_name)),
|
||||
base_path.join(format!("{}.nfo", &base_name)),
|
||||
base_path.join(format!("{}.zh-CN.default.ass", &base_name)),
|
||||
Some(base_path.join(format!("{}-fanart.jpg", &base_name))),
|
||||
)
|
||||
} else {
|
||||
(
|
||||
@@ -413,6 +413,8 @@ pub async fn download_page(
|
||||
base_path
|
||||
.join("Season 1")
|
||||
.join(format!("{} - S01E{:0>2}.zh-CN.default.ass", &base_name, page_model.pid)),
|
||||
// 对于多页视频,会在上一步 fetch_video_poster 中获取剧集的 fanart,无需在此处下载单集的
|
||||
None,
|
||||
)
|
||||
};
|
||||
let dimension = if page_model.width.is_some() && page_model.height.is_some() {
|
||||
@@ -437,6 +439,7 @@ pub async fn download_page(
|
||||
&page_model,
|
||||
downloader,
|
||||
poster_path,
|
||||
fanart_path,
|
||||
)),
|
||||
Box::pin(fetch_page_video(
|
||||
seprate_status[1],
|
||||
@@ -489,6 +492,7 @@ pub async fn fetch_page_poster(
|
||||
page_model: &page::Model,
|
||||
downloader: &Downloader,
|
||||
poster_path: PathBuf,
|
||||
fanart_path: Option<PathBuf>,
|
||||
) -> Result<()> {
|
||||
if !should_run {
|
||||
return Ok(());
|
||||
@@ -504,7 +508,11 @@ pub async fn fetch_page_poster(
|
||||
None => video_model.cover.as_str(),
|
||||
}
|
||||
};
|
||||
downloader.fetch(url, &poster_path).await
|
||||
downloader.fetch(url, &poster_path).await?;
|
||||
if let Some(fanart_path) = fanart_path {
|
||||
fs::copy(&poster_path, &fanart_path).await?;
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub async fn fetch_page_video(
|
||||
@@ -522,7 +530,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?;
|
||||
@@ -591,11 +599,14 @@ pub async fn fetch_video_poster(
|
||||
video_model: &video::Model,
|
||||
downloader: &Downloader,
|
||||
poster_path: PathBuf,
|
||||
fanart_path: PathBuf,
|
||||
) -> Result<()> {
|
||||
if !should_run {
|
||||
return Ok(());
|
||||
}
|
||||
downloader.fetch(&video_model.cover, &poster_path).await
|
||||
downloader.fetch(&video_model.cover, &poster_path).await?;
|
||||
fs::copy(&poster_path, &fanart_path).await?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub async fn fetch_upper_face(
|
||||
|
||||
@@ -16,9 +16,9 @@ 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();
|
||||
|
||||
@@ -11,10 +11,10 @@ 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, SCAN_ONLY};
|
||||
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() -> ! {
|
||||
|
||||
Reference in New Issue
Block a user