mirror of
https://github.com/jxxghp/MoviePilot-Frontend.git
synced 2026-08-14 10:05:10 +08:00
feat(music): add audio quality controls
This commit is contained in:
@@ -125,6 +125,19 @@ export const innerFilterRules = [
|
||||
{ title: i18n.global.t('filterRules.hkVoi'), value: ' HKVOI ' },
|
||||
{ title: i18n.global.t('filterRules.notHkVoi'), value: ' !HKVOI ' },
|
||||
{ title: i18n.global.t('filterRules.free'), value: ' FREE ' },
|
||||
{ title: i18n.global.t('filterRules.audioHires'), value: ' HIRES ' },
|
||||
{ title: i18n.global.t('filterRules.audioLossless'), value: ' LOSSLESS ' },
|
||||
{ title: i18n.global.t('filterRules.audioFlac'), value: ' FLAC ' },
|
||||
{ title: i18n.global.t('filterRules.audioAlac'), value: ' ALAC ' },
|
||||
{ title: i18n.global.t('filterRules.audioApe'), value: ' APE ' },
|
||||
{ title: i18n.global.t('filterRules.audioWav'), value: ' WAV ' },
|
||||
{ title: i18n.global.t('filterRules.audioDsd'), value: ' DSD ' },
|
||||
{ title: i18n.global.t('filterRules.audioMp3'), value: ' MP3 ' },
|
||||
{ title: i18n.global.t('filterRules.audioAac'), value: ' AAC ' },
|
||||
{ title: i18n.global.t('filterRules.audioOpus'), value: ' OPUS ' },
|
||||
{ title: i18n.global.t('filterRules.bitrate320'), value: ' BITRATE320 ' },
|
||||
{ title: i18n.global.t('filterRules.bitrate256'), value: ' BITRATE256 ' },
|
||||
{ title: i18n.global.t('filterRules.bitrate192'), value: ' BITRATE192 ' },
|
||||
{ title: i18n.global.t('filterRules.resolution4k'), value: ' 4K ' },
|
||||
{ title: i18n.global.t('filterRules.resolution1080p'), value: ' 1080P ' },
|
||||
{ title: i18n.global.t('filterRules.resolution720p'), value: ' 720P ' },
|
||||
@@ -247,6 +260,46 @@ export const effectOptions = ref([
|
||||
},
|
||||
])
|
||||
|
||||
// 音乐音质等级选择框数据
|
||||
export const audioQualityOptions = ref([
|
||||
{ title: i18n.global.t('audioQualityOptions.all'), value: '' },
|
||||
{ title: i18n.global.t('audioQualityOptions.hires'), value: 'hires' },
|
||||
{ title: i18n.global.t('audioQualityOptions.lossless'), value: 'hires|lossless' },
|
||||
{ title: i18n.global.t('audioQualityOptions.lossy'), value: 'lossy' },
|
||||
])
|
||||
|
||||
// 音频格式选择框数据
|
||||
export const audioFormatOptions = ref([
|
||||
{ title: i18n.global.t('audioFormatOptions.all'), value: '' },
|
||||
{ title: i18n.global.t('audioFormatOptions.lossless'), value: 'DSD|FLAC|ALAC|APE|WAV|AIFF|PCM' },
|
||||
...['DSD', 'FLAC', 'ALAC', 'APE', 'WAV', 'AIFF', 'PCM', 'MP3', 'AAC', 'OGG', 'OPUS', 'WMA'].map(value => ({
|
||||
title: value,
|
||||
value,
|
||||
})),
|
||||
])
|
||||
|
||||
export const audioBitrateOptions = [
|
||||
{ title: '128 kbps', value: 128000 },
|
||||
{ title: '192 kbps', value: 192000 },
|
||||
{ title: '256 kbps', value: 256000 },
|
||||
{ title: '320 kbps', value: 320000 },
|
||||
]
|
||||
|
||||
export const audioBitDepthOptions = [
|
||||
{ title: '16-bit', value: 16 },
|
||||
{ title: '24-bit', value: 24 },
|
||||
{ title: '32-bit', value: 32 },
|
||||
]
|
||||
|
||||
export const audioSampleRateOptions = [
|
||||
{ title: '44.1 kHz', value: 44100 },
|
||||
{ title: '48 kHz', value: 48000 },
|
||||
{ title: '88.2 kHz', value: 88200 },
|
||||
{ title: '96 kHz', value: 96000 },
|
||||
{ title: '176.4 kHz', value: 176400 },
|
||||
{ title: '192 kHz', value: 192000 },
|
||||
]
|
||||
|
||||
// 媒体类型选项
|
||||
export const mediaTypeOptions = [
|
||||
{
|
||||
|
||||
@@ -62,6 +62,16 @@ export interface Subscribe {
|
||||
resolution?: string
|
||||
// 特效
|
||||
effect?: string
|
||||
// 音乐音质等级正则
|
||||
audio_quality?: string
|
||||
// 音频格式正则
|
||||
audio_format?: string
|
||||
// 最低码率(bps)
|
||||
min_bitrate?: number
|
||||
// 最低位深(bit)
|
||||
min_bit_depth?: number
|
||||
// 最低采样率(Hz)
|
||||
min_sample_rate?: number
|
||||
// 总集数
|
||||
total_episode?: number
|
||||
// 开始集数
|
||||
@@ -88,6 +98,14 @@ export interface Subscribe {
|
||||
search_imdbid?: any
|
||||
// 当前优先级
|
||||
current_priority: number
|
||||
// 当前音乐版本格式
|
||||
current_audio_format?: string
|
||||
// 当前音乐版本码率(bps)
|
||||
current_bitrate?: number
|
||||
// 当前音乐版本位深(bit)
|
||||
current_bit_depth?: number
|
||||
// 当前音乐版本采样率(Hz)
|
||||
current_sample_rate?: number
|
||||
// 洗版时已下载剧集的优先级状态
|
||||
episode_priority?: Record<string, number>
|
||||
// 保存目录
|
||||
@@ -272,6 +290,20 @@ export interface TransferHistory {
|
||||
errmsg?: string
|
||||
// 日期
|
||||
date?: string
|
||||
// 音乐实体类型
|
||||
music_type?: MusicEntityType
|
||||
// 专辑预期总曲目数
|
||||
total_tracks?: number
|
||||
// 实际音频格式
|
||||
audio_format?: string
|
||||
// 是否无损音频
|
||||
audio_lossless?: boolean
|
||||
// 实际位深(bit)
|
||||
bit_depth?: number
|
||||
// 实际采样率(Hz)
|
||||
sample_rate?: number
|
||||
// 实际码率(bps)
|
||||
bitrate?: number
|
||||
// 源文件项
|
||||
src_fileitem?: FileItem
|
||||
}
|
||||
@@ -476,6 +508,22 @@ export interface MediaInfo {
|
||||
cover_url?: string
|
||||
// ListenBrainz 收听次数
|
||||
listen_count?: number
|
||||
// 音频格式
|
||||
audio_format?: string
|
||||
// 是否无损音频
|
||||
audio_lossless?: boolean
|
||||
// 音质等级
|
||||
audio_quality?: 'hires' | 'lossless' | 'lossy'
|
||||
// 音质洗版分数
|
||||
audio_quality_score?: number
|
||||
// 格式化音频参数
|
||||
audio_specs?: string
|
||||
// 音频位深
|
||||
bit_depth?: number
|
||||
// 音频采样率
|
||||
sample_rate?: number
|
||||
// 音频码率
|
||||
bitrate?: number
|
||||
}
|
||||
|
||||
// 音乐可浏览实体类型
|
||||
@@ -1209,6 +1257,14 @@ export interface MetaInfo {
|
||||
audio_encode?: string
|
||||
// 音频格式
|
||||
audio_format?: string
|
||||
// 是否无损音频
|
||||
audio_lossless?: boolean
|
||||
// 音质等级
|
||||
audio_quality?: 'hires' | 'lossless' | 'lossy'
|
||||
// 音质洗版分数
|
||||
audio_quality_score?: number
|
||||
// 格式化音频参数
|
||||
audio_specs?: string
|
||||
// 音频位深
|
||||
bit_depth?: number
|
||||
// 音频采样率
|
||||
|
||||
@@ -4,7 +4,7 @@ import type { Context } from '@/api/types'
|
||||
import { isNullOrEmptyObject } from '@/@core/utils'
|
||||
import { useGlobalSettingsStore } from '@/stores'
|
||||
import { getDisplayImageUrl } from '@/utils/imageUtils'
|
||||
import { formatMusicDuration } from '@/utils/music'
|
||||
import { formatMusicDuration, getMusicAudioSpecItems } from '@/utils/music'
|
||||
|
||||
const { t } = useI18n()
|
||||
const globalSettingsStore = useGlobalSettingsStore()
|
||||
@@ -100,29 +100,16 @@ const musicSummary = computed(() => {
|
||||
return values.join(' · ')
|
||||
})
|
||||
|
||||
// 采样率统一换算为常见的 kHz 展示。
|
||||
function formatSampleRate(sampleRate?: number) {
|
||||
if (!sampleRate) return ''
|
||||
const value = sampleRate >= 1000 ? sampleRate / 1000 : sampleRate
|
||||
return `${Number.isInteger(value) ? value : value.toFixed(1)} kHz`
|
||||
}
|
||||
|
||||
// 码率统一换算为 kbps 展示。
|
||||
function formatBitrate(bitrate?: number) {
|
||||
if (!bitrate) return ''
|
||||
const value = bitrate >= 1000 ? Math.round(bitrate / 1000) : bitrate
|
||||
return `${value} kbps`
|
||||
}
|
||||
|
||||
// 音频规格仅展示文件标签中实际存在的字段。
|
||||
// 本地文件实际参数优先,资源标题识别场景回退到标准音乐信息。
|
||||
const musicAudioChips = computed(() => {
|
||||
const metaInfo = props.context?.meta_info
|
||||
return [
|
||||
metaInfo?.audio_format?.toUpperCase(),
|
||||
metaInfo?.bit_depth ? `${metaInfo.bit_depth}-bit` : '',
|
||||
formatSampleRate(metaInfo?.sample_rate),
|
||||
formatBitrate(metaInfo?.bitrate),
|
||||
].filter(Boolean)
|
||||
const mediaInfo = props.context?.media_info
|
||||
return getMusicAudioSpecItems({
|
||||
audio_format: metaInfo?.audio_format || mediaInfo?.audio_format,
|
||||
bit_depth: metaInfo?.bit_depth || mediaInfo?.bit_depth,
|
||||
sample_rate: metaInfo?.sample_rate || mediaInfo?.sample_rate,
|
||||
bitrate: metaInfo?.bitrate || mediaInfo?.bitrate,
|
||||
})
|
||||
})
|
||||
|
||||
// 音乐详情外链
|
||||
|
||||
@@ -12,6 +12,7 @@ import {
|
||||
buildMusicDetailRoute,
|
||||
buildMusicResourceRoute,
|
||||
formatMusicDuration,
|
||||
formatMusicAudioSpecs,
|
||||
getMusicArtistLinks,
|
||||
getMusicKey,
|
||||
} from '@/utils/music'
|
||||
@@ -58,6 +59,8 @@ const metaItems = computed(() => {
|
||||
if (releaseDate) items.push({ icon: 'mdi-calendar-blank-outline', label: releaseDate })
|
||||
const duration = formatMusicDuration(props.music?.duration)
|
||||
if (duration) items.push({ icon: 'mdi-clock-outline', label: duration })
|
||||
const audioSpecs = formatMusicAudioSpecs(props.music)
|
||||
if (audioSpecs) items.push({ icon: 'mdi-waveform', label: audioSpecs })
|
||||
if (props.music?.track_number)
|
||||
items.push({
|
||||
hideOnNarrow: true,
|
||||
|
||||
@@ -11,7 +11,7 @@ import { useDisplay } from 'vuetify'
|
||||
import { useGlobalSettingsStore } from '@/stores'
|
||||
import { openSharedDialog } from '@/composables/useSharedDialog'
|
||||
import { getDisplayImageUrl } from '@/utils/imageUtils'
|
||||
import { buildMusicDetailRoute } from '@/utils/music'
|
||||
import { buildMusicDetailRoute, formatMusicAudioSpecs, formatMusicBitrate } from '@/utils/music'
|
||||
|
||||
const SubscribeEditDialog = defineAsyncComponent(() => import('../dialog/SubscribeEditDialog.vue'))
|
||||
const SubscribeFilesDialog = defineAsyncComponent(() => import('../dialog/SubscribeFilesDialog.vue'))
|
||||
@@ -139,19 +139,38 @@ const subscribeProgressText = computed(() => {
|
||||
// 音乐订阅始终展示实体类型;旧数据缺少 music_type 时按既有单曲语义兼容。
|
||||
const musicSubscribeMeta = computed(() => {
|
||||
if (props.media?.type !== '音乐') return null
|
||||
const currentSpecs = formatMusicAudioSpecs({
|
||||
audio_format: props.media.current_audio_format,
|
||||
bit_depth: props.media.current_bit_depth,
|
||||
sample_rate: props.media.current_sample_rate,
|
||||
bitrate: props.media.current_bitrate,
|
||||
})
|
||||
const selectedQuality = {
|
||||
hires: t('music.audioQualityHires'),
|
||||
'hires|lossless': t('music.audioQualityLossless'),
|
||||
lossy: t('music.audioQualityLossy'),
|
||||
}[props.media.audio_quality || '']
|
||||
const selectedFormat = props.media.audio_format
|
||||
? props.media.audio_format === 'DSD|FLAC|ALAC|APE|WAV|AIFF|PCM'
|
||||
? t('music.audioFormatLossless')
|
||||
: props.media.audio_format.replaceAll('|', '/')
|
||||
: ''
|
||||
const selectedBitrate = props.media.min_bitrate ? `≥ ${formatMusicBitrate(props.media.min_bitrate)}` : ''
|
||||
const qualityText = currentSpecs || [selectedQuality, selectedFormat, selectedBitrate].filter(Boolean).join(' · ')
|
||||
if (props.media.music_type === 'album') {
|
||||
const trackCount = props.media.total_tracks
|
||||
const entityText = trackCount
|
||||
? `${t('music.entityAlbum')} · ${t('music.trackCount', { count: trackCount })}`
|
||||
: t('music.entityAlbum')
|
||||
return {
|
||||
icon: 'mdi-album',
|
||||
text: trackCount
|
||||
? `${t('music.entityAlbum')} · ${t('music.trackCount', { count: trackCount })}`
|
||||
: t('music.entityAlbum'),
|
||||
text: [entityText, qualityText].filter(Boolean).join(' · '),
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
icon: 'mdi-music-note',
|
||||
text: t('music.entityRecording'),
|
||||
text: [t('music.entityRecording'), qualityText].filter(Boolean).join(' · '),
|
||||
}
|
||||
})
|
||||
|
||||
|
||||
@@ -50,7 +50,7 @@ describe('MediaInfoCard', () => {
|
||||
expect(screen.getByText('FLAC')).toBeInTheDocument()
|
||||
expect(screen.getByText('24-bit')).toBeInTheDocument()
|
||||
expect(screen.getByText('48 kHz')).toBeInTheDocument()
|
||||
expect(screen.getByText('1411 kbps')).toBeInTheDocument()
|
||||
expect(screen.getByText('1,411 kbps')).toBeInTheDocument()
|
||||
expect(screen.getByText('TWA530505002')).toBeInTheDocument()
|
||||
expect(document.querySelector('.v-img__img')).toHaveAttribute(
|
||||
'src',
|
||||
|
||||
@@ -194,6 +194,21 @@ describe('SubscribeCard display and progress', () => {
|
||||
expect(screen.queryByText(/^\d{1,4} \/ \d{1,4}$/)).not.toBeInTheDocument()
|
||||
})
|
||||
|
||||
it('shows the current music quality on a music subscription card', async () => {
|
||||
await renderCard({
|
||||
current_audio_format: 'FLAC',
|
||||
current_bit_depth: 24,
|
||||
current_bitrate: 2304000,
|
||||
current_sample_rate: 96000,
|
||||
music_type: 'album',
|
||||
name: '高解析专辑',
|
||||
total_tracks: 11,
|
||||
type: '音乐',
|
||||
})
|
||||
|
||||
expect(screen.getByText('专辑 · 11 首 · FLAC · 24-bit · 96 kHz · 2,304 kbps')).toBeInTheDocument()
|
||||
})
|
||||
|
||||
it.each([480, 1024])('identifies recording subscriptions at %ipx', async width => {
|
||||
setViewport(width)
|
||||
await renderCard({ music_type: 'recording', name: '晴天', total_tracks: undefined, type: '音乐' })
|
||||
|
||||
@@ -6,7 +6,16 @@ import type { DownloaderConf, FilterRuleGroup, Site, Subscribe, TransferDirector
|
||||
import { useDisplay } from 'vuetify'
|
||||
import { useConfirm } from '@/composables/useConfirm'
|
||||
import { useI18n } from 'vue-i18n'
|
||||
import { qualityOptions, resolutionOptions, effectOptions } from '@/api/constants'
|
||||
import {
|
||||
qualityOptions,
|
||||
resolutionOptions,
|
||||
effectOptions,
|
||||
audioQualityOptions,
|
||||
audioFormatOptions,
|
||||
audioBitrateOptions,
|
||||
audioBitDepthOptions,
|
||||
audioSampleRateOptions,
|
||||
} from '@/api/constants'
|
||||
import { useUserStore } from '@/stores'
|
||||
import { buildUserPermissionContext, hasPermission } from '@/utils/permission'
|
||||
import { formatSeason } from '@/@core/utils/formatters'
|
||||
@@ -217,7 +226,8 @@ async function saveDefaultSubscribeConfig() {
|
||||
try {
|
||||
let subscribe_config_url = ''
|
||||
if (props.type === '电影') subscribe_config_url = 'system/setting/DefaultMovieSubscribeConfig'
|
||||
else subscribe_config_url = 'system/setting/DefaultTvSubscribeConfig'
|
||||
else if (props.type === '电视剧') subscribe_config_url = 'system/setting/DefaultTvSubscribeConfig'
|
||||
else subscribe_config_url = 'system/setting/DefaultMusicSubscribeConfig'
|
||||
|
||||
const result: { [key: string]: any } = await api.post(subscribe_config_url, subscribeForm.value)
|
||||
if (result.success) {
|
||||
@@ -248,7 +258,8 @@ async function queryDefaultSubscribeConfig() {
|
||||
try {
|
||||
let subscribe_config_url = ''
|
||||
if (props.type === '电影') subscribe_config_url = 'system/setting/public/DefaultMovieSubscribeConfig'
|
||||
else subscribe_config_url = 'system/setting/public/DefaultTvSubscribeConfig'
|
||||
else if (props.type === '电视剧') subscribe_config_url = 'system/setting/public/DefaultTvSubscribeConfig'
|
||||
else subscribe_config_url = 'system/setting/public/DefaultMusicSubscribeConfig'
|
||||
|
||||
const result: { [key: string]: any } = await api.get(subscribe_config_url)
|
||||
|
||||
@@ -468,6 +479,67 @@ onMounted(() => {
|
||||
/>
|
||||
</VCol>
|
||||
</VRow>
|
||||
<template v-else>
|
||||
<VRow>
|
||||
<VCol cols="12" md="4">
|
||||
<VAutocomplete
|
||||
v-model="subscribeForm.audio_quality"
|
||||
:label="t('dialog.subscribeEdit.audioQuality')"
|
||||
:items="audioQualityOptions"
|
||||
:hint="t('dialog.subscribeEdit.audioQualityHint')"
|
||||
clearable
|
||||
persistent-hint
|
||||
prepend-inner-icon="mdi-waveform"
|
||||
/>
|
||||
</VCol>
|
||||
<VCol cols="12" md="4">
|
||||
<VAutocomplete
|
||||
v-model="subscribeForm.audio_format"
|
||||
:label="t('dialog.subscribeEdit.audioFormat')"
|
||||
:items="audioFormatOptions"
|
||||
:hint="t('dialog.subscribeEdit.audioFormatHint')"
|
||||
clearable
|
||||
persistent-hint
|
||||
prepend-inner-icon="mdi-file-music-outline"
|
||||
/>
|
||||
</VCol>
|
||||
<VCol cols="12" md="4">
|
||||
<VAutocomplete
|
||||
v-model="subscribeForm.min_bitrate"
|
||||
:label="t('dialog.subscribeEdit.minBitrate')"
|
||||
:items="audioBitrateOptions"
|
||||
:hint="t('dialog.subscribeEdit.minBitrateHint')"
|
||||
clearable
|
||||
persistent-hint
|
||||
prepend-inner-icon="mdi-speedometer"
|
||||
/>
|
||||
</VCol>
|
||||
</VRow>
|
||||
<VRow>
|
||||
<VCol cols="12" md="6">
|
||||
<VAutocomplete
|
||||
v-model="subscribeForm.min_bit_depth"
|
||||
:label="t('dialog.subscribeEdit.minBitDepth')"
|
||||
:items="audioBitDepthOptions"
|
||||
:hint="t('dialog.subscribeEdit.minBitDepthHint')"
|
||||
clearable
|
||||
persistent-hint
|
||||
prepend-inner-icon="mdi-numeric"
|
||||
/>
|
||||
</VCol>
|
||||
<VCol cols="12" md="6">
|
||||
<VAutocomplete
|
||||
v-model="subscribeForm.min_sample_rate"
|
||||
:label="t('dialog.subscribeEdit.minSampleRate')"
|
||||
:items="audioSampleRateOptions"
|
||||
:hint="t('dialog.subscribeEdit.minSampleRateHint')"
|
||||
clearable
|
||||
persistent-hint
|
||||
prepend-inner-icon="mdi-sine-wave"
|
||||
/>
|
||||
</VCol>
|
||||
</VRow>
|
||||
</template>
|
||||
<VRow>
|
||||
<VCol cols="12">
|
||||
<VAutocomplete
|
||||
@@ -505,12 +577,16 @@ onMounted(() => {
|
||||
/>
|
||||
</VCol>
|
||||
</VRow>
|
||||
<VRow v-if="!isMusicSubscribe">
|
||||
<VRow>
|
||||
<VCol cols="12" md="4">
|
||||
<VSwitch
|
||||
v-model="subscribeForm.best_version"
|
||||
:label="t('dialog.subscribeEdit.bestVersion')"
|
||||
:hint="t('dialog.subscribeEdit.bestVersionHint')"
|
||||
:hint="
|
||||
isMusicSubscribe
|
||||
? t('dialog.subscribeEdit.musicBestVersionHint')
|
||||
: t('dialog.subscribeEdit.bestVersionHint')
|
||||
"
|
||||
persistent-hint
|
||||
/>
|
||||
</VCol>
|
||||
@@ -522,7 +598,7 @@ onMounted(() => {
|
||||
persistent-hint
|
||||
/>
|
||||
</VCol>
|
||||
<VCol cols="12" md="4">
|
||||
<VCol v-if="!isMusicSubscribe" cols="12" md="4">
|
||||
<VSwitch
|
||||
v-model="subscribeForm.search_imdbid"
|
||||
:label="t('dialog.subscribeEdit.searchImdbid')"
|
||||
|
||||
@@ -219,32 +219,80 @@ describe('SubscribeEditDialog', () => {
|
||||
expect(events.save).not.toHaveBeenCalled()
|
||||
})
|
||||
|
||||
it.each(['电影', '电视剧'] as const)('loads and saves %s default configuration as an administrator', async type => {
|
||||
const configRequested = vi.fn()
|
||||
const saved = vi.fn()
|
||||
server.use(
|
||||
defaultSubscribeConfigHandler(
|
||||
type,
|
||||
createSubscribe({ id: 0, show_edit_dialog: false, type }),
|
||||
200,
|
||||
configRequested,
|
||||
),
|
||||
saveDefaultSubscribeConfigHandler(type, { success: true }, 200, saved),
|
||||
)
|
||||
useDialogOptions()
|
||||
const user = userEvent.setup()
|
||||
const { events } = await renderDialog({ default: true, type })
|
||||
await waitFor(() => expect(configRequested).toHaveBeenCalledOnce())
|
||||
await waitFor(() => expect(screen.getByLabelText('订阅时编辑更多规则')).not.toBeChecked())
|
||||
it.each(['电影', '电视剧', '音乐'] as const)(
|
||||
'loads and saves %s default configuration as an administrator',
|
||||
async type => {
|
||||
const configRequested = vi.fn()
|
||||
const saved = vi.fn()
|
||||
server.use(
|
||||
defaultSubscribeConfigHandler(
|
||||
type,
|
||||
createSubscribe({ id: 0, show_edit_dialog: false, type }),
|
||||
200,
|
||||
configRequested,
|
||||
),
|
||||
saveDefaultSubscribeConfigHandler(type, { success: true }, 200, saved),
|
||||
)
|
||||
useDialogOptions()
|
||||
const user = userEvent.setup()
|
||||
const { events } = await renderDialog({ default: true, type })
|
||||
await waitFor(() => expect(configRequested).toHaveBeenCalledOnce())
|
||||
await waitFor(() => expect(screen.getByLabelText('订阅时编辑更多规则')).not.toBeChecked())
|
||||
|
||||
await user.click(screen.getByLabelText('订阅时编辑更多规则'))
|
||||
await user.click(screen.getByLabelText('订阅时编辑更多规则'))
|
||||
|
||||
await fireEvent.click(screen.getByRole('button', { name: '保存' }))
|
||||
|
||||
await waitFor(() => expect(saved).toHaveBeenCalledOnce())
|
||||
expect(saved.mock.calls[0][0]).toMatchObject({ show_edit_dialog: true, type })
|
||||
expect(events.save).toHaveBeenCalledOnce()
|
||||
expect(mocks.toastSuccess).toHaveBeenCalledWith(`${type}订阅默认规则保存成功`)
|
||||
},
|
||||
)
|
||||
|
||||
it('loads and submits music quality filters and quality upgrades', async () => {
|
||||
const record = createSubscribe({
|
||||
audio_format: 'FLAC',
|
||||
audio_quality: 'hires',
|
||||
best_version: 1,
|
||||
current_audio_format: 'MP3',
|
||||
current_bitrate: 320000,
|
||||
id: 811,
|
||||
min_bit_depth: 24,
|
||||
min_bitrate: 320000,
|
||||
min_sample_rate: 96000,
|
||||
music_type: 'album',
|
||||
name: '音乐音质测试专辑',
|
||||
tmdbid: 0,
|
||||
type: '音乐',
|
||||
})
|
||||
const updated = vi.fn()
|
||||
server.use(subscribeDetailsHandler(811, record), updateSubscribeHandler({ success: true }, 200, updated))
|
||||
useDialogOptions()
|
||||
await renderDialog({ subid: 811 })
|
||||
|
||||
expect(await screen.findByText('音乐音质测试专辑')).toBeInTheDocument()
|
||||
expect(screen.getByLabelText('音质等级')).toBeInTheDocument()
|
||||
expect(screen.getByLabelText('音频格式')).toBeInTheDocument()
|
||||
expect(screen.getByLabelText('最低码率')).toBeInTheDocument()
|
||||
expect(screen.getByLabelText('最低位深')).toBeInTheDocument()
|
||||
expect(screen.getByLabelText('最低采样率')).toBeInTheDocument()
|
||||
expect(screen.getByLabelText('洗版')).toBeChecked()
|
||||
expect(screen.queryByLabelText('使用 ImdbID 搜索')).not.toBeInTheDocument()
|
||||
expect(screen.queryByLabelText('质量')).not.toBeInTheDocument()
|
||||
|
||||
await fireEvent.click(screen.getByRole('button', { name: '保存' }))
|
||||
|
||||
await waitFor(() => expect(saved).toHaveBeenCalledOnce())
|
||||
expect(saved.mock.calls[0][0]).toMatchObject({ show_edit_dialog: true, type })
|
||||
expect(events.save).toHaveBeenCalledOnce()
|
||||
expect(mocks.toastSuccess).toHaveBeenCalledWith(`${type}订阅默认规则保存成功`)
|
||||
await waitFor(() => expect(updated).toHaveBeenCalledOnce())
|
||||
expect(updated.mock.calls[0][0]).toMatchObject({
|
||||
audio_format: 'FLAC',
|
||||
audio_quality: 'hires',
|
||||
best_version: true,
|
||||
min_bit_depth: 24,
|
||||
min_bitrate: 320000,
|
||||
min_sample_rate: 96000,
|
||||
type: '音乐',
|
||||
})
|
||||
})
|
||||
|
||||
it('submits the complete TV editing form and exposes the close action', async () => {
|
||||
|
||||
@@ -274,7 +274,10 @@ describe('useMediaSubscribe entry flows', () => {
|
||||
year: '2003',
|
||||
})
|
||||
const created = vi.fn()
|
||||
server.use(createSubscribeHandler({ data: { id: 502 }, success: true }, 200, created))
|
||||
server.use(
|
||||
createSubscribeHandler({ data: { id: 502 }, success: true }, 200, created),
|
||||
defaultSubscribeConfigHandler('音乐', { show_edit_dialog: false }),
|
||||
)
|
||||
await renderSubscribeHarness({ media })
|
||||
|
||||
await fireEvent.click(screen.getByRole('button', { name: 'primary' }))
|
||||
@@ -305,7 +308,10 @@ describe('useMediaSubscribe entry flows', () => {
|
||||
year: '2003',
|
||||
})
|
||||
const created = vi.fn()
|
||||
server.use(createSubscribeHandler({ data: { id: 503 }, success: true }, 200, created))
|
||||
server.use(
|
||||
createSubscribeHandler({ data: { id: 503 }, success: true }, 200, created),
|
||||
defaultSubscribeConfigHandler('音乐', { show_edit_dialog: false }),
|
||||
)
|
||||
await renderSubscribeHarness({ media })
|
||||
|
||||
await fireEvent.click(screen.getByRole('button', { name: 'primary' }))
|
||||
|
||||
@@ -273,14 +273,15 @@ export function useMediaSubscribe(options: UseMediaSubscribeOptions) {
|
||||
// 查询系统默认订阅配置。
|
||||
async function queryDefaultSubscribeConfig(): Promise<SubscribeConfig | undefined> {
|
||||
if (!options.canSubscribe()) return undefined
|
||||
if (currentMedia()?.type === '音乐') return undefined
|
||||
|
||||
try {
|
||||
const media = currentMedia()
|
||||
const subscribeConfigUrl =
|
||||
media?.type === '电影'
|
||||
? 'system/setting/public/DefaultMovieSubscribeConfig'
|
||||
: 'system/setting/public/DefaultTvSubscribeConfig'
|
||||
const subscribeConfigUrl = {
|
||||
电影: 'system/setting/public/DefaultMovieSubscribeConfig',
|
||||
电视剧: 'system/setting/public/DefaultTvSubscribeConfig',
|
||||
音乐: 'system/setting/public/DefaultMusicSubscribeConfig',
|
||||
}[media?.type || '']
|
||||
if (!subscribeConfigUrl) return undefined
|
||||
const result: { [key: string]: any } = await api.get(subscribeConfigUrl)
|
||||
|
||||
return result.data?.value
|
||||
|
||||
@@ -128,6 +128,16 @@ export default {
|
||||
h265: 'H265',
|
||||
h264: 'H264',
|
||||
},
|
||||
audioQualityOptions: {
|
||||
all: 'All Audio Qualities',
|
||||
hires: 'Hi-Res Lossless',
|
||||
lossless: 'Lossless (including Hi-Res)',
|
||||
lossy: 'Lossy',
|
||||
},
|
||||
audioFormatOptions: {
|
||||
all: 'All Formats',
|
||||
lossless: 'All Lossless Formats',
|
||||
},
|
||||
resolutionOptions: {
|
||||
all: 'All',
|
||||
'4k': '4K',
|
||||
@@ -445,6 +455,10 @@ export default {
|
||||
entityRecording: 'Track',
|
||||
entityAlbum: 'Album',
|
||||
entityArtist: 'Artist',
|
||||
audioQualityHires: 'Hi-Res',
|
||||
audioQualityLossless: 'Lossless',
|
||||
audioQualityLossy: 'Lossy',
|
||||
audioFormatLossless: 'Lossless Formats',
|
||||
gender: 'Gender',
|
||||
area: 'Area',
|
||||
lifeSpan: 'Active Period',
|
||||
@@ -1501,6 +1515,19 @@ export default {
|
||||
hkVoi: 'Cantonese Dubbing',
|
||||
notHkVoi: 'Exclude: Cantonese Dubbing',
|
||||
free: 'Promotion: Free',
|
||||
audioHires: 'Audio Quality: Hi-Res',
|
||||
audioLossless: 'Audio Quality: Lossless',
|
||||
audioFlac: 'Audio Format: FLAC',
|
||||
audioAlac: 'Audio Format: ALAC',
|
||||
audioApe: 'Audio Format: APE',
|
||||
audioWav: 'Audio Format: WAV',
|
||||
audioDsd: 'Audio Format: DSD',
|
||||
audioMp3: 'Audio Format: MP3',
|
||||
audioAac: 'Audio Format: AAC',
|
||||
audioOpus: 'Audio Format: OPUS',
|
||||
bitrate320: 'Minimum Bitrate: 320 kbps',
|
||||
bitrate256: 'Minimum Bitrate: 256 kbps',
|
||||
bitrate192: 'Minimum Bitrate: 192 kbps',
|
||||
resolution4k: 'Resolution: 4K',
|
||||
resolution1080p: 'Resolution: 1080P',
|
||||
resolution720p: 'Resolution: 720P',
|
||||
@@ -3378,6 +3405,16 @@ export default {
|
||||
resolutionHint: 'Subscription resource resolution',
|
||||
effect: 'Effects',
|
||||
effectHint: 'Subscription resource effects',
|
||||
audioQuality: 'Audio Quality',
|
||||
audioQualityHint: 'Filter by Hi-Res, lossless, or lossy tier',
|
||||
audioFormat: 'Audio Format',
|
||||
audioFormatHint: 'Filter by FLAC, ALAC, DSD, MP3, and other formats',
|
||||
minBitrate: 'Minimum Bitrate',
|
||||
minBitrateHint: 'Resource titles must declare at least this bitrate',
|
||||
minBitDepth: 'Minimum Bit Depth',
|
||||
minBitDepthHint: 'Resource titles must declare at least this bit depth',
|
||||
minSampleRate: 'Minimum Sample Rate',
|
||||
minSampleRateHint: 'Resource titles must declare at least this sample rate',
|
||||
subscribeSites: 'Subscription Sites',
|
||||
subscribeSitesHint: 'Range of sites for subscription, use system settings if none selected',
|
||||
downloader: 'Downloader',
|
||||
@@ -3386,6 +3423,7 @@ export default {
|
||||
savePathHint: 'Specify download save path for this subscription, leave empty to use default download directory',
|
||||
bestVersion: 'Version Upgrade',
|
||||
bestVersionHint: 'Perform version upgrade subscription based on upgrade priorities',
|
||||
musicBestVersionHint: 'Keep downloading higher-scoring audio versions until top-tier Hi-Res is reached',
|
||||
bestVersionFull: 'Full Season Upgrade',
|
||||
bestVersionFullHint: 'Only download full-season packs and do not split packs by episode',
|
||||
searchImdbid: 'Search Using ImdbID',
|
||||
|
||||
@@ -126,6 +126,16 @@ export default {
|
||||
h265: 'H265',
|
||||
h264: 'H264',
|
||||
},
|
||||
audioQualityOptions: {
|
||||
all: '全部音质',
|
||||
hires: 'Hi-Res 高解析无损',
|
||||
lossless: '无损(含 Hi-Res)',
|
||||
lossy: '有损',
|
||||
},
|
||||
audioFormatOptions: {
|
||||
all: '全部格式',
|
||||
lossless: '全部无损格式',
|
||||
},
|
||||
resolutionOptions: {
|
||||
all: '全部',
|
||||
'4k': '4k',
|
||||
@@ -437,6 +447,10 @@ export default {
|
||||
entityRecording: '单曲',
|
||||
entityAlbum: '专辑',
|
||||
entityArtist: '艺术家',
|
||||
audioQualityHires: 'Hi-Res',
|
||||
audioQualityLossless: '无损',
|
||||
audioQualityLossy: '有损',
|
||||
audioFormatLossless: '无损格式',
|
||||
gender: '性别',
|
||||
area: '地区',
|
||||
lifeSpan: '活跃时间',
|
||||
@@ -1491,6 +1505,19 @@ export default {
|
||||
hkVoi: '粤语配音',
|
||||
notHkVoi: '排除: 粤语配音',
|
||||
free: '促销: 免费',
|
||||
audioHires: '音质: Hi-Res',
|
||||
audioLossless: '音质: 无损',
|
||||
audioFlac: '音频格式: FLAC',
|
||||
audioAlac: '音频格式: ALAC',
|
||||
audioApe: '音频格式: APE',
|
||||
audioWav: '音频格式: WAV',
|
||||
audioDsd: '音频格式: DSD',
|
||||
audioMp3: '音频格式: MP3',
|
||||
audioAac: '音频格式: AAC',
|
||||
audioOpus: '音频格式: OPUS',
|
||||
bitrate320: '最低码率: 320 kbps',
|
||||
bitrate256: '最低码率: 256 kbps',
|
||||
bitrate192: '最低码率: 192 kbps',
|
||||
resolution4k: '分辨率: 4K',
|
||||
resolution1080p: '分辨率: 1080P',
|
||||
resolution720p: '分辨率: 720P',
|
||||
@@ -3320,6 +3347,16 @@ export default {
|
||||
resolutionHint: '订阅资源分辨率',
|
||||
effect: '特效',
|
||||
effectHint: '订阅资源特效',
|
||||
audioQuality: '音质等级',
|
||||
audioQualityHint: '按 Hi-Res、无损或有损等级筛选',
|
||||
audioFormat: '音频格式',
|
||||
audioFormatHint: '按 FLAC、ALAC、DSD、MP3 等格式筛选',
|
||||
minBitrate: '最低码率',
|
||||
minBitrateHint: '资源标题必须声明不低于该码率',
|
||||
minBitDepth: '最低位深',
|
||||
minBitDepthHint: '资源标题必须声明不低于该位深',
|
||||
minSampleRate: '最低采样率',
|
||||
minSampleRateHint: '资源标题必须声明不低于该采样率',
|
||||
subscribeSites: '订阅站点',
|
||||
subscribeSitesHint: '订阅的站点范围,不选使用系统设置',
|
||||
downloader: '下载器',
|
||||
@@ -3328,6 +3365,7 @@ export default {
|
||||
savePathHint: '指定该订阅的下载保存路径,留空自动使用设定的下载目录',
|
||||
bestVersion: '洗版',
|
||||
bestVersionHint: '根据洗版优先级进行洗版订阅',
|
||||
musicBestVersionHint: '持续下载音质分数更高的版本,达到顶级 Hi-Res 后完成',
|
||||
bestVersionFull: '全集洗版',
|
||||
bestVersionFullHint: '只下载覆盖全集的整包资源,不按单集拆包下载',
|
||||
searchImdbid: '使用 ImdbID 搜索',
|
||||
|
||||
@@ -126,6 +126,16 @@ export default {
|
||||
h265: 'H265',
|
||||
h264: 'H264',
|
||||
},
|
||||
audioQualityOptions: {
|
||||
all: '全部音質',
|
||||
hires: 'Hi-Res 高解析無損',
|
||||
lossless: '無損(含 Hi-Res)',
|
||||
lossy: '有損',
|
||||
},
|
||||
audioFormatOptions: {
|
||||
all: '全部格式',
|
||||
lossless: '全部無損格式',
|
||||
},
|
||||
resolutionOptions: {
|
||||
all: '全部',
|
||||
'4k': '4k',
|
||||
@@ -437,6 +447,10 @@ export default {
|
||||
entityRecording: '單曲',
|
||||
entityAlbum: '專輯',
|
||||
entityArtist: '藝術家',
|
||||
audioQualityHires: 'Hi-Res',
|
||||
audioQualityLossless: '無損',
|
||||
audioQualityLossy: '有損',
|
||||
audioFormatLossless: '無損格式',
|
||||
gender: '性別',
|
||||
area: '地區',
|
||||
lifeSpan: '活躍時間',
|
||||
@@ -1490,6 +1504,19 @@ export default {
|
||||
hkVoi: '粵語配音',
|
||||
notHkVoi: '排除: 粵語配音',
|
||||
free: '促銷: 免費',
|
||||
audioHires: '音質: Hi-Res',
|
||||
audioLossless: '音質: 無損',
|
||||
audioFlac: '音訊格式: FLAC',
|
||||
audioAlac: '音訊格式: ALAC',
|
||||
audioApe: '音訊格式: APE',
|
||||
audioWav: '音訊格式: WAV',
|
||||
audioDsd: '音訊格式: DSD',
|
||||
audioMp3: '音訊格式: MP3',
|
||||
audioAac: '音訊格式: AAC',
|
||||
audioOpus: '音訊格式: OPUS',
|
||||
bitrate320: '最低碼率: 320 kbps',
|
||||
bitrate256: '最低碼率: 256 kbps',
|
||||
bitrate192: '最低碼率: 192 kbps',
|
||||
resolution4k: '解析度: 4K',
|
||||
resolution1080p: '解析度: 1080P',
|
||||
resolution720p: '解析度: 720P',
|
||||
@@ -3319,6 +3346,16 @@ export default {
|
||||
resolutionHint: '訂閱資源分辨率',
|
||||
effect: '特效',
|
||||
effectHint: '訂閱資源特效',
|
||||
audioQuality: '音質等級',
|
||||
audioQualityHint: '按 Hi-Res、無損或有損等級篩選',
|
||||
audioFormat: '音訊格式',
|
||||
audioFormatHint: '按 FLAC、ALAC、DSD、MP3 等格式篩選',
|
||||
minBitrate: '最低碼率',
|
||||
minBitrateHint: '資源標題必須聲明不低於該碼率',
|
||||
minBitDepth: '最低位深',
|
||||
minBitDepthHint: '資源標題必須聲明不低於該位深',
|
||||
minSampleRate: '最低採樣率',
|
||||
minSampleRateHint: '資源標題必須聲明不低於該採樣率',
|
||||
subscribeSites: '訂閱站點',
|
||||
subscribeSitesHint: '訂閱的站點範圍,不選使用系統設置',
|
||||
downloader: '下載器',
|
||||
@@ -3327,6 +3364,7 @@ export default {
|
||||
savePathHint: '指定該訂閱的下載儲存路徑,留空自動使用設定的下載目錄',
|
||||
bestVersion: '洗版',
|
||||
bestVersionHint: '根據洗版優先級進行洗版訂閱',
|
||||
musicBestVersionHint: '持續下載音質分數更高的版本,達到頂級 Hi-Res 後完成',
|
||||
bestVersionFull: '全集洗版',
|
||||
bestVersionFullHint: '只下載覆蓋全集的整包資源,不按單集拆包下載',
|
||||
searchImdbid: '使用 ImdbID 搜索',
|
||||
|
||||
@@ -17,6 +17,46 @@ export interface MusicArtistLink {
|
||||
id?: string
|
||||
}
|
||||
|
||||
export interface MusicAudioInfo {
|
||||
audio_format?: string
|
||||
audio_lossless?: boolean
|
||||
audio_quality?: 'hires' | 'lossless' | 'lossy'
|
||||
audio_specs?: string
|
||||
bit_depth?: number
|
||||
sample_rate?: number
|
||||
bitrate?: number
|
||||
}
|
||||
|
||||
/** 将采样率统一换算为 kHz。 */
|
||||
export function formatMusicSampleRate(sampleRate?: number): string {
|
||||
if (!sampleRate) return ''
|
||||
const value = sampleRate >= 1000 ? sampleRate / 1000 : sampleRate
|
||||
return `${Number.isInteger(value) ? value : value.toFixed(1)} kHz`
|
||||
}
|
||||
|
||||
/** 将码率统一换算为 kbps。 */
|
||||
export function formatMusicBitrate(bitrate?: number): string {
|
||||
if (!bitrate) return ''
|
||||
const value = bitrate >= 1000 ? Math.round(bitrate / 1000) : bitrate
|
||||
return `${value.toLocaleString()} kbps`
|
||||
}
|
||||
|
||||
/** 返回识别卡、音乐卡和历史记录共用的音频规格片段。 */
|
||||
export function getMusicAudioSpecItems(item?: MusicAudioInfo): string[] {
|
||||
if (!item) return []
|
||||
return [
|
||||
item.audio_format?.toUpperCase(),
|
||||
item.bit_depth ? `${item.bit_depth}-bit` : '',
|
||||
formatMusicSampleRate(item.sample_rate),
|
||||
formatMusicBitrate(item.bitrate),
|
||||
].filter((value): value is string => Boolean(value))
|
||||
}
|
||||
|
||||
/** 返回紧凑的完整音频规格文本,优先使用后端规范化结果。 */
|
||||
export function formatMusicAudioSpecs(item?: MusicAudioInfo): string {
|
||||
return item?.audio_specs || getMusicAudioSpecItems(item).join(' · ')
|
||||
}
|
||||
|
||||
/** 返回音乐对象可用于路由和订阅的统一来源。 */
|
||||
export function getMusicSource(item: MusicRouteTarget): string | undefined {
|
||||
return item.source || item.media_source
|
||||
|
||||
@@ -4,6 +4,7 @@ import type { TransferHistory } from '@/api/types'
|
||||
import noImage from '@images/no-image.jpeg'
|
||||
import { formatDateDifference, formatFileSize } from '@/@core/utils/formatters'
|
||||
import { useI18n } from 'vue-i18n'
|
||||
import { formatMusicAudioSpecs } from '@/utils/music'
|
||||
|
||||
const { t } = useI18n()
|
||||
|
||||
@@ -32,6 +33,7 @@ function getPosterUrl(item: TransferHistory) {
|
||||
/** 组合媒体类型、季集和文件大小作为记录副标题。 */
|
||||
function getImportMeta(item: TransferHistory) {
|
||||
const values = [item.type, item.seasons, item.episodes]
|
||||
if (item.type === '音乐') values.push(formatMusicAudioSpecs(item))
|
||||
const fileSize = Number(item.src_fileitem?.size ?? 0)
|
||||
if (fileSize > 0) values.push(formatFileSize(fileSize))
|
||||
|
||||
|
||||
@@ -30,4 +30,26 @@ describe('dashboard recent imports', () => {
|
||||
const renderedItem = await screen.findByText('异步入库记录')
|
||||
expect(container.querySelector('[data-layout-size-source]')).toContainElement(renderedItem)
|
||||
})
|
||||
|
||||
it('shows normalized audio specs for recent music imports', async () => {
|
||||
mocks.apiGet.mockResolvedValue({
|
||||
data: {
|
||||
list: [
|
||||
{
|
||||
id: 2,
|
||||
title: '晴天',
|
||||
type: '音乐',
|
||||
audio_format: 'FLAC',
|
||||
bit_depth: 24,
|
||||
sample_rate: 96_000,
|
||||
bitrate: 2_304_000,
|
||||
},
|
||||
],
|
||||
},
|
||||
})
|
||||
|
||||
await renderWithProviders(DashboardRecentImports)
|
||||
|
||||
expect(await screen.findByText(/FLAC · 24-bit · 96 kHz · 2,304 kbps/)).toBeInTheDocument()
|
||||
})
|
||||
})
|
||||
|
||||
@@ -19,6 +19,7 @@ import { useGlobalSettingsStore, useUserStore } from '@/stores'
|
||||
import { openSharedDialog } from '@/composables/useSharedDialog'
|
||||
import { buildUserPermissionContext, hasPermission } from '@/utils/permission'
|
||||
import { getDisplayImageUrl } from '@/utils/imageUtils'
|
||||
import { formatMusicAudioSpecs } from '@/utils/music'
|
||||
|
||||
const TransferHistoryDeleteDialog = defineAsyncComponent(
|
||||
() => import('@/components/dialog/TransferHistoryDeleteDialog.vue'),
|
||||
@@ -1091,7 +1092,7 @@ function getHistoryDisplayTitle(item: TransferHistory) {
|
||||
|
||||
// 获取移动端卡片副标题,优先展示二级分类和年份。
|
||||
function getHistorySubtitle(item: TransferHistory) {
|
||||
return [item.category, item.year].filter(Boolean).join(' / ')
|
||||
return [item.category, item.year, item.type === '音乐' ? formatMusicAudioSpecs(item) : ''].filter(Boolean).join(' / ')
|
||||
}
|
||||
|
||||
// 获取存储展示名称,配置缺失时回退到原始存储标识。
|
||||
|
||||
@@ -530,6 +530,28 @@ describe('TransferHistoryView', () => {
|
||||
expect(screen.getByRole('button', { name: '批量选择' })).toBeInTheDocument()
|
||||
})
|
||||
|
||||
it('shows actual audio specs in mobile music history', async () => {
|
||||
mocks.desktop = false
|
||||
const item = createHistory(2, '晴天', {
|
||||
audio_format: 'FLAC',
|
||||
bit_depth: 24,
|
||||
bitrate: 2_304_000,
|
||||
category: '华语流行',
|
||||
sample_rate: 96_000,
|
||||
type: '音乐',
|
||||
year: '2003',
|
||||
})
|
||||
mocks.apiGet.mockImplementation((path: string) => {
|
||||
if (path === 'system/setting/public/Storages') return Promise.resolve(storageResponse())
|
||||
return Promise.resolve(historyResponse([item]))
|
||||
})
|
||||
|
||||
await renderHistory()
|
||||
await fireEvent.click(screen.getByRole('button', { name: '加载下一页' }))
|
||||
|
||||
expect(await screen.findByText('华语流行 / 2003 / FLAC · 24-bit · 96 kHz · 2,304 kbps')).toBeInTheDocument()
|
||||
})
|
||||
|
||||
it('prevents a mobile request invalidated by a route reset from appending stale records', async () => {
|
||||
mocks.desktop = false
|
||||
const oldRequest = createDeferred<ReturnType<typeof historyResponse>>()
|
||||
|
||||
@@ -12,7 +12,7 @@ import { HttpResponse, http, type JsonBodyType, type RequestHandler } from 'msw'
|
||||
|
||||
const API_BASE_URL = 'http://localhost/api/v1/'
|
||||
|
||||
export type SubscribeMediaType = '电影' | '电视剧'
|
||||
export type SubscribeMediaType = '电影' | '电视剧' | '音乐'
|
||||
|
||||
export interface SubscribeMutationResponse {
|
||||
success: boolean
|
||||
@@ -24,7 +24,13 @@ export const subscribeApiUrls = {
|
||||
create: new URL('subscribe/', API_BASE_URL).href,
|
||||
defaultConfig: (type: SubscribeMediaType, writable = false) =>
|
||||
new URL(
|
||||
`system/setting/${writable ? '' : 'public/'}${type === '电影' ? 'DefaultMovieSubscribeConfig' : 'DefaultTvSubscribeConfig'}`,
|
||||
`system/setting/${writable ? '' : 'public/'}${
|
||||
{
|
||||
电影: 'DefaultMovieSubscribeConfig',
|
||||
电视剧: 'DefaultTvSubscribeConfig',
|
||||
音乐: 'DefaultMusicSubscribeConfig',
|
||||
}[type]
|
||||
}`,
|
||||
API_BASE_URL,
|
||||
).href,
|
||||
deleteById: (id: number) => new URL(`subscribe/${id}`, API_BASE_URL).href,
|
||||
@@ -43,7 +49,10 @@ export const subscribeApiUrls = {
|
||||
queryByMedia: (mediaId: string) => new URL(`subscribe/media/${mediaId}`, API_BASE_URL).href,
|
||||
list: new URL('subscribe/', API_BASE_URL).href,
|
||||
orderConfig: (type: SubscribeMediaType) =>
|
||||
new URL(`user/config/${type === '电影' ? 'SubscribeMovieOrder' : 'SubscribeTvOrder'}`, API_BASE_URL).href,
|
||||
new URL(
|
||||
`user/config/${{ 电影: 'SubscribeMovieOrder', 电视剧: 'SubscribeTvOrder', 音乐: 'SubscribeMusicOrder' }[type]}`,
|
||||
API_BASE_URL,
|
||||
).href,
|
||||
popular: new URL('subscribe/popular', API_BASE_URL).href,
|
||||
resetById: (id: number) => new URL(`subscribe/reset/${id}`, API_BASE_URL).href,
|
||||
searchById: (id: number) => new URL(`subscribe/search/${id}`, API_BASE_URL).href,
|
||||
@@ -318,7 +327,12 @@ export function deleteSubscribeByMediaHandler(
|
||||
})
|
||||
}
|
||||
|
||||
export function subscribeDetailsHandler(id: number, subscribe: Subscribe, status = 200, onRequest: () => void = () => {}) {
|
||||
export function subscribeDetailsHandler(
|
||||
id: number,
|
||||
subscribe: Subscribe,
|
||||
status = 200,
|
||||
onRequest: () => void = () => {},
|
||||
) {
|
||||
return http.get(subscribeApiUrls.details(id), () => {
|
||||
onRequest()
|
||||
return jsonResponse(subscribe as unknown as JsonBodyType, status)
|
||||
|
||||
Reference in New Issue
Block a user