feat: 优化音源解析功能,添加音源配置

This commit is contained in:
alger
2025-04-22 23:39:08 +08:00
parent 35b9cbfdbd
commit ed9cf9c4c5
12 changed files with 163 additions and 16 deletions
+8
View File
@@ -56,6 +56,14 @@ export default {
dolby: 'Dolby Atmos', dolby: 'Dolby Atmos',
jymaster: 'Master' jymaster: 'Master'
}, },
musicSources: 'Music Sources',
musicSourcesDesc: 'Select music sources for song resolution',
musicSourcesWarning: 'At least one music source must be selected',
musicUnblockEnable: 'Enable Music Unblocking',
musicUnblockEnableDesc: 'When enabled, attempts to resolve unplayable songs',
configureMusicSources: 'Configure Sources',
selectedMusicSources: 'Selected sources:',
noMusicSources: 'No sources selected',
autoPlay: 'Auto Play', autoPlay: 'Auto Play',
autoPlayDesc: 'Auto resume playback when reopening the app' autoPlayDesc: 'Auto resume playback when reopening the app'
}, },
+5
View File
@@ -0,0 +1,5 @@
"playback": {
"musicSources": "音源设置",
"musicSourcesDesc": "选择音乐解析使用的音源平台",
"musicSourcesWarning": "至少需要选择一个音源平台"
}
+8
View File
@@ -56,6 +56,14 @@ export default {
dolby: '杜比全景声', dolby: '杜比全景声',
jymaster: '超清母带' jymaster: '超清母带'
}, },
musicSources: '音源设置',
musicSourcesDesc: '选择音乐解析使用的音源平台',
musicSourcesWarning: '至少需要选择一个音源平台',
musicUnblockEnable: '启用音乐解析',
musicUnblockEnableDesc: '开启后将尝试解析无法播放的音乐',
configureMusicSources: '配置音源',
selectedMusicSources: '已选音源:',
noMusicSources: '未选择音源',
autoPlay: '自动播放', autoPlay: '自动播放',
autoPlayDesc: '重新打开应用时是否自动继续播放' autoPlayDesc: '重新打开应用时是否自动继续播放'
}, },
+10 -4
View File
@@ -5,16 +5,22 @@ import server from 'netease-cloud-music-api-alger/server';
import os from 'os'; import os from 'os';
import path from 'path'; import path from 'path';
import { unblockMusic } from './unblockMusic'; import { unblockMusic, type Platform } from './unblockMusic';
const store = new Store(); const store = new Store();
if (!fs.existsSync(path.resolve(os.tmpdir(), 'anonymous_token'))) { if (!fs.existsSync(path.resolve(os.tmpdir(), 'anonymous_token'))) {
fs.writeFileSync(path.resolve(os.tmpdir(), 'anonymous_token'), '', 'utf-8'); fs.writeFileSync(path.resolve(os.tmpdir(), 'anonymous_token'), '', 'utf-8');
} }
// 处理解锁音乐请求 // 设置音乐解析的处理程序
ipcMain.handle('unblock-music', async (_, id, data) => { ipcMain.handle('unblock-music', async (_event, id, songData, enabledSources) => {
return unblockMusic(id, data); try {
const result = await unblockMusic(id, songData, 1, enabledSources as Platform[]);
return result;
} catch (error) {
console.error('音乐解析失败:', error);
return { error: (error as Error).message || '未知错误' };
}
}); });
async function startMusicApi(): Promise<void> { async function startMusicApi(): Promise<void> {
+3 -1
View File
@@ -21,5 +21,7 @@
"downloadPath": "", "downloadPath": "",
"language": "zh-CN", "language": "zh-CN",
"alwaysShowDownloadButton": false, "alwaysShowDownloadButton": false,
"unlimitedDownload": false "unlimitedDownload": false,
"enableMusicUnblock": true,
"enabledMusicSources": ["migu", "kugou", "pyncmd","bilibili", "youtube"]
} }
+13 -6
View File
@@ -6,6 +6,8 @@ interface SongData {
name: string; name: string;
artists: Array<{ name: string }>; artists: Array<{ name: string }>;
album?: { name: string }; album?: { name: string };
ar?: Array<{ name: string }>;
al?: { name: string };
} }
interface ResponseData { interface ResponseData {
@@ -27,24 +29,29 @@ interface UnblockResult {
}; };
} }
// 所有可用平台
export const ALL_PLATFORMS: Platform[] = ['migu', 'kugou', 'pyncmd', 'kuwo', 'bilibili', 'youtube'];
/** /**
* 音乐解析函数 * 音乐解析函数
* @param id 歌曲ID * @param id 歌曲ID
* @param songData 歌曲信息 * @param songData 歌曲信息
* @param retryCount 重试次数 * @param retryCount 重试次数
* @param enabledPlatforms 启用的平台列表,默认为所有平台
* @returns Promise<UnblockResult> * @returns Promise<UnblockResult>
*/ */
const unblockMusic = async ( const unblockMusic = async (
id: number | string, id: number | string,
songData: SongData, songData: SongData,
retryCount = 3 retryCount = 1,
enabledPlatforms?: Platform[]
): Promise<UnblockResult> => { ): Promise<UnblockResult> => {
// 所有可用平台 const platforms = enabledPlatforms || ALL_PLATFORMS;
const platforms: Platform[] = ['migu', 'kugou', 'pyncmd', 'joox', 'kuwo', 'bilibili', 'youtube']; songData.album = songData.album || songData.al;
songData.artists = songData.artists || songData.ar;
const retry = async (attempt: number): Promise<UnblockResult> => { const retry = async (attempt: number): Promise<UnblockResult> => {
try { try {
const data = await match(parseInt(String(id), 10), platforms, songData); const data = await match(parseInt(String(id), 10), platforms,songData);
const result: UnblockResult = { const result: UnblockResult = {
data: { data: {
data, data,
@@ -58,7 +65,7 @@ const unblockMusic = async (
} catch (err) { } catch (err) {
if (attempt < retryCount) { if (attempt < retryCount) {
// 延迟重试,每次重试增加延迟时间 // 延迟重试,每次重试增加延迟时间
await new Promise((resolve) => setTimeout(resolve, 1000 * attempt)); await new Promise((resolve) => setTimeout(resolve, 100 * attempt));
return retry(attempt + 1); return retry(attempt + 1);
} }
+1 -1
View File
@@ -14,7 +14,7 @@ interface API {
openLyric: () => void; openLyric: () => void;
sendLyric: (data: any) => void; sendLyric: (data: any) => void;
sendSong: (data: any) => void; sendSong: (data: any) => void;
unblockMusic: (id: number, data: any) => Promise<any>; unblockMusic: (id: number, data: any, enabledSources?: string[]) => Promise<any>;
onLyricWindowClosed: (callback: () => void) => void; onLyricWindowClosed: (callback: () => void) => void;
startDownload: (url: string) => void; startDownload: (url: string) => void;
onDownloadProgress: (callback: (progress: number, status: string) => void) => void; onDownloadProgress: (callback: (progress: number, status: string) => void) => void;
+1 -1
View File
@@ -16,7 +16,7 @@ const api = {
openLyric: () => ipcRenderer.send('open-lyric'), openLyric: () => ipcRenderer.send('open-lyric'),
sendLyric: (data) => ipcRenderer.send('send-lyric', data), sendLyric: (data) => ipcRenderer.send('send-lyric', data),
sendSong: (data) => ipcRenderer.send('update-current-song', data), sendSong: (data) => ipcRenderer.send('update-current-song', data),
unblockMusic: (id) => ipcRenderer.invoke('unblock-music', id), unblockMusic: (id, data, enabledSources) => ipcRenderer.invoke('unblock-music', id, data, enabledSources),
// 歌词窗口关闭事件 // 歌词窗口关闭事件
onLyricWindowClosed: (callback: () => void) => { onLyricWindowClosed: (callback: () => void) => {
ipcRenderer.on('lyric-window-closed', () => callback()); ipcRenderer.on('lyric-window-closed', () => callback());
+9 -1
View File
@@ -4,6 +4,7 @@ import type { ILyric } from '@/type/lyric';
import { isElectron } from '@/utils'; import { isElectron } from '@/utils';
import request from '@/utils/request'; import request from '@/utils/request';
import requestMusic from '@/utils/request_music'; import requestMusic from '@/utils/request_music';
import { cloneDeep } from 'lodash';
const { addData, getData, deleteData } = musicDB; const { addData, getData, deleteData } = musicDB;
@@ -79,8 +80,15 @@ export const getMusicLrc = async (id: number) => {
}; };
export const getParsingMusicUrl = (id: number, data: any) => { export const getParsingMusicUrl = (id: number, data: any) => {
if (isElectron) { if (isElectron) {
return window.api.unblockMusic(id, data); const settingStore = useSettingsStore();
// 如果禁用了音乐解析功能,则直接返回空结果
if (!settingStore.setData.enableMusicUnblock) {
return Promise.resolve({ data: { code: 404, message: '音乐解析功能已禁用' } });
}
return window.api.unblockMusic(id, cloneDeep(data), cloneDeep(settingStore.setData.enabledMusicSources));
} }
return requestMusic.get<any>('/music', { params: { id } }); return requestMusic.get<any>('/music', { params: { id } });
}; };
+2
View File
@@ -28,6 +28,8 @@ declare module 'vue' {
NEmpty: typeof import('naive-ui')['NEmpty'] NEmpty: typeof import('naive-ui')['NEmpty']
NForm: typeof import('naive-ui')['NForm'] NForm: typeof import('naive-ui')['NForm']
NFormItem: typeof import('naive-ui')['NFormItem'] NFormItem: typeof import('naive-ui')['NFormItem']
NGrid: typeof import('naive-ui')['NGrid']
NGridItem: typeof import('naive-ui')['NGridItem']
NIcon: typeof import('naive-ui')['NIcon'] NIcon: typeof import('naive-ui')['NIcon']
NImage: typeof import('naive-ui')['NImage'] NImage: typeof import('naive-ui')['NImage']
NInput: typeof import('naive-ui')['NInput'] NInput: typeof import('naive-ui')['NInput']
+3 -2
View File
@@ -67,6 +67,7 @@ export const getSongUrl = async (
} }
} catch (error) { } catch (error) {
console.error('error', error); console.error('error', error);
url = data.data[0].url || '';
} }
if (isDownloaded) { if (isDownloaded) {
return songDetail; return songDetail;
@@ -344,7 +345,7 @@ export const usePlayerStore = defineStore('player', () => {
(item: SongResult) => item.id === music.id && item.source === music.source (item: SongResult) => item.id === music.id && item.source === music.source
); );
fetchSongs(playList.value, playListIndex.value + 1, playListIndex.value + 6); fetchSongs(playList.value, playListIndex.value + 1, playListIndex.value + 3);
}; };
const setPlay = async (song: SongResult) => { const setPlay = async (song: SongResult) => {
@@ -453,7 +454,7 @@ export const usePlayerStore = defineStore('player', () => {
} }
await handlePlayMusic(prevSong); await handlePlayMusic(prevSong);
await fetchSongs(playList.value, playListIndex.value - 5, nowPlayListIndex); await fetchSongs(playList.value, playListIndex.value - 3, nowPlayListIndex);
}; };
const togglePlayMode = () => { const togglePlayMode = () => {
+100
View File
@@ -150,6 +150,39 @@
/> />
</div> </div>
<div class="set-item">
<div>
<div class="set-item-title">{{ t('settings.playback.musicSources') }}</div>
<div class="set-item-content">
<div class="flex items-center gap-2">
<n-switch v-model:value="setData.enableMusicUnblock">
<template #checked>{{ t('common.on') }}</template>
<template #unchecked>{{ t('common.off') }}</template>
</n-switch>
<span>{{ t('settings.playback.musicUnblockEnableDesc') }}</span>
</div>
<div v-if="setData.enableMusicUnblock" class="mt-2">
<div class="text-sm">
<span class="text-gray-500">{{ t('settings.playback.selectedMusicSources') }}</span>
<span v-if="musicSources.length > 0" class="text-gray-400">
{{ musicSources.map((source) => getSourceLabel(source)).join(', ') }}
</span>
<span v-else class="text-red-500 text-xs">
{{ t('settings.playback.noMusicSources') }}
</span>
</div>
</div>
</div>
</div>
<n-button
size="small"
:disabled="!setData.enableMusicUnblock"
@click="showMusicSourcesModal = true"
>
{{ t('settings.playback.configureMusicSources') }}
</n-button>
</div>
<div class="set-item"> <div class="set-item">
<div> <div>
<div class="set-item-title">{{ t('settings.playback.autoPlay') }}</div> <div class="set-item-title">{{ t('settings.playback.autoPlay') }}</div>
@@ -470,6 +503,33 @@
</n-checkbox-group> </n-checkbox-group>
</n-space> </n-space>
</n-modal> </n-modal>
<!-- 音源设置弹窗 -->
<n-modal
v-model:show="showMusicSourcesModal"
preset="dialog"
:title="t('settings.playback.musicSources')"
:positive-text="t('common.confirm')"
:negative-text="t('common.cancel')"
@positive-click="showMusicSourcesModal = false"
@negative-click="showMusicSourcesModal = false"
>
<n-space vertical>
<p>{{ t('settings.playback.musicSourcesDesc') }}</p>
<n-checkbox-group v-model:value="musicSources">
<n-grid :cols="2" :x-gap="12" :y-gap="8">
<n-grid-item v-for="source in musicSourceOptions" :key="source.value">
<n-checkbox :value="source.value">
{{ source.label }}
</n-checkbox>
</n-grid-item>
</n-grid>
</n-checkbox-group>
<div v-if="musicSources.length === 0" class="text-red-500 text-sm">
{{ t('settings.playback.musicSourcesWarning') }}
</div>
</n-space>
</n-modal>
</div> </div>
</template> </template>
@@ -494,6 +554,11 @@ import { checkUpdate, UpdateResult } from '@/utils/update';
import config from '../../../../package.json'; import config from '../../../../package.json';
// 手动定义Platform类型,避免从主进程导入的问题
type Platform = 'qq' | 'migu' | 'kugou' | 'pyncmd'| 'kuwo' | 'bilibili' | 'youtube';
// 所有平台
const ALL_PLATFORMS: Platform[] = ['migu', 'kugou', 'pyncmd', 'kuwo', 'bilibili', 'youtube'];
const settingsStore = useSettingsStore(); const settingsStore = useSettingsStore();
const userStore = useUserStore(); const userStore = useUserStore();
@@ -977,6 +1042,41 @@ onMounted(() => {
handleScroll({ target: { scrollTop: 0 } }); handleScroll({ target: { scrollTop: 0 } });
}); });
}); });
// 音源设置相关
const musicSourceOptions = ref([
{ label: 'MiGu音乐', value: 'migu' },
{ label: '酷狗音乐', value: 'kugou' },
{ label: 'pyncmd', value: 'pyncmd' },
{ label: '酷我音乐', value: 'kuwo' },
{ label: 'Bilibili音乐', value: 'bilibili' },
{ label: 'YouTube', value: 'youtube' }
]);
// 已选择的音源列表
const musicSources = computed({
get: () => {
if (!setData.value.enabledMusicSources) {
return ALL_PLATFORMS;
}
return setData.value.enabledMusicSources as Platform[];
},
set: (newValue: Platform[]) => {
// 确保至少选择一个音源
const valuesToSet = newValue.length > 0 ? newValue : ALL_PLATFORMS;
setData.value = {
...setData.value,
enabledMusicSources: valuesToSet
};
}
});
const showMusicSourcesModal = ref(false);
const getSourceLabel = (source: Platform) => {
const sourceLabel = musicSourceOptions.value.find(s => s.value === source)?.label;
return sourceLabel || source;
};
</script> </script>
<style lang="scss" scoped> <style lang="scss" scoped>