diff --git a/crates/bili_sync/src/api/request.rs b/crates/bili_sync/src/api/request.rs index 18f129e..27a7490 100644 --- a/crates/bili_sync/src/api/request.rs +++ b/crates/bili_sync/src/api/request.rs @@ -1,5 +1,5 @@ use bili_sync_entity::rule::Rule; -use serde::Deserialize; +use serde::{Deserialize, Serialize}; use validator::Validate; use crate::bilibili::CollectionType; @@ -91,3 +91,8 @@ pub struct UpdateVideoSourceRequest { pub rule: Option, pub use_dynamic_api: Option, } + +#[derive(Serialize, Deserialize)] +pub struct DefaultPathRequest { + pub name: String, +} 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 51e24df..b2e7c0e 100644 --- a/crates/bili_sync/src/api/routes/video_sources/mod.rs +++ b/crates/bili_sync/src/api/routes/video_sources/mod.rs @@ -2,7 +2,7 @@ use std::sync::Arc; use anyhow::Result; use axum::Router; -use axum::extract::{Extension, Path}; +use axum::extract::{Extension, Path, Query}; use axum::routing::{get, post, put}; use bili_sync_entity::rule::Rule; use bili_sync_entity::*; @@ -14,19 +14,25 @@ use sea_orm::{ColumnTrait, DatabaseConnection, EntityTrait, QuerySelect, Transac use crate::adapter::_ActiveModel; use crate::api::error::InnerApiError; use crate::api::request::{ - InsertCollectionRequest, InsertFavoriteRequest, InsertSubmissionRequest, UpdateVideoSourceRequest, + DefaultPathRequest, InsertCollectionRequest, InsertFavoriteRequest, InsertSubmissionRequest, + UpdateVideoSourceRequest, }; use crate::api::response::{ UpdateVideoSourceResponse, VideoSource, VideoSourceDetail, VideoSourcesDetailsResponse, VideoSourcesResponse, }; use crate::api::wrapper::{ApiError, ApiResponse, ValidatedJson}; use crate::bilibili::{BiliClient, Collection, CollectionItem, FavoriteList, Submission}; +use crate::config::{PathSafeTemplate, TEMPLATE}; use crate::utils::rule::FieldEvaluatable; pub(super) fn router() -> Router { Router::new() .route("/video-sources", get(get_video_sources)) .route("/video-sources/details", get(get_video_sources_details)) + .route( + "/video-sources/{type}/default-path", + get(get_video_sources_default_path), + ) // 仅用于前端获取默认路径 .route("/video-sources/{type}/{id}", put(update_video_source)) .route("/video-sources/{type}/{id}/evaluate", post(evaluate_video_source)) .route("/video-sources/favorites", post(insert_favorite)) @@ -154,6 +160,20 @@ pub async fn get_video_sources_details( })) } +pub async fn get_video_sources_default_path( + Path(source_type): Path, + Query(params): Query, +) -> Result, ApiError> { + let template_name = match source_type.as_str() { + "favorites" => "favorite_default_path", + "collections" => "collection_default_path", + "submissions" => "submission_default_path", + _ => return Err(InnerApiError::BadRequest("Invalid video source type".to_string()).into()), + }; + let (template, params) = (TEMPLATE.load(), serde_json::to_value(params)?); + Ok(ApiResponse::ok(template.path_safe_render(template_name, ¶ms)?)) +} + /// 更新视频来源 pub async fn update_video_source( Path((source_type, id)): Path<(String, i32)>, diff --git a/crates/bili_sync/src/config/current.rs b/crates/bili_sync/src/config/current.rs index 3465df7..03b0763 100644 --- a/crates/bili_sync/src/config/current.rs +++ b/crates/bili_sync/src/config/current.rs @@ -8,7 +8,9 @@ use validator::Validate; use crate::bilibili::{Credential, DanmakuOption, FilterOption}; use crate::config::default::{default_auth_token, default_bind_address, default_time_format}; -use crate::config::item::{ConcurrentLimit, NFOTimeType, SkipOption}; +use crate::config::item::{ + ConcurrentLimit, NFOTimeType, SkipOption, default_collection_path, default_favorite_path, default_submission_path, +}; use crate::utils::model::{load_db_config, save_db_config}; pub static CONFIG_DIR: LazyLock = @@ -25,6 +27,12 @@ pub struct Config { pub skip_option: SkipOption, pub video_name: String, pub page_name: String, + #[serde(default = "default_favorite_path")] + pub favorite_default_path: String, + #[serde(default = "default_collection_path")] + pub collection_default_path: String, + #[serde(default = "default_submission_path")] + pub submission_default_path: String, pub interval: u64, pub upper_path: PathBuf, pub nfo_time_type: NFOTimeType, @@ -98,6 +106,9 @@ impl Default for Config { skip_option: SkipOption::default(), video_name: "{{title}}".to_owned(), page_name: "{{bvid}}".to_owned(), + favorite_default_path: default_favorite_path(), + collection_default_path: default_collection_path(), + submission_default_path: default_submission_path(), interval: 1200, upper_path: CONFIG_DIR.join("upper_face"), nfo_time_type: NFOTimeType::FavTime, diff --git a/crates/bili_sync/src/config/handlebar.rs b/crates/bili_sync/src/config/handlebar.rs index 9fe4d7e..ec42db7 100644 --- a/crates/bili_sync/src/config/handlebar.rs +++ b/crates/bili_sync/src/config/handlebar.rs @@ -12,8 +12,11 @@ pub static TEMPLATE: LazyLock>> = fn create_template(config: &Config) -> Result> { let mut handlebars = handlebars::Handlebars::new(); handlebars.register_helper("truncate", Box::new(truncate)); - handlebars.path_safe_register("video", config.video_name.to_owned())?; - handlebars.path_safe_register("page", config.page_name.to_owned())?; + handlebars.path_safe_register("video", config.video_name.clone())?; + handlebars.path_safe_register("page", config.page_name.clone())?; + 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())?; Ok(handlebars) } diff --git a/crates/bili_sync/src/config/item.rs b/crates/bili_sync/src/config/item.rs index fb76f9d..43bcf1f 100644 --- a/crates/bili_sync/src/config/item.rs +++ b/crates/bili_sync/src/config/item.rs @@ -85,3 +85,15 @@ impl PathSafeTemplate for handlebars::Handlebars<'_> { Ok(filenamify(&self.render(name, data)?).replace("__SEP__", std::path::MAIN_SEPARATOR_STR)) } } + +pub fn default_favorite_path() -> String { + "收藏夹/{{name}}".to_owned() +} + +pub fn default_collection_path() -> String { + "合集/{{name}}".to_owned() +} + +pub fn default_submission_path() -> String { + "投稿/{{name}}".to_owned() +} diff --git a/web/src/lib/api.ts b/web/src/lib/api.ts index fe1ade3..3d3abc0 100644 --- a/web/src/lib/api.ts +++ b/web/src/lib/api.ts @@ -221,6 +221,10 @@ class ApiClient { return this.post(`/video-sources/${type}/${id}/evaluate`, null); } + async getDefaultPath(type: string, name: string): Promise> { + return this.get(`/video-sources/${type}/default-path`, { name }); + } + async getConfig(): Promise> { return this.get('/config'); } @@ -268,6 +272,7 @@ const api = { apiClient.updateVideoSource(type, id, request), evaluateVideoSourceRules: (type: string, id: number) => apiClient.evaluateVideoSourceRules(type, id), + getDefaultPath: (type: string, name: string) => apiClient.getDefaultPath(type, name), getConfig: () => apiClient.getConfig(), updateConfig: (config: Config) => apiClient.updateConfig(config), getDashboard: () => apiClient.getDashboard(), diff --git a/web/src/lib/components/subscription-card.svelte b/web/src/lib/components/subscription-card.svelte index 8d3f1f6..ffdf2ec 100644 --- a/web/src/lib/components/subscription-card.svelte +++ b/web/src/lib/components/subscription-card.svelte @@ -20,18 +20,18 @@ | FavoriteWithSubscriptionStatus | CollectionWithSubscriptionStatus | UpperWithSubscriptionStatus; - export let type: 'favorite' | 'collection' | 'upper' = 'favorite'; + export let type: 'favorites' | 'collections' | 'submissions' = 'favorites'; export let onSubscriptionSuccess: (() => void) | null = null; let dialogOpen = false; function getIcon() { switch (type) { - case 'favorite': + case 'favorites': return HeartIcon; - case 'collection': + case 'collections': return FolderIcon; - case 'upper': + case 'submissions': return UserIcon; default: return VideoIcon; @@ -40,11 +40,11 @@ function getTypeLabel() { switch (type) { - case 'favorite': + case 'favorites': return '收藏夹'; - case 'collection': + case 'collections': return '合集'; - case 'upper': + case 'submissions': return 'UP 主'; default: return ''; @@ -53,11 +53,11 @@ function getTitle(): string { switch (type) { - case 'favorite': + case 'favorites': return (item as FavoriteWithSubscriptionStatus).title; - case 'collection': + case 'collections': return (item as CollectionWithSubscriptionStatus).title; - case 'upper': + case 'submissions': return (item as UpperWithSubscriptionStatus).uname; default: return ''; @@ -66,12 +66,10 @@ function getSubtitle(): string { switch (type) { - case 'favorite': + case 'favorites': return `uid: ${(item as FavoriteWithSubscriptionStatus).mid}`; - case 'collection': + case 'collections': return `uid: ${(item as CollectionWithSubscriptionStatus).mid}`; - case 'upper': - return ''; default: return ''; } @@ -79,7 +77,7 @@ function getDescription(): string { switch (type) { - case 'upper': + case 'submissions': return (item as UpperWithSubscriptionStatus).sign || ''; default: return ''; @@ -88,9 +86,9 @@ function isDisabled(): boolean { switch (type) { - case 'collection': + case 'collections': return (item as CollectionWithSubscriptionStatus).invalid; - case 'upper': { + case 'submissions': { return (item as UpperWithSubscriptionStatus).invalid; } default: @@ -100,9 +98,9 @@ function getDisabledReason(): string { switch (type) { - case 'collection': + case 'collections': return '已失效'; - case 'upper': + case 'submissions': return '账号已注销'; default: return ''; @@ -111,7 +109,7 @@ function getCount(): number | null { switch (type) { - case 'favorite': + case 'favorites': return (item as FavoriteWithSubscriptionStatus).media_count; default: return null; @@ -124,7 +122,7 @@ function getAvatarUrl(): string { switch (type) { - case 'upper': + case 'submissions': return (item as UpperWithSubscriptionStatus).face; default: return ''; @@ -171,7 +169,7 @@ ? 'opacity-50' : ''}" > - {#if avatarUrl && type === 'upper'} + {#if avatarUrl && type === 'submissions'} {title} - - diff --git a/web/src/lib/components/subscription-dialog.svelte b/web/src/lib/components/subscription-dialog.svelte index a8a6a59..8aa57b9 100644 --- a/web/src/lib/components/subscription-dialog.svelte +++ b/web/src/lib/components/subscription-dialog.svelte @@ -2,6 +2,7 @@ import { Button } from '$lib/components/ui/button/index.js'; import { Input } from '$lib/components/ui/input/index.js'; import { Label } from '$lib/components/ui/label/index.js'; + import { toast } from 'svelte-sonner'; import { Sheet, SheetContent, @@ -10,7 +11,6 @@ SheetHeader, SheetTitle } from '$lib/components/ui/sheet/index.js'; - import { toast } from 'svelte-sonner'; import api from '$lib/api'; import type { FavoriteWithSubscriptionStatus, @@ -22,47 +22,40 @@ ApiError } from '$lib/types'; - export let open = false; - export let item: - | FavoriteWithSubscriptionStatus - | CollectionWithSubscriptionStatus - | UpperWithSubscriptionStatus - | null = null; - export let type: 'favorite' | 'collection' | 'upper' = 'favorite'; - export let onSuccess: (() => void) | null = null; + interface Props { + open: boolean; + item: + | FavoriteWithSubscriptionStatus + | CollectionWithSubscriptionStatus + | UpperWithSubscriptionStatus + | null; + type: 'favorites' | 'collections' | 'submissions'; + onSuccess: (() => void) | null; + } - let customPath = ''; - let loading = false; + let { + open = $bindable(false), + item = null, + type = 'favorites', + onSuccess = null + }: Props = $props(); + + let customPath = $state(''); + let loading = $state(false); // 根据类型和 item 生成默认路径 - function generateDefaultPath(): string { - if (!item) return ''; - - switch (type) { - case 'favorite': { - const favorite = item as FavoriteWithSubscriptionStatus; - return `收藏夹/${favorite.title}`; - } - case 'collection': { - const collection = item as CollectionWithSubscriptionStatus; - return `合集/${collection.title}`; - } - case 'upper': { - const upper = item as UpperWithSubscriptionStatus; - return `UP 主/${upper.uname}`; - } - default: - return ''; - } + async function generateDefaultPath(): Promise { + if (!itemTitle) return ''; + return (await api.getDefaultPath(type, itemTitle)).data; } function getTypeLabel(): string { switch (type) { - case 'favorite': + case 'favorites': return '收藏夹'; - case 'collection': + case 'collections': return '合集'; - case 'upper': + case 'submissions': return 'UP 主'; default: return ''; @@ -73,11 +66,11 @@ if (!item) return ''; switch (type) { - case 'favorite': + case 'favorites': return (item as FavoriteWithSubscriptionStatus).title; - case 'collection': + case 'collections': return (item as CollectionWithSubscriptionStatus).title; - case 'upper': + case 'submissions': return (item as UpperWithSubscriptionStatus).uname; default: return ''; @@ -92,7 +85,7 @@ let response; switch (type) { - case 'favorite': { + case 'favorites': { const favorite = item as FavoriteWithSubscriptionStatus; const request: InsertFavoriteRequest = { fid: favorite.fid, @@ -101,7 +94,7 @@ response = await api.insertFavorite(request); break; } - case 'collection': { + case 'collections': { const collection = item as CollectionWithSubscriptionStatus; const request: InsertCollectionRequest = { sid: collection.sid, @@ -111,7 +104,7 @@ response = await api.insertCollection(request); break; } - case 'upper': { + case 'submissions': { const upper = item as UpperWithSubscriptionStatus; const request: InsertSubmissionRequest = { upper_id: upper.mid, @@ -145,10 +138,20 @@ open = false; } - // 当对话框打开时重置 path - $: if (open && item) { - customPath = generateDefaultPath(); - } + $effect(() => { + if (open && item) { + generateDefaultPath() + .then((path) => { + customPath = path; + }) + .catch((error) => { + toast.error('获取默认路径失败', { + description: (error as ApiError).message + }); + customPath = ''; + }); + } + }); const typeLabel = getTypeLabel(); const itemTitle = getItemTitle(); @@ -173,14 +176,14 @@ {typeLabel}名称: {itemTitle} - {#if type === 'favorite'} + {#if type === 'favorites'} {@const favorite = item as FavoriteWithSubscriptionStatus}
视频数量: {favorite.media_count} 个
{/if} - {#if type === 'upper'} + {#if type === 'submissions'} {@const upper = item as UpperWithSubscriptionStatus} {#if upper.sign}
diff --git a/web/src/lib/types.ts b/web/src/lib/types.ts index 057dfe3..aefd8a3 100644 --- a/web/src/lib/types.ts +++ b/web/src/lib/types.ts @@ -281,6 +281,9 @@ export interface Config { skip_option: SkipOption; video_name: string; page_name: string; + favorite_default_path: string; + collection_default_path: string; + submission_default_path: string; interval: number; upper_path: string; nfo_time_type: string; diff --git a/web/src/routes/me/collections/+page.svelte b/web/src/routes/me/collections/+page.svelte index d912aa1..ea91e1f 100644 --- a/web/src/routes/me/collections/+page.svelte +++ b/web/src/routes/me/collections/+page.svelte @@ -77,7 +77,7 @@
diff --git a/web/src/routes/me/favorites/+page.svelte b/web/src/routes/me/favorites/+page.svelte index 8e428ca..588ae51 100644 --- a/web/src/routes/me/favorites/+page.svelte +++ b/web/src/routes/me/favorites/+page.svelte @@ -63,7 +63,7 @@
diff --git a/web/src/routes/me/uppers/+page.svelte b/web/src/routes/me/uppers/+page.svelte index 734d2ea..5495dc9 100644 --- a/web/src/routes/me/uppers/+page.svelte +++ b/web/src/routes/me/uppers/+page.svelte @@ -78,7 +78,7 @@
diff --git a/web/src/routes/settings/+page.svelte b/web/src/routes/settings/+page.svelte index f63ba73..ce5a05e 100644 --- a/web/src/routes/settings/+page.svelte +++ b/web/src/routes/settings/+page.svelte @@ -191,8 +191,6 @@
- -
@@ -209,6 +207,23 @@ +
+
+ + +
+
+ + +
+
+ + +
+
+ + +