466 lines
14 KiB
TypeScript
466 lines
14 KiB
TypeScript
import { computed, readonly, ref } from 'vue'
|
|
import { isBilibiliSong } from '~/utils/bilibiliSource'
|
|
import { useMusicSources } from '~/composables/useMusicSources'
|
|
import { useLyricSettings } from '~/composables/useLyricSettings'
|
|
import type { MusicTrackMeta } from '~/utils/musicUrl'
|
|
|
|
export interface PlayableSong {
|
|
id: number | string
|
|
title: string
|
|
artist: string
|
|
album?: string | null
|
|
duration?: number | null
|
|
musicUrl?: string | null
|
|
cover?: string | null
|
|
musicPlatform?: string
|
|
musicId?: string
|
|
playUrl?: string | null
|
|
sourceInfo?: Record<string, any>
|
|
}
|
|
|
|
interface PlaylistItem {
|
|
id: number | string
|
|
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) // 歌曲总时长(秒)
|
|
|
|
// 预载中的封面图引用,防止加载完成前被回收
|
|
let pendingCoverImage: HTMLImageElement | null = null
|
|
|
|
export function useAudioPlayer() {
|
|
// 播放歌曲
|
|
const playSong = (song: PlayableSong, playlist?: PlayableSong[], playlistIndex?: number) => {
|
|
// 允许没有 musicUrl 但有 musicPlatform+musicId 的歌曲通过(由 loadSong 动态解析)
|
|
if (!song.musicUrl && !isBilibiliSong(song) && !(song.musicPlatform && song.musicId)) {
|
|
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) => String(item.id) === String(song.id))
|
|
currentPlaylistIndex.value = index >= 0 ? index : -1
|
|
}
|
|
} else if (currentSong.value && String(currentSong.value.id) !== String(song.id)) {
|
|
// 如果没有提供播放列表且是新歌曲,清空播放列表
|
|
currentPlaylist.value = []
|
|
currentPlaylistIndex.value = -1
|
|
}
|
|
|
|
// 如果是同一首歌,则开始播放(不切换状态,由调用方决定)
|
|
if (currentSong.value && String(currentSong.value.id) === String(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: { unblock?: boolean; musicInfo?: MusicTrackMeta } = isPodcast
|
|
? { unblock: false }
|
|
: {}
|
|
options.musicInfo = {
|
|
name: nextSong.title,
|
|
artist: nextSong.artist,
|
|
album: nextSong.album || undefined
|
|
}
|
|
|
|
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: { unblock?: boolean; musicInfo?: MusicTrackMeta } = isPodcast
|
|
? { unblock: false }
|
|
: {}
|
|
options.musicInfo = {
|
|
name: prevSong.title,
|
|
artist: prevSong.artist,
|
|
album: prevSong.album || undefined
|
|
}
|
|
|
|
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 | string) => {
|
|
return isPlaying.value && currentSong.value && String(currentSong.value.id) === String(songId)
|
|
}
|
|
|
|
// 检查指定ID的歌曲是否为当前歌曲(不管是否在播放)
|
|
const isCurrentSong = (songId: number | string) => {
|
|
return currentSong.value && String(currentSong.value.id) === String(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
|
|
})
|
|
|
|
// 预载下一首歌曲的封面与歌词(切歌时直接命中缓存,减少等待)
|
|
const preloadNextSongAssets = (song: PlayableSong) => {
|
|
if (typeof window === 'undefined') return
|
|
|
|
// 封面预载
|
|
if (song.cover) {
|
|
const img = new Image()
|
|
const clear = () => {
|
|
if (pendingCoverImage === img) pendingCoverImage = null
|
|
}
|
|
img.onload = clear
|
|
img.onerror = clear
|
|
img.src = song.cover
|
|
pendingCoverImage = img
|
|
}
|
|
|
|
// 歌词预载:静默请求,参数与正式取词路径一致以命中 getLyrics 的请求缓存
|
|
if (song.musicPlatform && song.musicId) {
|
|
try {
|
|
const { getLyrics } = useMusicSources()
|
|
getLyrics(song.musicPlatform as 'netease' | 'tencent', song.musicId, {
|
|
title: song.title,
|
|
artist: song.artist,
|
|
album: song.album || undefined,
|
|
duration: song.duration || undefined,
|
|
priority: useLyricSettings().lyricPriority.value
|
|
}).catch(() => {})
|
|
} catch (error) {
|
|
console.warn('[AudioPlayer] 歌词预载失败:', error)
|
|
}
|
|
}
|
|
}
|
|
|
|
// 提前预加载下一首歌曲的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) return false
|
|
|
|
// 封面与歌词预载不依赖播放 URL,先行触发
|
|
preloadNextSongAssets(nextSong)
|
|
|
|
if (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: { unblock?: boolean; musicInfo?: MusicTrackMeta } = isPodcast
|
|
? { unblock: false }
|
|
: {}
|
|
options.musicInfo = {
|
|
name: nextSong.title,
|
|
artist: nextSong.artist,
|
|
album: nextSong.album || undefined
|
|
}
|
|
|
|
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 && String(currentSong.value.id) === String(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
|
|
}
|
|
}
|