mirror of
https://github.com/jxxghp/MoviePilot-Frontend.git
synced 2026-09-07 00:36:41 +08:00
feat: 完善音乐识别结果弹窗
This commit is contained in:
@@ -2,6 +2,9 @@
|
||||
import type { PropType } from 'vue'
|
||||
import type { Context } from '@/api/types'
|
||||
import { isNullOrEmptyObject } from '@/@core/utils'
|
||||
import { formatMusicDuration } from '@/utils/music'
|
||||
|
||||
const { t } = useI18n()
|
||||
|
||||
// 输入参数
|
||||
const props = defineProps({
|
||||
@@ -16,6 +19,9 @@ const isMusic = computed(
|
||||
() => props.context?.media_info?.type === '音乐' || props.context?.meta_info?.type === '音乐',
|
||||
)
|
||||
|
||||
// 音乐封面加载失败时保留方形唱片占位,避免弹窗信息结构塌缩。
|
||||
const musicCoverLoadError = ref(false)
|
||||
|
||||
// TMDB图片转换为w500大小
|
||||
function getW500Image(url = '') {
|
||||
if (!url) return ''
|
||||
@@ -30,19 +36,81 @@ function openTmdbPage(type: string, tmdbId: number) {
|
||||
window.open(url, '_blank')
|
||||
}
|
||||
|
||||
// 秒数格式化为 m:ss 时长文本
|
||||
function formatDuration(seconds: number | undefined) {
|
||||
if (!seconds || seconds <= 0) return ''
|
||||
const minutes = Math.floor(seconds / 60)
|
||||
const rest = seconds % 60
|
||||
return `${minutes}:${rest.toString().padStart(2, '0')}`
|
||||
}
|
||||
|
||||
// 音乐封面优先使用方形封面,仅 W500 图片处理对TMDB类 URL 生效
|
||||
const musicCover = computed(() => getW500Image(
|
||||
props.context?.media_info?.cover_url || props.context?.media_info?.poster_path || '',
|
||||
))
|
||||
|
||||
// 音乐艺术家优先使用标准识别结果,远端未命中时回退到文件标签。
|
||||
const musicArtist = computed(() => {
|
||||
const mediaInfo = props.context?.media_info
|
||||
const metaInfo = props.context?.meta_info
|
||||
return mediaInfo?.artist || mediaInfo?.artists?.join(' / ') || metaInfo?.artist || metaInfo?.artists?.join(' / ') || ''
|
||||
})
|
||||
|
||||
// 专辑艺术家只在与歌曲艺术家不同时单独展示。
|
||||
const musicAlbumArtist = computed(() => {
|
||||
const albumArtist = props.context?.media_info?.album_artist || props.context?.meta_info?.album_artist
|
||||
return albumArtist && albumArtist !== musicArtist.value ? albumArtist : ''
|
||||
})
|
||||
|
||||
// 音乐专辑优先使用标准识别结果,远端未命中时回退到文件标签。
|
||||
const musicAlbum = computed(() => props.context?.media_info?.album || props.context?.meta_info?.album || '')
|
||||
|
||||
// 音乐实体类型转换为面向用户的本地化名称。
|
||||
const musicTypeLabel = computed(() => {
|
||||
const labels = {
|
||||
album: t('music.entityAlbum'),
|
||||
artist: t('music.entityArtist'),
|
||||
recording: t('music.entityRecording'),
|
||||
}
|
||||
const musicType = props.context?.media_info?.music_type
|
||||
return musicType ? labels[musicType] : ''
|
||||
})
|
||||
|
||||
// 音乐标题下方汇总发行时间、时长和曲序,信息密度与影视识别结果保持一致。
|
||||
const musicSummary = computed(() => {
|
||||
const mediaInfo = props.context?.media_info
|
||||
const metaInfo = props.context?.meta_info
|
||||
const values: string[] = []
|
||||
const releaseDate = mediaInfo?.release_date || mediaInfo?.year || metaInfo?.year
|
||||
if (releaseDate) values.push(String(releaseDate))
|
||||
const duration = formatMusicDuration(mediaInfo?.duration || metaInfo?.duration)
|
||||
if (duration) values.push(duration)
|
||||
const trackNumber = mediaInfo?.track_number || metaInfo?.track_number
|
||||
const totalTracks = mediaInfo?.total_tracks || metaInfo?.total_tracks
|
||||
if (trackNumber) {
|
||||
const track = totalTracks ? `${trackNumber}/${totalTracks}` : String(trackNumber)
|
||||
values.push(`${t('music.track')} ${track}`)
|
||||
}
|
||||
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 musicLink = computed(() => props.context?.media_info?.detail_link || '')
|
||||
|
||||
@@ -50,6 +118,10 @@ const musicLink = computed(() => props.context?.media_info?.detail_link || '')
|
||||
function openMusicDetail() {
|
||||
if (musicLink.value) window.open(musicLink.value, '_blank')
|
||||
}
|
||||
|
||||
watch(musicCover, () => {
|
||||
musicCoverLoadError.value = false
|
||||
})
|
||||
</script>
|
||||
|
||||
<template>
|
||||
@@ -59,13 +131,16 @@ function openMusicDetail() {
|
||||
v-if="recognizedName"
|
||||
class="d-flex justify-space-between flex-wrap flex-md-nowrap flex-column flex-md-row"
|
||||
>
|
||||
<div v-if="isMusic && musicCover" class="ma-auto">
|
||||
<div v-if="isMusic" class="ma-auto recognized-music-cover">
|
||||
<VImg
|
||||
v-if="musicCover && !musicCoverLoadError"
|
||||
width="10rem"
|
||||
aspect-ratio="1"
|
||||
class="object-cover rounded-lg ring-1 ring-gray-500"
|
||||
:src="musicCover"
|
||||
cover
|
||||
eager
|
||||
@error="musicCoverLoadError = true"
|
||||
>
|
||||
<template #placeholder>
|
||||
<div class="w-full h-full">
|
||||
@@ -73,6 +148,15 @@ function openMusicDetail() {
|
||||
</div>
|
||||
</template>
|
||||
</VImg>
|
||||
<VSheet
|
||||
v-else
|
||||
width="10rem"
|
||||
height="10rem"
|
||||
rounded="lg"
|
||||
class="d-flex align-center justify-center music-cover-placeholder"
|
||||
>
|
||||
<VIcon icon="mdi-album" size="64" color="medium-emphasis" />
|
||||
</VSheet>
|
||||
</div>
|
||||
<div v-else-if="context?.media_info?.poster_path" class="ma-auto">
|
||||
<VImg
|
||||
@@ -97,7 +181,21 @@ function openMusicDetail() {
|
||||
{{ context?.meta_info?.season_episode }}
|
||||
</span>
|
||||
</div>
|
||||
<VCardSubtitle class="text-center text-md-left">
|
||||
<div v-if="isMusic" class="music-recognition-summary text-center text-md-left">
|
||||
<div v-if="musicArtist" class="text-body-1 font-weight-medium text-high-emphasis">
|
||||
{{ musicArtist }}
|
||||
</div>
|
||||
<div v-if="musicAlbum" class="text-body-2 text-medium-emphasis">
|
||||
{{ t('music.album') }}:{{ musicAlbum }}
|
||||
</div>
|
||||
<div v-if="musicAlbumArtist" class="text-body-2 text-medium-emphasis">
|
||||
{{ t('music.albumArtist') }}:{{ musicAlbumArtist }}
|
||||
</div>
|
||||
<div v-if="musicSummary" class="text-body-2 text-medium-emphasis">
|
||||
{{ musicSummary }}
|
||||
</div>
|
||||
</div>
|
||||
<VCardSubtitle v-else class="text-center text-md-left">
|
||||
{{ context?.media_info?.year || context?.meta_info?.year }}
|
||||
</VCardSubtitle>
|
||||
</VCardItem>
|
||||
@@ -124,39 +222,15 @@ function openMusicDetail() {
|
||||
variant="elevated"
|
||||
class="me-1 mb-1 text-white bg-blue-500"
|
||||
>
|
||||
{{ context?.media_info?.music_type }}
|
||||
{{ musicTypeLabel }}
|
||||
</VChip>
|
||||
<!-- 艺术家 -->
|
||||
<!-- 音乐分类 -->
|
||||
<VChip
|
||||
v-if="isMusic && context?.media_info?.artist"
|
||||
v-if="isMusic && (context?.media_info?.category || context?.media_info?.album_type)"
|
||||
variant="elevated"
|
||||
class="me-1 mb-1 text-white bg-purple-500"
|
||||
>
|
||||
{{ context?.media_info?.artist }}
|
||||
</VChip>
|
||||
<!-- 专辑 -->
|
||||
<VChip
|
||||
v-if="isMusic && context?.media_info?.album"
|
||||
variant="elevated"
|
||||
class="me-1 mb-1 text-white bg-purple-500"
|
||||
>
|
||||
{{ context?.media_info?.album }}
|
||||
</VChip>
|
||||
<!-- 专辑艺术家 -->
|
||||
<VChip
|
||||
v-if="isMusic && context?.media_info?.album_artist"
|
||||
variant="elevated"
|
||||
class="me-1 mb-1 text-white bg-purple-500"
|
||||
>
|
||||
{{ context?.media_info?.album_artist }}
|
||||
</VChip>
|
||||
<!-- 发行日期 -->
|
||||
<VChip
|
||||
v-if="isMusic && context?.media_info?.release_date"
|
||||
variant="elevated"
|
||||
class="me-1 mb-1 text-white bg-purple-500"
|
||||
>
|
||||
{{ context?.media_info?.release_date }}
|
||||
{{ context?.media_info?.category || context?.media_info?.album_type }}
|
||||
</VChip>
|
||||
<!-- 风格 -->
|
||||
<VChip
|
||||
@@ -169,34 +243,12 @@ function openMusicDetail() {
|
||||
</VChip>
|
||||
<!-- 音频技术参数 -->
|
||||
<VChip
|
||||
v-if="isMusic && context?.meta_info?.audio_format"
|
||||
v-for="audioChip in isMusic ? musicAudioChips : []"
|
||||
:key="audioChip"
|
||||
variant="elevated"
|
||||
class="me-1 mb-1 text-white bg-orange-500"
|
||||
>
|
||||
{{ context?.meta_info?.audio_format }}
|
||||
</VChip>
|
||||
<VChip
|
||||
v-if="isMusic && (context?.meta_info?.bit_depth || context?.meta_info?.sample_rate)"
|
||||
variant="elevated"
|
||||
class="me-1 mb-1 text-white bg-orange-500"
|
||||
>
|
||||
{{ [context?.meta_info?.bit_depth, context?.meta_info?.sample_rate].filter(Boolean).join(' kHz ') }}
|
||||
</VChip>
|
||||
<!-- 时长 -->
|
||||
<VChip
|
||||
v-if="isMusic && formatDuration(context?.media_info?.duration)"
|
||||
variant="elevated"
|
||||
class="me-1 mb-1 text-white bg-orange-500"
|
||||
>
|
||||
{{ formatDuration(context?.media_info?.duration) }}
|
||||
</VChip>
|
||||
<!-- 曲目信息 -->
|
||||
<VChip
|
||||
v-if="isMusic && context?.media_info?.track_number"
|
||||
variant="elevated"
|
||||
class="me-1 mb-1 text-white bg-red-500"
|
||||
>
|
||||
{{ `曲目 ${context?.media_info?.track_number}${context?.media_info?.total_tracks ? ` / ${context?.media_info?.total_tracks}` : ''}` }}
|
||||
{{ audioChip }}
|
||||
</VChip>
|
||||
<!-- ISRC -->
|
||||
<VChip
|
||||
@@ -283,4 +335,20 @@ function openMusicDetail() {
|
||||
</VExpansionPanel>
|
||||
</VExpansionPanels>
|
||||
</div>
|
||||
</template>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.recognized-music-cover {
|
||||
flex: 0 0 auto;
|
||||
}
|
||||
|
||||
.music-cover-placeholder {
|
||||
background: rgba(var(--v-theme-on-surface), 0.08);
|
||||
}
|
||||
|
||||
.music-recognition-summary {
|
||||
display: grid;
|
||||
gap: 0.2rem;
|
||||
margin-block-start: 0.35rem;
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -9,25 +9,53 @@ describe('MediaInfoCard', () => {
|
||||
props: {
|
||||
context: {
|
||||
meta_info: {
|
||||
artists: ['周杰伦'],
|
||||
album: '完美的一天',
|
||||
artists: ['孙燕姿'],
|
||||
audio_format: 'FLAC',
|
||||
bit_depth: 24,
|
||||
bitrate: 1411200,
|
||||
duration: 221,
|
||||
sample_rate: 48000,
|
||||
title: '晴天',
|
||||
title: '眼泪成诗',
|
||||
total_tracks: 10,
|
||||
track_number: 2,
|
||||
type: '音乐',
|
||||
},
|
||||
media_info: {
|
||||
artist: '周杰伦',
|
||||
title: '晴天',
|
||||
album: '完美的一天',
|
||||
artist: '孙燕姿',
|
||||
category: 'Album',
|
||||
cover_url: 'https://coverartarchive.org/release-group/album-1/front-500',
|
||||
duration: 221,
|
||||
genres: ['华语流行'],
|
||||
isrc: 'TWA530505002',
|
||||
music_type: 'recording',
|
||||
release_date: '2005-10-07',
|
||||
title: '眼泪成诗',
|
||||
total_tracks: 10,
|
||||
track_number: 2,
|
||||
type: '音乐',
|
||||
},
|
||||
},
|
||||
},
|
||||
})
|
||||
|
||||
expect(screen.getByText('晴天')).toBeInTheDocument()
|
||||
expect(screen.getByText('眼泪成诗')).toBeInTheDocument()
|
||||
expect(screen.getByText('孙燕姿')).toBeInTheDocument()
|
||||
expect(screen.getByText('专辑:完美的一天')).toBeInTheDocument()
|
||||
expect(screen.getByText('2005-10-07 · 3:41 · 音轨 2/10')).toBeInTheDocument()
|
||||
expect(screen.getByText('单曲')).toBeInTheDocument()
|
||||
expect(screen.getByText('Album')).toBeInTheDocument()
|
||||
expect(screen.getByText('华语流行')).toBeInTheDocument()
|
||||
expect(screen.getByText('FLAC')).toBeInTheDocument()
|
||||
expect(screen.getByText('24 kHz 48000')).toBeInTheDocument()
|
||||
expect(screen.getByText('24-bit')).toBeInTheDocument()
|
||||
expect(screen.getByText('48 kHz')).toBeInTheDocument()
|
||||
expect(screen.getByText('1411 kbps')).toBeInTheDocument()
|
||||
expect(screen.getByText('TWA530505002')).toBeInTheDocument()
|
||||
expect(document.querySelector('.v-img__img')).toHaveAttribute(
|
||||
'src',
|
||||
'https://coverartarchive.org/release-group/album-1/front-500',
|
||||
)
|
||||
expect(screen.queryByText('识别失败,无法识别到有效信息!')).not.toBeInTheDocument()
|
||||
})
|
||||
|
||||
|
||||
Reference in New Issue
Block a user