feat: 重置任务状态时支持 force 参数,默认不启用 (#388)

This commit is contained in:
ᴀᴍᴛᴏᴀᴇʀ
2025-07-11 19:01:01 +08:00
committed by GitHub
parent 267e9373f9
commit adc2e32e58
14 changed files with 185 additions and 57 deletions

View File

@@ -2,6 +2,7 @@ use serde::Deserialize;
use validator::Validate;
use crate::bilibili::CollectionType;
#[derive(Deserialize)]
pub struct VideosRequest {
pub collection: Option<i32>,
@@ -13,6 +14,12 @@ pub struct VideosRequest {
pub page_size: Option<u64>,
}
#[derive(Deserialize)]
pub struct ResetRequest {
#[serde(default)]
pub force: bool,
}
#[derive(Deserialize, Validate)]
pub struct StatusUpdate {
#[validate(range(min = 0, max = 4))]

View File

@@ -2,9 +2,9 @@ use std::collections::HashSet;
use std::sync::Arc;
use anyhow::Result;
use axum::Router;
use axum::extract::{Extension, Path, Query};
use axum::routing::{get, post};
use axum::{Json, Router};
use bili_sync_entity::*;
use sea_orm::{
ColumnTrait, DatabaseConnection, EntityTrait, PaginatorTrait, QueryFilter, QueryOrder, TransactionTrait,
@@ -12,7 +12,7 @@ use sea_orm::{
use crate::api::error::InnerApiError;
use crate::api::helper::{update_page_download_status, update_video_download_status};
use crate::api::request::{UpdateVideoStatusRequest, VideosRequest};
use crate::api::request::{ResetRequest, UpdateVideoStatusRequest, VideosRequest};
use crate::api::response::{
PageInfo, ResetAllVideosResponse, ResetVideoResponse, UpdateVideoStatusResponse, VideoInfo, VideoResponse,
VideosResponse,
@@ -91,6 +91,7 @@ pub async fn get_video(
pub async fn reset_video(
Path(id): Path<i32>,
Extension(db): Extension<Arc<DatabaseConnection>>,
Json(request): Json<ResetRequest>,
) -> Result<ApiResponse<ResetVideoResponse>, ApiError> {
let (video_info, pages_info) = tokio::try_join!(
video::Entity::find_by_id(id)
@@ -109,7 +110,7 @@ pub async fn reset_video(
.into_iter()
.filter_map(|mut page_info| {
let mut page_status = PageStatus::from(page_info.download_status);
if page_status.reset_failed() {
if (request.force && page_status.force_reset_failed()) || page_status.reset_failed() {
page_info.download_status = page_status.into();
Some(page_info)
} else {
@@ -118,9 +119,9 @@ pub async fn reset_video(
})
.collect::<Vec<_>>();
let mut video_status = VideoStatus::from(video_info.download_status);
let mut video_resetted = video_status.reset_failed();
let mut video_resetted = (request.force && video_status.force_reset_failed()) || video_status.reset_failed();
if !resetted_pages_info.is_empty() {
video_status.set(4, 0); // 将“分P下载”重置为 0
video_status.set(4, 0); // 将“分下载”重置为 0
video_resetted = true;
}
let resetted_videos_info = if video_resetted {
@@ -150,6 +151,7 @@ pub async fn reset_video(
pub async fn reset_all_videos(
Extension(db): Extension<Arc<DatabaseConnection>>,
Json(request): Json<ResetRequest>,
) -> Result<ApiResponse<ResetAllVideosResponse>, ApiError> {
// 先查询所有视频和页面数据
let (all_videos, all_pages) = tokio::try_join!(
@@ -160,7 +162,7 @@ pub async fn reset_all_videos(
.into_iter()
.filter_map(|mut page_info| {
let mut page_status = PageStatus::from(page_info.download_status);
if page_status.reset_failed() {
if (request.force && page_status.force_reset_failed()) || page_status.reset_failed() {
page_info.download_status = page_status.into();
Some(page_info)
} else {
@@ -173,9 +175,10 @@ pub async fn reset_all_videos(
.into_iter()
.filter_map(|mut video_info| {
let mut video_status = VideoStatus::from(video_info.download_status);
let mut video_resetted = video_status.reset_failed();
let mut video_resetted =
(request.force && video_status.force_reset_failed()) || video_status.reset_failed();
if video_ids_with_resetted_pages.contains(&video_info.id) {
video_status.set(4, 0); // 将"分P下载"重置为 0
video_status.set(4, 0); // 将"分下载"重置为 0
video_resetted = true;
}
if video_resetted {

View File

@@ -39,7 +39,15 @@ impl<const N: usize> Status<N> {
changed = true;
}
}
// 理论上 changed 可以直接从上面的循环中得到,因为 completed 标志位的改变是由子任务状态的改变引起的,子任务没有改变则 completed 也不会改变
changed
}
/// 重置所有失败的状态,将状态设置为 0b000返回值表示 status 是否发生了变化
/// force 版本在普通版本的基础上,会额外检查是否存在需要运行的任务,如果存在则修正 completed 标记位为“未完成”
/// 这个方法的典型用例是在引入新的任务状态后重置历史视频,允许历史视频执行新引入的任务
pub fn force_reset_failed(&mut self) -> bool {
let mut changed = self.reset_failed();
// 理论上上面的 changed 就足够了,因为 completed 标志位的改变是由子任务状态的改变引起的,子任务没有改变则 completed 也不会改变
// 但考虑特殊情况,新版本引入了一个新的子任务项,此时会出现明明有子任务未执行,但 completed 标记位仍然为 true 的情况
// 当然可以在新版本迁移文件中全局重置 completed 标记位,但这样影响范围太大感觉不太好
// 在后面进行这部分额外判断可以兼容这种情况,在由用户手动触发的 reset_failed 调用中修正 completed 标记位
@@ -161,7 +169,7 @@ impl<const N: usize> From<[u32; N]> for Status<N> {
}
}
/// 包含五个子任务从前到后依次是视频封面、视频信息、Up 主头像、Up 主信息、分 P 下载
/// 包含五个子任务从前到后依次是视频封面、视频信息、Up 主头像、Up 主信息、分下载
pub type VideoStatus = Status<5>;
/// 包含五个子任务,从前到后分别是:视频封面、视频内容、视频信息、视频弹幕、视频字幕
@@ -233,10 +241,13 @@ mod test {
assert!(status.reset_failed());
assert!(!status.get_completed());
assert_eq!(<[u32; 3]>::from(status), [3, 0, 7]);
// 没有内容需要重置,但 completed 标记位是错误的(模拟新增一个子任务状态的情况),此时 reset_failed 会修正 completed 标记位
// 没有内容需要重置,但 completed 标记位是错误的(模拟新增一个子任务状态的情况)
// 此时 reset_failed 不会修正 completed 标记位,而 force_reset_failed 会
status.set_completed(true);
assert!(status.get_completed());
assert!(status.reset_failed());
assert!(!status.reset_failed());
assert!(status.get_completed());
assert!(status.force_reset_failed());
assert!(!status.get_completed());
// 重置一个已经成功的任务,没有改变状态,也不会修改标记位
let mut status = Status::<3>::from([7, 7, 7]);

View File

@@ -256,7 +256,7 @@ pub async fn download_video_pages(
&video_model,
base_upper_path.join("person.nfo"),
),
// 分发并执行分 P 下载的任务
// 分发并执行分下载的任务
dispatch_download_page(
separate_status[4],
bili_client,
@@ -332,10 +332,10 @@ pub async fn dispatch_download_page(
.take_while(|res| {
match res {
Ok(model) => {
// 该视频的所有分页的下载状态都会在此返回,需要根据这些状态确认视频层“分 P 下载”子任务的状态
// 该视频的所有分页的下载状态都会在此返回,需要根据这些状态确认视频层“分下载”子任务的状态
// 在过去的实现中,此处仅仅根据 page_download_status 的最高标志位来判断,如果最高标志位是 true 则认为完成
// 这样会导致即使分页中有失败到 MAX_RETRY 的情况,视频层的分 P 下载状态也会被认为是 Succeeded不够准确
// 新版本实现会将此处取值为所有子任务状态的最小值,这样只有所有分页的子任务全部成功时才会认为视频层的分 P 下载状态是 Succeeded
// 这样会导致即使分页中有失败到 MAX_RETRY 的情况,视频层的分下载状态也会被认为是 Succeeded不够准确
// 新版本实现会将此处取值为所有子任务状态的最小值,这样只有所有分页的子任务全部成功时才会认为视频层的分下载状态是 Succeeded
let page_download_status = model.download_status.try_as_ref().expect("download_status must be set");
let separate_status: [u32; 5] = PageStatus::from(*page_download_status).into();
for status in separate_status {

View File

@@ -10,7 +10,7 @@
"@lucide/svelte": "^0.525.0",
"@sveltejs/adapter-static": "^3.0.8",
"@sveltejs/kit": "2.22.2",
"@sveltejs/vite-plugin-svelte": "^5.0.0",
"@sveltejs/vite-plugin-svelte": "^6.0.0",
"@tailwindcss/forms": "^0.5.9",
"@tailwindcss/typography": "^0.5.15",
"@tailwindcss/vite": "^4.0.0",
@@ -211,9 +211,9 @@
"@sveltejs/kit": ["@sveltejs/kit@2.22.2", "", { "dependencies": { "@sveltejs/acorn-typescript": "^1.0.5", "@types/cookie": "^0.6.0", "acorn": "^8.14.1", "cookie": "^0.6.0", "devalue": "^5.1.0", "esm-env": "^1.2.2", "kleur": "^4.1.5", "magic-string": "^0.30.5", "mrmime": "^2.0.0", "sade": "^1.8.1", "set-cookie-parser": "^2.6.0", "sirv": "^3.0.0", "vitefu": "^1.0.6" }, "peerDependencies": { "@sveltejs/vite-plugin-svelte": "^3.0.0 || ^4.0.0-next.1 || ^5.0.0 || ^6.0.0-next.0", "svelte": "^4.0.0 || ^5.0.0-next.0", "vite": "^5.0.3 || ^6.0.0 || ^7.0.0-beta.0" }, "bin": { "svelte-kit": "svelte-kit.js" } }, "sha512-2MvEpSYabUrsJAoq5qCOBGAlkICjfjunrnLcx3YAk2XV7TvAIhomlKsAgR4H/4uns5rAfYmj7Wet5KRtc8dPIg=="],
"@sveltejs/vite-plugin-svelte": ["@sveltejs/vite-plugin-svelte@5.0.3", "", { "dependencies": { "@sveltejs/vite-plugin-svelte-inspector": "^4.0.1", "debug": "^4.4.0", "deepmerge": "^4.3.1", "kleur": "^4.1.5", "magic-string": "^0.30.15", "vitefu": "^1.0.4" }, "peerDependencies": { "svelte": "^5.0.0", "vite": "^6.0.0" } }, "sha512-MCFS6CrQDu1yGwspm4qtli0e63vaPCehf6V7pIMP15AsWgMKrqDGCPFF/0kn4SP0ii4aySu4Pa62+fIRGFMjgw=="],
"@sveltejs/vite-plugin-svelte": ["@sveltejs/vite-plugin-svelte@6.0.0", "", { "dependencies": { "@sveltejs/vite-plugin-svelte-inspector": "^5.0.0-next.1", "debug": "^4.4.1", "deepmerge": "^4.3.1", "kleur": "^4.1.5", "magic-string": "^0.30.17", "vitefu": "^1.1.1" }, "peerDependencies": { "svelte": "^5.0.0", "vite": "^6.3.0 || ^7.0.0" } }, "sha512-mma5GJ23pYiWpTNbN//g9XI3Hfob3aAlXPP42qRtvjgTAU6pfJyLyNPTdLjFuj+jfC9JslP4J3AkeiJNhjtLLA=="],
"@sveltejs/vite-plugin-svelte-inspector": ["@sveltejs/vite-plugin-svelte-inspector@4.0.1", "", { "dependencies": { "debug": "^4.3.7" }, "peerDependencies": { "@sveltejs/vite-plugin-svelte": "^5.0.0", "svelte": "^5.0.0", "vite": "^6.0.0" } }, "sha512-J/Nmb2Q2y7mck2hyCX4ckVHcR5tu2J+MtBEQqpDrrgELZ2uvraQcK/ioCV61AqkdXFgriksOKIceDcQmqnGhVw=="],
"@sveltejs/vite-plugin-svelte-inspector": ["@sveltejs/vite-plugin-svelte-inspector@5.0.0", "", { "dependencies": { "debug": "^4.4.1" }, "peerDependencies": { "@sveltejs/vite-plugin-svelte": "^6.0.0-next.0", "svelte": "^5.0.0", "vite": "^6.3.0 || ^7.0.0" } }, "sha512-iwQ8Z4ET6ZFSt/gC+tVfcsSBHwsqc6RumSaiLUkAurW3BCpJam65cmHw0oOlDMTO0u+PZi9hilBRYN+LZNHTUQ=="],
"@swc/helpers": ["@swc/helpers@0.5.17", "", { "dependencies": { "tslib": "^2.8.0" } }, "sha512-5IKx/Y13RsYd+sauPb2x+U/xZikHjolzfuDgTAl/Tdf3Q8rslRvC19NKDLgAJQ6wsqADk10ntlv08nPFw/gO/A=="],
@@ -707,6 +707,8 @@
"@humanfs/node/@humanwhocodes/retry": ["@humanwhocodes/retry@0.3.1", "", {}, "sha512-JBxkERygn7Bv/GbN5Rv8Ul6LVknS+5Bp6RgDC/O8gEBU/yeH5Ui5C/OlWrTb6qct7LjjfT6Re2NxB0ln0yYybA=="],
"@sveltejs/vite-plugin-svelte/vitefu": ["vitefu@1.1.1", "", { "peerDependencies": { "vite": "^3.0.0 || ^4.0.0 || ^5.0.0 || ^6.0.0 || ^7.0.0-beta.0" }, "optionalPeers": ["vite"] }, "sha512-B/Fegf3i8zh0yFbpzZ21amWzHmuNlLlmJT6n7bu5e+pCHUKQIfXSYokrqOBGEMMe9UG2sostKQF9mml/vYaWJQ=="],
"@tailwindcss/oxide-wasm32-wasi/@emnapi/core": ["@emnapi/core@1.4.3", "", { "dependencies": { "@emnapi/wasi-threads": "1.0.2", "tslib": "^2.4.0" }, "bundled": true }, "sha512-4m62DuCE07lw01soJwPiBGC0nAww0Q+RY70VZ+n49yDIO13yyinhbWCeNnaob0lakDtWQzSdtNWzJeOJt2ma+g=="],
"@tailwindcss/oxide-wasm32-wasi/@emnapi/runtime": ["@emnapi/runtime@1.4.3", "", { "dependencies": { "tslib": "^2.4.0" }, "bundled": true }, "sha512-pBPWdu6MLKROBX05wSNKcNb++m5Er+KQ9QkB+WVM+pW2Kx9hoSrVTnu3BdkI5eBLZoKu/J6mW/B6i6bJB2ytXQ=="],

View File

@@ -8,7 +8,7 @@
"@lucide/svelte": "^0.525.0",
"@sveltejs/adapter-static": "^3.0.8",
"@sveltejs/kit": "2.22.2",
"@sveltejs/vite-plugin-svelte": "^5.0.0",
"@sveltejs/vite-plugin-svelte": "^6.0.0",
"@tailwindcss/forms": "^0.5.9",
"@tailwindcss/typography": "^0.5.15",
"@tailwindcss/vite": "^4.0.0",

View File

@@ -20,7 +20,8 @@ import type {
Config,
DashBoardResponse,
SysInfo,
TaskStatus
TaskStatus,
ResetRequest
} from './types';
import { wsManager } from './ws';
@@ -150,12 +151,12 @@ class ApiClient {
return this.get<VideoResponse>(`/videos/${id}`);
}
async resetVideo(id: number): Promise<ApiResponse<ResetVideoResponse>> {
return this.post<ResetVideoResponse>(`/videos/${id}/reset`);
async resetVideo(id: number, request: ResetRequest): Promise<ApiResponse<ResetVideoResponse>> {
return this.post<ResetVideoResponse>(`/videos/${id}/reset`, request);
}
async resetAllVideos(): Promise<ApiResponse<ResetAllVideosResponse>> {
return this.post<ResetAllVideosResponse>('/videos/reset-all');
async resetAllVideos(request: ResetRequest): Promise<ApiResponse<ResetAllVideosResponse>> {
return this.post<ResetAllVideosResponse>('/videos/reset-all', request);
}
async updateVideoStatus(
@@ -245,8 +246,8 @@ const api = {
getVideoSources: () => apiClient.getVideoSources(),
getVideos: (params?: VideosRequest) => apiClient.getVideos(params),
getVideo: (id: number) => apiClient.getVideo(id),
resetVideo: (id: number) => apiClient.resetVideo(id),
resetAllVideos: () => apiClient.resetAllVideos(),
resetVideo: (id: number, request: ResetRequest) => apiClient.resetVideo(id, request),
resetAllVideos: (request: ResetRequest) => apiClient.resetAllVideos(request),
updateVideoStatus: (id: number, request: UpdateVideoStatusRequest) =>
apiClient.updateVideoStatus(id, request),
getCreatedFavorites: () => apiClient.getCreatedFavorites(),

View File

@@ -19,7 +19,7 @@
export let onsubmit: (request: UpdateVideoStatusRequest) => void;
// 视频任务名称(与后端 VideoStatus 对应)
const videoTaskNames = ['视频封面', '视频信息', 'UP主头像', 'UP主信息', '分P下载'];
const videoTaskNames = ['视频封面', '视频信息', 'UP主头像', 'UP主信息', '分下载'];
// 分页任务名称(与后端 PageStatus 对应)
const pageTaskNames = ['视频封面', '视频内容', '视频信息', '视频弹幕', '视频字幕'];
@@ -169,9 +169,9 @@
<SheetHeader class="px-6 pb-2">
<SheetTitle class="text-lg">编辑状态</SheetTitle>
<SheetDescription class="text-muted-foreground space-y-1 text-sm">
<div>修改视频和分页的下载状态。可将任务重置为未开始状态,或者标记为已完成。</div>
<div class="font-medium text-rose-600">
⚠️ 已完成任务被重置为未开始,任务重新执行时会覆盖现存文件
<div>自行编辑视频和分页的下载状态。可将任意子任务状态修改为“未开始”或“已完成</div>
<div class="leading-relaxed text-orange-600">
⚠️ 仅当分页下载状态不是“已完成”时,程序才会尝试执行分页下载
</div>
</SheetDescription>
</SheetHeader>

View File

@@ -0,0 +1,36 @@
<script lang="ts">
import { Checkbox as CheckboxPrimitive } from 'bits-ui';
import CheckIcon from '@lucide/svelte/icons/check';
import MinusIcon from '@lucide/svelte/icons/minus';
import { cn, type WithoutChildrenOrChild } from '$lib/utils.js';
let {
ref = $bindable(null),
checked = $bindable(false),
indeterminate = $bindable(false),
class: className,
...restProps
}: WithoutChildrenOrChild<CheckboxPrimitive.RootProps> = $props();
</script>
<CheckboxPrimitive.Root
bind:ref
data-slot="checkbox"
class={cn(
'border-input dark:bg-input/30 data-[state=checked]:bg-primary data-[state=checked]:text-primary-foreground dark:data-[state=checked]:bg-primary data-[state=checked]:border-primary focus-visible:border-ring focus-visible:ring-ring/50 aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive peer flex size-4 shrink-0 items-center justify-center rounded-[4px] border shadow-xs transition-shadow outline-none focus-visible:ring-[3px] disabled:cursor-not-allowed disabled:opacity-50',
className
)}
bind:checked
bind:indeterminate
{...restProps}
>
{#snippet children({ checked, indeterminate })}
<div data-slot="checkbox-indicator" class="text-current transition-none">
{#if checked}
<CheckIcon class="size-3.5" />
{:else if indeterminate}
<MinusIcon class="size-3.5" />
{/if}
</div>
{/snippet}
</CheckboxPrimitive.Root>

View File

@@ -0,0 +1,6 @@
import Root from './checkbox.svelte';
export {
Root,
//
Root as Checkbox
};

View File

@@ -4,6 +4,8 @@
import { Button } from '$lib/components/ui/button/index.js';
import * as AlertDialog from '$lib/components/ui/alert-dialog/index.js';
import * as DropdownMenu from '$lib/components/ui/dropdown-menu/index.js';
import { Checkbox } from '$lib/components/ui/checkbox/index.js';
import { Label } from '$lib/components/ui/label/index.js';
import type { VideoInfo } from '$lib/types';
import RotateCcwIcon from '@lucide/svelte/icons/rotate-ccw';
import InfoIcon from '@lucide/svelte/icons/info';
@@ -21,10 +23,12 @@
export let customSubtitle: string = ''; // 自定义副标题
export let taskNames: string[] = []; // 自定义任务名称
export let showProgress: boolean = true; // 是否显示进度信息
export let onReset: (() => Promise<void>) | null = null; // 自定义重置函数
export let onReset: ((forceReset: boolean) => Promise<void>) | null = null; // 自定义重置函数
export let resetDialogOpen = false; // 导出对话框状态,让父组件可以控制
export let resetting = false;
let forceReset = false;
function getStatusText(status: number): string {
if (status === 7) {
return '已完成';
@@ -66,7 +70,7 @@
if (taskNames.length > 0) {
return taskNames[index] || `任务${index + 1}`;
}
const defaultTaskNames = ['视频封面', '视频信息', 'UP主头像', 'UP主信息', '分P下载'];
const defaultTaskNames = ['视频封面', '视频信息', 'UP主头像', 'UP主信息', '分下载'];
return defaultTaskNames[index] || `任务${index + 1}`;
}
@@ -77,10 +81,11 @@
async function handleReset() {
resetting = true;
if (onReset) {
await onReset();
await onReset(forceReset);
}
resetting = false;
resetDialogOpen = false;
forceReset = false;
}
function handleViewDetail() {
@@ -202,16 +207,43 @@
<AlertDialog.Root bind:open={resetDialogOpen}>
<AlertDialog.Content>
<AlertDialog.Header>
<AlertDialog.Title>确认重置</AlertDialog.Title>
<AlertDialog.Title>重置视频</AlertDialog.Title>
<AlertDialog.Description>
确定要重置视频 "{displayTitle}"
的下载状态吗?此操作会将所有失败状态的下载状态重置为未开始,无法撤销。
确定要重置视频 <strong>"{displayTitle}"</strong> 的下载状态吗?
<br />
此操作会将所有的失败状态重置为未开始,<span class="text-destructive font-medium"
>无法撤销</span
>
</AlertDialog.Description>
</AlertDialog.Header>
<div class="space-y-4 py-4">
<div class="rounded-lg border border-orange-200 bg-orange-50 p-3">
<div class="mb-2 flex items-center space-x-2">
<Checkbox id="force-reset-all" bind:checked={forceReset} />
<Label for="force-reset-all" class="text-sm font-medium text-orange-700"
>⚠️ 强制重置</Label
>
</div>
<p class="text-xs leading-relaxed text-orange-700">
除重置失败状态外还会检查修复任务状态的标识位 <br />
版本升级引入新任务时勾选该选项进行重置,可以允许旧视频执行新任务
</p>
</div>
</div>
<AlertDialog.Footer>
<AlertDialog.Cancel>取消</AlertDialog.Cancel>
<AlertDialog.Action onclick={handleReset} disabled={resetting}>
{resetting ? '重置中...' : '确认重置'}
<AlertDialog.Cancel
onclick={() => {
forceReset = false;
}}>取消</AlertDialog.Cancel
>
<AlertDialog.Action
onclick={handleReset}
disabled={resetting}
class={forceReset ? 'bg-orange-600 hover:bg-orange-700' : ''}
>
{resetting ? '重置中...' : forceReset ? '确认强制重置' : '确认重置'}
</AlertDialog.Action>
</AlertDialog.Footer>
</AlertDialog.Content>

View File

@@ -104,6 +104,11 @@ export interface UpdateVideoStatusResponse {
pages: PageInfo[];
}
// 重置请求类型
export interface ResetRequest {
force: boolean;
}
// 收藏夹相关类型
export interface FavoriteWithSubscriptionStatus {
title: string;

View File

@@ -160,12 +160,12 @@
}}
mode="detail"
showActions={false}
taskNames={['视频封面', '视频信息', 'UP主头像', 'UP主信息', '分P下载']}
taskNames={['视频封面', '视频信息', 'UP主头像', 'UP主信息', '分下载']}
bind:resetDialogOpen
bind:resetting
onReset={async () => {
onReset={async (forceReset: boolean) => {
try {
const result = await api.resetVideo(videoData!.video.id);
const result = await api.resetVideo(videoData!.video.id, { force: forceReset });
const data = result.data;
if (data.resetted) {
videoData = {

View File

@@ -5,6 +5,8 @@
import * as AlertDialog from '$lib/components/ui/alert-dialog/index.js';
import RotateCcwIcon from '@lucide/svelte/icons/rotate-ccw';
import api from '$lib/api';
import { Checkbox } from '$lib/components/ui/checkbox/index.js';
import { Label } from '$lib/components/ui/label/index.js';
import type { VideosResponse, VideoSourcesResponse, ApiError, VideoSource } from '$lib/types';
import { onMount } from 'svelte';
import { page } from '$app/stores';
@@ -33,6 +35,8 @@
let resetAllDialogOpen = false;
let resettingAll = false;
let forceReset = false;
let videoSources: VideoSourcesResponse | null = null;
let filters: Record<string, Filter> | null = null;
@@ -91,9 +95,9 @@
loadVideos(query, pageNum, videoSource);
}
async function handleResetVideo(id: number) {
async function handleResetVideo(id: number, forceReset: boolean) {
try {
const result = await api.resetVideo(id);
const result = await api.resetVideo(id, { force: forceReset });
const data = result.data;
if (data.resetted) {
toast.success('重置成功', {
@@ -117,7 +121,7 @@
async function handleResetAllVideos() {
resettingAll = true;
try {
const result = await api.resetAllVideos();
const result = await api.resetAllVideos({ force: forceReset });
const data = result.data;
if (data.resetted) {
toast.success('重置成功', {
@@ -225,7 +229,7 @@
disabled={resettingAll || loading}
>
<RotateCcwIcon class="mr-1.5 h-3 w-3 {resettingAll ? 'animate-spin' : ''}" />
重置所有
重置全部
</Button>
</div>
</div>
@@ -242,8 +246,8 @@
{#each videosData.videos as video (video.id)}
<VideoCard
{video}
onReset={async () => {
await handleResetVideo(video.id);
onReset={async (forceReset: boolean) => {
await handleResetVideo(video.id, forceReset);
}}
/>
{/each}
@@ -264,29 +268,50 @@
</div>
{/if}
<!-- 重置所有视频确认对话框 -->
<AlertDialog.Root bind:open={resetAllDialogOpen}>
<AlertDialog.Content>
<AlertDialog.Header>
<AlertDialog.Title>重置所有视频</AlertDialog.Title>
<AlertDialog.Title>重置全部视频</AlertDialog.Title>
<AlertDialog.Description>
此操作将重置所有视频和分页的失败状态为未下载状态,使它们在下次下载任务中重新尝试。
<br />
<strong class="text-destructive">此操作不可撤销,确定要继续吗?</strong>
确定要重置<strong>全部视频</strong>的下载状态吗?<br />
此操作会将所有的失败状态重置为未开始,<span class="text-destructive font-medium"
>无法撤销</span
>
</AlertDialog.Description>
</AlertDialog.Header>
<div class="space-y-4 py-4">
<div class="rounded-lg border border-orange-200 bg-orange-50 p-3">
<div class="mb-2 flex items-center space-x-2">
<Checkbox id="force-reset-all" bind:checked={forceReset} />
<Label for="force-reset-all" class="text-sm font-medium text-orange-700"
>⚠️ 强制重置</Label
>
</div>
<p class="text-xs leading-relaxed text-orange-700">
除重置失败状态外还会检查修复任务状态的标识位 <br />
版本升级引入新任务时勾选该选项进行重置,可以允许旧视频执行新任务
</p>
</div>
</div>
<AlertDialog.Footer>
<AlertDialog.Cancel disabled={resettingAll}>取消</AlertDialog.Cancel>
<AlertDialog.Cancel
disabled={resettingAll}
onclick={() => {
forceReset = false;
}}>取消</AlertDialog.Cancel
>
<AlertDialog.Action
onclick={handleResetAllVideos}
disabled={resettingAll}
class="bg-destructive text-destructive-foreground hover:bg-destructive/90"
class={forceReset ? 'bg-orange-600 hover:bg-orange-700' : ''}
>
{#if resettingAll}
<RotateCcwIcon class="mr-2 h-4 w-4 animate-spin" />
重置中...
{:else}
确认重置
{forceReset ? '确认强制重置' : '确认重置'}
{/if}
</AlertDialog.Action>
</AlertDialog.Footer>