mirror of
https://github.com/jxxghp/MoviePilot-Frontend.git
synced 2026-08-22 08:52:21 +08:00
feat: support selectable anime metadata sources
This commit is contained in:
@@ -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
|
||||
// 类型
|
||||
|
||||
@@ -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),
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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}`
|
||||
}
|
||||
|
||||
|
||||
@@ -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: {[tmdbid/doubanid=xxx;type=movie/tv;g=xxx;s=xxx;e=xxx]} 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: {[tmdbid/doubanid/bangumiid/anilistid=xxx;type=movie/tv;g=xxx;s=xxx;e=xxx]} 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: {[tmdbid/doubanid=xxx;type=movie/tv;g=xxx;s=xxx;e=xxx]} 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: {[tmdbid/doubanid/bangumiid/anilistid=xxx;type=movie/tv;g=xxx;s=xxx;e=xxx]} 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',
|
||||
|
||||
@@ -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' +
|
||||
'其中替换词支持格式:{[tmdbid/doubanid=xxx;type=movie/tv;g=xxx;s=xxx;e=xxx]} 直接指定TMDBID/豆瓣ID识别,其中g为剧集组编号,s、e为季数和集数(均可选)',
|
||||
'其中替换词支持格式:{[tmdbid/doubanid/bangumiid/anilistid=xxx;type=movie/tv;g=xxx;s=xxx;e=xxx]} 直接指定媒体数据源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其中替换词支持格式:{[tmdbid/doubanid=xxx;type=movie/tv;g=xxx;s=xxx;e=xxx]} 直接指定TMDBID/豆瓣ID识别,其中g为剧集组编号,s、e为季数和集数(均可选)',
|
||||
'屏蔽词\n被替换词 => 替换词\n前定位词 <> 后定位词 >> 集偏移量(EP)\n被替换词 => 替换词 && 前定位词 <> 后定位词 >> 集偏移量(EP)\n其中替换词支持格式:{[tmdbid/doubanid/bangumiid/anilistid=xxx;type=movie/tv;g=xxx;s=xxx;e=xxx]} 直接指定媒体数据源ID识别,其中g为剧集组编号,s、e为季数和集数(均可选)',
|
||||
cancelSubscribe: '取消订阅',
|
||||
save: '保存',
|
||||
cancelSubscribeConfirm: '是否确认取消订阅?',
|
||||
@@ -3669,6 +3674,8 @@ export default {
|
||||
type: '类型',
|
||||
customTypeHint: '自定义媒体服务器类型,用于插件等场景',
|
||||
enableMediaServer: '启用媒体服务器',
|
||||
syncInterval: '自动同步间隔',
|
||||
syncIntervalHint: '留空时使用旧版全局默认值({interval} 小时),设置为 0 时关闭此服务器的自动同步',
|
||||
nameRequired: '必填,不可重名',
|
||||
serverAlias: '媒体服务器的别名',
|
||||
host: '地址',
|
||||
|
||||
@@ -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' +
|
||||
'其中替換詞支持格式:{[tmdbid/doubanid=xxx;type=movie/tv;g=xxx;s=xxx;e=xxx]} 直接指定TMDBID/豆瓣ID識別,其中g為劇集組編號,s、e為季數和集數(均可選)',
|
||||
'其中替換詞支持格式:{[tmdbid/doubanid/bangumiid/anilistid=xxx;type=movie/tv;g=xxx;s=xxx;e=xxx]} 直接指定媒體數據源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其中替換詞支援格式:{[tmdbid/doubanid=xxx;type=movie/tv;g=xxx;s=xxx;e=xxx]} 直接指定TMDBID/豆瓣ID識別,其中g為劇集組編號,s、e為季數和集數(均可選)',
|
||||
'屏蔽詞\n被替換詞 => 替換詞\n前定位詞 <> 後定位詞 >> 集偏移量(EP)\n被替換詞 => 替換詞 && 前定位詞 <> 後定位詞 >> 集偏移量(EP)\n其中替換詞支援格式:{[tmdbid/doubanid/bangumiid/anilistid=xxx;type=movie/tv;g=xxx;s=xxx;e=xxx]} 直接指定媒體數據源ID識別,其中g為劇集組編號,s、e為季數和集數(均可選)',
|
||||
cancelSubscribe: '取消訂閱',
|
||||
save: '儲存',
|
||||
cancelSubscribeConfirm: '是否確認取消訂閱?',
|
||||
@@ -3666,6 +3671,8 @@ export default {
|
||||
type: '類型',
|
||||
customTypeHint: '自定義媒體伺服器類型,用於插件等場景',
|
||||
enableMediaServer: '啟用媒體伺服器',
|
||||
syncInterval: '自動同步間隔',
|
||||
syncIntervalHint: '留空時使用舊版全局預設值({interval} 小時),設置為 0 時關閉此服務器的自動同步',
|
||||
nameRequired: '必填;不可與其他名稱重名',
|
||||
serverAlias: '媒體伺服器的別名',
|
||||
host: '地址',
|
||||
|
||||
@@ -41,6 +41,8 @@ const $toast = useToast()
|
||||
const sourceItems = [
|
||||
{ 'title': 'TheMovieDb', 'value': 'themoviedb' },
|
||||
{ 'title': '豆瓣', 'value': 'douban' },
|
||||
{ 'title': 'Bangumi', 'value': 'bangumi' },
|
||||
{ 'title': 'AniList', 'value': 'anilist' },
|
||||
]
|
||||
|
||||
// 存储选项(排除已添加的)
|
||||
|
||||
@@ -48,6 +48,10 @@ const mediaSourcesDict = [
|
||||
title: 'Bangumi',
|
||||
value: 'bangumi',
|
||||
},
|
||||
{
|
||||
title: 'AniList',
|
||||
value: 'anilist',
|
||||
},
|
||||
]
|
||||
|
||||
// 当前选中的媒体信息数据源
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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"
|
||||
/>
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user