mirror of
https://github.com/jxxghp/MoviePilot-Frontend.git
synced 2026-09-01 05:27:34 +08:00
feat(media-source): 支持插件注册的媒体数据源,各来源选择器与全局搜索适配扩展来源
This commit is contained in:
@@ -1,4 +1,10 @@
|
||||
import { isMusicMediaSource, isValidMediaSourceId, parseMediaDataSources } from '@/utils/mediaId'
|
||||
import {
|
||||
isMusicMediaSource,
|
||||
isValidMediaSourceId,
|
||||
parseMediaDataSources,
|
||||
setMediaSourceCatalog,
|
||||
supportsMediaSourceType,
|
||||
} from '@/utils/mediaId'
|
||||
import { describe, expect, it } from 'vitest'
|
||||
|
||||
describe('media source identity utils', () => {
|
||||
@@ -6,6 +12,31 @@ describe('media source identity utils', () => {
|
||||
expect(isMusicMediaSource(source)).toBe(true)
|
||||
})
|
||||
|
||||
it('recognizes a plugin-registered music source', () => {
|
||||
setMediaSourceCatalog([
|
||||
{
|
||||
name: 'Acme Music',
|
||||
media_source: 'acme.music',
|
||||
media_types: ['音乐'],
|
||||
},
|
||||
])
|
||||
expect(isMusicMediaSource('acme.music')).toBe(true)
|
||||
setMediaSourceCatalog([])
|
||||
})
|
||||
|
||||
it('keeps a source with mixed video and music capabilities in both lists', () => {
|
||||
const source = {
|
||||
media_types: ['电影', '电视剧', '音乐'],
|
||||
}
|
||||
expect(supportsMediaSourceType(source, 'media')).toBe(true)
|
||||
expect(supportsMediaSourceType(source, 'music')).toBe(true)
|
||||
})
|
||||
|
||||
it('accepts API-style media type keys for plugin sources', () => {
|
||||
expect(supportsMediaSourceType({ media_types: ['movie', 'music'] }, 'media')).toBe(true)
|
||||
expect(supportsMediaSourceType({ media_types: ['movie', 'music'] }, 'music')).toBe(true)
|
||||
})
|
||||
|
||||
it.each([
|
||||
['themoviedb', ['themoviedb']],
|
||||
[' musicbrainz, theaudiodb,unknown,musicbrainz ', ['musicbrainz', 'theaudiodb', 'unknown']],
|
||||
|
||||
+42
-3
@@ -1,9 +1,43 @@
|
||||
import type { MediaDataSource } from '@/api/types'
|
||||
import { ref } from 'vue'
|
||||
import type { MediaDataSource, MediaSourceInfo } 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
|
||||
const MEDIA_SOURCE_PATTERN = /^[a-z][a-z0-9._-]{0,63}$/
|
||||
const mediaSourceCatalog = ref<MediaSourceInfo[]>([])
|
||||
|
||||
const MUSIC_MEDIA_TYPES = new Set(['音乐', 'music'])
|
||||
const VIDEO_MEDIA_TYPES = new Set(['电影', '电视剧', 'movie', 'tv'])
|
||||
|
||||
/** 更新宿主与插件合并后的来源目录,并保留合法来源的稳定顺序。 */
|
||||
export function setMediaSourceCatalog(sources: MediaSourceInfo[]) {
|
||||
const seen = new Set<string>()
|
||||
mediaSourceCatalog.value = sources.reduce<MediaSourceInfo[]>((catalog, source) => {
|
||||
const value = source?.media_source?.toString().trim().toLowerCase()
|
||||
if (!value || !MEDIA_SOURCE_PATTERN.test(value) || seen.has(value)) return catalog
|
||||
seen.add(value)
|
||||
catalog.push({ ...source, media_source: value })
|
||||
return catalog
|
||||
}, [])
|
||||
}
|
||||
|
||||
/** 返回当前已加载的来源目录,供来源选择器构造选项。 */
|
||||
export function getMediaSourceCatalog() {
|
||||
return mediaSourceCatalog
|
||||
}
|
||||
|
||||
/** 判断注册来源是否声明了指定的影视或音乐能力。 */
|
||||
export function supportsMediaSourceType(
|
||||
source: Pick<MediaSourceInfo, 'media_types'>,
|
||||
kind: 'media' | 'music',
|
||||
): boolean {
|
||||
const mediaTypes = (source.media_types ?? [])
|
||||
.filter(type => typeof type === 'string')
|
||||
.map(type => type.trim().toLowerCase())
|
||||
const supportedTypes = kind === 'music' ? MUSIC_MEDIA_TYPES : VIDEO_MEDIA_TYPES
|
||||
return mediaTypes.some(type => supportedTypes.has(type))
|
||||
}
|
||||
|
||||
/** 判断外部输入是否为内置或插件注册的规范媒体来源标识。 */
|
||||
export function isMediaDataSource(value: unknown): value is MediaDataSource {
|
||||
@@ -23,9 +57,14 @@ export function parseMediaDataSources(value: unknown): MediaDataSource[] {
|
||||
]
|
||||
}
|
||||
|
||||
/** 判断当前请求来源是否为内置音乐元数据源。 */
|
||||
/** 判断当前请求来源是否为内置或插件注册的音乐元数据源。 */
|
||||
export function isMusicMediaSource(source?: MediaDataSource): boolean {
|
||||
return MUSIC_MEDIA_SOURCES.includes(source as (typeof MUSIC_MEDIA_SOURCES)[number])
|
||||
const normalized = source?.toString().trim().toLowerCase()
|
||||
if (!normalized) return false
|
||||
return (
|
||||
MUSIC_MEDIA_SOURCES.includes(normalized as (typeof MUSIC_MEDIA_SOURCES)[number]) ||
|
||||
mediaSourceCatalog.value.some(item => item.media_source === normalized && supportsMediaSourceType(item, 'music'))
|
||||
)
|
||||
}
|
||||
|
||||
/** 按媒体数据源校验有固定格式的原生 ID,其余来源只要求非空。 */
|
||||
|
||||
Reference in New Issue
Block a user