From 59a296d429bbf61488097db25c30ba8d2a34ff9f Mon Sep 17 00:00:00 2001 From: jxxghp Date: Mon, 10 Aug 2026 23:30:31 +0800 Subject: [PATCH] feat(music): support MusicBrainz in recognition test - add MusicBrainz to recognition source options with music result display - hide custom words input when MusicBrainz is selected (music ignores words) - link native MusicBrainz recording/release-group detail page --- src/locales/en-US.ts | 3 +- src/locales/zh-CN.ts | 3 +- src/locales/zh-TW.ts | 3 +- src/views/system/NameTestView.vue | 61 +++++++++++--- .../system/__tests__/NameTestView.spec.ts | 84 +++++++++++++++++++ 5 files changed, 141 insertions(+), 13 deletions(-) diff --git a/src/locales/en-US.ts b/src/locales/en-US.ts index fd2ee972..396de964 100644 --- a/src/locales/en-US.ts +++ b/src/locales/en-US.ts @@ -1729,7 +1729,7 @@ export default { recognizing: 'Recognizing...', recognizeAgain: 'Recognize Again', title: 'Title', - titleHint: 'Enter a torrent name, release title, or file name', + titleHint: 'Enter a torrent name, release title, or file name; audio file names are recognized as music', subtitle: 'Subtitle', subtitleHint: 'Optional torrent description, alias, or release details to improve recognition accuracy', source: 'Recognition Source', @@ -2779,6 +2779,7 @@ export default { douban: 'Douban', bangumi: 'Bangumi', anilist: 'AniList', + musicbrainz: 'MusicBrainz', }, }, }, diff --git a/src/locales/zh-CN.ts b/src/locales/zh-CN.ts index bbe9c4b5..f6e4484f 100644 --- a/src/locales/zh-CN.ts +++ b/src/locales/zh-CN.ts @@ -1719,7 +1719,7 @@ export default { recognizing: '识别中...', recognizeAgain: '重新识别', title: '标题', - titleHint: '输入种子名、发布标题或文件名', + titleHint: '输入种子名、发布标题或文件名,音频文件名将按音乐识别', subtitle: '副标题', subtitleHint: '可选,补充种子描述、别名或发布信息以提高识别准确度', source: '识别数据源', @@ -2728,6 +2728,7 @@ export default { douban: '豆瓣', bangumi: 'Bangumi', anilist: 'AniList', + musicbrainz: 'MusicBrainz', }, }, }, diff --git a/src/locales/zh-TW.ts b/src/locales/zh-TW.ts index 3e91c858..f5a9bccd 100644 --- a/src/locales/zh-TW.ts +++ b/src/locales/zh-TW.ts @@ -1718,7 +1718,7 @@ export default { recognizing: '識別中...', recognizeAgain: '重新識別', title: '標題', - titleHint: '輸入種子名、發佈標題或文件名', + titleHint: '輸入種子名、發佈標題或文件名,音頻文件名將按音樂識別', subtitle: '副標題', subtitleHint: '可選,補充種子描述、別名或發佈信息以提高識別準確度', source: '識別數據源', @@ -2727,6 +2727,7 @@ export default { douban: '豆瓣', bangumi: 'Bangumi', anilist: 'AniList', + musicbrainz: 'MusicBrainz', }, }, }, diff --git a/src/views/system/NameTestView.vue b/src/views/system/NameTestView.vue index f72cac75..ef42f2c1 100644 --- a/src/views/system/NameTestView.vue +++ b/src/views/system/NameTestView.vue @@ -27,6 +27,7 @@ interface MediaIdentity { interface MediaSourceDisplay { icon?: string + iconColor?: string image?: string key: string label: string @@ -43,6 +44,7 @@ const MEDIA_SOURCE_LABELS: Record = { anilist: 'AniList', bangumi: 'Bangumi', douban: 'Douban', + musicbrainz: 'MusicBrainz', themoviedb: 'TheMovieDb', } @@ -52,6 +54,12 @@ const MEDIA_SOURCE_LOGOS: Record = { themoviedb: getLogoUrl('tmdb'), } +// 无专属 logo 的数据源使用品牌色图标展示 +const MEDIA_SOURCE_ICONS: Record = { + anilist: { icon: 'mdi-alpha-a-circle', color: '#02a9ff' }, + musicbrainz: { icon: 'mdi-album', color: '#eb743b' }, +} + const NAME_TEST_TITLE_HISTORY_KEY = 'MP_NAME_TEST_TITLE_HISTORY' const NAME_TEST_TITLE_HISTORY_LIMIT = 5 @@ -66,6 +74,7 @@ const mediaSourceItems = computed<{ title: string; value: MediaDataSource }[]>(( { title: t('setting.cache.recognitionSource.douban'), value: 'douban' }, { title: t('setting.cache.recognitionSource.bangumi'), value: 'bangumi' }, { title: t('setting.cache.recognitionSource.anilist'), value: 'anilist' }, + { title: t('setting.cache.recognitionSource.musicbrainz'), value: 'musicbrainz' }, ]) // 获取后台默认识别数据源,未知值兼容回退到TheMovieDb。 @@ -88,6 +97,9 @@ const nameTestForm = reactive({ source: getDefaultMediaSource(), }) +// MusicBrainz 仅支持音乐识别,音乐不应用自定义识别词,隐藏输入区避免误导 +const showCustomWords = computed(() => nameTestForm.source !== 'musicbrainz') + /** 从本地存储读取最近使用的识别标题。 */ function loadTitleHistory() { try { @@ -140,17 +152,36 @@ const savingCustomWords = ref(false) const metaInfo = computed(() => nameTestResult.value?.meta_info) const mediaInfo = computed(() => nameTestResult.value?.media_info) -const isRecognized = computed(() => Boolean(metaInfo.value?.name)) +// 音乐识别的元信息没有 name 字段,依靠 title 判断识别是否成功 +const isMusicResult = computed(() => mediaInfo.value?.type === '音乐' || metaInfo.value?.type === '音乐') +const isRecognized = computed(() => + Boolean(metaInfo.value?.name || (isMusicResult.value && metaInfo.value?.title)), +) const resultTitle = computed(() => mediaInfo.value?.title || metaInfo.value?.name || t('nameTest.unrecognized')) const resultSubtitle = computed(() => { const parts = [mediaInfo.value?.year || metaInfo.value?.year] - if (metaInfo.value?.season_episode) parts.push(metaInfo.value.season_episode) + if (isMusicResult.value) { + // 音乐结果没有季集信息,展示艺术家和专辑辅助确认 + const artistText = mediaInfo.value?.artist || metaInfo.value?.artist + if (artistText) parts.push(artistText) + if (mediaInfo.value?.album) parts.push(mediaInfo.value.album) + } else if (metaInfo.value?.season_episode) { + parts.push(metaInfo.value.season_episode) + } return parts.filter(Boolean).join(' · ') || t('nameTest.waitingResult') }) const mediaClassification = computed(() => { return [mediaInfo.value?.type || metaInfo.value?.type, mediaInfo.value?.category].filter(Boolean).join(' · ') || '-' }) const resourceChips = computed(() => { + if (isMusicResult.value) { + return [ + mediaInfo.value?.music_type, + metaInfo.value?.audio_format, + metaInfo.value?.audio_specs, + mediaInfo.value?.category, + ].filter(Boolean) as string[] + } return [ metaInfo.value?.web_source, metaInfo.value?.edition, @@ -186,6 +217,9 @@ function getMediaOfficialLink(media: MediaInfo, source: string, mediaId: string) return `https://bgm.tv/subject/${encodedId}` case 'anilist': return `https://anilist.co/anime/${encodedId}` + case 'musicbrainz': + // MusicBrainz 各实体共用 UUID,优先使用识别结果自带的详情页地址 + return media.detail_link || `https://musicbrainz.org/recording/${encodedId}` default: return undefined } @@ -209,9 +243,11 @@ function getMediaIdentity(media?: MediaInfo): MediaIdentity | undefined { const mediaIdentity = computed(() => getMediaIdentity(mediaInfo.value)) const recognizedMediaSource = computed(() => { const sourceKey = mediaIdentity.value?.sourceKey || nameTestForm.source + const iconInfo = MEDIA_SOURCE_ICONS[sourceKey] return { - icon: sourceKey === 'anilist' ? 'mdi-alpha-a-circle' : undefined, + icon: iconInfo?.icon, + iconColor: iconInfo?.color, image: MEDIA_SOURCE_LOGOS[sourceKey], key: sourceKey, label: mediaIdentity.value?.source || MEDIA_SOURCE_LABELS[sourceKey] || sourceKey, @@ -227,10 +263,14 @@ const pipelineSteps = computed(() => [ { icon: 'mdi-puzzle-check-outline', title: t('nameTest.steps.meta.title'), - value: - [metaInfo.value?.name, metaInfo.value?.resource_term, metaInfo.value?.release_group] - .filter(Boolean) - .join(' · ') || '-', + value: isMusicResult.value + ? // 音乐元信息展示曲名、艺术家、专辑和音频格式 + [metaInfo.value?.title, metaInfo.value?.artist, metaInfo.value?.album, metaInfo.value?.audio_format] + .filter(Boolean) + .join(' · ') || '-' + : [metaInfo.value?.name, metaInfo.value?.resource_term, metaInfo.value?.release_group] + .filter(Boolean) + .join(' · ') || '-', }, { icon: 'mdi-shape-outline', @@ -293,7 +333,8 @@ async function nameTest() { params: { title: nameTestForm.title, subtitle: nameTestForm.subtitle, - custom_words: nameTestForm.customWords?.trim() || undefined, + // 音乐识别不应用识别词,隐藏状态下不随请求携带 + custom_words: showCustomWords.value ? nameTestForm.customWords?.trim() || undefined : undefined, source: nameTestForm.source, }, }) @@ -383,7 +424,7 @@ async function saveCustomWords() { prepend-inner-icon="mdi-subtitles" /> - +