mirror of
https://github.com/jxxghp/MoviePilot-Frontend.git
synced 2026-09-07 08:46:40 +08:00
refactor(media): unify frontend media identity
This commit is contained in:
@@ -1,4 +1,4 @@
|
||||
import { isMusicMediaSource, isValidMediaSourceId } from '@/utils/mediaId'
|
||||
import { isMusicMediaSource, isValidMediaSourceId, parseMediaDataSources } from '@/utils/mediaId'
|
||||
import { describe, expect, it } from 'vitest'
|
||||
|
||||
describe('media source identity utils', () => {
|
||||
@@ -6,12 +6,32 @@ describe('media source identity utils', () => {
|
||||
expect(isMusicMediaSource(source)).toBe(true)
|
||||
})
|
||||
|
||||
it('uses UUIDs for MusicBrainz and numeric IDs for alternate music sources', () => {
|
||||
it.each([
|
||||
['themoviedb', ['themoviedb']],
|
||||
[' musicbrainz, theaudiodb,unknown,musicbrainz ', ['musicbrainz', 'theaudiodb']],
|
||||
[
|
||||
['douban', 'anilist,bangumi', null, 'douban'],
|
||||
['douban', 'anilist', 'bangumi'],
|
||||
],
|
||||
[undefined, []],
|
||||
])('normalizes route media sources from %j', (value, expected) => {
|
||||
expect(parseMediaDataSources(value)).toEqual(expected)
|
||||
})
|
||||
|
||||
it('validates source-specific IDs without assuming every provider uses numeric IDs', () => {
|
||||
expect(isValidMediaSourceId('', 'themoviedb')).toBe(true)
|
||||
expect(isValidMediaSourceId(' ', 'douban')).toBe(true)
|
||||
expect(isValidMediaSourceId(0, 'themoviedb')).toBe(false)
|
||||
expect(isValidMediaSourceId('0', 'bilibili')).toBe(false)
|
||||
expect(isValidMediaSourceId('977e6978-139d-425c-bb98-6b0c62d1e45e', 'musicbrainz')).toBe(true)
|
||||
expect(isValidMediaSourceId('not-a-uuid', 'musicbrainz')).toBe(false)
|
||||
expect(isValidMediaSourceId('32793500', 'theaudiodb')).toBe(true)
|
||||
expect(isValidMediaSourceId('1401853', 'doubanmusic')).toBe(true)
|
||||
expect(isValidMediaSourceId('1401853:3', 'doubanmusic')).toBe(true)
|
||||
expect(isValidMediaSourceId('1401853:track', 'doubanmusic')).toBe(false)
|
||||
expect(isValidMediaSourceId('not-a-number', 'theaudiodb')).toBe(false)
|
||||
expect(isValidMediaSourceId('album-32793500', 'theaudiodb')).toBe(true)
|
||||
expect(isValidMediaSourceId('tt0111161', 'imdb')).toBe(true)
|
||||
expect(isValidMediaSourceId('0111161', 'imdb')).toBe(false)
|
||||
expect(isValidMediaSourceId('BV1xx411c7mD', 'bilibili')).toBe(true)
|
||||
})
|
||||
})
|
||||
|
||||
@@ -14,7 +14,7 @@ describe('music utils', () => {
|
||||
it('routes a recording to the music detail page', () => {
|
||||
expect(buildMusicDetailRoute({ media_source: 'musicbrainz', media_id: 'recording-1', title: '晴天' })).toEqual({
|
||||
path: '/music/detail',
|
||||
query: { media_source: 'musicbrainz', mediaid: 'recording-1', title: '晴天' },
|
||||
query: { media_source: 'musicbrainz', media_id: 'recording-1', title: '晴天' },
|
||||
})
|
||||
})
|
||||
|
||||
@@ -28,7 +28,7 @@ describe('music utils', () => {
|
||||
}),
|
||||
).toEqual({
|
||||
path: '/music/album',
|
||||
query: { media_source: 'musicbrainz', mediaid: 'release-group-1', title: '叶惠美' },
|
||||
query: { media_source: 'musicbrainz', media_id: 'release-group-1', title: '叶惠美' },
|
||||
})
|
||||
})
|
||||
|
||||
@@ -42,7 +42,7 @@ describe('music utils', () => {
|
||||
}),
|
||||
).toEqual({
|
||||
path: '/music/artist',
|
||||
query: { media_source: 'musicbrainz', mediaid: 'artist-1', title: 'Queen' },
|
||||
query: { media_source: 'musicbrainz', media_id: 'artist-1', title: 'Queen' },
|
||||
})
|
||||
})
|
||||
|
||||
@@ -53,14 +53,14 @@ describe('music utils', () => {
|
||||
})
|
||||
})
|
||||
|
||||
it('builds album and artist routes with the default source', () => {
|
||||
expect(buildMusicAlbumRoute('release-group-1', '叶惠美')).toEqual({
|
||||
it('builds album and artist routes with an explicit source', () => {
|
||||
expect(buildMusicAlbumRoute('release-group-1', '叶惠美', 'musicbrainz')).toEqual({
|
||||
path: '/music/album',
|
||||
query: { media_source: 'musicbrainz', mediaid: 'release-group-1', title: '叶惠美' },
|
||||
query: { media_source: 'musicbrainz', media_id: 'release-group-1', title: '叶惠美' },
|
||||
})
|
||||
expect(buildMusicArtistRoute('artist-1', 'Queen')).toEqual({
|
||||
expect(buildMusicArtistRoute('artist-1', 'Queen', 'musicbrainz')).toEqual({
|
||||
path: '/music/artist',
|
||||
query: { media_source: 'musicbrainz', mediaid: 'artist-1', title: 'Queen' },
|
||||
query: { media_source: 'musicbrainz', media_id: 'artist-1', title: 'Queen' },
|
||||
})
|
||||
})
|
||||
|
||||
@@ -78,7 +78,8 @@ describe('music utils', () => {
|
||||
).toMatchObject({
|
||||
path: '/resource',
|
||||
query: {
|
||||
keyword: 'musicbrainz:recording-1',
|
||||
media_id: 'recording-1',
|
||||
media_source: 'musicbrainz',
|
||||
music_type: 'recording',
|
||||
sites: '11,12',
|
||||
type: '音乐',
|
||||
|
||||
+25
-7
@@ -1,23 +1,41 @@
|
||||
import type { MediaDataSource } from '@/api/types'
|
||||
import { MediaSource, type MediaDataSource } from '@/api/types'
|
||||
|
||||
const MUSICBRAINZ_ID_PATTERN = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i
|
||||
const IMDB_ID_PATTERN = /^tt\d+$/i
|
||||
export const MUSIC_MEDIA_SOURCES = ['musicbrainz', 'theaudiodb', 'doubanmusic'] as const
|
||||
|
||||
/** 判断外部输入是否属于产品协议中固定的媒体来源枚举。 */
|
||||
export function isMediaDataSource(value: unknown): value is MediaDataSource {
|
||||
return typeof value === 'string' && Object.values(MediaSource).includes(value as MediaSource)
|
||||
}
|
||||
|
||||
/** 将路由或表单中的单值、逗号分隔值及数组统一解析为去重后的媒体来源枚举。 */
|
||||
export function parseMediaDataSources(value: unknown): MediaDataSource[] {
|
||||
const values = Array.isArray(value) ? value : [value]
|
||||
return [
|
||||
...new Set(
|
||||
values
|
||||
.flatMap(item => (typeof item === 'string' ? item.split(',') : []))
|
||||
.map(item => item.trim())
|
||||
.filter(isMediaDataSource),
|
||||
),
|
||||
]
|
||||
}
|
||||
|
||||
/** 判断当前请求来源是否为内置音乐元数据源。 */
|
||||
export function isMusicMediaSource(source?: MediaDataSource): boolean {
|
||||
return MUSIC_MEDIA_SOURCES.includes(source as (typeof MUSIC_MEDIA_SOURCES)[number])
|
||||
}
|
||||
|
||||
/** 按媒体数据源校验原生 ID,并兼容豆瓣音乐的曲目复合 ID。 */
|
||||
/** 按媒体数据源校验有固定格式的原生 ID,其余来源只要求非空。 */
|
||||
export function isValidMediaSourceId(value: string | number | null | undefined, source?: MediaDataSource): boolean {
|
||||
const normalized = value?.toString().trim()
|
||||
if (!normalized) return true
|
||||
// 无媒体上下文或来源为非 MusicBrainz 时,UUID 形态同样按 MusicBrainz ID 放行
|
||||
if (source === 'musicbrainz' || MUSICBRAINZ_ID_PATTERN.test(normalized)) {
|
||||
return MUSICBRAINZ_ID_PATTERN.test(normalized)
|
||||
}
|
||||
if (normalized === '0') return false
|
||||
if (source === 'musicbrainz') return MUSICBRAINZ_ID_PATTERN.test(normalized)
|
||||
if (source === 'doubanmusic' && normalized.includes(':')) {
|
||||
return /^\d+:\d+$/.test(normalized)
|
||||
}
|
||||
return /^\d+$/.test(normalized)
|
||||
if (source === 'imdb') return IMDB_ID_PATTERN.test(normalized)
|
||||
return true
|
||||
}
|
||||
|
||||
+19
-10
@@ -1,8 +1,8 @@
|
||||
import type { MediaInfo, MusicAlbumInfo, MusicArtistInfo, MusicEntityType } from '@/api/types'
|
||||
import type { MediaDataSource, MediaInfo, MusicAlbumInfo, MusicArtistInfo, MusicEntityType } from '@/api/types'
|
||||
import type { RouteLocationRaw } from 'vue-router'
|
||||
|
||||
export interface MusicRouteTarget {
|
||||
media_source?: string
|
||||
media_source?: MediaDataSource
|
||||
media_id?: string | number
|
||||
music_type?: MusicEntityType
|
||||
title?: string
|
||||
@@ -57,7 +57,7 @@ export function formatMusicAudioSpecs(item?: MusicAudioInfo): string {
|
||||
}
|
||||
|
||||
/** 返回音乐对象可用于路由和订阅的统一来源。 */
|
||||
export function getMusicSource(item: MusicRouteTarget): string | undefined {
|
||||
export function getMusicSource(item: MusicRouteTarget): MediaDataSource | undefined {
|
||||
return item.media_source
|
||||
}
|
||||
|
||||
@@ -68,7 +68,7 @@ export function getMusicSourceLabel(source?: string, translate?: (key: string) =
|
||||
theaudiodb: 'TheAudioDB',
|
||||
doubanmusic: translate?.('setting.cache.recognitionSource.doubanmusic') || '豆瓣音乐',
|
||||
}
|
||||
return (source && labels[source]) || source || 'MusicBrainz'
|
||||
return (source && labels[source]) || source || ''
|
||||
}
|
||||
|
||||
/** 返回音乐候选在列表和状态缓存中的稳定身份。 */
|
||||
@@ -79,13 +79,21 @@ export function getMusicKey(item: MusicRouteTarget): string {
|
||||
}
|
||||
|
||||
/** 构造专辑详情路由。 */
|
||||
export function buildMusicAlbumRoute(albumId: string, title?: string, mediaSource = 'musicbrainz'): RouteLocationRaw {
|
||||
return { path: '/music/album', query: { media_source: mediaSource, mediaid: albumId, title } }
|
||||
export function buildMusicAlbumRoute(
|
||||
albumId: string,
|
||||
title: string | undefined,
|
||||
mediaSource: MediaDataSource,
|
||||
): RouteLocationRaw {
|
||||
return { path: '/music/album', query: { media_source: mediaSource, media_id: albumId, title } }
|
||||
}
|
||||
|
||||
/** 构造艺术家详情路由。 */
|
||||
export function buildMusicArtistRoute(artistId: string, name?: string, mediaSource = 'musicbrainz'): RouteLocationRaw {
|
||||
return { path: '/music/artist', query: { media_source: mediaSource, mediaid: artistId, title: name } }
|
||||
export function buildMusicArtistRoute(
|
||||
artistId: string,
|
||||
name: string | undefined,
|
||||
mediaSource: MediaDataSource,
|
||||
): RouteLocationRaw {
|
||||
return { path: '/music/artist', query: { media_source: mediaSource, media_id: artistId, title: name } }
|
||||
}
|
||||
|
||||
/** 按音乐实体类型构造详情路由,缺少标准身份时回退到音乐搜索页。 */
|
||||
@@ -101,7 +109,7 @@ export function buildMusicDetailRoute(item: MusicRouteTarget): RouteLocationRaw
|
||||
path: '/music/detail',
|
||||
query: {
|
||||
media_source: source,
|
||||
mediaid: mediaId,
|
||||
media_id: mediaId,
|
||||
title: item.title || item.name,
|
||||
},
|
||||
}
|
||||
@@ -119,7 +127,8 @@ export function buildMusicResourceRoute(
|
||||
return {
|
||||
path: '/resource',
|
||||
query: {
|
||||
keyword: `${source}:${item.media_id}`,
|
||||
media_source: source,
|
||||
media_id: item.media_id,
|
||||
type: '音乐',
|
||||
music_type: (item as MusicRouteTarget).music_type || 'recording',
|
||||
title: item.title,
|
||||
|
||||
Reference in New Issue
Block a user