Compare commits
7 Commits
fcf8362a15
...
be15a89ad6
Author | SHA1 | Date | |
---|---|---|---|
be15a89ad6 | |||
282af70b74 | |||
b0743178ed | |||
8ee2b928f9 | |||
5be5b4812f | |||
ae2d8875ad | |||
dcf13b2f07 |
13
CLAUDE.md
13
CLAUDE.md
|
@ -83,6 +83,12 @@ npm run qc # Alias for quality-check
|
|||
- **URL Refresh Logic**: Checks resource availability before playback/preload
|
||||
- **Cache Invalidation**: Automatic cleanup when resource URLs change
|
||||
|
||||
### Shuffle/Random Play Logic
|
||||
- **shuffleList**: Array storing the shuffled order of original list indices
|
||||
- **currentIndex**: In shuffle mode, this is the index within shuffleList
|
||||
- **Accessing current song**: `list[shuffleList[currentIndex]]` in shuffle mode
|
||||
- **getNextIndex**: Returns the actual list index of the next song to play
|
||||
|
||||
## File Structure Notes
|
||||
|
||||
### `/src/utils/`
|
||||
|
@ -99,6 +105,13 @@ npm run qc # Alias for quality-check
|
|||
- **content.js**: Injects the Vue app into target websites
|
||||
- **background.js**: Extension background script
|
||||
|
||||
## Code Style and Formatting
|
||||
|
||||
### Indentation
|
||||
- **This project uses Tab indentation (not spaces)**
|
||||
- Ensure all code edits maintain consistent Tab indentation
|
||||
- When editing files, preserve the existing Tab character formatting
|
||||
|
||||
## Development Considerations
|
||||
|
||||
### Extension Context
|
||||
|
|
15
src/assets/icons/soundwave.vue
Normal file
15
src/assets/icons/soundwave.vue
Normal file
|
@ -0,0 +1,15 @@
|
|||
<script setup lang="ts">
|
||||
defineProps<{
|
||||
size: number
|
||||
}>()
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="currentColor" :class="`w-${size} h-${size}`">
|
||||
<rect x="3" y="9" width="2" height="6" rx="1"></rect>
|
||||
<rect x="7" y="5" width="2" height="14" rx="1"></rect>
|
||||
<rect x="11" y="7" width="2" height="10" rx="1"></rect>
|
||||
<rect x="15" y="4" width="2" height="16" rx="1"></rect>
|
||||
<rect x="19" y="10" width="2" height="4" rx="1"></rect>
|
||||
</svg>
|
||||
</template>
|
|
@ -2,11 +2,8 @@
|
|||
import { artistsOrganize } from '../utils'
|
||||
import { ref } from 'vue'
|
||||
import { useFavourites } from '../stores/useFavourites'
|
||||
import apis from '../apis'
|
||||
import axios from 'axios'
|
||||
|
||||
import StarSlashIcon from '../assets/icons/starslash.vue'
|
||||
import { onMounted } from 'vue'
|
||||
|
||||
const favourites = useFavourites()
|
||||
|
||||
|
@ -20,27 +17,6 @@ const props = defineProps<{
|
|||
const emit = defineEmits<{
|
||||
(e: 'play', index: number): void
|
||||
}>()
|
||||
|
||||
onMounted(async () => {
|
||||
try {
|
||||
// 添加缓存控制头和随机参数来避免缓存
|
||||
await axios.head(props.item.song.sourceUrl ?? '', {
|
||||
headers: {
|
||||
'Cache-Control': 'no-cache, no-store, must-revalidate',
|
||||
'Pragma': 'no-cache',
|
||||
'Expires': '0'
|
||||
},
|
||||
params: {
|
||||
_t: Date.now() // 添加时间戳参数避免缓存
|
||||
}
|
||||
})
|
||||
} catch (error) {
|
||||
// 刷新资源地址
|
||||
const updatedSong = await apis.getSong(props.item.song.cid)
|
||||
console.log('Updated song:', updatedSong)
|
||||
favourites.updateSongInFavourites(props.item.song.cid, updatedSong)
|
||||
}
|
||||
})
|
||||
</script>
|
||||
|
||||
<template>
|
||||
|
|
|
@ -1,10 +1,11 @@
|
|||
<script setup lang="ts">
|
||||
import { usePlayQueueStore } from '../stores/usePlayQueueStore'
|
||||
import { artistsOrganize } from '../utils'
|
||||
import { artistsOrganize, supportsWebAudioVisualization } from '../utils'
|
||||
|
||||
import XIcon from '../assets/icons/x.vue'
|
||||
import UpHyphenIcon from '../assets/icons/uphypen.vue'
|
||||
import DownHyphenIcon from '../assets/icons/downhyphen.vue'
|
||||
import SoundwaveIcon from '../assets/icons/soundwave.vue'
|
||||
|
||||
import { ref } from 'vue'
|
||||
|
||||
|
@ -18,6 +19,9 @@ const playQueueStore = usePlayQueueStore()
|
|||
|
||||
const hover = ref(false)
|
||||
|
||||
// 检查浏览器是否支持音频可视化
|
||||
const isAudioVisualizationSupported = supportsWebAudioVisualization()
|
||||
|
||||
function moveUp() {
|
||||
if (props.index === 0) return
|
||||
|
||||
|
@ -156,12 +160,14 @@ function removeItem() {
|
|||
<img :src="queueItem.album?.coverUrl" />
|
||||
<div class="w-full h-full absolute top-0 left-0 bg-neutral-900/75 flex justify-center items-center"
|
||||
v-if="isCurrent">
|
||||
<div style="height: 1rem;" class="flex justify-center items-center gap-[.125rem]">
|
||||
<!-- 在支持的浏览器上显示可视化,否则显示音波图标 -->
|
||||
<div v-if="isAudioVisualizationSupported" style="height: 1rem;" class="flex justify-center items-center gap-[.125rem]">
|
||||
<div class="bg-white w-[.125rem] rounded-full" v-for="(bar, index) in playQueueStore.visualizer"
|
||||
:key="index" :style="{
|
||||
height: `${Math.max(10, bar)}%`
|
||||
}" />
|
||||
</div>
|
||||
<SoundwaveIcon v-else :size="6" class="text-white animate-pulse" />
|
||||
</div>
|
||||
</div>
|
||||
<div class="flex flex-col text-left flex-auto w-0">
|
||||
|
|
|
@ -1,15 +1,17 @@
|
|||
<!-- Player.vue - 添加预加载功能 -->
|
||||
<script setup lang="ts">
|
||||
|
||||
import { usePlayQueueStore } from '../stores/usePlayQueueStore'
|
||||
import { useTemplateRef, watch, nextTick, computed } from 'vue'
|
||||
import { computed, nextTick, ref, useTemplateRef, watch } from 'vue'
|
||||
import { useRoute } from 'vue-router'
|
||||
import { useFavourites } from '../stores/useFavourites'
|
||||
import { usePlayQueueStore } from '../stores/usePlayQueueStore'
|
||||
|
||||
import PlayIcon from '../assets/icons/play.vue'
|
||||
import LoadingIndicator from '../assets/icons/loadingindicator.vue'
|
||||
import { audioVisualizer } from '../utils'
|
||||
import PlayIcon from '../assets/icons/play.vue'
|
||||
import PauseIcon from '../assets/icons/pause.vue'
|
||||
import { audioVisualizer, checkAndRefreshSongResource, supportsWebAudioVisualization } from '../utils'
|
||||
|
||||
const playQueueStore = usePlayQueueStore()
|
||||
const favourites = useFavourites()
|
||||
const route = useRoute()
|
||||
const player = useTemplateRef('playerRef')
|
||||
|
||||
|
@ -17,15 +19,20 @@ const player = useTemplateRef('playerRef')
|
|||
console.log('[Player] 检查 store 方法:', {
|
||||
preloadNext: typeof playQueueStore.preloadNext,
|
||||
getPreloadedAudio: typeof playQueueStore.getPreloadedAudio,
|
||||
clearPreloadedAudio: typeof playQueueStore.clearPreloadedAudio
|
||||
clearPreloadedAudio: typeof playQueueStore.clearPreloadedAudio,
|
||||
})
|
||||
|
||||
// 获取当前歌曲的计算属性
|
||||
const currentTrack = computed(() => {
|
||||
if (playQueueStore.playMode.shuffle && playQueueStore.shuffleList.length > 0) {
|
||||
return playQueueStore.list[playQueueStore.shuffleList[playQueueStore.currentIndex]]
|
||||
if (
|
||||
playQueueStore.playMode.shuffle &&
|
||||
playQueueStore.shuffleList.length > 0
|
||||
) {
|
||||
return playQueueStore.list[
|
||||
playQueueStore.shuffleList[playQueueStore.currentIndex]
|
||||
]
|
||||
}
|
||||
|
||||
|
||||
return playQueueStore.list[playQueueStore.currentIndex]
|
||||
})
|
||||
|
||||
|
@ -35,86 +42,132 @@ const currentAudioSrc = computed(() => {
|
|||
return track ? track.song.sourceUrl : ''
|
||||
})
|
||||
|
||||
watch(() => playQueueStore.isPlaying, (newValue) => {
|
||||
if (newValue) {
|
||||
player.value?.play()
|
||||
setMetadata()
|
||||
}
|
||||
else { player.value?.pause() }
|
||||
})
|
||||
watch(
|
||||
() => playQueueStore.isPlaying,
|
||||
(newValue) => {
|
||||
if (newValue) {
|
||||
player.value?.play()
|
||||
setMetadata()
|
||||
} else {
|
||||
player.value?.pause()
|
||||
}
|
||||
},
|
||||
)
|
||||
|
||||
// 监听当前索引变化,处理预加载逻辑
|
||||
watch(() => playQueueStore.currentIndex, async () => {
|
||||
console.log('[Player] 当前索引变化:', playQueueStore.currentIndex)
|
||||
watch(
|
||||
() => playQueueStore.currentIndex,
|
||||
async () => {
|
||||
console.log('[Player] 当前索引变化:', playQueueStore.currentIndex)
|
||||
|
||||
// 检查是否可以使用预加载的音频
|
||||
const track = currentTrack.value
|
||||
if (track) {
|
||||
const songId = track.song.cid
|
||||
// 检查是否可以使用预加载的音频
|
||||
const track = currentTrack.value
|
||||
if (track) {
|
||||
const songId = track.song.cid
|
||||
|
||||
try {
|
||||
const preloadedAudio = playQueueStore.getPreloadedAudio(songId)
|
||||
try {
|
||||
// 首先检查和刷新当前歌曲的资源
|
||||
console.log('[Player] 检查当前歌曲资源:', track.song.name)
|
||||
const updatedSong = await checkAndRefreshSongResource(
|
||||
track.song,
|
||||
(updated) => {
|
||||
// 更新播放队列中的歌曲信息
|
||||
// 在随机播放模式下,currentIndex 是 shuffleList 的索引
|
||||
// 需要通过 shuffleList[currentIndex] 获取实际的 list 索引
|
||||
const actualIndex =
|
||||
playQueueStore.playMode.shuffle &&
|
||||
playQueueStore.shuffleList.length > 0
|
||||
? playQueueStore.shuffleList[playQueueStore.currentIndex]
|
||||
: playQueueStore.currentIndex
|
||||
if (playQueueStore.list[actualIndex]) {
|
||||
playQueueStore.list[actualIndex].song = updated
|
||||
}
|
||||
// 如果歌曲在收藏夹中,也更新收藏夹
|
||||
favourites.updateSongInFavourites(songId, updated)
|
||||
},
|
||||
)
|
||||
|
||||
if (preloadedAudio) {
|
||||
console.log(`[Player] 使用预加载的音频: ${track.song.name}`)
|
||||
// 使用更新后的歌曲信息
|
||||
const preloadedAudio = playQueueStore.getPreloadedAudio(songId)
|
||||
|
||||
// 直接使用预加载的音频数据
|
||||
if (player.value) {
|
||||
// 复制预加载音频的状态到主播放器
|
||||
player.value.src = preloadedAudio.src
|
||||
player.value.currentTime = 0
|
||||
if (preloadedAudio && updatedSong.sourceUrl === track.song.sourceUrl) {
|
||||
console.log(`[Player] 使用预加载的音频: ${track.song.name}`)
|
||||
|
||||
// 清理使用过的预加载音频
|
||||
playQueueStore.clearPreloadedAudio(songId)
|
||||
// 直接使用预加载的音频数据
|
||||
if (player.value) {
|
||||
// 复制预加载音频的状态到主播放器
|
||||
player.value.src = preloadedAudio.src
|
||||
player.value.currentTime = 0
|
||||
|
||||
// 如果正在播放状态,立即播放
|
||||
if (playQueueStore.isPlaying) {
|
||||
await nextTick()
|
||||
player.value.play().catch(console.error)
|
||||
// 清理使用过的预加载音频
|
||||
playQueueStore.clearPreloadedAudio(songId)
|
||||
|
||||
// 如果正在播放状态,立即播放
|
||||
if (playQueueStore.isPlaying) {
|
||||
await nextTick()
|
||||
player.value.play().catch(console.error)
|
||||
}
|
||||
|
||||
playQueueStore.isBuffering = false
|
||||
}
|
||||
} else {
|
||||
console.log(`[Player] 正常加载音频: ${track.song.name}`)
|
||||
playQueueStore.isBuffering = true
|
||||
|
||||
playQueueStore.isBuffering = false
|
||||
// 如果资源地址已更新,清除旧的预加载音频
|
||||
if (updatedSong.sourceUrl !== track.song.sourceUrl) {
|
||||
playQueueStore.clearPreloadedAudio(songId)
|
||||
}
|
||||
}
|
||||
} else {
|
||||
console.log(`[Player] 正常加载音频: ${track.song.name}`)
|
||||
} catch (error) {
|
||||
console.error('[Player] 处理预加载音频时出错:', error)
|
||||
playQueueStore.isBuffering = true
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('[Player] 处理预加载音频时出错:', error)
|
||||
playQueueStore.isBuffering = true
|
||||
}
|
||||
}
|
||||
|
||||
setMetadata()
|
||||
setMetadata()
|
||||
|
||||
// 延迟预加载下一首歌,避免影响当前歌曲加载
|
||||
setTimeout(() => {
|
||||
try {
|
||||
console.log('[Player] 尝试预加载下一首歌')
|
||||
// 延迟预加载下一首歌,避免影响当前歌曲加载
|
||||
setTimeout(async () => {
|
||||
try {
|
||||
console.log('[Player] 尝试预加载下一首歌')
|
||||
|
||||
// 检查函数是否存在
|
||||
if (typeof playQueueStore.preloadNext === 'function') {
|
||||
playQueueStore.preloadNext()
|
||||
playQueueStore.limitPreloadCache()
|
||||
} else {
|
||||
console.error('[Player] preloadNext 不是一个函数')
|
||||
// 检查函数是否存在
|
||||
if (typeof playQueueStore.preloadNext === 'function') {
|
||||
await playQueueStore.preloadNext()
|
||||
|
||||
// 预加载完成后,检查播放队列是否有更新,同步到收藏夹
|
||||
playQueueStore.list.forEach((item) => {
|
||||
if (favourites.isFavourite(item.song.cid)) {
|
||||
favourites.updateSongInFavourites(item.song.cid, item.song)
|
||||
}
|
||||
})
|
||||
|
||||
playQueueStore.limitPreloadCache()
|
||||
} else {
|
||||
console.error('[Player] preloadNext 不是一个函数')
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('[Player] 预加载失败:', error)
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('[Player] 预加载失败:', error)
|
||||
}
|
||||
}, 1000)
|
||||
})
|
||||
}, 1000)
|
||||
},
|
||||
)
|
||||
|
||||
function artistsOrganize(list: string[]) {
|
||||
if (list.length === 0) { return '未知音乐人' }
|
||||
return list.map((artist) => {
|
||||
return artist
|
||||
}).join(' / ')
|
||||
if (list.length === 0) {
|
||||
return '未知音乐人'
|
||||
}
|
||||
return list
|
||||
.map((artist) => {
|
||||
return artist
|
||||
})
|
||||
.join(' / ')
|
||||
}
|
||||
|
||||
function setMetadata() {
|
||||
if ('mediaSession' in navigator) {
|
||||
let current = currentTrack.value
|
||||
const current = currentTrack.value
|
||||
if (!current) return
|
||||
|
||||
navigator.mediaSession.metadata = new MediaMetadata({
|
||||
|
@ -122,8 +175,12 @@ function setMetadata() {
|
|||
artist: artistsOrganize(current.song.artists ?? []),
|
||||
album: current.album?.name,
|
||||
artwork: [
|
||||
{ src: current.album?.coverUrl ?? '', sizes: '500x500', type: 'image/png' },
|
||||
]
|
||||
{
|
||||
src: current.album?.coverUrl ?? '',
|
||||
sizes: '500x500',
|
||||
type: 'image/png',
|
||||
},
|
||||
],
|
||||
})
|
||||
|
||||
navigator.mediaSession.setActionHandler('previoustrack', playPrevious)
|
||||
|
@ -133,16 +190,21 @@ function setMetadata() {
|
|||
playQueueStore.currentTime = player.value?.currentTime ?? 0
|
||||
}
|
||||
|
||||
watch(() => playQueueStore.updatedCurrentTime, (newValue) => {
|
||||
if (newValue === null) { return }
|
||||
if (player.value) player.value.currentTime = newValue
|
||||
playQueueStore.updatedCurrentTime = null
|
||||
})
|
||||
watch(
|
||||
() => playQueueStore.updatedCurrentTime,
|
||||
(newValue) => {
|
||||
if (newValue === null) {
|
||||
return
|
||||
}
|
||||
if (player.value) player.value.currentTime = newValue
|
||||
playQueueStore.updatedCurrentTime = null
|
||||
},
|
||||
)
|
||||
}
|
||||
|
||||
function playNext() {
|
||||
if (playQueueStore.currentIndex === playQueueStore.list.length - 1) {
|
||||
console.log("at the bottom, pause")
|
||||
console.log('at the bottom, pause')
|
||||
playQueueStore.currentIndex = 0
|
||||
if (playQueueStore.playMode.repeat === 'all') {
|
||||
playQueueStore.currentIndex = 0
|
||||
|
@ -158,11 +220,17 @@ function playNext() {
|
|||
}
|
||||
|
||||
function playPrevious() {
|
||||
if (player.value && (player.value.currentTime ?? 0) < 5 && playQueueStore.currentIndex > 0) {
|
||||
if (
|
||||
player.value &&
|
||||
(player.value.currentTime ?? 0) < 5 &&
|
||||
playQueueStore.currentIndex > 0
|
||||
) {
|
||||
playQueueStore.currentIndex--
|
||||
playQueueStore.isPlaying = true
|
||||
} else {
|
||||
if (player.value) { player.value.currentTime = 0 }
|
||||
if (player.value) {
|
||||
player.value.currentTime = 0
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -179,8 +247,10 @@ function updateCurrentTime() {
|
|||
const preloadTrigger = (config.preloadTrigger || 50) / 100 // 转换为小数
|
||||
const remainingTimeThreshold = config.remainingTimeThreshold || 30
|
||||
|
||||
if ((progress > preloadTrigger || remainingTime < remainingTimeThreshold) && !playQueueStore.isPreloading) {
|
||||
|
||||
if (
|
||||
(progress > preloadTrigger || remainingTime < remainingTimeThreshold) &&
|
||||
!playQueueStore.isPreloading
|
||||
) {
|
||||
try {
|
||||
if (typeof playQueueStore.preloadNext === 'function') {
|
||||
playQueueStore.preloadNext()
|
||||
|
@ -194,115 +264,160 @@ function updateCurrentTime() {
|
|||
}
|
||||
}
|
||||
|
||||
console.log('[Player] 初始化 audioVisualizer')
|
||||
const { barHeights, connectAudio, isAnalyzing, error } = audioVisualizer({
|
||||
sensitivity: 1.5,
|
||||
barCount: 6,
|
||||
maxDecibels: -10,
|
||||
bassBoost: 0.8,
|
||||
midBoost: 1.2,
|
||||
trebleBoost: 1.4,
|
||||
threshold: 0
|
||||
})
|
||||
// 检查浏览器是否支持音频可视化
|
||||
const isAudioVisualizationSupported = supportsWebAudioVisualization()
|
||||
console.log('[Player] 音频可视化支持状态:', isAudioVisualizationSupported)
|
||||
|
||||
console.log('[Player] audioVisualizer 返回值:', { barHeights: barHeights.value, isAnalyzing: isAnalyzing.value })
|
||||
// 只在支持的浏览器上初始化音频可视化
|
||||
let barHeights = ref<number[]>([0, 0, 0, 0, 0, 0])
|
||||
let connectAudio = (_audio: HTMLAudioElement) => {}
|
||||
let isAnalyzing = ref(false)
|
||||
let error = ref<string | null>(null)
|
||||
|
||||
if (isAudioVisualizationSupported) {
|
||||
console.log('[Player] 初始化 audioVisualizer')
|
||||
const visualizer = audioVisualizer({
|
||||
sensitivity: 1.5,
|
||||
barCount: 6,
|
||||
maxDecibels: -10,
|
||||
bassBoost: 0.8,
|
||||
midBoost: 1.2,
|
||||
trebleBoost: 1.4,
|
||||
threshold: 0,
|
||||
})
|
||||
|
||||
barHeights = visualizer.barHeights
|
||||
connectAudio = visualizer.connectAudio
|
||||
isAnalyzing = visualizer.isAnalyzing
|
||||
error = visualizer.error
|
||||
|
||||
console.log('[Player] audioVisualizer 返回值:', {
|
||||
barHeights: barHeights.value,
|
||||
isAnalyzing: isAnalyzing.value,
|
||||
})
|
||||
} else {
|
||||
console.log('[Player] 音频可视化被禁用(Safari 或不支持的浏览器)')
|
||||
}
|
||||
|
||||
// 监听播放列表变化
|
||||
watch(() => playQueueStore.list.length, async (newLength) => {
|
||||
console.log('[Player] 播放列表长度变化:', newLength)
|
||||
if (newLength === 0) {
|
||||
console.log('[Player] 播放列表为空,跳过连接')
|
||||
return
|
||||
}
|
||||
|
||||
// 等待下一帧,确保 audio 元素已经渲染
|
||||
await nextTick()
|
||||
|
||||
if (player.value) {
|
||||
console.log('[Player] 连接音频元素到可视化器')
|
||||
console.log('[Player] 音频元素状态:', {
|
||||
src: player.value.src?.substring(0, 50) + '...',
|
||||
readyState: player.value.readyState,
|
||||
paused: player.value.paused
|
||||
})
|
||||
connectAudio(player.value)
|
||||
} else {
|
||||
console.log('[Player] ❌ 音频元素不存在')
|
||||
}
|
||||
|
||||
playQueueStore.visualizer = barHeights.value
|
||||
|
||||
// 开始预加载第一首歌的下一首
|
||||
setTimeout(() => {
|
||||
playQueueStore.preloadNext()
|
||||
}, 2000)
|
||||
|
||||
// 初始化音量
|
||||
if (player.value) {
|
||||
initializeVolume()
|
||||
}
|
||||
})
|
||||
|
||||
// 监听音频元素变化
|
||||
watch(() => player.value, (audioElement) => {
|
||||
if (audioElement && playQueueStore.list.length > 0) {
|
||||
connectAudio(audioElement)
|
||||
}
|
||||
})
|
||||
|
||||
// 监听可视化器数据变化
|
||||
watch(() => barHeights.value, (newHeights) => {
|
||||
playQueueStore.visualizer = newHeights
|
||||
}, { deep: true })
|
||||
|
||||
// 监听错误
|
||||
watch(() => error.value, (newError) => {
|
||||
if (newError) {
|
||||
console.error('[Player] 可视化器错误:', newError)
|
||||
}
|
||||
})
|
||||
|
||||
// 切换播放模式
|
||||
watch(() => playQueueStore.playMode.shuffle, (isShuffle) => {
|
||||
if (isShuffle) {
|
||||
const currentIndex = playQueueStore.currentIndex
|
||||
const trackCount = playQueueStore.list.length
|
||||
|
||||
// 1. 已播放部分:不变
|
||||
let shuffledList = [...Array(currentIndex).keys()]
|
||||
|
||||
// 2. 构建待打乱的列表
|
||||
let shuffleSpace = [...Array(trackCount).keys()].filter(index =>
|
||||
playQueueStore.shuffleCurrent ? index >= currentIndex : index > currentIndex
|
||||
)
|
||||
|
||||
// 3. 随机打乱
|
||||
shuffleSpace.sort(() => Math.random() - 0.5)
|
||||
|
||||
// 4. 如果当前曲目不参与打乱,插入回当前位置(即 currentIndex 处)
|
||||
if (!playQueueStore.shuffleCurrent) {
|
||||
shuffledList.push(currentIndex)
|
||||
watch(
|
||||
() => playQueueStore.list.length,
|
||||
async (newLength) => {
|
||||
console.log('[Player] 播放列表长度变化:', newLength)
|
||||
if (newLength === 0) {
|
||||
console.log('[Player] 播放列表为空,跳过连接')
|
||||
return
|
||||
}
|
||||
|
||||
// 5. 拼接:已播放部分 + 当前(可选)+ 打乱后的剩余部分
|
||||
shuffledList = shuffledList.concat(shuffleSpace)
|
||||
// 等待下一帧,确保 audio 元素已经渲染
|
||||
await nextTick()
|
||||
|
||||
// 6. 应用 shuffleList
|
||||
playQueueStore.shuffleList = shuffledList
|
||||
if (player.value) {
|
||||
if (isAudioVisualizationSupported) {
|
||||
console.log('[Player] 连接音频元素到可视化器')
|
||||
console.log('[Player] 音频元素状态:', {
|
||||
src: player.value.src?.substring(0, 50) + '...',
|
||||
readyState: player.value.readyState,
|
||||
paused: player.value.paused,
|
||||
})
|
||||
connectAudio(player.value)
|
||||
} else {
|
||||
console.log('[Player] 跳过音频可视化连接(不支持的浏览器)')
|
||||
}
|
||||
} else {
|
||||
console.log('[Player] ❌ 音频元素不存在')
|
||||
}
|
||||
|
||||
// 清除 shuffleCurrent 状态
|
||||
playQueueStore.shuffleCurrent = undefined
|
||||
} else {
|
||||
// 退出随机播放:恢复当前播放曲目的原索引
|
||||
playQueueStore.currentIndex = playQueueStore.shuffleList[playQueueStore.currentIndex]
|
||||
}
|
||||
playQueueStore.visualizer = barHeights.value
|
||||
|
||||
// 切换播放模式后重新预加载
|
||||
setTimeout(() => {
|
||||
playQueueStore.clearAllPreloadedAudio()
|
||||
playQueueStore.preloadNext()
|
||||
}, 500)
|
||||
})
|
||||
// 开始预加载第一首歌的下一首
|
||||
setTimeout(() => {
|
||||
playQueueStore.preloadNext()
|
||||
}, 2000)
|
||||
|
||||
// 初始化音量
|
||||
if (player.value) {
|
||||
initializeVolume()
|
||||
}
|
||||
},
|
||||
)
|
||||
|
||||
// 监听音频元素变化
|
||||
watch(
|
||||
() => player.value,
|
||||
(audioElement) => {
|
||||
if (audioElement && playQueueStore.list.length > 0 && isAudioVisualizationSupported) {
|
||||
connectAudio(audioElement)
|
||||
}
|
||||
},
|
||||
)
|
||||
|
||||
// 监听可视化器数据变化
|
||||
watch(
|
||||
() => barHeights.value,
|
||||
(newHeights) => {
|
||||
playQueueStore.visualizer = newHeights
|
||||
},
|
||||
{ deep: true },
|
||||
)
|
||||
|
||||
// 监听错误
|
||||
watch(
|
||||
() => error.value,
|
||||
(newError) => {
|
||||
if (newError) {
|
||||
console.error('[Player] 可视化器错误:', newError)
|
||||
}
|
||||
},
|
||||
)
|
||||
|
||||
// 切换播放模式
|
||||
watch(
|
||||
() => playQueueStore.playMode.shuffle,
|
||||
(isShuffle) => {
|
||||
if (isShuffle) {
|
||||
const currentIndex = playQueueStore.currentIndex
|
||||
const trackCount = playQueueStore.list.length
|
||||
|
||||
// 1. 已播放部分:不变
|
||||
let shuffledList = [...Array(currentIndex).keys()]
|
||||
|
||||
// 2. 构建待打乱的列表
|
||||
const shuffleSpace = [...Array(trackCount).keys()].filter((index) =>
|
||||
playQueueStore.shuffleCurrent
|
||||
? index >= currentIndex
|
||||
: index > currentIndex,
|
||||
)
|
||||
|
||||
// 3. 随机打乱
|
||||
shuffleSpace.sort(() => Math.random() - 0.5)
|
||||
|
||||
// 4. 如果当前曲目不参与打乱,插入回当前位置(即 currentIndex 处)
|
||||
if (!playQueueStore.shuffleCurrent) {
|
||||
shuffledList.push(currentIndex)
|
||||
}
|
||||
|
||||
// 5. 拼接:已播放部分 + 当前(可选)+ 打乱后的剩余部分
|
||||
shuffledList = shuffledList.concat(shuffleSpace)
|
||||
|
||||
// 6. 应用 shuffleList
|
||||
playQueueStore.shuffleList = shuffledList
|
||||
|
||||
// 清除 shuffleCurrent 状态
|
||||
playQueueStore.shuffleCurrent = undefined
|
||||
} else {
|
||||
// 退出随机播放:恢复当前播放曲目的原索引
|
||||
playQueueStore.currentIndex =
|
||||
playQueueStore.shuffleList[playQueueStore.currentIndex]
|
||||
}
|
||||
|
||||
// 切换播放模式后重新预加载
|
||||
setTimeout(() => {
|
||||
playQueueStore.clearAllPreloadedAudio()
|
||||
playQueueStore.preloadNext()
|
||||
}, 500)
|
||||
},
|
||||
)
|
||||
|
||||
function getCurrentTrack() {
|
||||
return currentTrack.value
|
||||
|
@ -313,7 +428,7 @@ function initializeVolume() {
|
|||
if (player.value) {
|
||||
const savedVolume = localStorage.getItem('audioVolume')
|
||||
if (savedVolume) {
|
||||
const volumeValue = parseFloat(savedVolume)
|
||||
const volumeValue = Number.parseFloat(savedVolume)
|
||||
player.value.volume = volumeValue
|
||||
console.log('[Player] 初始化音量:', volumeValue)
|
||||
} else {
|
||||
|
@ -339,7 +454,7 @@ function syncVolumeFromStorage() {
|
|||
if (player.value) {
|
||||
const savedVolume = localStorage.getItem('audioVolume')
|
||||
if (savedVolume) {
|
||||
const volumeValue = parseFloat(savedVolume)
|
||||
const volumeValue = Number.parseFloat(savedVolume)
|
||||
if (player.value.volume !== volumeValue) {
|
||||
player.value.volume = volumeValue
|
||||
}
|
||||
|
@ -404,12 +519,14 @@ setInterval(syncVolumeFromStorage, 100)
|
|||
}">
|
||||
<div v-if="playQueueStore.isPlaying">
|
||||
<LoadingIndicator v-if="playQueueStore.isBuffering === true" :size="4" />
|
||||
<div v-else class="h-4 flex justify-center items-center gap-[.125rem]">
|
||||
<!-- 在支持的浏览器上显示可视化,否则显示暂停图标 -->
|
||||
<div v-else-if="isAudioVisualizationSupported" class="h-4 flex justify-center items-center gap-[.125rem]">
|
||||
<div class="bg-white/75 w-[.125rem] rounded-full" v-for="(bar, index) in playQueueStore.visualizer"
|
||||
:key="index" :style="{
|
||||
height: `${Math.max(10, bar)}%`
|
||||
}" />
|
||||
</div>
|
||||
<PauseIcon v-else :size="4" />
|
||||
</div>
|
||||
<PlayIcon v-else :size="4" />
|
||||
</button>
|
||||
|
|
|
@ -1,5 +1,6 @@
|
|||
import { defineStore } from "pinia"
|
||||
import { ref, computed } from "vue"
|
||||
import { defineStore } from 'pinia'
|
||||
import { computed, ref } from 'vue'
|
||||
import { checkAndRefreshSongResource } from '../utils'
|
||||
|
||||
export const usePlayQueueStore = defineStore('queue', () => {
|
||||
const list = ref<QueueItem[]>([])
|
||||
|
@ -13,11 +14,11 @@ export const usePlayQueueStore = defineStore('queue', () => {
|
|||
const visualizer = ref<number[]>([0, 0, 0, 0, 0, 0])
|
||||
const shuffleList = ref<number[]>([])
|
||||
const playMode = ref<{
|
||||
shuffle: boolean,
|
||||
shuffle: boolean
|
||||
repeat: 'off' | 'single' | 'all'
|
||||
}>({
|
||||
shuffle: false,
|
||||
repeat: 'off'
|
||||
repeat: 'off',
|
||||
})
|
||||
const shuffleCurrent = ref<boolean | undefined>(undefined)
|
||||
|
||||
|
@ -35,10 +36,13 @@ export const usePlayQueueStore = defineStore('queue', () => {
|
|||
}
|
||||
|
||||
if (playMode.value.shuffle && shuffleList.value.length > 0) {
|
||||
const currentShuffleIndex = shuffleList.value.indexOf(currentIndex.value)
|
||||
// 当前在 shuffleList 中的位置
|
||||
const currentShuffleIndex = currentIndex.value
|
||||
if (currentShuffleIndex < shuffleList.value.length - 1) {
|
||||
// 返回下一个位置对应的原始 list 索引
|
||||
return shuffleList.value[currentShuffleIndex + 1]
|
||||
} else if (playMode.value.repeat === 'all') {
|
||||
// 返回第一个位置对应的原始 list 索引
|
||||
return shuffleList.value[0]
|
||||
}
|
||||
return -1
|
||||
|
@ -55,19 +59,14 @@ export const usePlayQueueStore = defineStore('queue', () => {
|
|||
|
||||
// 预加载下一首歌
|
||||
const preloadNext = async () => {
|
||||
|
||||
const nextIndex = getNextIndex.value
|
||||
if (nextIndex === -1) {
|
||||
return
|
||||
}
|
||||
|
||||
// 获取下一首歌曲对象
|
||||
let nextSong
|
||||
if (playMode.value.shuffle && shuffleList.value.length > 0) {
|
||||
nextSong = list.value[shuffleList.value[nextIndex]]
|
||||
} else {
|
||||
nextSong = list.value[nextIndex]
|
||||
}
|
||||
// nextIndex 已经是原始 list 中的索引
|
||||
const nextSong = list.value[nextIndex]
|
||||
|
||||
if (!nextSong || !nextSong.song) {
|
||||
return
|
||||
|
@ -89,6 +88,24 @@ export const usePlayQueueStore = defineStore('queue', () => {
|
|||
isPreloading.value = true
|
||||
preloadProgress.value = 0
|
||||
|
||||
// 在预加载前检查和刷新资源
|
||||
console.log('[Store] 预加载前检查资源:', nextSong.song.name)
|
||||
const updatedSong = await checkAndRefreshSongResource(
|
||||
nextSong.song,
|
||||
(updated) => {
|
||||
// 更新播放队列中的歌曲信息
|
||||
// nextIndex 已经是原始 list 中的索引
|
||||
if (list.value[nextIndex]) {
|
||||
list.value[nextIndex].song = updated
|
||||
}
|
||||
|
||||
// 如果歌曲在收藏夹中,也更新收藏夹
|
||||
// 注意:这里不直接导入 favourites store 以避免循环依赖
|
||||
// 改为触发一个事件或者在调用方处理
|
||||
console.log('[Store] 预加载时需要更新收藏夹:', updated.name)
|
||||
},
|
||||
)
|
||||
|
||||
const audio = new Audio()
|
||||
audio.preload = 'auto'
|
||||
audio.crossOrigin = 'anonymous'
|
||||
|
@ -107,19 +124,20 @@ export const usePlayQueueStore = defineStore('queue', () => {
|
|||
preloadedAudio.value.set(songId, audio)
|
||||
isPreloading.value = false
|
||||
preloadProgress.value = 100
|
||||
console.log('[Store] 预加载完成:', updatedSong.name)
|
||||
})
|
||||
|
||||
// 监听加载错误
|
||||
audio.addEventListener('error', (e) => {
|
||||
console.error(`[Store] 预加载音频失败: ${e}`)
|
||||
console.error(`[Store] 预加载音频失败: ${updatedSong.name}`, e)
|
||||
isPreloading.value = false
|
||||
preloadProgress.value = 0
|
||||
})
|
||||
|
||||
// 设置音频源并开始加载
|
||||
audio.src = nextSong.song.sourceUrl
|
||||
|
||||
// 使用更新后的音频源
|
||||
audio.src = updatedSong.sourceUrl!
|
||||
} catch (error) {
|
||||
console.error('[Store] 预加载过程出错:', error)
|
||||
isPreloading.value = false
|
||||
}
|
||||
}
|
||||
|
@ -167,7 +185,7 @@ export const usePlayQueueStore = defineStore('queue', () => {
|
|||
progress: preloadProgress.value,
|
||||
cacheSize: preloadedAudio.value.size,
|
||||
cachedSongs: Array.from(preloadedAudio.value.keys()),
|
||||
nextIndex: getNextIndex.value
|
||||
nextIndex: getNextIndex.value,
|
||||
})
|
||||
}
|
||||
|
||||
|
@ -194,6 +212,6 @@ export const usePlayQueueStore = defineStore('queue', () => {
|
|||
clearPreloadedAudio,
|
||||
clearAllPreloadedAudio,
|
||||
limitPreloadCache,
|
||||
debugPreloadState
|
||||
debugPreloadState,
|
||||
}
|
||||
})
|
||||
})
|
||||
|
|
98
src/utils/browserDetection.ts
Normal file
98
src/utils/browserDetection.ts
Normal file
|
@ -0,0 +1,98 @@
|
|||
/**
|
||||
* 浏览器检测工具
|
||||
*/
|
||||
|
||||
/**
|
||||
* 检测是否为 Safari 浏览器
|
||||
* @returns {boolean} 如果是 Safari 返回 true,否则返回 false
|
||||
*/
|
||||
export function isSafari(): boolean {
|
||||
const ua = navigator.userAgent.toLowerCase()
|
||||
|
||||
// 检测 Safari 浏览器(包括 iOS 和 macOS)
|
||||
// Safari 的 User Agent 包含 'safari' 但不包含 'chrome' 或 'chromium'
|
||||
const isSafariBrowser = ua.includes('safari') &&
|
||||
!ua.includes('chrome') &&
|
||||
!ua.includes('chromium') &&
|
||||
!ua.includes('android')
|
||||
|
||||
// 额外检查:使用 Safari 特有的 API
|
||||
const isSafariByFeature = 'safari' in window ||
|
||||
/^((?!chrome|android).)*safari/i.test(navigator.userAgent)
|
||||
|
||||
return isSafariBrowser || isSafariByFeature
|
||||
}
|
||||
|
||||
/**
|
||||
* 检测是否为移动版 Safari
|
||||
* @returns {boolean} 如果是移动版 Safari 返回 true,否则返回 false
|
||||
*/
|
||||
export function isMobileSafari(): boolean {
|
||||
return /iPad|iPhone|iPod/.test(navigator.userAgent) && !window.MSStream
|
||||
}
|
||||
|
||||
/**
|
||||
* 检测是否支持 Web Audio API 的完整功能
|
||||
* @returns {boolean} 如果支持返回 true,否则返回 false
|
||||
*/
|
||||
export function supportsWebAudioVisualization(): boolean {
|
||||
// Safari 在某些情况下对 AudioContext 的支持有限制
|
||||
// 特别是在处理跨域音频资源时
|
||||
if (isSafari()) {
|
||||
console.log('[BrowserDetection] Safari detected, audio visualization disabled')
|
||||
return false
|
||||
}
|
||||
|
||||
// 检查基本的 Web Audio API 支持
|
||||
const hasAudioContext = 'AudioContext' in window || 'webkitAudioContext' in window
|
||||
const hasAnalyserNode = hasAudioContext && (
|
||||
'AnalyserNode' in window ||
|
||||
(window.AudioContext && 'createAnalyser' in AudioContext.prototype)
|
||||
)
|
||||
|
||||
return hasAudioContext && hasAnalyserNode
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取浏览器信息
|
||||
* @returns {object} 包含浏览器类型和版本信息的对象
|
||||
*/
|
||||
export function getBrowserInfo() {
|
||||
const ua = navigator.userAgent
|
||||
let browserName = 'Unknown'
|
||||
let browserVersion = 'Unknown'
|
||||
|
||||
if (isSafari()) {
|
||||
browserName = 'Safari'
|
||||
const versionMatch = ua.match(/Version\/(\d+\.\d+)/)
|
||||
if (versionMatch) {
|
||||
browserVersion = versionMatch[1]
|
||||
}
|
||||
} else if (ua.includes('Chrome')) {
|
||||
browserName = 'Chrome'
|
||||
const versionMatch = ua.match(/Chrome\/(\d+\.\d+)/)
|
||||
if (versionMatch) {
|
||||
browserVersion = versionMatch[1]
|
||||
}
|
||||
} else if (ua.includes('Firefox')) {
|
||||
browserName = 'Firefox'
|
||||
const versionMatch = ua.match(/Firefox\/(\d+\.\d+)/)
|
||||
if (versionMatch) {
|
||||
browserVersion = versionMatch[1]
|
||||
}
|
||||
} else if (ua.includes('Edge')) {
|
||||
browserName = 'Edge'
|
||||
const versionMatch = ua.match(/Edge\/(\d+\.\d+)/)
|
||||
if (versionMatch) {
|
||||
browserVersion = versionMatch[1]
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
name: browserName,
|
||||
version: browserVersion,
|
||||
isSafari: isSafari(),
|
||||
isMobileSafari: isMobileSafari(),
|
||||
supportsAudioVisualization: supportsWebAudioVisualization()
|
||||
}
|
||||
}
|
|
@ -1,5 +1,17 @@
|
|||
import artistsOrganize from "./artistsOrganize"
|
||||
import { audioVisualizer } from "./audioVisualizer"
|
||||
import cicdInfo from "./cicdInfo"
|
||||
import { checkAndRefreshSongResource, checkAndRefreshMultipleSongs } from "./songResourceChecker"
|
||||
import { isSafari, isMobileSafari, supportsWebAudioVisualization, getBrowserInfo } from "./browserDetection"
|
||||
|
||||
export { artistsOrganize, audioVisualizer, cicdInfo }
|
||||
export {
|
||||
artistsOrganize,
|
||||
audioVisualizer,
|
||||
cicdInfo,
|
||||
checkAndRefreshSongResource,
|
||||
checkAndRefreshMultipleSongs,
|
||||
isSafari,
|
||||
isMobileSafari,
|
||||
supportsWebAudioVisualization,
|
||||
getBrowserInfo
|
||||
}
|
||||
|
|
80
src/utils/songResourceChecker.ts
Normal file
80
src/utils/songResourceChecker.ts
Normal file
|
@ -0,0 +1,80 @@
|
|||
import axios from 'axios'
|
||||
import apis from '../apis'
|
||||
|
||||
/**
|
||||
* 检查歌曲资源 URL 是否可用,如果不可用则刷新
|
||||
* @param song 要检查的歌曲对象
|
||||
* @param updateCallback 更新歌曲信息的回调函数
|
||||
* @returns 更新后的歌曲对象(如果需要更新)或原始歌曲对象
|
||||
*/
|
||||
export const checkAndRefreshSongResource = async (
|
||||
song: Song,
|
||||
updateCallback?: (updatedSong: Song) => void
|
||||
): Promise<Song> => {
|
||||
if (!song.sourceUrl) {
|
||||
console.warn('[ResourceChecker] 歌曲没有 sourceUrl:', song.name)
|
||||
return song
|
||||
}
|
||||
|
||||
try {
|
||||
// 检查资源是否可用
|
||||
await axios.head(song.sourceUrl, {
|
||||
headers: {
|
||||
'Cache-Control': 'no-cache, no-store, must-revalidate',
|
||||
'Pragma': 'no-cache',
|
||||
'Expires': '0'
|
||||
},
|
||||
params: {
|
||||
_t: Date.now() // 添加时间戳参数避免缓存
|
||||
},
|
||||
timeout: 5000 // 5秒超时
|
||||
})
|
||||
|
||||
// 资源可用,返回原始歌曲
|
||||
console.log('[ResourceChecker] 资源可用:', song.name)
|
||||
return song
|
||||
} catch (error) {
|
||||
// 资源不可用,刷新歌曲信息
|
||||
console.log('[ResourceChecker] 资源不可用,正在刷新:', song.name, error)
|
||||
|
||||
try {
|
||||
const updatedSong = await apis.getSong(song.cid)
|
||||
console.log('[ResourceChecker] 歌曲信息已刷新:', updatedSong.name)
|
||||
|
||||
// 调用更新回调(如果提供)
|
||||
if (updateCallback) {
|
||||
updateCallback(updatedSong)
|
||||
}
|
||||
|
||||
return updatedSong
|
||||
} catch (refreshError) {
|
||||
console.error('[ResourceChecker] 刷新歌曲信息失败:', refreshError)
|
||||
// 刷新失败,返回原始歌曲
|
||||
return song
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 批量检查多首歌曲的资源
|
||||
* @param songs 要检查的歌曲数组
|
||||
* @param updateCallback 更新单首歌曲信息的回调函数
|
||||
* @returns 更新后的歌曲数组
|
||||
*/
|
||||
export const checkAndRefreshMultipleSongs = async (
|
||||
songs: Song[],
|
||||
updateCallback?: (updatedSong: Song, originalIndex: number) => void
|
||||
): Promise<Song[]> => {
|
||||
const results: Song[] = []
|
||||
|
||||
for (let i = 0; i < songs.length; i++) {
|
||||
const originalSong = songs[i]
|
||||
const updatedSong = await checkAndRefreshSongResource(
|
||||
originalSong,
|
||||
updateCallback ? (updated) => updateCallback(updated, i) : undefined
|
||||
)
|
||||
results.push(updatedSong)
|
||||
}
|
||||
|
||||
return results
|
||||
}
|
Loading…
Reference in New Issue
Block a user