feat: 添加 GD 音乐台支持及相关设置,优化音源解析功能

This commit is contained in:
alger
2025-04-23 00:10:28 +08:00
parent ed9cf9c4c5
commit 7df1c25168
7 changed files with 262 additions and 13 deletions

189
src/renderer/api/gdmusic.ts Normal file
View File

@@ -0,0 +1,189 @@
import axios from 'axios';
import type { MusicSourceType } from '@/type/music';
/**
* GD音乐台解析服务
*/
export interface GDMusicResponse {
url: string;
br: number;
size: number;
md5: string;
platform: string;
gain: number;
}
export interface ParsedMusicResult {
data: {
data: GDMusicResponse;
params: {
id: number;
type: string;
}
}
}
/**
* 从GD音乐台解析音乐URL
* @param id 音乐ID
* @param data 音乐数据,包含名称和艺术家信息
* @param quality 音质设置
* @returns 解析后的音乐URL及相关信息
*/
export const parseFromGDMusic = async (
id: number,
data: any,
quality: string = '320'
): Promise<ParsedMusicResult | null> => {
try {
// 处理不同数据结构
if (!data) {
console.error('GD音乐台解析歌曲数据为空');
throw new Error('歌曲数据为空');
}
const songName = data.name || '';
let artistNames = '';
// 处理不同的艺术家字段结构
if (data.artists && Array.isArray(data.artists)) {
artistNames = data.artists.map(artist => artist.name).join(' ');
} else if (data.ar && Array.isArray(data.ar)) {
artistNames = data.ar.map(artist => artist.name).join(' ');
} else if (data.artist) {
artistNames = typeof data.artist === 'string' ? data.artist : '';
}
const searchQuery = `${songName} ${artistNames}`.trim();
if (!searchQuery || searchQuery.length < 2) {
console.error('GD音乐台解析搜索查询过短', { name: songName, artists: artistNames });
throw new Error('搜索查询过短');
}
// 所有可用的音乐源
const allSources = [
'tencent', 'kugou', 'kuwo', 'migu', 'netease',
'joox', 'ytmusic', 'spotify', 'qobuz', 'deezer'
] as MusicSourceType[];
console.log('GD音乐台开始搜索:', searchQuery);
// 依次尝试所有音源
for (const source of allSources) {
try {
const result = await searchAndGetUrl(source, searchQuery, quality);
if (result) {
console.log(`GD音乐台成功通过 ${result.source} 解析音乐!`);
// 返回符合原API格式的数据
return {
data: {
data: {
url: result.url.replace(/\\/g, ''),
br: parseInt(result.br, 10) * 1000 || 320000,
size: result.size || 0,
md5: '',
platform: 'gdmusic',
gain: 0
},
params: {
id: parseInt(String(id), 10),
type: 'song'
}
}
};
}
} catch (error) {
console.error(`GD音乐台 ${source} 音源解析失败:`, error);
// 该音源失败,继续尝试下一个音源
continue;
}
}
console.log('GD音乐台所有音源均解析失败');
return null;
} catch (error) {
console.error('GD音乐台解析完全失败:', error);
return null;
}
};
/**
* 获取音质映射
* @param qualitySetting 设置中的音质选项
* @returns 映射到GD音乐台的音质参数
*/
export const getQualityMapping = (qualitySetting: string): string => {
const qualityMap: Record<string, string> = {
standard: '128',
higher: '320',
exhigh: '320',
lossless: '740',
hires: '999',
jyeffect: '999',
sky: '999',
dolby: '999',
jymaster: '999'
};
return qualityMap[qualitySetting] || '320';
};
interface GDMusicUrlResult {
url: string;
br: string;
size: number;
source: string;
}
const baseUrl = 'https://music-api.gdstudio.xyz/api.php';
/**
* 在指定音源搜索歌曲并获取URL
* @param source 音源
* @param searchQuery 搜索关键词
* @param quality 音质
* @returns 音乐URL结果
*/
async function searchAndGetUrl(
source: MusicSourceType,
searchQuery: string,
quality: string
): Promise<GDMusicUrlResult | null> {
// 1. 搜索歌曲
const searchUrl = `${baseUrl}?types=search&source=${source}&name=${encodeURIComponent(searchQuery)}&count=1&pages=1`;
console.log(`GD音乐台尝试音源 ${source} 搜索:`, searchUrl);
const searchResponse = await axios.get(searchUrl, { timeout: 5000 });
if (searchResponse.data && Array.isArray(searchResponse.data) && searchResponse.data.length > 0) {
const firstResult = searchResponse.data[0];
if (!firstResult || !firstResult.id) {
console.log(`GD音乐台 ${source} 搜索结果无效`);
return null;
}
const trackId = firstResult.id;
const trackSource = firstResult.source || source;
// 2. 获取歌曲URL
const songUrl = `${baseUrl}?types=url&source=${trackSource}&id=${trackId}&br=${quality}`;
console.log(`GD音乐台尝试获取 ${trackSource} 歌曲URL:`, songUrl);
const songResponse = await axios.get(songUrl, { timeout: 5000 });
if (songResponse.data && songResponse.data.url) {
return {
url: songResponse.data.url,
br: songResponse.data.br,
size: songResponse.data.size || 0,
source: trackSource
};
} else {
console.log(`GD音乐台 ${trackSource} 未返回有效URL`);
return null;
}
} else {
console.log(`GD音乐台 ${source} 搜索结果为空`);
return null;
}
}

View File

@@ -5,6 +5,7 @@ import { isElectron } from '@/utils';
import request from '@/utils/request';
import requestMusic from '@/utils/request_music';
import { cloneDeep } from 'lodash';
import { parseFromGDMusic, getQualityMapping } from './gdmusic';
const { addData, getData, deleteData } = musicDB;
@@ -79,17 +80,39 @@ export const getMusicLrc = async (id: number) => {
}
};
export const getParsingMusicUrl = (id: number, data: any) => {
if (isElectron) {
const settingStore = useSettingsStore();
export const getParsingMusicUrl = async (id: number, data: any) => {
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));
// 如果禁用了音乐解析功能,则直接返回空结果
if (!settingStore.setData.enableMusicUnblock) {
return Promise.resolve({ data: { code: 404, message: '音乐解析功能已禁用' } });
}
// 检查是否选择了GD音乐台解析
const enabledSources = settingStore.setData.enabledMusicSources || [];
if (enabledSources.includes('gdmusic')) {
// 获取音质设置并转换为GD音乐台格式
try {
const quality = getQualityMapping(settingStore.setData.musicQuality || 'higher');
// 调用封装的GD音乐台解析服务
const gdResult = await parseFromGDMusic(id, data, quality);
if (gdResult) {
return gdResult;
}
} catch (error) {
console.error('GD音乐台解析失败:', error);
}
console.log('GD音乐台所有音源均解析失败尝试使用unblockMusic');
}
// 如果GD音乐台解析失败或者未启用尝试使用unblockMusic
if (isElectron) {
const filteredSources = enabledSources.filter(source => source !== 'gdmusic');
return window.api.unblockMusic(id, cloneDeep(data), cloneDeep(filteredSources));
}
return requestMusic.get<any>('/music', { params: { id } });
};

View File

@@ -239,3 +239,19 @@ export interface IArtists {
img1v1: number;
trans: null;
}
// 音乐源类型定义
export type MusicSourceType =
| 'tencent'
| 'kugou'
| 'kuwo'
| 'migu'
| 'netease'
| 'joox'
| 'ytmusic'
| 'spotify'
| 'qobuz'
| 'deezer'
| 'gdmusic';
// 更多音乐相关的类型可以在这里定义

View File

@@ -521,6 +521,16 @@
<n-grid-item v-for="source in musicSourceOptions" :key="source.value">
<n-checkbox :value="source.value">
{{ source.label }}
<template v-if="source.value === 'gdmusic'">
<n-tooltip>
<template #trigger>
<n-icon size="16" class="ml-1 text-blue-500 cursor-help">
<i class="ri-information-line"></i>
</n-icon>
</template>
{{ t('settings.playback.gdmusicInfo') }}
</n-tooltip>
</template>
</n-checkbox>
</n-grid-item>
</n-grid>
@@ -528,6 +538,14 @@
<div v-if="musicSources.length === 0" class="text-red-500 text-sm">
{{ t('settings.playback.musicSourcesWarning') }}
</div>
<!-- GD音乐台设置 -->
<div v-if="musicSources.includes('gdmusic')" class="mt-4 border-t pt-4 border-gray-200 dark:border-gray-700">
<h3 class="text-base font-medium mb-2">GD音乐台(music.gdstudio.xyz)设置</h3>
<p class="text-sm text-gray-500 dark:text-gray-400 mb-2">
GD音乐台将自动尝试多个音乐平台进行解析无需额外配置。优先级高于其他解析方式但是请求可能较慢。感谢music.gdstudio.xyz
</p>
</div>
</n-space>
</n-modal>
</div>
@@ -555,9 +573,9 @@ import { checkUpdate, UpdateResult } from '@/utils/update';
import config from '../../../../package.json';
// 手动定义Platform类型避免从主进程导入的问题
type Platform = 'qq' | 'migu' | 'kugou' | 'pyncmd'| 'kuwo' | 'bilibili' | 'youtube';
type Platform = 'qq' | 'migu' | 'kugou' | 'pyncmd' | 'joox' | 'kuwo' | 'bilibili' | 'youtube' | 'gdmusic';
// 所有平台
const ALL_PLATFORMS: Platform[] = ['migu', 'kugou', 'pyncmd', 'kuwo', 'bilibili', 'youtube'];
const ALL_PLATFORMS: Platform[] = ['migu', 'kugou', 'pyncmd', 'kuwo', 'bilibili', 'youtube', 'gdmusic'];
const settingsStore = useSettingsStore();
const userStore = useUserStore();
@@ -1050,7 +1068,8 @@ const musicSourceOptions = ref([
{ label: 'pyncmd', value: 'pyncmd' },
{ label: '酷我音乐', value: 'kuwo' },
{ label: 'Bilibili音乐', value: 'bilibili' },
{ label: 'YouTube', value: 'youtube' }
{ label: 'YouTube', value: 'youtube' },
{ label: 'GD音乐台', value: 'gdmusic' }
]);
// 已选择的音源列表