mirror of
https://github.com/jxxghp/MoviePilot-Frontend.git
synced 2026-09-04 23:18:44 +08:00
fix(music): preserve subscription entity semantics
This commit is contained in:
@@ -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,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()
|
||||
|
||||
@@ -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()),
|
||||
},
|
||||
})
|
||||
|
||||
|
||||
Reference in New Issue
Block a user