diff --git a/crates/bili_sync/src/adapter/collection.rs b/crates/bili_sync/src/adapter/collection.rs index 0f1433d..9a8233a 100644 --- a/crates/bili_sync/src/adapter/collection.rs +++ b/crates/bili_sync/src/adapter/collection.rs @@ -110,4 +110,9 @@ impl VideoSource for collection::Model { .await?; Ok((updated_model.into(), Box::pin(collection.into_video_stream()))) } + + async fn delete_from_db(self, conn: &impl ConnectionTrait) -> Result<()> { + self.delete(conn).await?; + Ok(()) + } } diff --git a/crates/bili_sync/src/adapter/favorite.rs b/crates/bili_sync/src/adapter/favorite.rs index a50bee8..9669593 100644 --- a/crates/bili_sync/src/adapter/favorite.rs +++ b/crates/bili_sync/src/adapter/favorite.rs @@ -73,4 +73,9 @@ impl VideoSource for favorite::Model { .await?; Ok((updated_model.into(), Box::pin(favorite.into_video_stream()))) } + + async fn delete_from_db(self, conn: &impl ConnectionTrait) -> Result<()> { + self.delete(conn).await?; + Ok(()) + } } diff --git a/crates/bili_sync/src/adapter/mod.rs b/crates/bili_sync/src/adapter/mod.rs index db95911..a1bc466 100644 --- a/crates/bili_sync/src/adapter/mod.rs +++ b/crates/bili_sync/src/adapter/mod.rs @@ -121,6 +121,8 @@ pub trait VideoSource { })?; Ok(()) } + + async fn delete_from_db(self, conn: &impl ConnectionTrait) -> Result<()>; } pub enum _ActiveModel { diff --git a/crates/bili_sync/src/adapter/submission.rs b/crates/bili_sync/src/adapter/submission.rs index 155fbbc..1ce2a74 100644 --- a/crates/bili_sync/src/adapter/submission.rs +++ b/crates/bili_sync/src/adapter/submission.rs @@ -114,4 +114,9 @@ impl VideoSource for submission::Model { }; Ok((updated_model.into(), video_stream)) } + + async fn delete_from_db(self, conn: &impl ConnectionTrait) -> Result<()> { + self.delete(conn).await?; + Ok(()) + } } diff --git a/crates/bili_sync/src/adapter/watch_later.rs b/crates/bili_sync/src/adapter/watch_later.rs index e572cfc..c123706 100644 --- a/crates/bili_sync/src/adapter/watch_later.rs +++ b/crates/bili_sync/src/adapter/watch_later.rs @@ -58,4 +58,9 @@ impl VideoSource for watch_later::Model { let watch_later = WatchLater::new(bili_client, credential); Ok((self.into(), Box::pin(watch_later.into_video_stream()))) } + + async fn delete_from_db(self, conn: &impl ConnectionTrait) -> Result<()> { + self.delete(conn).await?; + Ok(()) + } } 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 059343a..e8b5cf3 100644 --- a/crates/bili_sync/src/api/routes/video_sources/mod.rs +++ b/crates/bili_sync/src/api/routes/video_sources/mod.rs @@ -9,9 +9,9 @@ use bili_sync_entity::*; use bili_sync_migration::Expr; use sea_orm::ActiveValue::Set; use sea_orm::entity::prelude::*; -use sea_orm::{ColumnTrait, DatabaseConnection, EntityTrait, QuerySelect, TransactionTrait}; +use sea_orm::{ColumnTrait, DatabaseConnection, EntityTrait, QuerySelect, QueryTrait, TransactionTrait}; -use crate::adapter::_ActiveModel; +use crate::adapter::{_ActiveModel, VideoSource as _, VideoSourceEnum}; use crate::api::error::InnerApiError; use crate::api::request::{ DefaultPathRequest, InsertCollectionRequest, InsertFavoriteRequest, InsertSubmissionRequest, @@ -33,7 +33,10 @@ pub(super) fn router() -> Router { "/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}", + put(update_video_source).delete(remove_video_source), + ) .route("/video-sources/{type}/{id}/evaluate", post(evaluate_video_source)) .route("/video-sources/favorites", post(insert_favorite)) .route("/video-sources/collections", post(insert_collection)) @@ -242,6 +245,43 @@ pub async fn update_video_source( Ok(ApiResponse::ok(UpdateVideoSourceResponse { rule_display })) } +pub async fn remove_video_source( + Path((source_type, id)): Path<(String, i32)>, + Extension(db): Extension, +) -> Result, ApiError> { + // 不允许删除稍后再看 + let video_source: Option = match source_type.as_str() { + "collections" => collection::Entity::find_by_id(id).one(&db).await?.map(Into::into), + "favorites" => favorite::Entity::find_by_id(id).one(&db).await?.map(Into::into), + "submissions" => submission::Entity::find_by_id(id).one(&db).await?.map(Into::into), + _ => return Err(InnerApiError::BadRequest("Invalid video source type".to_string()).into()), + }; + let Some(video_source) = video_source else { + return Err(InnerApiError::NotFound(id).into()); + }; + let txn = db.begin().await?; + page::Entity::delete_many() + .filter( + page::Column::VideoId.in_subquery( + video::Entity::find() + .filter(video_source.filter_expr()) + .select_only() + .column(video::Column::Id) + .as_query() + .to_owned(), + ), + ) + .exec(&txn) + .await?; + video::Entity::delete_many() + .filter(video_source.filter_expr()) + .exec(&txn) + .await?; + video_source.delete_from_db(&txn).await?; + txn.commit().await?; + Ok(ApiResponse::ok(true)) +} + pub async fn evaluate_video_source( Path((source_type, id)): Path<(String, i32)>, Extension(db): Extension, diff --git a/web/src/lib/api.ts b/web/src/lib/api.ts index 3d3abc0..c3fa929 100644 --- a/web/src/lib/api.ts +++ b/web/src/lib/api.ts @@ -217,6 +217,10 @@ class ApiClient { return this.put(`/video-sources/${type}/${id}`, request); } + async removeVideoSource(type: string, id: number): Promise> { + return this.request(`/video-sources/${type}/${id}`, 'DELETE'); + } + async evaluateVideoSourceRules(type: string, id: number): Promise> { return this.post(`/video-sources/${type}/${id}/evaluate`, null); } @@ -270,6 +274,7 @@ const api = { getVideoSourcesDetails: () => apiClient.getVideoSourcesDetails(), updateVideoSource: (type: string, id: number, request: UpdateVideoSourceRequest) => apiClient.updateVideoSource(type, id, request), + removeVideoSource: (type: string, id: number) => apiClient.removeVideoSource(type, id), evaluateVideoSourceRules: (type: string, id: number) => apiClient.evaluateVideoSourceRules(type, id), getDefaultPath: (type: string, name: string) => apiClient.getDefaultPath(type, name), diff --git a/web/src/routes/video-sources/+page.svelte b/web/src/routes/video-sources/+page.svelte index df2a7ea..03d738f 100644 --- a/web/src/routes/video-sources/+page.svelte +++ b/web/src/routes/video-sources/+page.svelte @@ -14,6 +14,7 @@ import ClockIcon from '@lucide/svelte/icons/clock'; import PlusIcon from '@lucide/svelte/icons/plus'; import InfoIcon from '@lucide/svelte/icons/info'; + import TrashIcon2 from '@lucide/svelte/icons/trash-2'; import * as Tooltip from '$lib/components/ui/tooltip/index.js'; import { toast } from 'svelte-sonner'; import { setBreadcrumb } from '$lib/stores/breadcrumb'; @@ -45,6 +46,13 @@ let evaluateType = ''; let evaluating = false; + // 删除对话框状态 + let showRemoveDialog = false; + let removeSource: VideoSourceDetail | null = null; + let removeType = ''; + let removeIdx: number = 0; + let removing = false; + // 编辑表单数据 let editForm = { path: '', @@ -100,6 +108,13 @@ showEvaluateDialog = true; } + function openRemoveDialog(type: string, source: VideoSourceDetail, idx: number) { + removeSource = source; + removeType = type; + removeIdx = idx; + showRemoveDialog = true; + } + // 保存编辑 async function saveEdit() { if (!editingSource) return; @@ -162,6 +177,33 @@ } } + async function removeVideoSource() { + if (!removeSource) return; + removing = true; + try { + let response = await api.removeVideoSource(removeType, removeSource.id); + if (response && response.data) { + if (videoSourcesData) { + const sources = videoSourcesData[ + removeType as keyof VideoSourcesDetailsResponse + ] as VideoSourceDetail[]; + sources.splice(removeIdx, 1); + videoSourcesData = { ...videoSourcesData }; + } + showRemoveDialog = false; + toast.success('删除视频源成功'); + } else { + toast.error('删除视频源失败'); + } + } catch (error) { + toast.error('删除视频源失败', { + description: (error as ApiError).message + }); + } finally { + removing = false; + } + } + function getSourcesForTab(tabValue: string): VideoSourceDetail[] { if (!videoSourcesData) return []; return videoSourcesData[tabValue as keyof VideoSourcesDetailsResponse] as VideoSourceDetail[]; @@ -342,6 +384,17 @@ > + {#if activeTab !== 'watch_later'} + + {/if} {/each} @@ -471,6 +524,31 @@ + + + + 删除视频源 + + 确定要删除视频源 "{removeSource?.name}" 吗?
+ 删除后该视频源相关的所有条目将从数据库中移除(不影响磁盘文件),该操作无法撤销
+
+
+ + { + showRemoveDialog = false; + }}>取消 + + {removing ? '删除中' : '删除'} + + +
+
+