diff --git a/src/main/unblockMusic.ts b/src/main/unblockMusic.ts index 171fdb3..2e4dcb6 100644 --- a/src/main/unblockMusic.ts +++ b/src/main/unblockMusic.ts @@ -32,6 +32,49 @@ interface UnblockResult { // 所有可用平台 export const ALL_PLATFORMS: Platform[] = ['migu', 'kugou', 'pyncmd', 'bilibili']; +/** + * 确保对象数据结构完整,处理null或undefined的情况 + * @param data 需要处理的数据对象 + */ +function ensureDataStructure(data: any): any { + // 如果数据本身为空,则返回一个基本结构 + if (!data) { + return { + name: '', + artists: [], + album: { name: '' } + }; + } + + // 确保name字段存在 + if (data.name === undefined || data.name === null) { + data.name = ''; + } + + // 确保artists字段存在且为数组 + if (!data.artists || !Array.isArray(data.artists)) { + data.artists = data.ar && Array.isArray(data.ar) ? data.ar : []; + } + + // 确保artists中的每个元素都有name属性 + if (data.artists.length > 0) { + data.artists = data.artists.map(artist => { + return artist ? { name: artist.name || '' } : { name: '' }; + }); + } + + // 确保album对象存在并有name属性 + if (!data.album || typeof data.album !== 'object') { + data.album = data.al && typeof data.al === 'object' ? data.al : { name: '' }; + } + + if (!data.album.name) { + data.album.name = ''; + } + + return data; +} + /** * 音乐解析函数 * @param id 歌曲ID @@ -46,16 +89,18 @@ const unblockMusic = async ( retryCount = 1, enabledPlatforms?: Platform[] ): Promise => { + // 过滤 enabledPlatforms,确保只包含 ALL_PLATFORMS 中存在的平台 const filteredPlatforms = enabledPlatforms ? enabledPlatforms.filter(platform => ALL_PLATFORMS.includes(platform)) : ALL_PLATFORMS; - songData.album = songData.album || songData.al; - songData.artists = songData.artists || songData.ar; + // 处理歌曲数据,确保数据结构完整 + const processedSongData = ensureDataStructure(songData); + const retry = async (attempt: number): Promise => { try { - const data = await match(parseInt(String(id), 10), filteredPlatforms, songData); + const data = await match(parseInt(String(id), 10), filteredPlatforms, processedSongData); const result: UnblockResult = { data: { data, diff --git a/src/renderer/api/music.ts b/src/renderer/api/music.ts index 127f13f..8848dfa 100644 --- a/src/renderer/api/music.ts +++ b/src/renderer/api/music.ts @@ -82,6 +82,66 @@ export const getMusicLrc = async (id: number) => { } }; +/** + * 从Bilibili获取音频URL + * @param data 歌曲数据 + * @returns 解析结果 + */ +const getBilibiliAudio = async (data: SongResult) => { + const songName = data?.name || ''; + const artistName = Array.isArray(data?.ar) && data.ar.length > 0 && data.ar[0]?.name ? data.ar[0].name : ''; + const albumName = data?.al && typeof data.al === 'object' && data.al?.name ? data.al.name : ''; + + const searchQuery = [songName, artistName, albumName].filter(Boolean).join(' ').trim(); + console.log('开始搜索bilibili音频:', searchQuery); + + const url = await searchAndGetBilibiliAudioUrl(searchQuery); + return { + data: { + code: 200, + message: 'success', + data: { url } + } + }; +}; + +/** + * 从GD音乐台获取音频URL + * @param id 歌曲ID + * @param data 歌曲数据 + * @returns 解析结果,失败时返回null + */ +const getGDMusicAudio = async (id: number, data: SongResult) => { + try { + const gdResult = await parseFromGDMusic(id, data, '999'); + if (gdResult) { + return gdResult; + } + } catch (error) { + console.error('GD音乐台解析失败:', error); + } + return null; +}; + +/** + * 使用unblockMusic解析音频URL + * @param id 歌曲ID + * @param data 歌曲数据 + * @param sources 音源列表 + * @returns 解析结果 + */ +const getUnblockMusicAudio = (id: number, data: SongResult, sources: any[]) => { + const filteredSources = sources.filter(source => source !== 'gdmusic'); + console.log(`使用unblockMusic解析,音源:`, filteredSources); + return window.api.unblockMusic(id, cloneDeep(data), cloneDeep(filteredSources)); +}; + +/** + * 获取解析后的音乐URL + * @param id 歌曲ID + * @param data 歌曲数据 + * @returns 解析结果 + */ export const getParsingMusicUrl = async (id: number, data: SongResult) => { const settingStore = useSettingsStore(); @@ -90,65 +150,51 @@ export const getParsingMusicUrl = async (id: number, data: SongResult) => { return Promise.resolve({ data: { code: 404, message: '音乐解析功能已禁用' } }); } - // 获取音源设置,优先使用歌曲自定义音源 + // 1. 确定使用的音源列表(自定义或全局) const songId = String(id); - const savedSource = localStorage.getItem(`song_source_${songId}`); - let enabledSources: any[] = []; + const savedSourceStr = localStorage.getItem(`song_source_${songId}`); + let musicSources: any[] = []; - // 如果有歌曲自定义音源,使用自定义音源 - if (savedSource) { - try { - enabledSources = JSON.parse(savedSource); - console.log(`使用歌曲 ${id} 自定义音源:`, enabledSources); - if(enabledSources.includes('bilibili')){ - // 构建搜索关键词,依次判断歌曲名称、歌手名称和专辑名称是否存在 - const songName = data?.name || ''; - const artistName = Array.isArray(data?.ar) && data.ar.length > 0 && data.ar[0]?.name ? data.ar[0].name : ''; - const albumName = data?.al && typeof data.al === 'object' && data.al?.name ? data.al.name : ''; - const name = [songName, artistName, albumName].filter(Boolean).join(' ').trim(); - console.log('开始搜索bilibili音频', name); - return { - data: { - code: 200, - message: 'success', - data: { - url: await searchAndGetBilibiliAudioUrl(name) - } - } - } + try { + if (savedSourceStr) { + // 使用自定义音源 + musicSources = JSON.parse(savedSourceStr); + console.log(`使用歌曲 ${id} 自定义音源:`, musicSources); + } else { + // 使用全局音源设置 + musicSources = settingStore.setData.enabledMusicSources || []; + console.log(`使用全局音源设置:`, musicSources); + if (isElectron && musicSources.length > 0) { + return getUnblockMusicAudio(id, data, musicSources); } - } catch (e) { - console.error('e',e) - console.error('解析自定义音源失败, 使用全局设置', e); - enabledSources = settingStore.setData.enabledMusicSources || []; } - } else { - // 没有自定义音源,使用全局音源设置 - enabledSources = settingStore.setData.enabledMusicSources || []; + } catch (e) { + console.error('解析音源设置失败,使用全局设置', e); + musicSources = settingStore.setData.enabledMusicSources || []; } - // 检查是否选择了GD音乐台解析 + // 2. 按优先级解析 - if (enabledSources.includes('gdmusic')) { - // 获取音质设置并转换为GD音乐台格式 - try { - const gdResult = await parseFromGDMusic(id, data, '999'); - if (gdResult) { - return gdResult; - } - } catch (error) { - console.error('GD音乐台解析失败:', error); - } - - console.log('GD音乐台所有音源均解析失败,尝试使用unblockMusic'); + // 2.1 Bilibili解析(优先级最高) + if (musicSources.includes('bilibili')) { + return await getBilibiliAudio(data); } - // 如果GD音乐台解析失败或者未启用,尝试使用unblockMusic - if (isElectron) { - const filteredSources = enabledSources.filter(source => source !== 'gdmusic'); - return window.api.unblockMusic(id, cloneDeep(data), cloneDeep(filteredSources)); + // 2.2 GD音乐台解析 + if (musicSources.includes('gdmusic')) { + const gdResult = await getGDMusicAudio(id, data); + if (gdResult) return gdResult; + // GD解析失败,继续下一步 + console.log('GD音乐台解析失败,尝试使用其他音源'); + } + console.log('musicSources',musicSources) + // 2.3 使用unblockMusic解析其他音源 + if (isElectron && musicSources.length > 0) { + return getUnblockMusicAudio(id, data, musicSources); } + // 3. 后备方案:使用API请求 + console.log('无可用音源或不在Electron环境中,使用API请求'); return requestMusic.get('/music', { params: { id } }); }; diff --git a/src/renderer/components/settings/MusicSourceSettings.vue b/src/renderer/components/settings/MusicSourceSettings.vue index 266a29b..22916ad 100644 --- a/src/renderer/components/settings/MusicSourceSettings.vue +++ b/src/renderer/components/settings/MusicSourceSettings.vue @@ -67,10 +67,10 @@ const visible = ref(props.show); const selectedSources = ref(props.sources); const musicSourceOptions = ref([ - { label: 'MiGu音乐', value: 'migu' }, - { label: '酷狗音乐', value: 'kugou' }, + { label: 'MG', value: 'migu' }, + { label: 'KG', value: 'kugou' }, { label: 'pyncmd', value: 'pyncmd' }, - { label: 'Bilibili音乐', value: 'bilibili' }, + { label: 'Bilibili', value: 'bilibili' }, { label: 'GD音乐台', value: 'gdmusic' } ]); diff --git a/src/renderer/services/audioService.ts b/src/renderer/services/audioService.ts index 0f7217e..9fa8d67 100644 --- a/src/renderer/services/audioService.ts +++ b/src/renderer/services/audioService.ts @@ -811,6 +811,40 @@ class AudioService { console.log('Volume applied (linear):', linearVolume); } + + // 添加方法检查当前音频是否在加载状态 + isLoading(): boolean { + if (!this.currentSound) return false; + + // 检查Howl对象的内部状态 + // 如果状态为1表示已经加载但未完成,状态为2表示正在加载 + const state = (this.currentSound as any)._state; + // 如果操作锁激活也认为是加载状态 + return this.operationLock || (state === 'loading' || state === 1); + } + + // 检查音频是否真正在播放 + isActuallyPlaying(): boolean { + if (!this.currentSound) return false; + + try { + // 综合判断: + // 1. Howler API是否报告正在播放 + // 2. 是否不在加载状态 + // 3. 确保音频上下文状态正常 + const isPlaying = this.currentSound.playing(); + const isLoading = this.isLoading(); + const contextRunning = Howler.ctx && Howler.ctx.state === 'running'; + + console.log(`实际播放状态检查: playing=${isPlaying}, loading=${isLoading}, contextRunning=${contextRunning}`); + + // 只有在三个条件都满足时才认为是真正在播放 + return isPlaying && !isLoading && contextRunning; + } catch (error) { + console.error('检查播放状态出错:', error); + return false; + } + } } export const audioService = new AudioService(); diff --git a/src/renderer/store/modules/player.ts b/src/renderer/store/modules/player.ts index 1821894..bca3e43 100644 --- a/src/renderer/store/modules/player.ts +++ b/src/renderer/store/modules/player.ts @@ -1,6 +1,7 @@ import { cloneDeep } from 'lodash'; import { defineStore } from 'pinia'; import { computed, ref } from 'vue'; +import { useThrottleFn } from '@vueuse/core'; import i18n from '@/../i18n/renderer'; import { getBilibiliAudioUrl } from '@/api/bilibili'; @@ -556,16 +557,95 @@ export const usePlayerStore = defineStore('player', () => { } }; + // 添加用户意图跟踪变量 + const userPlayIntent = ref(true); + + let checkPlayTime: NodeJS.Timeout | null = null; + + // 添加独立的播放状态检测函数 + const checkPlaybackState = (song: SongResult, timeout: number = 4000) => { + if(checkPlayTime) { + clearTimeout(checkPlayTime); + } + const sound = audioService.getCurrentSound(); + if (!sound) return; + + // 使用audioService的事件系统监听播放状态 + // 添加一次性播放事件监听器 + const onPlayHandler = () => { + // 播放事件触发,表示成功播放 + console.log('播放事件触发,歌曲成功开始播放'); + audioService.off('play', onPlayHandler); + audioService.off('playerror', onPlayErrorHandler); + }; + + // 添加一次性播放错误事件监听器 + const onPlayErrorHandler = async () => { + console.log('播放错误事件触发,尝试重新获取URL'); + audioService.off('play', onPlayHandler); + audioService.off('playerror', onPlayErrorHandler); + + // 只有用户仍然希望播放时才重试 + if (userPlayIntent.value && play.value) { + // 重置URL并重新播放 + playMusic.value.playMusicUrl = undefined; + // 保持播放状态,但强制重新获取URL + const refreshedSong = { ...song, isFirstPlay: true }; + await handlePlayMusic(refreshedSong, true); + } + }; + + // 注册事件监听器 + audioService.on('play', onPlayHandler); + audioService.on('playerror', onPlayErrorHandler); + + // 额外的安全检查:如果指定时间后仍未播放也未触发错误,且用户仍希望播放 + checkPlayTime = setTimeout(() => { + // 使用更准确的方法检查是否真正在播放 + if (!audioService.isActuallyPlaying() && userPlayIntent.value && play.value) { + console.log(`${timeout}ms后歌曲未真正播放且用户仍希望播放,尝试重新获取URL`); + // 移除事件监听器 + audioService.off('play', onPlayHandler); + audioService.off('playerror', onPlayErrorHandler); + + // 重置URL并重新播放 + playMusic.value.playMusicUrl = undefined; + // 保持播放状态,强制重新获取URL + (async () => { + const refreshedSong = { ...song, isFirstPlay: true }; + await handlePlayMusic(refreshedSong, true); + })(); + } + }, timeout); + }; + const setPlay = async (song: SongResult) => { try { - // 如果是当前正在播放的音乐,则切换播放/暂停状态 - if (playMusic.value.id === song.id && playMusic.value.playMusicUrl === song.playMusicUrl && !song.isFirstPlay) { - if (play.value) { + // 检查URL是否已过期 + if (song.expiredAt && song.expiredAt < Date.now()) { + console.info(`歌曲URL已过期,重新获取: ${song.name}`); + song.playMusicUrl = undefined; + // 重置过期时间,以便重新获取 + song.expiredAt = undefined; + } + + // 如果是当前正在播放的音乐,则切换播放/暂停状态 + if (playMusic.value.id === song.id && playMusic.value.playMusicUrl === song.playMusicUrl && !song.isFirstPlay) { + if (play.value) { setPlayMusic(false); audioService.getCurrentSound()?.pause(); + // 设置用户意图为暂停 + userPlayIntent.value = false; } else { setPlayMusic(true); - audioService.getCurrentSound()?.play(); + // 设置用户意图为播放 + userPlayIntent.value = true; + const sound = audioService.getCurrentSound(); + if (sound) { + sound.play(); + // 使用独立的播放状态检测函数 + checkPlaybackState(playMusic.value); + } } return; } @@ -600,10 +680,14 @@ export const usePlayerStore = defineStore('player', () => { const setPlayMusic = async (value: boolean | SongResult) => { if (typeof value === 'boolean') { setIsPlay(value); + // 记录用户的播放意图 + userPlayIntent.value = value; } else { await handlePlayMusic(value); play.value = true; isPlay.value = true; + // 设置为播放意图 + userPlayIntent.value = true; localStorage.setItem('currentPlayMusic', JSON.stringify(playMusic.value)); localStorage.setItem('currentPlayMusicUrl', playMusicUrl.value); } @@ -803,8 +887,7 @@ export const usePlayerStore = defineStore('player', () => { } }; - // 修改nextPlay方法,改进播放逻辑 - const nextPlay = async () => { + const _nextPlay = async () => { try { @@ -927,9 +1010,10 @@ export const usePlayerStore = defineStore('player', () => { } }; - // 修改 prevPlay 方法,使用与 nextPlay 相似的逻辑改进 - const prevPlay = async () => { + // 节流 + const nextPlay = useThrottleFn(_nextPlay, 500); + const _prevPlay = async () => { try { @@ -1010,6 +1094,9 @@ export const usePlayerStore = defineStore('player', () => { } }; + // 节流 + const prevPlay = useThrottleFn(_prevPlay, 500); + const togglePlayMode = () => { playMode.value = (playMode.value + 1) % 3; localStorage.setItem('playMode', JSON.stringify(playMode.value)); @@ -1185,6 +1272,12 @@ export const usePlayerStore = defineStore('player', () => { // 播放新音频,传递是否应该播放的状态 console.log('调用audioService.play,播放状态:', shouldPlay); const newSound = await audioService.play(playMusicUrl.value, playMusic.value, shouldPlay, initialPosition || 0); + + // 添加播放状态检测(仅当需要播放时) + if (shouldPlay) { + checkPlaybackState(playMusic.value); + } + // 发布音频就绪事件,让 MusicHook.ts 来处理设置监听器 window.dispatchEvent(new CustomEvent('audio-ready', { detail: { sound: newSound, shouldPlay } })); @@ -1215,15 +1308,18 @@ export const usePlayerStore = defineStore('player', () => { // 延迟较长时间,确保锁已完全释放 setTimeout(() => { - // 直接重试当前歌曲,而不是切换到下一首 - playAudio().catch(e => { - console.error('重试播放失败,切换到下一首:', e); - - // 只有再次失败才切换到下一首 - if (playList.value.length > 1) { - nextPlay(); - } - }); + // 如果用户仍希望播放 + if (userPlayIntent.value && play.value) { + // 直接重试当前歌曲,而不是切换到下一首 + playAudio().catch(e => { + console.error('重试播放失败,切换到下一首:', e); + + // 只有再次失败才切换到下一首 + if (playList.value.length > 1) { + nextPlay(); + } + }); + } }, 1000); } else { // 其他错误,切换到下一首 @@ -1253,7 +1349,7 @@ export const usePlayerStore = defineStore('player', () => { return false; } - // 保存用户选择的音源 + // 保存用户选择的音源(作为数组传递,确保unblockMusic可以使用) const songId = String(currentSong.id); localStorage.setItem(`song_source_${songId}`, JSON.stringify([sourcePlatform])); @@ -1267,11 +1363,17 @@ export const usePlayerStore = defineStore('player', () => { const numericId = typeof currentSong.id === 'string' ? parseInt(currentSong.id, 10) : currentSong.id; + + console.log(`使用音源 ${sourcePlatform} 重新解析歌曲 ${numericId}`); - const res = await getParsingMusicUrl(numericId, cloneDeep(currentSong)); + // 克隆一份歌曲数据,防止修改原始数据 + const songData = cloneDeep(currentSong); + + const res = await getParsingMusicUrl(numericId, songData); if (res && res.data && res.data.data && res.data.data.url) { // 更新URL const newUrl = res.data.data.url; + console.log(`解析成功,获取新URL: ${newUrl.substring(0, 50)}...`); // 使用新URL更新播放 const updatedMusic = { @@ -1286,6 +1388,7 @@ export const usePlayerStore = defineStore('player', () => { return true; } else { + console.warn(`使用音源 ${sourcePlatform} 解析失败`); return false; } } catch (error) { @@ -1307,6 +1410,8 @@ export const usePlayerStore = defineStore('player', () => { currentSound.pause(); } setPlayMusic(false); + // 明确设置用户意图为暂停 + userPlayIntent.value = false; } catch (error) { console.error('暂停播放失败:', error); } @@ -1345,8 +1450,8 @@ export const usePlayerStore = defineStore('player', () => { clearPlayAll, setPlay, setIsPlay, - nextPlay, - prevPlay, + nextPlay: nextPlay as unknown as typeof _nextPlay, + prevPlay: prevPlay as unknown as typeof _prevPlay, setPlayMusic, setMusicFull, setPlayList,