feat: support selectable anime metadata sources

This commit is contained in:
jxxghp
2026-07-21 11:32:16 +08:00
parent eee83542df
commit 00c0c86345
17 changed files with 408 additions and 270 deletions

View File

@@ -1,3 +1,5 @@
export type MediaDataSource = 'themoviedb' | 'douban' | 'bangumi' | 'anilist'
// 订阅
export interface Subscribe {
// 订阅ID
@@ -218,6 +220,10 @@ export interface TransferHistory {
tvdbid?: number
// 豆瓣ID
doubanid?: string
// 媒体数据源
media_source?: MediaDataSource
// 数据源原生ID
media_id?: string
// 季Sxx
seasons?: string
// 集Exx
@@ -238,7 +244,7 @@ export interface TransferHistory {
// 媒体信息
export interface MediaInfo {
// 来源themoviedb、douban、bangumi
// 来源themoviedb、douban、bangumi、anilist
source?: string
// 类型 电影、电视剧、合集
type?: string
@@ -259,7 +265,11 @@ export interface MediaInfo {
// 豆瓣ID
douban_id?: string
// Bangumi ID
bangumi_id?: string
bangumi_id?: string | number
// AniList ID
anilist_id?: number
// AniDB ID
anidb_id?: number
// 合集ID
collection_id?: number
// 其它媒体ID前缀
@@ -1353,6 +1363,8 @@ export interface MediaServerConf {
enabled: boolean
// 同步媒体体库列表
sync_libraries?: string[]
// 自动同步间隔(小时),为空时使用旧全局配置
sync_interval?: number | null
}
// 文件整理目录配置
@@ -1513,6 +1525,10 @@ export interface TransferForm {
tmdbid?: number
// 豆瓣 ID
doubanid?: string
// 媒体数据源
media_source?: MediaDataSource
// 数据源原生ID
media_id?: string | null
// 季号
season?: number
// 类型

View File

@@ -25,6 +25,11 @@ const props = defineProps({
type: Array as PropType<MediaServerConf[]>,
required: true,
},
// 旧版全局同步间隔,用作服务器未单独设置时的默认值
defaultSyncInterval: {
type: Number,
default: null,
},
})
// 定义触发的自定义事件
@@ -56,6 +61,7 @@ function openMediaServerInfoDialog() {
{
mediaserver: props.mediaserver,
mediaservers: props.mediaservers,
defaultSyncInterval: props.defaultSyncInterval,
},
{
change: (...args: unknown[]) => emit('change', ...args),

View File

@@ -2,7 +2,7 @@
import { useToast } from 'vue-toastification'
import api from '@/api'
import { doneNProgress, startNProgress } from '@/api/nprogress'
import type { DownloaderConf, MediaInfo, TorrentInfo, TransferDirectoryConf } from '@/api/types'
import type { DownloaderConf, MediaDataSource, MediaInfo, TorrentInfo, TransferDirectoryConf } from '@/api/types'
import { formatFileSize } from '@/@core/utils/formatters'
import { VCardTitle, VChip } from 'vuetify/lib/components/index.mjs'
import { useI18n } from 'vue-i18n'
@@ -18,7 +18,11 @@ const globalSettingsStore = useGlobalSettingsStore()
const globalSettings = globalSettingsStore.globalSettings
// 当前识别类型
const mediaSource = ref(globalSettings.RECOGNIZE_SOURCE || 'themoviedb')
const mediaSource = ref<MediaDataSource>(
['themoviedb', 'douban', 'bangumi', 'anilist'].includes(globalSettings.RECOGNIZE_SOURCE)
? globalSettings.RECOGNIZE_SOURCE
: 'themoviedb',
)
// 输入参数
const props = defineProps({
@@ -51,11 +55,19 @@ const loading = ref(false)
// 是否显示高级选项
const showAdvancedOptions = ref(false)
// TMDB ID
const tmdbid = ref<number | undefined>(undefined)
// 当前数据源的原生媒体ID
const mediaId = ref<string | undefined>(undefined)
// 豆瓣ID
const doubanId = ref<string | undefined>(undefined)
// 当前数据源对应的原生ID标签。
const mediaIdLabel = computed(() => {
const labels: Record<MediaDataSource, string> = {
themoviedb: t('dialog.reorganize.tmdbId'),
douban: t('dialog.reorganize.doubanId'),
bangumi: t('dialog.reorganize.bangumiId'),
anilist: t('dialog.reorganize.anilistId'),
}
return labels[mediaSource.value]
})
// TMDB选择对话框
const mediaSelectorDialog = ref(false)
@@ -140,11 +152,9 @@ async function addDownload() {
}
// 添加媒体ID辅助识别
if (tmdbid.value) {
payload.tmdbid = tmdbid.value
}
if (doubanId.value) {
payload.doubanid = doubanId.value
if (mediaId.value) {
payload.media_source = mediaSource.value
payload.media_id = mediaId.value
}
const endpoint = props.media ? 'download/' : 'download/add'
@@ -269,23 +279,8 @@ onMounted(() => {
<VRow v-show="showAdvancedOptions" class="px-5">
<VCol cols="12">
<VTextField
v-if="mediaSource === 'themoviedb'"
v-model="tmdbid"
:label="t('dialog.reorganize.tmdbId')"
:placeholder="t('dialog.reorganize.mediaIdPlaceholder')"
:rules="[numberValidator]"
append-inner-icon="mdi-magnify"
:hint="t('dialog.reorganize.mediaIdHint')"
persistent-hint
prepend-inner-icon="mdi-identifier"
variant="underlined"
density="comfortable"
@click:append-inner="mediaSelectorDialog = true"
/>
<VTextField
v-else
v-model="doubanId"
:label="t('dialog.reorganize.doubanId')"
v-model="mediaId"
:label="mediaIdLabel"
:placeholder="t('dialog.reorganize.mediaIdPlaceholder')"
:rules="[numberValidator]"
append-inner-icon="mdi-magnify"
@@ -307,13 +302,7 @@ onMounted(() => {
</VCard>
<!-- 媒体ID选择器 -->
<VDialog v-model="mediaSelectorDialog" width="40rem" scrollable max-height="85vh">
<MediaIdSelector
v-if="mediaSource === 'themoviedb'"
v-model="tmdbid"
@close="mediaSelectorDialog = false"
:type="mediaSource"
/>
<MediaIdSelector v-else v-model="doubanId" @close="mediaSelectorDialog = false" :type="mediaSource" />
<MediaIdSelector v-model="mediaId" @close="mediaSelectorDialog = false" :type="mediaSource" />
</VDialog>
</VDialog>
</template>

View File

@@ -2,7 +2,7 @@
import { useToast } from 'vue-toastification'
import api from '@/api'
import { doneNProgress, startNProgress } from '@/api/nprogress'
import type { SubtitleInfo, TransferDirectoryConf } from '@/api/types'
import type { MediaDataSource, SubtitleInfo, TransferDirectoryConf } from '@/api/types'
import { formatFileSize } from '@/@core/utils/formatters'
import { useI18n } from 'vue-i18n'
import MediaIdSelector from '../misc/MediaIdSelector.vue'
@@ -17,7 +17,11 @@ const globalSettingsStore = useGlobalSettingsStore()
const globalSettings = globalSettingsStore.globalSettings
// 当前识别类型
const mediaSource = ref(globalSettings.RECOGNIZE_SOURCE || 'themoviedb')
const mediaSource = ref<MediaDataSource>(
['themoviedb', 'douban', 'bangumi', 'anilist'].includes(globalSettings.RECOGNIZE_SOURCE)
? globalSettings.RECOGNIZE_SOURCE
: 'themoviedb',
)
// 输入参数
const props = defineProps({
@@ -43,11 +47,19 @@ const loading = ref(false)
// 是否显示高级选项
const showAdvancedOptions = ref(false)
// TMDB ID
const tmdbid = ref<number | undefined>(undefined)
// 当前数据源的原生媒体ID
const mediaId = ref<string | undefined>(undefined)
// 豆瓣ID
const doubanId = ref<string | undefined>(undefined)
// 当前数据源对应的原生ID标签。
const mediaIdLabel = computed(() => {
const labels: Record<MediaDataSource, string> = {
themoviedb: t('dialog.reorganize.tmdbId'),
douban: t('dialog.reorganize.doubanId'),
bangumi: t('dialog.reorganize.bangumiId'),
anilist: t('dialog.reorganize.anilistId'),
}
return labels[mediaSource.value]
})
// TMDB选择对话框
const mediaSelectorDialog = ref(false)
@@ -98,11 +110,9 @@ async function addSubtitleDownload() {
save_path: selectedDirectory.value,
}
if (tmdbid.value) {
payload.tmdbid = tmdbid.value
}
if (doubanId.value) {
payload.doubanid = doubanId.value
if (mediaId.value) {
payload.media_source = mediaSource.value
payload.media_id = mediaId.value
}
const result: { [key: string]: any } = await api.post('download/subtitle', payload)
@@ -221,23 +231,8 @@ onMounted(() => {
<VRow v-show="showAdvancedOptions" class="px-5">
<VCol cols="12">
<VTextField
v-if="mediaSource === 'themoviedb'"
v-model="tmdbid"
:label="t('dialog.reorganize.tmdbId')"
:placeholder="t('dialog.reorganize.mediaIdPlaceholder')"
:rules="[numberValidator]"
append-inner-icon="mdi-magnify"
:hint="t('dialog.reorganize.mediaIdHint')"
persistent-hint
prepend-inner-icon="mdi-identifier"
variant="underlined"
density="comfortable"
@click:append-inner="mediaSelectorDialog = true"
/>
<VTextField
v-else
v-model="doubanId"
:label="t('dialog.reorganize.doubanId')"
v-model="mediaId"
:label="mediaIdLabel"
:placeholder="t('dialog.reorganize.mediaIdPlaceholder')"
:rules="[numberValidator]"
append-inner-icon="mdi-magnify"
@@ -258,13 +253,7 @@ onMounted(() => {
</VCardText>
</VCard>
<VDialog v-model="mediaSelectorDialog" width="40rem" scrollable max-height="85vh">
<MediaIdSelector
v-if="mediaSource === 'themoviedb'"
v-model="tmdbid"
@close="mediaSelectorDialog = false"
:type="mediaSource"
/>
<MediaIdSelector v-else v-model="doubanId" @close="mediaSelectorDialog = false" :type="mediaSource" />
<MediaIdSelector v-model="mediaId" @close="mediaSelectorDialog = false" :type="mediaSource" />
</VDialog>
</VDialog>
</template>

View File

@@ -26,6 +26,10 @@ const props = defineProps({
type: Array as PropType<MediaServerConf[]>,
required: true,
},
defaultSyncInterval: {
type: Number,
default: null,
},
})
// 定义触发的自定义事件
@@ -203,6 +207,20 @@ onMounted(() => {
prepend-inner-icon="mdi-key"
/>
</VCol>
<VCol cols="12" md="6">
<VTextField
v-model.number="mediaServerInfo.sync_interval"
type="number"
min="0"
step="1"
clearable
:label="t('mediaserver.syncInterval')"
:hint="t('mediaserver.syncIntervalHint', { interval: props.defaultSyncInterval ?? 6 })"
persistent-hint
suffix="h"
prepend-inner-icon="mdi-sync"
/>
</VCol>
<VCol cols="12">
<VAutocomplete
v-model="mediaServerInfo.sync_libraries"
@@ -243,7 +261,7 @@ onMounted(() => {
prepend-inner-icon="mdi-server"
/>
</VCol>
<VCol cols="12">
<VCol cols="6">
<VTextField
v-model="mediaServerInfo.config.play_host"
:label="t('mediaserver.playHost')"
@@ -274,6 +292,20 @@ onMounted(() => {
prepend-inner-icon="mdi-lock"
/>
</VCol>
<VCol cols="12" md="6">
<VTextField
v-model.number="mediaServerInfo.sync_interval"
type="number"
min="0"
step="1"
clearable
:label="t('mediaserver.syncInterval')"
:hint="t('mediaserver.syncIntervalHint', { interval: props.defaultSyncInterval ?? 6 })"
persistent-hint
suffix="h"
prepend-inner-icon="mdi-sync"
/>
</VCol>
<VCol cols="12">
<VAutocomplete
v-model="mediaServerInfo.sync_libraries"
@@ -335,6 +367,20 @@ onMounted(() => {
prepend-inner-icon="mdi-key"
/>
</VCol>
<VCol cols="12" md="6">
<VTextField
v-model.number="mediaServerInfo.sync_interval"
type="number"
min="0"
step="1"
clearable
:label="t('mediaserver.syncInterval')"
:hint="t('mediaserver.syncIntervalHint', { interval: props.defaultSyncInterval ?? 6 })"
persistent-hint
suffix="h"
prepend-inner-icon="mdi-sync"
/>
</VCol>
<VCol cols="12">
<VAutocomplete
v-model="mediaServerInfo.sync_libraries"
@@ -375,7 +421,7 @@ onMounted(() => {
prepend-inner-icon="mdi-server"
/>
</VCol>
<VCol cols="12">
<VCol cols="6">
<VTextField
v-model="mediaServerInfo.config.play_host"
:label="t('mediaserver.playHost')"
@@ -403,6 +449,20 @@ onMounted(() => {
prepend-inner-icon="mdi-lock"
/>
</VCol>
<VCol cols="12" md="6">
<VTextField
v-model.number="mediaServerInfo.sync_interval"
type="number"
min="0"
step="1"
clearable
:label="t('mediaserver.syncInterval')"
:hint="t('mediaserver.syncIntervalHint', { interval: props.defaultSyncInterval ?? 6 })"
persistent-hint
suffix="h"
prepend-inner-icon="mdi-sync"
/>
</VCol>
<VCol cols="12">
<VAutocomplete
v-model="mediaServerInfo.sync_libraries"
@@ -443,7 +503,7 @@ onMounted(() => {
prepend-inner-icon="mdi-server"
/>
</VCol>
<VCol cols="12">
<VCol cols="6">
<VTextField
v-model="mediaServerInfo.config.play_host"
:label="t('mediaserver.playHost')"
@@ -471,20 +531,18 @@ onMounted(() => {
prepend-inner-icon="mdi-lock"
/>
</VCol>
<VCol cols="12">
<VAutocomplete
v-model="mediaServerInfo.sync_libraries"
:label="t('mediaserver.syncLibraries')"
:items="librariesOptions"
chips
multiple
<VCol cols="12" md="6">
<VTextField
v-model.number="mediaServerInfo.sync_interval"
type="number"
min="0"
step="1"
clearable
:hint="t('mediaserver.syncLibrariesHint')"
:label="t('mediaserver.syncInterval')"
:hint="t('mediaserver.syncIntervalHint', { interval: props.defaultSyncInterval ?? 6 })"
persistent-hint
active
append-inner-icon="mdi-refresh"
prepend-inner-icon="mdi-library"
@click:append-inner="loadLibrary(mediaServerInfo.name)"
suffix="h"
prepend-inner-icon="mdi-sync"
/>
</VCol>
<VCol cols="12" md="6">
@@ -508,6 +566,22 @@ onMounted(() => {
inset
/>
</VCol>
<VCol cols="12">
<VAutocomplete
v-model="mediaServerInfo.sync_libraries"
:label="t('mediaserver.syncLibraries')"
:items="librariesOptions"
chips
multiple
clearable
:hint="t('mediaserver.syncLibrariesHint')"
persistent-hint
active
append-inner-icon="mdi-refresh"
prepend-inner-icon="mdi-library"
@click:append-inner="loadLibrary(mediaServerInfo.name)"
/>
</VCol>
</VRow>
<VRow v-else-if="mediaServerInfo.type == 'plex'">
<VCol cols="12" md="6">
@@ -553,6 +627,20 @@ onMounted(() => {
prepend-inner-icon="mdi-key"
/>
</VCol>
<VCol cols="12" md="6">
<VTextField
v-model.number="mediaServerInfo.sync_interval"
type="number"
min="0"
step="1"
clearable
:label="t('mediaserver.syncInterval')"
:hint="t('mediaserver.syncIntervalHint', { interval: props.defaultSyncInterval ?? 6 })"
persistent-hint
suffix="h"
prepend-inner-icon="mdi-sync"
/>
</VCol>
<VCol cols="12">
<VAutocomplete
v-model="mediaServerInfo.sync_libraries"

View File

@@ -10,6 +10,7 @@ import {
ManualTransferPayload,
ManualTransferPreviewData,
ManualTransferPreviewItem,
MediaDataSource,
MediaInfo,
StorageConf,
TransferDirectoryConf,
@@ -37,13 +38,22 @@ const props = defineProps({
target_path: String,
})
// 从 provide 中获取全局设置
// 全局设置
const globalSettingsStore = useGlobalSettingsStore()
const globalSettings = globalSettingsStore.globalSettings
// 当前识别类型
const mediaSource = ref(globalSettings.RECOGNIZE_SOURCE || 'themoviedb')
const mediaSourceItems: { title: string; value: MediaDataSource }[] = [
{ title: 'TheMovieDb', value: 'themoviedb' },
{ title: '豆瓣', value: 'douban' },
{ title: 'Bangumi', value: 'bangumi' },
{ title: 'AniList', value: 'anilist' },
]
// 获取后台设置中的默认识别数据源未知值兼容回退到TheMovieDb。
function getDefaultMediaSource(): MediaDataSource {
const configuredSource = globalSettings.RECOGNIZE_SOURCE as MediaDataSource
return mediaSourceItems.some(item => item.value === configuredSource) ? configuredSource : 'themoviedb'
}
// 定义事件
const emit = defineEmits(['done', 'close'])
@@ -304,6 +314,8 @@ const transferForm = reactive<TransferForm>({
logid: 0,
target_storage: initialTargetPath ? (props.target_storage ?? 'local') : null,
target_path: initialTargetPath,
media_source: getDefaultMediaSource(),
media_id: null,
transfer_type: null,
min_filesize: 0,
scrape: initialTargetPath ? false : null,
@@ -313,6 +325,20 @@ const transferForm = reactive<TransferForm>({
episode_group: null,
})
// 当前手动识别与刮削数据源。
const mediaSource = computed(() => transferForm.media_source ?? 'themoviedb')
// 当前数据源对应的原生ID标签。
const mediaIdLabel = computed(() => {
const labels: Record<MediaDataSource, string> = {
themoviedb: t('dialog.reorganize.tmdbId'),
douban: t('dialog.reorganize.doubanId'),
bangumi: t('dialog.reorganize.bangumiId'),
anilist: t('dialog.reorganize.anilistId'),
}
return labels[mediaSource.value]
})
// 处理媒体搜索结果选择,同步搜索结果中已识别的媒体类型。
function handleMediaSelected(item: Pick<MediaInfo, 'type'>) {
const typeName = resolveTransferMediaType(item.type)
@@ -403,28 +429,39 @@ watch(
},
)
// 监听 TMDB 编号变化,自动加载可用剧集组并清空旧选择
// 监听媒体编号变化仅在TMDB电视剧场景加载剧集组
watch(
() => transferForm.tmdbid,
tmdbid => {
() => transferForm.media_id,
mediaId => {
transferForm.episode_group = null
episodeGroups.value = []
if (episodeGroupQueryTimer) clearTimeout(episodeGroupQueryTimer)
if (transferForm.type_name !== '电视剧' || mediaSource.value !== 'themoviedb') return
episodeGroupQueryTimer = setTimeout(() => getEpisodeGroups(tmdbid), 400)
episodeGroupQueryTimer = setTimeout(() => getEpisodeGroups(mediaId ?? undefined), 400)
},
)
// 切换媒体类型或识别源时,非 TMDB 电视剧不保留剧集组选择。
watch([() => transferForm.type_name, () => mediaSource.value], ([typeName, source]) => {
if (typeName === '电视剧' && source === 'themoviedb' && transferForm.tmdbid) {
getEpisodeGroups(transferForm.tmdbid)
if (typeName === '电视剧' && source === 'themoviedb' && transferForm.media_id) {
getEpisodeGroups(transferForm.media_id)
return
}
transferForm.episode_group = null
episodeGroups.value = []
})
// 切换数据源时清空上一来源的原生ID避免把同一数字误传给新来源。
watch(
() => transferForm.media_source,
(source, previousSource) => {
if (previousSource && source !== previousSource) {
transferForm.media_id = null
mediaSelectorDialog.value = false
}
},
)
watch(
() => transferForm.episode_group,
episodeGroup => {
@@ -859,6 +896,8 @@ function createTransferPayload(options: { item?: FileItem; items?: FileItem[]; l
target_storage: normalizeOptionalText(transferForm.target_storage),
target_path: normalizeTargetPath(transferForm.target_path),
transfer_type: normalizeOptionalText(transferForm.transfer_type),
media_source: mediaSource.value,
media_id: normalizeOptionalText(transferForm.media_id),
episode_group: normalizeEpisodeGroup(transferForm.episode_group),
}
@@ -1385,7 +1424,7 @@ onUnmounted(() => {
</VCol>
</VRow>
<VRow>
<VCol cols="12" md="6">
<VCol cols="12" md="4">
<VSelect
v-model="transferForm.type_name"
:label="t('dialog.reorganize.mediaType')"
@@ -1399,25 +1438,21 @@ onUnmounted(() => {
prepend-inner-icon="mdi-movie-open"
/>
</VCol>
<VCol cols="12" md="6">
<VTextField
v-if="mediaSource === 'themoviedb'"
v-model="transferForm.tmdbid"
:disabled="transferForm.type_name === ''"
:label="t('dialog.reorganize.tmdbId')"
:placeholder="t('dialog.reorganize.mediaIdPlaceholder')"
:rules="[numberValidator]"
append-inner-icon="mdi-magnify"
:hint="t('dialog.reorganize.mediaIdHint')"
<VCol cols="12" md="4">
<VSelect
v-model="transferForm.media_source"
:items="mediaSourceItems"
:label="t('dialog.reorganize.mediaSource')"
:hint="t('dialog.reorganize.mediaSourceHint')"
persistent-hint
prepend-inner-icon="mdi-identifier"
@click:append-inner="mediaSelectorDialog = true"
prepend-inner-icon="mdi-database-search"
/>
</VCol>
<VCol cols="12" md="4">
<VTextField
v-else
v-model="transferForm.doubanid"
v-model="transferForm.media_id"
:disabled="transferForm.type_name === ''"
:label="t('dialog.reorganize.doubanId')"
:label="mediaIdLabel"
:placeholder="t('dialog.reorganize.mediaIdPlaceholder')"
:rules="[numberValidator]"
append-inner-icon="mdi-magnify"
@@ -1437,7 +1472,7 @@ onUnmounted(() => {
item-value="value"
:item-props="episodeGroupItemProps"
:loading="episodeGroupLoading"
:disabled="!transferForm.tmdbid"
:disabled="!transferForm.media_id"
clearable
:label="t('dialog.reorganize.episodeGroup')"
:placeholder="t('dialog.reorganize.episodeGroupPlaceholder')"
@@ -1744,18 +1779,10 @@ onUnmounted(() => {
</VCard>
<!-- 手动整理进度框 -->
<ProgressDialog v-if="progressDialog" v-model="progressDialog" :text="progressText" :value="progressValue" />
<!-- TMDB ID搜索框 -->
<!-- 媒体数据源ID搜索框 -->
<VDialog v-model="mediaSelectorDialog" width="40rem" scrollable max-height="85vh">
<MediaIdSelector
v-if="mediaSource === 'themoviedb'"
v-model="transferForm.tmdbid"
@close="mediaSelectorDialog = false"
@select="handleMediaSelected"
:type="mediaSource"
/>
<MediaIdSelector
v-else
v-model="transferForm.doubanid"
v-model="transferForm.media_id"
@close="mediaSelectorDialog = false"
@select="handleMediaSelected"
:type="mediaSource"

View File

@@ -1,21 +1,22 @@
<script lang="ts" setup>
import api from '@/api'
import type { MediaInfo } from '@/api/types'
import type { MediaDataSource, MediaInfo } from '@/api/types'
import { useI18n } from 'vue-i18n'
const { t } = useI18n()
// 定义输入变量
const props = defineProps({
type: String, // 来源 themoviedb | douban
})
const props = defineProps<{
type?: MediaDataSource
}>()
interface TmdbItem {
interface MediaSelectorItem {
// 数据源原生ID
id: string
// 媒体标题
title: string
// 媒体简介,包含类型标签
overview: string
// TMDB ID
tmdbid: number
// 豆瓣 ID
doubanid: string
// 海报地址
poster: string
// 媒体类型
@@ -25,7 +26,7 @@ interface TmdbItem {
// update:modelValue 事件
const emit = defineEmits(['update:modelValue', 'select', 'close'])
const items = ref<TmdbItem[]>([])
const items = ref<MediaSelectorItem[]>([])
// 搜索词
const keyword = ref('')
@@ -37,8 +38,8 @@ const loading = ref(false)
const inputKeyword = ref<HTMLElement | null>(null)
// 选中条目并通知父组件同步额外媒体信息。
function selectMedia(item: TmdbItem) {
emit('update:modelValue', item.tmdbid || item.doubanid)
function selectMedia(item: MediaSelectorItem) {
emit('update:modelValue', item.id)
emit('select', item)
emit('close')
}
@@ -51,16 +52,18 @@ function getW500Image(url = '') {
// 搜索词条
async function searchMedias() {
if (!keyword) return
const searchKeyword = keyword.value.trim()
if (!searchKeyword) return
// 调用API搜索词条
try {
loading.value = true
const result: MediaInfo[] = await api.get('media/search', {
params: {
title: keyword.value,
title: searchKeyword,
page: 1,
count: 20,
source: props.type,
},
})
@@ -69,19 +72,25 @@ async function searchMedias() {
// 赋值
for (const item of result) {
if (props.type && props.type !== item.source) continue
const mediaId =
item.media_id ||
item.tmdb_id?.toString() ||
item.douban_id ||
item.bangumi_id?.toString() ||
item.anilist_id?.toString()
if (!mediaId) continue
items.value.push({
tmdbid: item.tmdb_id || 0,
doubanid: item.douban_id || '',
id: mediaId,
poster: getW500Image(item.poster_path),
type: item.type,
title: `${item.title}${item.year}`,
title: item.year ? `${item.title}${item.year}` : item.title || '',
overview: `<span class="text-primary">${item.type}</span> ${item.overview}`,
})
}
loading.value = false
} catch (e) {
console.error(e)
} finally {
loading.value = false
}
}
@@ -100,9 +109,9 @@ onMounted(() => {
<VTextField
ref="inputKeyword"
v-model="keyword"
label="输入名称搜索"
:label="t('dialog.reorganize.mediaSearchInput')"
single-line
placeholder="电影或电视剧名称"
:placeholder="t('dialog.reorganize.mediaSearchPlaceholder')"
variant="solo"
prepend-inner-icon="mdi-magnify"
flat

View File

@@ -54,6 +54,7 @@ export function getMediaSubscribeId(media?: MediaInfo) {
if (media?.tmdb_id) return `tmdb:${media.tmdb_id}`
if (media?.douban_id) return `douban:${media.douban_id}`
if (media?.bangumi_id) return `bangumi:${media.bangumi_id}`
if (media?.anilist_id) return `anilist:${media.anilist_id}`
return `${media?.mediaid_prefix}:${media?.media_id}`
}

View File

@@ -1536,7 +1536,11 @@ export default {
recognizing: 'Recognizing...',
recognizeAgain: 'Recognize Again',
title: 'Title',
titleHint: 'Enter a torrent name, release title, or file name',
subtitle: 'Subtitle',
subtitleHint: 'Optional torrent description, alias, or release details to improve recognition accuracy',
source: 'Recognition Source',
sourceHint: 'Uses the backend recognition setting by default; switch it for this test only',
customWords: 'Custom Words',
customWordsPlaceholder: 'Enter one recognition rule per line; applied directly to this recognition test',
customWordsHint:
@@ -1546,8 +1550,6 @@ export default {
saveWordsNoChange: 'These words already exist, no need to save again',
saveWordsFailed: 'Failed to save custom words',
requestFailed: 'Recognition request failed',
inputTitle: 'Test Input',
inputSubtitle: 'Enter a torrent or file name to inspect the recognition breakdown',
unrecognized: 'No media recognized',
waitingResult: 'Waiting for recognition result',
analysisTitle: 'Analysis Flow',
@@ -1589,14 +1591,15 @@ export default {
testing: 'Testing...',
testAgain: 'Test Again',
title: 'Title',
titleHint: 'Enter a release title to simulate a search or download result',
subtitle: 'Subtitle',
subtitleHint: 'Optional release description, tags, or site subtitle used for recognition and filtering',
ruleGroup: 'Rule Group',
ruleGroupHint: 'Select the filter rule group to validate',
ruleGroupPlaceholder: 'Please select',
priority: 'Priority: {value}',
noPriorityRule: 'No priority rule matched!',
requestFailed: 'Rule test request failed',
inputTitle: 'Rule Test',
inputSubtitle: 'Select a rule group to inspect filter matching and priority',
waitingResult: 'Waiting for rule test result',
matched: 'Filter rule matched',
priorityLabel: 'Priority',
@@ -1692,12 +1695,6 @@ export default {
wallpaperHint: 'Choose the source of the login page background',
recognizeSource: 'Recognition Data Source',
recognizeSourceHint: 'Set the default media info recognition data source',
mediaServerSyncInterval: 'Media Server Sync Interval',
mediaServerSyncIntervalHint: 'Time interval for syncing media server data to local',
hours: 'hours',
required: 'Required field, please fill in',
numbersOnly: 'Only numbers are supported, please do not enter other characters',
minInterval: 'Interval cannot be less than 1 hour',
apiToken: 'API Token',
apiTokenHint: 'Set the token value used when external requests access MoviePilot API',
apiTokenMinChars: 'Cannot be less than 16 characters',
@@ -2127,6 +2124,8 @@ export default {
userAgentHint: 'User-Agent of the browser with CookieCloud plugin',
browserEmulation: 'Browser Emulation',
browserEmulationHint: 'Choose how to emulate browser when accessing sites (CloakBrowser or FlareSolverr)',
ocrHost: 'OCR Server',
ocrHostHint: 'Used for site check-in, cookie updates, and other captcha recognition tasks',
flaresolverrUrl: 'FlareSolverr URL',
flaresolverrUrlHint: 'Required when using FlareSolverr, e.g. http://127.0.0.1:8191',
siteDataRefresh: 'Site Data Refresh',
@@ -2238,7 +2237,7 @@ export default {
'Word to replace => Replacement\n' +
'Front word <> Back word >> Episode offset (EP)\n' +
'Word to replace => Replacement && Front word <> Back word >> Episode offset (EP)\n' +
'Replacement format supports: &#123;[tmdbid/doubanid=xxx;type=movie/tv;g=xxx;s=xxx;e=xxx]&#125; to directly specify TMDBID/Douban ID, where g is the episode group ID and s/e are season and episode numbers (all optional)',
'Replacement format supports: &#123;[tmdbid/doubanid/bangumiid/anilistid=xxx;type=movie/tv;g=xxx;s=xxx;e=xxx]&#125; to directly specify a media data source ID, where g is the episode group ID and s/e are season and episode numbers (all optional)',
identifierSaveSuccess: 'Custom identifiers saved successfully',
identifierSaveFailed: 'Failed to save custom identifiers!',
@@ -3037,10 +3036,16 @@ export default {
targetPathPlaceholder: 'Choose Auto or enter a path',
mediaType: 'Type',
mediaTypeHint: 'File media type',
mediaSource: 'Data Source',
mediaSourceHint: 'Uses the backend recognition setting by default; switch it for this organization and scrape',
tmdbId: 'TheMovieDb ID',
doubanId: 'Douban ID',
bangumiId: 'Bangumi ID',
anilistId: 'AniList ID',
mediaIdHint: 'Query media ID by name, leave empty for auto recognition',
mediaIdPlaceholder: 'Leave empty for auto recognition',
mediaSearchInput: 'Search Media',
mediaSearchPlaceholder: 'Enter a media title',
episodeGroup: 'Episode Group',
episodeGroupHint:
'After entering a TMDB ID, episode groups are queried automatically; group IDs can still be entered manually',
@@ -3175,7 +3180,7 @@ export default {
customWords: 'Custom Recognition Words',
customWordsHint: 'Recognition words only used for this subscription',
customWordsPlaceholder:
'Block word\nReplaced word => Replacement word\nPrefix <> Suffix >> Episode offset (EP)\nReplaced word => Replacement word && Prefix <> Suffix >> Episode offset (EP)\nReplacement word supports format: &#123;[tmdbid/doubanid=xxx;type=movie/tv;g=xxx;s=xxx;e=xxx]&#125; to directly specify TMDBID/Douban ID recognition, where g is the episode group ID and s/e are season and episode numbers (all optional)',
'Block word\nReplaced word => Replacement word\nPrefix <> Suffix >> Episode offset (EP)\nReplaced word => Replacement word && Prefix <> Suffix >> Episode offset (EP)\nReplacement word supports format: &#123;[tmdbid/doubanid/bangumiid/anilistid=xxx;type=movie/tv;g=xxx;s=xxx;e=xxx]&#125; to directly specify a media data source ID, where g is the episode group ID and s/e are season and episode numbers (all optional)',
cancelSubscribe: 'Cancel Subscription',
save: 'Save',
cancelSubscribeConfirm: 'Are you sure you want to cancel the subscription?',
@@ -3728,6 +3733,9 @@ export default {
type: 'Type',
customTypeHint: 'Custom media server type, for plugin scenarios',
enableMediaServer: 'Enable Media Server',
syncInterval: 'Automatic Sync Interval',
syncIntervalHint:
'Leave blank to use the legacy global default ({interval} hours); set to 0 to disable automatic sync for this server',
nameRequired: 'Required; cannot be duplicated',
serverAlias: 'Media server alias',
host: 'Host',

View File

@@ -1529,7 +1529,11 @@ export default {
recognizing: '识别中...',
recognizeAgain: '重新识别',
title: '标题',
titleHint: '输入种子名、发布标题或文件名',
subtitle: '副标题',
subtitleHint: '可选,补充种子描述、别名或发布信息以提高识别准确度',
source: '识别数据源',
sourceHint: '默认使用后台识别设置,可为本次测试单独切换',
customWords: '识别词',
customWordsPlaceholder: '每行输入一组识别规则,可直接用于本次识别测试',
customWordsHint: '格式与"识别词管理"一致:屏蔽词 / 被替换词 => 替换词 / 前定位词 <> 后定位词 >> 集偏移量',
@@ -1538,8 +1542,6 @@ export default {
saveWordsNoChange: '识别词已存在,无需重复保存',
saveWordsFailed: '识别词保存失败',
requestFailed: '识别请求失败',
inputTitle: '测试输入',
inputSubtitle: '输入种子名或文件名,查看媒体识别拆解结果',
unrecognized: '未识别到媒体',
waitingResult: '等待识别结果',
analysisTitle: '解析链路',
@@ -1581,14 +1583,15 @@ export default {
testing: '正在测试...',
testAgain: '重新测试',
title: '标题',
titleHint: '输入用于模拟搜索或下载结果的发布标题',
subtitle: '副标题',
subtitleHint: '可选,补充发布描述、标签或站点副标题以参与识别和过滤',
ruleGroup: '规则组',
ruleGroupHint: '选择要验证的过滤规则组',
ruleGroupPlaceholder: '请选择',
priority: '优先级:{value}',
noPriorityRule: '未命中任何优先级规则!',
requestFailed: '规则测试请求失败',
inputTitle: '规则测试',
inputSubtitle: '选择规则组后,查看过滤命中和优先级结果',
waitingResult: '等待规则测试结果',
matched: '命中过滤规则',
priorityLabel: '优先级',
@@ -1684,12 +1687,6 @@ export default {
wallpaperHint: '选择登陆页面背景来源',
recognizeSource: '识别数据源',
recognizeSourceHint: '设置默认媒体信息识别数据源',
mediaServerSyncInterval: '媒体服务器同步间隔',
mediaServerSyncIntervalHint: '定时同步媒体服务器数据到本地的时间间隔',
hours: '小时',
required: '必选项,请勿留空',
numbersOnly: '仅支持输入数字,请勿输入其他字符',
minInterval: '间隔不能小于1个小时',
apiToken: 'API令牌',
apiTokenHint: '设置外部请求MoviePilot API时使用的token值',
apiTokenMinChars: '不能小于16位字符',
@@ -2093,6 +2090,8 @@ export default {
siteOptions: '站点选项',
browserEmulation: '浏览器仿真',
browserEmulationHint: '站点访问仿真方式,支持 CloakBrowser 或 FlareSolverr',
ocrHost: '验证码识别服务器',
ocrHostHint: '用于站点签到、更新站点Cookie等识别验证码',
flaresolverrUrl: 'FlareSolverr 服务地址',
flaresolverrUrlHint: '当仿真方式为 FlareSolverr 时生效例如http://127.0.0.1:8191',
siteDataRefreshInterval: '站点数据刷新间隔',
@@ -2200,7 +2199,7 @@ export default {
'被替换词 => 替换词\n' +
'前定位词 <> 后定位词 >> 集偏移量EP\n' +
'被替换词 => 替换词 && 前定位词 <> 后定位词 >> 集偏移量EP\n' +
'其中替换词支持格式:&#123;[tmdbid/doubanid=xxx;type=movie/tv;g=xxx;s=xxx;e=xxx]&#125; 直接指定TMDBID/豆瓣ID识别其中g为剧集组编号s、e为季数和集数均可选',
'其中替换词支持格式:&#123;[tmdbid/doubanid/bangumiid/anilistid=xxx;type=movie/tv;g=xxx;s=xxx;e=xxx]&#125; 直接指定媒体数据源ID识别其中g为剧集组编号s、e为季数和集数均可选',
identifierSaveSuccess: '自定义识别词保存成功',
identifierSaveFailed: '自定义识别词保存失败!',
@@ -2987,10 +2986,16 @@ export default {
targetPathPlaceholder: '选择自动或输入路径',
mediaType: '类型',
mediaTypeHint: '文件的媒体类型',
mediaSource: '数据源',
mediaSourceHint: '默认使用后台识别设置,可为本次整理与刮削单独切换',
tmdbId: 'TheMovieDb编号',
doubanId: '豆瓣编号',
bangumiId: 'Bangumi编号',
anilistId: 'AniList编号',
mediaIdHint: '按名称查询媒体编号,留空自动识别',
mediaIdPlaceholder: '留空自动识别',
mediaSearchInput: '搜索媒体',
mediaSearchPlaceholder: '输入媒体名称',
episodeGroup: '剧集组',
episodeGroupHint: '输入 TMDB 编号后自动查询剧集组,也可手动填写剧集组编号',
episodeGroupPlaceholder: '先输入 TMDB 编号',
@@ -3123,7 +3128,7 @@ export default {
customWords: '自定义识别词',
customWordsHint: '只对该订阅使用的识别词',
customWordsPlaceholder:
'屏蔽词\n被替换词 => 替换词\n前定位词 <> 后定位词 >> 集偏移量EP\n被替换词 => 替换词 && 前定位词 <> 后定位词 >> 集偏移量EP\n其中替换词支持格式&#123;[tmdbid/doubanid=xxx;type=movie/tv;g=xxx;s=xxx;e=xxx]&#125; 直接指定TMDBID/豆瓣ID识别其中g为剧集组编号s、e为季数和集数均可选',
'屏蔽词\n被替换词 => 替换词\n前定位词 <> 后定位词 >> 集偏移量EP\n被替换词 => 替换词 && 前定位词 <> 后定位词 >> 集偏移量EP\n其中替换词支持格式&#123;[tmdbid/doubanid/bangumiid/anilistid=xxx;type=movie/tv;g=xxx;s=xxx;e=xxx]&#125; 直接指定媒体数据源ID识别其中g为剧集组编号s、e为季数和集数均可选',
cancelSubscribe: '取消订阅',
save: '保存',
cancelSubscribeConfirm: '是否确认取消订阅?',
@@ -3669,6 +3674,8 @@ export default {
type: '类型',
customTypeHint: '自定义媒体服务器类型,用于插件等场景',
enableMediaServer: '启用媒体服务器',
syncInterval: '自动同步间隔',
syncIntervalHint: '留空时使用旧版全局默认值({interval} 小时),设置为 0 时关闭此服务器的自动同步',
nameRequired: '必填,不可重名',
serverAlias: '媒体服务器的别名',
host: '地址',

View File

@@ -1528,7 +1528,11 @@ export default {
recognizing: '識別中...',
recognizeAgain: '重新識別',
title: '標題',
titleHint: '輸入種子名、發佈標題或文件名',
subtitle: '副標題',
subtitleHint: '可選,補充種子描述、別名或發佈信息以提高識別準確度',
source: '識別數據源',
sourceHint: '預設使用後台識別設置,可為本次測試單獨切換',
customWords: '識別詞',
customWordsPlaceholder: '每行輸入一組識別規則,可直接用於本次識別測試',
customWordsHint: '格式與「識別詞管理」一致:屏蔽詞 / 被替換詞 => 替換詞 / 前定位詞 <> 後定位詞 >> 集偏移量',
@@ -1537,8 +1541,6 @@ export default {
saveWordsNoChange: '識別詞已存在,無需重複儲存',
saveWordsFailed: '識別詞儲存失敗',
requestFailed: '識別請求失敗',
inputTitle: '測試輸入',
inputSubtitle: '輸入種子名或檔案名,查看媒體識別拆解結果',
unrecognized: '未識別到媒體',
waitingResult: '等待識別結果',
analysisTitle: '解析鏈路',
@@ -1580,14 +1582,15 @@ export default {
testing: '正在測試...',
testAgain: '重新測試',
title: '標題',
titleHint: '輸入用於模擬搜索或下載結果的發佈標題',
subtitle: '副標題',
subtitleHint: '可選,補充發佈描述、標籤或站點副標題以參與識別和過濾',
ruleGroup: '規則組',
ruleGroupHint: '選擇要驗證的過濾規則組',
ruleGroupPlaceholder: '請選擇',
priority: '優先級:{value}',
noPriorityRule: '未命中任何優先級規則!',
requestFailed: '規則測試請求失敗',
inputTitle: '規則測試',
inputSubtitle: '選擇規則組後,查看過濾命中和優先級結果',
waitingResult: '等待規則測試結果',
matched: '命中過濾規則',
priorityLabel: '優先級',
@@ -1683,12 +1686,6 @@ export default {
wallpaperHint: '選擇登陸頁面背景來源',
recognizeSource: '識別數據源',
recognizeSourceHint: '設置默認媒體信息識別數據源',
mediaServerSyncInterval: '媒體服務器同步間隔',
mediaServerSyncIntervalHint: '定時同步媒體服務器數據到本地的時間間隔',
hours: '小時',
required: '必選項,請勿留空',
numbersOnly: '僅支持輸入數字,請勿輸入其他字符',
minInterval: '間隔不能小於1個小時',
apiToken: 'API令牌',
apiTokenHint: '設置外部請求MoviePilot API時使用的token值',
apiTokenMinChars: '不能小於16位字符',
@@ -2092,6 +2089,8 @@ export default {
siteOptions: '站點選項',
browserEmulation: '瀏覽器仿真',
browserEmulationHint: '站點訪問仿真方式,支援 CloakBrowser 或 FlareSolverr',
ocrHost: '驗證碼識別服務器',
ocrHostHint: '用於站點簽到、更新站點Cookie等識別驗證碼',
flaresolverrUrl: 'FlareSolverr 服務地址',
flaresolverrUrlHint: '當仿真方式為 FlareSolverr 時生效例如http://127.0.0.1:8191',
siteDataRefreshInterval: '站點數據刷新間隔',
@@ -2199,7 +2198,7 @@ export default {
'被替換詞 => 替換詞\n' +
'前定位詞 <> 後定位詞 >> 集偏移量EP\n' +
'被替換詞 => 替換詞 && 前定位詞 <> 後定位詞 >> 集偏移量EP\n' +
'其中替換詞支持格式:&#123;[tmdbid/doubanid=xxx;type=movie/tv;g=xxx;s=xxx;e=xxx]&#125; 直接指定TMDBID/豆瓣ID識別其中g為劇集組編號s、e為季數和集數均可選',
'其中替換詞支持格式:&#123;[tmdbid/doubanid/bangumiid/anilistid=xxx;type=movie/tv;g=xxx;s=xxx;e=xxx]&#125; 直接指定媒體數據源ID識別其中g為劇集組編號s、e為季數和集數均可選',
identifierSaveSuccess: '自定義識別詞保存成功',
identifierSaveFailed: '自定義識別詞保存失敗!',
@@ -2986,10 +2985,16 @@ export default {
targetPathPlaceholder: '選擇自動或輸入路徑',
mediaType: '類型',
mediaTypeHint: '文件的媒體類型',
mediaSource: '數據源',
mediaSourceHint: '預設使用後台識別設置,可為本次整理與刮削單獨切換',
tmdbId: 'TheMovieDb編號',
doubanId: '豆瓣編號',
bangumiId: 'Bangumi編號',
anilistId: 'AniList編號',
mediaIdHint: '按名稱查詢媒體編號,留空自動識別',
mediaIdPlaceholder: '留空自動識別',
mediaSearchInput: '搜索媒體',
mediaSearchPlaceholder: '輸入媒體名稱',
episodeGroup: '劇集組',
episodeGroupHint: '輸入 TMDB 編號後自動查詢劇集組,也可手動填寫劇集組編號',
episodeGroupPlaceholder: '先輸入 TMDB 編號',
@@ -3122,7 +3127,7 @@ export default {
customWords: '自定義識別詞',
customWordsHint: '只對該訂閱使用的識別詞',
customWordsPlaceholder:
'屏蔽詞\n被替換詞 => 替換詞\n前定位詞 <> 後定位詞 >> 集偏移量EP\n被替換詞 => 替換詞 && 前定位詞 <> 後定位詞 >> 集偏移量EP\n其中替換詞支援格式&#123;[tmdbid/doubanid=xxx;type=movie/tv;g=xxx;s=xxx;e=xxx]&#125; 直接指定TMDBID/豆瓣ID識別其中g為劇集組編號s、e為季數和集數均可選',
'屏蔽詞\n被替換詞 => 替換詞\n前定位詞 <> 後定位詞 >> 集偏移量EP\n被替換詞 => 替換詞 && 前定位詞 <> 後定位詞 >> 集偏移量EP\n其中替換詞支援格式&#123;[tmdbid/doubanid/bangumiid/anilistid=xxx;type=movie/tv;g=xxx;s=xxx;e=xxx]&#125; 直接指定媒體數據源ID識別其中g為劇集組編號s、e為季數和集數均可選',
cancelSubscribe: '取消訂閱',
save: '儲存',
cancelSubscribeConfirm: '是否確認取消訂閱?',
@@ -3666,6 +3671,8 @@ export default {
type: '類型',
customTypeHint: '自定義媒體伺服器類型,用於插件等場景',
enableMediaServer: '啟用媒體伺服器',
syncInterval: '自動同步間隔',
syncIntervalHint: '留空時使用舊版全局預設值({interval} 小時),設置為 0 時關閉此服務器的自動同步',
nameRequired: '必填;不可與其他名稱重名',
serverAlias: '媒體伺服器的別名',
host: '地址',

View File

@@ -41,6 +41,8 @@ const $toast = useToast()
const sourceItems = [
{ 'title': 'TheMovieDb', 'value': 'themoviedb' },
{ 'title': '豆瓣', 'value': 'douban' },
{ 'title': 'Bangumi', 'value': 'bangumi' },
{ 'title': 'AniList', 'value': 'anilist' },
]
// 存储选项(排除已添加的)

View File

@@ -48,6 +48,10 @@ const mediaSourcesDict = [
title: 'Bangumi',
value: 'bangumi',
},
{
title: 'AniList',
value: 'anilist',
},
]
// 当前选中的媒体信息数据源

View File

@@ -46,6 +46,7 @@ const siteSetting = ref<any>({
SITE_MESSAGE: false,
SEARCH_RESOURCE_PAGES: 1,
BROWSER_EMULATION: 'cloakbrowser',
OCR_HOST: '',
FLARESOLVERR_URL: '',
},
})
@@ -276,6 +277,16 @@ useSilentSettingRefresh(loadSiteSettings, {
prepend-inner-icon="mdi-web"
/>
</VCol>
<VCol cols="12" md="6">
<VTextField
v-model="siteSetting.Site.OCR_HOST"
:label="t('setting.site.ocrHost')"
placeholder="https://movie-pilot.org"
:hint="t('setting.site.ocrHostHint')"
persistent-hint
prepend-inner-icon="mdi-text-recognition"
/>
</VCol>
<VCol cols="12" md="6" v-if="siteSetting.Site.BROWSER_EMULATION == 'flaresolverr'">
<VTextField
v-model="siteSetting.Site.FLARESOLVERR_URL"

View File

@@ -43,10 +43,7 @@ const SystemSettings = ref<any>({
APP_DOMAIN: null,
API_TOKEN: null,
WALLPAPER: 'tmdb',
MEDIASERVER_SYNC_INTERVAL: null,
RECOGNIZE_SOURCE: 'themoviedb',
GITHUB_TOKEN: null,
OCR_HOST: null,
CUSTOMIZE_WALLPAPER_API_URL: null,
AI_AGENT_ENABLE: false,
AI_AGENT_GLOBAL: false,
@@ -197,6 +194,9 @@ const isRequest = ref(true)
// 选中的媒体服务器
const mediaServers = ref<MediaServerConf[]>([])
// 旧版全局媒体服务器同步间隔,仅用于未单独设置时的默认值提示
const legacyMediaServerSyncInterval = ref<number | null>(null)
// 下载器
const downloaders = ref<DownloaderConf[]>([])
@@ -691,6 +691,8 @@ async function loadSystemSettings() {
try {
const result: { [key: string]: any } = await api.get('system/env')
if (result.success) {
const defaultSyncInterval = Number(result.data.MEDIASERVER_SYNC_INTERVAL ?? Number.NaN)
legacyMediaServerSyncInterval.value = Number.isFinite(defaultSyncInterval) ? defaultSyncInterval : null
// 将API返回的值赋值给SystemSettings
for (const sectionKey of Object.keys(SystemSettings.value) as Array<keyof typeof SystemSettings.value>) {
Object.keys(SystemSettings.value[sectionKey]).forEach((key: string) => {
@@ -1100,36 +1102,6 @@ watch(currentLlmSnapshotKey, (snapshotKey, previousSnapshotKey) => {
</VCol>
</VRow>
</VCol>
<VCol cols="12" md="6">
<VSelect
v-model="SystemSettings.Basic.RECOGNIZE_SOURCE"
:label="t('setting.system.recognizeSource')"
:hint="t('setting.system.recognizeSourceHint')"
persistent-hint
:items="[
{ title: 'TheMovieDb', value: 'themoviedb' },
{ title: '豆瓣', value: 'douban' },
]"
prepend-inner-icon="mdi-database"
/>
</VCol>
<VCol cols="12" md="6">
<VTextField
v-model="SystemSettings.Basic.MEDIASERVER_SYNC_INTERVAL"
:label="t('setting.system.mediaServerSyncInterval')"
:hint="t('setting.system.mediaServerSyncIntervalHint')"
persistent-hint
:suffix="t('setting.system.hours')"
type="number"
min="1"
:rules="[
(v: any) => !!v || t('setting.system.required'),
(v: any) => !isNaN(v) || t('setting.system.numbersOnly'),
(v: any) => v >= 1 || t('setting.system.minInterval'),
]"
prepend-inner-icon="mdi-sync"
/>
</VCol>
<VCol cols="12" md="6">
<VTextField
v-model="SystemSettings.Basic.API_TOKEN"
@@ -1159,16 +1131,6 @@ watch(currentLlmSnapshotKey, (snapshotKey, previousSnapshotKey) => {
>
</VTextField>
</VCol>
<VCol cols="12" md="6">
<VTextField
v-model="SystemSettings.Basic.OCR_HOST"
:label="t('setting.system.ocrHost')"
placeholder="https://movie-pilot.org"
:hint="t('setting.system.ocrHostHint')"
persistent-hint
prepend-inner-icon="mdi-text-recognition"
/>
</VCol>
</VRow>
<VCard
variant="outlined"
@@ -1817,6 +1779,7 @@ watch(currentLlmSnapshotKey, (snapshotKey, previousSnapshotKey) => {
<MediaServerCard
:mediaserver="element"
:mediaservers="mediaServers"
:default-sync-interval="legacyMediaServerSyncInterval ?? undefined"
@close="removeMediaServer(element)"
@change="onMediaServerChange"
/>

View File

@@ -3,9 +3,10 @@ import { computed, reactive, ref } from 'vue'
import { useToast } from 'vue-toastification'
import { requiredValidator } from '@/@validators'
import api from '@/api'
import type { Context } from '@/api/types'
import type { Context, MediaDataSource, MediaInfo } from '@/api/types'
import { getMediaSubscribeId } from '@/composables/useMediaSubscribe'
import router from '@/router'
import { useGlobalSettingsStore } from '@/stores'
import { useI18n } from 'vue-i18n'
interface PipelineStep {
@@ -16,6 +17,20 @@ interface PipelineStep {
// 国际化
const { t } = useI18n()
const globalSettingsStore = useGlobalSettingsStore()
const mediaSourceItems: { title: string; value: MediaDataSource }[] = [
{ title: 'TheMovieDb', value: 'themoviedb' },
{ title: '豆瓣', value: 'douban' },
{ title: 'Bangumi', value: 'bangumi' },
{ title: 'AniList', value: 'anilist' },
]
// 获取后台默认识别数据源未知值兼容回退到TheMovieDb。
function getDefaultMediaSource(): MediaDataSource {
const configuredSource = globalSettingsStore.globalSettings.RECOGNIZE_SOURCE as MediaDataSource
return mediaSourceItems.some(item => item.value === configuredSource) ? configuredSource : 'themoviedb'
}
// 提示
const $toast = useToast()
@@ -28,6 +43,7 @@ const nameTestForm = reactive({
title: '',
subtitle: '',
customWords: '',
source: getDefaultMediaSource(),
})
// 识别按钮状态
@@ -67,9 +83,25 @@ const resourceChips = computed(() => {
// 是否已匹配到具体媒体,决定是否展示查看详情入口
const canViewMediaDetail = computed(() =>
Boolean(
mediaInfo.value?.tmdb_id || mediaInfo.value?.douban_id || mediaInfo.value?.bangumi_id || mediaInfo.value?.media_id,
mediaInfo.value?.tmdb_id ||
mediaInfo.value?.douban_id ||
mediaInfo.value?.bangumi_id ||
mediaInfo.value?.anilist_id ||
mediaInfo.value?.media_id,
),
)
/** 生成识别结果中的数据源原生ID摘要并兼容旧接口字段。 */
function getMediaIdentityLabel(media?: MediaInfo) {
if (!media) return t('nameTest.unrecognized')
if (media.media_id) return `${media.source || media.mediaid_prefix} ${media.media_id}`
if (media.tmdb_id) return `TMDB ${media.tmdb_id}`
if (media.douban_id) return `Douban ${media.douban_id}`
if (media.bangumi_id) return `Bangumi ${media.bangumi_id}`
if (media.anilist_id) return `AniList ${media.anilist_id}`
return media.title || t('nameTest.unrecognized')
}
const pipelineSteps = computed<PipelineStep[]>(() => [
{
icon: 'mdi-file-document-outline',
@@ -87,11 +119,7 @@ const pipelineSteps = computed<PipelineStep[]>(() => [
{
icon: 'mdi-movie-search-outline',
title: t('nameTest.steps.media.title'),
value: mediaInfo.value?.tmdb_id
? `TMDB ${mediaInfo.value.tmdb_id}`
: mediaInfo.value?.douban_id
? `Douban ${mediaInfo.value.douban_id}`
: mediaInfo.value?.title || t('nameTest.unrecognized'),
value: getMediaIdentityLabel(mediaInfo.value),
},
])
@@ -130,6 +158,7 @@ async function nameTest() {
title: nameTestForm.title,
subtitle: nameTestForm.subtitle,
custom_words: nameTestForm.customWords || undefined,
source: nameTestForm.source,
},
})
nameTestText.value = t('nameTest.recognizeAgain')
@@ -184,32 +213,34 @@ async function saveCustomWords() {
<template>
<div class="shortcut-workbench">
<section class="shortcut-panel shortcut-input-panel">
<div class="panel-heading">
<div>
<div class="text-subtitle-1 font-weight-medium">
{{ t('nameTest.inputTitle') }}
</div>
<div class="text-caption text-medium-emphasis">
{{ t('nameTest.inputSubtitle') }}
</div>
</div>
<VIcon icon="mdi-text-recognition" color="primary" />
</div>
<VForm validate-on="submit lazy" @submit.prevent="nameTest">
<VRow class="shortcut-form">
<VCol cols="12" class="shortcut-form-col">
<VTextField
v-model="nameTestForm.title"
:label="t('nameTest.title')"
:hint="t('nameTest.titleHint')"
persistent-hint
:rules="[requiredValidator]"
prepend-inner-icon="mdi-movie-open"
/>
</VCol>
<VCol cols="12" class="shortcut-form-col">
<VSelect
v-model="nameTestForm.source"
:items="mediaSourceItems"
:label="t('nameTest.source')"
:hint="t('nameTest.sourceHint')"
persistent-hint
prepend-inner-icon="mdi-database-search"
/>
</VCol>
<VCol cols="12" class="shortcut-form-col">
<VTextarea
v-model="nameTestForm.subtitle"
:label="t('nameTest.subtitle')"
:hint="t('nameTest.subtitleHint')"
persistent-hint
rows="2"
auto-grow
prepend-inner-icon="mdi-subtitles"
@@ -220,6 +251,8 @@ async function saveCustomWords() {
v-model="nameTestForm.customWords"
:label="t('nameTest.customWords')"
:placeholder="t('nameTest.customWordsPlaceholder')"
:hint="t('nameTest.customWordsHint')"
persistent-hint
rows="3"
auto-grow
prepend-inner-icon="mdi-tag-text-outline"
@@ -370,14 +403,6 @@ async function saveCustomWords() {
box-shadow: var(--app-surface-shadow);
}
.panel-heading {
display: flex;
align-items: flex-start;
justify-content: space-between;
gap: 0.75rem;
margin-block-end: 1rem;
}
.shortcut-form {
margin: 0;
}

View File

@@ -180,24 +180,14 @@ onMounted(() => {
<template>
<div class="shortcut-workbench">
<section class="shortcut-panel shortcut-input-panel">
<div class="panel-heading">
<div>
<div class="text-subtitle-1 font-weight-medium">
{{ t('ruleTest.inputTitle') }}
</div>
<div class="text-caption text-medium-emphasis">
{{ t('ruleTest.inputSubtitle') }}
</div>
</div>
<VIcon icon="mdi-filter-cog" color="primary" />
</div>
<VForm ref="ruleTestFormRef" validate-on="submit lazy" @submit.prevent="ruleTest">
<VRow class="shortcut-form">
<VCol cols="12" class="shortcut-form-col">
<VTextField
v-model="ruleTestForm.title"
:label="t('ruleTest.title')"
:hint="t('ruleTest.titleHint')"
persistent-hint
:rules="[requiredValidator]"
prepend-inner-icon="mdi-movie-open"
/>
@@ -207,6 +197,8 @@ onMounted(() => {
v-model="ruleTestForm.rulegroup"
:items="filterRuleGroupItems"
:label="t('ruleTest.ruleGroup')"
:hint="t('ruleTest.ruleGroupHint')"
persistent-hint
:loading="filterRuleGroupLoading"
:rules="[requiredValidator]"
prepend-inner-icon="mdi-filter"
@@ -216,6 +208,8 @@ onMounted(() => {
<VTextarea
v-model="ruleTestForm.subtitle"
:label="t('ruleTest.subtitle')"
:hint="t('ruleTest.subtitleHint')"
persistent-hint
rows="2"
auto-grow
prepend-inner-icon="mdi-subtitles"
@@ -317,14 +311,6 @@ onMounted(() => {
padding: 1rem;
}
.panel-heading {
display: flex;
gap: 0.75rem;
align-items: flex-start;
justify-content: space-between;
margin-block-end: 1rem;
}
.shortcut-form {
margin: 0;
}