-
- {{ t('setting.cache.refresh') }}
-
-
- {{ t('setting.cache.clearAll') }}
-
+
+
+
+ {{ recognitionSourceName }}
@@ -515,6 +600,21 @@ watch([searchFilter, statusFilter], () => {
+
+
+
+
+ {{ musicSourceName }}
+
+
+
+
@@ -531,23 +631,14 @@ watch([searchFilter, statusFilter], () => {
overflow-y: auto;
}
-.recognition-cache-categories {
+.recognition-cache-section-title {
display: flex;
- flex: 0 0 auto;
- justify-content: center;
-}
-
-.recognition-cache-categories__switcher {
- overflow: hidden;
- border: var(--app-surface-border);
- border-radius: var(--app-control-radius);
- backdrop-filter: var(--app-grouped-list-backdrop-filter);
- background: var(--app-grouped-list-background);
- box-shadow: var(--app-surface-shadow);
-}
-
-.recognition-cache-categories__switcher :deep(.v-btn) {
- min-inline-size: 150px;
+ align-items: center;
+ color: rgba(var(--v-theme-on-surface), 0.72);
+ font-size: 14px;
+ font-weight: 700;
+ gap: 8px;
+ margin-block-start: 4px;
}
.cache-panel-toolbar {
@@ -619,7 +710,7 @@ watch([searchFilter, statusFilter], () => {
.cache-panel-filters {
display: grid;
gap: 12px;
- grid-template-columns: minmax(0, 1fr) minmax(180px, 0.35fr);
+ grid-template-columns: minmax(0, 1fr) minmax(150px, 0.28fr) minmax(150px, 0.28fr);
}
.cache-panel-filters :deep(.v-field) {
@@ -720,19 +811,6 @@ watch([searchFilter, statusFilter], () => {
padding-inline: 16px;
}
- .recognition-cache-categories {
- inline-size: 100%;
- }
-
- .recognition-cache-categories__switcher {
- inline-size: 100%;
- }
-
- .recognition-cache-categories__switcher :deep(.v-btn) {
- flex: 1 1 0;
- min-inline-size: 0;
- }
-
.cache-panel-toolbar {
align-items: flex-start;
}
diff --git a/src/components/cache/__tests__/MusicRecognitionCachePanel.spec.ts b/src/components/cache/__tests__/MusicRecognitionCachePanel.spec.ts
index c911acaf..96339620 100644
--- a/src/components/cache/__tests__/MusicRecognitionCachePanel.spec.ts
+++ b/src/components/cache/__tests__/MusicRecognitionCachePanel.spec.ts
@@ -1,66 +1,49 @@
import MusicRecognitionCachePanel from '@/components/cache/MusicRecognitionCachePanel.vue'
-import { screen, waitFor } from '@testing-library/vue'
+import type { MusicRecognitionCacheItem } from '@/api/types'
+import { screen } from '@testing-library/vue'
import userEvent from '@testing-library/user-event'
import { renderWithProviders } from '@tests/support/render'
-import { beforeEach, describe, expect, it, vi } from 'vitest'
-
-const mocks = vi.hoisted(() => ({
- apiGet: vi.fn(),
- apiDelete: vi.fn(),
- toastError: vi.fn(),
- toastSuccess: vi.fn(),
-}))
-
-vi.mock('@/api', () => ({
- default: {
- get: mocks.apiGet,
- delete: mocks.apiDelete,
- },
-}))
-
-vi.mock('vue-toastification', () => ({
- useToast: () => ({
- error: mocks.toastError,
- success: mocks.toastSuccess,
- }),
-}))
+import { describe, expect, it } from 'vitest'
const cacheKey = '[音乐]晴天-周杰伦-叶惠美-2003'
-function mockMusicCacheData() {
- mocks.apiGet.mockResolvedValue({
- data: {
- count: 2,
- recognized: 1,
- unrecognized: 1,
- data: [
- {
- key: cacheKey,
- media_id: 'rec-1',
- title: '晴天',
- artists: ['周杰伦'],
- album: '叶惠美',
- year: 2003,
- music_type: 'recording',
- cover_url: '',
- },
- {
- key: '[音乐]未知曲目--None-None',
- media_id: '',
- title: '未知曲目',
- artists: [],
- album: '',
- year: '',
- music_type: 'recording',
- cover_url: '',
- },
- ],
- },
- })
+const musicItems: MusicRecognitionCacheItem[] = [
+ {
+ key: cacheKey,
+ media_id: 'rec-1',
+ title: '晴天',
+ artists: ['周杰伦'],
+ album: '叶惠美',
+ year: 2003,
+ music_type: 'recording',
+ cover_url: '',
+ },
+ {
+ key: '[音乐]未知曲目--None-None',
+ media_id: '',
+ title: '未知曲目',
+ artists: [],
+ album: '',
+ year: '',
+ music_type: 'recording',
+ cover_url: '',
+ },
+]
+
+interface MusicPanelProps {
+ items?: MusicRecognitionCacheItem[]
+ loading?: boolean
+ selectedItems?: string[]
}
-async function renderMusicRecognitionCachePanel() {
+function renderMusicPanel(props: MusicPanelProps = {}) {
return renderWithProviders(MusicRecognitionCachePanel, {
+ props: {
+ items: musicItems,
+ loading: false,
+ selectedItems: [],
+ ...props,
+ },
initialState: {
globalSettings: {
data: {
@@ -73,54 +56,33 @@ async function renderMusicRecognitionCachePanel() {
})
}
-describe('MusicRecognitionCachePanel', () => {
- beforeEach(() => {
- mocks.apiGet.mockReset()
- mocks.apiDelete.mockReset()
- mocks.toastError.mockReset()
- mocks.toastSuccess.mockReset()
- })
+describe('MusicRecognitionCachePanel table section', () => {
+ it('renders music cache items provided through props', async () => {
+ await renderMusicPanel()
- it('loads music recognition cache from the music endpoint', async () => {
- mockMusicCacheData()
-
- await renderMusicRecognitionCachePanel()
-
- await waitFor(() => expect(mocks.apiGet).toHaveBeenCalledWith('music/cache'))
expect(await screen.findByText('晴天')).toBeInTheDocument()
expect(screen.getByText('周杰伦')).toBeInTheDocument()
expect(screen.getByText('未知曲目')).toBeInTheDocument()
expect(screen.getByText('MusicBrainz ID')).toBeInTheDocument()
+ expect(screen.getByText('已识别')).toBeInTheDocument()
+ expect(screen.getByText('未识别')).toBeInTheDocument()
})
- it('shows empty hint when music cache is empty', async () => {
- mocks.apiGet.mockResolvedValue({
- data: {
- count: 0,
- recognized: 0,
- unrecognized: 0,
- data: [],
- },
- })
+ it('shows empty hint when no items are provided', async () => {
+ await renderMusicPanel({ items: [] })
- await renderMusicRecognitionCachePanel()
-
- await waitFor(() => expect(mocks.apiGet).toHaveBeenCalledWith('music/cache'))
expect(await screen.findByText('暂无 MusicBrainz 识别缓存')).toBeInTheDocument()
})
- it('deletes a music cache item through the encoded endpoint', async () => {
- mockMusicCacheData()
- mocks.apiDelete.mockResolvedValue({ success: true, message: '音乐识别缓存删除成功' })
-
- await renderMusicRecognitionCachePanel()
+ it('emits delete with the cache key when a row delete button is clicked', async () => {
+ const { emitted } = await renderMusicPanel()
await screen.findByText('晴天')
const user = userEvent.setup()
const deleteButtons = screen.getAllByRole('button', { name: '删除' })
await user.click(deleteButtons[0])
- await waitFor(() => expect(mocks.apiDelete).toHaveBeenCalledWith(`music/cache/${encodeURIComponent(cacheKey)}`))
- expect(mocks.toastSuccess).toHaveBeenCalled()
+ expect(emitted('delete')).toBeTruthy()
+ expect(emitted('delete')?.[0]).toEqual([cacheKey])
})
})
diff --git a/src/components/cache/__tests__/RecognitionCachePanel.spec.ts b/src/components/cache/__tests__/RecognitionCachePanel.spec.ts
index 7b7a5607..2a3731eb 100644
--- a/src/components/cache/__tests__/RecognitionCachePanel.spec.ts
+++ b/src/components/cache/__tests__/RecognitionCachePanel.spec.ts
@@ -22,6 +22,22 @@ vi.mock('vue-toastification', () => ({
}),
}))
+const emptyMusicResponse = {
+ data: {
+ count: 0,
+ recognized: 0,
+ unrecognized: 0,
+ data: [],
+ },
+}
+
+/** 按请求端点分别返回 TMDB 与音乐识别缓存数据。 */
+function mockCacheApis(tmdbData: Record
) {
+ mocks.apiGet.mockImplementation((url: string) =>
+ Promise.resolve(url === 'music/cache' ? emptyMusicResponse : { data: tmdbData }),
+ )
+}
+
async function renderRecognitionCachePanel(recognitionSource = 'themoviedb') {
return renderWithProviders(RecognitionCachePanel, {
initialState: {
@@ -44,15 +60,13 @@ describe('RecognitionCachePanel shared recognition statistics', () => {
})
it('shows the persisted shared recognition count when sharing is enabled', async () => {
- mocks.apiGet.mockResolvedValue({
- data: {
- count: 12,
- recognized: 9,
- unrecognized: 3,
- shared_recognized: 27,
- shared_recognize_enabled: true,
- data: [],
- },
+ mockCacheApis({
+ count: 12,
+ recognized: 9,
+ unrecognized: 3,
+ shared_recognized: 27,
+ shared_recognize_enabled: true,
+ data: [],
})
await renderRecognitionCachePanel()
@@ -63,15 +77,13 @@ describe('RecognitionCachePanel shared recognition statistics', () => {
})
it('hides shared recognition statistics when sharing is disabled', async () => {
- mocks.apiGet.mockResolvedValue({
- data: {
- count: 12,
- recognized: 9,
- unrecognized: 3,
- shared_recognized: 27,
- shared_recognize_enabled: false,
- data: [],
- },
+ mockCacheApis({
+ count: 12,
+ recognized: 9,
+ unrecognized: 3,
+ shared_recognized: 27,
+ shared_recognize_enabled: false,
+ data: [],
})
await renderRecognitionCachePanel()
@@ -81,15 +93,13 @@ describe('RecognitionCachePanel shared recognition statistics', () => {
})
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: [],
- },
+ mockCacheApis({
+ count: 0,
+ recognized: 0,
+ unrecognized: 0,
+ shared_recognized: 0,
+ shared_recognize_enabled: false,
+ data: [],
})
await renderRecognitionCachePanel('douban')
@@ -97,3 +107,61 @@ describe('RecognitionCachePanel shared recognition statistics', () => {
await waitFor(() => expect(mocks.apiGet).toHaveBeenCalledWith('tmdb/cache'))
})
})
+
+describe('RecognitionCachePanel unified movie/TV and music management', () => {
+ beforeEach(() => {
+ mocks.apiGet.mockReset()
+ mocks.toastError.mockReset()
+ mocks.toastSuccess.mockReset()
+ })
+
+ it('loads both cache sources and aggregates the statistics', async () => {
+ mocks.apiGet.mockImplementation((url: string) =>
+ Promise.resolve(
+ url === 'music/cache'
+ ? {
+ data: {
+ count: 3,
+ recognized: 2,
+ unrecognized: 1,
+ data: [
+ {
+ key: '[音乐]晴天-周杰伦-叶惠美-2003',
+ media_id: 'rec-1',
+ title: '晴天',
+ artists: ['周杰伦'],
+ album: '叶惠美',
+ year: 2003,
+ music_type: 'recording',
+ cover_url: '',
+ },
+ ],
+ },
+ }
+ : {
+ data: {
+ count: 12,
+ recognized: 9,
+ unrecognized: 3,
+ shared_recognized: 0,
+ shared_recognize_enabled: false,
+ data: [],
+ },
+ },
+ ),
+ )
+
+ await renderRecognitionCachePanel()
+
+ await waitFor(() => expect(mocks.apiGet).toHaveBeenCalledWith('tmdb/cache'))
+ await waitFor(() => expect(mocks.apiGet).toHaveBeenCalledWith('music/cache'))
+ // 总条数 12 + 3,已识别 9 + 2,未识别 3 + 1
+ expect(await screen.findByText('15')).toBeInTheDocument()
+ expect(screen.getByText('11')).toBeInTheDocument()
+ expect(screen.getByText('4')).toBeInTheDocument()
+ // 音乐条目直接展示在统一面板中
+ expect(screen.getByText('晴天')).toBeInTheDocument()
+ // 类型筛选下拉框默认展示“全部”
+ expect(screen.getByText('全部')).toBeInTheDocument()
+ })
+})
diff --git a/src/locales/en-US.ts b/src/locales/en-US.ts
index 1b43e3d3..cfc96258 100644
--- a/src/locales/en-US.ts
+++ b/src/locales/en-US.ts
@@ -2703,8 +2703,9 @@ export default {
cacheType: 'Cache Type',
torrentCache: 'Resource Cache',
recognitionCache: 'Recognition Cache',
- recognitionCategoryLabel: 'Recognition Cache Category',
- recognitionCategory: {
+ recognitionType: 'Type',
+ recognitionTypeOptions: {
+ all: 'All',
media: 'Movie/TV',
music: 'Music',
},
@@ -2718,6 +2719,7 @@ export default {
filterByTitle: 'Filter by Title',
filterBySite: 'Filter by Site',
filterRecognitionCache: 'Search cache key, title, or {source} ID',
+ filterAllRecognitionCache: 'Search cache key, title, artist, or recognition ID',
selectSite: 'Select Site',
loadingMore: 'Loading...',
refresh: 'Refresh Cache',
@@ -2785,6 +2787,7 @@ export default {
},
clearConfirm: 'Are you sure you want to clear all cache?',
recognitionClearConfirm: 'Clear all {source} recognition cache?',
+ recognitionClearAllConfirm: 'Clear all recognition cache (movie/TV and music)?',
tmdbClearConfirm: 'Clear all TheMovieDb recognition cache?',
recognitionSource: {
themoviedb: 'TheMovieDb',
diff --git a/src/locales/zh-CN.ts b/src/locales/zh-CN.ts
index 1d5bf55f..22f2e587 100644
--- a/src/locales/zh-CN.ts
+++ b/src/locales/zh-CN.ts
@@ -2652,8 +2652,9 @@ export default {
cacheType: '缓存类型',
torrentCache: '资源缓存',
recognitionCache: '识别缓存',
- recognitionCategoryLabel: '识别缓存分类',
- recognitionCategory: {
+ recognitionType: '类型',
+ recognitionTypeOptions: {
+ all: '全部',
media: '电影/电视剧',
music: '音乐',
},
@@ -2667,6 +2668,7 @@ export default {
filterByTitle: '按标题筛选',
filterBySite: '按站点筛选',
filterRecognitionCache: '搜索缓存键、标题或 {source} ID',
+ filterAllRecognitionCache: '搜索缓存键、标题、艺术家或识别 ID',
selectSite: '选择站点',
loadingMore: '加载中...',
refresh: '刷新缓存',
@@ -2734,6 +2736,7 @@ export default {
},
clearConfirm: '确认清空所有缓存吗?',
recognitionClearConfirm: '确认清空全部 {source} 识别缓存吗?',
+ recognitionClearAllConfirm: '确认清空全部识别缓存(影视与音乐)吗?',
tmdbClearConfirm: '确认清空全部 TheMovieDb 识别缓存吗?',
recognitionSource: {
themoviedb: 'TheMovieDb',
diff --git a/src/locales/zh-TW.ts b/src/locales/zh-TW.ts
index 60de6437..c9aaf4f4 100644
--- a/src/locales/zh-TW.ts
+++ b/src/locales/zh-TW.ts
@@ -2651,8 +2651,9 @@ export default {
cacheType: '緩存類型',
torrentCache: '資源緩存',
recognitionCache: '識別緩存',
- recognitionCategoryLabel: '識別緩存分類',
- recognitionCategory: {
+ recognitionType: '類型',
+ recognitionTypeOptions: {
+ all: '全部',
media: '電影/電視劇',
music: '音樂',
},
@@ -2666,6 +2667,7 @@ export default {
filterByTitle: '按標題篩選',
filterBySite: '按站點篩選',
filterRecognitionCache: '搜索緩存鍵、標題或 {source} ID',
+ filterAllRecognitionCache: '搜索緩存鍵、標題、藝術家或識別 ID',
selectSite: '選擇站點',
loadingMore: '加載中...',
refresh: '刷新緩存',
@@ -2733,6 +2735,7 @@ export default {
},
clearConfirm: '確認清空所有緩存嗎?',
recognitionClearConfirm: '確認清空全部 {source} 識別緩存嗎?',
+ recognitionClearAllConfirm: '確認清空全部識別緩存(影視與音樂)嗎?',
tmdbClearConfirm: '確認清空全部 TheMovieDb 識別緩存嗎?',
recognitionSource: {
themoviedb: 'TheMovieDb',