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
+18 -2
View File
@@ -1,3 +1,5 @@
export type MediaDataSource = 'themoviedb' | 'douban' | 'bangumi' | 'anilist'
// 订阅 // 订阅
export interface Subscribe { export interface Subscribe {
// 订阅ID // 订阅ID
@@ -218,6 +220,10 @@ export interface TransferHistory {
tvdbid?: number tvdbid?: number
// 豆瓣ID // 豆瓣ID
doubanid?: string doubanid?: string
// 媒体数据源
media_source?: MediaDataSource
// 数据源原生ID
media_id?: string
// 季Sxx // 季Sxx
seasons?: string seasons?: string
// 集Exx // 集Exx
@@ -238,7 +244,7 @@ export interface TransferHistory {
// 媒体信息 // 媒体信息
export interface MediaInfo { export interface MediaInfo {
// 来源:themoviedb、douban、bangumi // 来源:themoviedb、douban、bangumi、anilist
source?: string source?: string
// 类型 电影、电视剧、合集 // 类型 电影、电视剧、合集
type?: string type?: string
@@ -259,7 +265,11 @@ export interface MediaInfo {
// 豆瓣ID // 豆瓣ID
douban_id?: string douban_id?: string
// Bangumi ID // Bangumi ID
bangumi_id?: string bangumi_id?: string | number
// AniList ID
anilist_id?: number
// AniDB ID
anidb_id?: number
// 合集ID // 合集ID
collection_id?: number collection_id?: number
// 其它媒体ID前缀 // 其它媒体ID前缀
@@ -1353,6 +1363,8 @@ export interface MediaServerConf {
enabled: boolean enabled: boolean
// 同步媒体体库列表 // 同步媒体体库列表
sync_libraries?: string[] sync_libraries?: string[]
// 自动同步间隔(小时),为空时使用旧全局配置
sync_interval?: number | null
} }
// 文件整理目录配置 // 文件整理目录配置
@@ -1513,6 +1525,10 @@ export interface TransferForm {
tmdbid?: number tmdbid?: number
// 豆瓣 ID // 豆瓣 ID
doubanid?: string doubanid?: string
// 媒体数据源
media_source?: MediaDataSource
// 数据源原生ID
media_id?: string | null
// 季号 // 季号
season?: number season?: number
// 类型 // 类型
+6
View File
@@ -25,6 +25,11 @@ const props = defineProps({
type: Array as PropType<MediaServerConf[]>, type: Array as PropType<MediaServerConf[]>,
required: true, required: true,
}, },
// 旧版全局同步间隔,用作服务器未单独设置时的默认值
defaultSyncInterval: {
type: Number,
default: null,
},
}) })
// 定义触发的自定义事件 // 定义触发的自定义事件
@@ -56,6 +61,7 @@ function openMediaServerInfoDialog() {
{ {
mediaserver: props.mediaserver, mediaserver: props.mediaserver,
mediaservers: props.mediaservers, mediaservers: props.mediaservers,
defaultSyncInterval: props.defaultSyncInterval,
}, },
{ {
change: (...args: unknown[]) => emit('change', ...args), change: (...args: unknown[]) => emit('change', ...args),
+24 -35
View File
@@ -2,7 +2,7 @@
import { useToast } from 'vue-toastification' import { useToast } from 'vue-toastification'
import api from '@/api' import api from '@/api'
import { doneNProgress, startNProgress } from '@/api/nprogress' 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 { formatFileSize } from '@/@core/utils/formatters'
import { VCardTitle, VChip } from 'vuetify/lib/components/index.mjs' import { VCardTitle, VChip } from 'vuetify/lib/components/index.mjs'
import { useI18n } from 'vue-i18n' import { useI18n } from 'vue-i18n'
@@ -18,7 +18,11 @@ const globalSettingsStore = useGlobalSettingsStore()
const globalSettings = globalSettingsStore.globalSettings 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({ const props = defineProps({
@@ -51,11 +55,19 @@ const loading = ref(false)
// 是否显示高级选项 // 是否显示高级选项
const showAdvancedOptions = ref(false) const showAdvancedOptions = ref(false)
// TMDB ID // 当前数据源的原生媒体ID
const tmdbid = ref<number | undefined>(undefined) const mediaId = ref<string | undefined>(undefined)
// 豆瓣ID // 当前数据源对应的原生ID标签。
const doubanId = ref<string | undefined>(undefined) 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选择对话框 // TMDB选择对话框
const mediaSelectorDialog = ref(false) const mediaSelectorDialog = ref(false)
@@ -140,11 +152,9 @@ async function addDownload() {
} }
// 添加媒体ID辅助识别 // 添加媒体ID辅助识别
if (tmdbid.value) { if (mediaId.value) {
payload.tmdbid = tmdbid.value payload.media_source = mediaSource.value
} payload.media_id = mediaId.value
if (doubanId.value) {
payload.doubanid = doubanId.value
} }
const endpoint = props.media ? 'download/' : 'download/add' const endpoint = props.media ? 'download/' : 'download/add'
@@ -269,23 +279,8 @@ onMounted(() => {
<VRow v-show="showAdvancedOptions" class="px-5"> <VRow v-show="showAdvancedOptions" class="px-5">
<VCol cols="12"> <VCol cols="12">
<VTextField <VTextField
v-if="mediaSource === 'themoviedb'" v-model="mediaId"
v-model="tmdbid" :label="mediaIdLabel"
: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')"
:placeholder="t('dialog.reorganize.mediaIdPlaceholder')" :placeholder="t('dialog.reorganize.mediaIdPlaceholder')"
:rules="[numberValidator]" :rules="[numberValidator]"
append-inner-icon="mdi-magnify" append-inner-icon="mdi-magnify"
@@ -307,13 +302,7 @@ onMounted(() => {
</VCard> </VCard>
<!-- 媒体ID选择器 --> <!-- 媒体ID选择器 -->
<VDialog v-model="mediaSelectorDialog" width="40rem" scrollable max-height="85vh"> <VDialog v-model="mediaSelectorDialog" width="40rem" scrollable max-height="85vh">
<MediaIdSelector <MediaIdSelector v-model="mediaId" @close="mediaSelectorDialog = false" :type="mediaSource" />
v-if="mediaSource === 'themoviedb'"
v-model="tmdbid"
@close="mediaSelectorDialog = false"
:type="mediaSource"
/>
<MediaIdSelector v-else v-model="doubanId" @close="mediaSelectorDialog = false" :type="mediaSource" />
</VDialog> </VDialog>
</VDialog> </VDialog>
</template> </template>
@@ -2,7 +2,7 @@
import { useToast } from 'vue-toastification' import { useToast } from 'vue-toastification'
import api from '@/api' import api from '@/api'
import { doneNProgress, startNProgress } from '@/api/nprogress' 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 { formatFileSize } from '@/@core/utils/formatters'
import { useI18n } from 'vue-i18n' import { useI18n } from 'vue-i18n'
import MediaIdSelector from '../misc/MediaIdSelector.vue' import MediaIdSelector from '../misc/MediaIdSelector.vue'
@@ -17,7 +17,11 @@ const globalSettingsStore = useGlobalSettingsStore()
const globalSettings = globalSettingsStore.globalSettings 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({ const props = defineProps({
@@ -43,11 +47,19 @@ const loading = ref(false)
// 是否显示高级选项 // 是否显示高级选项
const showAdvancedOptions = ref(false) const showAdvancedOptions = ref(false)
// TMDB ID // 当前数据源的原生媒体ID
const tmdbid = ref<number | undefined>(undefined) const mediaId = ref<string | undefined>(undefined)
// 豆瓣ID // 当前数据源对应的原生ID标签。
const doubanId = ref<string | undefined>(undefined) 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选择对话框 // TMDB选择对话框
const mediaSelectorDialog = ref(false) const mediaSelectorDialog = ref(false)
@@ -98,11 +110,9 @@ async function addSubtitleDownload() {
save_path: selectedDirectory.value, save_path: selectedDirectory.value,
} }
if (tmdbid.value) { if (mediaId.value) {
payload.tmdbid = tmdbid.value payload.media_source = mediaSource.value
} payload.media_id = mediaId.value
if (doubanId.value) {
payload.doubanid = doubanId.value
} }
const result: { [key: string]: any } = await api.post('download/subtitle', payload) const result: { [key: string]: any } = await api.post('download/subtitle', payload)
@@ -221,23 +231,8 @@ onMounted(() => {
<VRow v-show="showAdvancedOptions" class="px-5"> <VRow v-show="showAdvancedOptions" class="px-5">
<VCol cols="12"> <VCol cols="12">
<VTextField <VTextField
v-if="mediaSource === 'themoviedb'" v-model="mediaId"
v-model="tmdbid" :label="mediaIdLabel"
: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')"
:placeholder="t('dialog.reorganize.mediaIdPlaceholder')" :placeholder="t('dialog.reorganize.mediaIdPlaceholder')"
:rules="[numberValidator]" :rules="[numberValidator]"
append-inner-icon="mdi-magnify" append-inner-icon="mdi-magnify"
@@ -258,13 +253,7 @@ onMounted(() => {
</VCardText> </VCardText>
</VCard> </VCard>
<VDialog v-model="mediaSelectorDialog" width="40rem" scrollable max-height="85vh"> <VDialog v-model="mediaSelectorDialog" width="40rem" scrollable max-height="85vh">
<MediaIdSelector <MediaIdSelector v-model="mediaId" @close="mediaSelectorDialog = false" :type="mediaSource" />
v-if="mediaSource === 'themoviedb'"
v-model="tmdbid"
@close="mediaSelectorDialog = false"
:type="mediaSource"
/>
<MediaIdSelector v-else v-model="doubanId" @close="mediaSelectorDialog = false" :type="mediaSource" />
</VDialog> </VDialog>
</VDialog> </VDialog>
</template> </template>
+103 -15
View File
@@ -26,6 +26,10 @@ const props = defineProps({
type: Array as PropType<MediaServerConf[]>, type: Array as PropType<MediaServerConf[]>,
required: true, required: true,
}, },
defaultSyncInterval: {
type: Number,
default: null,
},
}) })
// 定义触发的自定义事件 // 定义触发的自定义事件
@@ -203,6 +207,20 @@ onMounted(() => {
prepend-inner-icon="mdi-key" prepend-inner-icon="mdi-key"
/> />
</VCol> </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"> <VCol cols="12">
<VAutocomplete <VAutocomplete
v-model="mediaServerInfo.sync_libraries" v-model="mediaServerInfo.sync_libraries"
@@ -243,7 +261,7 @@ onMounted(() => {
prepend-inner-icon="mdi-server" prepend-inner-icon="mdi-server"
/> />
</VCol> </VCol>
<VCol cols="12"> <VCol cols="6">
<VTextField <VTextField
v-model="mediaServerInfo.config.play_host" v-model="mediaServerInfo.config.play_host"
:label="t('mediaserver.playHost')" :label="t('mediaserver.playHost')"
@@ -274,6 +292,20 @@ onMounted(() => {
prepend-inner-icon="mdi-lock" prepend-inner-icon="mdi-lock"
/> />
</VCol> </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"> <VCol cols="12">
<VAutocomplete <VAutocomplete
v-model="mediaServerInfo.sync_libraries" v-model="mediaServerInfo.sync_libraries"
@@ -335,6 +367,20 @@ onMounted(() => {
prepend-inner-icon="mdi-key" prepend-inner-icon="mdi-key"
/> />
</VCol> </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"> <VCol cols="12">
<VAutocomplete <VAutocomplete
v-model="mediaServerInfo.sync_libraries" v-model="mediaServerInfo.sync_libraries"
@@ -375,7 +421,7 @@ onMounted(() => {
prepend-inner-icon="mdi-server" prepend-inner-icon="mdi-server"
/> />
</VCol> </VCol>
<VCol cols="12"> <VCol cols="6">
<VTextField <VTextField
v-model="mediaServerInfo.config.play_host" v-model="mediaServerInfo.config.play_host"
:label="t('mediaserver.playHost')" :label="t('mediaserver.playHost')"
@@ -403,6 +449,20 @@ onMounted(() => {
prepend-inner-icon="mdi-lock" prepend-inner-icon="mdi-lock"
/> />
</VCol> </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"> <VCol cols="12">
<VAutocomplete <VAutocomplete
v-model="mediaServerInfo.sync_libraries" v-model="mediaServerInfo.sync_libraries"
@@ -443,7 +503,7 @@ onMounted(() => {
prepend-inner-icon="mdi-server" prepend-inner-icon="mdi-server"
/> />
</VCol> </VCol>
<VCol cols="12"> <VCol cols="6">
<VTextField <VTextField
v-model="mediaServerInfo.config.play_host" v-model="mediaServerInfo.config.play_host"
:label="t('mediaserver.playHost')" :label="t('mediaserver.playHost')"
@@ -471,20 +531,18 @@ onMounted(() => {
prepend-inner-icon="mdi-lock" prepend-inner-icon="mdi-lock"
/> />
</VCol> </VCol>
<VCol cols="12"> <VCol cols="12" md="6">
<VAutocomplete <VTextField
v-model="mediaServerInfo.sync_libraries" v-model.number="mediaServerInfo.sync_interval"
:label="t('mediaserver.syncLibraries')" type="number"
:items="librariesOptions" min="0"
chips step="1"
multiple
clearable clearable
:hint="t('mediaserver.syncLibrariesHint')" :label="t('mediaserver.syncInterval')"
:hint="t('mediaserver.syncIntervalHint', { interval: props.defaultSyncInterval ?? 6 })"
persistent-hint persistent-hint
active suffix="h"
append-inner-icon="mdi-refresh" prepend-inner-icon="mdi-sync"
prepend-inner-icon="mdi-library"
@click:append-inner="loadLibrary(mediaServerInfo.name)"
/> />
</VCol> </VCol>
<VCol cols="12" md="6"> <VCol cols="12" md="6">
@@ -508,6 +566,22 @@ onMounted(() => {
inset inset
/> />
</VCol> </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>
<VRow v-else-if="mediaServerInfo.type == 'plex'"> <VRow v-else-if="mediaServerInfo.type == 'plex'">
<VCol cols="12" md="6"> <VCol cols="12" md="6">
@@ -553,6 +627,20 @@ onMounted(() => {
prepend-inner-icon="mdi-key" prepend-inner-icon="mdi-key"
/> />
</VCol> </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"> <VCol cols="12">
<VAutocomplete <VAutocomplete
v-model="mediaServerInfo.sync_libraries" v-model="mediaServerInfo.sync_libraries"
+63 -36
View File
@@ -10,6 +10,7 @@ import {
ManualTransferPayload, ManualTransferPayload,
ManualTransferPreviewData, ManualTransferPreviewData,
ManualTransferPreviewItem, ManualTransferPreviewItem,
MediaDataSource,
MediaInfo, MediaInfo,
StorageConf, StorageConf,
TransferDirectoryConf, TransferDirectoryConf,
@@ -37,13 +38,22 @@ const props = defineProps({
target_path: String, target_path: String,
}) })
// 从 provide 中获取全局设置
// 全局设置 // 全局设置
const globalSettingsStore = useGlobalSettingsStore() const globalSettingsStore = useGlobalSettingsStore()
const globalSettings = globalSettingsStore.globalSettings const globalSettings = globalSettingsStore.globalSettings
// 当前识别类型 const mediaSourceItems: { title: string; value: MediaDataSource }[] = [
const mediaSource = ref(globalSettings.RECOGNIZE_SOURCE || 'themoviedb') { 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']) const emit = defineEmits(['done', 'close'])
@@ -304,6 +314,8 @@ const transferForm = reactive<TransferForm>({
logid: 0, logid: 0,
target_storage: initialTargetPath ? (props.target_storage ?? 'local') : null, target_storage: initialTargetPath ? (props.target_storage ?? 'local') : null,
target_path: initialTargetPath, target_path: initialTargetPath,
media_source: getDefaultMediaSource(),
media_id: null,
transfer_type: null, transfer_type: null,
min_filesize: 0, min_filesize: 0,
scrape: initialTargetPath ? false : null, scrape: initialTargetPath ? false : null,
@@ -313,6 +325,20 @@ const transferForm = reactive<TransferForm>({
episode_group: null, 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'>) { function handleMediaSelected(item: Pick<MediaInfo, 'type'>) {
const typeName = resolveTransferMediaType(item.type) const typeName = resolveTransferMediaType(item.type)
@@ -403,28 +429,39 @@ watch(
}, },
) )
// 监听 TMDB 编号变化,自动加载可用剧集组并清空旧选择 // 监听媒体编号变化,仅在TMDB电视剧场景加载剧集组
watch( watch(
() => transferForm.tmdbid, () => transferForm.media_id,
tmdbid => { mediaId => {
transferForm.episode_group = null transferForm.episode_group = null
episodeGroups.value = [] episodeGroups.value = []
if (episodeGroupQueryTimer) clearTimeout(episodeGroupQueryTimer) if (episodeGroupQueryTimer) clearTimeout(episodeGroupQueryTimer)
if (transferForm.type_name !== '电视剧' || mediaSource.value !== 'themoviedb') return if (transferForm.type_name !== '电视剧' || mediaSource.value !== 'themoviedb') return
episodeGroupQueryTimer = setTimeout(() => getEpisodeGroups(tmdbid), 400) episodeGroupQueryTimer = setTimeout(() => getEpisodeGroups(mediaId ?? undefined), 400)
}, },
) )
// 切换媒体类型或识别源时,非 TMDB 电视剧不保留剧集组选择。 // 切换媒体类型或识别源时,非 TMDB 电视剧不保留剧集组选择。
watch([() => transferForm.type_name, () => mediaSource.value], ([typeName, source]) => { watch([() => transferForm.type_name, () => mediaSource.value], ([typeName, source]) => {
if (typeName === '电视剧' && source === 'themoviedb' && transferForm.tmdbid) { if (typeName === '电视剧' && source === 'themoviedb' && transferForm.media_id) {
getEpisodeGroups(transferForm.tmdbid) getEpisodeGroups(transferForm.media_id)
return return
} }
transferForm.episode_group = null transferForm.episode_group = null
episodeGroups.value = [] episodeGroups.value = []
}) })
// 切换数据源时清空上一来源的原生ID,避免把同一数字误传给新来源。
watch(
() => transferForm.media_source,
(source, previousSource) => {
if (previousSource && source !== previousSource) {
transferForm.media_id = null
mediaSelectorDialog.value = false
}
},
)
watch( watch(
() => transferForm.episode_group, () => transferForm.episode_group,
episodeGroup => { episodeGroup => {
@@ -859,6 +896,8 @@ function createTransferPayload(options: { item?: FileItem; items?: FileItem[]; l
target_storage: normalizeOptionalText(transferForm.target_storage), target_storage: normalizeOptionalText(transferForm.target_storage),
target_path: normalizeTargetPath(transferForm.target_path), target_path: normalizeTargetPath(transferForm.target_path),
transfer_type: normalizeOptionalText(transferForm.transfer_type), transfer_type: normalizeOptionalText(transferForm.transfer_type),
media_source: mediaSource.value,
media_id: normalizeOptionalText(transferForm.media_id),
episode_group: normalizeEpisodeGroup(transferForm.episode_group), episode_group: normalizeEpisodeGroup(transferForm.episode_group),
} }
@@ -1385,7 +1424,7 @@ onUnmounted(() => {
</VCol> </VCol>
</VRow> </VRow>
<VRow> <VRow>
<VCol cols="12" md="6"> <VCol cols="12" md="4">
<VSelect <VSelect
v-model="transferForm.type_name" v-model="transferForm.type_name"
:label="t('dialog.reorganize.mediaType')" :label="t('dialog.reorganize.mediaType')"
@@ -1399,25 +1438,21 @@ onUnmounted(() => {
prepend-inner-icon="mdi-movie-open" prepend-inner-icon="mdi-movie-open"
/> />
</VCol> </VCol>
<VCol cols="12" md="6"> <VCol cols="12" md="4">
<VTextField <VSelect
v-if="mediaSource === 'themoviedb'" v-model="transferForm.media_source"
v-model="transferForm.tmdbid" :items="mediaSourceItems"
:disabled="transferForm.type_name === ''" :label="t('dialog.reorganize.mediaSource')"
:label="t('dialog.reorganize.tmdbId')" :hint="t('dialog.reorganize.mediaSourceHint')"
:placeholder="t('dialog.reorganize.mediaIdPlaceholder')"
:rules="[numberValidator]"
append-inner-icon="mdi-magnify"
:hint="t('dialog.reorganize.mediaIdHint')"
persistent-hint persistent-hint
prepend-inner-icon="mdi-identifier" prepend-inner-icon="mdi-database-search"
@click:append-inner="mediaSelectorDialog = true"
/> />
</VCol>
<VCol cols="12" md="4">
<VTextField <VTextField
v-else v-model="transferForm.media_id"
v-model="transferForm.doubanid"
:disabled="transferForm.type_name === ''" :disabled="transferForm.type_name === ''"
:label="t('dialog.reorganize.doubanId')" :label="mediaIdLabel"
:placeholder="t('dialog.reorganize.mediaIdPlaceholder')" :placeholder="t('dialog.reorganize.mediaIdPlaceholder')"
:rules="[numberValidator]" :rules="[numberValidator]"
append-inner-icon="mdi-magnify" append-inner-icon="mdi-magnify"
@@ -1437,7 +1472,7 @@ onUnmounted(() => {
item-value="value" item-value="value"
:item-props="episodeGroupItemProps" :item-props="episodeGroupItemProps"
:loading="episodeGroupLoading" :loading="episodeGroupLoading"
:disabled="!transferForm.tmdbid" :disabled="!transferForm.media_id"
clearable clearable
:label="t('dialog.reorganize.episodeGroup')" :label="t('dialog.reorganize.episodeGroup')"
:placeholder="t('dialog.reorganize.episodeGroupPlaceholder')" :placeholder="t('dialog.reorganize.episodeGroupPlaceholder')"
@@ -1744,18 +1779,10 @@ onUnmounted(() => {
</VCard> </VCard>
<!-- 手动整理进度框 --> <!-- 手动整理进度框 -->
<ProgressDialog v-if="progressDialog" v-model="progressDialog" :text="progressText" :value="progressValue" /> <ProgressDialog v-if="progressDialog" v-model="progressDialog" :text="progressText" :value="progressValue" />
<!-- TMDB ID搜索框 --> <!-- 媒体数据源ID搜索框 -->
<VDialog v-model="mediaSelectorDialog" width="40rem" scrollable max-height="85vh"> <VDialog v-model="mediaSelectorDialog" width="40rem" scrollable max-height="85vh">
<MediaIdSelector <MediaIdSelector
v-if="mediaSource === 'themoviedb'" v-model="transferForm.media_id"
v-model="transferForm.tmdbid"
@close="mediaSelectorDialog = false"
@select="handleMediaSelected"
:type="mediaSource"
/>
<MediaIdSelector
v-else
v-model="transferForm.doubanid"
@close="mediaSelectorDialog = false" @close="mediaSelectorDialog = false"
@select="handleMediaSelected" @select="handleMediaSelected"
:type="mediaSource" :type="mediaSource"
+30 -21
View File
@@ -1,21 +1,22 @@
<script lang="ts" setup> <script lang="ts" setup>
import api from '@/api' 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({ const props = defineProps<{
type: String, // 来源 themoviedb | douban type?: MediaDataSource
}) }>()
interface TmdbItem { interface MediaSelectorItem {
// 数据源原生ID
id: string
// 媒体标题 // 媒体标题
title: string title: string
// 媒体简介,包含类型标签 // 媒体简介,包含类型标签
overview: string overview: string
// TMDB ID
tmdbid: number
// 豆瓣 ID
doubanid: string
// 海报地址 // 海报地址
poster: string poster: string
// 媒体类型 // 媒体类型
@@ -25,7 +26,7 @@ interface TmdbItem {
// update:modelValue 事件 // update:modelValue 事件
const emit = defineEmits(['update:modelValue', 'select', 'close']) const emit = defineEmits(['update:modelValue', 'select', 'close'])
const items = ref<TmdbItem[]>([]) const items = ref<MediaSelectorItem[]>([])
// 搜索词 // 搜索词
const keyword = ref('') const keyword = ref('')
@@ -37,8 +38,8 @@ const loading = ref(false)
const inputKeyword = ref<HTMLElement | null>(null) const inputKeyword = ref<HTMLElement | null>(null)
// 选中条目并通知父组件同步额外媒体信息。 // 选中条目并通知父组件同步额外媒体信息。
function selectMedia(item: TmdbItem) { function selectMedia(item: MediaSelectorItem) {
emit('update:modelValue', item.tmdbid || item.doubanid) emit('update:modelValue', item.id)
emit('select', item) emit('select', item)
emit('close') emit('close')
} }
@@ -51,16 +52,18 @@ function getW500Image(url = '') {
// 搜索词条 // 搜索词条
async function searchMedias() { async function searchMedias() {
if (!keyword) return const searchKeyword = keyword.value.trim()
if (!searchKeyword) return
// 调用API搜索词条 // 调用API搜索词条
try { try {
loading.value = true loading.value = true
const result: MediaInfo[] = await api.get('media/search', { const result: MediaInfo[] = await api.get('media/search', {
params: { params: {
title: keyword.value, title: searchKeyword,
page: 1, page: 1,
count: 20, count: 20,
source: props.type,
}, },
}) })
@@ -69,19 +72,25 @@ async function searchMedias() {
// 赋值 // 赋值
for (const item of result) { 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({ items.value.push({
tmdbid: item.tmdb_id || 0, id: mediaId,
doubanid: item.douban_id || '',
poster: getW500Image(item.poster_path), poster: getW500Image(item.poster_path),
type: item.type, 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}`, overview: `<span class="text-primary">${item.type}</span> ${item.overview}`,
}) })
} }
loading.value = false
} catch (e) { } catch (e) {
console.error(e) console.error(e)
} finally {
loading.value = false
} }
} }
@@ -100,9 +109,9 @@ onMounted(() => {
<VTextField <VTextField
ref="inputKeyword" ref="inputKeyword"
v-model="keyword" v-model="keyword"
label="输入名称搜索" :label="t('dialog.reorganize.mediaSearchInput')"
single-line single-line
placeholder="电影或电视剧名称" :placeholder="t('dialog.reorganize.mediaSearchPlaceholder')"
variant="solo" variant="solo"
prepend-inner-icon="mdi-magnify" prepend-inner-icon="mdi-magnify"
flat flat
+1
View File
@@ -54,6 +54,7 @@ export function getMediaSubscribeId(media?: MediaInfo) {
if (media?.tmdb_id) return `tmdb:${media.tmdb_id}` if (media?.tmdb_id) return `tmdb:${media.tmdb_id}`
if (media?.douban_id) return `douban:${media.douban_id}` if (media?.douban_id) return `douban:${media.douban_id}`
if (media?.bangumi_id) return `bangumi:${media.bangumi_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}` return `${media?.mediaid_prefix}:${media?.media_id}`
} }
+20 -12
View File
@@ -1536,7 +1536,11 @@ export default {
recognizing: 'Recognizing...', recognizing: 'Recognizing...',
recognizeAgain: 'Recognize Again', recognizeAgain: 'Recognize Again',
title: 'Title', title: 'Title',
titleHint: 'Enter a torrent name, release title, or file name',
subtitle: 'Subtitle', 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', customWords: 'Custom Words',
customWordsPlaceholder: 'Enter one recognition rule per line; applied directly to this recognition test', customWordsPlaceholder: 'Enter one recognition rule per line; applied directly to this recognition test',
customWordsHint: customWordsHint:
@@ -1546,8 +1550,6 @@ export default {
saveWordsNoChange: 'These words already exist, no need to save again', saveWordsNoChange: 'These words already exist, no need to save again',
saveWordsFailed: 'Failed to save custom words', saveWordsFailed: 'Failed to save custom words',
requestFailed: 'Recognition request failed', requestFailed: 'Recognition request failed',
inputTitle: 'Test Input',
inputSubtitle: 'Enter a torrent or file name to inspect the recognition breakdown',
unrecognized: 'No media recognized', unrecognized: 'No media recognized',
waitingResult: 'Waiting for recognition result', waitingResult: 'Waiting for recognition result',
analysisTitle: 'Analysis Flow', analysisTitle: 'Analysis Flow',
@@ -1589,14 +1591,15 @@ export default {
testing: 'Testing...', testing: 'Testing...',
testAgain: 'Test Again', testAgain: 'Test Again',
title: 'Title', title: 'Title',
titleHint: 'Enter a release title to simulate a search or download result',
subtitle: 'Subtitle', subtitle: 'Subtitle',
subtitleHint: 'Optional release description, tags, or site subtitle used for recognition and filtering',
ruleGroup: 'Rule Group', ruleGroup: 'Rule Group',
ruleGroupHint: 'Select the filter rule group to validate',
ruleGroupPlaceholder: 'Please select', ruleGroupPlaceholder: 'Please select',
priority: 'Priority: {value}', priority: 'Priority: {value}',
noPriorityRule: 'No priority rule matched!', noPriorityRule: 'No priority rule matched!',
requestFailed: 'Rule test request failed', 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', waitingResult: 'Waiting for rule test result',
matched: 'Filter rule matched', matched: 'Filter rule matched',
priorityLabel: 'Priority', priorityLabel: 'Priority',
@@ -1692,12 +1695,6 @@ export default {
wallpaperHint: 'Choose the source of the login page background', wallpaperHint: 'Choose the source of the login page background',
recognizeSource: 'Recognition Data Source', recognizeSource: 'Recognition Data Source',
recognizeSourceHint: 'Set the default media info 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', apiToken: 'API Token',
apiTokenHint: 'Set the token value used when external requests access MoviePilot API', apiTokenHint: 'Set the token value used when external requests access MoviePilot API',
apiTokenMinChars: 'Cannot be less than 16 characters', apiTokenMinChars: 'Cannot be less than 16 characters',
@@ -2127,6 +2124,8 @@ export default {
userAgentHint: 'User-Agent of the browser with CookieCloud plugin', userAgentHint: 'User-Agent of the browser with CookieCloud plugin',
browserEmulation: 'Browser Emulation', browserEmulation: 'Browser Emulation',
browserEmulationHint: 'Choose how to emulate browser when accessing sites (CloakBrowser or FlareSolverr)', 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', flaresolverrUrl: 'FlareSolverr URL',
flaresolverrUrlHint: 'Required when using FlareSolverr, e.g. http://127.0.0.1:8191', flaresolverrUrlHint: 'Required when using FlareSolverr, e.g. http://127.0.0.1:8191',
siteDataRefresh: 'Site Data Refresh', siteDataRefresh: 'Site Data Refresh',
@@ -2238,7 +2237,7 @@ export default {
'Word to replace => Replacement\n' + 'Word to replace => Replacement\n' +
'Front word <> Back word >> Episode offset (EP)\n' + 'Front word <> Back word >> Episode offset (EP)\n' +
'Word to replace => Replacement && 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', identifierSaveSuccess: 'Custom identifiers saved successfully',
identifierSaveFailed: 'Failed to save custom identifiers!', identifierSaveFailed: 'Failed to save custom identifiers!',
@@ -3037,10 +3036,16 @@ export default {
targetPathPlaceholder: 'Choose Auto or enter a path', targetPathPlaceholder: 'Choose Auto or enter a path',
mediaType: 'Type', mediaType: 'Type',
mediaTypeHint: 'File media 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', tmdbId: 'TheMovieDb ID',
doubanId: 'Douban ID', doubanId: 'Douban ID',
bangumiId: 'Bangumi ID',
anilistId: 'AniList ID',
mediaIdHint: 'Query media ID by name, leave empty for auto recognition', mediaIdHint: 'Query media ID by name, leave empty for auto recognition',
mediaIdPlaceholder: 'Leave empty for auto recognition', mediaIdPlaceholder: 'Leave empty for auto recognition',
mediaSearchInput: 'Search Media',
mediaSearchPlaceholder: 'Enter a media title',
episodeGroup: 'Episode Group', episodeGroup: 'Episode Group',
episodeGroupHint: episodeGroupHint:
'After entering a TMDB ID, episode groups are queried automatically; group IDs can still be entered manually', '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', customWords: 'Custom Recognition Words',
customWordsHint: 'Recognition words only used for this subscription', customWordsHint: 'Recognition words only used for this subscription',
customWordsPlaceholder: 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', cancelSubscribe: 'Cancel Subscription',
save: 'Save', save: 'Save',
cancelSubscribeConfirm: 'Are you sure you want to cancel the subscription?', cancelSubscribeConfirm: 'Are you sure you want to cancel the subscription?',
@@ -3728,6 +3733,9 @@ export default {
type: 'Type', type: 'Type',
customTypeHint: 'Custom media server type, for plugin scenarios', customTypeHint: 'Custom media server type, for plugin scenarios',
enableMediaServer: 'Enable Media Server', 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', nameRequired: 'Required; cannot be duplicated',
serverAlias: 'Media server alias', serverAlias: 'Media server alias',
host: 'Host', host: 'Host',
+19 -12
View File
@@ -1529,7 +1529,11 @@ export default {
recognizing: '识别中...', recognizing: '识别中...',
recognizeAgain: '重新识别', recognizeAgain: '重新识别',
title: '标题', title: '标题',
titleHint: '输入种子名、发布标题或文件名',
subtitle: '副标题', subtitle: '副标题',
subtitleHint: '可选,补充种子描述、别名或发布信息以提高识别准确度',
source: '识别数据源',
sourceHint: '默认使用后台识别设置,可为本次测试单独切换',
customWords: '识别词', customWords: '识别词',
customWordsPlaceholder: '每行输入一组识别规则,可直接用于本次识别测试', customWordsPlaceholder: '每行输入一组识别规则,可直接用于本次识别测试',
customWordsHint: '格式与"识别词管理"一致:屏蔽词 / 被替换词 => 替换词 / 前定位词 <> 后定位词 >> 集偏移量', customWordsHint: '格式与"识别词管理"一致:屏蔽词 / 被替换词 => 替换词 / 前定位词 <> 后定位词 >> 集偏移量',
@@ -1538,8 +1542,6 @@ export default {
saveWordsNoChange: '识别词已存在,无需重复保存', saveWordsNoChange: '识别词已存在,无需重复保存',
saveWordsFailed: '识别词保存失败', saveWordsFailed: '识别词保存失败',
requestFailed: '识别请求失败', requestFailed: '识别请求失败',
inputTitle: '测试输入',
inputSubtitle: '输入种子名或文件名,查看媒体识别拆解结果',
unrecognized: '未识别到媒体', unrecognized: '未识别到媒体',
waitingResult: '等待识别结果', waitingResult: '等待识别结果',
analysisTitle: '解析链路', analysisTitle: '解析链路',
@@ -1581,14 +1583,15 @@ export default {
testing: '正在测试...', testing: '正在测试...',
testAgain: '重新测试', testAgain: '重新测试',
title: '标题', title: '标题',
titleHint: '输入用于模拟搜索或下载结果的发布标题',
subtitle: '副标题', subtitle: '副标题',
subtitleHint: '可选,补充发布描述、标签或站点副标题以参与识别和过滤',
ruleGroup: '规则组', ruleGroup: '规则组',
ruleGroupHint: '选择要验证的过滤规则组',
ruleGroupPlaceholder: '请选择', ruleGroupPlaceholder: '请选择',
priority: '优先级:{value}', priority: '优先级:{value}',
noPriorityRule: '未命中任何优先级规则!', noPriorityRule: '未命中任何优先级规则!',
requestFailed: '规则测试请求失败', requestFailed: '规则测试请求失败',
inputTitle: '规则测试',
inputSubtitle: '选择规则组后,查看过滤命中和优先级结果',
waitingResult: '等待规则测试结果', waitingResult: '等待规则测试结果',
matched: '命中过滤规则', matched: '命中过滤规则',
priorityLabel: '优先级', priorityLabel: '优先级',
@@ -1684,12 +1687,6 @@ export default {
wallpaperHint: '选择登陆页面背景来源', wallpaperHint: '选择登陆页面背景来源',
recognizeSource: '识别数据源', recognizeSource: '识别数据源',
recognizeSourceHint: '设置默认媒体信息识别数据源', recognizeSourceHint: '设置默认媒体信息识别数据源',
mediaServerSyncInterval: '媒体服务器同步间隔',
mediaServerSyncIntervalHint: '定时同步媒体服务器数据到本地的时间间隔',
hours: '小时',
required: '必选项,请勿留空',
numbersOnly: '仅支持输入数字,请勿输入其他字符',
minInterval: '间隔不能小于1个小时',
apiToken: 'API令牌', apiToken: 'API令牌',
apiTokenHint: '设置外部请求MoviePilot API时使用的token值', apiTokenHint: '设置外部请求MoviePilot API时使用的token值',
apiTokenMinChars: '不能小于16位字符', apiTokenMinChars: '不能小于16位字符',
@@ -2093,6 +2090,8 @@ export default {
siteOptions: '站点选项', siteOptions: '站点选项',
browserEmulation: '浏览器仿真', browserEmulation: '浏览器仿真',
browserEmulationHint: '站点访问仿真方式,支持 CloakBrowser 或 FlareSolverr', browserEmulationHint: '站点访问仿真方式,支持 CloakBrowser 或 FlareSolverr',
ocrHost: '验证码识别服务器',
ocrHostHint: '用于站点签到、更新站点Cookie等识别验证码',
flaresolverrUrl: 'FlareSolverr 服务地址', flaresolverrUrl: 'FlareSolverr 服务地址',
flaresolverrUrlHint: '当仿真方式为 FlareSolverr 时生效,例如:http://127.0.0.1:8191', flaresolverrUrlHint: '当仿真方式为 FlareSolverr 时生效,例如:http://127.0.0.1:8191',
siteDataRefreshInterval: '站点数据刷新间隔', siteDataRefreshInterval: '站点数据刷新间隔',
@@ -2200,7 +2199,7 @@ export default {
'被替换词 => 替换词\n' + '被替换词 => 替换词\n' +
'前定位词 <> 后定位词 >> 集偏移量(EP\n' + '前定位词 <> 后定位词 >> 集偏移量(EP\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: '自定义识别词保存成功', identifierSaveSuccess: '自定义识别词保存成功',
identifierSaveFailed: '自定义识别词保存失败!', identifierSaveFailed: '自定义识别词保存失败!',
@@ -2987,10 +2986,16 @@ export default {
targetPathPlaceholder: '选择自动或输入路径', targetPathPlaceholder: '选择自动或输入路径',
mediaType: '类型', mediaType: '类型',
mediaTypeHint: '文件的媒体类型', mediaTypeHint: '文件的媒体类型',
mediaSource: '数据源',
mediaSourceHint: '默认使用后台识别设置,可为本次整理与刮削单独切换',
tmdbId: 'TheMovieDb编号', tmdbId: 'TheMovieDb编号',
doubanId: '豆瓣编号', doubanId: '豆瓣编号',
bangumiId: 'Bangumi编号',
anilistId: 'AniList编号',
mediaIdHint: '按名称查询媒体编号,留空自动识别', mediaIdHint: '按名称查询媒体编号,留空自动识别',
mediaIdPlaceholder: '留空自动识别', mediaIdPlaceholder: '留空自动识别',
mediaSearchInput: '搜索媒体',
mediaSearchPlaceholder: '输入媒体名称',
episodeGroup: '剧集组', episodeGroup: '剧集组',
episodeGroupHint: '输入 TMDB 编号后自动查询剧集组,也可手动填写剧集组编号', episodeGroupHint: '输入 TMDB 编号后自动查询剧集组,也可手动填写剧集组编号',
episodeGroupPlaceholder: '先输入 TMDB 编号', episodeGroupPlaceholder: '先输入 TMDB 编号',
@@ -3123,7 +3128,7 @@ export default {
customWords: '自定义识别词', customWords: '自定义识别词',
customWordsHint: '只对该订阅使用的识别词', customWordsHint: '只对该订阅使用的识别词',
customWordsPlaceholder: 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: '取消订阅', cancelSubscribe: '取消订阅',
save: '保存', save: '保存',
cancelSubscribeConfirm: '是否确认取消订阅?', cancelSubscribeConfirm: '是否确认取消订阅?',
@@ -3669,6 +3674,8 @@ export default {
type: '类型', type: '类型',
customTypeHint: '自定义媒体服务器类型,用于插件等场景', customTypeHint: '自定义媒体服务器类型,用于插件等场景',
enableMediaServer: '启用媒体服务器', enableMediaServer: '启用媒体服务器',
syncInterval: '自动同步间隔',
syncIntervalHint: '留空时使用旧版全局默认值({interval} 小时),设置为 0 时关闭此服务器的自动同步',
nameRequired: '必填,不可重名', nameRequired: '必填,不可重名',
serverAlias: '媒体服务器的别名', serverAlias: '媒体服务器的别名',
host: '地址', host: '地址',
+19 -12
View File
@@ -1528,7 +1528,11 @@ export default {
recognizing: '識別中...', recognizing: '識別中...',
recognizeAgain: '重新識別', recognizeAgain: '重新識別',
title: '標題', title: '標題',
titleHint: '輸入種子名、發佈標題或文件名',
subtitle: '副標題', subtitle: '副標題',
subtitleHint: '可選,補充種子描述、別名或發佈信息以提高識別準確度',
source: '識別數據源',
sourceHint: '預設使用後台識別設置,可為本次測試單獨切換',
customWords: '識別詞', customWords: '識別詞',
customWordsPlaceholder: '每行輸入一組識別規則,可直接用於本次識別測試', customWordsPlaceholder: '每行輸入一組識別規則,可直接用於本次識別測試',
customWordsHint: '格式與「識別詞管理」一致:屏蔽詞 / 被替換詞 => 替換詞 / 前定位詞 <> 後定位詞 >> 集偏移量', customWordsHint: '格式與「識別詞管理」一致:屏蔽詞 / 被替換詞 => 替換詞 / 前定位詞 <> 後定位詞 >> 集偏移量',
@@ -1537,8 +1541,6 @@ export default {
saveWordsNoChange: '識別詞已存在,無需重複儲存', saveWordsNoChange: '識別詞已存在,無需重複儲存',
saveWordsFailed: '識別詞儲存失敗', saveWordsFailed: '識別詞儲存失敗',
requestFailed: '識別請求失敗', requestFailed: '識別請求失敗',
inputTitle: '測試輸入',
inputSubtitle: '輸入種子名或檔案名,查看媒體識別拆解結果',
unrecognized: '未識別到媒體', unrecognized: '未識別到媒體',
waitingResult: '等待識別結果', waitingResult: '等待識別結果',
analysisTitle: '解析鏈路', analysisTitle: '解析鏈路',
@@ -1580,14 +1582,15 @@ export default {
testing: '正在測試...', testing: '正在測試...',
testAgain: '重新測試', testAgain: '重新測試',
title: '標題', title: '標題',
titleHint: '輸入用於模擬搜索或下載結果的發佈標題',
subtitle: '副標題', subtitle: '副標題',
subtitleHint: '可選,補充發佈描述、標籤或站點副標題以參與識別和過濾',
ruleGroup: '規則組', ruleGroup: '規則組',
ruleGroupHint: '選擇要驗證的過濾規則組',
ruleGroupPlaceholder: '請選擇', ruleGroupPlaceholder: '請選擇',
priority: '優先級:{value}', priority: '優先級:{value}',
noPriorityRule: '未命中任何優先級規則!', noPriorityRule: '未命中任何優先級規則!',
requestFailed: '規則測試請求失敗', requestFailed: '規則測試請求失敗',
inputTitle: '規則測試',
inputSubtitle: '選擇規則組後,查看過濾命中和優先級結果',
waitingResult: '等待規則測試結果', waitingResult: '等待規則測試結果',
matched: '命中過濾規則', matched: '命中過濾規則',
priorityLabel: '優先級', priorityLabel: '優先級',
@@ -1683,12 +1686,6 @@ export default {
wallpaperHint: '選擇登陸頁面背景來源', wallpaperHint: '選擇登陸頁面背景來源',
recognizeSource: '識別數據源', recognizeSource: '識別數據源',
recognizeSourceHint: '設置默認媒體信息識別數據源', recognizeSourceHint: '設置默認媒體信息識別數據源',
mediaServerSyncInterval: '媒體服務器同步間隔',
mediaServerSyncIntervalHint: '定時同步媒體服務器數據到本地的時間間隔',
hours: '小時',
required: '必選項,請勿留空',
numbersOnly: '僅支持輸入數字,請勿輸入其他字符',
minInterval: '間隔不能小於1個小時',
apiToken: 'API令牌', apiToken: 'API令牌',
apiTokenHint: '設置外部請求MoviePilot API時使用的token值', apiTokenHint: '設置外部請求MoviePilot API時使用的token值',
apiTokenMinChars: '不能小於16位字符', apiTokenMinChars: '不能小於16位字符',
@@ -2092,6 +2089,8 @@ export default {
siteOptions: '站點選項', siteOptions: '站點選項',
browserEmulation: '瀏覽器仿真', browserEmulation: '瀏覽器仿真',
browserEmulationHint: '站點訪問仿真方式,支援 CloakBrowser 或 FlareSolverr', browserEmulationHint: '站點訪問仿真方式,支援 CloakBrowser 或 FlareSolverr',
ocrHost: '驗證碼識別服務器',
ocrHostHint: '用於站點簽到、更新站點Cookie等識別驗證碼',
flaresolverrUrl: 'FlareSolverr 服務地址', flaresolverrUrl: 'FlareSolverr 服務地址',
flaresolverrUrlHint: '當仿真方式為 FlareSolverr 時生效,例如:http://127.0.0.1:8191', flaresolverrUrlHint: '當仿真方式為 FlareSolverr 時生效,例如:http://127.0.0.1:8191',
siteDataRefreshInterval: '站點數據刷新間隔', siteDataRefreshInterval: '站點數據刷新間隔',
@@ -2199,7 +2198,7 @@ export default {
'被替換詞 => 替換詞\n' + '被替換詞 => 替換詞\n' +
'前定位詞 <> 後定位詞 >> 集偏移量(EP\n' + '前定位詞 <> 後定位詞 >> 集偏移量(EP\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: '自定義識別詞保存成功', identifierSaveSuccess: '自定義識別詞保存成功',
identifierSaveFailed: '自定義識別詞保存失敗!', identifierSaveFailed: '自定義識別詞保存失敗!',
@@ -2986,10 +2985,16 @@ export default {
targetPathPlaceholder: '選擇自動或輸入路徑', targetPathPlaceholder: '選擇自動或輸入路徑',
mediaType: '類型', mediaType: '類型',
mediaTypeHint: '文件的媒體類型', mediaTypeHint: '文件的媒體類型',
mediaSource: '數據源',
mediaSourceHint: '預設使用後台識別設置,可為本次整理與刮削單獨切換',
tmdbId: 'TheMovieDb編號', tmdbId: 'TheMovieDb編號',
doubanId: '豆瓣編號', doubanId: '豆瓣編號',
bangumiId: 'Bangumi編號',
anilistId: 'AniList編號',
mediaIdHint: '按名稱查詢媒體編號,留空自動識別', mediaIdHint: '按名稱查詢媒體編號,留空自動識別',
mediaIdPlaceholder: '留空自動識別', mediaIdPlaceholder: '留空自動識別',
mediaSearchInput: '搜索媒體',
mediaSearchPlaceholder: '輸入媒體名稱',
episodeGroup: '劇集組', episodeGroup: '劇集組',
episodeGroupHint: '輸入 TMDB 編號後自動查詢劇集組,也可手動填寫劇集組編號', episodeGroupHint: '輸入 TMDB 編號後自動查詢劇集組,也可手動填寫劇集組編號',
episodeGroupPlaceholder: '先輸入 TMDB 編號', episodeGroupPlaceholder: '先輸入 TMDB 編號',
@@ -3122,7 +3127,7 @@ export default {
customWords: '自定義識別詞', customWords: '自定義識別詞',
customWordsHint: '只對該訂閱使用的識別詞', customWordsHint: '只對該訂閱使用的識別詞',
customWordsPlaceholder: 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: '取消訂閱', cancelSubscribe: '取消訂閱',
save: '儲存', save: '儲存',
cancelSubscribeConfirm: '是否確認取消訂閱?', cancelSubscribeConfirm: '是否確認取消訂閱?',
@@ -3666,6 +3671,8 @@ export default {
type: '類型', type: '類型',
customTypeHint: '自定義媒體伺服器類型,用於插件等場景', customTypeHint: '自定義媒體伺服器類型,用於插件等場景',
enableMediaServer: '啟用媒體伺服器', enableMediaServer: '啟用媒體伺服器',
syncInterval: '自動同步間隔',
syncIntervalHint: '留空時使用舊版全局預設值({interval} 小時),設置為 0 時關閉此服務器的自動同步',
nameRequired: '必填;不可與其他名稱重名', nameRequired: '必填;不可與其他名稱重名',
serverAlias: '媒體伺服器的別名', serverAlias: '媒體伺服器的別名',
host: '地址', host: '地址',
@@ -41,6 +41,8 @@ const $toast = useToast()
const sourceItems = [ const sourceItems = [
{ 'title': 'TheMovieDb', 'value': 'themoviedb' }, { 'title': 'TheMovieDb', 'value': 'themoviedb' },
{ 'title': '豆瓣', 'value': 'douban' }, { 'title': '豆瓣', 'value': 'douban' },
{ 'title': 'Bangumi', 'value': 'bangumi' },
{ 'title': 'AniList', 'value': 'anilist' },
] ]
// //
@@ -48,6 +48,10 @@ const mediaSourcesDict = [
title: 'Bangumi', title: 'Bangumi',
value: 'bangumi', value: 'bangumi',
}, },
{
title: 'AniList',
value: 'anilist',
},
] ]
// //
+11
View File
@@ -46,6 +46,7 @@ const siteSetting = ref<any>({
SITE_MESSAGE: false, SITE_MESSAGE: false,
SEARCH_RESOURCE_PAGES: 1, SEARCH_RESOURCE_PAGES: 1,
BROWSER_EMULATION: 'cloakbrowser', BROWSER_EMULATION: 'cloakbrowser',
OCR_HOST: '',
FLARESOLVERR_URL: '', FLARESOLVERR_URL: '',
}, },
}) })
@@ -276,6 +277,16 @@ useSilentSettingRefresh(loadSiteSettings, {
prepend-inner-icon="mdi-web" prepend-inner-icon="mdi-web"
/> />
</VCol> </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'"> <VCol cols="12" md="6" v-if="siteSetting.Site.BROWSER_EMULATION == 'flaresolverr'">
<VTextField <VTextField
v-model="siteSetting.Site.FLARESOLVERR_URL" v-model="siteSetting.Site.FLARESOLVERR_URL"
+6 -43
View File
@@ -43,10 +43,7 @@ const SystemSettings = ref<any>({
APP_DOMAIN: null, APP_DOMAIN: null,
API_TOKEN: null, API_TOKEN: null,
WALLPAPER: 'tmdb', WALLPAPER: 'tmdb',
MEDIASERVER_SYNC_INTERVAL: null,
RECOGNIZE_SOURCE: 'themoviedb',
GITHUB_TOKEN: null, GITHUB_TOKEN: null,
OCR_HOST: null,
CUSTOMIZE_WALLPAPER_API_URL: null, CUSTOMIZE_WALLPAPER_API_URL: null,
AI_AGENT_ENABLE: false, AI_AGENT_ENABLE: false,
AI_AGENT_GLOBAL: false, AI_AGENT_GLOBAL: false,
@@ -197,6 +194,9 @@ const isRequest = ref(true)
// //
const mediaServers = ref<MediaServerConf[]>([]) const mediaServers = ref<MediaServerConf[]>([])
//
const legacyMediaServerSyncInterval = ref<number | null>(null)
// //
const downloaders = ref<DownloaderConf[]>([]) const downloaders = ref<DownloaderConf[]>([])
@@ -691,6 +691,8 @@ async function loadSystemSettings() {
try { try {
const result: { [key: string]: any } = await api.get('system/env') const result: { [key: string]: any } = await api.get('system/env')
if (result.success) { if (result.success) {
const defaultSyncInterval = Number(result.data.MEDIASERVER_SYNC_INTERVAL ?? Number.NaN)
legacyMediaServerSyncInterval.value = Number.isFinite(defaultSyncInterval) ? defaultSyncInterval : null
// APISystemSettings // APISystemSettings
for (const sectionKey of Object.keys(SystemSettings.value) as Array<keyof typeof SystemSettings.value>) { for (const sectionKey of Object.keys(SystemSettings.value) as Array<keyof typeof SystemSettings.value>) {
Object.keys(SystemSettings.value[sectionKey]).forEach((key: string) => { Object.keys(SystemSettings.value[sectionKey]).forEach((key: string) => {
@@ -1100,36 +1102,6 @@ watch(currentLlmSnapshotKey, (snapshotKey, previousSnapshotKey) => {
</VCol> </VCol>
</VRow> </VRow>
</VCol> </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"> <VCol cols="12" md="6">
<VTextField <VTextField
v-model="SystemSettings.Basic.API_TOKEN" v-model="SystemSettings.Basic.API_TOKEN"
@@ -1159,16 +1131,6 @@ watch(currentLlmSnapshotKey, (snapshotKey, previousSnapshotKey) => {
> >
</VTextField> </VTextField>
</VCol> </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> </VRow>
<VCard <VCard
variant="outlined" variant="outlined"
@@ -1817,6 +1779,7 @@ watch(currentLlmSnapshotKey, (snapshotKey, previousSnapshotKey) => {
<MediaServerCard <MediaServerCard
:mediaserver="element" :mediaserver="element"
:mediaservers="mediaServers" :mediaservers="mediaServers"
:default-sync-interval="legacyMediaServerSyncInterval ?? undefined"
@close="removeMediaServer(element)" @close="removeMediaServer(element)"
@change="onMediaServerChange" @change="onMediaServerChange"
/> />
+52 -27
View File
@@ -3,9 +3,10 @@ import { computed, reactive, ref } from 'vue'
import { useToast } from 'vue-toastification' import { useToast } from 'vue-toastification'
import { requiredValidator } from '@/@validators' import { requiredValidator } from '@/@validators'
import api from '@/api' import api from '@/api'
import type { Context } from '@/api/types' import type { Context, MediaDataSource, MediaInfo } from '@/api/types'
import { getMediaSubscribeId } from '@/composables/useMediaSubscribe' import { getMediaSubscribeId } from '@/composables/useMediaSubscribe'
import router from '@/router' import router from '@/router'
import { useGlobalSettingsStore } from '@/stores'
import { useI18n } from 'vue-i18n' import { useI18n } from 'vue-i18n'
interface PipelineStep { interface PipelineStep {
@@ -16,6 +17,20 @@ interface PipelineStep {
// //
const { t } = useI18n() 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() const $toast = useToast()
@@ -28,6 +43,7 @@ const nameTestForm = reactive({
title: '', title: '',
subtitle: '', subtitle: '',
customWords: '', customWords: '',
source: getDefaultMediaSource(),
}) })
// //
@@ -67,9 +83,25 @@ const resourceChips = computed(() => {
// //
const canViewMediaDetail = computed(() => const canViewMediaDetail = computed(() =>
Boolean( 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[]>(() => [ const pipelineSteps = computed<PipelineStep[]>(() => [
{ {
icon: 'mdi-file-document-outline', icon: 'mdi-file-document-outline',
@@ -87,11 +119,7 @@ const pipelineSteps = computed<PipelineStep[]>(() => [
{ {
icon: 'mdi-movie-search-outline', icon: 'mdi-movie-search-outline',
title: t('nameTest.steps.media.title'), title: t('nameTest.steps.media.title'),
value: mediaInfo.value?.tmdb_id value: getMediaIdentityLabel(mediaInfo.value),
? `TMDB ${mediaInfo.value.tmdb_id}`
: mediaInfo.value?.douban_id
? `Douban ${mediaInfo.value.douban_id}`
: mediaInfo.value?.title || t('nameTest.unrecognized'),
}, },
]) ])
@@ -130,6 +158,7 @@ async function nameTest() {
title: nameTestForm.title, title: nameTestForm.title,
subtitle: nameTestForm.subtitle, subtitle: nameTestForm.subtitle,
custom_words: nameTestForm.customWords || undefined, custom_words: nameTestForm.customWords || undefined,
source: nameTestForm.source,
}, },
}) })
nameTestText.value = t('nameTest.recognizeAgain') nameTestText.value = t('nameTest.recognizeAgain')
@@ -184,32 +213,34 @@ async function saveCustomWords() {
<template> <template>
<div class="shortcut-workbench"> <div class="shortcut-workbench">
<section class="shortcut-panel shortcut-input-panel"> <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"> <VForm validate-on="submit lazy" @submit.prevent="nameTest">
<VRow class="shortcut-form"> <VRow class="shortcut-form">
<VCol cols="12" class="shortcut-form-col"> <VCol cols="12" class="shortcut-form-col">
<VTextField <VTextField
v-model="nameTestForm.title" v-model="nameTestForm.title"
:label="t('nameTest.title')" :label="t('nameTest.title')"
:hint="t('nameTest.titleHint')"
persistent-hint
:rules="[requiredValidator]" :rules="[requiredValidator]"
prepend-inner-icon="mdi-movie-open" prepend-inner-icon="mdi-movie-open"
/> />
</VCol> </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"> <VCol cols="12" class="shortcut-form-col">
<VTextarea <VTextarea
v-model="nameTestForm.subtitle" v-model="nameTestForm.subtitle"
:label="t('nameTest.subtitle')" :label="t('nameTest.subtitle')"
:hint="t('nameTest.subtitleHint')"
persistent-hint
rows="2" rows="2"
auto-grow auto-grow
prepend-inner-icon="mdi-subtitles" prepend-inner-icon="mdi-subtitles"
@@ -220,6 +251,8 @@ async function saveCustomWords() {
v-model="nameTestForm.customWords" v-model="nameTestForm.customWords"
:label="t('nameTest.customWords')" :label="t('nameTest.customWords')"
:placeholder="t('nameTest.customWordsPlaceholder')" :placeholder="t('nameTest.customWordsPlaceholder')"
:hint="t('nameTest.customWordsHint')"
persistent-hint
rows="3" rows="3"
auto-grow auto-grow
prepend-inner-icon="mdi-tag-text-outline" prepend-inner-icon="mdi-tag-text-outline"
@@ -370,14 +403,6 @@ async function saveCustomWords() {
box-shadow: var(--app-surface-shadow); 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 { .shortcut-form {
margin: 0; margin: 0;
} }
+6 -20
View File
@@ -180,24 +180,14 @@ onMounted(() => {
<template> <template>
<div class="shortcut-workbench"> <div class="shortcut-workbench">
<section class="shortcut-panel shortcut-input-panel"> <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"> <VForm ref="ruleTestFormRef" validate-on="submit lazy" @submit.prevent="ruleTest">
<VRow class="shortcut-form"> <VRow class="shortcut-form">
<VCol cols="12" class="shortcut-form-col"> <VCol cols="12" class="shortcut-form-col">
<VTextField <VTextField
v-model="ruleTestForm.title" v-model="ruleTestForm.title"
:label="t('ruleTest.title')" :label="t('ruleTest.title')"
:hint="t('ruleTest.titleHint')"
persistent-hint
:rules="[requiredValidator]" :rules="[requiredValidator]"
prepend-inner-icon="mdi-movie-open" prepend-inner-icon="mdi-movie-open"
/> />
@@ -207,6 +197,8 @@ onMounted(() => {
v-model="ruleTestForm.rulegroup" v-model="ruleTestForm.rulegroup"
:items="filterRuleGroupItems" :items="filterRuleGroupItems"
:label="t('ruleTest.ruleGroup')" :label="t('ruleTest.ruleGroup')"
:hint="t('ruleTest.ruleGroupHint')"
persistent-hint
:loading="filterRuleGroupLoading" :loading="filterRuleGroupLoading"
:rules="[requiredValidator]" :rules="[requiredValidator]"
prepend-inner-icon="mdi-filter" prepend-inner-icon="mdi-filter"
@@ -216,6 +208,8 @@ onMounted(() => {
<VTextarea <VTextarea
v-model="ruleTestForm.subtitle" v-model="ruleTestForm.subtitle"
:label="t('ruleTest.subtitle')" :label="t('ruleTest.subtitle')"
:hint="t('ruleTest.subtitleHint')"
persistent-hint
rows="2" rows="2"
auto-grow auto-grow
prepend-inner-icon="mdi-subtitles" prepend-inner-icon="mdi-subtitles"
@@ -317,14 +311,6 @@ onMounted(() => {
padding: 1rem; padding: 1rem;
} }
.panel-heading {
display: flex;
gap: 0.75rem;
align-items: flex-start;
justify-content: space-between;
margin-block-end: 1rem;
}
.shortcut-form { .shortcut-form {
margin: 0; margin: 0;
} }