mirror of
https://github.com/jxxghp/MoviePilot-Frontend.git
synced 2026-09-06 16:16:42 +08:00
feat(music): support album subscription semantics
This commit is contained in:
@@ -162,11 +162,6 @@
|
|||||||
"count": 1
|
"count": 1
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"src/components/cards/__tests__/SubscribeCard.spec.ts": {
|
|
||||||
"sonarjs/super-linear-regex": {
|
|
||||||
"count": 2
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"src/components/dialog/AboutDialog.vue": {
|
"src/components/dialog/AboutDialog.vue": {
|
||||||
"@typescript-eslint/no-explicit-any": {
|
"@typescript-eslint/no-explicit-any": {
|
||||||
"count": 8
|
"count": 8
|
||||||
@@ -1056,4 +1051,4 @@
|
|||||||
"count": 1
|
"count": 1
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
+9
-1
@@ -18,7 +18,7 @@ export interface Subscribe {
|
|||||||
name: string
|
name: string
|
||||||
// 订阅年份
|
// 订阅年份
|
||||||
year: string
|
year: string
|
||||||
// 订阅类型 电影/电视剧
|
// 订阅类型 电影/电视剧/音乐
|
||||||
type: string
|
type: string
|
||||||
// 搜索关键字
|
// 搜索关键字
|
||||||
keyword?: string
|
keyword?: string
|
||||||
@@ -34,6 +34,10 @@ export interface Subscribe {
|
|||||||
media_source?: MediaDataSource
|
media_source?: MediaDataSource
|
||||||
// 数据源原生ID
|
// 数据源原生ID
|
||||||
media_id?: string
|
media_id?: string
|
||||||
|
// 音乐实体类型:recording 单曲、album 专辑、artist 艺术家
|
||||||
|
music_type?: MusicEntityType
|
||||||
|
// 专辑总曲目数
|
||||||
|
total_tracks?: number
|
||||||
// 其它媒体ID
|
// 其它媒体ID
|
||||||
mediaid?: string
|
mediaid?: string
|
||||||
// 季号
|
// 季号
|
||||||
@@ -140,6 +144,10 @@ export interface SubscribeShare {
|
|||||||
media_source?: MediaDataSource
|
media_source?: MediaDataSource
|
||||||
// 数据源原生ID
|
// 数据源原生ID
|
||||||
media_id?: string
|
media_id?: string
|
||||||
|
// 音乐实体类型:recording 单曲、album 专辑、artist 艺术家
|
||||||
|
music_type?: MusicEntityType
|
||||||
|
// 专辑总曲目数
|
||||||
|
total_tracks?: number
|
||||||
// 季号
|
// 季号
|
||||||
season?: number
|
season?: number
|
||||||
// 海报
|
// 海报
|
||||||
|
|||||||
@@ -25,6 +25,7 @@ const userStore = useUserStore()
|
|||||||
const userPermissions = computed(() => buildUserPermissionContext(userStore.superUser, userStore.permissions))
|
const userPermissions = computed(() => buildUserPermissionContext(userStore.superUser, userStore.permissions))
|
||||||
const canSearch = computed(() => hasPermission(userPermissions.value, 'search'))
|
const canSearch = computed(() => hasPermission(userPermissions.value, 'search'))
|
||||||
const canSubscribe = computed(() => hasPermission(userPermissions.value, 'subscribe'))
|
const canSubscribe = computed(() => hasPermission(userPermissions.value, 'subscribe'))
|
||||||
|
const isActionableMedia = computed(() => props.music?.music_type !== 'artist')
|
||||||
|
|
||||||
// 封面加载失败后改用专辑占位图标
|
// 封面加载失败后改用专辑占位图标
|
||||||
const imageLoadError = ref(false)
|
const imageLoadError = ref(false)
|
||||||
@@ -90,7 +91,7 @@ const { openMusicSiteSearch } = useMusicSiteSearch(sites =>
|
|||||||
|
|
||||||
/** 查询当前音乐是否已订阅,用于决定心形图标是实心还是空心。 */
|
/** 查询当前音乐是否已订阅,用于决定心形图标是实心还是空心。 */
|
||||||
async function checkSubscribeStatus() {
|
async function checkSubscribeStatus() {
|
||||||
if (!canSubscribe.value || !props.music?.media_id) return
|
if (!isActionableMedia.value || !canSubscribe.value || !props.music?.media_id) return
|
||||||
try {
|
try {
|
||||||
isSubscribed.value = await getCachedMediaSubscribeStatus(getSubscribeStatusKey(), () =>
|
isSubscribed.value = await getCachedMediaSubscribeStatus(getSubscribeStatusKey(), () =>
|
||||||
subscribeActions.checkSubscribe(null),
|
subscribeActions.checkSubscribe(null),
|
||||||
@@ -161,7 +162,7 @@ onMounted(checkSubscribeStatus)
|
|||||||
</VChip>
|
</VChip>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="music-card-supporting text-medium-emphasis">
|
<div v-if="props.music?.music_type !== 'artist'" class="music-card-supporting text-medium-emphasis">
|
||||||
<VIcon icon="mdi-account-music" size="16" />
|
<VIcon icon="mdi-account-music" size="16" />
|
||||||
<div class="music-card-artists">
|
<div class="music-card-artists">
|
||||||
<template v-if="artistLinks.length">
|
<template v-if="artistLinks.length">
|
||||||
@@ -183,7 +184,10 @@ onMounted(checkSubscribeStatus)
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div v-if="props.music?.album" class="music-card-supporting text-medium-emphasis">
|
<div
|
||||||
|
v-if="props.music?.album && props.music?.music_type !== 'album'"
|
||||||
|
class="music-card-supporting text-medium-emphasis"
|
||||||
|
>
|
||||||
<VIcon icon="mdi-album" size="16" />
|
<VIcon icon="mdi-album" size="16" />
|
||||||
<div class="music-card-album">
|
<div class="music-card-album">
|
||||||
<span>{{ t('music.album') }}:</span>
|
<span>{{ t('music.album') }}:</span>
|
||||||
@@ -216,7 +220,7 @@ onMounted(checkSubscribeStatus)
|
|||||||
|
|
||||||
<div class="music-card-actions">
|
<div class="music-card-actions">
|
||||||
<IconBtn
|
<IconBtn
|
||||||
v-if="canSubscribe"
|
v-if="canSubscribe && isActionableMedia"
|
||||||
:icon="isSubscribed ? 'mdi-heart' : 'mdi-heart-outline'"
|
:icon="isSubscribed ? 'mdi-heart' : 'mdi-heart-outline'"
|
||||||
:color="isSubscribed ? 'error' : 'medium-emphasis'"
|
:color="isSubscribed ? 'error' : 'medium-emphasis'"
|
||||||
:aria-label="isSubscribed ? t('music.unsubscribe') : t('music.subscribe')"
|
:aria-label="isSubscribed ? t('music.unsubscribe') : t('music.subscribe')"
|
||||||
@@ -226,7 +230,7 @@ onMounted(checkSubscribeStatus)
|
|||||||
@click.stop="subscribeActions.handleSubscribe()"
|
@click.stop="subscribeActions.handleSubscribe()"
|
||||||
/>
|
/>
|
||||||
<IconBtn
|
<IconBtn
|
||||||
v-if="canSearch"
|
v-if="canSearch && isActionableMedia"
|
||||||
icon="mdi-magnify"
|
icon="mdi-magnify"
|
||||||
color="primary"
|
color="primary"
|
||||||
:aria-label="t('music.searchResources')"
|
:aria-label="t('music.searchResources')"
|
||||||
|
|||||||
@@ -136,6 +136,12 @@ const subscribeProgressText = computed(() => {
|
|||||||
return `${downloadedEpisode.value} / ${total}`
|
return `${downloadedEpisode.value} / ${total}`
|
||||||
})
|
})
|
||||||
|
|
||||||
|
// 专辑订阅按整包完成,不伪造分集进度;这里只展示整专规模,提醒用户该目标包含多首曲目。
|
||||||
|
const musicAlbumTrackText = computed(() => {
|
||||||
|
if (props.media?.type !== '音乐' || props.media?.music_type !== 'album' || !props.media?.total_tracks) return ''
|
||||||
|
return `${t('music.entityAlbum')} · ${t('music.trackCount', { count: props.media.total_tracks })}`
|
||||||
|
})
|
||||||
|
|
||||||
// 订阅卡片 hover 文案:
|
// 订阅卡片 hover 文案:
|
||||||
// - 普通订阅:「已下载 X · 共 Y 集」
|
// - 普通订阅:「已下载 X · 共 Y 集」
|
||||||
// - 洗版订阅:「已下载 X · 已洗版 N · 共 Y 集」
|
// - 洗版订阅:「已下载 X · 已洗版 N · 共 Y 集」
|
||||||
@@ -634,8 +640,11 @@ function handleCardClick() {
|
|||||||
:data-subscribe-state-icon="compactStateDisplay.icon"
|
:data-subscribe-state-icon="compactStateDisplay.icon"
|
||||||
size="16"
|
size="16"
|
||||||
/>
|
/>
|
||||||
<span v-if="subscribeProgressText" class="subscribe-card-mobile-progress-text">
|
<span
|
||||||
{{ subscribeProgressText }}
|
v-if="subscribeProgressText || musicAlbumTrackText"
|
||||||
|
class="subscribe-card-mobile-progress-text"
|
||||||
|
>
|
||||||
|
{{ subscribeProgressText || musicAlbumTrackText }}
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@@ -699,7 +708,9 @@ function handleCardClick() {
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</VCardText>
|
</VCardText>
|
||||||
<VCardText class="absolute inset-x-0 bottom-2 z-10 flex min-w-0 justify-space-between align-center flex-wrap px-3">
|
<VCardText
|
||||||
|
class="absolute inset-x-0 bottom-2 z-10 flex min-w-0 justify-space-between align-center flex-wrap px-3"
|
||||||
|
>
|
||||||
<div class="flex min-w-0 max-w-full align-center">
|
<div class="flex min-w-0 max-w-full align-center">
|
||||||
<VIcon
|
<VIcon
|
||||||
v-if="props.media?.total_episode && props.sortable"
|
v-if="props.media?.total_episode && props.sortable"
|
||||||
@@ -722,6 +733,13 @@ function handleCardClick() {
|
|||||||
{{ subscribeProgressTooltip }}
|
{{ subscribeProgressTooltip }}
|
||||||
</VTooltip>
|
</VTooltip>
|
||||||
</div>
|
</div>
|
||||||
|
<div
|
||||||
|
v-else-if="musicAlbumTrackText"
|
||||||
|
class="flex flex-shrink-0 align-center text-subtitle-2 me-2 text-white"
|
||||||
|
>
|
||||||
|
<VIcon icon="mdi-album" size="small" class="me-1" />
|
||||||
|
{{ musicAlbumTrackText }}
|
||||||
|
</div>
|
||||||
<VIcon
|
<VIcon
|
||||||
v-if="props.media?.username && props.sortable"
|
v-if="props.media?.username && props.sortable"
|
||||||
icon="mdi-account"
|
icon="mdi-account"
|
||||||
|
|||||||
@@ -138,7 +138,7 @@ describe('SubscribeCard display and progress', () => {
|
|||||||
expect((image as HTMLImageElement).src).toContain('system/cache/image?url=')
|
expect((image as HTMLImageElement).src).toContain('system/cache/image?url=')
|
||||||
expect((image as HTMLImageElement).src).toContain(encodeURIComponent(media.backdrop || ''))
|
expect((image as HTMLImageElement).src).toContain(encodeURIComponent(media.backdrop || ''))
|
||||||
expect(screen.queryByRole('progressbar')).not.toBeInTheDocument()
|
expect(screen.queryByRole('progressbar')).not.toBeInTheDocument()
|
||||||
expect(screen.queryByText(/\d+ \/ \d+/)).not.toBeInTheDocument()
|
expect(screen.queryByText(/^\d{1,4} \/ \d{1,4}$/)).not.toBeInTheDocument()
|
||||||
})
|
})
|
||||||
|
|
||||||
it('uses the poster as the background fallback and replaces failed images with the placeholder', async () => {
|
it('uses the poster as the background fallback and replaces failed images with the placeholder', async () => {
|
||||||
@@ -172,6 +172,21 @@ describe('SubscribeCard display and progress', () => {
|
|||||||
expect(container.querySelector('img')).toBeNull()
|
expect(container.querySelector('img')).toBeNull()
|
||||||
})
|
})
|
||||||
|
|
||||||
|
it.each([480, 1024])('shows whole-album track count without fake episode progress at %ipx', async width => {
|
||||||
|
setViewport(width)
|
||||||
|
await renderCard({
|
||||||
|
music_type: 'album',
|
||||||
|
name: '叶惠美',
|
||||||
|
total_episode: undefined,
|
||||||
|
total_tracks: 11,
|
||||||
|
type: '音乐',
|
||||||
|
})
|
||||||
|
|
||||||
|
expect(screen.getByText('专辑 · 11 首')).toBeInTheDocument()
|
||||||
|
expect(screen.queryByRole('progressbar')).not.toBeInTheDocument()
|
||||||
|
expect(screen.queryByText(/^\d{1,4} \/ \d{1,4}$/)).not.toBeInTheDocument()
|
||||||
|
})
|
||||||
|
|
||||||
it.each([
|
it.each([
|
||||||
['regular progress', 10, 4, '6 / 10', '60'],
|
['regular progress', 10, 4, '6 / 10', '60'],
|
||||||
['negative missing episodes', 10, -2, '10 / 10', '100'],
|
['negative missing episodes', 10, -2, '10 / 10', '100'],
|
||||||
@@ -181,7 +196,7 @@ describe('SubscribeCard display and progress', () => {
|
|||||||
await renderCard({ lack_episode: lackEpisode, season: 2, total_episode: totalEpisode, type: '电视剧' })
|
await renderCard({ lack_episode: lackEpisode, season: 2, total_episode: totalEpisode, type: '电视剧' })
|
||||||
|
|
||||||
if (expectedText) expect(screen.getByText(expectedText)).toBeInTheDocument()
|
if (expectedText) expect(screen.getByText(expectedText)).toBeInTheDocument()
|
||||||
else expect(screen.queryByText(/\d+ \/ \d+/)).not.toBeInTheDocument()
|
else expect(screen.queryByText(/^\d{1,4} \/ \d{1,4}$/)).not.toBeInTheDocument()
|
||||||
|
|
||||||
if (expectedProgress) expect(screen.getByRole('progressbar')).toHaveAttribute('aria-valuenow', expectedProgress)
|
if (expectedProgress) expect(screen.getByRole('progressbar')).toHaveAttribute('aria-valuenow', expectedProgress)
|
||||||
else expect(screen.queryByRole('progressbar')).not.toBeInTheDocument()
|
else expect(screen.queryByRole('progressbar')).not.toBeInTheDocument()
|
||||||
|
|||||||
@@ -9,6 +9,7 @@ import {
|
|||||||
} from '@/composables/useMediaSubscribe'
|
} from '@/composables/useMediaSubscribe'
|
||||||
import { getActiveRequestsCount } from '@/utils/requestOptimizer'
|
import { getActiveRequestsCount } from '@/utils/requestOptimizer'
|
||||||
import { fireEvent, screen, waitFor } from '@testing-library/vue'
|
import { fireEvent, screen, waitFor } from '@testing-library/vue'
|
||||||
|
import { createMediaInfo } from '@tests/support/factories/media'
|
||||||
import { createSubscribe, createSubscribeMovie, createSubscribeTv } from '@tests/support/factories/subscribe'
|
import { createSubscribe, createSubscribeMovie, createSubscribeTv } from '@tests/support/factories/subscribe'
|
||||||
import {
|
import {
|
||||||
createSubscribeHandler,
|
createSubscribeHandler,
|
||||||
@@ -261,6 +262,56 @@ describe('useMediaSubscribe entry flows', () => {
|
|||||||
expect(mocks.doneProgress).toHaveBeenCalledOnce()
|
expect(mocks.doneProgress).toHaveBeenCalledOnce()
|
||||||
})
|
})
|
||||||
|
|
||||||
|
it('creates an album subscription with its entity type and complete track count', async () => {
|
||||||
|
const media = createMediaInfo({
|
||||||
|
media_id: 'release-group-1',
|
||||||
|
music_type: 'album',
|
||||||
|
source: 'musicbrainz',
|
||||||
|
title: '叶惠美',
|
||||||
|
tmdb_id: undefined,
|
||||||
|
total_tracks: 11,
|
||||||
|
type: '音乐',
|
||||||
|
year: '2003',
|
||||||
|
})
|
||||||
|
const created = vi.fn()
|
||||||
|
server.use(createSubscribeHandler({ data: { id: 502 }, 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: 'release-group-1',
|
||||||
|
media_source: 'musicbrainz',
|
||||||
|
mediaid: 'musicbrainz:release-group-1',
|
||||||
|
music_type: 'album',
|
||||||
|
name: '叶惠美',
|
||||||
|
season: null,
|
||||||
|
total_tracks: 11,
|
||||||
|
type: '音乐',
|
||||||
|
year: '2003',
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
it('does not create a subscription for an artist browsing entity', async () => {
|
||||||
|
const media = createMediaInfo({
|
||||||
|
media_id: 'artist-1',
|
||||||
|
music_type: 'artist',
|
||||||
|
source: 'musicbrainz',
|
||||||
|
title: '周杰伦',
|
||||||
|
tmdb_id: undefined,
|
||||||
|
type: '音乐',
|
||||||
|
})
|
||||||
|
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' }))
|
||||||
|
|
||||||
|
expect(created).not.toHaveBeenCalled()
|
||||||
|
expect(mocks.startProgress).not.toHaveBeenCalled()
|
||||||
|
})
|
||||||
|
|
||||||
it('creates an AniList subscription without promoting its auxiliary TMDB ID', async () => {
|
it('creates an AniList subscription without promoting its auxiliary TMDB ID', async () => {
|
||||||
const media = createSubscribeTv({
|
const media = createSubscribeTv({
|
||||||
anilist_id: 154587,
|
anilist_id: 154587,
|
||||||
|
|||||||
@@ -309,7 +309,8 @@ export function useMediaSubscribe(options: UseMediaSubscribeOptions) {
|
|||||||
addOptions: AddSubscribeOptions = {},
|
addOptions: AddSubscribeOptions = {},
|
||||||
) {
|
) {
|
||||||
const media = currentMedia()
|
const media = currentMedia()
|
||||||
if (!media) return
|
// 艺术家仅用于继续浏览,其下作品必须按单曲或专辑分别订阅。
|
||||||
|
if (!media || media.music_type === 'artist') return
|
||||||
const identity = getMediaSubscribeIdentity(media)
|
const identity = getMediaSubscribeIdentity(media)
|
||||||
|
|
||||||
startNProgress()
|
startNProgress()
|
||||||
@@ -326,6 +327,9 @@ export function useMediaSubscribe(options: UseMediaSubscribeOptions) {
|
|||||||
media_source: identity?.source,
|
media_source: identity?.source,
|
||||||
media_id: identity?.mediaId,
|
media_id: identity?.mediaId,
|
||||||
mediaid: identity?.mediaKey ?? '',
|
mediaid: identity?.mediaKey ?? '',
|
||||||
|
// 专辑订阅必须保留实体类型和曲目总数,后端据此校验整专资源并决定何时完成订阅。
|
||||||
|
music_type: media.music_type,
|
||||||
|
total_tracks: media.total_tracks,
|
||||||
season: media.type === '电影' ? null : season,
|
season: media.type === '电影' ? null : season,
|
||||||
...payload,
|
...payload,
|
||||||
episode_group: episodeGroup.value,
|
episode_group: episodeGroup.value,
|
||||||
@@ -499,7 +503,7 @@ export function useMediaSubscribe(options: UseMediaSubscribeOptions) {
|
|||||||
// 处理媒体主订阅入口,电视剧统一进入季选择弹窗。
|
// 处理媒体主订阅入口,电视剧统一进入季选择弹窗。
|
||||||
function handlePrimarySubscribe() {
|
function handlePrimarySubscribe() {
|
||||||
const media = currentMedia()
|
const media = currentMedia()
|
||||||
if (!media) return
|
if (!media || media.music_type === 'artist') return
|
||||||
|
|
||||||
const season = media.type === '电影' ? null : getPrimarySeason()
|
const season = media.type === '电影' ? null : getPrimarySeason()
|
||||||
|
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
import MusicPage from '@/pages/music.vue'
|
import MusicPage from '@/pages/music.vue'
|
||||||
import { fireEvent, screen, waitFor } from '@testing-library/vue'
|
import { fireEvent, screen, waitFor, within } from '@testing-library/vue'
|
||||||
import { renderWithProviders } from '@tests/support/render'
|
import { renderWithProviders } from '@tests/support/render'
|
||||||
import { beforeEach, describe, expect, it, vi } from 'vitest'
|
import { beforeEach, describe, expect, it, vi } from 'vitest'
|
||||||
|
|
||||||
@@ -49,6 +49,33 @@ const musicResult = {
|
|||||||
year: 2003,
|
year: 2003,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const albumResult = {
|
||||||
|
album: '七里香',
|
||||||
|
album_id: 'release-group-2',
|
||||||
|
album_type: 'Album',
|
||||||
|
artist: '周杰伦',
|
||||||
|
artists: ['周杰伦'],
|
||||||
|
artist_ids: ['artist-1'],
|
||||||
|
category: 'Album',
|
||||||
|
media_id: 'release-group-2',
|
||||||
|
music_type: 'album',
|
||||||
|
release_date: '2004-08-03',
|
||||||
|
source: 'musicbrainz',
|
||||||
|
title: '七里香',
|
||||||
|
type: '音乐',
|
||||||
|
year: 2004,
|
||||||
|
}
|
||||||
|
|
||||||
|
const artistResult = {
|
||||||
|
category: 'Person',
|
||||||
|
media_id: 'artist-1',
|
||||||
|
music_type: 'artist',
|
||||||
|
source: 'musicbrainz',
|
||||||
|
title: '周杰伦',
|
||||||
|
type: '音乐',
|
||||||
|
version: 'Taiwanese singer-songwriter',
|
||||||
|
}
|
||||||
|
|
||||||
const musicSite = { id: 11, is_active: true, name: '音乐站点', url: 'https://music.example' }
|
const musicSite = { id: 11, is_active: true, name: '音乐站点', url: 'https://music.example' }
|
||||||
|
|
||||||
/** 按请求路径分派音乐搜索与订阅状态查询。 */
|
/** 按请求路径分派音乐搜索与订阅状态查询。 */
|
||||||
@@ -181,6 +208,32 @@ describe('music page', () => {
|
|||||||
expect(router.currentRoute.value.query).toMatchObject({ mediaid: 'artist-1' })
|
expect(router.currentRoute.value.query).toMatchObject({ mediaid: 'artist-1' })
|
||||||
})
|
})
|
||||||
|
|
||||||
|
it('renders album and artist search entities with entity-correct actions and routes', async () => {
|
||||||
|
mocks.apiGet.mockImplementation((path: string) => {
|
||||||
|
if (path === 'media/search') return Promise.resolve([musicResult, albumResult, artistResult])
|
||||||
|
if (path.startsWith('subscribe/media/')) return Promise.reject({ response: { status: 404 } })
|
||||||
|
return Promise.resolve([])
|
||||||
|
})
|
||||||
|
const { router } = await renderMusicPage()
|
||||||
|
|
||||||
|
const artistEntity = await screen.findByText('艺术家')
|
||||||
|
const artistCard = artistEntity.closest('.music-card')
|
||||||
|
expect(artistCard).not.toBeNull()
|
||||||
|
expect(within(artistCard as HTMLElement).queryByRole('button', { name: '订阅' })).not.toBeInTheDocument()
|
||||||
|
expect(within(artistCard as HTMLElement).queryByRole('button', { name: '搜索资源' })).not.toBeInTheDocument()
|
||||||
|
|
||||||
|
await fireEvent.click(screen.getByText('七里香'))
|
||||||
|
await waitFor(() => expect(router.currentRoute.value.path).toBe('/music/album'))
|
||||||
|
expect(router.currentRoute.value.query).toMatchObject({ mediaid: 'release-group-2' })
|
||||||
|
|
||||||
|
await router.push('/music?query=晴天')
|
||||||
|
const restoredArtistEntity = await screen.findByText('艺术家')
|
||||||
|
const restoredArtistCard = restoredArtistEntity.closest('.music-card')
|
||||||
|
await fireEvent.click(within(restoredArtistCard as HTMLElement).getByText('周杰伦'))
|
||||||
|
await waitFor(() => expect(router.currentRoute.value.path).toBe('/music/artist'))
|
||||||
|
expect(router.currentRoute.value.query).toMatchObject({ mediaid: 'artist-1' })
|
||||||
|
})
|
||||||
|
|
||||||
it('selects a music-capable site before routing the resource search', async () => {
|
it('selects a music-capable site before routing the resource search', async () => {
|
||||||
const { router } = await renderMusicPage()
|
const { router } = await renderMusicPage()
|
||||||
|
|
||||||
|
|||||||
@@ -81,6 +81,17 @@ describe('music utils', () => {
|
|||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
|
it('does not build a resource route for an artist browsing entity', () => {
|
||||||
|
expect(
|
||||||
|
buildMusicResourceRoute({
|
||||||
|
source: 'musicbrainz',
|
||||||
|
media_id: 'artist-1',
|
||||||
|
music_type: 'artist',
|
||||||
|
title: 'Queen',
|
||||||
|
} as never),
|
||||||
|
).toBeUndefined()
|
||||||
|
})
|
||||||
|
|
||||||
it('keeps the entity type inside the list key so albums and tracks never collide', () => {
|
it('keeps the entity type inside the list key so albums and tracks never collide', () => {
|
||||||
const track = getMusicKey({ source: 'musicbrainz', media_id: 'same-id' })
|
const track = getMusicKey({ source: 'musicbrainz', media_id: 'same-id' })
|
||||||
const album = getMusicKey({ source: 'musicbrainz', media_id: 'same-id', music_type: 'album' })
|
const album = getMusicKey({ source: 'musicbrainz', media_id: 'same-id', music_type: 'album' })
|
||||||
|
|||||||
@@ -63,6 +63,8 @@ export function buildMusicResourceRoute(
|
|||||||
item: MediaInfo | MusicAlbumInfo,
|
item: MediaInfo | MusicAlbumInfo,
|
||||||
sites: number[] = [],
|
sites: number[] = [],
|
||||||
): RouteLocationRaw | undefined {
|
): RouteLocationRaw | undefined {
|
||||||
|
// 艺术家是浏览入口,不是可直接下载的媒体实体。
|
||||||
|
if ((item as MusicRouteTarget).music_type === 'artist') return undefined
|
||||||
const source = getMusicSource(item as MusicRouteTarget)
|
const source = getMusicSource(item as MusicRouteTarget)
|
||||||
if (!source || !item.media_id) return undefined
|
if (!source || !item.media_id) return undefined
|
||||||
return {
|
return {
|
||||||
|
|||||||
@@ -151,7 +151,7 @@ function getSubscribeStatus(subscribe: Subscribe) {
|
|||||||
return 'paused' // 暂停
|
return 'paused' // 暂停
|
||||||
}
|
}
|
||||||
|
|
||||||
// 电影和音乐没有分集进度,只有状态。
|
// 电影、单曲和整专都是原子下载目标;整专曲目总数由订阅卡片单独展示,不映射成分集进度。
|
||||||
if (subscribe.type === '电影' || subscribe.type === '音乐') {
|
if (subscribe.type === '电影' || subscribe.type === '音乐') {
|
||||||
return 'all'
|
return 'all'
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user