fix: 修改国际化问题 和 音源优先级以及音源解析错误处理

This commit is contained in:
alger
2025-09-13 22:52:37 +08:00
parent dc99331911
commit e91667a2e6
10 changed files with 494 additions and 424 deletions
+115 -59
View File
@@ -9,9 +9,9 @@ import request from '@/utils/request';
import requestMusic from '@/utils/request_music';
import { searchAndGetBilibiliAudioUrl } from './bilibili';
import type { ParsedMusicResult } from './gdmusic';
import { parseFromGDMusic } from './gdmusic';
import { parseFromCustomApi } from './parseFromCustomApi';
import type { ParsedMusicResult } from './gdmusic';
const { addData, getData, deleteData } = musicDB;
@@ -32,6 +32,8 @@ export const getMusicUrl = async (id: number, isDownloaded: boolean = false) =>
params: {
id,
level: settingStore.setData.musicQuality || 'higher',
encodeType: settingStore.setData.musicQuality == 'lossless' ? 'aac' : 'flac',
// level为lossless时,encodeType=flac时网易云会返回hires音质,encodeType=aac时网易云会返回lossless音质
cookie: `${localStorage.getItem('token')} os=pc;`
}
});
@@ -47,7 +49,8 @@ export const getMusicUrl = async (id: number, isDownloaded: boolean = false) =>
return await request.get('/song/url/v1', {
params: {
id,
level: settingStore.setData.musicQuality || 'higher'
level: settingStore.setData.musicQuality || 'higher',
encodeType: settingStore.setData.musicQuality == 'lossless' ? 'aac' : 'flac'
}
});
};
@@ -116,7 +119,8 @@ const getBilibiliAudio = async (data: SongResult) => {
* @param data 歌曲数据
* @returns 解析结果,失败时返回null
*/
const getGDMusicAudio = async (id: number, data: SongResult): Promise<ParsedMusicResult | null> => { // <-- 在这里明确声明返回类型
const getGDMusicAudio = async (id: number, data: SongResult): Promise<ParsedMusicResult | null> => {
// <-- 在这里明确声明返回类型
try {
const gdResult = await parseFromGDMusic(id, data, '999');
if (gdResult) {
@@ -148,74 +152,120 @@ const getUnblockMusicAudio = (id: number, data: SongResult, sources: any[]) => {
* @returns 解析结果
*/
export const getParsingMusicUrl = async (id: number, data: SongResult) => {
if (isElectron) {
const settingStore = useSettingsStore();
try {
if (isElectron) {
let musicSources: any[] = [];
let quality: string = 'higher';
try {
const settingStore = useSettingsStore();
const enableMusicUnblock = settingStore?.setData?.enableMusicUnblock;
// 如果禁用了音乐解析功能,则直接返回空结果
if (!settingStore.setData.enableMusicUnblock) {
return Promise.resolve({ data: { code: 404, message: '音乐解析功能已禁用' } });
}
// 如果禁用了音乐解析功能,则直接返回空结果
if (!enableMusicUnblock) {
return Promise.resolve({ data: { code: 404, message: '音乐解析功能已禁用' } });
}
// 1. 确定使用的音源列表(自定义或全局)
const songId = String(id);
const savedSourceStr = localStorage.getItem(`song_source_${songId}`);
let musicSources: any[] = [];
// 1. 确定使用的音源列表(自定义或全局)
const songId = String(id);
const savedSourceStr = (() => {
try {
return localStorage.getItem(`song_source_${songId}`);
} catch (e) {
console.warn('读取本地存储失败,忽略自定义音源', e);
return null;
}
})();
try {
if (savedSourceStr) {
// 使用自定义音源
musicSources = JSON.parse(savedSourceStr);
console.log(`使用歌曲 ${id} 自定义音源:`, musicSources);
} else {
// 使用全局音源设置
musicSources = settingStore.setData.enabledMusicSources || [];
console.log(`使用全局音源设置:`, musicSources);
if (savedSourceStr) {
try {
musicSources = JSON.parse(savedSourceStr);
console.log(`使用歌曲 ${id} 自定义音源:`, musicSources);
} catch (e) {
console.error('解析音源设置失败,回退到默认全局设置', e);
musicSources = settingStore?.setData?.enabledMusicSources || [];
}
} else {
// 使用全局音源设置
musicSources = settingStore?.setData?.enabledMusicSources || [];
console.log(`使用全局音源设置:`, musicSources);
}
quality = settingStore?.setData?.musicQuality || 'higher';
} catch (e) {
console.error('读取设置失败,使用默认配置', e);
musicSources = [];
quality = 'higher';
}
} catch (e) {
console.error('解析音源设置失败,回退到默认全局设置', e);
musicSources = settingStore.setData.enabledMusicSources || [];
}
const quality = settingStore.setData.musicQuality || 'higher';
// 优先级 1: 自定义 API
if (musicSources.includes('custom') && settingStore.setData.customApiPlugin) {
console.log('尝试使用 自定义API 解析...');
const customResult = await parseFromCustomApi(id, data, quality);
if (customResult) {
return customResult; // 成功则直接返回
// 优先级 1: 自定义 API
try {
const hasCustom = Array.isArray(musicSources) && musicSources.includes('custom');
const customEnabled = (() => {
try {
const st = useSettingsStore();
return Boolean(st?.setData?.customApiPlugin);
} catch {
return false;
}
})();
if (hasCustom && customEnabled) {
console.log('尝试使用 自定义API 解析...');
const customResult = await parseFromCustomApi(id, data, quality);
if (customResult) {
return customResult; // 成功则直接返回
}
console.log('自定义API解析失败,继续尝试其他音源...');
}
} catch (e) {
console.error('自定义API解析发生异常,继续尝试其他音源', e);
}
console.log('自定义API解析失败,继续尝试其他音源...');
}
// 优先级 2: Bilibili
if (musicSources.includes('bilibili')) {
console.log('尝试使用 Bilibili 解析...');
const bilibiliResult = await getBilibiliAudio(data);
if (bilibiliResult?.data?.data?.url) { // 检查返回的 URL 是否有效
return bilibiliResult;
// 优先级 2: Bilibili
try {
if (Array.isArray(musicSources) && musicSources.includes('bilibili')) {
console.log('尝试使用 Bilibili 解析...');
const bilibiliResult = await getBilibiliAudio(data);
if (bilibiliResult?.data?.data?.url) {
return bilibiliResult;
}
console.log('Bilibili解析失败,继续尝试其他音源...');
}
} catch (e) {
console.error('Bilibili解析发生异常,继续尝试其他音源', e);
}
console.log('Bilibili解析失败,继续尝试其他音源...');
}
// 优先级 3: GD 音乐台
if (musicSources.includes('gdmusic')) {
console.log('尝试使用 GD音乐台 解析...');
const gdResult = await getGDMusicAudio(id, data);
if (gdResult) {
return gdResult;
// 优先级 3: GD 音乐台
try {
if (Array.isArray(musicSources) && musicSources.includes('gdmusic')) {
console.log('尝试使用 GD音乐台 解析...');
const gdResult = await getGDMusicAudio(id, data);
if (gdResult) {
return gdResult;
}
console.log('GD音乐台解析失败,继续尝试其他音源...');
}
} catch (e) {
console.error('GD音乐台解析发生异常,继续尝试其他音源', e);
}
console.log('GD音乐台解析失败,继续尝试其他音源...');
}
// 优先级 4: UnblockMusic (migu, kugou, pyncmd)
const unblockSources = musicSources.filter(
source => !['custom', 'bilibili', 'gdmusic'].includes(source)
);
if (unblockSources.length > 0) {
console.log('尝试使用 UnblockMusic 解析:', unblockSources);
return getUnblockMusicAudio(id, data, unblockSources);
// 优先级 4: UnblockMusic (migu, kugou, pyncmd)
try {
const unblockSources = (Array.isArray(musicSources) ? musicSources : []).filter(
(source) => !['custom', 'bilibili', 'gdmusic'].includes(source)
);
if (unblockSources.length > 0) {
console.log('尝试使用 UnblockMusic 解析:', unblockSources);
// 捕获内部可能的异常
return await getUnblockMusicAudio(id, data, unblockSources);
} else {
console.warn('UnblockMusic API 不可用,跳过此解析方式');
}
} catch (e) {
console.error('UnblockMusic 解析发生异常,继续后备方案', e);
}
}
} catch (e) {
console.error('getParsingMusicUrl 执行异常,将使用后备方案:', e);
}
// 后备方案:使用API请求
@@ -228,6 +278,12 @@ export const likeSong = (id: number, like: boolean = true) => {
return request.get('/like', { params: { id, like } });
};
// 将每日推荐中的歌曲标记为不感兴趣,并获取一首新歌
export const dislikeRecommendedSong = (id: number | string) => {
return request.get('/recommend/songs/dislike', {
params: { id }
});
};
// 获取用户喜欢的音乐列表
export const getLikedList = (uid: number) => {
return request.get('/likelist', {
+85 -84
View File
@@ -1,106 +1,107 @@
import axios from 'axios';
import {get} from 'lodash';
import {useSettingsStore} from '@/store';
import { get } from 'lodash';
// 从同级目录的 gdmusic.ts 导入类型,确保兼容性
import type {ParsedMusicResult} from './gdmusic';
import { useSettingsStore } from '@/store';
import type { ParsedMusicResult } from './gdmusic';
/**
* 定义自定义API JSON插件的结构
*/
interface CustomApiPlugin {
name: string;
apiUrl: string;
method?: 'GET' | 'POST';
params: Record<string, string>;
qualityMapping?: Record<string, string>;
responseUrlPath: string;
name: string;
apiUrl: string;
method?: 'GET' | 'POST';
params: Record<string, string>;
qualityMapping?: Record<string, string>;
responseUrlPath: string;
}
/**
* 从用户导入的自定义API JSON配置中解析音乐URL
*/
export const parseFromCustomApi = async (
id: number,
_songData: any,
quality: string = 'higher',
timeout: number = 10000
id: number,
_songData: any,
quality: string = 'higher',
timeout: number = 10000
): Promise<ParsedMusicResult | null> => {
const settingsStore = useSettingsStore();
const pluginString = settingsStore.setData.customApiPlugin;
const settingsStore = useSettingsStore();
const pluginString = settingsStore.setData.customApiPlugin;
if (!pluginString) {
return null;
if (!pluginString) {
return null;
}
let plugin: CustomApiPlugin;
try {
plugin = JSON.parse(pluginString);
if (!plugin.apiUrl || !plugin.params || !plugin.responseUrlPath) {
console.error('自定义APIJSON配置文件格式不正确。');
return null;
}
} catch (error) {
console.error('自定义API:解析JSON配置文件失败。', error);
return null;
}
console.log(`自定义API:正在使用插件 [${plugin.name}] 进行解析...`);
try {
// 1. 准备请求参数,替换占位符
const finalParams: Record<string, string> = {};
for (const [key, value] of Object.entries(plugin.params)) {
if (value === '{songId}') {
finalParams[key] = String(id);
} else if (value === '{quality}') {
// 使用 qualityMapping (如果存在) 进行音质翻译,否则直接使用原quality
finalParams[key] = plugin.qualityMapping?.[quality] ?? quality;
} else {
// 固定值参数
finalParams[key] = value;
}
}
let plugin: CustomApiPlugin;
try {
plugin = JSON.parse(pluginString);
if (!plugin.apiUrl || !plugin.params || !plugin.responseUrlPath) {
console.error('自定义APIJSON配置文件格式不正确。');
return null;
}
} catch (error) {
console.error('自定义API:解析JSON配置文件失败。', error);
return null;
// 2. 判断请求方法,默认为GET
const method = plugin.method?.toUpperCase() === 'POST' ? 'POST' : 'GET';
let response;
// 3. 根据方法发送不同的请求
if (method === 'POST') {
console.log('自定义API:发送 POST 请求到:', plugin.apiUrl, '参数:', finalParams);
response = await axios.post(plugin.apiUrl, finalParams, { timeout });
} else {
// 默认为 GET
const finalUrl = `${plugin.apiUrl}?${new URLSearchParams(finalParams).toString()}`;
console.log('自定义API:发送 GET 请求到:', finalUrl);
response = await axios.get(finalUrl, { timeout });
}
console.log(`自定义API:正在使用插件 [${plugin.name}] 进行解析...`);
// 4. 使用 lodash.get 安全地从响应数据中提取URL
const musicUrl = get(response.data, plugin.responseUrlPath);
try {
// 1. 准备请求参数,替换占位符
const finalParams: Record<string, string> = {};
for (const [key, value] of Object.entries(plugin.params)) {
if (value === '{songId}') {
finalParams[key] = String(id);
} else if (value === '{quality}') {
// 使用 qualityMapping (如果存在) 进行音质翻译,否则直接使用原quality
finalParams[key] = plugin.qualityMapping?.[quality] ?? quality;
} else {
// 固定值参数
finalParams[key] = value;
}
if (musicUrl && typeof musicUrl === 'string') {
console.log('自定义API:成功获取URL');
// 5. 组装成应用所需的标准格式并返回
return {
data: {
data: {
url: musicUrl,
br: parseInt(quality) * 1000,
size: 0,
md5: '',
platform: plugin.name.toLowerCase().replace(/\s/g, ''),
gain: 0
},
params: { id, type: 'song' }
}
// 2. 判断请求方法,默认为GET
const method = plugin.method?.toUpperCase() === 'POST' ? 'POST' : 'GET';
let response;
// 3. 根据方法发送不同的请求
if (method === 'POST') {
console.log('自定义API:发送 POST 请求到:', plugin.apiUrl, '参数:', finalParams);
response = await axios.post(plugin.apiUrl, finalParams, { timeout });
} else { // 默认为 GET
const finalUrl = `${plugin.apiUrl}?${new URLSearchParams(finalParams).toString()}`;
console.log('自定义API:发送 GET 请求到:', finalUrl);
response = await axios.get(finalUrl, { timeout });
}
// 4. 使用 lodash.get 安全地从响应数据中提取URL
const musicUrl = get(response.data, plugin.responseUrlPath);
if (musicUrl && typeof musicUrl === 'string') {
console.log('自定义API:成功获取URL');
// 5. 组装成应用所需的标准格式并返回
return {
data: {
data: {
url: musicUrl,
br: parseInt(quality) * 1000,
size: 0,
md5: '',
platform: plugin.name.toLowerCase().replace(/\s/g, ''),
gain: 0
},
params: { id, type: 'song' }
}
};
} else {
console.error('自定义API:根据路径未能从响应中找到URL:', plugin.responseUrlPath);
return null;
}
} catch (error) {
console.error(`自定义API [${plugin.name}] 执行失败:`, error);
return null;
};
} else {
console.error('自定义API:根据路径未能从响应中找到URL:', plugin.responseUrlPath);
return null;
}
};
} catch (error) {
console.error(`自定义API [${plugin.name}] 执行失败:`, error);
return null;
}
};
@@ -1,12 +1,12 @@
<template>
<n-modal
v-model:show="visible"
preset="dialog"
:title="t('settings.playback.musicSources')"
:positive-text="t('common.confirm')"
:negative-text="t('common.cancel')"
@positive-click="handleConfirm"
@negative-click="handleCancel"
v-model:show="visible"
preset="dialog"
:title="t('settings.playback.musicSources')"
:positive-text="t('common.confirm')"
:negative-text="t('common.cancel')"
@positive-click="handleConfirm"
@negative-click="handleCancel"
>
<n-space vertical>
<p>{{ t('settings.playback.musicSourcesDesc') }}</p>
@@ -59,7 +59,6 @@
<p v-else class="text-sm text-gray-500">尚未导入</p>
</div>
</div>
</n-space>
</n-modal>
</template>
@@ -68,8 +67,8 @@
import { useMessage } from 'naive-ui';
import { ref, watch } from 'vue';
import { useI18n } from 'vue-i18n';
import { useSettingsStore } from '@/store';
import { useSettingsStore } from '@/store';
import { type Platform } from '@/types/music';
// 扩展 Platform 类型以包含 'custom'
@@ -120,45 +119,48 @@ const importPlugin = async () => {
};
// 监听自定义插件内容的变化。如果用户清除了插件,要确保 'custom' 选项被取消勾选
watch(() => settingsStore.setData.customApiPlugin, (newPluginContent) => {
if (!newPluginContent) {
const index = selectedSources.value.indexOf('custom');
if (index > -1) {
selectedSources.value.splice(index, 1);
watch(
() => settingsStore.setData.customApiPlugin,
(newPluginContent) => {
if (!newPluginContent) {
const index = selectedSources.value.indexOf('custom');
if (index > -1) {
selectedSources.value.splice(index, 1);
}
}
}
});
);
// 同步外部show属性变化
watch(
() => props.show,
(newVal) => {
visible.value = newVal;
}
() => props.show,
(newVal) => {
visible.value = newVal;
}
);
// 同步内部visible变化
watch(
() => visible.value,
(newVal) => {
emit('update:show', newVal);
}
() => visible.value,
(newVal) => {
emit('update:show', newVal);
}
);
// 同步外部sources属性变化
watch(
() => props.sources,
(newVal) => {
selectedSources.value = [...newVal];
},
{ deep: true }
() => props.sources,
(newVal) => {
selectedSources.value = [...newVal];
},
{ deep: true }
);
const handleConfirm = () => {
// 确保至少选择一个音源
const defaultPlatforms = ['migu', 'kugou', 'pyncmd', 'bilibili'];
const valuesToEmit =
selectedSources.value.length > 0 ? [...new Set(selectedSources.value)] : defaultPlatforms;
selectedSources.value.length > 0 ? [...new Set(selectedSources.value)] : defaultPlatforms;
emit('update:sources', valuesToEmit);
visible.value = false;
};
@@ -167,4 +169,4 @@ const handleCancel = () => {
selectedSources.value = [...props.sources];
visible.value = false;
};
</script>
</script>
+5 -3
View File
@@ -122,7 +122,9 @@ export const getSongUrl = async (
try {
const songSources = JSON.parse(savedSourceStr);
useCustomApiForSong = songSources.includes('custom');
} catch (e) { /* ignore parsing error */ }
} catch (e) {
console.error('解析歌曲音源设置失败:', e);
}
}
// 如果全局或歌曲专属设置中启用了自定义API,则最优先尝试
@@ -140,11 +142,11 @@ export const getSongUrl = async (
} else {
// 自定义API失败,给出提示,然后继续走默认流程
console.log('自定义API解析失败,将使用默认降级流程...');
message.warning('自定义API解析失败,正在尝试使用内置音源...'); // 给用户一个提示
message.warning(i18n.global.t('player.reparse.customApiFailed')); // 给用户一个提示
}
} catch (error) {
console.error('调用自定义API时发生错误:', error);
message.error('自定义API请求出错,正在尝试使用内置音源...');
message.error(i18n.global.t('player.reparse.customApiError'));
}
}
// 如果自定义API失败或未启用,则执行【原有】的解析流程