refactor(cache): manage TMDB recognition cache only

This commit is contained in:
jxxghp
2026-08-06 07:48:43 +08:00
parent 0045ecf5d9
commit 6fd259427e
7 changed files with 38 additions and 41 deletions
+1 -1
View File
@@ -1,6 +1,6 @@
{ {
"name": "moviepilot", "name": "moviepilot",
"version": "2.15.4", "version": "2.15.5",
"private": true, "private": true,
"type": "module", "type": "module",
"bin": "dist/service.js", "bin": "dist/service.js",
+1 -3
View File
@@ -1889,9 +1889,7 @@ export interface RecognitionCacheItem {
key: string key: string
// TMDB ID0 表示未识别 // TMDB ID0 表示未识别
tmdb_id?: number tmdb_id?: number
// 豆瓣 ID0 表示未识别 // 识别缓存对应的字符串 ID
douban_id?: string | number
// 当前识别数据源对应的统一 ID
recognition_id?: string recognition_id?: string
// 识别后的标题 // 识别后的标题
title: string title: string
+11 -29
View File
@@ -11,9 +11,9 @@ import { getDisplayImageUrl } from '@/utils/imageUtils'
type RecognitionStatusFilter = 'all' | 'recognized' | 'unrecognized' type RecognitionStatusFilter = 'all' | 'recognized' | 'unrecognized'
type InfiniteScrollStatus = 'ok' | 'empty' | 'loading' | 'error' type InfiniteScrollStatus = 'ok' | 'empty' | 'loading' | 'error'
type RecognitionCacheSource = 'tmdb' | 'douban'
const MOBILE_CACHE_PAGE_SIZE = 20 const MOBILE_CACHE_PAGE_SIZE = 20
const RECOGNITION_CACHE_ENDPOINT = 'tmdb/cache'
const { t } = useI18n() const { t } = useI18n()
const display = useDisplay() const display = useDisplay()
@@ -22,18 +22,8 @@ const $toast = useToast()
const globalSettingsStore = useGlobalSettingsStore() const globalSettingsStore = useGlobalSettingsStore()
const isMobile = computed(() => display.smAndDown.value) const isMobile = computed(() => display.smAndDown.value)
const recognitionSource = computed<RecognitionCacheSource>(() => const recognitionSourceName = computed(() => t('setting.cache.recognitionSource.themoviedb'))
globalSettingsStore.globalSettings.RECOGNIZE_SOURCE === 'douban' ? 'douban' : 'tmdb', const recognitionIdLabel = computed(() => t('setting.cache.tmdbId'))
)
const recognitionSourceName = computed(() =>
recognitionSource.value === 'douban'
? t('setting.cache.recognitionSource.douban')
: t('setting.cache.recognitionSource.themoviedb'),
)
const recognitionIdLabel = computed(() =>
recognitionSource.value === 'douban' ? t('setting.cache.doubanId') : t('setting.cache.tmdbId'),
)
const recognitionCacheEndpoint = computed(() => `${recognitionSource.value}/cache`)
const recognitionFilterPlaceholder = computed(() => const recognitionFilterPlaceholder = computed(() =>
t('setting.cache.filterRecognitionCache', { source: recognitionSourceName.value }), t('setting.cache.filterRecognitionCache', { source: recognitionSourceName.value }),
) )
@@ -106,12 +96,12 @@ function loadMoreMobileCache({ done }: { done: (status: InfiniteScrollStatus) =>
done(mobileHasMore.value ? 'ok' : 'empty') done(mobileHasMore.value ? 'ok' : 'empty')
} }
/** 加载当前识别数据源对应的缓存列表。 */ /** 加载 TMDB 主识别缓存列表。 */
async function loadCacheData(showSuccess = false) { async function loadCacheData(showSuccess = false) {
const requestId = ++cacheLoadRequestId const requestId = ++cacheLoadRequestId
try { try {
loading.value = true loading.value = true
const response = (await api.get(recognitionCacheEndpoint.value)) as unknown as ApiResponse<RecognitionCacheData> const response = (await api.get(RECOGNITION_CACHE_ENDPOINT)) as unknown as ApiResponse<RecognitionCacheData>
if (requestId !== cacheLoadRequestId) return if (requestId !== cacheLoadRequestId) return
const responseData = response.data ?? { const responseData = response.data ?? {
count: 0, count: 0,
@@ -139,7 +129,7 @@ async function loadCacheData(showSuccess = false) {
} }
} }
/** 清空当前识别数据源的全部识别缓存。 */ /** 清空全部 TMDB 主识别缓存。 */
async function clearAllCache() { async function clearAllCache() {
const confirmed = await createConfirm({ const confirmed = await createConfirm({
type: 'warn', type: 'warn',
@@ -150,7 +140,7 @@ async function clearAllCache() {
try { try {
loading.value = true loading.value = true
const response = (await api.delete(recognitionCacheEndpoint.value)) as unknown as ApiResponse const response = (await api.delete(RECOGNITION_CACHE_ENDPOINT)) as unknown as ApiResponse
if (!response.success) throw new Error(response.message) if (!response.success) throw new Error(response.message)
$toast.success(response.message || t('setting.cache.clearSuccess')) $toast.success(response.message || t('setting.cache.clearSuccess'))
await loadCacheData() await loadCacheData()
@@ -163,10 +153,10 @@ async function clearAllCache() {
} }
} }
/** 请求当前识别数据源接口删除指定识别缓存。 */ /** 请求 TMDB 接口删除指定识别缓存。 */
async function deleteCacheItem(key: string) { async function deleteCacheItem(key: string) {
const response = (await api.delete( const response = (await api.delete(
`${recognitionCacheEndpoint.value}/${encodeURIComponent(key)}`, `${RECOGNITION_CACHE_ENDPOINT}/${encodeURIComponent(key)}`,
)) as unknown as ApiResponse )) as unknown as ApiResponse
if (!response.success) throw new Error(response.message) if (!response.success) throw new Error(response.message)
} }
@@ -217,10 +207,9 @@ function getPosterUrl(item: RecognitionCacheItem): string {
return getDisplayImageUrl(sourceUrl, globalSettingsStore.globalSettings.GLOBAL_IMAGE_CACHE) return getDisplayImageUrl(sourceUrl, globalSettingsStore.globalSettings.GLOBAL_IMAGE_CACHE)
} }
/** 获取当前识别数据源对应的媒体 ID。 */ /** 获取识别缓存对应的 TMDB ID。 */
function getRecognitionId(item: RecognitionCacheItem): string { function getRecognitionId(item: RecognitionCacheItem): string {
const recognitionId = recognitionSource.value === 'douban' ? item.douban_id : item.tmdb_id return item.tmdb_id ? String(item.tmdb_id) : ''
return recognitionId ? String(recognitionId) : ''
} }
/** 判断识别缓存条目是否包含有效媒体 ID。 */ /** 判断识别缓存条目是否包含有效媒体 ID。 */
@@ -260,13 +249,6 @@ onMounted(() => {
watch([searchFilter, statusFilter], () => { watch([searchFilter, statusFilter], () => {
resetMobilePagination() resetMobilePagination()
}) })
watch(recognitionSource, () => {
searchFilter.value = ''
statusFilter.value = 'all'
selectedItems.value = []
void loadCacheData()
})
</script> </script>
<template> <template>
@@ -22,13 +22,13 @@ vi.mock('vue-toastification', () => ({
}), }),
})) }))
async function renderRecognitionCachePanel() { async function renderRecognitionCachePanel(recognitionSource = 'themoviedb') {
return renderWithProviders(RecognitionCachePanel, { return renderWithProviders(RecognitionCachePanel, {
initialState: { initialState: {
globalSettings: { globalSettings: {
data: { data: {
GLOBAL_IMAGE_CACHE: false, GLOBAL_IMAGE_CACHE: false,
RECOGNIZE_SOURCE: 'themoviedb', RECOGNIZE_SOURCE: recognitionSource,
TMDB_IMAGE_DOMAIN: 'image.tmdb.org', TMDB_IMAGE_DOMAIN: 'image.tmdb.org',
}, },
}, },
@@ -79,4 +79,21 @@ describe('RecognitionCachePanel shared recognition statistics', () => {
expect(screen.queryByText('共享识别')).not.toBeInTheDocument() expect(screen.queryByText('共享识别')).not.toBeInTheDocument()
}) })
it('loads TMDB cache even when Douban is selected as the recognition source', async () => {
mocks.apiGet.mockResolvedValue({
data: {
count: 0,
recognized: 0,
unrecognized: 0,
shared_recognized: 0,
shared_recognize_enabled: false,
data: [],
},
})
await renderRecognitionCachePanel('douban')
await waitFor(() => expect(mocks.apiGet).toHaveBeenCalledWith('tmdb/cache'))
})
}) })
+2 -2
View File
@@ -2033,8 +2033,8 @@ export default {
tmdbLocale: 'TMDB Metadata Language', tmdbLocale: 'TMDB Metadata Language',
tmdbLocalePlaceholder: 'en', tmdbLocalePlaceholder: 'en',
tmdbLocaleHint: 'Customize TheMovieDb metadata language', tmdbLocaleHint: 'Customize TheMovieDb metadata language',
metaCacheExpire: 'Media Metadata Cache Expiration Time', metaCacheExpire: 'Per-item Media Metadata Cache TTL',
metaCacheExpireHint: 'Recognition metadata local cache time, use built-in default value when set to 0', metaCacheExpireHint: 'Independent TTL for each recognition metadata item; use the built-in default when set to 0',
metaCacheExpireRequired: 'Please enter metadata cache time', metaCacheExpireRequired: 'Please enter metadata cache time',
metaCacheExpireMin: 'Metadata cache time must be greater than or equal to 0', metaCacheExpireMin: 'Metadata cache time must be greater than or equal to 0',
scrapFollowTmdb: 'Follow TMDB Recognition', scrapFollowTmdb: 'Follow TMDB Recognition',
+2 -2
View File
@@ -2008,8 +2008,8 @@ export default {
tmdbLocale: 'TMDB 元数据语言', tmdbLocale: 'TMDB 元数据语言',
tmdbLocalePlaceholder: 'zh', tmdbLocalePlaceholder: 'zh',
tmdbLocaleHint: '自定义 TheMovieDb 元数据语言', tmdbLocaleHint: '自定义 TheMovieDb 元数据语言',
metaCacheExpire: '媒体元数据缓存过期时间', metaCacheExpire: '单条媒体元数据缓存有效期',
metaCacheExpireHint: '识别元数据本地缓存时间,为 0 时使用内置默认值', metaCacheExpireHint: '每条识别元数据的独立有效期,为 0 时使用内置默认值',
metaCacheExpireRequired: '请输入元数据缓存时间', metaCacheExpireRequired: '请输入元数据缓存时间',
metaCacheExpireMin: '元数据缓存时间必须大于等于0', metaCacheExpireMin: '元数据缓存时间必须大于等于0',
scrapFollowTmdb: '跟随TMDB识别整理', scrapFollowTmdb: '跟随TMDB识别整理',
+2 -2
View File
@@ -2007,8 +2007,8 @@ export default {
tmdbLocale: 'TMDB 元數據語言', tmdbLocale: 'TMDB 元數據語言',
tmdbLocalePlaceholder: 'zh', tmdbLocalePlaceholder: 'zh',
tmdbLocaleHint: '自定義 TheMovieDb 元數據語言', tmdbLocaleHint: '自定義 TheMovieDb 元數據語言',
metaCacheExpire: '媒體元數據緩存過期時間', metaCacheExpire: '單筆媒體元數據緩存有效期',
metaCacheExpireHint: '識別元數據本地緩存時間,為 0 時使用內置默認值', metaCacheExpireHint: '每筆識別元數據的獨立有效期,為 0 時使用內置默認值',
metaCacheExpireRequired: '請輸入元數據緩存時間', metaCacheExpireRequired: '請輸入元數據緩存時間',
metaCacheExpireMin: '元數據緩存時間必須大於等於0', metaCacheExpireMin: '元數據緩存時間必須大於等於0',
scrapFollowTmdb: '跟隨TMDB識別整理', scrapFollowTmdb: '跟隨TMDB識別整理',