feat: 添加剧集组功能,支持自动查询和手动输入剧集组编号

This commit is contained in:
jxxghp
2026-05-24 23:33:17 +08:00
parent 96d3426d0c
commit c6981e9955
4 changed files with 121 additions and 17 deletions

View File

@@ -150,6 +150,15 @@ let previewFileBodyResizeObserver: ResizeObserver | undefined
// 所有存储
const storages = ref<StorageConf[]>([])
// 所有剧集组
const episodeGroups = ref<{ [key: string]: any }[]>([])
// 剧集组加载状态
const episodeGroupLoading = ref(false)
// 剧集组查询防抖句柄
let episodeGroupQueryTimer: ReturnType<typeof setTimeout> | undefined
// 查询存储
async function loadStorages() {
try {
@@ -169,6 +178,57 @@ const storageOptions = computed(() => {
}))
})
// 剧集组选项属性
function episodeGroupItemProps(item: { title: string; subtitle?: string }) {
return {
title: item.title,
subtitle: item.subtitle,
}
}
// 剧集组选项,保留空值作为不指定剧集组。
const episodeGroupOptions = computed(() => {
const options = (
episodeGroups.value as { id: string; name: string; group_count: number; episode_count: number }[]
).map(item => {
return {
title: item.name,
subtitle: `${t('dialog.reorganize.seasonCount', { count: item.group_count })} • ${t(
'dialog.reorganize.episodeCount',
{ count: item.episode_count },
)}`,
value: item.id,
}
})
options.unshift({
title: t('dialog.reorganize.defaultEpisodeGroup'),
subtitle: t('dialog.reorganize.defaultEpisodeGroupHint'),
value: '',
})
return options
})
// 查询指定 TMDB 剧集的所有剧集组。
async function getEpisodeGroups(tmdbid?: number | string) {
const normalizedTmdbId = Number(tmdbid)
if (!Number.isInteger(normalizedTmdbId) || normalizedTmdbId <= 0) {
episodeGroups.value = []
return
}
episodeGroupLoading.value = true
try {
episodeGroups.value = await api.get(`media/groups/${normalizedTmdbId}`)
} catch (error) {
console.error(error)
episodeGroups.value = []
} finally {
episodeGroupLoading.value = false
}
}
// 标题
const dialogTitle = computed(() => {
return t('dialog.reorganize.manualTitle')
@@ -252,6 +312,31 @@ watch(
},
)
// 监听 TMDB 编号变化,自动加载可用剧集组并清空旧选择。
watch(
() => transferForm.tmdbid,
tmdbid => {
transferForm.episode_group = ''
episodeGroups.value = []
if (episodeGroupQueryTimer) clearTimeout(episodeGroupQueryTimer)
if (transferForm.type_name !== '电视剧' || mediaSource.value !== 'themoviedb') return
episodeGroupQueryTimer = setTimeout(() => getEpisodeGroups(tmdbid), 400)
},
)
// 切换媒体类型或识别源时,非 TMDB 电视剧不保留剧集组选择。
watch(
[() => transferForm.type_name, () => mediaSource.value],
([typeName, source]) => {
if (typeName === '电视剧' && source === 'themoviedb' && transferForm.tmdbid) {
getEpisodeGroups(transferForm.tmdbid)
return
}
transferForm.episode_group = ''
episodeGroups.value = []
},
)
// 过滤后的预览数据
const filteredPreviewItems = computed(() => {
return previewData.value?.items ?? []
@@ -528,6 +613,7 @@ function createTransferPayload(options: { item?: FileItem; items?: FileItem[]; l
...transferForm,
fileitem: sourceItem,
logid: options.logid ?? 0,
episode_group: transferForm.episode_group?.trim() || undefined,
}
if (options.items?.length) {
@@ -1014,6 +1100,7 @@ onMounted(() => {
onUnmounted(() => {
stopLoadingProgress()
if (episodeGroupQueryTimer) clearTimeout(episodeGroupQueryTimer)
previewFileBodyResizeObserver?.disconnect()
})
</script>
@@ -1123,9 +1210,14 @@ onUnmounted(() => {
</VCol>
</VRow>
<VRow v-show="transferForm.type_name === '电视剧'">
<VCol cols="12" md="6">
<VTextField
<VCol v-if="mediaSource === 'themoviedb'" cols="12" md="6">
<VCombobox
v-model="transferForm.episode_group"
:items="episodeGroupOptions"
:item-props="episodeGroupItemProps"
:loading="episodeGroupLoading"
:disabled="!transferForm.tmdbid"
clearable
:label="t('dialog.reorganize.episodeGroup')"
:placeholder="t('dialog.reorganize.episodeGroupPlaceholder')"
:hint="t('dialog.reorganize.episodeGroupHint')"

View File

@@ -1850,7 +1850,7 @@ export default {
'Word to replace => Replacement\n' +
'Front word <> Back word >> Episode offset (EP)\n' +
'Word to replace => Replacement && Front word <> Back word >> Episode offset (EP)\n' +
'Replacement format supports: &#123;[tmdbid/doubanid=xxx;type=movie/tv;s=xxx;e=xxx]&#125; to directly specify TMDBID/Douban ID, where s and e are season and episode numbers (optional)',
'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)',
identifierSaveSuccess: 'Custom identifiers saved successfully',
identifierSaveFailed: 'Failed to save custom identifiers!',
@@ -2544,9 +2544,13 @@ export default {
doubanId: 'Douban ID',
mediaIdHint: 'Query media ID by name, leave empty for auto recognition',
mediaIdPlaceholder: 'Leave empty for auto recognition',
episodeGroup: 'Episode Group ID',
episodeGroupHint: 'Specify episode group',
episodeGroupPlaceholder: 'Manually query episode group',
episodeGroup: 'Episode Group',
episodeGroupHint: 'After entering a TMDB ID, episode groups are queried automatically; group IDs can still be entered manually',
episodeGroupPlaceholder: 'Enter TMDB ID first',
defaultEpisodeGroup: 'No episode group',
defaultEpisodeGroupHint: 'Use TMDB default season and episode order',
seasonCount: '{count} seasons',
episodeCount: '{count} episodes',
season: 'Season',
seasonHint: 'Which season',
episodeDetail: 'Episode',
@@ -2660,7 +2664,7 @@ export default {
customWords: 'Custom Recognition Words',
customWordsHint: 'Recognition words only used for this subscription',
customWordsPlaceholder:
'Block word\nReplaced word => Replacement word\nPrefix <> Suffix >> Episode offset (EP)\nReplaced word => Replacement word && Prefix <> Suffix >> Episode offset (EP)\nReplacement word supports format: &#123; tmdbid/doubanid=xxx;type=movie/tv;s=xxx;e=xxx &#125; to directly specify TMDBID/Douban ID recognition, where s, e are season and episode numbers (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=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)',
cancelSubscribe: 'Cancel Subscription',
save: 'Save',
cancelSubscribeConfirm: 'Are you sure you want to cancel the subscription?',

View File

@@ -1819,7 +1819,7 @@ export default {
'被替换词 => 替换词\n' +
'前定位词 <> 后定位词 >> 集偏移量EP\n' +
'被替换词 => 替换词 && 前定位词 <> 后定位词 >> 集偏移量EP\n' +
'其中替换词支持格式:&#123;[tmdbid/doubanid=xxx;type=movie/tv;s=xxx;e=xxx]&#125; 直接指定TMDBID/豆瓣ID识别其中s、e为季数和集数可选',
'其中替换词支持格式:&#123;[tmdbid/doubanid=xxx;type=movie/tv;g=xxx;s=xxx;e=xxx]&#125; 直接指定TMDBID/豆瓣ID识别其中g为剧集组编号s、e为季数和集数可选)',
identifierSaveSuccess: '自定义识别词保存成功',
identifierSaveFailed: '自定义识别词保存失败!',
@@ -2498,9 +2498,13 @@ export default {
doubanId: '豆瓣编号',
mediaIdHint: '按名称查询媒体编号,留空自动识别',
mediaIdPlaceholder: '留空自动识别',
episodeGroup: '剧集组编号',
episodeGroupHint: '指定剧集组',
episodeGroupPlaceholder: '手动查询剧集组',
episodeGroup: '剧集组',
episodeGroupHint: '输入 TMDB 编号后自动查询剧集组,也可手动填写剧集组编号',
episodeGroupPlaceholder: '先输入 TMDB 编号',
defaultEpisodeGroup: '不指定剧集组',
defaultEpisodeGroupHint: '使用 TMDB 默认季集排序',
seasonCount: '{count} 季',
episodeCount: '{count} 集',
season: '季',
seasonHint: '第几季',
episodeDetail: '集',
@@ -2613,7 +2617,7 @@ export default {
customWords: '自定义识别词',
customWordsHint: '只对该订阅使用的识别词',
customWordsPlaceholder:
'屏蔽词\n被替换词 => 替换词\n前定位词 <> 后定位词 >> 集偏移量EP\n被替换词 => 替换词 && 前定位词 <> 后定位词 >> 集偏移量EP\n其中替换词支持格式&#123; tmdbid/doubanid=xxx;type=movie/tv;s=xxx;e=xxx &#125; 直接指定TMDBID/豆瓣ID识别其中s、e为季数和集数可选',
'屏蔽词\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为季数和集数可选)',
cancelSubscribe: '取消订阅',
save: '保存',
cancelSubscribeConfirm: '是否确认取消订阅?',

View File

@@ -1820,7 +1820,7 @@ export default {
'被替換詞 => 替換詞\n' +
'前定位詞 <> 後定位詞 >> 集偏移量EP\n' +
'被替換詞 => 替換詞 && 前定位詞 <> 後定位詞 >> 集偏移量EP\n' +
'其中替換詞支持格式:&#123;[tmdbid/doubanid=xxx;type=movie/tv;s=xxx;e=xxx]&#125; 直接指定TMDBID/豆瓣ID識別其中s、e為季數和集數可選',
'其中替換詞支持格式:&#123;[tmdbid/doubanid=xxx;type=movie/tv;g=xxx;s=xxx;e=xxx]&#125; 直接指定TMDBID/豆瓣ID識別其中g為劇集組編號s、e為季數和集數可選)',
identifierSaveSuccess: '自定義識別詞保存成功',
identifierSaveFailed: '自定義識別詞保存失敗!',
@@ -2499,9 +2499,13 @@ export default {
doubanId: '豆瓣編號',
mediaIdHint: '按名稱查詢媒體編號,留空自動識別',
mediaIdPlaceholder: '留空自動識別',
episodeGroup: '劇集組編號',
episodeGroupHint: '指定劇集組',
episodeGroupPlaceholder: '手動查詢劇集組',
episodeGroup: '劇集組',
episodeGroupHint: '輸入 TMDB 編號後自動查詢劇集組,也可手動填寫劇集組編號',
episodeGroupPlaceholder: '先輸入 TMDB 編號',
defaultEpisodeGroup: '不指定劇集組',
defaultEpisodeGroupHint: '使用 TMDB 預設季集排序',
seasonCount: '{count} 季',
episodeCount: '{count} 集',
season: '季',
seasonHint: '第幾季',
episodeDetail: '集',
@@ -2614,7 +2618,7 @@ export default {
customWords: '自定義識別詞',
customWordsHint: '只對該訂閱使用的識別詞',
customWordsPlaceholder:
'屏蔽詞\n被替換詞 => 替換詞\n前定位詞 <> 後定位詞 >> 集偏移量EP\n被替換詞 => 替換詞 && 前定位詞 <> 後定位詞 >> 集偏移量EP\n其中替換詞支援格式&#123; tmdbid/doubanid=xxx;type=movie/tv;s=xxx;e=xxx &#125; 直接指定TMDBID/豆瓣ID識別其中s、e為季數和集數可選',
'屏蔽詞\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為季數和集數可選)',
cancelSubscribe: '取消訂閱',
save: '儲存',
cancelSubscribeConfirm: '是否確認取消訂閱?',