Files
BYW-VoiceHub/VoiceHub-main/app/composables/useAudioPlayer.ts
T
Henry_Du 4ce4649aaf
Build VoiceHub FPK / build-fpk (push) Failing after 56m5s
Build and Push Docker Image / build-and-push (push) Canceled after 1h9m35s
初次提交
2026-09-06 08:19:02 +08:00

398 lines
12 KiB
TypeScript

import { computed, readonly, ref } from 'vue'
import { isBilibiliSong } from '~/utils/bilibiliSource'
export interface PlayableSong {
id: number
title: string
artist: string
musicUrl?: string | null
cover?: string | null
musicPlatform?: string
musicId?: string
playUrl?: string | null
}
interface PlaylistItem {
id: number
song: PlayableSong
playDate?: string
playTimeId?: number
}
// 创建一个单例实例
const currentSong = ref<PlayableSong | null>(null)
const isPlaying = ref(false)
const currentPlaylist = ref<PlaylistItem[]>([])
const currentPlaylistIndex = ref(-1)
const currentPosition = ref(0) // 当前播放位置(秒)
const duration = ref(0) // 歌曲总时长(秒)
export function useAudioPlayer() {
// 播放歌曲
const playSong = (song: PlayableSong, playlist?: PlayableSong[], playlistIndex?: number) => {
// 对于哔哩哔哩视频,允许没有 musicUrl(会显示 iframe 预览)
if (!song.musicUrl && !isBilibiliSong(song)) {
console.error('歌曲没有可播放的URL')
return false
}
// 如果提供了播放列表,更新播放列表
if (playlist && playlist.length > 0) {
// 将PlayableSong数组转换为PlaylistItem数组
currentPlaylist.value = playlist.map((s) => ({ song: s }))
// 使用提供的索引或查找当前歌曲在播放列表中的位置
if (
typeof playlistIndex === 'number' &&
playlistIndex >= 0 &&
playlistIndex < playlist.length
) {
currentPlaylistIndex.value = playlistIndex
} else {
const index = playlist.findIndex((item) => item.id === song.id)
currentPlaylistIndex.value = index >= 0 ? index : -1
}
} else if (currentSong.value && currentSong.value.id !== song.id) {
// 如果没有提供播放列表且是新歌曲,清空播放列表
currentPlaylist.value = []
currentPlaylistIndex.value = -1
}
// 如果是同一首歌,则开始播放(不切换状态,由调用方决定)
if (currentSong.value && currentSong.value.id === song.id) {
// 保持当前歌曲的引用更新,以便获取可能更新的 URL 等信息
currentSong.value = song
isPlaying.value = true
return true
}
// 播放新歌曲
currentSong.value = song
isPlaying.value = true
currentPosition.value = 0
duration.value = 0 // 重置时长,等待新歌曲加载后更新
return true
}
// 暂停播放
const pauseSong = () => {
isPlaying.value = false
}
// 停止播放
const stopSong = () => {
isPlaying.value = false
currentSong.value = null
currentPosition.value = 0
duration.value = 0
}
// 播放下一首歌曲
const playNext = async () => {
if (currentPlaylist.value.length === 0 || currentPlaylistIndex.value === -1) {
console.log('没有播放列表或当前歌曲不在播放列表中')
return false
}
let nextIndex = currentPlaylistIndex.value + 1
let attempts = 0
const maxAttempts = currentPlaylist.value.length // 最多尝试整个播放列表的长度
// 循环尝试找到可播放的下一首歌曲
while (nextIndex < currentPlaylist.value.length && attempts < maxAttempts) {
const nextSong = currentPlaylist.value[nextIndex].song
// 如果已经有播放URL,直接使用
if (nextSong.musicUrl) {
currentPlaylistIndex.value = nextIndex
currentSong.value = nextSong
currentPosition.value = 0
duration.value = 0 // 重置时长
isPlaying.value = true
return true
}
// 如果没有播放URL,尝试动态获取
if (nextSong.musicPlatform && nextSong.musicId) {
try {
const { getMusicUrl } = await import('~/utils/musicUrl')
// 检查是否为播客内容
const isPodcast =
nextSong.musicPlatform === 'netease-podcast' ||
nextSong.sourceInfo?.type === 'voice' ||
(nextSong.sourceInfo?.source === 'netease-backup' &&
nextSong.sourceInfo?.type === 'voice')
const options = isPodcast ? { unblock: false } : {}
const url = await getMusicUrl(
nextSong.musicPlatform,
nextSong.musicId,
nextSong.playUrl,
options
)
if (url) {
nextSong.musicUrl = url
currentPlaylistIndex.value = nextIndex
currentSong.value = nextSong
currentPosition.value = 0
duration.value = 0 // 重置时长
isPlaying.value = true
return true
} else {
const msg = `无法获取《${nextSong.title}》的播放链接,已自动跳过`
console.warn(msg)
if (window.$showNotification) {
window.$showNotification(msg, 'warning')
}
}
} catch (error) {
const msg = `《${nextSong.title}》获取播放链接失败,已自动跳过`
console.warn(msg, error)
if (window.$showNotification) {
window.$showNotification(msg, 'error')
}
}
} else {
const msg = `《${nextSong.title}》缺少播放信息,已自动跳过`
console.warn(msg)
if (window.$showNotification) {
window.$showNotification(msg, 'warning')
}
}
// 尝试下一首
nextIndex++
attempts++
}
console.log('没有找到可播放的下一首歌曲')
return false
}
// 播放上一首歌曲
const playPrevious = async () => {
if (currentPlaylist.value.length === 0 || currentPlaylistIndex.value === -1) {
console.log('没有播放列表或当前歌曲不在播放列表中')
return false
}
let prevIndex = currentPlaylistIndex.value - 1
let attempts = 0
const maxAttempts = currentPlaylist.value.length // 最多尝试整个播放列表的长度
// 循环尝试找到可播放的上一首歌曲
while (prevIndex >= 0 && attempts < maxAttempts) {
const prevSong = currentPlaylist.value[prevIndex].song
// 如果已经有播放URL,直接使用
if (prevSong.musicUrl) {
currentPlaylistIndex.value = prevIndex
currentSong.value = prevSong
currentPosition.value = 0
duration.value = 0 // 重置时长
isPlaying.value = true
return true
}
// 如果没有播放URL,尝试动态获取
if (prevSong.musicPlatform && prevSong.musicId) {
try {
const { getMusicUrl } = await import('~/utils/musicUrl')
// 检查是否为播客内容
const isPodcast =
prevSong.musicPlatform === 'netease-podcast' ||
prevSong.sourceInfo?.type === 'voice' ||
(prevSong.sourceInfo?.source === 'netease-backup' &&
prevSong.sourceInfo?.type === 'voice')
const options = isPodcast ? { unblock: false } : {}
const url = await getMusicUrl(
prevSong.musicPlatform,
prevSong.musicId,
prevSong.playUrl,
options
)
if (url) {
prevSong.musicUrl = url
currentPlaylistIndex.value = prevIndex
currentSong.value = prevSong
currentPosition.value = 0
duration.value = 0 // 重置时长
isPlaying.value = true
return true
} else {
console.warn(`跳过第${prevIndex + 1}首歌曲:无法获取播放链接,可能是付费内容`)
}
} catch (error) {
console.warn(`跳过第${prevIndex + 1}首歌曲:获取播放URL失败`, error)
}
} else {
console.warn(`跳过第${prevIndex + 1}首歌曲:缺少音乐平台或ID信息`)
}
// 尝试上一首
prevIndex--
attempts++
}
console.log('没有找到可播放的上一首歌曲')
return false
}
// 设置播放位置
const setPosition = (position: number) => {
currentPosition.value = Math.max(0, Math.min(position, duration.value))
}
// 设置歌曲总时长
const setDuration = (newDuration: number) => {
duration.value = Math.max(0, newDuration)
}
// 更新播放位置(由音频元素的timeupdate事件调用)
const updatePosition = (position: number) => {
currentPosition.value = position
}
// 检查指定ID的歌曲是否正在播放
const isCurrentPlaying = (songId: number) => {
return isPlaying.value && currentSong.value && currentSong.value.id === songId
}
// 检查指定ID的歌曲是否为当前歌曲(不管是否在播放)
const isCurrentSong = (songId: number) => {
return currentSong.value && currentSong.value.id === songId
}
// 检查是否有下一首歌曲
const hasNext = computed(() => {
return (
currentPlaylist.value.length > 0 &&
currentPlaylistIndex.value >= 0 &&
currentPlaylistIndex.value < currentPlaylist.value.length - 1
)
})
// 检查是否有上一首歌曲
const hasPrevious = computed(() => {
return currentPlaylist.value.length > 0 && currentPlaylistIndex.value > 0
})
// 提前预加载下一首歌曲的URL
let isPrefetching = false
const prefetchNextSong = async () => {
if (isPrefetching || !hasNext.value) return false
const nextIndex = currentPlaylistIndex.value + 1
const nextSong = currentPlaylist.value[nextIndex]?.song
if (!nextSong || nextSong.musicUrl || !nextSong.musicPlatform || !nextSong.musicId) {
return false
}
isPrefetching = true
try {
const { getMusicUrl } = await import('~/utils/musicUrl')
const isPodcast =
nextSong.musicPlatform === 'netease-podcast' ||
nextSong.sourceInfo?.type === 'voice' ||
(nextSong.sourceInfo?.source === 'netease-backup' &&
nextSong.sourceInfo?.type === 'voice')
const options = isPodcast ? { unblock: false } : {}
const url = await getMusicUrl(
nextSong.musicPlatform,
nextSong.musicId,
nextSong.playUrl,
options
)
if (url) {
nextSong.musicUrl = url
console.log(`成功预加载下一首歌曲: ${nextSong.title}`)
}
} catch (error) {
console.warn(`预加载下一首歌曲《${nextSong.title}》失败:`, error)
// 注意:这里不显示错误通知,失败会在真正尝试播放时处理并提示
} finally {
isPrefetching = false
}
return true
}
// 获取当前播放列表
const getCurrentPlaylist = () => {
return readonly(currentPlaylist)
}
// 获取当前播放列表索引
const getCurrentPlaylistIndex = () => {
return readonly(currentPlaylistIndex)
}
// 获取当前播放的歌曲信息
const getCurrentSong = () => {
return readonly(currentSong)
}
// 获取当前播放状态
const getPlayingStatus = () => {
return readonly(isPlaying)
}
// 获取当前播放位置
const getCurrentPosition = () => {
return readonly(currentPosition)
}
// 获取歌曲总时长
const getDuration = () => {
return readonly(duration)
}
// 获取播放进度百分比
const getProgress = computed(() => {
if (duration.value === 0) return 0
return (currentPosition.value / duration.value) * 100
})
// 更新当前歌曲信息(用于音质切换等场景)
const updateCurrentSong = (updatedSong: PlayableSong) => {
if (currentSong.value && currentSong.value.id === updatedSong.id) {
currentSong.value = updatedSong
// 同时更新播放列表中的对应项
if (currentPlaylistIndex.value !== -1 && currentPlaylist.value[currentPlaylistIndex.value]) {
currentPlaylist.value[currentPlaylistIndex.value].song = updatedSong
}
return true
}
return false
}
return {
playSong,
pauseSong,
stopSong,
playNext,
playPrevious,
updateCurrentSong,
setPosition,
setDuration,
updatePosition,
isCurrentPlaying,
isCurrentSong,
getCurrentSong,
getPlayingStatus,
getCurrentPosition,
getDuration,
getProgress,
hasNext,
hasPrevious,
prefetchNextSong,
getCurrentPlaylist,
getCurrentPlaylistIndex
}
}