mirror of
https://github.com/amtoaer/bili-sync.git
synced 2026-09-05 07:28:04 +08:00
feat: 支持删除视频源 (#525)
This commit is contained in:
@@ -110,4 +110,9 @@ impl VideoSource for collection::Model {
|
|||||||
.await?;
|
.await?;
|
||||||
Ok((updated_model.into(), Box::pin(collection.into_video_stream())))
|
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(())
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -73,4 +73,9 @@ impl VideoSource for favorite::Model {
|
|||||||
.await?;
|
.await?;
|
||||||
Ok((updated_model.into(), Box::pin(favorite.into_video_stream())))
|
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(())
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -121,6 +121,8 @@ pub trait VideoSource {
|
|||||||
})?;
|
})?;
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async fn delete_from_db(self, conn: &impl ConnectionTrait) -> Result<()>;
|
||||||
}
|
}
|
||||||
|
|
||||||
pub enum _ActiveModel {
|
pub enum _ActiveModel {
|
||||||
|
|||||||
@@ -114,4 +114,9 @@ impl VideoSource for submission::Model {
|
|||||||
};
|
};
|
||||||
Ok((updated_model.into(), video_stream))
|
Ok((updated_model.into(), video_stream))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async fn delete_from_db(self, conn: &impl ConnectionTrait) -> Result<()> {
|
||||||
|
self.delete(conn).await?;
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -58,4 +58,9 @@ impl VideoSource for watch_later::Model {
|
|||||||
let watch_later = WatchLater::new(bili_client, credential);
|
let watch_later = WatchLater::new(bili_client, credential);
|
||||||
Ok((self.into(), Box::pin(watch_later.into_video_stream())))
|
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(())
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -9,9 +9,9 @@ use bili_sync_entity::*;
|
|||||||
use bili_sync_migration::Expr;
|
use bili_sync_migration::Expr;
|
||||||
use sea_orm::ActiveValue::Set;
|
use sea_orm::ActiveValue::Set;
|
||||||
use sea_orm::entity::prelude::*;
|
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::error::InnerApiError;
|
||||||
use crate::api::request::{
|
use crate::api::request::{
|
||||||
DefaultPathRequest, InsertCollectionRequest, InsertFavoriteRequest, InsertSubmissionRequest,
|
DefaultPathRequest, InsertCollectionRequest, InsertFavoriteRequest, InsertSubmissionRequest,
|
||||||
@@ -33,7 +33,10 @@ pub(super) fn router() -> Router {
|
|||||||
"/video-sources/{type}/default-path",
|
"/video-sources/{type}/default-path",
|
||||||
get(get_video_sources_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/{type}/{id}/evaluate", post(evaluate_video_source))
|
||||||
.route("/video-sources/favorites", post(insert_favorite))
|
.route("/video-sources/favorites", post(insert_favorite))
|
||||||
.route("/video-sources/collections", post(insert_collection))
|
.route("/video-sources/collections", post(insert_collection))
|
||||||
@@ -242,6 +245,43 @@ pub async fn update_video_source(
|
|||||||
Ok(ApiResponse::ok(UpdateVideoSourceResponse { rule_display }))
|
Ok(ApiResponse::ok(UpdateVideoSourceResponse { rule_display }))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub async fn remove_video_source(
|
||||||
|
Path((source_type, id)): Path<(String, i32)>,
|
||||||
|
Extension(db): Extension<DatabaseConnection>,
|
||||||
|
) -> Result<ApiResponse<bool>, ApiError> {
|
||||||
|
// 不允许删除稍后再看
|
||||||
|
let video_source: Option<VideoSourceEnum> = 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(
|
pub async fn evaluate_video_source(
|
||||||
Path((source_type, id)): Path<(String, i32)>,
|
Path((source_type, id)): Path<(String, i32)>,
|
||||||
Extension(db): Extension<DatabaseConnection>,
|
Extension(db): Extension<DatabaseConnection>,
|
||||||
|
|||||||
@@ -217,6 +217,10 @@ class ApiClient {
|
|||||||
return this.put<UpdateVideoSourceResponse>(`/video-sources/${type}/${id}`, request);
|
return this.put<UpdateVideoSourceResponse>(`/video-sources/${type}/${id}`, request);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async removeVideoSource(type: string, id: number): Promise<ApiResponse<boolean>> {
|
||||||
|
return this.request<boolean>(`/video-sources/${type}/${id}`, 'DELETE');
|
||||||
|
}
|
||||||
|
|
||||||
async evaluateVideoSourceRules(type: string, id: number): Promise<ApiResponse<boolean>> {
|
async evaluateVideoSourceRules(type: string, id: number): Promise<ApiResponse<boolean>> {
|
||||||
return this.post<boolean>(`/video-sources/${type}/${id}/evaluate`, null);
|
return this.post<boolean>(`/video-sources/${type}/${id}/evaluate`, null);
|
||||||
}
|
}
|
||||||
@@ -270,6 +274,7 @@ const api = {
|
|||||||
getVideoSourcesDetails: () => apiClient.getVideoSourcesDetails(),
|
getVideoSourcesDetails: () => apiClient.getVideoSourcesDetails(),
|
||||||
updateVideoSource: (type: string, id: number, request: UpdateVideoSourceRequest) =>
|
updateVideoSource: (type: string, id: number, request: UpdateVideoSourceRequest) =>
|
||||||
apiClient.updateVideoSource(type, id, request),
|
apiClient.updateVideoSource(type, id, request),
|
||||||
|
removeVideoSource: (type: string, id: number) => apiClient.removeVideoSource(type, id),
|
||||||
evaluateVideoSourceRules: (type: string, id: number) =>
|
evaluateVideoSourceRules: (type: string, id: number) =>
|
||||||
apiClient.evaluateVideoSourceRules(type, id),
|
apiClient.evaluateVideoSourceRules(type, id),
|
||||||
getDefaultPath: (type: string, name: string) => apiClient.getDefaultPath(type, name),
|
getDefaultPath: (type: string, name: string) => apiClient.getDefaultPath(type, name),
|
||||||
|
|||||||
@@ -14,6 +14,7 @@
|
|||||||
import ClockIcon from '@lucide/svelte/icons/clock';
|
import ClockIcon from '@lucide/svelte/icons/clock';
|
||||||
import PlusIcon from '@lucide/svelte/icons/plus';
|
import PlusIcon from '@lucide/svelte/icons/plus';
|
||||||
import InfoIcon from '@lucide/svelte/icons/info';
|
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 * as Tooltip from '$lib/components/ui/tooltip/index.js';
|
||||||
import { toast } from 'svelte-sonner';
|
import { toast } from 'svelte-sonner';
|
||||||
import { setBreadcrumb } from '$lib/stores/breadcrumb';
|
import { setBreadcrumb } from '$lib/stores/breadcrumb';
|
||||||
@@ -45,6 +46,13 @@
|
|||||||
let evaluateType = '';
|
let evaluateType = '';
|
||||||
let evaluating = false;
|
let evaluating = false;
|
||||||
|
|
||||||
|
// 删除对话框状态
|
||||||
|
let showRemoveDialog = false;
|
||||||
|
let removeSource: VideoSourceDetail | null = null;
|
||||||
|
let removeType = '';
|
||||||
|
let removeIdx: number = 0;
|
||||||
|
let removing = false;
|
||||||
|
|
||||||
// 编辑表单数据
|
// 编辑表单数据
|
||||||
let editForm = {
|
let editForm = {
|
||||||
path: '',
|
path: '',
|
||||||
@@ -100,6 +108,13 @@
|
|||||||
showEvaluateDialog = true;
|
showEvaluateDialog = true;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function openRemoveDialog(type: string, source: VideoSourceDetail, idx: number) {
|
||||||
|
removeSource = source;
|
||||||
|
removeType = type;
|
||||||
|
removeIdx = idx;
|
||||||
|
showRemoveDialog = true;
|
||||||
|
}
|
||||||
|
|
||||||
// 保存编辑
|
// 保存编辑
|
||||||
async function saveEdit() {
|
async function saveEdit() {
|
||||||
if (!editingSource) return;
|
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[] {
|
function getSourcesForTab(tabValue: string): VideoSourceDetail[] {
|
||||||
if (!videoSourcesData) return [];
|
if (!videoSourcesData) return [];
|
||||||
return videoSourcesData[tabValue as keyof VideoSourcesDetailsResponse] as VideoSourceDetail[];
|
return videoSourcesData[tabValue as keyof VideoSourcesDetailsResponse] as VideoSourceDetail[];
|
||||||
@@ -342,6 +384,17 @@
|
|||||||
>
|
>
|
||||||
<ListRestartIcon class="h-3 w-3" />
|
<ListRestartIcon class="h-3 w-3" />
|
||||||
</Button>
|
</Button>
|
||||||
|
{#if activeTab !== 'watch_later'}
|
||||||
|
<Button
|
||||||
|
size="sm"
|
||||||
|
variant="outline"
|
||||||
|
onclick={() => openRemoveDialog(key, source, index)}
|
||||||
|
class="h-8 w-8 p-0"
|
||||||
|
title="删除"
|
||||||
|
>
|
||||||
|
<TrashIcon2 class="h-3 w-3" />
|
||||||
|
</Button>
|
||||||
|
{/if}
|
||||||
</Table.Cell>
|
</Table.Cell>
|
||||||
</Table.Row>
|
</Table.Row>
|
||||||
{/each}
|
{/each}
|
||||||
@@ -471,6 +524,31 @@
|
|||||||
</AlertDialog.Content>
|
</AlertDialog.Content>
|
||||||
</AlertDialog.Root>
|
</AlertDialog.Root>
|
||||||
|
|
||||||
|
<AlertDialog.Root bind:open={showRemoveDialog}>
|
||||||
|
<AlertDialog.Content>
|
||||||
|
<AlertDialog.Header>
|
||||||
|
<AlertDialog.Title>删除视频源</AlertDialog.Title>
|
||||||
|
<AlertDialog.Description>
|
||||||
|
确定要删除视频源 <strong>"{removeSource?.name}"</strong> 吗?<br />
|
||||||
|
删除后该视频源相关的所有条目将从数据库中移除(不影响磁盘文件),该操作<span
|
||||||
|
class="text-destructive font-medium">无法撤销</span
|
||||||
|
>。<br />
|
||||||
|
</AlertDialog.Description>
|
||||||
|
</AlertDialog.Header>
|
||||||
|
<AlertDialog.Footer>
|
||||||
|
<AlertDialog.Cancel
|
||||||
|
disabled={removing}
|
||||||
|
onclick={() => {
|
||||||
|
showRemoveDialog = false;
|
||||||
|
}}>取消</AlertDialog.Cancel
|
||||||
|
>
|
||||||
|
<AlertDialog.Action onclick={removeVideoSource} disabled={removing}>
|
||||||
|
{removing ? '删除中' : '删除'}
|
||||||
|
</AlertDialog.Action>
|
||||||
|
</AlertDialog.Footer>
|
||||||
|
</AlertDialog.Content>
|
||||||
|
</AlertDialog.Root>
|
||||||
|
|
||||||
<!-- 添加对话框 -->
|
<!-- 添加对话框 -->
|
||||||
<Dialog.Root bind:open={showAddDialog}>
|
<Dialog.Root bind:open={showAddDialog}>
|
||||||
<Dialog.Content>
|
<Dialog.Content>
|
||||||
|
|||||||
Reference in New Issue
Block a user