feat: bili播放优化

This commit is contained in:
algerkong
2025-09-20 16:40:45 +08:00
parent 93022691e2
commit 67370b9072
16 changed files with 590 additions and 182 deletions
+51
View File
@@ -0,0 +1,51 @@
export default {
player: {
loading: 'Loading audio...',
retry: 'Retry',
playNow: 'Play Now',
loadingTitle: 'Loading...',
totalDuration: 'Total Duration: {duration}',
partsList: 'Parts List ({count} episodes)',
playStarted: 'Playback started',
switchingPart: 'Switching to part: {part}',
preloadingNext: 'Preloading next part: {part}',
playingCurrent: 'Playing current selected part: {name}',
num: 'M',
errors: {
invalidVideoId: 'Invalid video ID',
loadVideoDetailFailed: 'Failed to load video details',
loadPartInfoFailed: 'Unable to load video part information',
loadAudioUrlFailed: 'Failed to get audio playback URL',
videoDetailNotLoaded: 'Video details not loaded',
missingParams: 'Missing required parameters',
noAvailableAudioUrl: 'No available audio URL found',
loadPartAudioFailed: 'Failed to load part audio URL',
audioListEmpty: 'Audio list is empty, please retry',
currentPartNotFound: 'Current part audio not found',
audioUrlFailed: 'Failed to get audio URL',
playFailed: 'Playback failed, please retry',
getAudioUrlFailed: 'Failed to get audio URL, please retry',
audioNotFound: 'Corresponding audio not found, please retry',
preloadFailed: 'Failed to preload next part',
switchPartFailed: 'Failed to load audio URL when switching parts'
},
console: {
loadingDetail: 'Loading Bilibili video details',
detailData: 'Bilibili video detail data',
multipleParts: 'Video has multiple parts, total {count}',
noPartsData: 'Video has no parts or part data is empty',
loadingAudioSource: 'Loading audio source',
generatedAudioList: 'Generated audio list, total {count}',
getDashAudioUrl: 'Got dash audio URL',
getDurlAudioUrl: 'Got durl audio URL',
loadingPartAudio: 'Loading part audio URL: {part}, cid: {cid}',
loadPartAudioFailed: 'Failed to load part audio URL: {part}',
switchToPart: 'Switching to part: {part}',
audioNotFoundInList: 'Corresponding audio item not found',
preparingToPlay: 'Preparing to play current selected part: {name}',
preloadingNextPart: 'Preloading next part: {part}',
playingSelectedPart: 'Playing current selected part: {name}, audio URL: {url}',
preloadNextFailed: 'Failed to preload next part'
}
}
};
+51
View File
@@ -0,0 +1,51 @@
export default {
player: {
loading: 'オーディオ読み込み中...',
retry: '再試行',
playNow: '今すぐ再生',
loadingTitle: '読み込み中...',
totalDuration: '総再生時間: {duration}',
partsList: 'パートリスト ({count}話)',
playStarted: '再生を開始しました',
switchingPart: 'パートを切り替え中: {part}',
preloadingNext: '次のパートをプリロード中: {part}',
playingCurrent: '現在選択されたパートを再生中: {name}',
num: '万',
errors: {
invalidVideoId: '無効な動画ID',
loadVideoDetailFailed: '動画詳細の取得に失敗しました',
loadPartInfoFailed: '動画パート情報の読み込みができません',
loadAudioUrlFailed: 'オーディオ再生URLの取得に失敗しました',
videoDetailNotLoaded: '動画詳細が読み込まれていません',
missingParams: '必要なパラメータが不足しています',
noAvailableAudioUrl: '利用可能なオーディオURLが見つかりません',
loadPartAudioFailed: 'パートオーディオURLの読み込みに失敗しました',
audioListEmpty: 'オーディオリストが空です。再試行してください',
currentPartNotFound: '現在のパートのオーディオが見つかりません',
audioUrlFailed: 'オーディオURLの取得に失敗しました',
playFailed: '再生に失敗しました。再試行してください',
getAudioUrlFailed: 'オーディオURLの取得に失敗しました。再試行してください',
audioNotFound: '対応するオーディオが見つかりません。再試行してください',
preloadFailed: '次のパートのプリロードに失敗しました',
switchPartFailed: 'パート切り替え時のオーディオURL読み込みに失敗しました'
},
console: {
loadingDetail: 'Bilibiliビデオ詳細を読み込み中',
detailData: 'Bilibiliビデオ詳細データ',
multipleParts: 'ビデオに複数のパートがあります。合計{count}個',
noPartsData: 'ビデオにパートがないか、パートデータが空です',
loadingAudioSource: 'オーディオソースを読み込み中',
generatedAudioList: 'オーディオリストを生成しました。合計{count}個',
getDashAudioUrl: 'dashオーディオURLを取得しました',
getDurlAudioUrl: 'durlオーディオURLを取得しました',
loadingPartAudio: 'パートオーディオURLを読み込み中: {part}, cid: {cid}',
loadPartAudioFailed: 'パートオーディオURLの読み込みに失敗: {part}',
switchToPart: 'パートに切り替え中: {part}',
audioNotFoundInList: '対応するオーディオアイテムが見つかりません',
preparingToPlay: '現在選択されたパートの再生準備中: {name}',
preloadingNextPart: '次のパートをプリロード中: {part}',
playingSelectedPart: '現在選択されたパートを再生中: {name}、オーディオURL: {url}',
preloadNextFailed: '次のパートのプリロードに失敗しました'
}
}
};
+51
View File
@@ -0,0 +1,51 @@
export default {
player: {
loading: '오디오 로딩 중...',
retry: '다시 시도',
playNow: '지금 재생',
loadingTitle: '로딩 중...',
totalDuration: '총 재생시간: {duration}',
partsList: '파트 목록 ({count}화)',
playStarted: '재생이 시작되었습니다',
switchingPart: '파트 전환 중: {part}',
preloadingNext: '다음 파트 미리 로딩 중: {part}',
playingCurrent: '현재 선택된 파트 재생 중: {name}',
num: '만',
errors: {
invalidVideoId: '유효하지 않은 비디오 ID',
loadVideoDetailFailed: '비디오 세부정보 로드 실패',
loadPartInfoFailed: '비디오 파트 정보를 로드할 수 없습니다',
loadAudioUrlFailed: '오디오 재생 URL 가져오기 실패',
videoDetailNotLoaded: '비디오 세부정보가 로드되지 않았습니다',
missingParams: '필수 매개변수가 누락되었습니다',
noAvailableAudioUrl: '사용 가능한 오디오 URL을 찾을 수 없습니다',
loadPartAudioFailed: '파트 오디오 URL 로드 실패',
audioListEmpty: '오디오 목록이 비어있습니다. 다시 시도해주세요',
currentPartNotFound: '현재 파트의 오디오를 찾을 수 없습니다',
audioUrlFailed: '오디오 URL 가져오기 실패',
playFailed: '재생 실패. 다시 시도해주세요',
getAudioUrlFailed: '오디오 URL 가져오기 실패. 다시 시도해주세요',
audioNotFound: '해당 오디오를 찾을 수 없습니다. 다시 시도해주세요',
preloadFailed: '다음 파트 미리 로딩 실패',
switchPartFailed: '파트 전환 시 오디오 URL 로드 실패'
},
console: {
loadingDetail: 'Bilibili 비디오 세부정보 로딩 중',
detailData: 'Bilibili 비디오 세부정보 데이터',
multipleParts: '비디오에 여러 파트가 있습니다. 총 {count}개',
noPartsData: '비디오에 파트가 없거나 파트 데이터가 비어있습니다',
loadingAudioSource: '오디오 소스 로딩 중',
generatedAudioList: '오디오 목록을 생성했습니다. 총 {count}개',
getDashAudioUrl: 'dash 오디오 URL을 가져왔습니다',
getDurlAudioUrl: 'durl 오디오 URL을 가져왔습니다',
loadingPartAudio: '파트 오디오 URL 로딩 중: {part}, cid: {cid}',
loadPartAudioFailed: '파트 오디오 URL 로드 실패: {part}',
switchToPart: '파트로 전환 중: {part}',
audioNotFoundInList: '해당 오디오 항목을 찾을 수 없습니다',
preparingToPlay: '현재 선택된 파트 재생 준비 중: {name}',
preloadingNextPart: '다음 파트 미리 로딩 중: {part}',
playingSelectedPart: '현재 선택된 파트 재생 중: {name}, 오디오 URL: {url}',
preloadNextFailed: '다음 파트 미리 로딩 실패'
}
}
};
+51
View File
@@ -0,0 +1,51 @@
export default {
player: {
loading: '听书加载中...',
retry: '重试',
playNow: '立即播放',
loadingTitle: '加载中...',
totalDuration: '总时长: {duration}',
partsList: '分P列表 (共{count}集)',
playStarted: '已开始播放',
switchingPart: '切换到分P: {part}',
preloadingNext: '预加载下一个分P: {part}',
playingCurrent: '播放当前选中的分P: {name}',
num: '万',
errors: {
invalidVideoId: '视频ID无效',
loadVideoDetailFailed: '获取视频详情失败',
loadPartInfoFailed: '无法加载视频分P信息',
loadAudioUrlFailed: '获取音频播放地址失败',
videoDetailNotLoaded: '视频详情未加载',
missingParams: '缺少必要参数',
noAvailableAudioUrl: '未找到可用的音频地址',
loadPartAudioFailed: '加载分P音频URL失败',
audioListEmpty: '音频列表为空,请重试',
currentPartNotFound: '未找到当前分P的音频',
audioUrlFailed: '获取音频URL失败',
playFailed: '播放失败,请重试',
getAudioUrlFailed: '获取音频地址失败,请重试',
audioNotFound: '未找到对应的音频,请重试',
preloadFailed: '预加载下一个分P失败',
switchPartFailed: '切换分P时加载音频URL失败'
},
console: {
loadingDetail: '加载B站视频详情',
detailData: 'B站视频详情数据',
multipleParts: '视频有多个分P,共{count}个',
noPartsData: '视频无分P或分P数据为空',
loadingAudioSource: '加载音频源',
generatedAudioList: '已生成音频列表,共{count}首',
getDashAudioUrl: '获取到dash音频URL',
getDurlAudioUrl: '获取到durl音频URL',
loadingPartAudio: '加载分P音频URL: {part}, cid: {cid}',
loadPartAudioFailed: '加载分P音频URL失败: {part}',
switchToPart: '切换到分P: {part}',
audioNotFoundInList: '未找到对应的音频项',
preparingToPlay: '准备播放当前选中的分P: {name}',
preloadingNextPart: '预加载下一个分P: {part}',
playingSelectedPart: '播放当前选中的分P: {name},音频URL: {url}',
preloadNextFailed: '预加载下一个分P失败'
}
}
};
+51
View File
@@ -0,0 +1,51 @@
export default {
player: {
loading: '聽書載入中...',
retry: '重試',
playNow: '立即播放',
loadingTitle: '載入中...',
totalDuration: '總時長: {duration}',
partsList: '分P列表 (共{count}集)',
playStarted: '已開始播放',
switchingPart: '切換到分P: {part}',
preloadingNext: '預載入下一個分P: {part}',
playingCurrent: '播放當前選中的分P: {name}',
num: '萬',
errors: {
invalidVideoId: '影片ID無效',
loadVideoDetailFailed: '獲取影片詳情失敗',
loadPartInfoFailed: '無法載入影片分P資訊',
loadAudioUrlFailed: '獲取音訊播放地址失敗',
videoDetailNotLoaded: '影片詳情未載入',
missingParams: '缺少必要參數',
noAvailableAudioUrl: '未找到可用的音訊地址',
loadPartAudioFailed: '載入分P音訊URL失敗',
audioListEmpty: '音訊列表為空,請重試',
currentPartNotFound: '未找到當前分P的音訊',
audioUrlFailed: '獲取音訊URL失敗',
playFailed: '播放失敗,請重試',
getAudioUrlFailed: '獲取音訊地址失敗,請重試',
audioNotFound: '未找到對應的音訊,請重試',
preloadFailed: '預載入下一個分P失敗',
switchPartFailed: '切換分P時載入音訊URL失敗'
},
console: {
loadingDetail: '載入B站影片詳情',
detailData: 'B站影片詳情資料',
multipleParts: '影片有多個分P,共{count}個',
noPartsData: '影片無分P或分P資料為空',
loadingAudioSource: '載入音訊來源',
generatedAudioList: '已生成音訊列表,共{count}首',
getDashAudioUrl: '獲取到dash音訊URL',
getDurlAudioUrl: '獲取到durl音訊URL',
loadingPartAudio: '載入分P音訊URL: {part}, cid: {cid}',
loadPartAudioFailed: '載入分P音訊URL失敗: {part}',
switchToPart: '切換到分P: {part}',
audioNotFoundInList: '未找到對應的音訊項目',
preparingToPlay: '準備播放當前選中的分P: {name}',
preloadingNextPart: '預載入下一個分P: {part}',
playingSelectedPart: '播放當前選中的分P: {name},音訊URL: {url}',
preloadNextFailed: '預載入下一個分P失敗'
}
}
};
+226 -1
View File
@@ -1,4 +1,5 @@
import type { IBilibiliPlayUrl, IBilibiliVideoDetail } from '@/types/bilibili';
import type { IBilibiliPage, IBilibiliPlayUrl, IBilibiliVideoDetail } from '@/types/bilibili';
import type { SongResult } from '@/types/music';
import { getSetData, isElectron } from '@/utils';
import request from '@/utils/request';
@@ -217,3 +218,227 @@ export const searchAndGetBilibiliAudioUrl = async (keyword: string): Promise<str
throw error;
}
};
/**
* 解析B站ID格式
* @param biliId B站ID,可能是字符串格式(bvid--pid--cid
* @returns 解析后的对象 {bvid, pid, cid} 或 null
*/
export const parseBilibiliId = (
biliId: string | number
): { bvid: string; pid: string; cid: number } | null => {
const strBiliId = String(biliId);
if (strBiliId.includes('--')) {
const [bvid, pid, cid] = strBiliId.split('--');
if (!bvid || !pid || !cid) {
console.warn(`B站ID格式错误: ${strBiliId}, 正确格式应为 bvid--pid--cid`);
return null;
}
return { bvid, pid, cid: Number(cid) };
}
return null;
};
/**
* 创建默认的Artist对象
* @param name 艺术家名称
* @param id 艺术家ID
* @returns Artist对象
*/
const createDefaultArtist = (name: string, id: number = 0) => ({
name,
id,
picId: 0,
img1v1Id: 0,
briefDesc: '',
img1v1Url: '',
albumSize: 0,
alias: [],
trans: '',
musicSize: 0,
topicPerson: 0,
picUrl: ''
});
/**
* 创建默认的Album对象
* @param name 专辑名称
* @param picUrl 专辑图片URL
* @param artistName 艺术家名称
* @param artistId 艺术家ID
* @returns Album对象
*/
const createDefaultAlbum = (
name: string,
picUrl: string,
artistName: string,
artistId: number = 0
) => ({
name,
picUrl,
id: 0,
type: '',
size: 0,
picId: 0,
blurPicUrl: '',
companyId: 0,
pic: 0,
publishTime: 0,
description: '',
tags: '',
company: '',
briefDesc: '',
artist: createDefaultArtist(artistName, artistId),
songs: [],
alias: [],
status: 0,
copyrightId: 0,
commentThreadId: '',
artists: [],
subType: '',
transName: null,
onSale: false,
mark: 0,
picId_str: ''
});
/**
* 创建基础的B站SongResult对象
* @param config 配置对象
* @returns SongResult对象
*/
const createBaseBilibiliSong = (config: {
id: string | number;
name: string;
picUrl: string;
artistName: string;
artistId?: number;
albumName: string;
bilibiliData?: { bvid: string; cid: number };
playMusicUrl?: string;
duration?: number;
}): SongResult => {
const {
id,
name,
picUrl,
artistName,
artistId = 0,
albumName,
bilibiliData,
playMusicUrl,
duration
} = config;
const baseResult: SongResult = {
id,
name,
picUrl,
ar: [createDefaultArtist(artistName, artistId)],
al: createDefaultAlbum(albumName, picUrl, artistName, artistId),
count: 0,
source: 'bilibili' as const
};
if (bilibiliData) {
baseResult.bilibiliData = bilibiliData;
}
if (playMusicUrl) {
baseResult.playMusicUrl = playMusicUrl;
}
if (duration !== undefined) {
baseResult.duration = duration;
}
return baseResult as SongResult;
};
/**
* 从B站视频详情和分P信息创建SongResult对象
* @param videoDetail B站视频详情
* @param page 分P信息
* @param bvid B站视频ID
* @returns SongResult对象
*/
export const createSongFromBilibiliVideo = (
videoDetail: IBilibiliVideoDetail,
page: IBilibiliPage,
bvid: string
): SongResult => {
const pageName = page.part || '';
const title = `${pageName} - ${videoDetail.title}`;
const songId = `${bvid}--${page.page}--${page.cid}`;
const picUrl = getBilibiliProxyUrl(videoDetail.pic);
return createBaseBilibiliSong({
id: songId,
name: title,
picUrl,
artistName: videoDetail.owner.name,
artistId: videoDetail.owner.mid,
albumName: videoDetail.title,
bilibiliData: {
bvid,
cid: page.cid
}
});
};
/**
* 创建简化的SongResult对象(用于搜索结果直接播放)
* @param item 搜索结果项
* @param audioUrl 音频URL
* @returns SongResult对象
*/
export const createSimpleBilibiliSong = (item: any, audioUrl: string): SongResult => {
const duration = typeof item.duration === 'string' ? 0 : item.duration * 1000; // 转换为毫秒
return createBaseBilibiliSong({
id: item.id,
name: item.title,
picUrl: item.pic,
artistName: item.author,
albumName: item.title,
playMusicUrl: audioUrl,
duration
});
};
/**
* 批量处理B站视频,从ID列表获取SongResult列表
* @param bilibiliIds B站ID列表
* @returns SongResult列表
*/
export const processBilibiliVideos = async (
bilibiliIds: (string | number)[]
): Promise<SongResult[]> => {
const bilibiliSongs: SongResult[] = [];
for (const biliId of bilibiliIds) {
const parsedId = parseBilibiliId(biliId);
if (!parsedId) continue;
try {
const res = await getBilibiliVideoDetail(parsedId.bvid);
const videoDetail = res.data;
// 找到对应的分P
const page = videoDetail.pages.find((p) => p.cid === parsedId.cid);
if (!page) {
console.warn(`未找到对应的分P: cid=${parsedId.cid}`);
continue;
}
const songData = createSongFromBilibiliVideo(videoDetail, page, parsedId.bvid);
bilibiliSongs.push(songData);
} catch (error) {
console.error(`获取B站视频详情失败 (${biliId}):`, error);
}
}
return bilibiliSongs;
};
@@ -19,8 +19,12 @@
</template>
<script setup lang="ts">
import { useI18n } from 'vue-i18n';
import type { IBilibiliSearchResult } from '@/types/bilibili';
const { t } = useI18n();
const props = defineProps<{
item: IBilibiliSearchResult;
}>();
@@ -39,7 +43,7 @@ const handleClick = () => {
const formatNumber = (num?: number) => {
if (!num) return '0';
if (num >= 10000) {
return `${(num / 10000).toFixed(1)}`;
return `${(num / 10000).toFixed(1)}${t('bilibili.player.num')}`;
}
return num.toString();
};
+4 -4
View File
@@ -43,7 +43,7 @@
/>
</div>
<div class="music-info">
<div class="music-content-name">{{ playMusic.name }}</div>
<div class="music-content-name" v-html="playMusic.name"></div>
<div class="music-content-singer">
<n-ellipsis
class="text-ellipsis"
@@ -98,7 +98,7 @@
class="music-info-header"
:style="{ textAlign: config.centerLyrics ? 'center' : 'left' }"
>
<div class="music-info-name">{{ playMusic.name }}</div>
<div class="music-info-name" v-html="playMusic.name"></div>
<div class="music-info-singer">
<span
v-for="(item, index) in artistList"
@@ -562,7 +562,7 @@ defineExpose({
@apply text-center w-[600px];
.music-content-name {
@apply text-4xl mb-4;
@apply text-4xl mb-4 line-clamp-2;
color: var(--text-color-active);
}
@@ -641,7 +641,7 @@ defineExpose({
@apply mb-8;
.music-info-name {
@apply text-4xl font-bold mb-2;
@apply text-4xl font-bold mb-2 line-clamp-2;
color: var(--text-color-active);
}
@@ -32,7 +32,7 @@
<transition name="fade">
<div v-if="showFullLyrics && !isLandscape" class="fullscreen-lyrics" :class="config.theme">
<div class="fullscreen-header">
<div class="song-title">{{ playMusic.name }}</div>
<div class="song-title" v-html="playMusic.name"></div>
<div class="artist-name">
<span v-for="(item, index) in artistList" :key="index">
{{ item.name }}{{ index < artistList.length - 1 ? ' / ' : '' }}
@@ -97,7 +97,7 @@
<!-- 歌曲信息 -->
<div class="song-info">
<div class="song-title-container">
<h1 class="song-title">{{ playMusic.name }}</h1>
<h1 class="song-title" v-html="playMusic.name"></h1>
</div>
<p class="song-artist">
<span
@@ -190,7 +190,7 @@
<!-- 歌曲信息放置在顶部 -->
<div class="landscape-song-info">
<div class="flex flex-col flex-1">
<h1 class="song-title">{{ playMusic.name }}</h1>
<h1 class="song-title" v-html="playMusic.name"></h1>
<p class="song-artist">
<span
v-for="(item, index) in artistList"
@@ -16,7 +16,7 @@
<!-- 歌曲信息 -->
<div class="song-info" @click="setMusicFull">
<div class="song-title">{{ playMusic?.name || '未播放' }}</div>
<div class="song-title" v-html="playMusic?.name || '未播放'"></div>
<div class="song-artist">
<span
v-for="(artists, artistsindex) in artistList"
+1 -1
View File
@@ -58,7 +58,7 @@
<div class="music-content">
<div class="music-content-title flex items-center">
<n-ellipsis class="text-ellipsis" line-clamp="1">
{{ playMusic?.name || '' }}
<p v-html="playMusic?.name || ''"></p>
</n-ellipsis>
<span v-if="playbackRate !== 1.0" class="playback-rate-badge"> {{ playbackRate }}x </span>
</div>
+39 -76
View File
@@ -4,13 +4,15 @@
<div class="content-wrapper">
<div v-if="isLoading" class="loading-wrapper">
<n-spin size="large" />
<p>听书加载中...</p>
<p>{{ t('bilibili.player.loading') }}</p>
</div>
<div v-else-if="errorMessage" class="error-wrapper">
<i class="ri-error-warning-line text-4xl text-red-500"></i>
<p>{{ errorMessage }}</p>
<n-button type="primary" @click="loadVideoSource">重试</n-button>
<n-button type="primary" @click="loadVideoSource">{{
t('bilibili.player.retry')
}}</n-button>
</div>
<div v-else-if="videoDetail" class="bilibili-info-wrapper" :class="mainContentAnimation">
@@ -36,14 +38,16 @@
<template #icon>
<i class="ri-play-fill"></i>
</template>
立即播放
{{ t('bilibili.player.playNow') }}
</n-button>
</div>
</div>
<div class="video-info">
<div class="title">{{ videoDetail?.title || '加载中...' }}</div>
<div
class="title"
v-html="videoDetail?.title || t('bilibili.player.loadingTitle')"
></div>
<div class="author">
<i class="ri-user-line mr-1"></i>
<span>{{ videoDetail.owner?.name }}</span>
@@ -65,7 +69,13 @@
<p>{{ videoDetail.desc }}</p>
</div>
<div class="duration">
<p>总时长: {{ formatTotalDuration(videoDetail.duration) }}</p>
<p>
{{
t('bilibili.player.totalDuration', {
duration: formatTotalDuration(videoDetail.duration)
})
}}
</p>
</div>
</div>
</div>
@@ -76,7 +86,7 @@
:class="partsListAnimation"
>
<div class="parts-title">
分P列表 ({{ videoDetail.pages.length }})
{{ t('bilibili.player.partsList', { count: videoDetail.pages.length }) }}
<n-spin v-if="partLoading" size="small" class="ml-2" />
</div>
<div class="parts-list">
@@ -104,9 +114,15 @@
<script setup lang="ts">
import { useMessage } from 'naive-ui';
import { computed, onMounted, ref, watch } from 'vue';
import { useI18n } from 'vue-i18n';
import { useRoute, useRouter } from 'vue-router';
import { getBilibiliPlayUrl, getBilibiliProxyUrl, getBilibiliVideoDetail } from '@/api/bilibili';
import {
createSongFromBilibiliVideo as createBilibiliSong,
getBilibiliPlayUrl,
getBilibiliProxyUrl,
getBilibiliVideoDetail
} from '@/api/bilibili';
import { usePlayerStore } from '@/store/modules/player';
import type { IBilibiliPage, IBilibiliVideoDetail } from '@/types/bilibili';
import type { SongResult } from '@/types/music';
@@ -121,6 +137,7 @@ const route = useRoute();
const router = useRouter();
const message = useMessage();
const playerStore = usePlayerStore();
const { t } = useI18n();
// 从路由参数获取bvid
const bvid = computed(() => route.params.bvid as string);
@@ -165,7 +182,7 @@ onMounted(async () => {
if (bvid.value) {
await loadVideoDetail(bvid.value);
} else {
message.error('视频ID无效');
message.error(t('bilibili.player.errors.invalidVideoId'));
router.back();
}
});
@@ -193,11 +210,11 @@ const loadVideoDetail = async (bvid: string) => {
await loadVideoSource();
} else {
console.log('视频无分P或分P数据为空');
errorMessage.value = '无法加载视频分P信息';
errorMessage.value = t('bilibili.player.errors.loadPartInfoFailed');
}
} catch (error) {
console.error('获取视频详情失败', error);
errorMessage.value = '获取视频详情失败';
errorMessage.value = t('bilibili.player.errors.loadVideoDetailFailed');
} finally {
isLoading.value = false;
// 标记初始加载完成
@@ -231,33 +248,8 @@ const loadVideoSource = async () => {
return currentAudio;
}
// 其他分P创建占位对象,稍后按需加载
return {
id: `${bvid.value}--${page.page}--${page.cid}`, // 使用bvid--pid--cid作为唯一ID
name: `${page.part || ''} - ${videoDetail.value!.title}`,
picUrl: getBilibiliProxyUrl(videoDetail.value!.pic),
type: 0,
canDislike: false,
alg: '',
source: 'bilibili', // 设置来源为B站
song: {
name: `${page.part || ''} - ${videoDetail.value!.title}`,
id: `${bvid.value}--${page.page}--${page.cid}`,
ar: [
{
name: videoDetail.value!.owner.name,
id: videoDetail.value!.owner.mid
}
],
al: {
picUrl: getBilibiliProxyUrl(videoDetail.value!.pic)
}
} as any,
bilibiliData: {
bvid: bvid.value,
cid: page.cid
}
} as SongResult;
// 其他分P创建占位对象,稍后按需加载 - 使用公用方法
return createBilibiliSong(videoDetail.value!, page, bvid.value);
});
console.log('已生成音频列表,共', audioList.value.length, '首');
@@ -271,7 +263,7 @@ const loadVideoSource = async () => {
}
} catch (error) {
console.error('获取音频播放地址失败', error);
errorMessage.value = '获取音频播放地址失败';
errorMessage.value = t('bilibili.player.errors.loadAudioUrlFailed');
} finally {
isLoading.value = false;
}
@@ -282,37 +274,8 @@ const createSongFromBilibiliVideo = (): SongResult => {
throw new Error('视频详情未加载');
}
const pageName = currentPage.value.part || '';
const title = `${pageName} - ${videoDetail.value.title}`;
return {
id: `${bvid.value}--${currentPage.value.page}--${currentPage.value.cid}`, // 使用bvid--pid--cid作为唯一ID
name: title,
picUrl: getBilibiliProxyUrl(videoDetail.value.pic),
type: 0,
canDislike: false,
alg: '',
// 设置来源为B站
source: 'bilibili',
// playMusicUrl属性稍后通过loadSongUrl函数添加
song: {
name: title,
id: `${bvid.value}--${currentPage.value.page}--${currentPage.value.cid}`,
ar: [
{
name: videoDetail.value.owner.name,
id: videoDetail.value.owner.mid
}
],
al: {
picUrl: getBilibiliProxyUrl(videoDetail.value.pic)
}
} as any,
bilibiliData: {
bvid: bvid.value,
cid: currentPage.value.cid
}
} as SongResult;
// 使用公用方法创建SongResult
return createBilibiliSong(videoDetail.value, currentPage.value, bvid.value);
};
const loadSongUrl = async (
@@ -368,20 +331,20 @@ const switchPage = async (page: IBilibiliPage) => {
playCurrentAudio();
} catch (error) {
console.error('切换分P时加载音频URL失败:', error);
message.error('获取音频地址失败,请重试');
message.error(t('bilibili.player.errors.switchPartFailed'));
} finally {
partLoading.value = false;
}
} else {
console.error('未找到对应的音频项');
message.error('未找到对应的音频,请重试');
message.error(t('bilibili.player.errors.switchPartFailed'));
}
};
const playCurrentAudio = async () => {
if (audioList.value.length === 0) {
console.error('音频列表为空');
errorMessage.value = '音频列表为空,请重试';
errorMessage.value = t('bilibili.player.errors.audioListEmpty');
return;
}
@@ -392,7 +355,7 @@ const playCurrentAudio = async () => {
if (currentIndex === -1) {
console.error('未找到当前分P的音频');
errorMessage.value = '未找到当前分P的音频';
errorMessage.value = t('bilibili.player.errors.currentPartNotFound');
return;
}
@@ -428,7 +391,7 @@ const playCurrentAudio = async () => {
playerStore.setPlay(currentAudio);
// 播放后通知用户已开始播放
message.success('已开始播放');
message.success(t('bilibili.player.playStarted'));
} catch (error) {
console.error('播放音频失败:', error);
errorMessage.value = error instanceof Error ? error.message : '播放失败,请重试';
@@ -604,7 +567,7 @@ watch(
}
.parts-list {
@apply flex flex-wrap gap-2 max-h-60 overflow-y-auto pb-4;
@apply flex flex-wrap gap-2 pb-4;
.part-item {
@apply text-xs mb-2;
+3 -53
View File
@@ -100,7 +100,7 @@ import { computed, onMounted, ref, watch } from 'vue';
import { useI18n } from 'vue-i18n';
import { useRouter } from 'vue-router';
import { getBilibiliProxyUrl, getBilibiliVideoDetail } from '@/api/bilibili';
import { processBilibiliVideos } from '@/api/bilibili';
import { getMusicDetail } from '@/api/music';
import SongItem from '@/components/common/SongItem.vue';
import { useDownload } from '@/hooks/useDownload';
@@ -228,58 +228,8 @@ const getFavoriteSongs = async () => {
}
}
// 处理B站视频数据
const bilibiliSongs: SongResult[] = [];
for (const biliId of bilibiliIds) {
const strBiliId = String(biliId);
console.log(`处理B站ID: ${strBiliId}`);
if (strBiliId.includes('--')) {
// 从ID中提取B站视频信息 (bvid--pid--cid格式)
try {
const [bvid, pid, cid] = strBiliId.split('--');
if (!bvid || !pid || !cid) {
console.warn(`B站ID格式错误: ${strBiliId}, 正确格式应为 bvid--pid--cid`);
continue;
}
const res = await getBilibiliVideoDetail(bvid);
const videoDetail = res.data;
// 找到对应的分P
const page = videoDetail.pages.find((p) => p.cid === Number(cid));
if (!page) {
console.warn(`未找到对应的分P: cid=${cid}`);
continue;
}
const songData = {
id: strBiliId,
name: `${page.part || ''} - ${videoDetail.title}`,
picUrl: getBilibiliProxyUrl(videoDetail.pic),
ar: [
{
name: videoDetail.owner.name,
id: videoDetail.owner.mid
}
],
al: {
name: videoDetail.title,
picUrl: getBilibiliProxyUrl(videoDetail.pic)
},
source: 'bilibili',
bilibiliData: {
bvid,
cid: Number(cid)
}
} as SongResult;
bilibiliSongs.push(songData);
} catch (error) {
console.error(`获取B站视频详情失败 (${strBiliId}):`, error);
}
}
}
// 处理B站视频数据 - 使用公用方法
const bilibiliSongs = await processBilibiliVideos(bilibiliIds);
console.log('获取数据统计:', {
neteaseSongs: neteaseSongs.length,
+16 -37
View File
@@ -35,7 +35,7 @@
import { onMounted, ref, watch } from 'vue';
import { useI18n } from 'vue-i18n';
import { getBilibiliProxyUrl, getBilibiliVideoDetail } from '@/api/bilibili';
import { processBilibiliVideos } from '@/api/bilibili';
import { getMusicDetail } from '@/api/music';
import SongItem from '@/components/common/SongItem.vue';
import { useMusicHistory } from '@/hooks/MusicHistoryHook';
@@ -90,45 +90,24 @@ const getHistorySongs = async () => {
}
}
// 处理B站视频
const bilibiliSongs: SongResult[] = [];
for (const item of bilibiliItems) {
try {
const bvid = item.bilibiliData?.bvid;
if (!bvid) continue;
// 处理B站视频 - 使用公用方法
const bilibiliIds = bilibiliItems
.map((item) => `${item.bilibiliData?.bvid}--1--${item.bilibiliData?.cid}`)
.filter((id) => id && !id.includes('undefined'));
const res = await getBilibiliVideoDetail(bvid);
const videoDetail = res.data;
const bilibiliSongs = await processBilibiliVideos(bilibiliIds);
// 找到对应的分P
const page = videoDetail.pages.find((p) => p.cid === item.bilibiliData?.cid);
if (!page) continue;
bilibiliSongs.push({
id: `${bvid}--${page.page}--${page.cid}`,
name: `${page.part || ''} - ${videoDetail.title}`,
picUrl: getBilibiliProxyUrl(videoDetail.pic),
ar: [
{
name: videoDetail.owner.name,
id: videoDetail.owner.mid
}
],
al: {
name: videoDetail.title,
picUrl: getBilibiliProxyUrl(videoDetail.pic)
},
source: 'bilibili',
count: item.count || 0,
bilibiliData: {
bvid,
cid: page.cid
}
} as SongResult);
} catch (error) {
console.error('获取B站视频详情失败:', error);
// 添加count信息
bilibiliSongs.forEach((song) => {
const historyItem = bilibiliItems.find(
(item) =>
item.bilibiliData?.bvid === song.bilibiliData?.bvid &&
item.bilibiliData?.cid === song.bilibiliData?.cid
);
if (historyItem) {
song.count = historyItem.count || 0;
}
}
});
// 合并两种来源的数据,并保持原有顺序
const newSongs = currentPageItems
+1 -1
View File
@@ -18,7 +18,7 @@
<i class="ri-add-line"></i>
</div>
</n-button-group>
<div>{{ staticData.playMusic.name }}</div>
<div v-html="staticData.playMusic.name"></div>
</div>
<!-- 添加播放控制按钮 -->
<div class="play-controls">
+36 -4
View File
@@ -133,7 +133,13 @@ import { computed, onMounted, ref, watch } from 'vue';
import { useI18n } from 'vue-i18n';
import { useRoute, useRouter } from 'vue-router';
import { getBilibiliProxyUrl, searchBilibili } from '@/api/bilibili';
import {
createSimpleBilibiliSong,
getBilibiliAudioUrl,
getBilibiliProxyUrl,
getBilibiliVideoDetail,
searchBilibili
} from '@/api/bilibili';
import { getHotSearch } from '@/api/home';
import { getSearch } from '@/api/search';
import BilibiliItem from '@/components/common/BilibiliItem.vue';
@@ -424,9 +430,35 @@ const handleSearchHistory = (item: { keyword: string; type: number }) => {
};
// 处理B站视频播放
const handlePlayBilibili = (item: IBilibiliSearchResult) => {
// 使用路由导航到B站播放页面
router.push(`/bilibili/${item.bvid}`);
const handlePlayBilibili = async (item: IBilibiliSearchResult) => {
try {
// 获取视频详情以判断是否为单个视频
const videoDetail = await getBilibiliVideoDetail(item.bvid);
const pages = videoDetail.data.pages;
// 如果是单个视频(只有一个分P),直接播放
if (pages && pages.length === 1) {
// 获取音频URL并播放
const audioUrl = await getBilibiliAudioUrl(item.bvid, pages[0].cid);
// 使用公用方法创建播放项目
const playItem = createSimpleBilibiliSong(item, audioUrl);
playItem.bilibiliData = {
bvid: item.bvid,
cid: pages[0].cid
};
// 添加到播放列表并开始播放
playerStore.setPlay(playItem);
} else {
// 多P视频,跳转到详情页面
router.push(`/bilibili/${item.bvid}`);
}
} catch (error) {
console.error('处理B站视频播放失败:', error);
// 出错时回退到原来的逻辑,跳转详情页
router.push(`/bilibili/${item.bvid}`);
}
};
const handlePlayAll = () => {