feat(download): 未识别资源确认后继续下载 (#683)

Co-authored-by: liulang <liulang@25qp.cn>
This commit is contained in:
fliu2476
2026-08-19 13:41:08 +08:00
committed by GitHub
co-authored by liulang
parent 3b758ea543
commit 104a314c4c
5 changed files with 115 additions and 4 deletions
+33 -4
View File
@@ -1,6 +1,6 @@
<script setup lang="ts">
import { useToast } from 'vue-toastification'
import api from '@/api'
import api, { isApiBusinessFailure, isApiResponse } from '@/api'
import { doneNProgress, startNProgress } from '@/api/nprogress'
import type {
DownloaderConf,
@@ -16,6 +16,11 @@ import { useI18n } from 'vue-i18n'
import MediaIdSelector from '../misc/MediaIdSelector.vue'
import { isMediaDataSource, isMusicMediaSource, isValidMediaSourceId } from '@/utils/mediaId'
import { useGlobalSettingsStore } from '@/stores'
import { useConfirm } from '@/composables/useConfirm'
interface DownloadAddedData {
requires_confirmation?: boolean
}
// 多语言支持
const { t } = useI18n()
@@ -48,6 +53,7 @@ const emit = defineEmits(['done', 'error', 'close'])
// 提示框
const $toast = useToast()
const createConfirm = useConfirm()
// 选择的下载器
const selectedDownloader = ref<string | null>(null)
@@ -201,6 +207,7 @@ async function addDownload() {
media_in?: MediaInfo
media_source?: MediaDataSource
music_type?: Exclude<MusicEntityType, 'artist'>
allow_unrecognized?: boolean
save_path: string | null
torrent_in: TorrentInfo | undefined
} = {
@@ -222,7 +229,28 @@ async function addDownload() {
const endpoint = props.media ? 'download/' : 'download/add'
await api.post<null>(endpoint, payload, { feedback: 'silent' })
try {
await api.post<null>(endpoint, payload, { feedback: 'silent' })
} catch (error) {
if (
!isApiBusinessFailure(error) ||
!isApiResponse<DownloadAddedData>(error.payload) ||
error.payload.data?.requires_confirmation !== true
) {
throw error
}
const confirmed = await createConfirm({
type: 'warn',
title: t('dialog.addDownload.unrecognizedTitle'),
content: t('dialog.addDownload.unrecognizedContent'),
confirmText: t('dialog.addDownload.continueDownload'),
})
if (!confirmed) return
payload.allow_unrecognized = true
await api.post<null>(endpoint, payload, { feedback: 'silent' })
}
// 添加下载成功
$toast.success(
@@ -241,9 +269,10 @@ async function addDownload() {
}),
)
emit('error', message)
} finally {
loading.value = false
doneNProgress()
}
loading.value = false
doneNProgress()
}
onMounted(() => {
@@ -11,6 +11,7 @@ import { beforeEach, describe, expect, it, vi } from 'vitest'
const API_BASE_URL = 'http://localhost/api/v1/'
const mocks = vi.hoisted(() => ({
confirm: vi.fn(),
doneNProgress: vi.fn(),
startNProgress: vi.fn(),
toastError: vi.fn(),
@@ -27,6 +28,10 @@ vi.mock('vue-toastification', () => ({
useToast: () => ({ error: mocks.toastError, success: mocks.toastSuccess }),
}))
vi.mock('@/composables/useConfirm', () => ({
useConfirm: () => mocks.confirm,
}))
type SelectItem = string | { title: string; value: string }
const SelectStub = defineComponent({
@@ -297,6 +302,7 @@ describe('AddDownloadDialog directories', () => {
describe('AddDownloadDialog submissions', () => {
beforeEach(() => {
vi.clearAllMocks()
mocks.confirm.mockResolvedValue(false)
vi.spyOn(console, 'error').mockImplementation(() => {})
vi.spyOn(console, 'log').mockImplementation(() => {})
})
@@ -441,6 +447,72 @@ describe('AddDownloadDialog submissions', () => {
expect(mocks.doneNProgress).toHaveBeenCalledOnce()
})
it('retries an unrecognized download after confirmation', async () => {
const submitted: Array<Record<string, unknown>> = []
server.use(
http.post(new URL('download/add', API_BASE_URL).href, async ({ request }) => {
submitted.push((await request.json()) as Record<string, unknown>)
if (submitted.length === 1) {
return HttpResponse.json({
data: { requires_confirmation: true },
message: '无法识别媒体信息',
success: false,
})
}
return HttpResponse.json({ data: { download_id: 'collection-download' }, success: true })
}),
)
mocks.confirm.mockResolvedValue(true)
const user = userEvent.setup()
const { events, torrent } = await renderDialog()
await user.click(screen.getByRole('button', { name: '开始下载' }))
await waitFor(() => expect(events.done).toHaveBeenCalledWith(torrent.enclosure))
expect(mocks.confirm).toHaveBeenCalledWith({
type: 'warn',
title: '无法识别媒体信息',
content: '无法识别此资源的媒体信息,是否仍要下载?',
confirmText: '继续下载',
})
expect(submitted).toHaveLength(2)
expect(submitted[0]).not.toHaveProperty('allow_unrecognized')
expect(submitted[1]).toEqual({
...submitted[0],
allow_unrecognized: true,
})
expect(events.error).not.toHaveBeenCalled()
expect(mocks.toastError).not.toHaveBeenCalled()
expect(mocks.toastSuccess).toHaveBeenCalledWith('测试站 测试种子 下载成功!')
expect(mocks.startNProgress).toHaveBeenCalledOnce()
expect(mocks.doneNProgress).toHaveBeenCalledOnce()
})
it('keeps the download dialog open when unrecognized download confirmation is cancelled', async () => {
const submitted = vi.fn()
server.use(
downloadHandler(
'download/add',
{ data: { requires_confirmation: true }, message: '无法识别媒体信息', success: false },
200,
submitted,
),
)
const user = userEvent.setup()
const { events } = await renderDialog()
await user.click(screen.getByRole('button', { name: '开始下载' }))
await waitFor(() => expect(mocks.confirm).toHaveBeenCalledOnce())
expect(submitted).toHaveBeenCalledOnce()
expect(events.done).not.toHaveBeenCalled()
expect(events.error).not.toHaveBeenCalled()
expect(mocks.toastSuccess).not.toHaveBeenCalled()
expect(mocks.toastError).not.toHaveBeenCalled()
expect(mocks.doneNProgress).toHaveBeenCalledOnce()
expect(screen.getByRole('button', { name: '开始下载' })).not.toBeDisabled()
})
it('treats success:false at HTTP 200 as a business failure', async () => {
server.use(downloadHandler('download/add', { data: null, message: '下载器拒绝任务', success: false }))
const user = userEvent.setup()
@@ -450,6 +522,7 @@ describe('AddDownloadDialog submissions', () => {
await waitFor(() => expect(events.error).toHaveBeenCalledWith('下载器拒绝任务'))
expect(events.done).not.toHaveBeenCalled()
expect(mocks.confirm).not.toHaveBeenCalled()
expect(mocks.toastError).toHaveBeenCalledWith('测试站 测试种子 下载失败:下载器拒绝任务!')
expect(mocks.startNProgress).toHaveBeenCalledOnce()
expect(mocks.doneNProgress).toHaveBeenCalledOnce()
+3
View File
@@ -3012,6 +3012,9 @@ export default {
startDownload: 'Start Download',
downloadSuccess: '{site} {title} downloaded successfully!',
downloadFailed: '{site} {title} download failed: {message}!',
unrecognizedTitle: 'Media Information Not Recognized',
unrecognizedContent: 'Media information for this resource could not be recognized. Download it anyway?',
continueDownload: 'Continue Download',
showAdvancedOptions: 'Show Advanced Options',
hideAdvancedOptions: 'Hide Advanced Options',
},
+3
View File
@@ -2960,6 +2960,9 @@ export default {
startDownload: '开始下载',
downloadSuccess: '{site} {title} 下载成功!',
downloadFailed: '{site} {title} 下载失败:{message}',
unrecognizedTitle: '无法识别媒体信息',
unrecognizedContent: '无法识别此资源的媒体信息,是否仍要下载?',
continueDownload: '继续下载',
showAdvancedOptions: '显示高级选项',
hideAdvancedOptions: '隐藏高级选项',
},
+3
View File
@@ -2959,6 +2959,9 @@ export default {
startDownload: '開始下載',
downloadSuccess: '{site} {title} 下載成功!',
downloadFailed: '{site} {title} 下載失敗:{message}',
unrecognizedTitle: '無法識別媒體資訊',
unrecognizedContent: '無法識別此資源的媒體資訊,是否仍要下載?',
continueDownload: '繼續下載',
showAdvancedOptions: '顯示高級選項',
hideAdvancedOptions: '隱藏高級選項',
},