mirror of
https://github.com/algerkong/AlgerMusicPlayer.git
synced 2026-05-19 03:57:28 +08:00
feat: 扩展数据层与播放能力
This commit is contained in:
@@ -98,7 +98,6 @@ export const useIntelligenceModeStore = defineStore('intelligenceMode', () => {
|
||||
|
||||
setLocalStorageItem('isIntelligenceMode', true);
|
||||
setLocalStorageItem('intelligenceModeInfo', intelligenceModeInfo.value);
|
||||
setLocalStorageItem('playMode', playlistStore.playMode);
|
||||
|
||||
// 替换播放列表并开始播放
|
||||
playlistStore.setPlayList(intelligenceSongs, false, true);
|
||||
@@ -114,12 +113,36 @@ export const useIntelligenceModeStore = defineStore('intelligenceMode', () => {
|
||||
|
||||
/**
|
||||
* 清除心动模式状态
|
||||
* @param skipPlayModeChange 是否跳过播放模式切换
|
||||
*/
|
||||
const clearIntelligenceMode = () => {
|
||||
const clearIntelligenceMode = (skipPlayModeChange: boolean = false) => {
|
||||
console.log(
|
||||
'[IntelligenceMode] clearIntelligenceMode 被调用,skipPlayModeChange:',
|
||||
skipPlayModeChange
|
||||
);
|
||||
|
||||
isIntelligenceMode.value = false;
|
||||
intelligenceModeInfo.value = null;
|
||||
setLocalStorageItem('isIntelligenceMode', false);
|
||||
localStorage.removeItem('intelligenceModeInfo');
|
||||
|
||||
console.log(
|
||||
'[IntelligenceMode] 心动模式状态已清除,isIntelligenceMode:',
|
||||
isIntelligenceMode.value
|
||||
);
|
||||
|
||||
// 自动切换播放模式为顺序播放 (playMode = 0)
|
||||
if (!skipPlayModeChange) {
|
||||
(async () => {
|
||||
const { usePlaylistStore } = await import('./playlist');
|
||||
const playlistStore = usePlaylistStore();
|
||||
|
||||
if (playlistStore.playMode === 3) {
|
||||
console.log('[IntelligenceMode] 退出心动模式,自动切换播放模式为顺序播放');
|
||||
playlistStore.playMode = 0;
|
||||
}
|
||||
})();
|
||||
}
|
||||
};
|
||||
|
||||
return {
|
||||
|
||||
@@ -24,6 +24,14 @@ export const useMusicStore = defineStore('music', {
|
||||
this.canRemoveSong = canRemove;
|
||||
},
|
||||
|
||||
// 仅设置基础信息(用于先导航后获取数据)
|
||||
setBasicListInfo(name: string, listInfo: any = null, canRemove = false) {
|
||||
this.currentMusicList = null; // 标识数据未加载
|
||||
this.currentMusicListName = name;
|
||||
this.currentListInfo = listInfo;
|
||||
this.canRemoveSong = canRemove;
|
||||
},
|
||||
|
||||
// 清除当前音乐列表
|
||||
clearCurrentMusicList() {
|
||||
this.currentMusicList = null;
|
||||
|
||||
@@ -4,19 +4,21 @@ import { defineStore } from 'pinia';
|
||||
import { computed, ref } from 'vue';
|
||||
|
||||
import i18n from '@/../i18n/renderer';
|
||||
import { getBilibiliAudioUrl } from '@/api/bilibili';
|
||||
import { getParsingMusicUrl } from '@/api/music';
|
||||
import { useMusicHistory } from '@/hooks/MusicHistoryHook';
|
||||
import { usePodcastHistory } from '@/hooks/PodcastHistoryHook';
|
||||
import { useLyrics, useSongDetail } from '@/hooks/usePlayerHooks';
|
||||
import { audioService } from '@/services/audioService';
|
||||
import { playbackRequestManager } from '@/services/playbackRequestManager';
|
||||
import { preloadService } from '@/services/preloadService';
|
||||
import { SongSourceConfigManager } from '@/services/SongSourceConfigManager';
|
||||
import type { AudioOutputDevice } from '@/types/audio';
|
||||
import type { Platform, SongResult } from '@/types/music';
|
||||
import { getImgUrl } from '@/utils';
|
||||
import { getImageLinearBackground } from '@/utils/linearColor';
|
||||
|
||||
const musicHistory = useMusicHistory();
|
||||
const podcastHistory = usePodcastHistory();
|
||||
const { message } = createDiscreteApi(['message']);
|
||||
|
||||
/**
|
||||
@@ -36,6 +38,12 @@ export const usePlayerCoreStore = defineStore(
|
||||
const volume = ref(1);
|
||||
const userPlayIntent = ref(false); // 用户是否想要播放
|
||||
|
||||
// 音频输出设备
|
||||
const audioOutputDeviceId = ref<string>(
|
||||
localStorage.getItem('audioOutputDeviceId') || 'default'
|
||||
);
|
||||
const availableAudioDevices = ref<AudioOutputDevice[]>([]);
|
||||
|
||||
let checkPlayTime: NodeJS.Timeout | null = null;
|
||||
|
||||
// ==================== Computed ====================
|
||||
@@ -239,14 +247,18 @@ export const usePlayerCoreStore = defineStore(
|
||||
(prev: string, curr: any) => `${prev}${curr.name}/`,
|
||||
''
|
||||
)}`;
|
||||
} else if (music.source === 'bilibili' && music?.song?.ar?.[0]) {
|
||||
title += ` - ${music.song.ar[0].name}`;
|
||||
}
|
||||
document.title = 'AlgerMusic - ' + title;
|
||||
|
||||
try {
|
||||
// 添加到历史记录
|
||||
musicHistory.addMusic(music);
|
||||
if (music.isPodcast) {
|
||||
if (music.program) {
|
||||
podcastHistory.addPodcast(music.program);
|
||||
}
|
||||
} else {
|
||||
musicHistory.addMusic(music);
|
||||
}
|
||||
|
||||
// 获取歌曲详情
|
||||
const updatedPlayMusic = await getSongDetail(originalMusic, requestId);
|
||||
@@ -352,36 +364,6 @@ export const usePlayerCoreStore = defineStore(
|
||||
console.log('[playAudio] 恢复播放进度:', initialPosition);
|
||||
}
|
||||
|
||||
// B站视频URL检查
|
||||
if (
|
||||
playMusic.value.source === 'bilibili' &&
|
||||
(!playMusicUrl.value || playMusicUrl.value === 'undefined')
|
||||
) {
|
||||
console.log('B站视频URL无效,尝试重新获取');
|
||||
|
||||
if (playMusic.value.bilibiliData) {
|
||||
try {
|
||||
const proxyUrl = await getBilibiliAudioUrl(
|
||||
playMusic.value.bilibiliData.bvid,
|
||||
playMusic.value.bilibiliData.cid
|
||||
);
|
||||
|
||||
// 再次验证请求
|
||||
if (requestId && !playbackRequestManager.isRequestValid(requestId)) {
|
||||
console.log(`[playAudio] 获取B站URL后请求已失效: ${requestId}`);
|
||||
return null;
|
||||
}
|
||||
|
||||
(playMusic.value as any).playMusicUrl = proxyUrl;
|
||||
playMusicUrl.value = proxyUrl;
|
||||
} catch (error) {
|
||||
console.error('获取B站音频URL失败:', error);
|
||||
message.error(i18n.global.t('player.playFailed'));
|
||||
return null;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 使用 PreloadService 获取音频
|
||||
// 优先使用已预加载的 sound(通过 consume 获取并从缓存中移除)
|
||||
// 如果没有预加载,则进行加载
|
||||
@@ -514,11 +496,6 @@ export const usePlayerCoreStore = defineStore(
|
||||
return false;
|
||||
}
|
||||
|
||||
if (currentSong.source === 'bilibili') {
|
||||
console.warn('B站视频不支持重新解析');
|
||||
return false;
|
||||
}
|
||||
|
||||
// 使用 SongSourceConfigManager 保存配置
|
||||
SongSourceConfigManager.setConfig(
|
||||
currentSong.id,
|
||||
@@ -579,11 +556,6 @@ export const usePlayerCoreStore = defineStore(
|
||||
console.log('恢复上次播放的音乐:', playMusic.value.name);
|
||||
const isPlaying = settingStore.setData.autoPlay;
|
||||
|
||||
if (playMusic.value.source === 'bilibili' && playMusic.value.bilibiliData) {
|
||||
console.log('恢复B站视频播放', playMusic.value.bilibiliData);
|
||||
playMusic.value.playMusicUrl = undefined;
|
||||
}
|
||||
|
||||
await handlePlayMusic(
|
||||
{ ...playMusic.value, isFirstPlay: true, playMusicUrl: undefined },
|
||||
isPlaying
|
||||
@@ -602,6 +574,43 @@ export const usePlayerCoreStore = defineStore(
|
||||
}, 2000);
|
||||
};
|
||||
|
||||
// ==================== 音频输出设备管理 ====================
|
||||
|
||||
/**
|
||||
* 刷新可用音频输出设备列表
|
||||
*/
|
||||
const refreshAudioDevices = async () => {
|
||||
availableAudioDevices.value = await audioService.getAudioOutputDevices();
|
||||
};
|
||||
|
||||
/**
|
||||
* 切换音频输出设备
|
||||
*/
|
||||
const setAudioOutputDevice = async (deviceId: string): Promise<boolean> => {
|
||||
const success = await audioService.setAudioOutputDevice(deviceId);
|
||||
if (success) {
|
||||
audioOutputDeviceId.value = deviceId;
|
||||
}
|
||||
return success;
|
||||
};
|
||||
|
||||
/**
|
||||
* 初始化设备变化监听
|
||||
*/
|
||||
const initAudioDeviceListener = () => {
|
||||
if (navigator.mediaDevices) {
|
||||
navigator.mediaDevices.addEventListener('devicechange', async () => {
|
||||
await refreshAudioDevices();
|
||||
const exists = availableAudioDevices.value.some(
|
||||
(d) => d.deviceId === audioOutputDeviceId.value
|
||||
);
|
||||
if (!exists && audioOutputDeviceId.value !== 'default') {
|
||||
await setAudioOutputDevice('default');
|
||||
}
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
return {
|
||||
// 状态
|
||||
play,
|
||||
@@ -612,6 +621,8 @@ export const usePlayerCoreStore = defineStore(
|
||||
playbackRate,
|
||||
volume,
|
||||
userPlayIntent,
|
||||
audioOutputDeviceId,
|
||||
availableAudioDevices,
|
||||
|
||||
// Computed
|
||||
currentSong,
|
||||
@@ -631,14 +642,17 @@ export const usePlayerCoreStore = defineStore(
|
||||
handlePause,
|
||||
checkPlaybackState,
|
||||
reparseCurrentSong,
|
||||
initializePlayState
|
||||
initializePlayState,
|
||||
refreshAudioDevices,
|
||||
setAudioOutputDevice,
|
||||
initAudioDeviceListener
|
||||
};
|
||||
},
|
||||
{
|
||||
persist: {
|
||||
key: 'player-core-store',
|
||||
storage: localStorage,
|
||||
pick: ['playMusic', 'playMusicUrl', 'playbackRate', 'volume', 'isPlay']
|
||||
pick: ['playMusic', 'playMusicUrl', 'playbackRate', 'volume', 'isPlay', 'audioOutputDeviceId']
|
||||
}
|
||||
}
|
||||
);
|
||||
|
||||
@@ -192,11 +192,21 @@ export const usePlaylistStore = defineStore(
|
||||
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) {
|
||||
intelligenceStore.clearIntelligenceMode();
|
||||
console.log('[PlaylistStore] 退出心动模式,切换播放模式为顺序播放');
|
||||
playMode.value = 0;
|
||||
// 清除心动模式状态
|
||||
intelligenceStore.clearIntelligenceMode(true);
|
||||
console.log('[PlaylistStore] 心动模式已退出,新的播放模式:', playMode.value);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -355,7 +365,7 @@ export const usePlaylistStore = defineStore(
|
||||
if (!isIntelligence && wasIntelligence) {
|
||||
console.log('退出心动模式');
|
||||
const intelligenceStore = useIntelligenceModeStore();
|
||||
intelligenceStore.clearIntelligenceMode();
|
||||
intelligenceStore.clearIntelligenceMode(true);
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
@@ -0,0 +1,166 @@
|
||||
import { createDiscreteApi } from 'naive-ui';
|
||||
import { defineStore } from 'pinia';
|
||||
import { computed, ref, shallowRef } from 'vue';
|
||||
|
||||
import * as podcastApi from '@/api/podcast';
|
||||
import type { DjCategory, DjProgram, DjRadio } from '@/types/podcast';
|
||||
|
||||
const { message } = createDiscreteApi(['message']);
|
||||
|
||||
export const usePodcastStore = defineStore(
|
||||
'podcast',
|
||||
() => {
|
||||
const subscribedRadios = shallowRef<DjRadio[]>([]);
|
||||
const categories = shallowRef<DjCategory[]>([]);
|
||||
const currentRadio = shallowRef<DjRadio | null>(null);
|
||||
const currentPrograms = shallowRef<DjProgram[]>([]);
|
||||
const recommendRadios = shallowRef<DjRadio[]>([]);
|
||||
const todayPerfered = shallowRef<DjProgram[]>([]);
|
||||
const recentPrograms = shallowRef<DjProgram[]>([]);
|
||||
const isLoading = ref(false);
|
||||
|
||||
const subscribedCount = computed(() => subscribedRadios.value.length);
|
||||
|
||||
const isRadioSubscribed = computed(() => {
|
||||
return (rid: number) => subscribedRadios.value.some((r) => r.id === rid);
|
||||
});
|
||||
|
||||
const fetchSubscribedRadios = async () => {
|
||||
try {
|
||||
isLoading.value = true;
|
||||
const res = await podcastApi.getDjSublist();
|
||||
subscribedRadios.value = res.data?.djRadios || [];
|
||||
} catch (error) {
|
||||
console.error('获取订阅列表失败:', error);
|
||||
message.error('获取订阅列表失败');
|
||||
} finally {
|
||||
isLoading.value = false;
|
||||
}
|
||||
};
|
||||
|
||||
const toggleSubscribe = async (radio: DjRadio) => {
|
||||
const isSubed = isRadioSubscribed.value(radio.id);
|
||||
try {
|
||||
await podcastApi.subscribeDj(radio.id, isSubed ? 0 : 1);
|
||||
|
||||
if (isSubed) {
|
||||
message.success('已取消订阅');
|
||||
} else {
|
||||
message.success('订阅成功');
|
||||
}
|
||||
|
||||
await fetchSubscribedRadios();
|
||||
|
||||
if (currentRadio.value?.id === radio.id) {
|
||||
currentRadio.value = { ...currentRadio.value, subed: !isSubed };
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('订阅操作失败:', error);
|
||||
message.error(isSubed ? '取消订阅失败' : '订阅失败');
|
||||
}
|
||||
};
|
||||
|
||||
const fetchRadioDetail = async (rid: number) => {
|
||||
try {
|
||||
isLoading.value = true;
|
||||
const res = await podcastApi.getDjDetail(rid);
|
||||
currentRadio.value = res.data?.data;
|
||||
if (currentRadio.value) {
|
||||
currentRadio.value.subed = isRadioSubscribed.value(rid);
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('获取电台详情失败:', error);
|
||||
message.error('获取电台详情失败');
|
||||
} finally {
|
||||
isLoading.value = false;
|
||||
}
|
||||
};
|
||||
|
||||
const fetchRadioPrograms = async (rid: number, offset = 0) => {
|
||||
try {
|
||||
isLoading.value = true;
|
||||
const res = await podcastApi.getDjProgram(rid, 30, offset);
|
||||
if (offset === 0) {
|
||||
currentPrograms.value = res.data?.programs || [];
|
||||
} else {
|
||||
currentPrograms.value.push(...(res.data?.programs || []));
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('获取节目列表失败:', error);
|
||||
message.error('获取节目列表失败');
|
||||
} finally {
|
||||
isLoading.value = false;
|
||||
}
|
||||
};
|
||||
|
||||
const fetchCategories = async () => {
|
||||
try {
|
||||
const res = await podcastApi.getDjCategoryList();
|
||||
categories.value = res.data?.categories || [];
|
||||
} catch (error) {
|
||||
console.error('获取分类列表失败:', error);
|
||||
}
|
||||
};
|
||||
|
||||
const fetchRecommendRadios = async () => {
|
||||
try {
|
||||
const res = await podcastApi.getDjRecommend();
|
||||
recommendRadios.value = res.data?.djRadios || [];
|
||||
} catch (error) {
|
||||
console.error('获取推荐电台失败:', error);
|
||||
}
|
||||
};
|
||||
|
||||
const fetchTodayPerfered = async () => {
|
||||
try {
|
||||
const res = await podcastApi.getDjTodayPerfered();
|
||||
todayPerfered.value = res.data?.data || [];
|
||||
} catch (error) {
|
||||
console.error('获取今日优选失败:', error);
|
||||
}
|
||||
};
|
||||
|
||||
const fetchRecentPrograms = async () => {
|
||||
try {
|
||||
const res = await podcastApi.getRecentDj();
|
||||
recentPrograms.value = res.data?.data?.list || [];
|
||||
} catch (error) {
|
||||
console.error('获取最近播放失败:', error);
|
||||
}
|
||||
};
|
||||
|
||||
const clearCurrentRadio = () => {
|
||||
currentRadio.value = null;
|
||||
currentPrograms.value = [];
|
||||
};
|
||||
|
||||
return {
|
||||
subscribedRadios,
|
||||
categories,
|
||||
currentRadio,
|
||||
currentPrograms,
|
||||
recommendRadios,
|
||||
todayPerfered,
|
||||
recentPrograms,
|
||||
isLoading,
|
||||
subscribedCount,
|
||||
isRadioSubscribed,
|
||||
fetchSubscribedRadios,
|
||||
toggleSubscribe,
|
||||
fetchRadioDetail,
|
||||
fetchRadioPrograms,
|
||||
fetchCategories,
|
||||
fetchRecommendRadios,
|
||||
fetchTodayPerfered,
|
||||
fetchRecentPrograms,
|
||||
clearCurrentRadio
|
||||
};
|
||||
},
|
||||
{
|
||||
persist: {
|
||||
key: 'podcast-store',
|
||||
storage: localStorage,
|
||||
pick: ['subscribedRadios', 'categories']
|
||||
}
|
||||
}
|
||||
);
|
||||
@@ -5,8 +5,23 @@ import { getDayRecommend } from '@/api/home';
|
||||
import type { IDayRecommend } from '@/types/day_recommend';
|
||||
import type { SongResult } from '@/types/music';
|
||||
|
||||
// 获取当前日期字符串 YYYY-MM-DD
|
||||
const getTodayDateString = (): string => {
|
||||
const now = new Date();
|
||||
return `${now.getFullYear()}-${String(now.getMonth() + 1).padStart(2, '0')}-${String(now.getDate()).padStart(2, '0')}`;
|
||||
};
|
||||
|
||||
export const useRecommendStore = defineStore('recommend', () => {
|
||||
const dailyRecommendSongs = ref<SongResult[]>([]);
|
||||
const lastFetchDate = ref<string>('');
|
||||
|
||||
// 检查数据是否过期(跨天)
|
||||
const isDataStale = (): boolean => {
|
||||
if (!lastFetchDate.value || dailyRecommendSongs.value.length === 0) {
|
||||
return true;
|
||||
}
|
||||
return lastFetchDate.value !== getTodayDateString();
|
||||
};
|
||||
|
||||
const fetchDailyRecommendSongs = async () => {
|
||||
try {
|
||||
@@ -15,6 +30,7 @@ export const useRecommendStore = defineStore('recommend', () => {
|
||||
|
||||
if (recommendData && Array.isArray(recommendData.dailySongs)) {
|
||||
dailyRecommendSongs.value = recommendData.dailySongs as any;
|
||||
lastFetchDate.value = getTodayDateString();
|
||||
console.log(`[Recommend Store] 已加载 ${recommendData.dailySongs.length} 首每日推荐歌曲。`);
|
||||
} else {
|
||||
dailyRecommendSongs.value = [];
|
||||
@@ -25,6 +41,15 @@ export const useRecommendStore = defineStore('recommend', () => {
|
||||
}
|
||||
};
|
||||
|
||||
// 如果数据过期则刷新
|
||||
const refreshIfStale = async (): Promise<boolean> => {
|
||||
if (isDataStale()) {
|
||||
await fetchDailyRecommendSongs();
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
};
|
||||
|
||||
const replaceSongInDailyRecommend = (oldSongId: number | string, newSong: SongResult) => {
|
||||
const index = dailyRecommendSongs.value.findIndex((song) => song.id === oldSongId);
|
||||
if (index !== -1) {
|
||||
@@ -37,7 +62,10 @@ export const useRecommendStore = defineStore('recommend', () => {
|
||||
|
||||
return {
|
||||
dailyRecommendSongs,
|
||||
lastFetchDate,
|
||||
isDataStale,
|
||||
fetchDailyRecommendSongs,
|
||||
refreshIfStale,
|
||||
replaceSongInDailyRecommend
|
||||
};
|
||||
});
|
||||
|
||||
@@ -1,9 +1,10 @@
|
||||
import { defineStore } from 'pinia';
|
||||
import { ref } from 'vue';
|
||||
import { computed, ref } from 'vue';
|
||||
|
||||
import { logout } from '@/api/login';
|
||||
import { getLikedList } from '@/api/music';
|
||||
import { getUserAlbumSublist, getUserPlaylist } from '@/api/user';
|
||||
import type { IUserDetail } from '@/types/user';
|
||||
import { clearLoginStatus } from '@/utils/auth';
|
||||
|
||||
interface UserData {
|
||||
@@ -23,6 +24,8 @@ function getLocalStorageItem<T>(key: string, defaultValue: T): T {
|
||||
export const useUserStore = defineStore('user', () => {
|
||||
// 状态
|
||||
const user = ref<UserData | null>(getLocalStorageItem('user', null));
|
||||
const userDetail = ref<IUserDetail | null>(null);
|
||||
const recordList = ref<any[]>([]);
|
||||
const loginType = ref<'token' | 'cookie' | 'qr' | 'uid' | null>(
|
||||
getLocalStorageItem('loginType', null)
|
||||
);
|
||||
@@ -205,6 +208,8 @@ export const useUserStore = defineStore('user', () => {
|
||||
initializeCollectedAlbums,
|
||||
addCollectedAlbum,
|
||||
removeCollectedAlbum,
|
||||
isAlbumCollected
|
||||
isAlbumCollected,
|
||||
userDetail,
|
||||
recordList
|
||||
};
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user