Compare commits

..

6 Commits

Author SHA1 Message Date
amtoaer
25c7377b76 docs: 更换 README 图片 2024-04-28 23:47:21 +08:00
ᴀᴍᴛᴏᴀᴇʀ
cd245caabc build: 添加 justfile,方便本地构建镜像 (#85) 2024-04-28 23:43:37 +08:00
ᴀᴍᴛᴏᴀᴇʀ
8d9266b2ee feat: 拷贝一份 poster 作为 fanart 使用 (#84)
* feat: 拷贝一份 poster 作为 fanart 使用

* feat: 添加对于现有视频的迁移脚本
2024-04-28 22:13:26 +08:00
ᴀᴍᴛᴏᴀᴇʀ
db62f5527a refactor: 为 serde_json::Value 实现 trait,避免重复代码 (#82) 2024-04-27 00:45:09 +08:00
ᴀᴍᴛᴏᴀᴇʀ
0958893574 style: 尽量使用绝对路径引入包 (#81) 2024-04-26 19:50:23 +08:00
ᴀᴍᴛᴏᴀᴇʀ
97aec74242 fix: 修复 filter option 未使用的问题 (#80) 2024-04-26 19:34:31 +08:00
16 changed files with 128 additions and 103 deletions

10
Justfile Normal file
View 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

View 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()

View File

@@ -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,

View File

@@ -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(())
}
}

View File

@@ -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");

View File

@@ -1,4 +1,4 @@
use super::CanvasConfig;
use crate::bilibili::danmaku::canvas::CanvasConfig;
use crate::bilibili::danmaku::Danmu;
pub enum Collision {

View File

@@ -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)]

View File

@@ -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]

View File

@@ -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;

View File

@@ -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()
}
// 拿到收藏夹的所有权,返回一个收藏夹下的视频流

View File

@@ -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)
}
}

View File

@@ -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()))
}
}

View File

@@ -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(

View File

@@ -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();

View File

@@ -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() -> ! {