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

View File

@@ -56,6 +56,14 @@ export default {
dolby: 'Dolby Atmos',
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',
autoPlayDesc: 'Auto resume playback when reopening the app'
},

View File

@@ -0,0 +1,5 @@
"playback": {
"musicSources": "音源设置",
"musicSourcesDesc": "选择音乐解析使用的音源平台",
"musicSourcesWarning": "至少需要选择一个音源平台"
}

View File

@@ -56,6 +56,14 @@ export default {
dolby: '杜比全景声',
jymaster: '超清母带'
},
musicSources: '音源设置',
musicSourcesDesc: '选择音乐解析使用的音源平台',
musicSourcesWarning: '至少需要选择一个音源平台',
musicUnblockEnable: '启用音乐解析',
musicUnblockEnableDesc: '开启后将尝试解析无法播放的音乐',
configureMusicSources: '配置音源',
selectedMusicSources: '已选音源:',
noMusicSources: '未选择音源',
autoPlay: '自动播放',
autoPlayDesc: '重新打开应用时是否自动继续播放'
},

View File

@@ -5,16 +5,22 @@ import server from 'netease-cloud-music-api-alger/server';
import os from 'os';
import path from 'path';
import { unblockMusic } from './unblockMusic';
import { unblockMusic, type Platform } from './unblockMusic';
const store = new Store();
if (!fs.existsSync(path.resolve(os.tmpdir(), 'anonymous_token'))) {
fs.writeFileSync(path.resolve(os.tmpdir(), 'anonymous_token'), '', 'utf-8');
}
// 处理解锁音乐请求
ipcMain.handle('unblock-music', async (_, id, data) => {
return unblockMusic(id, data);
// 设置音乐解析的处理程序
ipcMain.handle('unblock-music', async (_event, id, songData, enabledSources) => {
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> {

View File

@@ -21,5 +21,7 @@
"downloadPath": "",
"language": "zh-CN",
"alwaysShowDownloadButton": false,
"unlimitedDownload": false
"unlimitedDownload": false,
"enableMusicUnblock": true,
"enabledMusicSources": ["migu", "kugou", "pyncmd","bilibili", "youtube"]
}

View File

@@ -6,6 +6,8 @@ interface SongData {
name: string;
artists: Array<{ name: string }>;
album?: { name: string };
ar?: Array<{ name: string }>;
al?: { name: string };
}
interface ResponseData {
@@ -27,24 +29,29 @@ interface UnblockResult {
};
}
// 所有可用平台
export const ALL_PLATFORMS: Platform[] = ['migu', 'kugou', 'pyncmd', 'kuwo', 'bilibili', 'youtube'];
/**
* 音乐解析函数
* @param id 歌曲ID
* @param songData 歌曲信息
* @param retryCount 重试次数
* @param enabledPlatforms 启用的平台列表,默认为所有平台
* @returns Promise<UnblockResult>
*/
const unblockMusic = async (
id: number | string,
songData: SongData,
retryCount = 3
retryCount = 1,
enabledPlatforms?: Platform[]
): Promise<UnblockResult> => {
// 所有可用平台
const platforms: Platform[] = ['migu', 'kugou', 'pyncmd', 'joox', 'kuwo', 'bilibili', 'youtube'];
const platforms = enabledPlatforms || ALL_PLATFORMS;
songData.album = songData.album || songData.al;
songData.artists = songData.artists || songData.ar;
const retry = async (attempt: number): Promise<UnblockResult> => {
try {
const data = await match(parseInt(String(id), 10), platforms, songData);
const data = await match(parseInt(String(id), 10), platforms,songData);
const result: UnblockResult = {
data: {
data,
@@ -58,7 +65,7 @@ const unblockMusic = async (
} catch (err) {
if (attempt < retryCount) {
// 延迟重试,每次重试增加延迟时间
await new Promise((resolve) => setTimeout(resolve, 1000 * attempt));
await new Promise((resolve) => setTimeout(resolve, 100 * attempt));
return retry(attempt + 1);
}

View File

@@ -14,7 +14,7 @@ interface API {
openLyric: () => void;
sendLyric: (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;
startDownload: (url: string) => void;
onDownloadProgress: (callback: (progress: number, status: string) => void) => void;

View File

@@ -16,7 +16,7 @@ const api = {
openLyric: () => ipcRenderer.send('open-lyric'),
sendLyric: (data) => ipcRenderer.send('send-lyric', 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) => {
ipcRenderer.on('lyric-window-closed', () => callback());

View File

@@ -4,6 +4,7 @@ import type { ILyric } from '@/type/lyric';
import { isElectron } from '@/utils';
import request from '@/utils/request';
import requestMusic from '@/utils/request_music';
import { cloneDeep } from 'lodash';
const { addData, getData, deleteData } = musicDB;
@@ -79,8 +80,15 @@ export const getMusicLrc = async (id: number) => {
};
export const getParsingMusicUrl = (id: number, data: any) => {
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 } });
};

View File

@@ -28,6 +28,8 @@ declare module 'vue' {
NEmpty: typeof import('naive-ui')['NEmpty']
NForm: typeof import('naive-ui')['NForm']
NFormItem: typeof import('naive-ui')['NFormItem']
NGrid: typeof import('naive-ui')['NGrid']
NGridItem: typeof import('naive-ui')['NGridItem']
NIcon: typeof import('naive-ui')['NIcon']
NImage: typeof import('naive-ui')['NImage']
NInput: typeof import('naive-ui')['NInput']

View File

@@ -67,6 +67,7 @@ export const getSongUrl = async (
}
} catch (error) {
console.error('error', error);
url = data.data[0].url || '';
}
if (isDownloaded) {
return songDetail;
@@ -344,7 +345,7 @@ export const usePlayerStore = defineStore('player', () => {
(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) => {
@@ -453,7 +454,7 @@ export const usePlayerStore = defineStore('player', () => {
}
await handlePlayMusic(prevSong);
await fetchSongs(playList.value, playListIndex.value - 5, nowPlayListIndex);
await fetchSongs(playList.value, playListIndex.value - 3, nowPlayListIndex);
};
const togglePlayMode = () => {

View File

@@ -150,6 +150,39 @@
/>
</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>
<div class="set-item-title">{{ t('settings.playback.autoPlay') }}</div>
@@ -470,6 +503,33 @@
</n-checkbox-group>
</n-space>
</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>
</template>
@@ -494,6 +554,11 @@ import { checkUpdate, UpdateResult } from '@/utils/update';
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 userStore = useUserStore();
@@ -977,6 +1042,41 @@ onMounted(() => {
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>
<style lang="scss" scoped>