mirror of
https://github.com/jxxghp/MoviePilot-Frontend.git
synced 2026-09-06 16:16:42 +08:00
fix(music): preserve subscription entity semantics
This commit is contained in:
@@ -26,7 +26,7 @@ import {
|
|||||||
getCachedMediaSubscribeStatus,
|
getCachedMediaSubscribeStatus,
|
||||||
setCachedMediaExistsStatus,
|
setCachedMediaExistsStatus,
|
||||||
} from '@/utils/mediaStatusCache'
|
} from '@/utils/mediaStatusCache'
|
||||||
import { buildMusicDetailRoute } from '@/utils/music'
|
import { buildMusicDetailRoute, getMusicKey } from '@/utils/music'
|
||||||
|
|
||||||
const SearchSiteDialog = defineAsyncComponent(() => import('@/components/dialog/SearchSiteDialog.vue'))
|
const SearchSiteDialog = defineAsyncComponent(() => import('@/components/dialog/SearchSiteDialog.vue'))
|
||||||
|
|
||||||
@@ -151,7 +151,8 @@ function getMediaId() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function getSubscribeStatusKey(season: number | null = props.media?.season ?? null) {
|
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() {
|
function getExistsStatusKey() {
|
||||||
@@ -167,6 +168,11 @@ function getExistsStatusKey() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function isSameSubscribeMedia(subscribe: Subscribe) {
|
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()
|
const mediaId = getMediaId()
|
||||||
if (subscribe.media_source && subscribe.media_id) {
|
if (subscribe.media_source && subscribe.media_id) {
|
||||||
const prefix = subscribe.media_source === 'themoviedb' ? 'tmdb' : subscribe.media_source
|
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 { useGlobalSettingsStore, useUserStore } from '@/stores'
|
||||||
import { buildUserPermissionContext, hasPermission } from '@/utils/permission'
|
import { buildUserPermissionContext, hasPermission } from '@/utils/permission'
|
||||||
import { getDisplayImageUrl } from '@/utils/imageUtils'
|
import { getDisplayImageUrl } from '@/utils/imageUtils'
|
||||||
import { getMediaSubscribeId, useMediaSubscribe } from '@/composables/useMediaSubscribe'
|
import { useMediaSubscribe } from '@/composables/useMediaSubscribe'
|
||||||
import { getCachedMediaSubscribeStatus } from '@/utils/mediaStatusCache'
|
import { getCachedMediaSubscribeStatus } from '@/utils/mediaStatusCache'
|
||||||
import { useMusicSiteSearch } from '@/composables/useMusicSiteSearch'
|
import { useMusicSiteSearch } from '@/composables/useMusicSiteSearch'
|
||||||
import {
|
import {
|
||||||
@@ -13,6 +13,7 @@ import {
|
|||||||
buildMusicResourceRoute,
|
buildMusicResourceRoute,
|
||||||
formatMusicDuration,
|
formatMusicDuration,
|
||||||
getMusicArtistLinks,
|
getMusicArtistLinks,
|
||||||
|
getMusicKey,
|
||||||
} from '@/utils/music'
|
} from '@/utils/music'
|
||||||
|
|
||||||
const { t } = useI18n()
|
const { t } = useI18n()
|
||||||
@@ -80,7 +81,7 @@ const showCover = computed(() => Boolean(coverUrl.value) && !imageLoadError.valu
|
|||||||
|
|
||||||
/** 生成订阅状态缓存键。 */
|
/** 生成订阅状态缓存键。 */
|
||||||
function getSubscribeStatusKey() {
|
function getSubscribeStatusKey() {
|
||||||
return `${getMediaSubscribeId(props.music)}::all`
|
return `${props.music ? getMusicKey(props.music) : ''}::all`
|
||||||
}
|
}
|
||||||
|
|
||||||
const subscribeActions = useMediaSubscribe({
|
const subscribeActions = useMediaSubscribe({
|
||||||
|
|||||||
@@ -329,6 +329,7 @@ describe('MediaCard', () => {
|
|||||||
const { container } = await renderCard(media)
|
const { container } = await renderCard(media)
|
||||||
getStatusObservers()[0]?.trigger()
|
getStatusObservers()[0]?.trigger()
|
||||||
await waitFor(() => expect(subscribeRequest).toHaveBeenCalledOnce())
|
await waitFor(() => expect(subscribeRequest).toHaveBeenCalledOnce())
|
||||||
|
expect((subscribeRequest.mock.calls[0][0] as URL).searchParams.get('music_type')).toBe('recording')
|
||||||
expect(existsRequest).not.toHaveBeenCalled()
|
expect(existsRequest).not.toHaveBeenCalled()
|
||||||
|
|
||||||
await fireEvent.mouseEnter(getHoverArea(container))
|
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 () => {
|
it('does not create a subscription for an artist browsing entity', async () => {
|
||||||
const media = createMediaInfo({
|
const media = createMediaInfo({
|
||||||
media_id: 'artist-1',
|
media_id: 'artist-1',
|
||||||
@@ -517,6 +545,37 @@ describe('useMediaSubscribe entry flows', () => {
|
|||||||
expect(screen.getByTestId('season-map')).toHaveTextContent('"2":false')
|
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 () => {
|
it('aligns visible seasons while preserving hidden subscriptions', async () => {
|
||||||
const media = createSubscribeTv({ title: '多季剧集', tmdb_id: 105 })
|
const media = createSubscribeTv({ title: '多季剧集', tmdb_id: 105 })
|
||||||
const deleted = vi.fn()
|
const deleted = vi.fn()
|
||||||
|
|||||||
@@ -100,6 +100,11 @@ export function getMediaSubscribeId(media?: MediaInfo) {
|
|||||||
return getMediaSubscribeIdentity(media)?.mediaKey ?? ''
|
return getMediaSubscribeIdentity(media)?.mediaKey ?? ''
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** 返回订阅 API 使用的音乐实体;旧音乐对象缺省时按单曲兼容。 */
|
||||||
|
function getMusicSubscribeType(media?: MediaInfo) {
|
||||||
|
return media?.type === '音乐' ? (media.music_type ?? 'recording') : undefined
|
||||||
|
}
|
||||||
|
|
||||||
// 将订阅模式转换为后端订阅字段。
|
// 将订阅模式转换为后端订阅字段。
|
||||||
function getSubscribePayload(mode: SubscribeMode): SubscribePayload {
|
function getSubscribePayload(mode: SubscribeMode): SubscribePayload {
|
||||||
return {
|
return {
|
||||||
@@ -328,8 +333,8 @@ export function useMediaSubscribe(options: UseMediaSubscribeOptions) {
|
|||||||
media_id: identity?.mediaId,
|
media_id: identity?.mediaId,
|
||||||
mediaid: identity?.mediaKey ?? '',
|
mediaid: identity?.mediaKey ?? '',
|
||||||
// 专辑订阅必须保留实体类型和曲目总数,后端据此校验整专资源并决定何时完成订阅。
|
// 专辑订阅必须保留实体类型和曲目总数,后端据此校验整专资源并决定何时完成订阅。
|
||||||
music_type: media.music_type,
|
music_type: getMusicSubscribeType(media),
|
||||||
total_tracks: media.total_tracks,
|
total_tracks: getMusicSubscribeType(media) === 'album' ? media.total_tracks : undefined,
|
||||||
season: media.type === '电影' ? null : season,
|
season: media.type === '电影' ? null : season,
|
||||||
...payload,
|
...payload,
|
||||||
episode_group: episodeGroup.value,
|
episode_group: episodeGroup.value,
|
||||||
@@ -387,6 +392,7 @@ export function useMediaSubscribe(options: UseMediaSubscribeOptions) {
|
|||||||
const result: { [key: string]: any } = await api.delete(`subscribe/media/${getMediaId()}`, {
|
const result: { [key: string]: any } = await api.delete(`subscribe/media/${getMediaId()}`, {
|
||||||
params: {
|
params: {
|
||||||
season: media.type === '电影' ? null : season,
|
season: media.type === '电影' ? null : season,
|
||||||
|
music_type: getMusicSubscribeType(media),
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
|
|
||||||
@@ -417,6 +423,7 @@ export function useMediaSubscribe(options: UseMediaSubscribeOptions) {
|
|||||||
params: {
|
params: {
|
||||||
season,
|
season,
|
||||||
title: currentMedia()?.title,
|
title: currentMedia()?.title,
|
||||||
|
music_type: getMusicSubscribeType(currentMedia()),
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
|
|
||||||
@@ -435,6 +442,7 @@ export function useMediaSubscribe(options: UseMediaSubscribeOptions) {
|
|||||||
params: {
|
params: {
|
||||||
season,
|
season,
|
||||||
title: currentMedia()?.title,
|
title: currentMedia()?.title,
|
||||||
|
music_type: getMusicSubscribeType(currentMedia()),
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user