mirror of
https://github.com/algerkong/AlgerMusicPlayer.git
synced 2026-05-18 03:17:29 +08:00
@@ -29,6 +29,15 @@ export default {
|
|||||||
lrc: {
|
lrc: {
|
||||||
noLrc: 'No lyrics, please enjoy'
|
noLrc: 'No lyrics, please enjoy'
|
||||||
},
|
},
|
||||||
|
reparse: {
|
||||||
|
title: 'Select Music Source',
|
||||||
|
desc: 'Click a source to directly reparse the current song. This source will be used next time this song plays.',
|
||||||
|
success: 'Reparse successful',
|
||||||
|
failed: 'Reparse failed',
|
||||||
|
warning: 'Please select a music source',
|
||||||
|
bilibiliNotSupported: 'Bilibili videos do not support reparsing',
|
||||||
|
processing: 'Processing...'
|
||||||
|
},
|
||||||
playBar: {
|
playBar: {
|
||||||
expand: 'Expand Lyrics',
|
expand: 'Expand Lyrics',
|
||||||
collapse: 'Collapse Lyrics',
|
collapse: 'Collapse Lyrics',
|
||||||
@@ -37,6 +46,7 @@ export default {
|
|||||||
noSongPlaying: 'No song playing',
|
noSongPlaying: 'No song playing',
|
||||||
eq: 'Equalizer',
|
eq: 'Equalizer',
|
||||||
playList: 'Play List',
|
playList: 'Play List',
|
||||||
|
reparse: 'Reparse',
|
||||||
playMode: {
|
playMode: {
|
||||||
sequence: 'Sequence',
|
sequence: 'Sequence',
|
||||||
loop: 'Loop',
|
loop: 'Loop',
|
||||||
|
|||||||
@@ -29,6 +29,15 @@ export default {
|
|||||||
lrc: {
|
lrc: {
|
||||||
noLrc: '暂无歌词, 请欣赏'
|
noLrc: '暂无歌词, 请欣赏'
|
||||||
},
|
},
|
||||||
|
reparse: {
|
||||||
|
title: '选择解析音源',
|
||||||
|
desc: '点击音源直接进行解析,下次播放此歌曲时将使用所选音源',
|
||||||
|
success: '重新解析成功',
|
||||||
|
failed: '重新解析失败',
|
||||||
|
warning: '请选择一个音源',
|
||||||
|
bilibiliNotSupported: 'B站视频不支持重新解析',
|
||||||
|
processing: '解析中...'
|
||||||
|
},
|
||||||
playBar: {
|
playBar: {
|
||||||
expand: '展开歌词',
|
expand: '展开歌词',
|
||||||
collapse: '收起歌词',
|
collapse: '收起歌词',
|
||||||
@@ -37,6 +46,7 @@ export default {
|
|||||||
noSongPlaying: '没有正在播放的歌曲',
|
noSongPlaying: '没有正在播放的歌曲',
|
||||||
eq: '均衡器',
|
eq: '均衡器',
|
||||||
playList: '播放列表',
|
playList: '播放列表',
|
||||||
|
reparse: '重新解析',
|
||||||
playMode: {
|
playMode: {
|
||||||
sequence: '顺序播放',
|
sequence: '顺序播放',
|
||||||
loop: '循环播放',
|
loop: '循环播放',
|
||||||
|
|||||||
@@ -88,8 +88,26 @@ export const getParsingMusicUrl = async (id: number, data: any) => {
|
|||||||
return Promise.resolve({ data: { code: 404, message: '音乐解析功能已禁用' } });
|
return Promise.resolve({ data: { code: 404, message: '音乐解析功能已禁用' } });
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 获取音源设置,优先使用歌曲自定义音源
|
||||||
|
const songId = String(id);
|
||||||
|
const savedSource = localStorage.getItem(`song_source_${songId}`);
|
||||||
|
let enabledSources: any[] = [];
|
||||||
|
|
||||||
|
// 如果有歌曲自定义音源,使用自定义音源
|
||||||
|
if (savedSource) {
|
||||||
|
try {
|
||||||
|
enabledSources = JSON.parse(savedSource);
|
||||||
|
console.log(`使用歌曲 ${id} 自定义音源:`, enabledSources);
|
||||||
|
} catch (e) {
|
||||||
|
console.error('解析自定义音源失败, 使用全局设置', e);
|
||||||
|
enabledSources = settingStore.setData.enabledMusicSources || [];
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// 没有自定义音源,使用全局音源设置
|
||||||
|
enabledSources = settingStore.setData.enabledMusicSources || [];
|
||||||
|
}
|
||||||
|
|
||||||
// 检查是否选择了GD音乐台解析
|
// 检查是否选择了GD音乐台解析
|
||||||
const enabledSources = settingStore.setData.enabledMusicSources || [];
|
|
||||||
if (enabledSources.includes('gdmusic')) {
|
if (enabledSources.includes('gdmusic')) {
|
||||||
// 获取音质设置并转换为GD音乐台格式
|
// 获取音质设置并转换为GD音乐台格式
|
||||||
try {
|
try {
|
||||||
|
|||||||
@@ -127,6 +127,12 @@
|
|||||||
</template>
|
</template>
|
||||||
{{ playMusic.id ? t('player.playBar.lyric') : t('player.playBar.noSongPlaying') }}
|
{{ playMusic.id ? t('player.playBar.lyric') : t('player.playBar.noSongPlaying') }}
|
||||||
</n-tooltip>
|
</n-tooltip>
|
||||||
|
<n-tooltip v-if="playMusic.id && isElectron" trigger="hover" :z-index="9999999">
|
||||||
|
<template #trigger>
|
||||||
|
<reparse-popover v-if="playMusic.id" />
|
||||||
|
</template>
|
||||||
|
{{ t('player.playBar.reparse') }}
|
||||||
|
</n-tooltip>
|
||||||
<n-popover
|
<n-popover
|
||||||
v-if="isElectron"
|
v-if="isElectron"
|
||||||
trigger="click"
|
trigger="click"
|
||||||
@@ -200,6 +206,7 @@ import { useI18n } from 'vue-i18n';
|
|||||||
import SongItem from '@/components/common/SongItem.vue';
|
import SongItem from '@/components/common/SongItem.vue';
|
||||||
import EqControl from '@/components/EQControl.vue';
|
import EqControl from '@/components/EQControl.vue';
|
||||||
import SleepTimerPopover from '@/components/player/SleepTimerPopover.vue';
|
import SleepTimerPopover from '@/components/player/SleepTimerPopover.vue';
|
||||||
|
import ReparsePopover from '@/components/player/ReparsePopover.vue';
|
||||||
import {
|
import {
|
||||||
allTime,
|
allTime,
|
||||||
artistList,
|
artistList,
|
||||||
@@ -576,10 +583,10 @@ const handleDeleteSong = (song: SongResult) => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
.audio-button {
|
.audio-button {
|
||||||
@apply flex items-center mx-4;
|
@apply flex items-center;
|
||||||
|
|
||||||
.iconfont {
|
.iconfont {
|
||||||
@apply text-2xl transition cursor-pointer m-4;
|
@apply text-2xl transition cursor-pointer mx-3;
|
||||||
@apply hover:text-green-500;
|
@apply hover:text-green-500;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,233 @@
|
|||||||
|
<template>
|
||||||
|
<n-popover
|
||||||
|
trigger="click"
|
||||||
|
:z-index="99999999"
|
||||||
|
placement="top"
|
||||||
|
content-class="music-source-popover"
|
||||||
|
raw
|
||||||
|
:show-arrow="false"
|
||||||
|
:delay="200"
|
||||||
|
>
|
||||||
|
<template #trigger>
|
||||||
|
<n-tooltip trigger="hover" :z-index="9999999">
|
||||||
|
<template #trigger>
|
||||||
|
<i
|
||||||
|
class="iconfont ri-refresh-line"
|
||||||
|
:class="{ 'text-green-500': isReparse, 'animate-spin': isReparsing }"
|
||||||
|
></i>
|
||||||
|
</template>
|
||||||
|
{{ t('player.playBar.reparse') }}
|
||||||
|
</n-tooltip>
|
||||||
|
</template>
|
||||||
|
<div class="reparse-popover bg-light-100 dark:bg-dark-100 p-4 rounded-xl max-w-60">
|
||||||
|
<div class="text-base font-medium mb-2">{{ t('player.reparse.title') }}</div>
|
||||||
|
<div class="text-sm opacity-70 mb-3">{{ t('player.reparse.desc') }}</div>
|
||||||
|
<div class="mb-3">
|
||||||
|
<div class="flex flex-col space-y-2">
|
||||||
|
<div
|
||||||
|
v-for="source in musicSourceOptions"
|
||||||
|
:key="source.value"
|
||||||
|
class="source-button flex items-center p-2 rounded-lg cursor-pointer transition-all duration-200 bg-light-200 dark:bg-dark-200 hover:bg-light-300 dark:hover:bg-dark-300"
|
||||||
|
:class="{
|
||||||
|
'bg-green-50 dark:bg-green-900/20 text-green-500': isCurrentSource(source.value),
|
||||||
|
'opacity-50 cursor-not-allowed': isReparsing || playMusic.source === 'bilibili'
|
||||||
|
}"
|
||||||
|
@click="directReparseMusic(source.value)"
|
||||||
|
>
|
||||||
|
<div class="flex items-center justify-center w-6 h-6 mr-3 text-lg">
|
||||||
|
<i :class="getSourceIcon(source.value)"></i>
|
||||||
|
</div>
|
||||||
|
<div class="flex-1 text-sm whitespace-nowrap overflow-hidden text-ellipsis">
|
||||||
|
{{ source.label }}
|
||||||
|
</div>
|
||||||
|
<div v-if="isReparsing && currentReparsingSource === source.value" class="w-5 h-5 flex items-center justify-center">
|
||||||
|
<i class="ri-loader-4-line animate-spin"></i>
|
||||||
|
</div>
|
||||||
|
<div v-else-if="isCurrentSource(source.value)" class="w-5 h-5 flex items-center justify-center">
|
||||||
|
<i class="ri-check-line"></i>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div v-if="playMusic.source === 'bilibili'" class="text-red-500 text-sm">
|
||||||
|
{{ t('player.reparse.bilibiliNotSupported') }}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</n-popover>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script lang="ts" setup>
|
||||||
|
import { computed, ref, watch } from 'vue';
|
||||||
|
import { useI18n } from 'vue-i18n';
|
||||||
|
import { useMessage } from 'naive-ui';
|
||||||
|
import { playMusic } from '@/hooks/MusicHook';
|
||||||
|
import { usePlayerStore } from '@/store/modules/player';
|
||||||
|
import type { Platform } from '@/types/music';
|
||||||
|
import { audioService } from '@/services/audioService';
|
||||||
|
|
||||||
|
const playerStore = usePlayerStore();
|
||||||
|
const { t } = useI18n();
|
||||||
|
const message = useMessage();
|
||||||
|
|
||||||
|
// 音源重新解析状态
|
||||||
|
const isReparsing = ref(false);
|
||||||
|
const currentReparsingSource = ref<Platform | null>(null);
|
||||||
|
|
||||||
|
// 实际存储选中音源的值
|
||||||
|
const selectedSourcesValue = ref<Platform[]>([]);
|
||||||
|
|
||||||
|
// 判断当前歌曲是否有自定义解析记录
|
||||||
|
const isReparse = computed(() => {
|
||||||
|
const songId = String(playMusic.value.id);
|
||||||
|
return localStorage.getItem(`song_source_${songId}`) !== null;
|
||||||
|
});
|
||||||
|
|
||||||
|
// 可选音源列表
|
||||||
|
const musicSourceOptions = ref([
|
||||||
|
{ label: 'MiGu音乐', value: 'migu' as Platform },
|
||||||
|
{ label: '酷狗音乐', value: 'kugou' as Platform },
|
||||||
|
{ label: 'pyncmd', value: 'pyncmd' as Platform },
|
||||||
|
{ label: '酷我音乐', value: 'kuwo' as Platform },
|
||||||
|
{ label: 'Bilibili音乐', value: 'bilibili' as Platform },
|
||||||
|
{ label: 'GD音乐台', value: 'gdmusic' as Platform }
|
||||||
|
]);
|
||||||
|
|
||||||
|
// 检查音源是否被选中
|
||||||
|
const isCurrentSource = (source: Platform) => {
|
||||||
|
return selectedSourcesValue.value.includes(source);
|
||||||
|
};
|
||||||
|
|
||||||
|
// 获取音源图标
|
||||||
|
const getSourceIcon = (source: Platform) => {
|
||||||
|
const iconMap: Record<Platform, string> = {
|
||||||
|
'migu': 'ri-music-2-fill',
|
||||||
|
'kugou': 'ri-music-fill',
|
||||||
|
'kuwo': 'ri-album-fill',
|
||||||
|
'qq': 'ri-qq-fill',
|
||||||
|
'joox': 'ri-disc-fill',
|
||||||
|
'pyncmd': 'ri-netease-cloud-music-fill',
|
||||||
|
'bilibili': 'ri-bilibili-fill',
|
||||||
|
'gdmusic': 'ri-google-fill'
|
||||||
|
};
|
||||||
|
|
||||||
|
return iconMap[source] || 'ri-music-2-fill';
|
||||||
|
};
|
||||||
|
|
||||||
|
// 初始化选中的音源
|
||||||
|
const initSelectedSources = () => {
|
||||||
|
const songId = String(playMusic.value.id);
|
||||||
|
const savedSource = localStorage.getItem(`song_source_${songId}`);
|
||||||
|
|
||||||
|
if (savedSource) {
|
||||||
|
try {
|
||||||
|
selectedSourcesValue.value = JSON.parse(savedSource);
|
||||||
|
} catch (e) {
|
||||||
|
selectedSourcesValue.value = [];
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
selectedSourcesValue.value = [];
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// 直接重新解析当前歌曲
|
||||||
|
const directReparseMusic = async (source: Platform) => {
|
||||||
|
if (isReparsing.value || playMusic.value.source === 'bilibili') {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
isReparsing.value = true;
|
||||||
|
currentReparsingSource.value = source;
|
||||||
|
|
||||||
|
// 更新选中的音源值为当前点击的音源
|
||||||
|
const songId = String(playMusic.value.id);
|
||||||
|
selectedSourcesValue.value = [source];
|
||||||
|
|
||||||
|
// 保存到localStorage
|
||||||
|
localStorage.setItem(`song_source_${songId}`, JSON.stringify(selectedSourcesValue.value));
|
||||||
|
|
||||||
|
const success = await playerStore.reparseCurrentSong(source);
|
||||||
|
|
||||||
|
if (success) {
|
||||||
|
message.success(t('player.reparse.success'));
|
||||||
|
} else {
|
||||||
|
message.error(t('player.reparse.failed'));
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error('解析失败:', error);
|
||||||
|
message.error(t('player.reparse.failed'));
|
||||||
|
} finally {
|
||||||
|
isReparsing.value = false;
|
||||||
|
currentReparsingSource.value = null;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// 监听歌曲ID变化,初始化音源设置
|
||||||
|
watch(() => playMusic.value.id, () => {
|
||||||
|
if (playMusic.value.id) {
|
||||||
|
initSelectedSources();
|
||||||
|
}
|
||||||
|
}, { immediate: true });
|
||||||
|
|
||||||
|
// 监听歌曲变化,检查是否有自定义音源
|
||||||
|
watch(() => playMusic.value.id, async (newId) => {
|
||||||
|
if (newId) {
|
||||||
|
const songId = String(newId);
|
||||||
|
const savedSource = localStorage.getItem(`song_source_${songId}`);
|
||||||
|
|
||||||
|
// 如果有保存的音源设置但当前不是使用自定义解析的播放,尝试应用
|
||||||
|
if (savedSource && playMusic.value.source !== 'bilibili') {
|
||||||
|
try {
|
||||||
|
const sources = JSON.parse(savedSource) as Platform[];
|
||||||
|
console.log(`检测到歌曲ID ${songId} 有自定义音源设置:`, sources);
|
||||||
|
|
||||||
|
// 当URL加载失败或过期时,自动应用自定义音源重新加载
|
||||||
|
audioService.on('url_expired', async (trackInfo) => {
|
||||||
|
if (trackInfo && trackInfo.id === playMusic.value.id) {
|
||||||
|
console.log('URL已过期,自动应用自定义音源重新加载');
|
||||||
|
try {
|
||||||
|
isReparsing.value = true;
|
||||||
|
const success = await playerStore.reparseCurrentSong(sources[0]);
|
||||||
|
if (!success) {
|
||||||
|
message.error(t('player.reparse.failed'));
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
console.error('自动重新解析失败:', e);
|
||||||
|
message.error(t('player.reparse.failed'));
|
||||||
|
} finally {
|
||||||
|
isReparsing.value = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
} catch (e) {
|
||||||
|
console.error('解析保存的音源设置失败:', e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style lang="scss" scoped>
|
||||||
|
.music-source-popover {
|
||||||
|
@apply w-64 rounded-xl overflow-hidden;
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes spin {
|
||||||
|
0% { transform: rotate(0deg); }
|
||||||
|
100% { transform: rotate(360deg); }
|
||||||
|
}
|
||||||
|
|
||||||
|
.animate-spin {
|
||||||
|
animation: spin 1s linear infinite;
|
||||||
|
}
|
||||||
|
|
||||||
|
.source-button {
|
||||||
|
&:hover:not(.opacity-50) {
|
||||||
|
@apply transform -translate-y-0.5 shadow-sm;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.iconfont {
|
||||||
|
@apply text-2xl mx-3;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
@@ -14,6 +14,7 @@ import { createDiscreteApi } from 'naive-ui';
|
|||||||
|
|
||||||
import { useSettingsStore } from './settings';
|
import { useSettingsStore } from './settings';
|
||||||
import { useUserStore } from './user';
|
import { useUserStore } from './user';
|
||||||
|
import { type Platform } from '@/types/music';
|
||||||
|
|
||||||
const musicHistory = useMusicHistory();
|
const musicHistory = useMusicHistory();
|
||||||
const { message } = createDiscreteApi(['message']);
|
const { message } = createDiscreteApi(['message']);
|
||||||
@@ -102,6 +103,27 @@ export const getSongUrl = async (
|
|||||||
}
|
}
|
||||||
|
|
||||||
const numericId = typeof id === 'string' ? parseInt(id, 10) : id;
|
const numericId = typeof id === 'string' ? parseInt(id, 10) : id;
|
||||||
|
|
||||||
|
// 检查是否有自定义音源设置
|
||||||
|
const songId = String(id);
|
||||||
|
const savedSource = localStorage.getItem(`song_source_${songId}`);
|
||||||
|
|
||||||
|
// 如果有自定义音源设置,直接使用getParsingMusicUrl获取URL
|
||||||
|
if (savedSource && songData.source !== 'bilibili') {
|
||||||
|
try {
|
||||||
|
console.log(`使用自定义音源解析歌曲 ID: ${songId}`);
|
||||||
|
const res = await getParsingMusicUrl(numericId, cloneDeep(songData));
|
||||||
|
if (res && res.data && res.data.data && res.data.data.url) {
|
||||||
|
return res.data.data.url;
|
||||||
|
}
|
||||||
|
// 如果自定义音源解析失败,继续使用正常的获取流程
|
||||||
|
console.warn('自定义音源解析失败,使用默认音源');
|
||||||
|
} catch (error) {
|
||||||
|
console.error('自定义音源解析出错:', error);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 正常获取URL流程
|
||||||
const { data } = await getMusicUrl(numericId, isDownloaded);
|
const { data } = await getMusicUrl(numericId, isDownloaded);
|
||||||
let url = '';
|
let url = '';
|
||||||
let songDetail = null;
|
let songDetail = null;
|
||||||
@@ -1170,6 +1192,62 @@ export const usePlayerStore = defineStore('player', () => {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// 使用指定的音源重新解析当前播放的歌曲
|
||||||
|
const reparseCurrentSong = async (sourcePlatform: Platform) => {
|
||||||
|
try {
|
||||||
|
const currentSong = playMusic.value;
|
||||||
|
if (!currentSong || !currentSong.id) {
|
||||||
|
console.warn('没有有效的播放对象');
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
// B站视频不支持重新解析
|
||||||
|
if (currentSong.source === 'bilibili') {
|
||||||
|
console.warn('B站视频不支持重新解析');
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 保存用户选择的音源
|
||||||
|
const songId = String(currentSong.id);
|
||||||
|
localStorage.setItem(`song_source_${songId}`, JSON.stringify([sourcePlatform]));
|
||||||
|
|
||||||
|
// 停止当前播放
|
||||||
|
const currentSound = audioService.getCurrentSound();
|
||||||
|
if (currentSound) {
|
||||||
|
currentSound.pause();
|
||||||
|
}
|
||||||
|
|
||||||
|
// 重新获取歌曲URL
|
||||||
|
const numericId = typeof currentSong.id === 'string'
|
||||||
|
? parseInt(currentSong.id, 10)
|
||||||
|
: currentSong.id;
|
||||||
|
|
||||||
|
const res = await getParsingMusicUrl(numericId, cloneDeep(currentSong));
|
||||||
|
if (res && res.data && res.data.data && res.data.data.url) {
|
||||||
|
// 更新URL
|
||||||
|
const newUrl = res.data.data.url;
|
||||||
|
|
||||||
|
// 使用新URL更新播放
|
||||||
|
const updatedMusic = {
|
||||||
|
...currentSong,
|
||||||
|
playMusicUrl: newUrl,
|
||||||
|
expiredAt: Date.now() + 1800000 // 半小时后过期
|
||||||
|
};
|
||||||
|
|
||||||
|
// 更新播放器状态并开始播放
|
||||||
|
await setPlay(updatedMusic);
|
||||||
|
setPlayMusic(true);
|
||||||
|
|
||||||
|
return true;
|
||||||
|
} else {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error('重新解析失败:', error);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
return {
|
return {
|
||||||
play,
|
play,
|
||||||
isPlay,
|
isPlay,
|
||||||
@@ -1212,6 +1290,7 @@ export const usePlayerStore = defineStore('player', () => {
|
|||||||
addToFavorite,
|
addToFavorite,
|
||||||
removeFromFavorite,
|
removeFromFavorite,
|
||||||
removeFromPlayList,
|
removeFromPlayList,
|
||||||
playAudio
|
playAudio,
|
||||||
|
reparseCurrentSong
|
||||||
};
|
};
|
||||||
});
|
});
|
||||||
|
|||||||
Reference in New Issue
Block a user