Compare commits

...

7 Commits

Author SHA1 Message Date
be15a89ad6
fix: 恢复非 Safari 浏览器的音频可视化效果
Some checks failed
构建扩展程序 / 发布至 Firefox 附加组件库 (push) Blocked by required conditions
构建扩展程序 / 构建 Chrome 扩展程序 (push) Failing after 1m0s
构建扩展程序 / 发布至 Chrome 应用商店 (push) Has been skipped
构建扩展程序 / 发布至 Edge 附加组件商店 (push) Has been skipped
构建扩展程序 / 构建 Firefox 附加组件 (push) Failing after 16m24s
- 只在 Safari 浏览器上使用静态图标替代可视化
- Chrome、Firefox 等浏览器保留原有的动态可视化效果
- Player.vue 和 PlayQueueItem.vue 现在都会检测浏览器支持情况

🤖 Generated with [Claude Code](https://claude.ai/code)

Co-Authored-By: Claude <noreply@anthropic.com>
2025-06-04 22:31:16 +10:00
282af70b74
fix: 替换音频可视化效果为静态图标
- Player.vue: 播放状态显示暂停图标而非可视化效果
- PlayQueueItem.vue: 当前播放项显示音波图标并带脉冲动画
- 创建新的 soundwave.vue 图标用于播放指示
- 避免在不支持的浏览器上显示空白或错误

🤖 Generated with [Claude Code](https://claude.ai/code)

Co-Authored-By: Claude <noreply@anthropic.com>
2025-06-04 22:29:22 +10:00
b0743178ed
fix: 修复 ref 未导入的错误
- 在 Player.vue 中添加缺失的 ref 导入
- 解决 "ReferenceError: Can't find variable: ref" 错误

🤖 Generated with [Claude Code](https://claude.ai/code)

Co-Authored-By: Claude <noreply@anthropic.com>
2025-06-04 22:25:24 +10:00
8ee2b928f9
fix: 解决 Safari 浏览器音频播放问题
- 创建浏览器检测工具,专门检测 Safari 和音频可视化支持
- 在 Safari 浏览器上禁用 AudioContext 连接,避免播放问题
- 保持其他浏览器的音频可视化功能正常工作

🤖 Generated with [Claude Code](https://claude.ai/code)

Co-Authored-By: Claude <noreply@anthropic.com>
2025-06-04 22:23:34 +10:00
5be5b4812f
docs: 更新 CLAUDE.md 添加代码风格和随机播放逻辑说明
- 添加项目使用 Tab 缩进的说明
- 添加随机播放模式的详细逻辑说明
- 说明 shuffleList 和 currentIndex 的关系

🤖 Generated with [Claude Code](https://claude.ai/code)

Co-Authored-By: Claude <noreply@anthropic.com>
2025-06-04 22:15:06 +10:00
ae2d8875ad
fix: 修复随机播放模式下预加载顺序错误的问题
- 修正 getNextIndex 在随机播放模式下返回正确的原始列表索引
- 简化预加载逻辑,因为 nextIndex 已经是正确的列表索引
- 保持 Player.vue 中更新歌曲信息时的索引计算逻辑

🤖 Generated with [Claude Code](https://claude.ai/code)

Co-Authored-By: Claude <noreply@anthropic.com>
2025-06-04 22:14:44 +10:00
dcf13b2f07
refactor: move resource URL refresh from favorites loading to playback/preload
Replace passive resource checking on playlist item mount with active checking
during playback and preload operations. This improves performance by reducing
unnecessary network requests and ensures resources are validated only when needed.

Changes:
- Create songResourceChecker utility for centralized resource validation
- Remove resource checking from PlayListItem component
- Add resource validation in Player component before playback
- Add resource validation in usePlayQueueStore before preload
- Maintain data consistency between play queue and favorites

🤖 Generated with [Claude Code](https://claude.ai/code)

Co-Authored-By: Claude <noreply@anthropic.com>
2025-06-04 21:22:39 +10:00
9 changed files with 562 additions and 227 deletions

View File

@ -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

View 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>

View File

@ -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>

View File

@ -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">

View File

@ -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,13 +19,18 @@ 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,16 +42,22 @@ const currentAudioSrc = computed(() => {
return track ? track.song.sourceUrl : ''
})
watch(() => playQueueStore.isPlaying, (newValue) => {
watch(
() => playQueueStore.isPlaying,
(newValue) => {
if (newValue) {
player.value?.play()
setMetadata()
} else {
player.value?.pause()
}
else { player.value?.pause() }
})
},
)
//
watch(() => playQueueStore.currentIndex, async () => {
watch(
() => playQueueStore.currentIndex,
async () => {
console.log('[Player] 当前索引变化:', playQueueStore.currentIndex)
// 使
@ -53,9 +66,31 @@ watch(() => playQueueStore.currentIndex, async () => {
const songId = track.song.cid
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)
},
)
// 使
const preloadedAudio = playQueueStore.getPreloadedAudio(songId)
if (preloadedAudio) {
if (preloadedAudio && updatedSong.sourceUrl === track.song.sourceUrl) {
console.log(`[Player] 使用预加载的音频: ${track.song.name}`)
// 使
@ -78,6 +113,11 @@ watch(() => playQueueStore.currentIndex, async () => {
} else {
console.log(`[Player] 正常加载音频: ${track.song.name}`)
playQueueStore.isBuffering = true
//
if (updatedSong.sourceUrl !== track.song.sourceUrl) {
playQueueStore.clearPreloadedAudio(songId)
}
}
} catch (error) {
console.error('[Player] 处理预加载音频时出错:', error)
@ -88,13 +128,21 @@ watch(() => playQueueStore.currentIndex, async () => {
setMetadata()
//
setTimeout(() => {
setTimeout(async () => {
try {
console.log('[Player] 尝试预加载下一首歌')
//
if (typeof playQueueStore.preloadNext === 'function') {
playQueueStore.preloadNext()
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 不是一个函数')
@ -103,18 +151,23 @@ watch(() => playQueueStore.currentIndex, async () => {
console.error('[Player] 预加载失败:', error)
}
}, 1000)
})
},
)
function artistsOrganize(list: string[]) {
if (list.length === 0) { return '未知音乐人' }
return list.map((artist) => {
if (list.length === 0) {
return '未知音乐人'
}
return list
.map((artist) => {
return artist
}).join(' / ')
})
.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 }
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,21 +264,45 @@ function updateCurrentTime() {
}
}
console.log('[Player] 初始化 audioVisualizer')
const { barHeights, connectAudio, isAnalyzing, error } = audioVisualizer({
//
const isAudioVisualizationSupported = supportsWebAudioVisualization()
console.log('[Player] 音频可视化支持状态:', isAudioVisualizationSupported)
//
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
})
threshold: 0,
})
console.log('[Player] audioVisualizer 返回值:', { barHeights: barHeights.value, isAnalyzing: isAnalyzing.value })
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) => {
watch(
() => playQueueStore.list.length,
async (newLength) => {
console.log('[Player] 播放列表长度变化:', newLength)
if (newLength === 0) {
console.log('[Player] 播放列表为空,跳过连接')
@ -219,13 +313,17 @@ watch(() => playQueueStore.list.length, async (newLength) => {
await nextTick()
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
paused: player.value.paused,
})
connectAudio(player.value)
} else {
console.log('[Player] 跳过音频可视化连接(不支持的浏览器)')
}
} else {
console.log('[Player] ❌ 音频元素不存在')
}
@ -241,29 +339,42 @@ watch(() => playQueueStore.list.length, async (newLength) => {
if (player.value) {
initializeVolume()
}
})
},
)
//
watch(() => player.value, (audioElement) => {
if (audioElement && playQueueStore.list.length > 0) {
watch(
() => player.value,
(audioElement) => {
if (audioElement && playQueueStore.list.length > 0 && isAudioVisualizationSupported) {
connectAudio(audioElement)
}
})
},
)
//
watch(() => barHeights.value, (newHeights) => {
watch(
() => barHeights.value,
(newHeights) => {
playQueueStore.visualizer = newHeights
}, { deep: true })
},
{ deep: true },
)
//
watch(() => error.value, (newError) => {
watch(
() => error.value,
(newError) => {
if (newError) {
console.error('[Player] 可视化器错误:', newError)
}
})
},
)
//
watch(() => playQueueStore.playMode.shuffle, (isShuffle) => {
watch(
() => playQueueStore.playMode.shuffle,
(isShuffle) => {
if (isShuffle) {
const currentIndex = playQueueStore.currentIndex
const trackCount = playQueueStore.list.length
@ -272,8 +383,10 @@ watch(() => playQueueStore.playMode.shuffle, (isShuffle) => {
let shuffledList = [...Array(currentIndex).keys()]
// 2.
let shuffleSpace = [...Array(trackCount).keys()].filter(index =>
playQueueStore.shuffleCurrent ? index >= currentIndex : index > currentIndex
const shuffleSpace = [...Array(trackCount).keys()].filter((index) =>
playQueueStore.shuffleCurrent
? index >= currentIndex
: index > currentIndex,
)
// 3.
@ -294,7 +407,8 @@ watch(() => playQueueStore.playMode.shuffle, (isShuffle) => {
playQueueStore.shuffleCurrent = undefined
} else {
// 退
playQueueStore.currentIndex = playQueueStore.shuffleList[playQueueStore.currentIndex]
playQueueStore.currentIndex =
playQueueStore.shuffleList[playQueueStore.currentIndex]
}
//
@ -302,7 +416,8 @@ watch(() => playQueueStore.playMode.shuffle, (isShuffle) => {
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>

View File

@ -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,
}
})

View 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()
}
}

View File

@ -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
}

View 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
}