mirror of
https://github.com/jxxghp/MoviePilot-Frontend.git
synced 2026-09-05 23:56:42 +08:00
feat(download): 未识别资源确认后继续下载 (#683)
Co-authored-by: liulang <liulang@25qp.cn>
This commit is contained in:
@@ -1,6 +1,6 @@
|
|||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { useToast } from 'vue-toastification'
|
import { useToast } from 'vue-toastification'
|
||||||
import api from '@/api'
|
import api, { isApiBusinessFailure, isApiResponse } from '@/api'
|
||||||
import { doneNProgress, startNProgress } from '@/api/nprogress'
|
import { doneNProgress, startNProgress } from '@/api/nprogress'
|
||||||
import type {
|
import type {
|
||||||
DownloaderConf,
|
DownloaderConf,
|
||||||
@@ -16,6 +16,11 @@ import { useI18n } from 'vue-i18n'
|
|||||||
import MediaIdSelector from '../misc/MediaIdSelector.vue'
|
import MediaIdSelector from '../misc/MediaIdSelector.vue'
|
||||||
import { isMediaDataSource, isMusicMediaSource, isValidMediaSourceId } from '@/utils/mediaId'
|
import { isMediaDataSource, isMusicMediaSource, isValidMediaSourceId } from '@/utils/mediaId'
|
||||||
import { useGlobalSettingsStore } from '@/stores'
|
import { useGlobalSettingsStore } from '@/stores'
|
||||||
|
import { useConfirm } from '@/composables/useConfirm'
|
||||||
|
|
||||||
|
interface DownloadAddedData {
|
||||||
|
requires_confirmation?: boolean
|
||||||
|
}
|
||||||
|
|
||||||
// 多语言支持
|
// 多语言支持
|
||||||
const { t } = useI18n()
|
const { t } = useI18n()
|
||||||
@@ -48,6 +53,7 @@ const emit = defineEmits(['done', 'error', 'close'])
|
|||||||
|
|
||||||
// 提示框
|
// 提示框
|
||||||
const $toast = useToast()
|
const $toast = useToast()
|
||||||
|
const createConfirm = useConfirm()
|
||||||
|
|
||||||
// 选择的下载器
|
// 选择的下载器
|
||||||
const selectedDownloader = ref<string | null>(null)
|
const selectedDownloader = ref<string | null>(null)
|
||||||
@@ -201,6 +207,7 @@ async function addDownload() {
|
|||||||
media_in?: MediaInfo
|
media_in?: MediaInfo
|
||||||
media_source?: MediaDataSource
|
media_source?: MediaDataSource
|
||||||
music_type?: Exclude<MusicEntityType, 'artist'>
|
music_type?: Exclude<MusicEntityType, 'artist'>
|
||||||
|
allow_unrecognized?: boolean
|
||||||
save_path: string | null
|
save_path: string | null
|
||||||
torrent_in: TorrentInfo | undefined
|
torrent_in: TorrentInfo | undefined
|
||||||
} = {
|
} = {
|
||||||
@@ -222,7 +229,28 @@ async function addDownload() {
|
|||||||
|
|
||||||
const endpoint = props.media ? 'download/' : 'download/add'
|
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(
|
$toast.success(
|
||||||
@@ -241,9 +269,10 @@ async function addDownload() {
|
|||||||
}),
|
}),
|
||||||
)
|
)
|
||||||
emit('error', message)
|
emit('error', message)
|
||||||
|
} finally {
|
||||||
|
loading.value = false
|
||||||
|
doneNProgress()
|
||||||
}
|
}
|
||||||
loading.value = false
|
|
||||||
doneNProgress()
|
|
||||||
}
|
}
|
||||||
|
|
||||||
onMounted(() => {
|
onMounted(() => {
|
||||||
|
|||||||
@@ -11,6 +11,7 @@ import { beforeEach, describe, expect, it, vi } from 'vitest'
|
|||||||
const API_BASE_URL = 'http://localhost/api/v1/'
|
const API_BASE_URL = 'http://localhost/api/v1/'
|
||||||
|
|
||||||
const mocks = vi.hoisted(() => ({
|
const mocks = vi.hoisted(() => ({
|
||||||
|
confirm: vi.fn(),
|
||||||
doneNProgress: vi.fn(),
|
doneNProgress: vi.fn(),
|
||||||
startNProgress: vi.fn(),
|
startNProgress: vi.fn(),
|
||||||
toastError: vi.fn(),
|
toastError: vi.fn(),
|
||||||
@@ -27,6 +28,10 @@ vi.mock('vue-toastification', () => ({
|
|||||||
useToast: () => ({ error: mocks.toastError, success: mocks.toastSuccess }),
|
useToast: () => ({ error: mocks.toastError, success: mocks.toastSuccess }),
|
||||||
}))
|
}))
|
||||||
|
|
||||||
|
vi.mock('@/composables/useConfirm', () => ({
|
||||||
|
useConfirm: () => mocks.confirm,
|
||||||
|
}))
|
||||||
|
|
||||||
type SelectItem = string | { title: string; value: string }
|
type SelectItem = string | { title: string; value: string }
|
||||||
|
|
||||||
const SelectStub = defineComponent({
|
const SelectStub = defineComponent({
|
||||||
@@ -297,6 +302,7 @@ describe('AddDownloadDialog directories', () => {
|
|||||||
describe('AddDownloadDialog submissions', () => {
|
describe('AddDownloadDialog submissions', () => {
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
vi.clearAllMocks()
|
vi.clearAllMocks()
|
||||||
|
mocks.confirm.mockResolvedValue(false)
|
||||||
vi.spyOn(console, 'error').mockImplementation(() => {})
|
vi.spyOn(console, 'error').mockImplementation(() => {})
|
||||||
vi.spyOn(console, 'log').mockImplementation(() => {})
|
vi.spyOn(console, 'log').mockImplementation(() => {})
|
||||||
})
|
})
|
||||||
@@ -441,6 +447,72 @@ describe('AddDownloadDialog submissions', () => {
|
|||||||
expect(mocks.doneNProgress).toHaveBeenCalledOnce()
|
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 () => {
|
it('treats success:false at HTTP 200 as a business failure', async () => {
|
||||||
server.use(downloadHandler('download/add', { data: null, message: '下载器拒绝任务', success: false }))
|
server.use(downloadHandler('download/add', { data: null, message: '下载器拒绝任务', success: false }))
|
||||||
const user = userEvent.setup()
|
const user = userEvent.setup()
|
||||||
@@ -450,6 +522,7 @@ describe('AddDownloadDialog submissions', () => {
|
|||||||
|
|
||||||
await waitFor(() => expect(events.error).toHaveBeenCalledWith('下载器拒绝任务'))
|
await waitFor(() => expect(events.error).toHaveBeenCalledWith('下载器拒绝任务'))
|
||||||
expect(events.done).not.toHaveBeenCalled()
|
expect(events.done).not.toHaveBeenCalled()
|
||||||
|
expect(mocks.confirm).not.toHaveBeenCalled()
|
||||||
expect(mocks.toastError).toHaveBeenCalledWith('测试站 测试种子 下载失败:下载器拒绝任务!')
|
expect(mocks.toastError).toHaveBeenCalledWith('测试站 测试种子 下载失败:下载器拒绝任务!')
|
||||||
expect(mocks.startNProgress).toHaveBeenCalledOnce()
|
expect(mocks.startNProgress).toHaveBeenCalledOnce()
|
||||||
expect(mocks.doneNProgress).toHaveBeenCalledOnce()
|
expect(mocks.doneNProgress).toHaveBeenCalledOnce()
|
||||||
|
|||||||
@@ -3012,6 +3012,9 @@ export default {
|
|||||||
startDownload: 'Start Download',
|
startDownload: 'Start Download',
|
||||||
downloadSuccess: '{site} {title} downloaded successfully!',
|
downloadSuccess: '{site} {title} downloaded successfully!',
|
||||||
downloadFailed: '{site} {title} download failed: {message}!',
|
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',
|
showAdvancedOptions: 'Show Advanced Options',
|
||||||
hideAdvancedOptions: 'Hide Advanced Options',
|
hideAdvancedOptions: 'Hide Advanced Options',
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -2960,6 +2960,9 @@ export default {
|
|||||||
startDownload: '开始下载',
|
startDownload: '开始下载',
|
||||||
downloadSuccess: '{site} {title} 下载成功!',
|
downloadSuccess: '{site} {title} 下载成功!',
|
||||||
downloadFailed: '{site} {title} 下载失败:{message}!',
|
downloadFailed: '{site} {title} 下载失败:{message}!',
|
||||||
|
unrecognizedTitle: '无法识别媒体信息',
|
||||||
|
unrecognizedContent: '无法识别此资源的媒体信息,是否仍要下载?',
|
||||||
|
continueDownload: '继续下载',
|
||||||
showAdvancedOptions: '显示高级选项',
|
showAdvancedOptions: '显示高级选项',
|
||||||
hideAdvancedOptions: '隐藏高级选项',
|
hideAdvancedOptions: '隐藏高级选项',
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -2959,6 +2959,9 @@ export default {
|
|||||||
startDownload: '開始下載',
|
startDownload: '開始下載',
|
||||||
downloadSuccess: '{site} {title} 下載成功!',
|
downloadSuccess: '{site} {title} 下載成功!',
|
||||||
downloadFailed: '{site} {title} 下載失敗:{message}!',
|
downloadFailed: '{site} {title} 下載失敗:{message}!',
|
||||||
|
unrecognizedTitle: '無法識別媒體資訊',
|
||||||
|
unrecognizedContent: '無法識別此資源的媒體資訊,是否仍要下載?',
|
||||||
|
continueDownload: '繼續下載',
|
||||||
showAdvancedOptions: '顯示高級選項',
|
showAdvancedOptions: '顯示高級選項',
|
||||||
hideAdvancedOptions: '隱藏高級選項',
|
hideAdvancedOptions: '隱藏高級選項',
|
||||||
},
|
},
|
||||||
|
|||||||
Reference in New Issue
Block a user