Compare commits

..

No commits in common. "be15a89ad6dc630ebc68611706e855c002defa51" and "fcf8362a156eff1d3d9f6c3234e41912f75ca45a" have entirely different histories.

9 changed files with 214 additions and 549 deletions

View File

@ -83,12 +83,6 @@ npm run qc # Alias for quality-check
- **URL Refresh Logic**: Checks resource availability before playback/preload - **URL Refresh Logic**: Checks resource availability before playback/preload
- **Cache Invalidation**: Automatic cleanup when resource URLs change - **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 ## File Structure Notes
### `/src/utils/` ### `/src/utils/`
@ -105,13 +99,6 @@ npm run qc # Alias for quality-check
- **content.js**: Injects the Vue app into target websites - **content.js**: Injects the Vue app into target websites
- **background.js**: Extension background script - **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 ## Development Considerations
### Extension Context ### Extension Context

View File

@ -1,15 +0,0 @@
<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>

View File

@ -2,8 +2,11 @@
import { artistsOrganize } from '../utils' import { artistsOrganize } from '../utils'
import { ref } from 'vue' import { ref } from 'vue'
import { useFavourites } from '../stores/useFavourites' import { useFavourites } from '../stores/useFavourites'
import apis from '../apis'
import axios from 'axios'
import StarSlashIcon from '../assets/icons/starslash.vue' import StarSlashIcon from '../assets/icons/starslash.vue'
import { onMounted } from 'vue'
const favourites = useFavourites() const favourites = useFavourites()
@ -17,6 +20,27 @@ const props = defineProps<{
const emit = defineEmits<{ const emit = defineEmits<{
(e: 'play', index: number): void (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> </script>
<template> <template>

View File

@ -1,11 +1,10 @@
<script setup lang="ts"> <script setup lang="ts">
import { usePlayQueueStore } from '../stores/usePlayQueueStore' import { usePlayQueueStore } from '../stores/usePlayQueueStore'
import { artistsOrganize, supportsWebAudioVisualization } from '../utils' import { artistsOrganize } from '../utils'
import XIcon from '../assets/icons/x.vue' import XIcon from '../assets/icons/x.vue'
import UpHyphenIcon from '../assets/icons/uphypen.vue' import UpHyphenIcon from '../assets/icons/uphypen.vue'
import DownHyphenIcon from '../assets/icons/downhyphen.vue' import DownHyphenIcon from '../assets/icons/downhyphen.vue'
import SoundwaveIcon from '../assets/icons/soundwave.vue'
import { ref } from 'vue' import { ref } from 'vue'
@ -19,9 +18,6 @@ const playQueueStore = usePlayQueueStore()
const hover = ref(false) const hover = ref(false)
//
const isAudioVisualizationSupported = supportsWebAudioVisualization()
function moveUp() { function moveUp() {
if (props.index === 0) return if (props.index === 0) return
@ -160,14 +156,12 @@ function removeItem() {
<img :src="queueItem.album?.coverUrl" /> <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" <div class="w-full h-full absolute top-0 left-0 bg-neutral-900/75 flex justify-center items-center"
v-if="isCurrent"> 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" <div class="bg-white w-[.125rem] rounded-full" v-for="(bar, index) in playQueueStore.visualizer"
:key="index" :style="{ :key="index" :style="{
height: `${Math.max(10, bar)}%` height: `${Math.max(10, bar)}%`
}" /> }" />
</div> </div>
<SoundwaveIcon v-else :size="6" class="text-white animate-pulse" />
</div> </div>
</div> </div>
<div class="flex flex-col text-left flex-auto w-0"> <div class="flex flex-col text-left flex-auto w-0">

View File

@ -1,17 +1,15 @@
<!-- Player.vue - 添加预加载功能 --> <!-- Player.vue - 添加预加载功能 -->
<script setup lang="ts"> <script setup lang="ts">
import { computed, nextTick, ref, useTemplateRef, watch } from 'vue'
import { useRoute } from 'vue-router'
import { useFavourites } from '../stores/useFavourites'
import { usePlayQueueStore } from '../stores/usePlayQueueStore'
import LoadingIndicator from '../assets/icons/loadingindicator.vue' import { usePlayQueueStore } from '../stores/usePlayQueueStore'
import { useTemplateRef, watch, nextTick, computed } from 'vue'
import { useRoute } from 'vue-router'
import PlayIcon from '../assets/icons/play.vue' import PlayIcon from '../assets/icons/play.vue'
import PauseIcon from '../assets/icons/pause.vue' import LoadingIndicator from '../assets/icons/loadingindicator.vue'
import { audioVisualizer, checkAndRefreshSongResource, supportsWebAudioVisualization } from '../utils' import { audioVisualizer } from '../utils'
const playQueueStore = usePlayQueueStore() const playQueueStore = usePlayQueueStore()
const favourites = useFavourites()
const route = useRoute() const route = useRoute()
const player = useTemplateRef('playerRef') const player = useTemplateRef('playerRef')
@ -19,20 +17,15 @@ const player = useTemplateRef('playerRef')
console.log('[Player] 检查 store 方法:', { console.log('[Player] 检查 store 方法:', {
preloadNext: typeof playQueueStore.preloadNext, preloadNext: typeof playQueueStore.preloadNext,
getPreloadedAudio: typeof playQueueStore.getPreloadedAudio, getPreloadedAudio: typeof playQueueStore.getPreloadedAudio,
clearPreloadedAudio: typeof playQueueStore.clearPreloadedAudio, clearPreloadedAudio: typeof playQueueStore.clearPreloadedAudio
}) })
// //
const currentTrack = computed(() => { const currentTrack = computed(() => {
if ( if (playQueueStore.playMode.shuffle && playQueueStore.shuffleList.length > 0) {
playQueueStore.playMode.shuffle && return playQueueStore.list[playQueueStore.shuffleList[playQueueStore.currentIndex]]
playQueueStore.shuffleList.length > 0
) {
return playQueueStore.list[
playQueueStore.shuffleList[playQueueStore.currentIndex]
]
} }
return playQueueStore.list[playQueueStore.currentIndex] return playQueueStore.list[playQueueStore.currentIndex]
}) })
@ -42,132 +35,86 @@ const currentAudioSrc = computed(() => {
return track ? track.song.sourceUrl : '' return track ? track.song.sourceUrl : ''
}) })
watch( watch(() => playQueueStore.isPlaying, (newValue) => {
() => playQueueStore.isPlaying, if (newValue) {
(newValue) => { player.value?.play()
if (newValue) { setMetadata()
player.value?.play() }
setMetadata() else { player.value?.pause() }
} else { })
player.value?.pause()
}
},
)
// //
watch( watch(() => playQueueStore.currentIndex, async () => {
() => playQueueStore.currentIndex, console.log('[Player] 当前索引变化:', playQueueStore.currentIndex)
async () => {
console.log('[Player] 当前索引变化:', playQueueStore.currentIndex)
// 使 // 使
const track = currentTrack.value const track = currentTrack.value
if (track) { if (track) {
const songId = track.song.cid const songId = track.song.cid
try { try {
// const preloadedAudio = playQueueStore.getPreloadedAudio(songId)
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) {
const preloadedAudio = playQueueStore.getPreloadedAudio(songId) console.log(`[Player] 使用预加载的音频: ${track.song.name}`)
if (preloadedAudio && updatedSong.sourceUrl === track.song.sourceUrl) { // 使
console.log(`[Player] 使用预加载的音频: ${track.song.name}`) if (player.value) {
//
player.value.src = preloadedAudio.src
player.value.currentTime = 0
// 使 // 使
if (player.value) { playQueueStore.clearPreloadedAudio(songId)
//
player.value.src = preloadedAudio.src
player.value.currentTime = 0
// 使 //
playQueueStore.clearPreloadedAudio(songId) if (playQueueStore.isPlaying) {
await nextTick()
// player.value.play().catch(console.error)
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)
}
} }
} catch (error) { } else {
console.error('[Player] 处理预加载音频时出错:', error) console.log(`[Player] 正常加载音频: ${track.song.name}`)
playQueueStore.isBuffering = true playQueueStore.isBuffering = true
} }
} catch (error) {
console.error('[Player] 处理预加载音频时出错:', error)
playQueueStore.isBuffering = true
} }
}
setMetadata() setMetadata()
// //
setTimeout(async () => { setTimeout(() => {
try { try {
console.log('[Player] 尝试预加载下一首歌') console.log('[Player] 尝试预加载下一首歌')
// //
if (typeof playQueueStore.preloadNext === 'function') { if (typeof playQueueStore.preloadNext === 'function') {
await playQueueStore.preloadNext() playQueueStore.preloadNext()
playQueueStore.limitPreloadCache()
// } else {
playQueueStore.list.forEach((item) => { console.error('[Player] preloadNext 不是一个函数')
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)
} }
}, 1000) } catch (error) {
}, console.error('[Player] 预加载失败:', error)
) }
}, 1000)
})
function artistsOrganize(list: string[]) { function artistsOrganize(list: string[]) {
if (list.length === 0) { if (list.length === 0) { return '未知音乐人' }
return '未知音乐人' return list.map((artist) => {
} return artist
return list }).join(' / ')
.map((artist) => {
return artist
})
.join(' / ')
} }
function setMetadata() { function setMetadata() {
if ('mediaSession' in navigator) { if ('mediaSession' in navigator) {
const current = currentTrack.value let current = currentTrack.value
if (!current) return if (!current) return
navigator.mediaSession.metadata = new MediaMetadata({ navigator.mediaSession.metadata = new MediaMetadata({
@ -175,12 +122,8 @@ function setMetadata() {
artist: artistsOrganize(current.song.artists ?? []), artist: artistsOrganize(current.song.artists ?? []),
album: current.album?.name, album: current.album?.name,
artwork: [ artwork: [
{ { src: current.album?.coverUrl ?? '', sizes: '500x500', type: 'image/png' },
src: current.album?.coverUrl ?? '', ]
sizes: '500x500',
type: 'image/png',
},
],
}) })
navigator.mediaSession.setActionHandler('previoustrack', playPrevious) navigator.mediaSession.setActionHandler('previoustrack', playPrevious)
@ -190,21 +133,16 @@ function setMetadata() {
playQueueStore.currentTime = player.value?.currentTime ?? 0 playQueueStore.currentTime = player.value?.currentTime ?? 0
} }
watch( watch(() => playQueueStore.updatedCurrentTime, (newValue) => {
() => playQueueStore.updatedCurrentTime, if (newValue === null) { return }
(newValue) => { if (player.value) player.value.currentTime = newValue
if (newValue === null) { playQueueStore.updatedCurrentTime = null
return })
}
if (player.value) player.value.currentTime = newValue
playQueueStore.updatedCurrentTime = null
},
)
} }
function playNext() { function playNext() {
if (playQueueStore.currentIndex === playQueueStore.list.length - 1) { if (playQueueStore.currentIndex === playQueueStore.list.length - 1) {
console.log('at the bottom, pause') console.log("at the bottom, pause")
playQueueStore.currentIndex = 0 playQueueStore.currentIndex = 0
if (playQueueStore.playMode.repeat === 'all') { if (playQueueStore.playMode.repeat === 'all') {
playQueueStore.currentIndex = 0 playQueueStore.currentIndex = 0
@ -220,17 +158,11 @@ function playNext() {
} }
function playPrevious() { function playPrevious() {
if ( if (player.value && (player.value.currentTime ?? 0) < 5 && playQueueStore.currentIndex > 0) {
player.value &&
(player.value.currentTime ?? 0) < 5 &&
playQueueStore.currentIndex > 0
) {
playQueueStore.currentIndex-- playQueueStore.currentIndex--
playQueueStore.isPlaying = true playQueueStore.isPlaying = true
} else { } else {
if (player.value) { if (player.value) { player.value.currentTime = 0 }
player.value.currentTime = 0
}
} }
} }
@ -247,10 +179,8 @@ function updateCurrentTime() {
const preloadTrigger = (config.preloadTrigger || 50) / 100 // const preloadTrigger = (config.preloadTrigger || 50) / 100 //
const remainingTimeThreshold = config.remainingTimeThreshold || 30 const remainingTimeThreshold = config.remainingTimeThreshold || 30
if ( if ((progress > preloadTrigger || remainingTime < remainingTimeThreshold) && !playQueueStore.isPreloading) {
(progress > preloadTrigger || remainingTime < remainingTimeThreshold) &&
!playQueueStore.isPreloading
) {
try { try {
if (typeof playQueueStore.preloadNext === 'function') { if (typeof playQueueStore.preloadNext === 'function') {
playQueueStore.preloadNext() playQueueStore.preloadNext()
@ -264,160 +194,115 @@ function updateCurrentTime() {
} }
} }
// console.log('[Player] 初始化 audioVisualizer')
const isAudioVisualizationSupported = supportsWebAudioVisualization() const { barHeights, connectAudio, isAnalyzing, error } = audioVisualizer({
console.log('[Player] 音频可视化支持状态:', isAudioVisualizationSupported) sensitivity: 1.5,
barCount: 6,
maxDecibels: -10,
bassBoost: 0.8,
midBoost: 1.2,
trebleBoost: 1.4,
threshold: 0
})
// 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( watch(() => playQueueStore.list.length, async (newLength) => {
() => playQueueStore.list.length, console.log('[Player] 播放列表长度变化:', newLength)
async (newLength) => { if (newLength === 0) {
console.log('[Player] 播放列表长度变化:', newLength) console.log('[Player] 播放列表为空,跳过连接')
if (newLength === 0) { return
console.log('[Player] 播放列表为空,跳过连接') }
return
}
// audio // audio
await nextTick() await nextTick()
if (player.value) { if (player.value) {
if (isAudioVisualizationSupported) { console.log('[Player] 连接音频元素到可视化器')
console.log('[Player] 连接音频元素到可视化器') console.log('[Player] 音频元素状态:', {
console.log('[Player] 音频元素状态:', { src: player.value.src?.substring(0, 50) + '...',
src: player.value.src?.substring(0, 50) + '...', readyState: player.value.readyState,
readyState: player.value.readyState, paused: player.value.paused
paused: player.value.paused, })
}) connectAudio(player.value)
connectAudio(player.value) } else {
} else { console.log('[Player] ❌ 音频元素不存在')
console.log('[Player] 跳过音频可视化连接(不支持的浏览器)') }
}
} else {
console.log('[Player] ❌ 音频元素不存在')
}
playQueueStore.visualizer = barHeights.value playQueueStore.visualizer = barHeights.value
// //
setTimeout(() => { setTimeout(() => {
playQueueStore.preloadNext() playQueueStore.preloadNext()
}, 2000) }, 2000)
// //
if (player.value) { if (player.value) {
initializeVolume() initializeVolume()
} }
}, })
)
// //
watch( watch(() => player.value, (audioElement) => {
() => player.value, if (audioElement && playQueueStore.list.length > 0) {
(audioElement) => { connectAudio(audioElement)
if (audioElement && playQueueStore.list.length > 0 && isAudioVisualizationSupported) { }
connectAudio(audioElement) })
}
},
)
// //
watch( watch(() => barHeights.value, (newHeights) => {
() => barHeights.value, playQueueStore.visualizer = newHeights
(newHeights) => { }, { deep: true })
playQueueStore.visualizer = newHeights
},
{ deep: true },
)
// //
watch( watch(() => error.value, (newError) => {
() => error.value, if (newError) {
(newError) => { console.error('[Player] 可视化器错误:', newError)
if (newError) { }
console.error('[Player] 可视化器错误:', newError) })
}
},
)
// //
watch( watch(() => playQueueStore.playMode.shuffle, (isShuffle) => {
() => playQueueStore.playMode.shuffle, if (isShuffle) {
(isShuffle) => { const currentIndex = playQueueStore.currentIndex
if (isShuffle) { const trackCount = playQueueStore.list.length
const currentIndex = playQueueStore.currentIndex
const trackCount = playQueueStore.list.length
// 1. // 1.
let shuffledList = [...Array(currentIndex).keys()] let shuffledList = [...Array(currentIndex).keys()]
// 2. // 2.
const shuffleSpace = [...Array(trackCount).keys()].filter((index) => let shuffleSpace = [...Array(trackCount).keys()].filter(index =>
playQueueStore.shuffleCurrent playQueueStore.shuffleCurrent ? index >= currentIndex : index > currentIndex
? index >= currentIndex )
: index > currentIndex,
)
// 3. // 3.
shuffleSpace.sort(() => Math.random() - 0.5) shuffleSpace.sort(() => Math.random() - 0.5)
// 4. currentIndex // 4. currentIndex
if (!playQueueStore.shuffleCurrent) { if (!playQueueStore.shuffleCurrent) {
shuffledList.push(currentIndex) shuffledList.push(currentIndex)
}
// 5. + +
shuffledList = shuffledList.concat(shuffleSpace)
// 6. shuffleList
playQueueStore.shuffleList = shuffledList
// shuffleCurrent
playQueueStore.shuffleCurrent = undefined
} else {
// 退
playQueueStore.currentIndex =
playQueueStore.shuffleList[playQueueStore.currentIndex]
} }
// // 5. + +
setTimeout(() => { shuffledList = shuffledList.concat(shuffleSpace)
playQueueStore.clearAllPreloadedAudio()
playQueueStore.preloadNext() // 6. shuffleList
}, 500) playQueueStore.shuffleList = shuffledList
},
) // shuffleCurrent
playQueueStore.shuffleCurrent = undefined
} else {
// 退
playQueueStore.currentIndex = playQueueStore.shuffleList[playQueueStore.currentIndex]
}
//
setTimeout(() => {
playQueueStore.clearAllPreloadedAudio()
playQueueStore.preloadNext()
}, 500)
})
function getCurrentTrack() { function getCurrentTrack() {
return currentTrack.value return currentTrack.value
@ -428,7 +313,7 @@ function initializeVolume() {
if (player.value) { if (player.value) {
const savedVolume = localStorage.getItem('audioVolume') const savedVolume = localStorage.getItem('audioVolume')
if (savedVolume) { if (savedVolume) {
const volumeValue = Number.parseFloat(savedVolume) const volumeValue = parseFloat(savedVolume)
player.value.volume = volumeValue player.value.volume = volumeValue
console.log('[Player] 初始化音量:', volumeValue) console.log('[Player] 初始化音量:', volumeValue)
} else { } else {
@ -454,7 +339,7 @@ function syncVolumeFromStorage() {
if (player.value) { if (player.value) {
const savedVolume = localStorage.getItem('audioVolume') const savedVolume = localStorage.getItem('audioVolume')
if (savedVolume) { if (savedVolume) {
const volumeValue = Number.parseFloat(savedVolume) const volumeValue = parseFloat(savedVolume)
if (player.value.volume !== volumeValue) { if (player.value.volume !== volumeValue) {
player.value.volume = volumeValue player.value.volume = volumeValue
} }
@ -519,14 +404,12 @@ setInterval(syncVolumeFromStorage, 100)
}"> }">
<div v-if="playQueueStore.isPlaying"> <div v-if="playQueueStore.isPlaying">
<LoadingIndicator v-if="playQueueStore.isBuffering === true" :size="4" /> <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" <div class="bg-white/75 w-[.125rem] rounded-full" v-for="(bar, index) in playQueueStore.visualizer"
:key="index" :style="{ :key="index" :style="{
height: `${Math.max(10, bar)}%` height: `${Math.max(10, bar)}%`
}" /> }" />
</div> </div>
<PauseIcon v-else :size="4" />
</div> </div>
<PlayIcon v-else :size="4" /> <PlayIcon v-else :size="4" />
</button> </button>

View File

@ -1,6 +1,5 @@
import { defineStore } from 'pinia' import { defineStore } from "pinia"
import { computed, ref } from 'vue' import { ref, computed } from "vue"
import { checkAndRefreshSongResource } from '../utils'
export const usePlayQueueStore = defineStore('queue', () => { export const usePlayQueueStore = defineStore('queue', () => {
const list = ref<QueueItem[]>([]) const list = ref<QueueItem[]>([])
@ -14,11 +13,11 @@ export const usePlayQueueStore = defineStore('queue', () => {
const visualizer = ref<number[]>([0, 0, 0, 0, 0, 0]) const visualizer = ref<number[]>([0, 0, 0, 0, 0, 0])
const shuffleList = ref<number[]>([]) const shuffleList = ref<number[]>([])
const playMode = ref<{ const playMode = ref<{
shuffle: boolean shuffle: boolean,
repeat: 'off' | 'single' | 'all' repeat: 'off' | 'single' | 'all'
}>({ }>({
shuffle: false, shuffle: false,
repeat: 'off', repeat: 'off'
}) })
const shuffleCurrent = ref<boolean | undefined>(undefined) const shuffleCurrent = ref<boolean | undefined>(undefined)
@ -36,13 +35,10 @@ export const usePlayQueueStore = defineStore('queue', () => {
} }
if (playMode.value.shuffle && shuffleList.value.length > 0) { if (playMode.value.shuffle && shuffleList.value.length > 0) {
// 当前在 shuffleList 中的位置 const currentShuffleIndex = shuffleList.value.indexOf(currentIndex.value)
const currentShuffleIndex = currentIndex.value
if (currentShuffleIndex < shuffleList.value.length - 1) { if (currentShuffleIndex < shuffleList.value.length - 1) {
// 返回下一个位置对应的原始 list 索引
return shuffleList.value[currentShuffleIndex + 1] return shuffleList.value[currentShuffleIndex + 1]
} else if (playMode.value.repeat === 'all') { } else if (playMode.value.repeat === 'all') {
// 返回第一个位置对应的原始 list 索引
return shuffleList.value[0] return shuffleList.value[0]
} }
return -1 return -1
@ -59,14 +55,19 @@ export const usePlayQueueStore = defineStore('queue', () => {
// 预加载下一首歌 // 预加载下一首歌
const preloadNext = async () => { const preloadNext = async () => {
const nextIndex = getNextIndex.value const nextIndex = getNextIndex.value
if (nextIndex === -1) { if (nextIndex === -1) {
return return
} }
// 获取下一首歌曲对象 // 获取下一首歌曲对象
// nextIndex 已经是原始 list 中的索引 let nextSong
const nextSong = list.value[nextIndex] if (playMode.value.shuffle && shuffleList.value.length > 0) {
nextSong = list.value[shuffleList.value[nextIndex]]
} else {
nextSong = list.value[nextIndex]
}
if (!nextSong || !nextSong.song) { if (!nextSong || !nextSong.song) {
return return
@ -88,24 +89,6 @@ export const usePlayQueueStore = defineStore('queue', () => {
isPreloading.value = true isPreloading.value = true
preloadProgress.value = 0 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() const audio = new Audio()
audio.preload = 'auto' audio.preload = 'auto'
audio.crossOrigin = 'anonymous' audio.crossOrigin = 'anonymous'
@ -124,20 +107,19 @@ export const usePlayQueueStore = defineStore('queue', () => {
preloadedAudio.value.set(songId, audio) preloadedAudio.value.set(songId, audio)
isPreloading.value = false isPreloading.value = false
preloadProgress.value = 100 preloadProgress.value = 100
console.log('[Store] 预加载完成:', updatedSong.name)
}) })
// 监听加载错误 // 监听加载错误
audio.addEventListener('error', (e) => { audio.addEventListener('error', (e) => {
console.error(`[Store] 预加载音频失败: ${updatedSong.name}`, e) console.error(`[Store] 预加载音频失败: ${e}`)
isPreloading.value = false isPreloading.value = false
preloadProgress.value = 0 preloadProgress.value = 0
}) })
// 使用更新后的音频源 // 设置音频源并开始加载
audio.src = updatedSong.sourceUrl! audio.src = nextSong.song.sourceUrl
} catch (error) { } catch (error) {
console.error('[Store] 预加载过程出错:', error)
isPreloading.value = false isPreloading.value = false
} }
} }
@ -185,7 +167,7 @@ export const usePlayQueueStore = defineStore('queue', () => {
progress: preloadProgress.value, progress: preloadProgress.value,
cacheSize: preloadedAudio.value.size, cacheSize: preloadedAudio.value.size,
cachedSongs: Array.from(preloadedAudio.value.keys()), cachedSongs: Array.from(preloadedAudio.value.keys()),
nextIndex: getNextIndex.value, nextIndex: getNextIndex.value
}) })
} }
@ -212,6 +194,6 @@ export const usePlayQueueStore = defineStore('queue', () => {
clearPreloadedAudio, clearPreloadedAudio,
clearAllPreloadedAudio, clearAllPreloadedAudio,
limitPreloadCache, limitPreloadCache,
debugPreloadState, debugPreloadState
} }
}) })

View File

@ -1,98 +0,0 @@
/**
*
*/
/**
* 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()
}
}

View File

@ -1,17 +1,5 @@
import artistsOrganize from "./artistsOrganize" import artistsOrganize from "./artistsOrganize"
import { audioVisualizer } from "./audioVisualizer" import { audioVisualizer } from "./audioVisualizer"
import cicdInfo from "./cicdInfo" import cicdInfo from "./cicdInfo"
import { checkAndRefreshSongResource, checkAndRefreshMultipleSongs } from "./songResourceChecker"
import { isSafari, isMobileSafari, supportsWebAudioVisualization, getBrowserInfo } from "./browserDetection"
export { export { artistsOrganize, audioVisualizer, cicdInfo }
artistsOrganize,
audioVisualizer,
cicdInfo,
checkAndRefreshSongResource,
checkAndRefreshMultipleSongs,
isSafari,
isMobileSafari,
supportsWebAudioVisualization,
getBrowserInfo
}

View File

@ -1,80 +0,0 @@
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
}