feat: 音乐搜索与识别前端联动统一

- 音乐搜索从 music/search 切换为 media/search?type=music
- MediaInfoCard 支持音乐卡片(方形封面、时长、音乐实体类型、详情外链)
- 订阅与资源选择器适配音乐元数据
- 同步中英繁语言包
This commit is contained in:
jxxghp
2026-08-09 10:18:42 +08:00
parent b9194ffb74
commit 009ce6173b
11 changed files with 204 additions and 89 deletions
+159 -31
View File
@@ -11,6 +11,11 @@ const props = defineProps({
// 音乐元数据使用 title,影视元数据使用 name,识别结果卡片统一兼容两种字段。
const recognizedName = computed(() => props.context?.meta_info?.name || props.context?.meta_info?.title)
// 是否为音乐识别结果
const isMusic = computed(
() => props.context?.media_info?.type === '音乐' || props.context?.meta_info?.type === '音乐',
)
// TMDB图片转换为w500大小
function getW500Image(url = '') {
if (!url) return ''
@@ -24,6 +29,27 @@ function openTmdbPage(type: string, tmdbId: number) {
const url = `https://www.themoviedb.org/${type === '电影' ? 'movie' : 'tv'}/${tmdbId}`
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 musicLink = computed(() => props.context?.media_info?.detail_link || '')
// 打开音乐详情外链
function openMusicDetail() {
if (musicLink.value) window.open(musicLink.value, '_blank')
}
</script>
<template>
@@ -33,7 +59,22 @@ function openTmdbPage(type: string, tmdbId: number) {
v-if="recognizedName"
class="d-flex justify-space-between flex-wrap flex-md-nowrap flex-column flex-md-row"
>
<div v-if="context?.media_info?.poster_path" class="ma-auto">
<div v-if="isMusic && musicCover" class="ma-auto">
<VImg
width="10rem"
aspect-ratio="1"
class="object-cover rounded-lg ring-1 ring-gray-500"
:src="musicCover"
cover
>
<template #placeholder>
<div class="w-full h-full">
<VSkeletonLoader class="object-cover" />
</div>
</template>
</VImg>
</div>
<div v-else-if="context?.media_info?.poster_path" class="ma-auto">
<VImg
width="10rem"
aspect-ratio="2/3"
@@ -77,46 +118,133 @@ function openTmdbPage(type: string, tmdbId: number) {
>
{{ context?.media_info?.type || context?.meta_info?.type }}
</VChip>
<!-- 音乐实体类型 -->
<VChip
v-if="isMusic && context?.media_info?.music_type"
variant="elevated"
class="me-1 mb-1 text-white bg-blue-500"
>
{{ context?.media_info?.music_type }}
</VChip>
<!-- 艺术家 -->
<VChip v-if="context?.media_info?.artist" variant="elevated" class="me-1 mb-1 text-white bg-purple-500">
{{ context?.media_info?.artist }}
</VChip>
<!-- 专辑 -->
<VChip v-if="context?.media_info?.album" variant="elevated" class="me-1 mb-1 text-white bg-purple-500">
{{ context?.media_info?.album }}
</VChip>
<!-- 专辑艺术家 -->
<VChip
v-if="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="context?.media_info?.release_date"
variant="elevated"
class="me-1 mb-1 text-white bg-purple-500"
>
{{ context?.media_info?.release_date }}
</VChip>
<!-- 风格 -->
<VChip
v-for="genre in context?.media_info?.genres"
:key="genre"
variant="elevated"
class="me-1 mb-1 text-white bg-purple-500"
>
{{ genre }}
</VChip>
<!-- 音频技术参数 -->
<VChip
v-if="context?.meta_info?.audio_format"
variant="elevated"
class="me-1 mb-1 text-white bg-orange-500"
>
{{ context?.meta_info?.audio_format }}
</VChip>
<VChip
v-if="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="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="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}` : ''}` }}
</VChip>
<!-- ISRC -->
<VChip v-if="context?.media_info?.isrc" variant="elevated" class="me-1 mb-1 text-white bg-red-500">
{{ context?.media_info?.isrc }}
</VChip>
<!-- MusicBrainz 外链 -->
<VChip
v-if="musicLink"
variant="elevated"
class="me-1 mb-1 text-white bg-green-500"
@click="openMusicDetail"
>
详情
</VChip>
<!-- 二级分类 -->
<VChip v-if="context?.media_info?.category" variant="elevated" class="me-1 mb-1 text-white bg-blue-500">
<VChip v-if="!isMusic && context?.media_info?.category" variant="elevated" class="me-1 mb-1 text-white bg-blue-500">
{{ context?.media_info?.category }}
</VChip>
<!-- TMDBID -->
<VChip
v-if="context?.media_info?.tmdb_id"
v-if="!isMusic && context?.media_info?.tmdb_id"
variant="elevated"
class="me-1 mb-1 text-white bg-green-500"
@click="openTmdbPage(context?.media_info?.type || '', context?.media_info?.tmdb_id)"
>
{{ context?.media_info?.tmdb_id }}
</VChip>
<!-- meta_info -->
<VChip v-if="context?.meta_info?.web_source" variant="elevated" class="me-1 mb-1 text-white bg-purple-500">
{{ context?.meta_info?.web_source }}
</VChip>
<VChip v-if="context?.meta_info?.edition" variant="elevated" class="me-1 mb-1 text-white bg-red-500">
{{ context?.meta_info?.edition }}
</VChip>
<VChip v-if="context?.meta_info?.resource_pix" variant="elevated" class="me-1 mb-1 text-white bg-red-500">
{{ context?.meta_info?.resource_pix }}
</VChip>
<VChip
v-if="context?.meta_info?.video_encode"
variant="elevated"
class="me-1 mb-1 text-white bg-orange-500"
>
{{ context?.meta_info?.video_encode }}
</VChip>
<VChip
v-if="context?.meta_info?.audio_encode"
variant="elevated"
class="me-1 mb-1 text-white bg-orange-500"
>
{{ context?.meta_info?.audio_encode }}
</VChip>
<VChip v-if="context?.meta_info?.resource_team" variant="elevated" class="me-1 mb-1 text-white bg-cyan-500">
{{ context?.meta_info?.resource_team }}
</VChip>
<!-- meta_info音乐不显示影视资源信息 -->
<template v-if="!isMusic">
<VChip v-if="context?.meta_info?.web_source" variant="elevated" class="me-1 mb-1 text-white bg-purple-500">
{{ context?.meta_info?.web_source }}
</VChip>
<VChip v-if="context?.meta_info?.edition" variant="elevated" class="me-1 mb-1 text-white bg-red-500">
{{ context?.meta_info?.edition }}
</VChip>
<VChip v-if="context?.meta_info?.resource_pix" variant="elevated" class="me-1 mb-1 text-white bg-red-500">
{{ context?.meta_info?.resource_pix }}
</VChip>
<VChip
v-if="context?.meta_info?.video_encode"
variant="elevated"
class="me-1 mb-1 text-white bg-orange-500"
>
{{ context?.meta_info?.video_encode }}
</VChip>
<VChip
v-if="context?.meta_info?.audio_encode"
variant="elevated"
class="me-1 mb-1 text-white bg-orange-500"
>
{{ context?.meta_info?.audio_encode }}
</VChip>
<VChip v-if="context?.meta_info?.resource_team" variant="elevated" class="me-1 mb-1 text-white bg-cyan-500">
{{ context?.meta_info?.resource_team }}
</VChip>
</template>
</VCardItem>
</div>
</div>
@@ -143,4 +271,4 @@ function openTmdbPage(type: string, tmdbId: number) {
</VExpansionPanel>
</VExpansionPanels>
</div>
</template>
</template>
+8 -37
View File
@@ -27,9 +27,6 @@ const keyword = ref<string>()
// 选择分类
const selectCategory = ref<number[]>([])
// 选择主媒体类型,用于按站点定义的电影、电视剧、音乐分类浏览。
const selectMediaType = ref<string>()
// 全部分类
const siteCategoryList = ref<SiteCategory[]>()
@@ -69,11 +66,8 @@ const categoryOptions = computed(() => {
})
})
const mediaTypeOptions = computed(() => [
{ title: t('mediaType.movie'), value: '电影' },
{ title: t('mediaType.tv'), value: '电视剧' },
{ title: t('mediaType.music'), value: '音乐' },
])
// 站点是否配置了资源分类
const hasSiteCategory = computed(() => (siteCategoryList.value?.length ?? 0) > 0)
// 总条数
const resourceTotalItems = computed(() => resourceDataList.value.length)
@@ -169,7 +163,6 @@ async function getResourceList() {
params: {
keyword: keyword.value,
cat: selectCategory.value?.join(','),
mtype: selectMediaType.value,
},
})
@@ -288,20 +281,9 @@ onMounted(() => {
clearable
prepend-inner-icon="mdi-folder"
hide-details
/>
</VCol>
<VCol cols="12" md="2">
<VSelect
v-model="selectMediaType"
:items="mediaTypeOptions"
class="site-resource-filter-input"
density="compact"
variant="solo-filled"
flat
clearable
:label="t('common.type')"
prepend-inner-icon="mdi-shape-outline"
hide-details
:disabled="!hasSiteCategory"
:hint="hasSiteCategory ? '' : t('dialog.siteResource.noCategory')"
persistent-hint
/>
</VCol>
<VCol cols="12" md="3" class="d-flex align-center">
@@ -369,20 +351,6 @@ onMounted(() => {
@keyup.enter="getResourceList"
/>
</VCol>
<VCol cols="12">
<VSelect
v-model="selectMediaType"
:items="mediaTypeOptions"
class="site-resource-filter-input"
density="compact"
variant="solo-filled"
flat
clearable
:label="t('common.type')"
prepend-inner-icon="mdi-shape-outline"
hide-details
/>
</VCol>
<VCol cols="12">
<VSelect
v-model="selectCategory"
@@ -398,6 +366,9 @@ onMounted(() => {
clearable
prepend-inner-icon="mdi-folder"
hide-details
:disabled="!hasSiteCategory"
:hint="hasSiteCategory ? '' : t('dialog.siteResource.noCategory')"
persistent-hint
/>
</VCol>
<VCol cols="12" class="d-flex gap-2">
+9 -13
View File
@@ -58,19 +58,15 @@ async function searchMedias() {
// 调用API搜索词条
try {
loading.value = true
const result: MediaInfo[] =
props.type === 'musicbrainz'
? await api.get('music/search', {
params: { query: searchKeyword, count: 20 },
})
: await api.get('media/search', {
params: {
title: searchKeyword,
page: 1,
count: 20,
source: props.type,
},
})
const result: MediaInfo[] = await api.get('media/search', {
params: {
title: searchKeyword,
type: props.type === 'musicbrainz' ? 'music' : 'media',
page: 1,
count: 20,
source: props.type,
},
})
// 清空
items.value = []