diff --git a/crates/bili_sync/src/adapter/helper/mod.rs b/crates/bili_sync/src/adapter/helper/mod.rs index df0891b..56e23e2 100644 --- a/crates/bili_sync/src/adapter/helper/mod.rs +++ b/crates/bili_sync/src/adapter/helper/mod.rs @@ -9,8 +9,7 @@ use sea_orm::ActiveValue::Set; use sea_orm::{Condition, DatabaseTransaction, QuerySelect}; use crate::bilibili::{BiliError, PageInfo, VideoInfo}; -use crate::config::TEMPLATE; -use crate::utils::filenamify::filenamify; +use crate::config::{PathSafeTemplate, TEMPLATE}; use crate::utils::id_time_key; /// 使用 condition 筛选视频,返回视频数量 @@ -66,11 +65,7 @@ pub(super) fn video_with_path( ) -> video::ActiveModel { if let Some(fmt_args) = &video_info.to_fmt_args() { video_model.path = Set(Path::new(base_path) - .join(filenamify( - TEMPLATE - .render("video", fmt_args) - .unwrap_or_else(|_| video_info.bvid().to_string()), - )) + .join(TEMPLATE.path_safe_render("video", fmt_args).unwrap()) .to_string_lossy() .to_string()); } diff --git a/crates/bili_sync/src/config/global.rs b/crates/bili_sync/src/config/global.rs index 4f0eccb..d4de207 100644 --- a/crates/bili_sync/src/config/global.rs +++ b/crates/bili_sync/src/config/global.rs @@ -5,6 +5,7 @@ use handlebars::handlebars_helper; use once_cell::sync::Lazy; use crate::config::clap::Args; +use crate::config::item::PathSafeTemplate; use crate::config::Config; /// 全局的 CONFIG,可以从中读取配置信息 @@ -39,10 +40,8 @@ pub static TEMPLATE: Lazy = Lazy::new(|| { } }); handlebars.register_helper("truncate", Box::new(truncate)); - handlebars - .register_template_string("video", &CONFIG.video_name) - .unwrap(); - handlebars.register_template_string("page", &CONFIG.page_name).unwrap(); + handlebars.path_safe_register("video", &CONFIG.video_name).unwrap(); + handlebars.path_safe_register("page", &CONFIG.page_name).unwrap(); handlebars }); diff --git a/crates/bili_sync/src/config/item.rs b/crates/bili_sync/src/config/item.rs index d973076..f5b02c9 100644 --- a/crates/bili_sync/src/config/item.rs +++ b/crates/bili_sync/src/config/item.rs @@ -1,11 +1,13 @@ use std::collections::HashMap; use std::path::PathBuf; +use anyhow::Result; use serde::de::{Deserializer, MapAccess, Visitor}; use serde::ser::SerializeMap; use serde::{Deserialize, Serialize}; use crate::bilibili::{CollectionItem, CollectionType}; +use crate::utils::filenamify::filenamify; /// 稍后再看的配置 #[derive(Serialize, Deserialize, Default)] @@ -58,6 +60,21 @@ impl Default for ConcurrentLimit { } } +pub trait PathSafeTemplate { + fn path_safe_register(&mut self, name: &'static str, template: &'static str) -> Result<()>; + fn path_safe_render(&self, name: &'static str, data: &serde_json::Value) -> Result; +} + +/// 通过将模板字符串中的分隔符替换为自定义的字符串,使得模板字符串中的分隔符得以保留 +impl PathSafeTemplate for handlebars::Handlebars<'_> { + fn path_safe_register(&mut self, name: &'static str, template: &'static str) -> Result<()> { + Ok(self.register_template_string(name, template.replace(std::path::MAIN_SEPARATOR_STR, "__SEP__"))?) + } + + fn path_safe_render(&self, name: &'static str, data: &serde_json::Value) -> Result { + Ok(filenamify(&self.render(name, data)?).replace("__SEP__", std::path::MAIN_SEPARATOR_STR)) + } +} /* 后面是用于自定义 Collection 的序列化、反序列化的样板代码 */ pub(super) fn serialize_collection_list( collection_list: &HashMap, diff --git a/crates/bili_sync/src/config/mod.rs b/crates/bili_sync/src/config/mod.rs index b7ddbda..16bb8b1 100644 --- a/crates/bili_sync/src/config/mod.rs +++ b/crates/bili_sync/src/config/mod.rs @@ -14,7 +14,7 @@ mod item; use crate::bilibili::{CollectionItem, Credential, DanmakuOption, FilterOption}; pub use crate::config::global::{ARGS, CONFIG, CONFIG_DIR, TEMPLATE}; use crate::config::item::{deserialize_collection_list, serialize_collection_list, ConcurrentLimit}; -pub use crate::config::item::{Delay, NFOTimeType, WatchLaterConfig}; +pub use crate::config::item::{Delay, NFOTimeType, PathSafeTemplate, WatchLaterConfig}; fn default_time_format() -> String { "%Y-%m-%d".to_string() diff --git a/crates/bili_sync/src/workflow.rs b/crates/bili_sync/src/workflow.rs index 6a7b7a1..9d3bb2c 100644 --- a/crates/bili_sync/src/workflow.rs +++ b/crates/bili_sync/src/workflow.rs @@ -14,11 +14,10 @@ use tokio::sync::{Mutex, Semaphore}; use crate::adapter::{video_list_from, Args, VideoListModel}; use crate::bilibili::{BestStream, BiliClient, BiliError, Dimension, PageInfo, Video, VideoInfo}; -use crate::config::{ARGS, CONFIG, TEMPLATE}; +use crate::config::{PathSafeTemplate, ARGS, CONFIG, TEMPLATE}; use crate::downloader::Downloader; use crate::error::{DownloadAbortError, ProcessPageError}; use crate::utils::delay; -use crate::utils::filenamify::filenamify; use crate::utils::model::{create_videos, update_pages_model, update_videos_model}; use crate::utils::nfo::{ModelWrapper, NFOMode, NFOSerializer}; use crate::utils::status::{PageStatus, VideoStatus}; @@ -313,7 +312,7 @@ pub async fn download_page( let seprate_status = status.should_run(); let is_single_page = video_model.single_page.unwrap(); let base_path = Path::new(&video_model.path); - let base_name = filenamify(TEMPLATE.render( + let base_name = TEMPLATE.path_safe_render( "page", &json!({ "bvid": &video_model.bvid, @@ -325,7 +324,7 @@ pub async fn download_page( "pubtime": video_model.pubtime.format(&CONFIG.time_format).to_string(), "fav_time": video_model.favtime.format(&CONFIG.time_format).to_string(), }), - )?); + )?; let (poster_path, video_path, nfo_path, danmaku_path, fanart_path) = if is_single_page { ( base_path.join(format!("{}-poster.jpg", &base_name)), @@ -623,15 +622,49 @@ mod tests { } }); 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 }}"); + let _ = template.path_safe_register("video", "test{{bvid}}test"); + let _ = template.path_safe_register("test_truncate", "哈哈,{{ truncate title 30 }}"); + let _ = template.path_safe_register("test_path_unix", "{{ truncate title 7 }}/test/a"); + let _ = template.path_safe_register("test_path_windows", r"{{ truncate title 7 }}\\test\\a"); + #[cfg(not(windows))] + { + assert_eq!( + template + .path_safe_render("test_path_unix", &json!({"title": "关注/永雏塔菲喵"})) + .unwrap(), + "关注_永雏塔菲/test/a" + ); + assert_eq!( + template + .path_safe_render("test_path_windows", &json!({"title": "关注/永雏塔菲喵"})) + .unwrap(), + "关注_永雏塔菲_test_a" + ); + } + #[cfg(windows)] + { + assert_eq!( + template + .path_safe_render("test_path_unix", &json!({"title": "关注/永雏塔菲喵"})) + .unwrap(), + "关注_永雏塔菲_test_a" + ); + assert_eq!( + template + .path_safe_render("test_path_windows", &json!({"title": "关注永雏/塔菲喵"})) + .unwrap(), + r"关注_永雏塔菲\\test\\a" + ); + } assert_eq!( - template.render("video", &json!({"bvid": "BV1b5411h7g7"})).unwrap(), + template + .path_safe_render("video", &json!({"bvid": "BV1b5411h7g7"})) + .unwrap(), "testBV1b5411h7g7test" ); assert_eq!( template - .render( + .path_safe_render( "test_truncate", &json!({"title": "你说得对,但是 Rust 是由 Mozilla 自主研发的一款全新的编译期格斗游戏。\ 编译将发生在一个被称作「Cargo」的构建系统中。在这里,被引用的指针将被授予「生命周期」之力,导引对象安全。\