diff --git a/crates/bili_sync/src/api/request.rs b/crates/bili_sync/src/api/request.rs index e4fcf69..3617ca1 100644 --- a/crates/bili_sync/src/api/request.rs +++ b/crates/bili_sync/src/api/request.rs @@ -99,8 +99,6 @@ pub struct FollowedUppersRequest { #[derive(Deserialize, Validate)] pub struct InsertFavoriteRequest { pub fid: i64, - #[validate(custom(function = "crate::utils::validation::validate_path"))] - pub path: String, } #[derive(Deserialize, Validate)] @@ -109,21 +107,16 @@ pub struct InsertCollectionRequest { pub mid: i64, #[serde(default)] pub collection_type: CollectionType, - #[validate(custom(function = "crate::utils::validation::validate_path"))] - pub path: String, } #[derive(Deserialize, Validate)] pub struct InsertSubmissionRequest { pub upper_id: i64, - #[validate(custom(function = "crate::utils::validation::validate_path"))] - pub path: String, } #[derive(Deserialize, Validate)] #[serde(rename_all = "camelCase")] pub struct UpdateVideoSourceRequest { - #[validate(custom(function = "crate::utils::validation::validate_path"))] pub path: String, pub enabled: bool, pub rule: Option, diff --git a/crates/bili_sync/src/api/routes/video_sources/mod.rs b/crates/bili_sync/src/api/routes/video_sources/mod.rs index e8b5cf3..0151483 100644 --- a/crates/bili_sync/src/api/routes/video_sources/mod.rs +++ b/crates/bili_sync/src/api/routes/video_sources/mod.rs @@ -10,6 +10,7 @@ use bili_sync_migration::Expr; use sea_orm::ActiveValue::Set; use sea_orm::entity::prelude::*; use sea_orm::{ColumnTrait, DatabaseConnection, EntityTrait, QuerySelect, QueryTrait, TransactionTrait}; +use serde_json::json; use crate::adapter::{_ActiveModel, VideoSource as _, VideoSourceEnum}; use crate::api::error::InnerApiError; @@ -138,10 +139,13 @@ pub async fn get_video_sources_details( .all(&db) )?; if watch_later.is_empty() { + let path = TEMPLATE + .read() + .path_safe_render("watch_later_default_path", &json!({ "name": "稍后再看" }))?; watch_later.push(VideoSourceDetail { id: 1, name: "稍后再看".to_string(), - path: String::new(), + path, rule: None, rule_display: None, use_dynamic_api: None, @@ -171,6 +175,7 @@ pub async fn get_video_sources_default_path( "favorites" => "favorite_default_path", "collections" => "collection_default_path", "submissions" => "submission_default_path", + "watch_later" => "watch_later_default_path", _ => return Err(InnerApiError::BadRequest("Invalid video source type".to_string()).into()), }; let template = TEMPLATE.read(); @@ -185,7 +190,22 @@ pub async fn update_video_source( Extension(db): Extension, ValidatedJson(request): ValidatedJson, ) -> Result, ApiError> { + if source_type != "watch_later" && crate::utils::validation::validate_path(&request.path).is_err() { + return Err( + InnerApiError::BadRequest("path: Validation error: path must be a non-empty absolute path".to_string()) + .into(), + ); + } let rule_display = request.rule.as_ref().map(|rule| rule.to_string()); + let watch_later_path = if source_type == "watch_later" { + Some( + TEMPLATE + .read() + .path_safe_render("watch_later_default_path", &json!({ "name": "稍后再看" }))?, + ) + } else { + None + }; let active_model = match source_type.as_str() { "collections" => collection::Entity::find_by_id(id).one(&db).await?.map(|model| { let mut active_model: collection::ActiveModel = model.into(); @@ -217,7 +237,11 @@ pub async fn update_video_source( Some(model) => { // 如果有记录,使用 id 对应的记录更新 let mut active_model: watch_later::ActiveModel = model.into(); - active_model.path = Set(request.path); + active_model.path = Set( + watch_later_path + .clone() + .expect("watch_later path should always be initialized"), + ); active_model.enabled = Set(request.enabled); active_model.rule = Set(request.rule); Some(_ActiveModel::WatchLater(active_model)) @@ -228,7 +252,9 @@ pub async fn update_video_source( } else { // 如果没有记录且 id 为 1,插入一个新的稍后再看记录 Some(_ActiveModel::WatchLater(watch_later::ActiveModel { - path: Set(request.path), + path: Set( + watch_later_path.expect("watch_later path should always be initialized"), + ), enabled: Set(request.enabled), rule: Set(request.rule), ..Default::default() @@ -368,10 +394,13 @@ pub async fn insert_favorite( let credential = &VersionedConfig::get().read().credential; let favorite = FavoriteList::new(bili_client.as_ref(), request.fid.to_string(), credential); let favorite_info = favorite.get_info().await?; + let path = TEMPLATE + .read() + .path_safe_render("favorite_default_path", &json!({ "name": favorite_info.title }))?; favorite::Entity::insert(favorite::ActiveModel { f_id: Set(favorite_info.id), name: Set(favorite_info.title.clone()), - path: Set(request.path), + path: Set(path), enabled: Set(false), ..Default::default() }) @@ -397,12 +426,15 @@ pub async fn insert_collection( credential, ); let collection_info = collection.get_info().await?; + let path = TEMPLATE + .read() + .path_safe_render("collection_default_path", &json!({ "name": collection_info.name }))?; collection::Entity::insert(collection::ActiveModel { s_id: Set(collection_info.sid), m_id: Set(collection_info.mid), r#type: Set(collection_info.collection_type.into()), name: Set(collection_info.name.clone()), - path: Set(request.path), + path: Set(path), enabled: Set(false), ..Default::default() }) @@ -418,13 +450,18 @@ pub async fn insert_submission( Extension(bili_client): Extension>, ValidatedJson(request): ValidatedJson, ) -> Result, ApiError> { - let credential = &VersionedConfig::get().read().credential; + let config = VersionedConfig::get().snapshot(); + let credential = &config.credential; let submission = Submission::new(bili_client.as_ref(), request.upper_id.to_string(), credential); let upper = submission.get_info().await?; + let upper_name = upper.name; + let path = TEMPLATE + .read() + .path_safe_render("submission_default_path", &json!({ "name": upper_name }))?; submission::Entity::insert(submission::ActiveModel { upper_id: Set(upper.mid.parse()?), - upper_name: Set(upper.name), - path: Set(request.path), + upper_name: Set(upper_name), + path: Set(path), enabled: Set(false), ..Default::default() }) diff --git a/crates/bili_sync/src/config/current.rs b/crates/bili_sync/src/config/current.rs index 1824934..42eff95 100644 --- a/crates/bili_sync/src/config/current.rs +++ b/crates/bili_sync/src/config/current.rs @@ -11,7 +11,7 @@ use crate::bilibili::{Credential, DanmakuOption, FilterOption}; use crate::config::args::ARGS; use crate::config::default::{ default_auth_token, default_bind_address, default_collection_path, default_favorite_path, default_submission_path, - default_time_format, + default_time_format, default_watch_later_path, }; use crate::config::item::{ConcurrentLimit, NFOTimeType, SkipOption, Trigger}; use crate::notifier::Notifier; @@ -43,6 +43,8 @@ pub struct Config { pub collection_default_path: String, #[serde(default = "default_submission_path")] pub submission_default_path: String, + #[serde(default = "default_watch_later_path")] + pub watch_later_default_path: String, pub interval: Trigger, pub upper_path: PathBuf, pub nfo_time_type: NFOTimeType, @@ -130,6 +132,7 @@ impl Default for Config { favorite_default_path: default_favorite_path(), collection_default_path: default_collection_path(), submission_default_path: default_submission_path(), + watch_later_default_path: default_watch_later_path(), interval: Trigger::default(), upper_path: CONFIG_DIR.join("upper_face"), nfo_time_type: NFOTimeType::FavTime, diff --git a/crates/bili_sync/src/config/default.rs b/crates/bili_sync/src/config/default.rs index eb8734e..9accc71 100644 --- a/crates/bili_sync/src/config/default.rs +++ b/crates/bili_sync/src/config/default.rs @@ -28,3 +28,7 @@ pub fn default_collection_path() -> String { pub fn default_submission_path() -> String { "投稿/{{name}}".to_owned() } + +pub fn default_watch_later_path() -> String { + "稍后再看".to_owned() +} diff --git a/crates/bili_sync/src/config/handlebar.rs b/crates/bili_sync/src/config/handlebar.rs index 79490d7..83b46ce 100644 --- a/crates/bili_sync/src/config/handlebar.rs +++ b/crates/bili_sync/src/config/handlebar.rs @@ -18,6 +18,7 @@ fn create_template(config: &Config) -> Result> { handlebars.path_safe_register("favorite_default_path", config.favorite_default_path.clone())?; handlebars.path_safe_register("collection_default_path", config.collection_default_path.clone())?; handlebars.path_safe_register("submission_default_path", config.submission_default_path.clone())?; + handlebars.path_safe_register("watch_later_default_path", config.watch_later_default_path.clone())?; if let Some(notifiers) = &config.notifiers { for notifier in notifiers.iter() { if let Notifier::Webhook { url, template, .. } = notifier { @@ -79,6 +80,13 @@ mod tests { .unwrap(), r"关注_永雏塔菲\\test\\a" ); + let _ = template.path_safe_register("test_drive_prefix", r"W:\\投稿\\{{title}}"); + assert_eq!( + template + .path_safe_render("test_drive_prefix", &json!({"title": "林:杏仁/Almond"})) + .unwrap(), + r"W:\\投稿\\林_杏仁_Almond" + ); } assert_eq!( template diff --git a/crates/bili_sync/src/config/item.rs b/crates/bili_sync/src/config/item.rs index 9e582d2..0f1e5e9 100644 --- a/crates/bili_sync/src/config/item.rs +++ b/crates/bili_sync/src/config/item.rs @@ -95,6 +95,19 @@ impl PathSafeTemplate for handlebars::Handlebars<'_> { } 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)) + let rendered = self.render(name, data)?; + #[cfg(windows)] + let path = { + let bytes = rendered.as_bytes(); + if bytes.len() >= 2 && bytes[0].is_ascii_alphabetic() && bytes[1] == b':' { + let (drive_prefix, rest) = rendered.split_at(2); + format!("{drive_prefix}{}", filenamify(rest)) + } else { + filenamify(&rendered) + } + }; + #[cfg(not(windows))] + let path = filenamify(&rendered); + Ok(path.replace("__SEP__", std::path::MAIN_SEPARATOR_STR)) } } diff --git a/web/package-lock.json b/web/package-lock.json index f9a496e..3d65e7b 100644 --- a/web/package-lock.json +++ b/web/package-lock.json @@ -1,12 +1,12 @@ { "name": "bili-sync-web", - "version": "2.9.4", + "version": "2.10.3", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "bili-sync-web", - "version": "2.9.4", + "version": "2.10.3", "dependencies": { "@types/qrcode": "^1.5.6", "qrcode": "^1.5.4" @@ -33,6 +33,7 @@ "layerchart": "^2.0.0-next.43", "mode-watcher": "^1.1.0", "prettier": "^3.7.4", + "prettier-plugin-organize-imports": "^4.3.0", "prettier-plugin-svelte": "^3.4.1", "prettier-plugin-tailwindcss": "^0.7.2", "svelte": "^5.46.1", @@ -4201,6 +4202,24 @@ "url": "https://github.com/prettier/prettier?sponsor=1" } }, + "node_modules/prettier-plugin-organize-imports": { + "version": "4.3.0", + "resolved": "https://registry.npmmirror.com/prettier-plugin-organize-imports/-/prettier-plugin-organize-imports-4.3.0.tgz", + "integrity": "sha512-FxFz0qFhyBsGdIsb697f/EkvHzi5SZOhWAjxcx2dLt+Q532bAlhswcXGYB1yzjZ69kW8UoadFBw7TyNwlq96Iw==", + "dev": true, + "license": "MIT", + "peer": true, + "peerDependencies": { + "prettier": ">=2.0", + "typescript": ">=2.9", + "vue-tsc": "^2.1.0 || 3" + }, + "peerDependenciesMeta": { + "vue-tsc": { + "optional": true + } + } + }, "node_modules/prettier-plugin-svelte": { "version": "3.4.1", "resolved": "https://registry.npmjs.org/prettier-plugin-svelte/-/prettier-plugin-svelte-3.4.1.tgz", diff --git a/web/src/lib/components/subscription-dialog.svelte b/web/src/lib/components/subscription-dialog.svelte index 6f7e33c..d703632 100644 --- a/web/src/lib/components/subscription-dialog.svelte +++ b/web/src/lib/components/subscription-dialog.svelte @@ -74,7 +74,7 @@ } async function handleSubscribe() { - if (!item || !customPath.trim()) return; + if (!item) return; loading = true; try { @@ -83,8 +83,7 @@ switch (item.type) { case 'favorite': { const request: InsertFavoriteRequest = { - fid: item.fid, - path: customPath.trim() + fid: item.fid }; response = await api.insertFavorite(request); break; @@ -92,16 +91,14 @@ case 'collection': { const request: InsertCollectionRequest = { sid: item.sid, - mid: item.mid, - path: customPath.trim() + mid: item.mid }; response = await api.insertCollection(request); break; } case 'upper': { const request: InsertSubmissionRequest = { - upper_id: item.mid, - path: customPath.trim() + upper_id: item.mid }; response = await api.insertSubmission(request); break; @@ -109,8 +106,9 @@ } if (response && response.data) { + const successPath = customPath.trim() || '自动生成路径'; toast.success('订阅成功', { - description: `已订阅${getTypeLabel()}「${getItemTitle()}」到路径「${customPath.trim()}」` + description: `已订阅${getTypeLabel()}「${getItemTitle()}」到路径「${successPath}」` }); open = false; if (onSuccess) { @@ -156,7 +154,13 @@ 订阅{typeLabel}
即将订阅{typeLabel}「{itemTitle}」
-
请手动编辑本地保存路径:
+ {#if item?.type === 'upper'} +
投稿下载路径将使用“设置-基本设置”中的默认地址。
+ {:else if item?.type === 'favorite'} +
收藏夹下载路径将使用“设置-基本设置-收藏夹快捷订阅路径模板”自动生成。
+ {:else} +
合集下载路径将使用“设置-基本设置-合集快捷订阅路径模板”自动生成。
+ {/if}
@@ -183,29 +187,12 @@ -
- - -
-

路径将作为文件夹名称,用于存放下载的视频文件。

-
-

路径示例:

-
-
Mac/Linux: /home/downloads/我的收藏
-
Windows: C:\Downloads\我的收藏
-
-
-
+ + +

+ 该路径由“设置-基本设置”中的对应模板自动生成。 +

@@ -221,7 +208,7 @@