mirror of
https://github.com/jxxghp/MoviePilot-Frontend.git
synced 2026-09-09 09:46:44 +08:00
feat(subscribe): 支持设置单条订阅搜索周期
This commit is contained in:
@@ -100,6 +100,8 @@ export interface ManualScrapeOptions {
|
|||||||
|
|
||||||
// 订阅
|
// 订阅
|
||||||
export interface Subscribe {
|
export interface Subscribe {
|
||||||
|
/** 独立定时搜索间隔(小时);null 或缺省跟随系统。 */
|
||||||
|
search_interval?: number | null
|
||||||
// 订阅ID
|
// 订阅ID
|
||||||
id: number
|
id: number
|
||||||
// 订阅名称
|
// 订阅名称
|
||||||
|
|||||||
@@ -78,6 +78,7 @@ const subscribeForm = ref<Subscribe>({
|
|||||||
media_source: undefined,
|
media_source: undefined,
|
||||||
media_id: undefined,
|
media_id: undefined,
|
||||||
state: '',
|
state: '',
|
||||||
|
search_interval: null,
|
||||||
last_update: '',
|
last_update: '',
|
||||||
username: '',
|
username: '',
|
||||||
sites: [],
|
sites: [],
|
||||||
@@ -90,6 +91,20 @@ const subscribeForm = ref<Subscribe>({
|
|||||||
episode_group: '',
|
episode_group: '',
|
||||||
})
|
})
|
||||||
|
|
||||||
|
// 切换为系统周期时显式提交 null,确保覆盖已保存的独立周期。
|
||||||
|
const customSearchInterval = computed({
|
||||||
|
get: () => subscribeForm.value.search_interval != null,
|
||||||
|
set: value => {
|
||||||
|
subscribeForm.value.search_interval = value ? 24 : null
|
||||||
|
},
|
||||||
|
})
|
||||||
|
|
||||||
|
// 空输入和非整数不能保存;接口采用同样的小时范围。
|
||||||
|
const searchIntervalValid = computed(() => {
|
||||||
|
const interval = subscribeForm.value.search_interval
|
||||||
|
return interval == null || (Number.isInteger(Number(interval)) && Number(interval) >= 1 && Number(interval) <= 8760)
|
||||||
|
})
|
||||||
|
|
||||||
// 提示框
|
// 提示框
|
||||||
const $toast = useToast()
|
const $toast = useToast()
|
||||||
|
|
||||||
@@ -190,6 +205,7 @@ const filterRuleGroupOptions = computed(() => {
|
|||||||
|
|
||||||
// 调用API修改订阅
|
// 调用API修改订阅
|
||||||
async function updateSubscribeInfo() {
|
async function updateSubscribeInfo() {
|
||||||
|
if (!searchIntervalValid.value) return
|
||||||
const displayName = getSubscribeDisplayName()
|
const displayName = getSubscribeDisplayName()
|
||||||
try {
|
try {
|
||||||
await api.put<null>('subscribe/', subscribeForm.value, { feedback: 'silent' })
|
await api.put<null>('subscribe/', subscribeForm.value, { feedback: 'silent' })
|
||||||
@@ -209,7 +225,7 @@ async function updateSubscribeInfo() {
|
|||||||
|
|
||||||
// 设置用户设置的默认订阅规则
|
// 设置用户设置的默认订阅规则
|
||||||
async function saveDefaultSubscribeConfig() {
|
async function saveDefaultSubscribeConfig() {
|
||||||
if (!canAdmin.value) return
|
if (!canAdmin.value || !searchIntervalValid.value) return
|
||||||
|
|
||||||
const typeName = getDefaultSubscribeTypeName()
|
const typeName = getDefaultSubscribeTypeName()
|
||||||
try {
|
try {
|
||||||
@@ -523,6 +539,32 @@ onMounted(() => {
|
|||||||
/>
|
/>
|
||||||
</VCol>
|
</VCol>
|
||||||
</VRow>
|
</VRow>
|
||||||
|
<VRow>
|
||||||
|
<VCol cols="12" :md="customSearchInterval ? 6 : 12">
|
||||||
|
<VSelect
|
||||||
|
v-model="customSearchInterval"
|
||||||
|
:items="[
|
||||||
|
{ title: t('dialog.subscribeEdit.searchIntervalSystem'), value: false },
|
||||||
|
{ title: t('dialog.subscribeEdit.searchIntervalCustom'), value: true },
|
||||||
|
]"
|
||||||
|
:label="t('dialog.subscribeEdit.searchInterval')"
|
||||||
|
:hint="t('dialog.subscribeEdit.searchIntervalHint')"
|
||||||
|
persistent-hint
|
||||||
|
prepend-inner-icon="mdi-timer-outline"
|
||||||
|
/>
|
||||||
|
</VCol>
|
||||||
|
<VCol v-if="customSearchInterval" cols="12" md="6">
|
||||||
|
<VTextField
|
||||||
|
v-model.number="subscribeForm.search_interval"
|
||||||
|
type="number"
|
||||||
|
min="1"
|
||||||
|
max="8760"
|
||||||
|
step="1"
|
||||||
|
:label="t('dialog.subscribeEdit.searchIntervalHours')"
|
||||||
|
:error-messages="searchIntervalValid ? [] : [t('dialog.subscribeEdit.searchIntervalInvalid')]"
|
||||||
|
/>
|
||||||
|
</VCol>
|
||||||
|
</VRow>
|
||||||
<VRow>
|
<VRow>
|
||||||
<VCol cols="12" md="6">
|
<VCol cols="12" md="6">
|
||||||
<VAutocomplete
|
<VAutocomplete
|
||||||
@@ -677,6 +719,7 @@ onMounted(() => {
|
|||||||
<VBtn
|
<VBtn
|
||||||
color="primary"
|
color="primary"
|
||||||
variant="flat"
|
variant="flat"
|
||||||
|
:disabled="!searchIntervalValid"
|
||||||
@click=";`${props.default ? saveDefaultSubscribeConfig() : updateSubscribeInfo()}`"
|
@click=";`${props.default ? saveDefaultSubscribeConfig() : updateSubscribeInfo()}`"
|
||||||
prepend-icon="mdi-content-save"
|
prepend-icon="mdi-content-save"
|
||||||
class="px-5"
|
class="px-5"
|
||||||
|
|||||||
@@ -117,6 +117,55 @@ describe('SubscribeEditDialog', () => {
|
|||||||
expect(screen.getByText('2 季 • 24 集')).toBeInTheDocument()
|
expect(screen.getByText('2 季 • 24 集')).toBeInTheDocument()
|
||||||
})
|
})
|
||||||
|
|
||||||
|
it('saves a custom search interval and explicitly restores the system setting', async () => {
|
||||||
|
const updated = vi.fn()
|
||||||
|
server.use(
|
||||||
|
subscribeDetailsHandler(
|
||||||
|
820,
|
||||||
|
createSubscribe({ id: 820, name: '搜索周期测试', search_interval: null, type: '电影' }),
|
||||||
|
),
|
||||||
|
updateSubscribeHandler({ success: true }, 200, updated),
|
||||||
|
)
|
||||||
|
useDialogOptions()
|
||||||
|
const user = userEvent.setup()
|
||||||
|
await renderDialog({ subid: 820 })
|
||||||
|
await screen.findByText('搜索周期测试')
|
||||||
|
await user.click(screen.getByLabelText('搜索周期'))
|
||||||
|
await user.click(await screen.findByRole('option', { name: '自定义' }))
|
||||||
|
await fireEvent.update(screen.getByLabelText('搜索间隔(小时)'), '6')
|
||||||
|
await user.click(screen.getByRole('button', { name: '保存' }))
|
||||||
|
await waitFor(() => expect(updated).toHaveBeenCalledOnce())
|
||||||
|
expect(updated.mock.calls[0][0]).toMatchObject({ search_interval: 6 })
|
||||||
|
|
||||||
|
await user.click(screen.getByLabelText('搜索周期'))
|
||||||
|
await user.click(await screen.findByRole('option', { name: '跟随系统' }))
|
||||||
|
expect(screen.queryByLabelText('搜索间隔(小时)')).not.toBeInTheDocument()
|
||||||
|
await user.click(screen.getByRole('button', { name: '保存' }))
|
||||||
|
await waitFor(() => expect(updated).toHaveBeenCalledTimes(2))
|
||||||
|
expect(updated.mock.calls[1][0]).toMatchObject({ search_interval: null })
|
||||||
|
})
|
||||||
|
|
||||||
|
it('loads a saved custom interval and prevents invalid values from being submitted', async () => {
|
||||||
|
const updated = vi.fn()
|
||||||
|
server.use(
|
||||||
|
subscribeDetailsHandler(
|
||||||
|
821,
|
||||||
|
createSubscribe({ id: 821, name: '周期校验测试', search_interval: 48, type: '电影' }),
|
||||||
|
),
|
||||||
|
updateSubscribeHandler({ success: true }, 200, updated),
|
||||||
|
)
|
||||||
|
useDialogOptions()
|
||||||
|
await renderDialog({ subid: 821 })
|
||||||
|
expect(await screen.findByLabelText('搜索间隔(小时)')).toHaveValue(48)
|
||||||
|
for (const value of ['', '0', '-1', '1.5', '8761']) {
|
||||||
|
await fireEvent.update(screen.getByLabelText('搜索间隔(小时)'), value)
|
||||||
|
expect(screen.getByRole('button', { name: '保存' })).toBeDisabled()
|
||||||
|
}
|
||||||
|
expect(updated).not.toHaveBeenCalled()
|
||||||
|
await fireEvent.update(screen.getByLabelText('搜索间隔(小时)'), '24')
|
||||||
|
expect(screen.getByRole('button', { name: '保存' })).toBeEnabled()
|
||||||
|
})
|
||||||
|
|
||||||
it('keeps movie titles free of season suffixes and skips episode groups', async () => {
|
it('keeps movie titles free of season suffixes and skips episode groups', async () => {
|
||||||
const record = createSubscribe({ id: 802, media_id: '8020', name: '电影测试项', season: undefined, type: '电影' })
|
const record = createSubscribe({ id: 802, media_id: '8020', name: '电影测试项', season: undefined, type: '电影' })
|
||||||
const episodeGroupsRequested = vi.fn()
|
const episodeGroupsRequested = vi.fn()
|
||||||
|
|||||||
@@ -4158,6 +4158,13 @@ export default {
|
|||||||
bestVersionFull: 'Full',
|
bestVersionFull: 'Full',
|
||||||
},
|
},
|
||||||
subscribeEdit: {
|
subscribeEdit: {
|
||||||
|
searchInterval: 'Search schedule',
|
||||||
|
searchIntervalSystem: 'Use system setting',
|
||||||
|
searchIntervalCustom: 'Custom',
|
||||||
|
searchIntervalHours: 'Search interval (hours)',
|
||||||
|
searchIntervalHint:
|
||||||
|
'Requires scheduled subscription search to be enabled. Manual searches and RSS refreshes are unaffected.',
|
||||||
|
searchIntervalInvalid: 'Enter a whole number of hours from 1 to 8760',
|
||||||
titleDefault: 'Default Subscription Rules',
|
titleDefault: 'Default Subscription Rules',
|
||||||
titleEdit: 'Edit Subscription',
|
titleEdit: 'Edit Subscription',
|
||||||
seasonFormat: 'Season {number}',
|
seasonFormat: 'Season {number}',
|
||||||
|
|||||||
@@ -4067,6 +4067,12 @@ export default {
|
|||||||
bestVersionFull: '全集洗版',
|
bestVersionFull: '全集洗版',
|
||||||
},
|
},
|
||||||
subscribeEdit: {
|
subscribeEdit: {
|
||||||
|
searchInterval: '搜索周期',
|
||||||
|
searchIntervalSystem: '跟随系统',
|
||||||
|
searchIntervalCustom: '自定义',
|
||||||
|
searchIntervalHours: '搜索间隔(小时)',
|
||||||
|
searchIntervalHint: '仅在开启订阅定时搜索时生效;手动搜索和 RSS 刷新不受影响。',
|
||||||
|
searchIntervalInvalid: '请输入 1–8760 的整数小时数',
|
||||||
titleDefault: '默认订阅规则',
|
titleDefault: '默认订阅规则',
|
||||||
titleEdit: '编辑订阅',
|
titleEdit: '编辑订阅',
|
||||||
seasonFormat: '第 {number} 季',
|
seasonFormat: '第 {number} 季',
|
||||||
|
|||||||
@@ -4036,6 +4036,12 @@ export default {
|
|||||||
bestVersionFull: '全集洗版',
|
bestVersionFull: '全集洗版',
|
||||||
},
|
},
|
||||||
subscribeEdit: {
|
subscribeEdit: {
|
||||||
|
searchInterval: '搜尋週期',
|
||||||
|
searchIntervalSystem: '跟隨系統',
|
||||||
|
searchIntervalCustom: '自訂',
|
||||||
|
searchIntervalHours: '搜尋間隔(小時)',
|
||||||
|
searchIntervalHint: '僅在開啟訂閱定時搜尋時生效;手動搜尋與 RSS 更新不受影響。',
|
||||||
|
searchIntervalInvalid: '請輸入 1–8760 的整數小時數',
|
||||||
titleDefault: '默認訂閱規則',
|
titleDefault: '默認訂閱規則',
|
||||||
titleEdit: '編輯訂閱',
|
titleEdit: '編輯訂閱',
|
||||||
seasonFormat: '第 {number} 季',
|
seasonFormat: '第 {number} 季',
|
||||||
|
|||||||
Reference in New Issue
Block a user