diff --git a/.eslintrc b/.eslintrc index c41d1c0..168f5b1 100644 --- a/.eslintrc +++ b/.eslintrc @@ -42,6 +42,9 @@ "no-console": "off", "no-continue": "off", "no-restricted-syntax": "off", + "no-return-assign": "off", + "no-unused-expressions": "off", + "no-return-await": "off", "no-plusplus": "off", "no-param-reassign": "off", "no-shadow": "off", diff --git a/app.js b/app.js index 888d985..56079a3 100644 --- a/app.js +++ b/app.js @@ -20,7 +20,7 @@ function createWindow() { win.setMinimumSize(1200, 780); if (process.env.NODE_ENV === 'development') { win.webContents.openDevTools({ mode: 'detach' }); - win.loadURL('http://localhost:7788/'); + win.loadURL('http://localhost:4488/'); } else { win.loadURL(`file://${__dirname}/dist/index.html`); } diff --git a/package.json b/package.json index d5d66e1..fb487fe 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "alger-music", - "version": "1.7.0", + "version": "2.0.0", "description": "这是一个用于音乐播放的应用程序。", "author": "Alger ", "main": "app.js", diff --git a/public/css/base.css b/public/css/base.css index 0ab255e..deec8cf 100644 --- a/public/css/base.css +++ b/public/css/base.css @@ -1,3 +1,7 @@ body{ background-color: #000; +} + +.n-popover:has(.music-play){ + border-radius: 1.5rem !important; } \ No newline at end of file diff --git a/src/api/home.ts b/src/api/home.ts index ce20cc0..83500de 100644 --- a/src/api/home.ts +++ b/src/api/home.ts @@ -43,7 +43,7 @@ export const getRecommendMusic = (params: IRecommendMusicParams) => { // 获取每日推荐 export const getDayRecommend = () => { - return request.get>('/recommend/songs'); + return request.get>>('/recommend/songs'); }; // 获取最新专辑推荐 diff --git a/src/components/RecommendSinger.vue b/src/components/RecommendSinger.vue index 602fb8f..c35a9d3 100644 --- a/src/components/RecommendSinger.vue +++ b/src/components/RecommendSinger.vue @@ -74,32 +74,32 @@ const store = useStore(); const hotSingerData = ref(); const dayRecommendData = ref(); const showMusic = ref(false); -// // 加载推荐歌手 -// const loadSingerList = async () => { -// const { data } = await getHotSinger({ offset: 0, limit: 5 }); -// hotSingerData.value = data; -// }; -// const loadDayRecommend = async () => { -// const { data } = await getDayRecommend(); -// dayRecommendData.value = data.data; -// }; -// 页面初始化 onMounted(async () => { await loadData(); }); const loadData = async () => { try { - const [{ data: singerData }, { data: dayRecommend }] = await Promise.all([ - getHotSinger({ offset: 0, limit: 5 }), - getDayRecommend(), - ]); - if (dayRecommend.data) { - singerData.artists = singerData.artists.slice(0, 4); + // 第一个请求:获取热门歌手 + const { data: singerData } = await getHotSinger({ offset: 0, limit: 5 }); + + // 第二个请求:获取每日推荐 + try { + const { + data: { data: dayRecommend }, + } = await getDayRecommend(); + console.log('dayRecommend', dayRecommend); + // 处理数据 + if (dayRecommend) { + singerData.artists = singerData.artists.slice(0, 4); + } + dayRecommendData.value = dayRecommend; + } catch (error) { + console.error('error', error); } + hotSingerData.value = singerData; - dayRecommendData.value = dayRecommend.data; } catch (error) { console.error('error', error); } diff --git a/src/components/common/SongItem.vue b/src/components/common/SongItem.vue index ec414b3..f7076b9 100644 --- a/src/components/common/SongItem.vue +++ b/src/components/common/SongItem.vue @@ -78,9 +78,11 @@ const imageLoad = async () => { if (!songImageRef.value) { return; } - const background = await getImageBackground((songImageRef.value as any).imageRef as unknown as HTMLImageElement); + const { backgroundColor } = await getImageBackground( + (songImageRef.value as any).imageRef as unknown as HTMLImageElement, + ); // eslint-disable-next-line vue/no-mutating-props - props.item.backgroundColor = background; + props.item.backgroundColor = backgroundColor; }; // 播放音乐 设置音乐详情 打开音乐底栏 diff --git a/src/hooks/MusicHook.ts b/src/hooks/MusicHook.ts index 1e9288f..584a2ff 100644 --- a/src/hooks/MusicHook.ts +++ b/src/hooks/MusicHook.ts @@ -1,156 +1,177 @@ +import { computed, ref } from 'vue'; + import { getMusicLrc } from '@/api/music'; +import store from '@/store'; import { ILyric } from '@/type/lyric'; +import type { ILyricText, SongResult } from '@/type/music'; const windowData = window as any; -export const isElectron = computed(() => { - return !!windowData.electronAPI; -}); - -interface ILrcData { - text: string; - trText: string; -} - -export const lrcData = ref(); -export const newLrcIndex = ref(0); -export const lrcArray = ref>([]); -export const lrcTimeArray = ref>([]); - -export const parseTime = (timeString: string) => { - const [minutes, seconds] = timeString.split(':'); - return Number(minutes) * 60 + Number(seconds); -}; - -const TIME_REGEX = /(\d{2}:\d{2}(\.\d*)?)/g; -const LRC_REGEX = /(\[(\d{2}):(\d{2})(\.(\d*))?\])/g; - -function parseLyricLine(lyricLine: string) { - const timeText = lyricLine.match(TIME_REGEX)?.[0] || ''; - const time = parseTime(timeText); - const text = lyricLine.replace(LRC_REGEX, '').trim(); - return { time, text }; -} - -interface ILyricText { - text: string; - trText: string; -} - -function parseLyrics(lyricsString: string) { - const lines = lyricsString.split('\n'); - const lyrics: Array = []; - const times: number[] = []; - lines.forEach((line) => { - const { time, text } = parseLyricLine(line); - times.push(time); - lyrics.push({ text, trText: '' }); - }); - return { lyrics, times }; -} - -export const loadLrc = async (playMusicId: number): Promise => { - try { - const { data } = await getMusicLrc(playMusicId); - const { lyrics, times } = parseLyrics(data.lrc.lyric); - let tlyric: { - [key: string]: string; - } = {}; - if (data.tlyric.lyric) { - const { lyrics: tLyrics, times: tTimes } = parseLyrics(data.tlyric.lyric); - tlyric = tLyrics.reduce((acc: any, cur, index) => { - acc[tTimes[index]] = cur.text; - return acc; - }, {}); - } - if (Object.keys(tlyric).length) { - lyrics.forEach((item, index) => { - item.trText = item.text ? tlyric[times[index].toString()] : ''; - }); - } - lrcTimeArray.value = times; - lrcArray.value = lyrics; - } catch (err) { - console.error('err', err); - } -}; - -// 歌词矫正时间Correction time -const correctionTime = ref(0.4); +export const isElectron = computed(() => !!windowData.electronAPI); + +export const lrcArray = ref([]); // 歌词数组 +export const lrcTimeArray = ref([]); // 歌词时间数组 +export const nowTime = ref(0); // 当前播放时间 +export const allTime = ref(0); // 总播放时间 +export const nowIndex = ref(0); // 当前播放歌词 +export const correctionTime = ref(0.4); // 歌词矫正时间Correction time +export const currentLrcProgress = ref(0); // 来存储当前歌词的进度 +export const audio = ref(); // 音频对象 +export const playMusic = computed(() => store.state.playMusic as SongResult); // 当前播放歌曲 + +watch( + () => store.state.playMusic, + () => { + nextTick(() => { + lrcArray.value = playMusic.value.lyric?.lrcArray || []; + lrcTimeArray.value = playMusic.value.lyric?.lrcTimeArray || []; + }); + }, + { + deep: true, + }, +); +const isPlaying = computed(() => store.state.play as boolean); // 增加矫正时间 -export const addCorrectionTime = (time: number) => { - correctionTime.value += time; -}; +export const addCorrectionTime = (time: number) => (correctionTime.value += time); // 减少矫正时间 -export const reduceCorrectionTime = (time: number) => { - correctionTime.value -= time; -}; +export const reduceCorrectionTime = (time: number) => (correctionTime.value -= time); -export const isCurrentLrc = (index: number, time: number) => { - const currentTime = Number(lrcTimeArray.value[index]); - const nextTime = Number(lrcTimeArray.value[index + 1]); +// 获取当前播放歌词 +export const isCurrentLrc = (index: number, time: number): boolean => { + const currentTime = lrcTimeArray.value[index]; + const nextTime = lrcTimeArray.value[index + 1]; const nowTime = time + correctionTime.value; const isTrue = nowTime > currentTime && nowTime < nextTime; - if (isTrue) { - newLrcIndex.value = index; - } return isTrue; }; -export const nowTime = ref(0); -export const allTime = ref(0); -export const nowIndex = ref(0); - -export const getLrcIndex = (time: number) => { +// 获取当前播放歌词INDEX +export const getLrcIndex = (time: number): number => { for (let i = 0; i < lrcTimeArray.value.length; i++) { if (isCurrentLrc(i, time)) { - nowIndex.value = i || nowIndex.value; + nowIndex.value = i; return i; } } return nowIndex.value; }; +// 获取当前播放歌词进度 +const currentLrcTiming = computed(() => { + const start = lrcTimeArray.value[nowIndex.value] || 0; + const end = lrcTimeArray.value[nowIndex.value + 1] || start + 1; + return { start, end }; +}); + +// 获取歌词样式 +export const getLrcStyle = (index: number) => { + if (index === nowIndex.value) { + return { + backgroundImage: `linear-gradient(to right, #ffffff ${currentLrcProgress.value}%, #ffffff8a ${currentLrcProgress.value}%)`, + backgroundClip: 'text', + WebkitBackgroundClip: 'text', + color: 'transparent', + transition: 'background-image 0.1s linear', + }; + } + return {}; +}; + +watch(nowTime, (newTime) => { + const newIndex = getLrcIndex(newTime); + if (newIndex !== nowIndex.value) { + nowIndex.value = newIndex; + currentLrcProgress.value = 0; // 重置进度 + } +}); + +// 播放进度 +export const useLyricProgress = () => { + let animationFrameId: number | null = null; + + const updateProgress = () => { + if (!isPlaying.value) return; + audio.value = audio.value || (document.querySelector('#MusicAudio') as HTMLAudioElement); + if (!audio.value) return; + const { start, end } = currentLrcTiming.value; + const duration = end - start; + const elapsed = audio.value.currentTime - start; + currentLrcProgress.value = Math.min(Math.max((elapsed / duration) * 100, 0), 100); + + animationFrameId = requestAnimationFrame(updateProgress); + }; + + const startProgressAnimation = () => { + if (!animationFrameId && isPlaying.value) { + updateProgress(); + } + }; + + const stopProgressAnimation = () => { + if (animationFrameId) { + cancelAnimationFrame(animationFrameId); + animationFrameId = null; + } + }; + + watch(isPlaying, (newIsPlaying) => { + if (newIsPlaying) { + startProgressAnimation(); + } else { + stopProgressAnimation(); + } + }); + + onMounted(() => { + if (isPlaying.value) { + startProgressAnimation(); + } + }); + + onUnmounted(() => { + stopProgressAnimation(); + }); + + return { + currentLrcProgress, + getLrcStyle, + }; +}; + // 设置当前播放时间 export const setAudioTime = (index: number, audio: HTMLAudioElement) => { - audio.currentTime = lrcTimeArray.value[index] as number; + audio.currentTime = lrcTimeArray.value[index]; audio.play(); }; -// 计算这个歌词的播放时间 -const getLrcTime = (index: number) => { - return Number(lrcTimeArray.value[index]); -}; - // 获取当前播放的歌词 export const getCurrentLrc = () => { const index = getLrcIndex(nowTime.value); - const currentLrc = lrcArray.value[index]; - const nextLrc = lrcArray.value[index + 1]; - return { currentLrc, nextLrc }; + return { + currentLrc: lrcArray.value[index], + nextLrc: lrcArray.value[index + 1], + }; }; // 获取一句歌词播放时间是 几秒到几秒 -export const getLrcTimeRange = (index: number) => { - const currentTime = Number(lrcTimeArray.value[index]); - const nextTime = Number(lrcTimeArray.value[index + 1]); - return { currentTime, nextTime }; -}; +export const getLrcTimeRange = (index: number) => ({ + currentTime: lrcTimeArray.value[index], + nextTime: lrcTimeArray.value[index + 1], +}); export const sendLyricToWin = (isPlay: boolean = true) => { + if (!isElectron.value) return; + try { - if (!isElectron.value) { - return; - } - // 设置lyricWinData 获取 当前播放的两句歌词 和歌词时间 - let lyricWinData = null; if (lrcArray.value.length > 0) { const nowIndex = getLrcIndex(nowTime.value); const { currentLrc, nextLrc } = getCurrentLrc(); const { currentTime, nextTime } = getLrcTimeRange(nowIndex); - lyricWinData = { + // 设置lyricWinData 获取 当前播放的两句歌词 和歌词时间 + const lyricWinData = { currentLrc, nextLrc, currentTime, @@ -160,20 +181,18 @@ export const sendLyricToWin = (isPlay: boolean = true) => { lrcArray: lrcArray.value, nowTime: nowTime.value, allTime: allTime.value, - startCurrentTime: getLrcTime(nowIndex), + startCurrentTime: lrcTimeArray.value[nowIndex], isPlay, }; windowData.electronAPI.sendLyric(JSON.stringify(lyricWinData)); } } catch (error) { - console.error('error', error); + console.error('Error sending lyric to window:', error); } }; export const openLyric = () => { - if (!isElectron.value) { - return; - } + if (!isElectron.value) return; windowData.electronAPI.openLyric(); sendLyricToWin(); }; diff --git a/src/hooks/MusicListHook.ts b/src/hooks/MusicListHook.ts new file mode 100644 index 0000000..9b5d5d0 --- /dev/null +++ b/src/hooks/MusicListHook.ts @@ -0,0 +1,175 @@ +import { getMusicLrc, getMusicUrl, getParsingMusicUrl } from '@/api/music'; +import { useMusicHistory } from '@/hooks/MusicHistoryHook'; +import type { ILyric, ILyricText, SongResult } from '@/type/music'; +import { getImgUrl, getMusicProxyUrl } from '@/utils'; +import { getImageLinearBackground } from '@/utils/linearColor'; + +const musicHistory = useMusicHistory(); + +// 获取歌曲url +const getSongUrl = async (id: number) => { + const { data } = await getMusicUrl(id); + let url = ''; + try { + if (data.data[0].freeTrialInfo || !data.data[0].url) { + const res = await getParsingMusicUrl(id); + url = res.data.data.url; + } + } catch (error) { + console.error('error', error); + } + url = url || data.data[0].url; + return getMusicProxyUrl(url); +}; + +const getSongDetail = async (playMusic: SongResult) => { + if (playMusic.playMusicUrl) { + return playMusic; + } + playMusic.playLoading = true; + const playMusicUrl = await getSongUrl(playMusic.id); + const { backgroundColor, primaryColor } = + playMusic.backgroundColor && playMusic.primaryColor + ? playMusic + : await getImageLinearBackground(getImgUrl(playMusic?.picUrl, '30y30')); + + playMusic.playLoading = false; + return { ...playMusic, playMusicUrl, backgroundColor, primaryColor }; +}; + +// 加载 当前歌曲 歌曲列表数据 下一首mp3预加载 歌词数据 +export const useMusicListHook = () => { + const handlePlayMusic = async (state: any, playMusic: SongResult) => { + const updatedPlayMusic = await getSongDetail(playMusic); + state.playMusic = updatedPlayMusic; + state.playMusicUrl = updatedPlayMusic.playMusicUrl; + state.play = true; + loadLrcAsync(state, updatedPlayMusic.id); + musicHistory.addMusic(state.playMusic); + const playListIndex = state.playList.findIndex((item: SongResult) => item.id === playMusic.id); + state.playListIndex = playListIndex; + // 请求后续五首歌曲的详情 + fetchSongs(state, playListIndex + 1, playListIndex + 6); + }; + + // 用于预加载下一首歌曲的 MP3 数据 + const preloadNextSong = (nextSongUrl: string) => { + const audio = new Audio(nextSongUrl); + audio.preload = 'auto'; // 设置预加载 + audio.load(); // 手动加载 + }; + + const fetchSongs = async (state: any, startIndex: number, endIndex: number) => { + const songs = state.playList.slice(Math.max(0, startIndex), Math.min(endIndex, state.playList.length)); + + const detailedSongs = await Promise.all( + songs.map(async (song: SongResult) => { + // 如果歌曲详情已经存在,就不重复请求 + if (!song.playMusicUrl) { + return await getSongDetail(song); + } + return song; + }), + ); + // 加载下一首的歌词 + const nextSong = detailedSongs[0]; + if (!(nextSong.lyric && nextSong.lyric.lrcTimeArray.length > 0)) { + nextSong.lyric = await loadLrc(nextSong.id); + } + + // 更新播放列表中的歌曲详情 + detailedSongs.forEach((song, index) => { + state.playList[startIndex + index] = song; + }); + preloadNextSong(nextSong.playMusicUrl); + }; + + const nextPlay = async (state: any) => { + if (state.playList.length === 0) { + state.play = true; + return; + } + const playListIndex = (state.playListIndex + 1) % state.playList.length; + await handlePlayMusic(state, state.playList[playListIndex]); + }; + + const prevPlay = async (state: any) => { + if (state.playList.length === 0) { + state.play = true; + return; + } + const playListIndex = (state.playListIndex - 1 + state.playList.length) % state.playList.length; + await handlePlayMusic(state, state.playList[playListIndex]); + await fetchSongs(state, playListIndex - 5, playListIndex); + }; + + const parseTime = (timeString: string): number => { + const [minutes, seconds] = timeString.split(':'); + return Number(minutes) * 60 + Number(seconds); + }; + + const parseLyricLine = (lyricLine: string): { time: number; text: string } => { + const TIME_REGEX = /(\d{2}:\d{2}(\.\d*)?)/g; + const LRC_REGEX = /(\[(\d{2}):(\d{2})(\.(\d*))?\])/g; + const timeText = lyricLine.match(TIME_REGEX)?.[0] || ''; + const time = parseTime(timeText); + const text = lyricLine.replace(LRC_REGEX, '').trim(); + return { time, text }; + }; + + const parseLyrics = (lyricsString: string): { lyrics: ILyricText[]; times: number[] } => { + const lines = lyricsString.split('\n'); + const lyrics: ILyricText[] = []; + const times: number[] = []; + lines.forEach((line) => { + const { time, text } = parseLyricLine(line); + times.push(time); + lyrics.push({ text, trText: '' }); + }); + return { lyrics, times }; + }; + + const loadLrc = async (playMusicId: number): Promise => { + try { + const { data } = await getMusicLrc(playMusicId); + const { lyrics, times } = parseLyrics(data.lrc.lyric); + const tlyric: Record = {}; + + if (data.tlyric.lyric) { + const { lyrics: tLyrics, times: tTimes } = parseLyrics(data.tlyric.lyric); + tLyrics.forEach((lyric, index) => { + tlyric[tTimes[index].toString()] = lyric.text; + }); + } + + lyrics.forEach((item, index) => { + item.trText = item.text ? tlyric[times[index].toString()] || '' : ''; + }); + return { + lrcTimeArray: times, + lrcArray: lyrics, + }; + } catch (err) { + console.error('Error loading lyrics:', err); + return { + lrcTimeArray: [], + lrcArray: [], + }; + } + }; + + // 异步加载歌词的方法 + const loadLrcAsync = async (state: any, playMusicId: number) => { + if (state.playMusic.lyric && state.playMusic.lyric.lrcTimeArray.length > 0) { + return; + } + const lyrics = await loadLrc(playMusicId); + state.playMusic.lyric = lyrics; + }; + + return { + handlePlayMusic, + nextPlay, + prevPlay, + }; +}; diff --git a/src/layout/components/MusicFull.vue b/src/layout/components/MusicFull.vue index d4384b5..17917af 100644 --- a/src/layout/components/MusicFull.vue +++ b/src/layout/components/MusicFull.vue @@ -1,69 +1,72 @@