From 89da6be8bbdeedb6051fc86cdf7c4dc0c0ab60e6 Mon Sep 17 00:00:00 2001 From: jxxghp Date: Tue, 18 Aug 2026 15:19:56 +0800 Subject: [PATCH] =?UTF-8?q?feat(media-source):=20=E6=94=AF=E6=8C=81?= =?UTF-8?q?=E6=8F=92=E4=BB=B6=E6=B3=A8=E5=86=8C=E7=9A=84=E5=AA=92=E4=BD=93?= =?UTF-8?q?=E6=95=B0=E6=8D=AE=E6=BA=90=EF=BC=8C=E5=90=84=E6=9D=A5=E6=BA=90?= =?UTF-8?q?=E9=80=89=E6=8B=A9=E5=99=A8=E4=B8=8E=E5=85=A8=E5=B1=80=E6=90=9C?= =?UTF-8?q?=E7=B4=A2=E9=80=82=E9=85=8D=E6=89=A9=E5=B1=95=E6=9D=A5=E6=BA=90?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/App.vue | 2 + src/api/types.ts | 7 +++ .../dialog/AddSubtitleDownloadDialog.vue | 8 +++- .../dialog/CacheReidentifyDialog.vue | 6 +++ src/components/dialog/ReorganizeDialog.vue | 7 +++ src/components/dialog/ScrapeDialog.vue | 7 +++ src/components/dialog/SearchBarDialog.vue | 24 ++++++++-- src/composables/useMediaSources.ts | 46 +++++++++++++++++++ src/utils/__tests__/mediaId.spec.ts | 33 ++++++++++++- src/utils/mediaId.ts | 45 ++++++++++++++++-- src/views/setting/AccountSettingSearch.vue | 10 +++- src/views/system/NameTestView.vue | 6 +++ 12 files changed, 190 insertions(+), 11 deletions(-) create mode 100644 src/composables/useMediaSources.ts diff --git a/src/App.vue b/src/App.vue index fd75d99b..916176a7 100644 --- a/src/App.vue +++ b/src/App.vue @@ -32,6 +32,7 @@ import { normalizeThemeMaterialAccent } from '@/utils/glassColor' import { configureApexChartsTheme } from '@/utils/apexCharts' import { useGlobalOfflineStatus, type ConnectionFailureReason } from '@/composables/useOfflineStatus' import { useSystemRestartStatus } from '@/composables/useSystemRestart' +import { loadMediaSources } from '@/composables/useMediaSources' import { useAppActivityLifecycle } from '@/composables/useAppActivityLifecycle' import { useGlassWallpaperTransaction } from '@/composables/useGlassWallpaperTransaction' import { @@ -913,6 +914,7 @@ async function initializeAuthenticatedState() { globalLoadingStateManager.setLoadingState('global-settings', true) await globalSettingsStore.initialize() await globalSettingsStore.loadUserSettings() + await loadMediaSources() } finally { globalLoadingStateManager.setLoadingState('global-settings', false) } diff --git a/src/api/types.ts b/src/api/types.ts index 58e352e2..a2d88ee4 100644 --- a/src/api/types.ts +++ b/src/api/types.ts @@ -17,6 +17,13 @@ export enum MediaSource { export type MediaDataSource = `${MediaSource}` | (string & {}) +/** 后端返回的媒体来源注册描述,供各类来源选择器共享。 */ +export interface MediaSourceInfo { + name: string + media_source: MediaDataSource + media_types: string[] +} + // 手动刮削选项 export interface ManualScrapeOptions { // 媒体数据源 diff --git a/src/components/dialog/AddSubtitleDownloadDialog.vue b/src/components/dialog/AddSubtitleDownloadDialog.vue index 5b32e050..74d40f79 100644 --- a/src/components/dialog/AddSubtitleDownloadDialog.vue +++ b/src/components/dialog/AddSubtitleDownloadDialog.vue @@ -8,6 +8,7 @@ import { useI18n } from 'vue-i18n' import MediaIdSelector from '../misc/MediaIdSelector.vue' import { useGlobalSettingsStore } from '@/stores' import { isMediaDataSource, isValidMediaSourceId } from '@/utils/mediaId' +import { useMediaSources } from '@/composables/useMediaSources' // 多语言支持 const { t } = useI18n() @@ -58,6 +59,8 @@ const normalizedMediaId = computed(() => selectedMediaId.value?.trim() || undefi const hasValidMediaIdentity = computed( () => Boolean(normalizedMediaId.value) && isValidMediaSourceId(normalizedMediaId.value, mediaSource.value), ) +const { mediaSourceItems: getMediaSourceItems } = useMediaSources() +const customMediaSourceItems = getMediaSourceItems('media') const mediaSourceItems = computed<{ title: string; value: MediaDataSource }[]>(() => { const labels: Partial> = { @@ -75,7 +78,10 @@ const mediaSourceItems = computed<{ title: string; value: MediaDataSource }[]>(( migu: 'Migu Video', tencentvideodiscover: 'Tencent Video', } - return Object.values(MediaSource).map(value => ({ title: labels[value] ?? value, value })) + return [ + ...Object.values(MediaSource).map(value => ({ title: labels[value] ?? value, value })), + ...customMediaSourceItems.value, + ] }) // 当前数据源对应的原生ID标签。 diff --git a/src/components/dialog/CacheReidentifyDialog.vue b/src/components/dialog/CacheReidentifyDialog.vue index 781b939a..15f88110 100644 --- a/src/components/dialog/CacheReidentifyDialog.vue +++ b/src/components/dialog/CacheReidentifyDialog.vue @@ -2,6 +2,7 @@ import { useI18n } from 'vue-i18n' import type { MediaDataSource, MusicEntityType } from '@/api/types' import { isMediaDataSource, isMusicMediaSource } from '@/utils/mediaId' +import { useMediaSources } from '@/composables/useMediaSources' const { t } = useI18n() @@ -41,6 +42,9 @@ const mediaSource = ref( const mediaId = ref() const musicType = ref>(props.musicType) const isMusicSelection = computed(() => isMusicMediaSource(mediaSource.value)) +const { mediaSourceItems: getMediaSourceItems } = useMediaSources() +const customMediaSourceItems = getMediaSourceItems('media') +const customMusicSourceItems = getMediaSourceItems('music') const musicEntityItems = computed(() => [ { title: t('setting.cache.musicType.recording'), value: 'recording' }, { title: t('setting.cache.musicType.album'), value: 'album' }, @@ -53,6 +57,8 @@ const mediaSourceItems = computed<{ title: string; value: MediaDataSource }[]>(( { title: t('setting.cache.recognitionSource.musicbrainz'), value: 'musicbrainz' }, { title: t('setting.cache.recognitionSource.theaudiodb'), value: 'theaudiodb' }, { title: t('setting.cache.recognitionSource.doubanmusic'), value: 'doubanmusic' }, + ...customMediaSourceItems.value, + ...customMusicSourceItems.value, ]) const mediaIdLabel = computed(() => { diff --git a/src/components/dialog/ReorganizeDialog.vue b/src/components/dialog/ReorganizeDialog.vue index bf37732a..830a6535 100644 --- a/src/components/dialog/ReorganizeDialog.vue +++ b/src/components/dialog/ReorganizeDialog.vue @@ -24,6 +24,7 @@ import { useI18n } from 'vue-i18n' import { useDisplay } from 'vuetify' import { useGlobalSettingsStore } from '@/stores' import { isMusicMediaSource, isValidMediaSourceId } from '@/utils/mediaId' +import { useMediaSources } from '@/composables/useMediaSources' // 国际化 const { t } = useI18n() @@ -44,6 +45,10 @@ const props = defineProps({ const globalSettingsStore = useGlobalSettingsStore() const globalSettings = globalSettingsStore.globalSettings +const { mediaSourceItems: getMediaSourceItems } = useMediaSources() +const customMediaSourceItems = getMediaSourceItems('media') +const customMusicSourceItems = getMediaSourceItems('music') + const mediaSourceItems = computed<{ title: string; value: MediaDataSource | null }[]>(() => [ { title: t('dialog.reorganize.auto'), value: null }, { title: t('setting.cache.recognitionSource.themoviedb'), value: 'themoviedb' }, @@ -53,6 +58,8 @@ const mediaSourceItems = computed<{ title: string; value: MediaDataSource | null { title: t('setting.cache.recognitionSource.musicbrainz'), value: 'musicbrainz' }, { title: t('setting.cache.recognitionSource.theaudiodb'), value: 'theaudiodb' }, { title: t('setting.cache.recognitionSource.doubanmusic'), value: 'doubanmusic' }, + ...customMediaSourceItems.value, + ...customMusicSourceItems.value, ]) /** 获取后台设置中的默认识别数据源,未知值兼容回退到 TheMovieDb。 */ diff --git a/src/components/dialog/ScrapeDialog.vue b/src/components/dialog/ScrapeDialog.vue index 3eae8d95..f233a6f9 100644 --- a/src/components/dialog/ScrapeDialog.vue +++ b/src/components/dialog/ScrapeDialog.vue @@ -4,6 +4,7 @@ import { useGlobalSettingsStore } from '@/stores' import { useI18n } from 'vue-i18n' import MediaIdSelector from '../misc/MediaIdSelector.vue' import { isMusicMediaSource, isValidMediaSourceId } from '@/utils/mediaId' +import { useMediaSources } from '@/composables/useMediaSources' const { t } = useI18n() @@ -24,6 +25,10 @@ const emit = defineEmits<{ (event: 'update:modelValue', value: boolean): void }>() +const { mediaSourceItems: getMediaSourceItems } = useMediaSources() +const customMediaSourceItems = getMediaSourceItems('media') +const customMusicSourceItems = getMediaSourceItems('music') + const mediaSourceItems = computed<{ title: string; value: MediaDataSource }[]>(() => [ { title: t('setting.cache.recognitionSource.themoviedb'), value: 'themoviedb' }, { title: t('setting.cache.recognitionSource.douban'), value: 'douban' }, @@ -32,6 +37,8 @@ const mediaSourceItems = computed<{ title: string; value: MediaDataSource }[]>(( { title: t('setting.cache.recognitionSource.musicbrainz'), value: 'musicbrainz' }, { title: t('setting.cache.recognitionSource.theaudiodb'), value: 'theaudiodb' }, { title: t('setting.cache.recognitionSource.doubanmusic'), value: 'doubanmusic' }, + ...customMediaSourceItems.value, + ...customMusicSourceItems.value, ]) const globalSettingsStore = useGlobalSettingsStore() diff --git a/src/components/dialog/SearchBarDialog.vue b/src/components/dialog/SearchBarDialog.vue index bf955229..2d7d3fbc 100644 --- a/src/components/dialog/SearchBarDialog.vue +++ b/src/components/dialog/SearchBarDialog.vue @@ -1,6 +1,6 @@