Compare commits

...

11 Commits

Author SHA1 Message Date
ᴀᴍᴛᴏᴀᴇʀ
0958893574 style: 尽量使用绝对路径引入包 (#81) 2024-04-26 19:50:23 +08:00
ᴀᴍᴛᴏᴀᴇʀ
97aec74242 fix: 修复 filter option 未使用的问题 (#80) 2024-04-26 19:34:31 +08:00
ᴀᴍᴛᴏᴀᴇʀ
aa9d8c9e66 fix: 修复配置文件初始化时未填充 credential 默认值的问题 (#78) 2024-04-25 23:00:43 +08:00
ᴀᴍᴛᴏᴀᴇʀ
1ad82e513e fix: 修复风控判断错误,以及可能的阻塞问题 (#77)
* fix: 尝试修复风控判断错误,以及可能的阻塞问题

* fix: 继续修复
2024-04-25 22:56:47 +08:00
ᴀᴍᴛᴏᴀᴇʀ
be4f62d4e1 feat: 支持 scan-only 参数,开启该参数时会跳过下载过程 (#76) 2024-04-25 18:41:59 +08:00
ᴀᴍᴛᴏᴀᴇʀ
2bdfdd8b8f chore: 设置默认日志等级为 info (#75) 2024-04-24 20:28:18 +08:00
ᴀᴍᴛᴏᴀᴇʀ
2366c36462 feat: 支持在模板中对文本进行截断,避免路径过长错误 (#73) 2024-04-23 23:21:29 +08:00
ᴀᴍᴛᴏᴀᴇʀ
badaeed104 fix: 配置文件存在但读取失败时应该仅报错,不覆盖配置 (#71) 2024-04-22 22:38:20 +08:00
amtoaer
ee7ee4b883 ci: 使用 nightly 进行代码检查 2024-04-12 22:17:24 +08:00
amtoaer
2429f3b742 docs: 更新 README.md 2024-04-12 22:06:09 +08:00
amtoaer
8d8218d515 ci: 格式化使用 nightly 进行检查 2024-04-12 20:39:20 +08:00
17 changed files with 165 additions and 80 deletions

View File

@@ -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
View File

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

View File

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

View File

@@ -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 等媒体库工具浏览。
支持展示视频封面、名称、加入日期、标签、分页等。
## 效果演示
**注:因为可能同时存在单页视频和多页视频,媒体库类型请选择“混合内容”。**
### 概览
![概览](./assets/overview.png)
### 详情
@@ -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_nameup 主名称、upper_midup 主 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 主合集下载
## 参考与借鉴

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

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

View File

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

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

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

View File

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

View File

@@ -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 自主研发的一"
);
}
}

View File

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

View File

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