From 67370b90727b1d0cb8cd8ecc31826cf33f6e9897 Mon Sep 17 00:00:00 2001 From: algerkong Date: Sat, 20 Sep 2025 16:40:45 +0800 Subject: [PATCH] =?UTF-8?q?feat:=20bili=E6=92=AD=E6=94=BE=E4=BC=98?= =?UTF-8?q?=E5=8C=96?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/i18n/lang/en-US/bilibili.ts | 51 ++++ src/i18n/lang/ja-JP/bilibili.ts | 51 ++++ src/i18n/lang/ko-KR/bilibili.ts | 51 ++++ src/i18n/lang/zh-CN/bilibili.ts | 51 ++++ src/i18n/lang/zh-Hant/bilibili.ts | 51 ++++ src/renderer/api/bilibili.ts | 227 +++++++++++++++++- .../components/common/BilibiliItem.vue | 6 +- src/renderer/components/lyric/MusicFull.vue | 8 +- .../components/lyric/MusicFullMobile.vue | 6 +- .../components/player/MiniPlayBar.vue | 2 +- src/renderer/components/player/PlayBar.vue | 2 +- .../views/bilibili/BilibiliPlayer.vue | 115 +++------ src/renderer/views/favorite/index.vue | 56 +---- src/renderer/views/history/index.vue | 53 ++-- src/renderer/views/lyric/index.vue | 2 +- src/renderer/views/search/index.vue | 40 ++- 16 files changed, 590 insertions(+), 182 deletions(-) create mode 100644 src/i18n/lang/en-US/bilibili.ts create mode 100644 src/i18n/lang/ja-JP/bilibili.ts create mode 100644 src/i18n/lang/ko-KR/bilibili.ts create mode 100644 src/i18n/lang/zh-CN/bilibili.ts create mode 100644 src/i18n/lang/zh-Hant/bilibili.ts diff --git a/src/i18n/lang/en-US/bilibili.ts b/src/i18n/lang/en-US/bilibili.ts new file mode 100644 index 0000000..ae49d95 --- /dev/null +++ b/src/i18n/lang/en-US/bilibili.ts @@ -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' + } + } +}; diff --git a/src/i18n/lang/ja-JP/bilibili.ts b/src/i18n/lang/ja-JP/bilibili.ts new file mode 100644 index 0000000..47efa3e --- /dev/null +++ b/src/i18n/lang/ja-JP/bilibili.ts @@ -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: '次のパートのプリロードに失敗しました' + } + } +}; diff --git a/src/i18n/lang/ko-KR/bilibili.ts b/src/i18n/lang/ko-KR/bilibili.ts new file mode 100644 index 0000000..efdc811 --- /dev/null +++ b/src/i18n/lang/ko-KR/bilibili.ts @@ -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: '다음 파트 미리 로딩 실패' + } + } +}; diff --git a/src/i18n/lang/zh-CN/bilibili.ts b/src/i18n/lang/zh-CN/bilibili.ts new file mode 100644 index 0000000..3d4d16f --- /dev/null +++ b/src/i18n/lang/zh-CN/bilibili.ts @@ -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失败' + } + } +}; diff --git a/src/i18n/lang/zh-Hant/bilibili.ts b/src/i18n/lang/zh-Hant/bilibili.ts new file mode 100644 index 0000000..a3bff8f --- /dev/null +++ b/src/i18n/lang/zh-Hant/bilibili.ts @@ -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失敗' + } + } +}; diff --git a/src/renderer/api/bilibili.ts b/src/renderer/api/bilibili.ts index b5e0efb..c0aca42 100644 --- a/src/renderer/api/bilibili.ts +++ b/src/renderer/api/bilibili.ts @@ -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 { + 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 => { + 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; +}; diff --git a/src/renderer/components/common/BilibiliItem.vue b/src/renderer/components/common/BilibiliItem.vue index 7ef5684..19d8fe3 100644 --- a/src/renderer/components/common/BilibiliItem.vue +++ b/src/renderer/components/common/BilibiliItem.vue @@ -19,8 +19,12 @@