fix(music): preserve subscription entity semantics

This commit is contained in:
jxxghp
2026-08-09 23:30:31 +08:00
parent 2a01a2ffe0
commit ce0b0d6330
5 changed files with 81 additions and 6 deletions
+8 -2
View File
@@ -26,7 +26,7 @@ import {
getCachedMediaSubscribeStatus,
setCachedMediaExistsStatus,
} from '@/utils/mediaStatusCache'
import { buildMusicDetailRoute } from '@/utils/music'
import { buildMusicDetailRoute, getMusicKey } from '@/utils/music'
const SearchSiteDialog = defineAsyncComponent(() => import('@/components/dialog/SearchSiteDialog.vue'))
@@ -151,7 +151,8 @@ function getMediaId() {
}
function getSubscribeStatusKey(season: number | null = props.media?.season ?? null) {
return `${getMediaId()}::${season ?? 'all'}`
const identity = props.media?.type === '音乐' ? getMusicKey(props.media) : getMediaId()
return `${identity}::${season ?? 'all'}`
}
function getExistsStatusKey() {
@@ -167,6 +168,11 @@ function getExistsStatusKey() {
}
function isSameSubscribeMedia(subscribe: Subscribe) {
if (props.media?.type === '音乐') {
const expectedMusicType = props.media.music_type ?? 'recording'
const subscribeMusicType = subscribe.music_type ?? 'recording'
if (subscribeMusicType !== expectedMusicType) return false
}
const mediaId = getMediaId()
if (subscribe.media_source && subscribe.media_id) {
const prefix = subscribe.media_source === 'themoviedb' ? 'tmdb' : subscribe.media_source
+3 -2
View File
@@ -3,7 +3,7 @@ import type { MediaInfo } from '@/api/types'
import { useGlobalSettingsStore, useUserStore } from '@/stores'
import { buildUserPermissionContext, hasPermission } from '@/utils/permission'
import { getDisplayImageUrl } from '@/utils/imageUtils'
import { getMediaSubscribeId, useMediaSubscribe } from '@/composables/useMediaSubscribe'
import { useMediaSubscribe } from '@/composables/useMediaSubscribe'
import { getCachedMediaSubscribeStatus } from '@/utils/mediaStatusCache'
import { useMusicSiteSearch } from '@/composables/useMusicSiteSearch'
import {
@@ -13,6 +13,7 @@ import {
buildMusicResourceRoute,
formatMusicDuration,
getMusicArtistLinks,
getMusicKey,
} from '@/utils/music'
const { t } = useI18n()
@@ -80,7 +81,7 @@ const showCover = computed(() => Boolean(coverUrl.value) && !imageLoadError.valu
/** 生成订阅状态缓存键。 */
function getSubscribeStatusKey() {
return `${getMediaSubscribeId(props.music)}::all`
return `${props.music ? getMusicKey(props.music) : ''}::all`
}
const subscribeActions = useMediaSubscribe({
@@ -329,6 +329,7 @@ describe('MediaCard', () => {
const { container } = await renderCard(media)
getStatusObservers()[0]?.trigger()
await waitFor(() => expect(subscribeRequest).toHaveBeenCalledOnce())
expect((subscribeRequest.mock.calls[0][0] as URL).searchParams.get('music_type')).toBe('recording')
expect(existsRequest).not.toHaveBeenCalled()
await fireEvent.mouseEnter(getHoverArea(container))
@@ -293,6 +293,34 @@ describe('useMediaSubscribe entry flows', () => {
})
})
it('creates a recording subscription without its album track count', async () => {
const media = createMediaInfo({
media_id: 'recording-1',
music_type: 'recording',
source: 'musicbrainz',
title: '晴天',
tmdb_id: undefined,
total_tracks: 11,
type: '音乐',
year: '2003',
})
const created = vi.fn()
server.use(createSubscribeHandler({ data: { id: 503 }, success: true }, 200, created))
await renderSubscribeHarness({ media })
await fireEvent.click(screen.getByRole('button', { name: 'primary' }))
await waitFor(() => expect(created).toHaveBeenCalledOnce())
expect(created.mock.calls[0][0]).toMatchObject({
media_id: 'recording-1',
media_source: 'musicbrainz',
music_type: 'recording',
name: '晴天',
type: '音乐',
})
expect(created.mock.calls[0][0]).not.toHaveProperty('total_tracks')
})
it('does not create a subscription for an artist browsing entity', async () => {
const media = createMediaInfo({
media_id: 'artist-1',
@@ -517,6 +545,37 @@ describe('useMediaSubscribe entry flows', () => {
expect(screen.getByTestId('season-map')).toHaveTextContent('"2":false')
})
it('queries and cancels music subscriptions with their entity type', async () => {
const media = createMediaInfo({
media_id: 'release-group-1',
music_type: 'album',
source: 'musicbrainz',
title: '叶惠美',
tmdb_id: undefined,
type: '音乐',
})
const queried = vi.fn()
const deleted = vi.fn()
server.use(
querySubscribeByMediaHandler(
'musicbrainz:release-group-1',
createSubscribe({ id: 801, music_type: 'album', type: '音乐' }),
200,
url => queried(url),
),
deleteSubscribeByMediaHandler('musicbrainz:release-group-1', { success: true }, 200, url => deleted(url)),
)
await renderSubscribeHarness({ isSubscribed: true, media })
await fireEvent.click(screen.getByRole('button', { name: 'check' }))
await waitFor(() => expect(screen.getByTestId('check-result')).toHaveTextContent('subscribed'))
expect((queried.mock.calls[0][0] as URL).searchParams.get('music_type')).toBe('album')
await fireEvent.click(screen.getByRole('button', { name: 'remove' }))
await waitFor(() => expect(deleted).toHaveBeenCalledOnce())
expect((deleted.mock.calls[0][0] as URL).searchParams.get('music_type')).toBe('album')
})
it('aligns visible seasons while preserving hidden subscriptions', async () => {
const media = createSubscribeTv({ title: '多季剧集', tmdb_id: 105 })
const deleted = vi.fn()
+10 -2
View File
@@ -100,6 +100,11 @@ export function getMediaSubscribeId(media?: MediaInfo) {
return getMediaSubscribeIdentity(media)?.mediaKey ?? ''
}
/** 返回订阅 API 使用的音乐实体;旧音乐对象缺省时按单曲兼容。 */
function getMusicSubscribeType(media?: MediaInfo) {
return media?.type === '音乐' ? (media.music_type ?? 'recording') : undefined
}
// 将订阅模式转换为后端订阅字段。
function getSubscribePayload(mode: SubscribeMode): SubscribePayload {
return {
@@ -328,8 +333,8 @@ export function useMediaSubscribe(options: UseMediaSubscribeOptions) {
media_id: identity?.mediaId,
mediaid: identity?.mediaKey ?? '',
// 专辑订阅必须保留实体类型和曲目总数,后端据此校验整专资源并决定何时完成订阅。
music_type: media.music_type,
total_tracks: media.total_tracks,
music_type: getMusicSubscribeType(media),
total_tracks: getMusicSubscribeType(media) === 'album' ? media.total_tracks : undefined,
season: media.type === '电影' ? null : season,
...payload,
episode_group: episodeGroup.value,
@@ -387,6 +392,7 @@ export function useMediaSubscribe(options: UseMediaSubscribeOptions) {
const result: { [key: string]: any } = await api.delete(`subscribe/media/${getMediaId()}`, {
params: {
season: media.type === '电影' ? null : season,
music_type: getMusicSubscribeType(media),
},
})
@@ -417,6 +423,7 @@ export function useMediaSubscribe(options: UseMediaSubscribeOptions) {
params: {
season,
title: currentMedia()?.title,
music_type: getMusicSubscribeType(currentMedia()),
},
})
@@ -435,6 +442,7 @@ export function useMediaSubscribe(options: UseMediaSubscribeOptions) {
params: {
season,
title: currentMedia()?.title,
music_type: getMusicSubscribeType(currentMedia()),
},
})