From 155ecffd89284a49b051e1ba83e4539b7b2ad16a Mon Sep 17 00:00:00 2001 From: jxxghp Date: Wed, 12 Aug 2026 06:51:58 +0800 Subject: [PATCH] feat: complete music entity workflows --- src/api/types.ts | 10 +++++ src/components/cards/MediaCard.vue | 1 + src/components/dialog/AddDownloadDialog.vue | 44 ++++++++++++++++++- .../dialog/CacheReidentifyDialog.vue | 29 +++++++++++- src/components/dialog/ReorganizeDialog.vue | 25 +++++++++-- src/components/dialog/ScrapeDialog.vue | 35 ++++++++++++--- .../__tests__/AddDownloadDialog.spec.ts | 23 ++++++++++ .../__tests__/CacheReidentifyDialog.spec.ts | 43 ++++++++++++++++++ .../dialog/__tests__/ReorganizeDialog.spec.ts | 32 ++++++++++++++ .../dialog/__tests__/ScrapeDialog.spec.ts | 23 ++++++++++ src/components/misc/MediaIdSelector.vue | 9 +++- src/locales/en-US.ts | 1 + src/locales/zh-CN.ts | 1 + src/pages/__tests__/music-detail.spec.ts | 1 + src/pages/__tests__/resource.spec.ts | 31 +++++++++++++ src/pages/resource.vue | 9 +++- src/utils/__tests__/mediaId.spec.ts | 2 + src/utils/__tests__/music.spec.ts | 7 ++- src/utils/mediaId.ts | 5 ++- src/utils/music.ts | 1 + src/views/discover/MusicDetailView.vue | 1 + src/views/system/CacheView.vue | 14 ++++-- 22 files changed, 324 insertions(+), 23 deletions(-) create mode 100644 src/components/dialog/__tests__/CacheReidentifyDialog.spec.ts diff --git a/src/api/types.ts b/src/api/types.ts index 00e29f4c..4f76d34d 100644 --- a/src/api/types.ts +++ b/src/api/types.ts @@ -9,6 +9,8 @@ export interface ManualScrapeOptions { media_id?: string // 媒体类型 type_name?: string + // 音乐实体类型 + music_type?: MusicEntityType } // 订阅 @@ -1906,6 +1908,8 @@ export interface TransferForm { media_source?: MediaDataSource // 数据源原生ID media_id?: string | null + // 音乐实体类型 + music_type?: Exclude | null // 季号 season?: number // 类型 @@ -2132,6 +2136,12 @@ export interface TorrentCacheItem { media_year?: string // 识别的媒体类型 media_type?: string + // 识别结果的数据源 + media_source?: MediaDataSource + // 数据源原生媒体 ID + media_id?: string + // 音乐实体类型 + music_type?: Exclude // 季集信息 season_episode?: string // 资源信息 diff --git a/src/components/cards/MediaCard.vue b/src/components/cards/MediaCard.vue index 98f58550..9099b42e 100644 --- a/src/components/cards/MediaCard.vue +++ b/src/components/cards/MediaCard.vue @@ -396,6 +396,7 @@ function handleSearch() { title: props.media?.title, year: props.media?.year, season: props.media?.season, + ...(props.media?.type === '音乐' ? { music_type: props.media.music_type ?? 'recording' } : {}), sites: selectedSites.value.join(','), }, }) diff --git a/src/components/dialog/AddDownloadDialog.vue b/src/components/dialog/AddDownloadDialog.vue index 233cd47c..3177a409 100644 --- a/src/components/dialog/AddDownloadDialog.vue +++ b/src/components/dialog/AddDownloadDialog.vue @@ -7,6 +7,7 @@ import type { DownloaderConf, MediaDataSource, MediaInfo, + MusicEntityType, TorrentInfo, TransferDirectoryConf, } from '@/api/types' @@ -14,7 +15,7 @@ import { formatFileSize } from '@/@core/utils/formatters' import { VCardTitle, VChip } from 'vuetify/lib/components/index.mjs' import { useI18n } from 'vue-i18n' import MediaIdSelector from '../misc/MediaIdSelector.vue' -import { isValidMediaSourceId } from '@/utils/mediaId' +import { isMusicMediaSource, isValidMediaSourceId } from '@/utils/mediaId' import { useGlobalSettingsStore } from '@/stores' // 多语言支持 @@ -46,6 +47,7 @@ const SUPPORTED_MEDIA_SOURCES: MediaDataSource[] = [ const mediaSource = computed(() => { const source = props.media?.source as MediaDataSource | undefined if (source && SUPPORTED_MEDIA_SOURCES.includes(source)) return source + if (props.torrent?.category === '音乐' || props.torrent?.category === 'music') return 'musicbrainz' if (SUPPORTED_MEDIA_SOURCES.includes(globalSettings.RECOGNIZE_SOURCE as MediaDataSource)) { return globalSettings.RECOGNIZE_SOURCE as MediaDataSource } @@ -79,6 +81,16 @@ const showAdvancedOptions = ref(false) // 当前数据源的原生媒体ID const mediaId = ref(undefined) +// 无完整媒体上下文时,音乐原生 ID 需要实体命名空间才能区分单曲和专辑。 +const musicType = ref>(props.media?.music_type === 'album' ? 'album' : 'recording') + +const isMusicSelection = computed(() => isMusicMediaSource(mediaSource.value)) + +const musicEntityOptions = computed(() => [ + { title: t('setting.cache.musicType.recording'), value: 'recording' }, + { title: t('setting.cache.musicType.album'), value: 'album' }, +]) + // 音乐媒体自带来源原生 ID,打开对话框时预填到高级选项中辅助识别。 watch( () => props.media, @@ -86,10 +98,20 @@ watch( if (media?.source && SUPPORTED_MEDIA_SOURCES.includes(media.source) && media.media_id) { mediaId.value = media.media_id } + if (media?.music_type === 'recording' || media?.music_type === 'album') { + musicType.value = media.music_type + } }, { immediate: true }, ) +// 同步媒体选择器返回的音乐实体,避免只保存 ID 后默认回落到单曲。 +function handleMediaSelected(item: Pick) { + if (item.music_type === 'recording' || item.music_type === 'album') { + musicType.value = item.music_type + } +} + // 当前数据源对应的原生ID标签。 const mediaIdLabel = computed(() => { const labels: Record = { @@ -186,6 +208,7 @@ async function addDownload() { media_id?: string media_in?: MediaInfo media_source?: MediaDataSource + music_type?: Exclude save_path: string | null torrent_in: TorrentInfo | undefined } = { @@ -202,6 +225,7 @@ async function addDownload() { if (mediaId.value) { payload.media_source = mediaSource.value payload.media_id = mediaId.value + if (isMusicSelection.value) payload.music_type = musicType.value } const endpoint = props.media ? 'download/' : 'download/add' @@ -324,6 +348,16 @@ onMounted(() => { + + + { - + diff --git a/src/components/dialog/CacheReidentifyDialog.vue b/src/components/dialog/CacheReidentifyDialog.vue index f93ba00b..5ae1a9bc 100644 --- a/src/components/dialog/CacheReidentifyDialog.vue +++ b/src/components/dialog/CacheReidentifyDialog.vue @@ -1,6 +1,7 @@ @@ -91,6 +108,14 @@ function submitReidentify() { persistent-hint /> + + + ({ target_path: initialTargetPath, media_source: getDefaultMediaSource(), media_id: null, + music_type: null, transfer_type: null, min_filesize: 0, scrape: initialTargetPath ? false : null, @@ -362,11 +363,14 @@ const mediaIdLabel = computed(() => { }) // 处理媒体搜索结果选择,同步搜索结果中已识别的媒体类型。 -function handleMediaSelected(item: Pick) { +function handleMediaSelected(item: Pick) { const typeName = resolveTransferMediaType(item.type) if (!typeName) return transferForm.type_name = typeName + if (item.music_type === 'recording' || item.music_type === 'album') { + transferForm.music_type = item.music_type + } } // 所有媒体库目录 @@ -480,6 +484,7 @@ watch( if (typeName === '音乐' && !isMusicMediaSource(transferForm.media_source)) { transferForm.media_source = 'musicbrainz' } + transferForm.music_type = typeName === '音乐' ? (transferForm.music_type ?? 'recording') : null }, ) @@ -1515,7 +1520,7 @@ onUnmounted(() => { - + { prepend-inner-icon="mdi-movie-open" /> - + { prepend-inner-icon="mdi-database-search" /> - + + + + { @close="mediaSelectorDialog = false" @select="handleMediaSelected" :type="mediaSource" + :music-types="['recording', 'album']" /> diff --git a/src/components/dialog/ScrapeDialog.vue b/src/components/dialog/ScrapeDialog.vue index 23535930..a40355d3 100644 --- a/src/components/dialog/ScrapeDialog.vue +++ b/src/components/dialog/ScrapeDialog.vue @@ -1,5 +1,5 @@