mirror of
https://github.com/algerkong/AlgerMusicPlayer.git
synced 2026-05-19 03:57:28 +08:00
067868f786
自定义序列化器仅保留必要字段,排除 lyric/song/playMusicUrl 等大体积数据 添加防抖 localStorage 包装降低写入频率,beforeunload 时刷新未写入数据
780 lines
26 KiB
TypeScript
780 lines
26 KiB
TypeScript
import { useThrottleFn } from '@vueuse/core';
|
|
import { debounce } from 'lodash';
|
|
import { createDiscreteApi } from 'naive-ui';
|
|
import { defineStore, storeToRefs } from 'pinia';
|
|
import { computed, ref, shallowRef, triggerRef } from 'vue';
|
|
|
|
import i18n from '@/../i18n/renderer';
|
|
import { useSongDetail } from '@/hooks/usePlayerHooks';
|
|
import { preloadService } from '@/services/preloadService';
|
|
import type { SongResult } from '@/types/music';
|
|
import { getImgUrl } from '@/utils';
|
|
import { performShuffle, preloadCoverImage } from '@/utils/playerUtils';
|
|
|
|
import { useIntelligenceModeStore } from './intelligenceMode';
|
|
import { usePlayerCoreStore } from './playerCore';
|
|
import { useSleepTimerStore } from './sleepTimer';
|
|
|
|
const { message } = createDiscreteApi(['message']);
|
|
|
|
/**
|
|
* 精简 SongResult 对象,只保留持久化必要字段
|
|
* 排除大体积字段:lyric, song, playMusicUrl, backgroundColor, primaryColor
|
|
*/
|
|
const minifySong = (s: SongResult) => ({
|
|
id: s.id,
|
|
name: s.name,
|
|
picUrl: s.picUrl,
|
|
ar: s.ar?.map((a) => ({ id: a.id, name: a.name })),
|
|
al: s.al,
|
|
source: s.source,
|
|
dt: s.dt
|
|
});
|
|
|
|
const minifySongList = (list: SongResult[] | undefined) => list?.map(minifySong) ?? [];
|
|
|
|
/**
|
|
* 防抖 localStorage 包装,降低写入频率
|
|
* 通过 pendingWrites 跟踪未写入数据,beforeunload 时刷新
|
|
*/
|
|
const pendingWrites = new Map<string, string>();
|
|
|
|
const flushPendingWrites = () => {
|
|
pendingWrites.forEach((value, key) => {
|
|
localStorage.setItem(key, value);
|
|
});
|
|
pendingWrites.clear();
|
|
};
|
|
|
|
const debouncedSetItem = debounce((key: string, value: string) => {
|
|
localStorage.setItem(key, value);
|
|
pendingWrites.delete(key);
|
|
}, 2000);
|
|
|
|
const debouncedLocalStorage = {
|
|
getItem: (key: string) => localStorage.getItem(key),
|
|
setItem: (key: string, value: string) => {
|
|
pendingWrites.set(key, value);
|
|
debouncedSetItem(key, value);
|
|
}
|
|
};
|
|
|
|
// 正常关闭时刷新未写入的数据
|
|
if (typeof window !== 'undefined') {
|
|
window.addEventListener('beforeunload', flushPendingWrites);
|
|
}
|
|
|
|
/**
|
|
* 播放列表管理 Store
|
|
* 负责:播放列表、索引、播放模式、预加载、上/下一首
|
|
*/
|
|
export const usePlaylistStore = defineStore(
|
|
'playlist',
|
|
() => {
|
|
// ==================== 状态 ====================
|
|
// 状态将由 pinia-plugin-persistedstate 自动从 localStorage 恢复
|
|
const playList = shallowRef<SongResult[]>([]);
|
|
const playListIndex = ref(0);
|
|
const playMode = ref(0);
|
|
const originalPlayList = shallowRef<SongResult[]>([]);
|
|
const playListDrawerVisible = ref(false);
|
|
|
|
// 连续失败计数器(用于防止无限循环)
|
|
const consecutiveFailCount = ref(0);
|
|
const MAX_CONSECUTIVE_FAILS = 5; // 最大连续失败次数
|
|
const SINGLE_TRACK_MAX_RETRIES = 3; // 单曲最大重试次数
|
|
|
|
// ==================== Computed ====================
|
|
const currentPlayList = computed(() => playList.value);
|
|
const currentPlayListIndex = computed(() => playListIndex.value);
|
|
|
|
// ==================== Actions ====================
|
|
|
|
/**
|
|
* 获取歌曲详情并预加载
|
|
*/
|
|
const fetchSongs = async (startIndex: number, endIndex: number) => {
|
|
try {
|
|
const songs = playList.value.slice(
|
|
Math.max(0, startIndex),
|
|
Math.min(endIndex, playList.value.length)
|
|
);
|
|
const { getSongDetail } = useSongDetail();
|
|
|
|
const detailedSongs = await Promise.all(
|
|
songs.map(async (song: SongResult) => {
|
|
try {
|
|
if (!song.playMusicUrl || (song.source === 'netease' && !song.backgroundColor)) {
|
|
return await getSongDetail(song);
|
|
}
|
|
return song;
|
|
} catch (error) {
|
|
console.error('获取歌曲详情失败:', error);
|
|
return song;
|
|
}
|
|
})
|
|
);
|
|
|
|
const nextSong = detailedSongs[0];
|
|
if (nextSong && !(nextSong.lyric && nextSong.lyric.lrcTimeArray.length > 0)) {
|
|
try {
|
|
const { useLyrics } = await import('@/hooks/usePlayerHooks');
|
|
const { loadLrc } = useLyrics();
|
|
nextSong.lyric = await loadLrc(nextSong.id);
|
|
} catch (error) {
|
|
console.error('加载歌词失败:', error);
|
|
}
|
|
}
|
|
|
|
detailedSongs.forEach((song, index) => {
|
|
if (song && startIndex + index < playList.value.length) {
|
|
playList.value[startIndex + index] = song;
|
|
}
|
|
});
|
|
// 触发 shallowRef 响应式更新(直接修改元素不会自动触发)
|
|
triggerRef(playList);
|
|
|
|
// 预加载下一首歌曲的音频和封面
|
|
if (nextSong) {
|
|
if (nextSong.playMusicUrl) {
|
|
preloadService.load(nextSong);
|
|
}
|
|
if (nextSong.picUrl) {
|
|
preloadCoverImage(nextSong.picUrl, getImgUrl);
|
|
}
|
|
}
|
|
} catch (error) {
|
|
console.error('获取歌曲列表失败:', error);
|
|
}
|
|
};
|
|
|
|
/**
|
|
* 智能预加载下一首歌曲
|
|
*/
|
|
const preloadNextSongs = (currentIndex: number) => {
|
|
if (playList.value.length <= 1) return;
|
|
|
|
let nextIndex: number;
|
|
|
|
if (playMode.value === 0) {
|
|
// 顺序播放模式
|
|
if (currentIndex >= playList.value.length - 1) {
|
|
return;
|
|
}
|
|
nextIndex = currentIndex + 1;
|
|
} else {
|
|
// 循环播放和随机播放模式
|
|
nextIndex = (currentIndex + 1) % playList.value.length;
|
|
}
|
|
|
|
const endIndex = Math.min(nextIndex + 2, playList.value.length);
|
|
|
|
if (nextIndex < playList.value.length) {
|
|
fetchSongs(nextIndex, endIndex);
|
|
|
|
// 循环模式且接近列表末尾,预加载列表开头
|
|
if (
|
|
(playMode.value === 1 || playMode.value === 2) &&
|
|
nextIndex + 1 >= playList.value.length &&
|
|
playList.value.length > 2
|
|
) {
|
|
setTimeout(() => {
|
|
fetchSongs(0, 1);
|
|
}, 1000);
|
|
}
|
|
}
|
|
};
|
|
|
|
/**
|
|
* 应用随机播放
|
|
*/
|
|
const shufflePlayList = () => {
|
|
console.log('[PlaylistStore] shufflePlayList called');
|
|
if (playList.value.length === 0) return;
|
|
|
|
// 保存原始列表
|
|
if (originalPlayList.value.length === 0) {
|
|
console.log('[PlaylistStore] Saving original list, length:', playList.value.length);
|
|
originalPlayList.value = [...playList.value];
|
|
}
|
|
|
|
const currentSong = playList.value[playListIndex.value];
|
|
console.log('[PlaylistStore] Current song before shuffle:', currentSong?.name);
|
|
|
|
// 执行洗牌
|
|
const shuffled = performShuffle([...playList.value], currentSong);
|
|
// 确保触发 shallowRef 的响应式
|
|
playList.value = [...shuffled];
|
|
playListIndex.value = 0;
|
|
|
|
console.log('[PlaylistStore] List shuffled, new length:', playList.value.length);
|
|
console.log('[PlaylistStore] New first song:', playList.value[0]?.name);
|
|
};
|
|
|
|
/**
|
|
* 恢复原始播放列表顺序
|
|
*/
|
|
const restoreOriginalOrder = () => {
|
|
console.log('[PlaylistStore] restoreOriginalOrder called');
|
|
if (originalPlayList.value.length === 0) return;
|
|
|
|
const currentSong = playList.value[playListIndex.value];
|
|
console.log('[PlaylistStore] Current song before restore:', currentSong?.name);
|
|
|
|
playList.value = [...originalPlayList.value];
|
|
originalPlayList.value = [];
|
|
|
|
// 找到当前歌曲在原始列表中的索引
|
|
if (currentSong) {
|
|
const index = playList.value.findIndex((s) => s.id === currentSong.id);
|
|
if (index !== -1) {
|
|
playListIndex.value = index;
|
|
}
|
|
}
|
|
console.log('[PlaylistStore] Original order restored, new index:', playListIndex.value);
|
|
};
|
|
|
|
/**
|
|
* 设置播放列表
|
|
*/
|
|
const setPlayList = (
|
|
list: SongResult[],
|
|
keepIndex: boolean = false,
|
|
fromIntelligenceMode: boolean = false
|
|
) => {
|
|
// 如果不是从心动模式调用,清除心动模式状态并切换播放模式
|
|
if (!fromIntelligenceMode) {
|
|
const intelligenceStore = useIntelligenceModeStore();
|
|
console.log('[PlaylistStore.setPlayList] 检查心动模式状态:', {
|
|
isIntelligenceMode: intelligenceStore.isIntelligenceMode,
|
|
currentPlayMode: playMode.value,
|
|
fromIntelligenceMode
|
|
});
|
|
|
|
if (intelligenceStore.isIntelligenceMode) {
|
|
console.log('[PlaylistStore] 退出心动模式,切换播放模式为顺序播放');
|
|
playMode.value = 0;
|
|
// 清除心动模式状态
|
|
intelligenceStore.clearIntelligenceMode(true);
|
|
console.log('[PlaylistStore] 心动模式已退出,新的播放模式:', playMode.value);
|
|
}
|
|
}
|
|
|
|
if (list.length === 0) {
|
|
playList.value = [];
|
|
playListIndex.value = 0;
|
|
originalPlayList.value = [];
|
|
return;
|
|
}
|
|
|
|
const playerCore = usePlayerCoreStore();
|
|
const { playMusic } = storeToRefs(playerCore);
|
|
|
|
// 根据当前播放模式处理新的播放列表
|
|
if (playMode.value === 2) {
|
|
// 随机模式
|
|
console.log('随机模式下设置新播放列表,保存原始顺序并洗牌');
|
|
|
|
originalPlayList.value = [...list];
|
|
|
|
const currentSong = playMusic.value;
|
|
const shuffledList = performShuffle(list, currentSong);
|
|
|
|
if (currentSong && currentSong.id) {
|
|
const currentSongIndex = shuffledList.findIndex((song) => song.id === currentSong.id);
|
|
playListIndex.value =
|
|
currentSongIndex !== -1 ? 0 : keepIndex ? Math.max(0, playListIndex.value) : 0;
|
|
} else {
|
|
playListIndex.value = keepIndex ? Math.max(0, playListIndex.value) : 0;
|
|
}
|
|
|
|
playList.value = shuffledList;
|
|
} else {
|
|
console.log('顺序/循环模式下设置新播放列表');
|
|
if (originalPlayList.value.length > 0) {
|
|
originalPlayList.value = [];
|
|
}
|
|
|
|
if (!keepIndex) {
|
|
const foundIndex = list.findIndex((item) => item.id === playMusic.value.id);
|
|
playListIndex.value = foundIndex !== -1 ? foundIndex : 0;
|
|
}
|
|
|
|
playList.value = list;
|
|
}
|
|
// pinia-plugin-persistedstate 会自动保存状态
|
|
};
|
|
|
|
/**
|
|
* 添加到下一首播放
|
|
*/
|
|
const addToNextPlay = (song: SongResult) => {
|
|
const list = [...playList.value];
|
|
const currentIndex = playListIndex.value;
|
|
|
|
// 如果歌曲已在播放列表中,先移除
|
|
const existingIndex = list.findIndex((item) => item.id === song.id);
|
|
if (existingIndex !== -1) {
|
|
list.splice(existingIndex, 1);
|
|
if (existingIndex <= currentIndex) {
|
|
playListIndex.value = Math.max(0, playListIndex.value - 1);
|
|
}
|
|
}
|
|
|
|
// 插入到当前播放歌曲的下一个位置
|
|
const insertIndex = playListIndex.value + 1;
|
|
list.splice(insertIndex, 0, song);
|
|
|
|
setPlayList(list, true);
|
|
};
|
|
|
|
/**
|
|
* 从播放列表移除歌曲
|
|
*/
|
|
const removeFromPlayList = (id: number | string) => {
|
|
const index = playList.value.findIndex((item) => item.id === id);
|
|
if (index === -1) return;
|
|
|
|
const playerCore = usePlayerCoreStore();
|
|
const { playMusic } = storeToRefs(playerCore);
|
|
|
|
// 如果删除的是当前播放的歌曲,先切换到下一首
|
|
if (id === playMusic.value.id) {
|
|
nextPlay();
|
|
}
|
|
|
|
const newPlayList = [...playList.value];
|
|
newPlayList.splice(index, 1);
|
|
setPlayList(newPlayList);
|
|
};
|
|
|
|
/**
|
|
* 清空播放列表
|
|
*/
|
|
const clearPlayAll = async () => {
|
|
const { audioService } = await import('@/services/audioService');
|
|
const playerCore = usePlayerCoreStore();
|
|
|
|
audioService.pause();
|
|
setTimeout(() => {
|
|
playerCore.playMusic = {} as SongResult;
|
|
playerCore.playMusicUrl = '';
|
|
playList.value = [];
|
|
playListIndex.value = 0;
|
|
originalPlayList.value = [];
|
|
// 只清除 playerCore 的 localStorage(这些由 playerCore store 管理)
|
|
localStorage.removeItem('currentPlayMusic');
|
|
localStorage.removeItem('currentPlayMusicUrl');
|
|
// playlist 状态由 pinia-plugin-persistedstate 自动管理
|
|
}, 500);
|
|
};
|
|
|
|
/**
|
|
* 切换播放模式
|
|
*/
|
|
const togglePlayMode = async () => {
|
|
const { useUserStore } = await import('./user');
|
|
const userStore = useUserStore();
|
|
const wasRandom = playMode.value === 2;
|
|
const wasIntelligence = playMode.value === 3;
|
|
|
|
let newMode = (playMode.value + 1) % 4;
|
|
|
|
// 如果要切换到心动模式,但用户未使用cookie登录,则跳过
|
|
if (newMode === 3 && (!userStore.user || userStore.loginType !== 'cookie')) {
|
|
console.log('跳过心动模式:需要cookie登录');
|
|
newMode = 0;
|
|
}
|
|
|
|
const isRandom = newMode === 2;
|
|
const isIntelligence = newMode === 3;
|
|
|
|
console.log(`[PlaylistStore] togglePlayMode: ${playMode.value} -> ${newMode}`);
|
|
playMode.value = newMode;
|
|
|
|
// 切换到随机模式时洗牌
|
|
if (isRandom && !wasRandom && playList.value.length > 0) {
|
|
shufflePlayList();
|
|
console.log('切换到随机模式,洗牌播放列表');
|
|
}
|
|
|
|
// 从随机模式切换出去时恢复原始顺序
|
|
if (!isRandom && wasRandom) {
|
|
restoreOriginalOrder();
|
|
console.log('切换出随机模式,恢复原始顺序');
|
|
}
|
|
|
|
// 切换到心动模式
|
|
if (isIntelligence && !wasIntelligence) {
|
|
console.log('切换到心动模式');
|
|
const intelligenceStore = useIntelligenceModeStore();
|
|
await intelligenceStore.playIntelligenceMode();
|
|
}
|
|
|
|
// 从心动模式切换出去
|
|
if (!isIntelligence && wasIntelligence) {
|
|
console.log('退出心动模式');
|
|
const intelligenceStore = useIntelligenceModeStore();
|
|
intelligenceStore.clearIntelligenceMode(true);
|
|
}
|
|
};
|
|
|
|
/**
|
|
* 下一首
|
|
* @param singleTrackRetryCount 单曲重试次数(同一首歌的重试)
|
|
*/
|
|
const _nextPlay = async (singleTrackRetryCount: number = 0) => {
|
|
try {
|
|
if (playList.value.length === 0) {
|
|
return;
|
|
}
|
|
|
|
const playerCore = usePlayerCoreStore();
|
|
const sleepTimerStore = useSleepTimerStore();
|
|
|
|
// 检查是否超过最大连续失败次数
|
|
if (consecutiveFailCount.value >= MAX_CONSECUTIVE_FAILS) {
|
|
console.error(`[nextPlay] 连续${MAX_CONSECUTIVE_FAILS}首歌曲播放失败,停止播放`);
|
|
message.warning(i18n.global.t('player.consecutiveFailsError'));
|
|
consecutiveFailCount.value = 0; // 重置计数器
|
|
playerCore.setIsPlay(false);
|
|
return;
|
|
}
|
|
|
|
// 检查是否是播放列表的最后一首且设置了播放列表结束定时
|
|
if (
|
|
playMode.value === 0 &&
|
|
playListIndex.value === playList.value.length - 1 &&
|
|
sleepTimerStore.sleepTimer.type === 'end'
|
|
) {
|
|
sleepTimerStore.stopPlayback();
|
|
return;
|
|
}
|
|
|
|
const currentIndex = playListIndex.value;
|
|
const nowPlayListIndex = (playListIndex.value + 1) % playList.value.length;
|
|
const nextSong = { ...playList.value[nowPlayListIndex] };
|
|
|
|
// 同一首歌重试时强制刷新在线 URL,避免卡在失效链接上
|
|
if (singleTrackRetryCount > 0 && !nextSong.playMusicUrl?.startsWith('local://')) {
|
|
nextSong.playMusicUrl = undefined;
|
|
nextSong.expiredAt = undefined;
|
|
}
|
|
|
|
console.log(
|
|
`[nextPlay] 尝试播放: ${nextSong.name}, 索引: ${currentIndex} -> ${nowPlayListIndex}, 单曲重试: ${singleTrackRetryCount}/${SINGLE_TRACK_MAX_RETRIES}, 连续失败: ${consecutiveFailCount.value}/${MAX_CONSECUTIVE_FAILS}`
|
|
);
|
|
console.log(
|
|
'[nextPlay] Current mode:',
|
|
playMode.value,
|
|
'Playlist length:',
|
|
playList.value.length
|
|
);
|
|
|
|
// 先尝试播放歌曲
|
|
const success = await playerCore.handlePlayMusic(nextSong, true);
|
|
|
|
if (success) {
|
|
// 播放成功,重置所有计数器并更新索引
|
|
consecutiveFailCount.value = 0;
|
|
playListIndex.value = nowPlayListIndex;
|
|
console.log(`[nextPlay] 播放成功,索引已更新为: ${nowPlayListIndex}`);
|
|
console.log(
|
|
'[nextPlay] New current song in list:',
|
|
playList.value[playListIndex.value]?.name
|
|
);
|
|
sleepTimerStore.handleSongChange();
|
|
} else {
|
|
console.error(`[nextPlay] 播放失败: ${nextSong.name}`);
|
|
|
|
// 单曲重试逻辑
|
|
if (singleTrackRetryCount < SINGLE_TRACK_MAX_RETRIES) {
|
|
console.log(
|
|
`[nextPlay] 单曲重试 ${singleTrackRetryCount + 1}/${SINGLE_TRACK_MAX_RETRIES}`
|
|
);
|
|
// 不更新索引,重试同一首歌
|
|
setTimeout(() => {
|
|
_nextPlay(singleTrackRetryCount + 1);
|
|
}, 1000);
|
|
} else {
|
|
// 单曲重试次数用尽,递增连续失败计数,尝试下一首
|
|
consecutiveFailCount.value++;
|
|
console.log(
|
|
`[nextPlay] 单曲重试用尽,连续失败计数: ${consecutiveFailCount.value}/${MAX_CONSECUTIVE_FAILS}`
|
|
);
|
|
|
|
if (playList.value.length > 1) {
|
|
// 更新索引到失败的歌曲位置,这样下次递归调用会继续往下
|
|
playListIndex.value = nowPlayListIndex;
|
|
message.warning(i18n.global.t('player.parseFailedPlayNext'));
|
|
|
|
// 延迟后尝试下一首(重置单曲重试计数)
|
|
setTimeout(() => {
|
|
_nextPlay(0);
|
|
}, 500);
|
|
} else {
|
|
// 只有一首歌且失败
|
|
message.error(i18n.global.t('player.playFailed'));
|
|
playerCore.setIsPlay(false);
|
|
}
|
|
}
|
|
}
|
|
} catch (error) {
|
|
console.error('切换下一首出错:', error);
|
|
}
|
|
};
|
|
|
|
const nextPlay = useThrottleFn(_nextPlay, 500);
|
|
|
|
/**
|
|
* 上一首
|
|
*/
|
|
const _prevPlay = async () => {
|
|
try {
|
|
if (playList.value.length === 0) {
|
|
return;
|
|
}
|
|
|
|
const playerCore = usePlayerCoreStore();
|
|
const currentIndex = playListIndex.value;
|
|
const nowPlayListIndex =
|
|
(playListIndex.value - 1 + playList.value.length) % playList.value.length;
|
|
|
|
const prevSong = { ...playList.value[nowPlayListIndex] };
|
|
|
|
console.log(
|
|
`[prevPlay] 尝试播放上一首: ${prevSong.name}, 索引: ${currentIndex} -> ${nowPlayListIndex}`
|
|
);
|
|
|
|
let success = false;
|
|
let retryCount = 0;
|
|
const maxRetries = 2;
|
|
|
|
// 先尝试播放歌曲,成功后再更新索引
|
|
while (!success && retryCount < maxRetries) {
|
|
success = await playerCore.handlePlayMusic(prevSong);
|
|
|
|
if (!success) {
|
|
retryCount++;
|
|
console.error(`播放上一首失败,尝试 ${retryCount}/${maxRetries}`);
|
|
|
|
if (retryCount >= maxRetries) {
|
|
console.error('多次尝试播放失败,将从播放列表中移除此歌曲');
|
|
const newPlayList = [...playList.value];
|
|
newPlayList.splice(nowPlayListIndex, 1);
|
|
|
|
if (newPlayList.length > 0) {
|
|
const keepCurrentIndexPosition = true;
|
|
setPlayList(newPlayList, keepCurrentIndexPosition);
|
|
|
|
if (newPlayList.length === 1) {
|
|
playListIndex.value = 0;
|
|
} else {
|
|
const newPrevIndex =
|
|
(playListIndex.value - 1 + newPlayList.length) % newPlayList.length;
|
|
playListIndex.value = newPrevIndex;
|
|
}
|
|
|
|
setTimeout(() => {
|
|
prevPlay();
|
|
}, 300);
|
|
return;
|
|
} else {
|
|
console.error('播放列表为空,停止尝试');
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
if (success) {
|
|
// 播放成功,更新索引
|
|
playListIndex.value = nowPlayListIndex;
|
|
console.log(`[prevPlay] 播放成功,索引已更新为: ${nowPlayListIndex}`);
|
|
} else {
|
|
console.error(`[prevPlay] 播放上一首失败,保持当前索引: ${currentIndex}`);
|
|
playerCore.setIsPlay(false);
|
|
message.error(i18n.global.t('player.playFailed'));
|
|
}
|
|
} catch (error) {
|
|
console.error('切换上一首出错:', error);
|
|
}
|
|
};
|
|
|
|
const prevPlay = useThrottleFn(_prevPlay, 500);
|
|
|
|
/**
|
|
* 设置播放列表抽屉显示状态
|
|
*/
|
|
const setPlayListDrawerVisible = (value: boolean) => {
|
|
playListDrawerVisible.value = value;
|
|
};
|
|
|
|
/**
|
|
* 设置播放(兼容旧API)
|
|
*/
|
|
const setPlay = async (song: SongResult) => {
|
|
try {
|
|
const playerCore = usePlayerCoreStore();
|
|
|
|
// 检查URL是否已过期
|
|
if (song.expiredAt && song.expiredAt < Date.now()) {
|
|
// 本地音乐(local:// 协议)不会过期
|
|
if (!song.playMusicUrl?.startsWith('local://')) {
|
|
console.info(`歌曲URL已过期,重新获取: ${song.name}`);
|
|
song.playMusicUrl = undefined;
|
|
song.expiredAt = undefined;
|
|
}
|
|
}
|
|
|
|
// 如果是当前正在播放的音乐,则切换播放/暂停状态
|
|
if (
|
|
playerCore.playMusic.id === song.id &&
|
|
playerCore.playMusic.playMusicUrl === song.playMusicUrl &&
|
|
!song.isFirstPlay
|
|
) {
|
|
if (playerCore.play) {
|
|
playerCore.setPlayMusic(false);
|
|
const { audioService } = await import('@/services/audioService');
|
|
audioService.getCurrentSound()?.pause();
|
|
playerCore.userPlayIntent = false;
|
|
} else {
|
|
playerCore.setPlayMusic(true);
|
|
playerCore.userPlayIntent = true;
|
|
const { audioService } = await import('@/services/audioService');
|
|
const sound = audioService.getCurrentSound();
|
|
if (sound) {
|
|
sound.play();
|
|
// 在恢复播放时也进行状态检测,防止URL已过期导致无声
|
|
playerCore.checkPlaybackState(playerCore.playMusic);
|
|
} else {
|
|
console.warn('[PlaylistStore.setPlay] 无可用音频实例,尝试重建播放链路');
|
|
const recoverSong = {
|
|
...playerCore.playMusic,
|
|
isFirstPlay: true,
|
|
playMusicUrl: playerCore.playMusic.playMusicUrl?.startsWith('local://')
|
|
? playerCore.playMusic.playMusicUrl
|
|
: undefined
|
|
};
|
|
const recovered = await playerCore.handlePlayMusic(recoverSong, true);
|
|
if (!recovered) {
|
|
playerCore.setIsPlay(false);
|
|
message.error(i18n.global.t('player.playFailed'));
|
|
}
|
|
}
|
|
}
|
|
return;
|
|
}
|
|
|
|
if (song.isFirstPlay) {
|
|
song.isFirstPlay = false;
|
|
}
|
|
|
|
// 查找歌曲在播放列表中的索引
|
|
const songIndex = playList.value.findIndex(
|
|
(item: SongResult) => item.id === song.id && item.source === song.source
|
|
);
|
|
|
|
// 更新播放索引
|
|
if (songIndex !== -1 && songIndex !== playListIndex.value) {
|
|
console.log('歌曲索引不匹配,更新为:', songIndex);
|
|
playListIndex.value = songIndex;
|
|
}
|
|
|
|
const success = await playerCore.handlePlayMusic(song);
|
|
|
|
// playerCore 的状态由其自己的 store 管理
|
|
|
|
if (success) {
|
|
playerCore.isPlay = true;
|
|
|
|
// 预加载下一首歌曲
|
|
if (songIndex !== -1) {
|
|
setTimeout(() => {
|
|
preloadNextSongs(playListIndex.value);
|
|
}, 3000);
|
|
}
|
|
}
|
|
return success;
|
|
} catch (error) {
|
|
console.error('设置播放失败:', error);
|
|
return false;
|
|
}
|
|
};
|
|
|
|
/**
|
|
* 初始化播放列表
|
|
* 注意:状态已由 pinia-plugin-persistedstate 自动恢复
|
|
* 这里只需要处理特殊逻辑(如随机模式的恢复)
|
|
*/
|
|
const initializePlaylist = async () => {
|
|
// 重启后恢复随机播放状态
|
|
if (playMode.value === 2 && playList.value.length > 0) {
|
|
if (originalPlayList.value.length === 0) {
|
|
console.log('重启后恢复随机播放模式,重新洗牌播放列表');
|
|
shufflePlayList();
|
|
} else {
|
|
console.log('重启后恢复随机播放模式,播放列表已是洗牌状态');
|
|
}
|
|
}
|
|
};
|
|
|
|
return {
|
|
// 状态
|
|
playList,
|
|
playListIndex,
|
|
playMode,
|
|
originalPlayList,
|
|
playListDrawerVisible,
|
|
|
|
// Computed
|
|
currentPlayList,
|
|
currentPlayListIndex,
|
|
|
|
// Actions
|
|
setPlayList,
|
|
addToNextPlay,
|
|
removeFromPlayList,
|
|
clearPlayAll,
|
|
togglePlayMode,
|
|
shufflePlayList,
|
|
restoreOriginalOrder,
|
|
preloadNextSongs,
|
|
nextPlay: nextPlay as unknown as typeof _nextPlay,
|
|
prevPlay: prevPlay as unknown as typeof _prevPlay,
|
|
setPlayListDrawerVisible,
|
|
setPlay,
|
|
initializePlaylist,
|
|
fetchSongs,
|
|
updateSong: (song: SongResult) => {
|
|
const index = playList.value.findIndex(
|
|
(item) => item.id === song.id && item.source === song.source
|
|
);
|
|
if (index !== -1) {
|
|
playList.value[index] = song;
|
|
// 触发响应式更新
|
|
playList.value = [...playList.value];
|
|
}
|
|
}
|
|
};
|
|
},
|
|
{
|
|
// 配置 pinia-plugin-persistedstate(精简序列化 + 防抖写入)
|
|
persist: {
|
|
key: 'playlist-store',
|
|
storage: debouncedLocalStorage,
|
|
pick: ['playList', 'playListIndex', 'playMode', 'originalPlayList'],
|
|
serializer: {
|
|
serialize: (state: any) => {
|
|
return JSON.stringify({
|
|
...state,
|
|
playList: minifySongList(state.playList),
|
|
originalPlayList: minifySongList(state.originalPlayList)
|
|
});
|
|
},
|
|
deserialize: JSON.parse
|
|
}
|
|
}
|
|
}
|
|
);
|