feat: 支持多数据源识别缓存管理

This commit is contained in:
jxxghp
2026-07-13 12:33:45 +08:00
parent a20977e522
commit a8db14a069
6 changed files with 165 additions and 88 deletions

View File

@@ -1723,12 +1723,16 @@ export interface TorrentCacheData {
data: TorrentCacheItem[]
}
// TheMovieDb 识别缓存项
export interface TmdbRecognitionCacheItem {
// 媒体识别缓存项
export interface RecognitionCacheItem {
// 缓存键
key: string
// TMDB ID0 表示未识别
tmdb_id: number
tmdb_id?: number
// 豆瓣 ID0 表示未识别
douban_id?: string | number
// 当前识别数据源对应的统一 ID
recognition_id?: string
// 识别后的标题
title: string
// 识别后的年份
@@ -1741,8 +1745,8 @@ export interface TmdbRecognitionCacheItem {
backdrop_path?: string
}
// TheMovieDb 识别缓存数据
export interface TmdbRecognitionCacheData {
// 媒体识别缓存数据
export interface RecognitionCacheData {
// 缓存总数
count: number
// 已识别数量
@@ -1750,7 +1754,7 @@ export interface TmdbRecognitionCacheData {
// 未识别数量
unrecognized: number
// 缓存数据
data: TmdbRecognitionCacheItem[]
data: RecognitionCacheItem[]
}
// 订阅分享统计

View File

@@ -3,13 +3,14 @@ import { useToast } from 'vue-toastification'
import { useDisplay } from 'vuetify'
import { useI18n } from 'vue-i18n'
import api from '@/api'
import type { ApiResponse, TmdbRecognitionCacheData, TmdbRecognitionCacheItem } from '@/api/types'
import type { ApiResponse, RecognitionCacheData, RecognitionCacheItem } from '@/api/types'
import { useConfirm } from '@/composables/useConfirm'
import { useGlobalSettingsStore } from '@/stores'
import { getDisplayImageUrl } from '@/utils/imageUtils'
type RecognitionStatusFilter = 'all' | 'recognized' | 'unrecognized'
type InfiniteScrollStatus = 'ok' | 'empty' | 'loading' | 'error'
type RecognitionCacheSource = 'tmdb' | 'douban'
const MOBILE_CACHE_PAGE_SIZE = 20
const MOBILE_CACHE_ITEM_HEIGHT = 144
@@ -19,14 +20,28 @@ const display = useDisplay()
const createConfirm = useConfirm()
const $toast = useToast()
const globalSettingsStore = useGlobalSettingsStore()
const globalSettings = globalSettingsStore.globalSettings
const isMobile = computed(() => display.smAndDown.value)
const recognitionSource = computed<RecognitionCacheSource>(() =>
globalSettingsStore.globalSettings.RECOGNIZE_SOURCE === 'douban' ? 'douban' : 'tmdb',
)
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(() =>
t('setting.cache.filterRecognitionCache', { source: recognitionSourceName.value }),
)
const loading = ref(false)
const searchFilter = ref('')
const statusFilter = ref<RecognitionStatusFilter>('all')
const selectedItems = ref<string[]>([])
const cacheData = ref<TmdbRecognitionCacheData>({
const cacheData = ref<RecognitionCacheData>({
count: 0,
recognized: 0,
unrecognized: 0,
@@ -34,6 +49,7 @@ const cacheData = ref<TmdbRecognitionCacheData>({
})
const mobileVisibleCount = ref(MOBILE_CACHE_PAGE_SIZE)
const mobileInfiniteKey = ref(0)
let cacheLoadRequestId = 0
const statusOptions = computed(() => [
{ title: t('setting.cache.allStatuses'), value: 'all' },
@@ -46,7 +62,7 @@ const tableHeaders = computed(() => [
{ title: t('setting.cache.poster'), key: 'poster', sortable: false, width: '76px' },
{ title: t('setting.cache.cacheKey'), key: 'key', sortable: true },
{ title: t('setting.cache.recognitionResult'), key: 'result', sortable: false, width: '220px' },
{ title: t('setting.cache.tmdbId'), key: 'tmdb_id', sortable: true, width: '110px' },
{ title: recognitionIdLabel.value, key: 'recognition_id', sortable: true, width: '120px' },
{ title: t('setting.cache.recognitionStatus'), key: 'status', sortable: true, width: '120px' },
{ title: t('setting.cache.actions'), key: 'actions', sortable: false, width: '72px' },
])
@@ -56,10 +72,10 @@ const filteredData = computed(() => {
return cacheData.value.data.filter(item => {
const matchesKeyword =
!keyword ||
[item.key, item.title, item.year, String(item.tmdb_id)].some(value => value.toLowerCase().includes(keyword))
[item.key, item.title, item.year, getRecognitionId(item)].some(value => value.toLowerCase().includes(keyword))
const matchesStatus =
statusFilter.value === 'all' ||
(statusFilter.value === 'recognized' ? item.tmdb_id > 0 : item.tmdb_id === 0)
(statusFilter.value === 'recognized' ? isRecognized(item) : !isRecognized(item))
return matchesKeyword && matchesStatus
})
})
@@ -89,35 +105,42 @@ function loadMoreMobileCache({ done }: { done: (status: InfiniteScrollStatus) =>
done(mobileHasMore.value ? 'ok' : 'empty')
}
/** 加载 TheMovieDb 识别缓存列表。 */
/** 加载当前识别数据源对应的缓存列表。 */
async function loadCacheData(showSuccess = false) {
const requestId = ++cacheLoadRequestId
try {
loading.value = true
const response = (await api.get('tmdb/cache')) as unknown as ApiResponse<TmdbRecognitionCacheData>
cacheData.value = response.data ?? { count: 0, recognized: 0, unrecognized: 0, data: [] }
const response = (await api.get(recognitionCacheEndpoint.value)) as unknown as ApiResponse<RecognitionCacheData>
if (requestId !== cacheLoadRequestId) return
const responseData = response.data ?? { count: 0, recognized: 0, unrecognized: 0, data: [] }
cacheData.value = {
...responseData,
data: responseData.data.map(item => ({ ...item, recognition_id: getRecognitionId(item) })),
}
selectedItems.value = selectedItems.value.filter(key => cacheData.value.data.some(item => item.key === key))
resetMobilePagination()
if (showSuccess) $toast.success(t('setting.cache.listRefreshSuccess'))
} catch (error) {
if (requestId !== cacheLoadRequestId) return
console.error(error)
$toast.error(t('setting.cache.loadFailed'))
} finally {
loading.value = false
if (requestId === cacheLoadRequestId) loading.value = false
}
}
/** 清空全部 TheMovieDb 识别缓存。 */
/** 清空当前识别数据源的全部识别缓存。 */
async function clearAllCache() {
const confirmed = await createConfirm({
type: 'warn',
title: t('common.confirm'),
content: t('setting.cache.tmdbClearConfirm'),
content: t('setting.cache.recognitionClearConfirm', { source: recognitionSourceName.value }),
})
if (!confirmed) return
try {
loading.value = true
const response = (await api.delete('tmdb/cache')) as unknown as ApiResponse
const response = (await api.delete(recognitionCacheEndpoint.value)) as unknown as ApiResponse
if (!response.success) throw new Error(response.message)
$toast.success(response.message || t('setting.cache.clearSuccess'))
await loadCacheData()
@@ -130,13 +153,13 @@ async function clearAllCache() {
}
}
/** 请求后端删除指定 TheMovieDb 识别缓存。 */
/** 请求当前识别数据源接口删除指定识别缓存。 */
async function deleteCacheItem(key: string) {
const response = (await api.delete(`tmdb/cache/${encodeURIComponent(key)}`)) as unknown as ApiResponse
const response = (await api.delete(`${recognitionCacheEndpoint.value}/${encodeURIComponent(key)}`)) as unknown as ApiResponse
if (!response.success) throw new Error(response.message)
}
/** 删除桌面端表格中选中的 TheMovieDb 识别缓存。 */
/** 删除桌面端表格中选中的识别缓存。 */
async function deleteSelectedItems() {
if (selectedItems.value.length === 0) {
$toast.warning(t('setting.cache.selectDeleteWarning'))
@@ -158,8 +181,8 @@ async function deleteSelectedItems() {
}
}
/** 删除单条 TheMovieDb 识别缓存。 */
async function deleteSingleItem(item: TmdbRecognitionCacheItem) {
/** 删除单条识别缓存。 */
async function deleteSingleItem(item: RecognitionCacheItem) {
try {
loading.value = true
await deleteCacheItem(item.key)
@@ -173,13 +196,25 @@ async function deleteSingleItem(item: TmdbRecognitionCacheItem) {
}
}
/** 获取 TheMovieDb 缓存海报的可展示地址。 */
function getPosterUrl(item: TmdbRecognitionCacheItem): string {
/** 获取识别缓存海报的可展示地址。 */
function getPosterUrl(item: RecognitionCacheItem): string {
if (!item.poster_path) return ''
const sourceUrl = item.poster_path.startsWith('/')
? `https://${globalSettings.TMDB_IMAGE_DOMAIN}/t/p/w300${item.poster_path}`
? `https://${globalSettingsStore.globalSettings.TMDB_IMAGE_DOMAIN}/t/p/w300${item.poster_path}`
: item.poster_path
return getDisplayImageUrl(sourceUrl, globalSettings.GLOBAL_IMAGE_CACHE)
return getDisplayImageUrl(sourceUrl, globalSettingsStore.globalSettings.GLOBAL_IMAGE_CACHE)
}
/** 获取当前识别数据源对应的媒体 ID。 */
function getRecognitionId(item: RecognitionCacheItem): string {
const recognitionId = recognitionSource.value === 'douban' ? item.douban_id : item.tmdb_id
return recognitionId ? String(recognitionId) : ''
}
/** 判断识别缓存条目是否包含有效媒体 ID。 */
function isRecognized(item: RecognitionCacheItem): boolean {
const recognitionId = getRecognitionId(item)
return Boolean(recognitionId && recognitionId !== '0')
}
/** 获取本地化的媒体类型名称。 */
@@ -197,8 +232,8 @@ function getMediaTypeColor(mediaType: string): string {
}
/** 获取识别状态的本地化名称。 */
function getRecognitionStatusLabel(item: TmdbRecognitionCacheItem): string {
return item.tmdb_id > 0 ? t('setting.cache.recognized') : t('setting.cache.unrecognized')
function getRecognitionStatusLabel(item: RecognitionCacheItem): string {
return isRecognized(item) ? t('setting.cache.recognized') : t('setting.cache.unrecognized')
}
onMounted(() => {
@@ -208,10 +243,17 @@ onMounted(() => {
watch([searchFilter, statusFilter], () => {
resetMobilePagination()
})
watch(recognitionSource, () => {
searchFilter.value = ''
statusFilter.value = 'all'
selectedItems.value = []
void loadCacheData()
})
</script>
<template>
<section class="tmdb-cache-panel">
<section class="recognition-cache-panel">
<div class="cache-panel-toolbar">
<div class="cache-panel-stats">
<div class="cache-panel-stat cache-panel-stat--primary">
@@ -266,8 +308,8 @@ watch([searchFilter, statusFilter], () => {
<VTextField
v-model="searchFilter"
class="cache-panel-filter"
:label="isMobile ? undefined : t('setting.cache.filterRecognitionCache')"
:placeholder="isMobile ? t('setting.cache.filterRecognitionCache') : undefined"
:label="isMobile ? undefined : recognitionFilterPlaceholder"
:placeholder="isMobile ? recognitionFilterPlaceholder : undefined"
prepend-inner-icon="mdi-magnify"
variant="outlined"
:density="isMobile ? 'comfortable' : 'compact'"
@@ -305,7 +347,7 @@ watch([searchFilter, statusFilter], () => {
mode="intersect"
side="end"
:items="mobileVisibleData"
class="tmdb-cache-mobile-scroll"
class="recognition-cache-mobile-scroll"
@load="loadMoreMobileCache"
>
<template #loading>
@@ -324,24 +366,24 @@ watch([searchFilter, statusFilter], () => {
:item-height="MOBILE_CACHE_ITEM_HEIGHT"
>
<template #default="{ item, itemRef }">
<article :ref="itemRef" :key="item.key" class="tmdb-cache-mobile-item">
<div class="tmdb-cache-poster">
<article :ref="itemRef" :key="item.key" class="recognition-cache-mobile-item">
<div class="recognition-cache-poster">
<VImg v-if="getPosterUrl(item)" :src="getPosterUrl(item)" :alt="item.title || item.key" cover />
<VIcon v-else icon="mdi-image-off-outline" size="28" />
</div>
<div class="tmdb-cache-mobile-item__content">
<div class="tmdb-cache-mobile-item__title">
<div class="recognition-cache-mobile-item__content">
<div class="recognition-cache-mobile-item__title">
{{ item.title || t('setting.cache.unrecognized') }}
</div>
<div class="tmdb-cache-mobile-item__meta">
<div class="recognition-cache-mobile-item__meta">
<VChip size="x-small" variant="tonal" :color="getMediaTypeColor(item.media_type)">
{{ getMediaTypeLabel(item.media_type) }}
</VChip>
<span v-if="item.year">{{ item.year }}</span>
<span v-if="item.tmdb_id">TMDB #{{ item.tmdb_id }}</span>
<span v-if="isRecognized(item)">{{ recognitionIdLabel }} #{{ getRecognitionId(item) }}</span>
</div>
<div class="tmdb-cache-mobile-item__key">{{ item.key }}</div>
<div class="recognition-cache-mobile-item__key">{{ item.key }}</div>
</div>
<VBtn
@@ -361,7 +403,7 @@ watch([searchFilter, statusFilter], () => {
<div v-else class="cache-panel-empty">
<VIcon icon="mdi-database-search-outline" size="42" />
<strong>{{ t('setting.cache.noRecognitionCache') }}</strong>
<strong>{{ t('setting.cache.noRecognitionCache', { source: recognitionSourceName }) }}</strong>
<span>{{ t('setting.cache.noRecognitionCacheHint') }}</span>
</div>
</template>
@@ -369,7 +411,7 @@ watch([searchFilter, statusFilter], () => {
<VDataTable
v-else
v-model="selectedItems"
class="tmdb-cache-table"
class="recognition-cache-table"
:headers="tableHeaders"
:items="filteredData"
:loading="loading"
@@ -382,18 +424,18 @@ watch([searchFilter, statusFilter], () => {
:loading-text="t('common.loadingText')"
>
<template #item.poster="{ item }">
<div class="tmdb-cache-table__poster">
<div class="recognition-cache-table__poster">
<VImg v-if="getPosterUrl(item)" :src="getPosterUrl(item)" :alt="item.title || item.key" cover />
<VIcon v-else icon="mdi-image-off-outline" />
</div>
</template>
<template #item.key="{ item }">
<div class="tmdb-cache-table__key">{{ item.key }}</div>
<div class="recognition-cache-table__key">{{ item.key }}</div>
</template>
<template #item.result="{ item }">
<div class="tmdb-cache-result">
<div class="recognition-cache-result">
<strong>{{ item.title || t('setting.cache.unrecognized') }}</strong>
<span v-if="item.year">{{ item.year }}</span>
<VChip size="x-small" variant="tonal" :color="getMediaTypeColor(item.media_type)">
@@ -402,13 +444,13 @@ watch([searchFilter, statusFilter], () => {
</div>
</template>
<template #item.tmdb_id="{ item }">
<span v-if="item.tmdb_id" class="font-weight-medium">#{{ item.tmdb_id }}</span>
<template #item.recognition_id="{ item }">
<span v-if="isRecognized(item)" class="font-weight-medium">#{{ getRecognitionId(item) }}</span>
<span v-else class="text-medium-emphasis">-</span>
</template>
<template #item.status="{ item }">
<VChip size="small" variant="tonal" :color="item.tmdb_id > 0 ? 'success' : 'warning'">
<VChip size="small" variant="tonal" :color="isRecognized(item) ? 'success' : 'warning'">
{{ getRecognitionStatusLabel(item) }}
</VChip>
</template>
@@ -423,7 +465,7 @@ watch([searchFilter, statusFilter], () => {
<template #no-data>
<div class="cache-panel-empty">
<VIcon icon="mdi-database-search-outline" size="42" />
<strong>{{ t('setting.cache.noRecognitionCache') }}</strong>
<strong>{{ t('setting.cache.noRecognitionCache', { source: recognitionSourceName }) }}</strong>
<span>{{ t('setting.cache.noRecognitionCacheHint') }}</span>
</div>
</template>
@@ -432,7 +474,7 @@ watch([searchFilter, statusFilter], () => {
</template>
<style scoped>
.tmdb-cache-panel {
.recognition-cache-panel {
display: flex;
flex: 1 1 auto;
flex-direction: column;
@@ -523,7 +565,7 @@ watch([searchFilter, statusFilter], () => {
min-block-size: 44px;
}
.tmdb-cache-table {
.recognition-cache-table {
overflow: hidden;
border: var(--app-surface-border);
border-radius: var(--app-surface-radius);
@@ -531,8 +573,8 @@ watch([searchFilter, statusFilter], () => {
max-block-size: calc(100dvh - 23rem);
}
.tmdb-cache-table__poster,
.tmdb-cache-poster {
.recognition-cache-table__poster,
.recognition-cache-poster {
display: flex;
overflow: hidden;
align-items: center;
@@ -542,19 +584,19 @@ watch([searchFilter, statusFilter], () => {
color: rgba(var(--v-theme-on-surface), 0.36);
}
.tmdb-cache-table__poster {
.recognition-cache-table__poster {
block-size: 62px;
inline-size: 44px;
margin-block: 4px;
}
.tmdb-cache-table__poster :deep(.v-img),
.tmdb-cache-poster :deep(.v-img) {
.recognition-cache-table__poster :deep(.v-img),
.recognition-cache-poster :deep(.v-img) {
block-size: 100%;
inline-size: 100%;
}
.tmdb-cache-table__key {
.recognition-cache-table__key {
max-inline-size: 36rem;
color: rgba(var(--v-theme-on-surface), 0.68);
font-family: monospace;
@@ -562,19 +604,19 @@ watch([searchFilter, statusFilter], () => {
overflow-wrap: anywhere;
}
.tmdb-cache-result {
.recognition-cache-result {
display: flex;
align-items: center;
flex-wrap: wrap;
gap: 6px;
}
.tmdb-cache-result strong {
.recognition-cache-result strong {
inline-size: 100%;
color: rgba(var(--v-theme-on-surface), 0.88);
}
.tmdb-cache-result span {
.recognition-cache-result span {
color: rgba(var(--v-theme-on-surface), 0.56);
font-size: 12px;
}
@@ -602,7 +644,7 @@ watch([searchFilter, statusFilter], () => {
}
@media (max-width: 959.98px) {
.tmdb-cache-panel {
.recognition-cache-panel {
overflow-y: auto;
block-size: 100%;
padding: 14px 16px calc(18px + env(safe-area-inset-bottom));
@@ -656,18 +698,18 @@ watch([searchFilter, statusFilter], () => {
font-size: 16px;
}
.tmdb-cache-mobile-scroll {
.recognition-cache-mobile-scroll {
overflow: visible !important;
min-block-size: 20rem;
}
.tmdb-cache-mobile-scroll :deep(.v-infinite-scroll__container),
.tmdb-cache-mobile-scroll :deep(.v-virtual-scroll),
.tmdb-cache-mobile-scroll :deep(.v-virtual-scroll__container) {
.recognition-cache-mobile-scroll :deep(.v-infinite-scroll__container),
.recognition-cache-mobile-scroll :deep(.v-virtual-scroll),
.recognition-cache-mobile-scroll :deep(.v-virtual-scroll__container) {
overflow: visible !important;
}
.tmdb-cache-mobile-scroll :deep(.v-infinite-scroll__side) {
.recognition-cache-mobile-scroll :deep(.v-infinite-scroll__side) {
padding-block: 14px 2px;
}
@@ -683,7 +725,7 @@ watch([searchFilter, statusFilter], () => {
gap: 8px;
}
.tmdb-cache-mobile-item {
.recognition-cache-mobile-item {
display: grid;
align-items: start;
border: var(--app-surface-border);
@@ -697,23 +739,23 @@ watch([searchFilter, statusFilter], () => {
gap: 12px;
}
.tmdb-cache-poster {
.recognition-cache-poster {
block-size: 78px;
inline-size: 54px;
}
.tmdb-cache-mobile-item__content {
.recognition-cache-mobile-item__content {
min-inline-size: 0;
}
.tmdb-cache-mobile-item__title {
.recognition-cache-mobile-item__title {
color: rgba(var(--v-theme-on-surface), 0.9);
font-size: 15px;
font-weight: 700;
overflow-wrap: anywhere;
}
.tmdb-cache-mobile-item__meta {
.recognition-cache-mobile-item__meta {
display: flex;
align-items: center;
flex-wrap: wrap;
@@ -723,7 +765,7 @@ watch([searchFilter, statusFilter], () => {
gap: 7px;
}
.tmdb-cache-mobile-item__key {
.recognition-cache-mobile-item__key {
margin-block-start: 8px;
color: rgba(var(--v-theme-on-surface), 0.48);
font-family: monospace;
@@ -734,7 +776,7 @@ watch([searchFilter, statusFilter], () => {
}
@media (max-width: 374.98px) {
.tmdb-cache-panel {
.recognition-cache-panel {
padding-inline: 12px;
}

View File

@@ -2389,7 +2389,7 @@ export default {
subtitle: 'Manage system caches',
cacheType: 'Cache Type',
torrentCache: 'Resource Cache',
tmdbRecognitionCache: 'Recognition Cache',
recognitionCache: 'Recognition Cache',
totalCount: 'Total Count',
siteCount: 'Site Count',
recognized: 'Recognized',
@@ -2398,7 +2398,7 @@ export default {
allStatuses: 'All Statuses',
filterByTitle: 'Filter by Title',
filterBySite: 'Filter by Site',
filterRecognitionCache: 'Search cache key, title, or TMDB ID',
filterRecognitionCache: 'Search cache key, title, or {source} ID',
selectSite: 'Select Site',
loadingMore: 'Loading...',
refresh: 'Refresh Cache',
@@ -2422,6 +2422,7 @@ export default {
poster: 'Poster',
cacheKey: 'Recognition Request',
tmdbId: 'TMDB ID',
doubanId: 'Douban ID',
torrentTitle: 'Title',
site: 'Site',
size: 'Size',
@@ -2432,7 +2433,7 @@ export default {
unrecognized: 'Unrecognized',
noData: 'No cache data',
noDataHint: 'Click "Refresh Cache" button to get the latest torrent cache',
noRecognitionCache: 'No TheMovieDb recognition cache',
noRecognitionCache: 'No {source} recognition cache',
noRecognitionCacheHint: 'Recognition records will appear here after media is identified',
reidentifyDialog: {
title: 'Re-identify',
@@ -2451,7 +2452,12 @@ export default {
unknown: 'Unknown',
},
clearConfirm: 'Are you sure you want to clear all cache?',
recognitionClearConfirm: 'Clear all {source} recognition cache?',
tmdbClearConfirm: 'Clear all TheMovieDb recognition cache?',
recognitionSource: {
themoviedb: 'TheMovieDb',
douban: 'Douban',
},
},
},
dialog: {

View File

@@ -2344,7 +2344,7 @@ export default {
subtitle: '管理多类系统缓存',
cacheType: '缓存类型',
torrentCache: '资源缓存',
tmdbRecognitionCache: '识别缓存',
recognitionCache: '识别缓存',
totalCount: '总条数',
siteCount: '站点数',
recognized: '已识别',
@@ -2353,7 +2353,7 @@ export default {
allStatuses: '全部状态',
filterByTitle: '按标题筛选',
filterBySite: '按站点筛选',
filterRecognitionCache: '搜索缓存键、标题或 TMDB ID',
filterRecognitionCache: '搜索缓存键、标题或 {source} ID',
selectSite: '选择站点',
loadingMore: '加载中...',
refresh: '刷新缓存',
@@ -2377,6 +2377,7 @@ export default {
poster: '海报',
cacheKey: '识别请求',
tmdbId: 'TMDB ID',
doubanId: '豆瓣 ID',
torrentTitle: '标题',
site: '站点',
size: '大小',
@@ -2387,7 +2388,7 @@ export default {
unrecognized: '未识别',
noData: '暂无缓存数据',
noDataHint: '点击"刷新缓存"按钮获取最新的种子缓存',
noRecognitionCache: '暂无 TheMovieDb 识别缓存',
noRecognitionCache: '暂无 {source} 识别缓存',
noRecognitionCacheHint: '完成媒体识别后,缓存记录会显示在这里',
reidentifyDialog: {
title: '重新识别',
@@ -2406,7 +2407,12 @@ export default {
unknown: '未知',
},
clearConfirm: '确认清空所有缓存吗?',
recognitionClearConfirm: '确认清空全部 {source} 识别缓存吗?',
tmdbClearConfirm: '确认清空全部 TheMovieDb 识别缓存吗?',
recognitionSource: {
themoviedb: 'TheMovieDb',
douban: '豆瓣',
},
},
},
dialog: {

View File

@@ -2343,7 +2343,7 @@ export default {
subtitle: '管理多類系統緩存',
cacheType: '緩存類型',
torrentCache: '資源緩存',
tmdbRecognitionCache: '識別緩存',
recognitionCache: '識別緩存',
totalCount: '總條數',
siteCount: '站點數',
recognized: '已識別',
@@ -2352,7 +2352,7 @@ export default {
allStatuses: '全部狀態',
filterByTitle: '按標題篩選',
filterBySite: '按站點篩選',
filterRecognitionCache: '搜索緩存鍵、標題或 TMDB ID',
filterRecognitionCache: '搜索緩存鍵、標題或 {source} ID',
selectSite: '選擇站點',
loadingMore: '加載中...',
refresh: '刷新緩存',
@@ -2376,6 +2376,7 @@ export default {
poster: '海報',
cacheKey: '識別請求',
tmdbId: 'TMDB ID',
doubanId: '豆瓣 ID',
torrentTitle: '標題',
site: '站點',
size: '大小',
@@ -2386,7 +2387,7 @@ export default {
unrecognized: '未識別',
noData: '暫無緩存數據',
noDataHint: '點擊"刷新緩存"按鈕獲取最新的種子緩存',
noRecognitionCache: '暫無 TheMovieDb 識別緩存',
noRecognitionCache: '暫無 {source} 識別緩存',
noRecognitionCacheHint: '完成媒體識別後,緩存記錄會顯示在這裡',
reidentifyDialog: {
title: '重新識別',
@@ -2405,7 +2406,12 @@ export default {
unknown: '未知',
},
clearConfirm: '確認清空所有緩存嗎?',
recognitionClearConfirm: '確認清空全部 {source} 識別緩存嗎?',
tmdbClearConfirm: '確認清空全部 TheMovieDb 識別緩存嗎?',
recognitionSource: {
themoviedb: 'TheMovieDb',
douban: '豆瓣',
},
},
},
dialog: {

View File

@@ -9,12 +9,12 @@ import { useGlobalSettingsStore } from '@/stores'
import { usePWA } from '@/composables/usePWA'
import { openSharedDialog } from '@/composables/useSharedDialog'
import { useDisplay } from 'vuetify'
import TmdbRecognitionCachePanel from '@/components/cache/TmdbRecognitionCachePanel.vue'
import RecognitionCachePanel from '@/components/cache/RecognitionCachePanel.vue'
const CacheReidentifyDialog = defineAsyncComponent(() => import('@/components/dialog/CacheReidentifyDialog.vue'))
type InfiniteScrollStatus = 'ok' | 'empty' | 'loading' | 'error'
type CacheManagerType = 'torrent' | 'tmdb'
type CacheManagerType = 'torrent' | 'recognition'
const MOBILE_CACHE_PAGE_SIZE = 20
@@ -355,14 +355,14 @@ watch([titleFilter, siteFilter], () => {
<VBtn value="torrent" prepend-icon="mdi-download-box-outline">
{{ t('setting.cache.torrentCache') }}
</VBtn>
<VBtn value="tmdb" prepend-icon="mdi-movie-search-outline">
{{ t('setting.cache.tmdbRecognitionCache') }}
<VBtn value="recognition" prepend-icon="mdi-movie-search-outline">
{{ t('setting.cache.recognitionCache') }}
</VBtn>
</VBtnToggle>
</header>
<div class="cache-manager__content">
<TmdbRecognitionCachePanel v-if="activeCacheType === 'tmdb'" />
<RecognitionCachePanel v-if="activeCacheType === 'recognition'" />
<template v-else>
<section v-if="isMobile" class="cache-mobile-page">
@@ -869,6 +869,7 @@ watch([titleFilter, siteFilter], () => {
}
.cache-manager__switcher :deep(.v-btn) {
align-items: center;
flex: 1 1 0;
block-size: auto;
min-block-size: 48px;
@@ -876,6 +877,18 @@ watch([titleFilter, siteFilter], () => {
padding-block: 6px;
padding-inline: 10px;
}
.cache-manager__switcher :deep(.v-btn__prepend),
.cache-manager__switcher :deep(.v-btn__content) {
align-self: center;
}
.cache-manager__switcher :deep(.v-btn__content) {
display: flex;
align-items: center;
min-block-size: 36px;
line-height: 1.25;
}
}
.cache-mobile-page {