mirror of
https://github.com/algerkong/AlgerMusicPlayer.git
synced 2026-05-17 02:07:29 +08:00
Merge pull request #465 from souvenp/feat/add-custom-api
This commit is contained in:
+125
-49
@@ -9,7 +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';
|
||||
|
||||
const { addData, getData, deleteData } = musicDB;
|
||||
|
||||
@@ -30,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;`
|
||||
}
|
||||
});
|
||||
@@ -45,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'
|
||||
}
|
||||
});
|
||||
};
|
||||
@@ -114,7 +119,8 @@ const getBilibiliAudio = async (data: SongResult) => {
|
||||
* @param data 歌曲数据
|
||||
* @returns 解析结果,失败时返回null
|
||||
*/
|
||||
const getGDMusicAudio = async (id: number, data: SongResult) => {
|
||||
const getGDMusicAudio = async (id: number, data: SongResult): Promise<ParsedMusicResult | null> => {
|
||||
// <-- 在这里明确声明返回类型
|
||||
try {
|
||||
const gdResult = await parseFromGDMusic(id, data, '999');
|
||||
if (gdResult) {
|
||||
@@ -146,59 +152,123 @@ 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: '音乐解析功能已禁用' } });
|
||||
}
|
||||
|
||||
// 1. 确定使用的音源列表(自定义或全局)
|
||||
const songId = String(id);
|
||||
const savedSourceStr = localStorage.getItem(`song_source_${songId}`);
|
||||
let musicSources: any[] = [];
|
||||
|
||||
try {
|
||||
if (savedSourceStr) {
|
||||
// 使用自定义音源
|
||||
musicSources = JSON.parse(savedSourceStr);
|
||||
console.log(`使用歌曲 ${id} 自定义音源:`, musicSources);
|
||||
} else {
|
||||
// 使用全局音源设置
|
||||
musicSources = settingStore.setData.enabledMusicSources || [];
|
||||
console.log(`使用全局音源设置:`, musicSources);
|
||||
if (musicSources.length > 0) {
|
||||
return getUnblockMusicAudio(id, data, musicSources);
|
||||
// 如果禁用了音乐解析功能,则直接返回空结果
|
||||
if (!enableMusicUnblock) {
|
||||
return Promise.resolve({ data: { code: 404, message: '音乐解析功能已禁用' } });
|
||||
}
|
||||
|
||||
// 1. 确定使用的音源列表(自定义或全局)
|
||||
const songId = String(id);
|
||||
const savedSourceStr = (() => {
|
||||
try {
|
||||
return localStorage.getItem(`song_source_${songId}`);
|
||||
} catch (e) {
|
||||
console.warn('读取本地存储失败,忽略自定义音源', e);
|
||||
return null;
|
||||
}
|
||||
})();
|
||||
|
||||
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';
|
||||
}
|
||||
|
||||
// 优先级 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);
|
||||
}
|
||||
|
||||
// 优先级 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);
|
||||
}
|
||||
|
||||
// 优先级 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);
|
||||
}
|
||||
|
||||
// 优先级 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('解析音源设置失败,使用全局设置', e);
|
||||
musicSources = settingStore.setData.enabledMusicSources || [];
|
||||
}
|
||||
|
||||
// 2. 按优先级解析
|
||||
|
||||
// 2.1 Bilibili解析(优先级最高)
|
||||
if (musicSources.includes('bilibili')) {
|
||||
return await getBilibiliAudio(data);
|
||||
}
|
||||
|
||||
// 2.2 GD音乐台解析
|
||||
if (musicSources.includes('gdmusic')) {
|
||||
const gdResult = await getGDMusicAudio(id, data);
|
||||
if (gdResult) return gdResult;
|
||||
// GD解析失败,继续下一步
|
||||
console.log('GD音乐台解析失败,尝试使用其他音源');
|
||||
}
|
||||
console.log('musicSources', musicSources);
|
||||
// 2.3 使用unblockMusic解析其他音源
|
||||
if (musicSources.length > 0) {
|
||||
return getUnblockMusicAudio(id, data, musicSources);
|
||||
}
|
||||
} catch (e) {
|
||||
console.error('getParsingMusicUrl 执行异常,将使用后备方案:', e);
|
||||
}
|
||||
|
||||
// 3. 后备方案:使用API请求
|
||||
// 后备方案:使用API请求
|
||||
console.log('无可用音源或不在Electron环境中,使用API请求');
|
||||
return requestMusic.get<any>('/music', { params: { id } });
|
||||
};
|
||||
@@ -208,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', {
|
||||
|
||||
@@ -0,0 +1,107 @@
|
||||
import axios from 'axios';
|
||||
import { get } from 'lodash';
|
||||
|
||||
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;
|
||||
}
|
||||
|
||||
/**
|
||||
* 从用户导入的自定义API JSON配置中解析音乐URL
|
||||
*/
|
||||
export const parseFromCustomApi = async (
|
||||
id: number,
|
||||
_songData: any,
|
||||
quality: string = 'higher',
|
||||
timeout: number = 10000
|
||||
): Promise<ParsedMusicResult | null> => {
|
||||
const settingsStore = useSettingsStore();
|
||||
const pluginString = settingsStore.setData.customApiPlugin;
|
||||
|
||||
if (!pluginString) {
|
||||
return null;
|
||||
}
|
||||
|
||||
let plugin: CustomApiPlugin;
|
||||
try {
|
||||
plugin = JSON.parse(pluginString);
|
||||
if (!plugin.apiUrl || !plugin.params || !plugin.responseUrlPath) {
|
||||
console.error('自定义API:JSON配置文件格式不正确。');
|
||||
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;
|
||||
}
|
||||
}
|
||||
|
||||
// 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;
|
||||
}
|
||||
};
|
||||
@@ -10,56 +10,77 @@
|
||||
>
|
||||
<n-space vertical>
|
||||
<p>{{ t('settings.playback.musicSourcesDesc') }}</p>
|
||||
|
||||
<n-checkbox-group v-model:value="selectedSources">
|
||||
<n-grid :cols="2" :x-gap="12" :y-gap="8">
|
||||
<n-grid-item v-for="source in musicSourceOptions" :key="source.value">
|
||||
<!-- 遍历常规音源 -->
|
||||
<n-grid-item v-for="source in regularMusicSources" :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-tooltip v-if="source.value === 'gdmusic'">
|
||||
<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>
|
||||
</n-checkbox>
|
||||
</n-grid-item>
|
||||
|
||||
<!-- 单独处理自定义API选项 -->
|
||||
<n-grid-item>
|
||||
<n-checkbox value="custom" :disabled="!settingsStore.setData.customApiPlugin">
|
||||
自定义 API
|
||||
<n-tooltip v-if="!settingsStore.setData.customApiPlugin">
|
||||
<template #trigger>
|
||||
<n-icon size="16" class="ml-1 text-gray-400 cursor-help">
|
||||
<i class="ri-question-line"></i>
|
||||
</n-icon>
|
||||
</template>
|
||||
请先导入JSON配置文件才能启用
|
||||
</n-tooltip>
|
||||
</n-checkbox>
|
||||
</n-grid-item>
|
||||
</n-grid>
|
||||
</n-checkbox-group>
|
||||
<div v-if="selectedSources.length === 0" class="text-red-500 text-sm">
|
||||
{{ t('settings.playback.musicSourcesWarning') }}
|
||||
</div>
|
||||
|
||||
<!-- GD音乐台设置 -->
|
||||
<div
|
||||
v-if="selectedSources.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 class="mt-4 border-t pt-4 border-gray-200 dark:border-gray-700"></div>
|
||||
|
||||
<!-- 自定义API导入区域 -->
|
||||
<div>
|
||||
<h3 class="text-base font-medium mb-2">自定义 API 设置</h3>
|
||||
<div class="flex items-center gap-4">
|
||||
<n-button @click="importPlugin" size="small"> 导入 JSON 配置 </n-button>
|
||||
<p v-if="settingsStore.setData.customApiPluginName" class="text-sm">
|
||||
当前: <span class="font-semibold">{{ settingsStore.setData.customApiPluginName }}</span>
|
||||
</p>
|
||||
<p v-else class="text-sm text-gray-500">尚未导入</p>
|
||||
</div>
|
||||
</div>
|
||||
</n-space>
|
||||
</n-modal>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { defineEmits, defineProps, ref, watch } from 'vue';
|
||||
import { useMessage } from 'naive-ui';
|
||||
import { ref, watch } from 'vue';
|
||||
import { useI18n } from 'vue-i18n';
|
||||
|
||||
import { useSettingsStore } from '@/store';
|
||||
import { type Platform } from '@/types/music';
|
||||
|
||||
// 扩展 Platform 类型以包含 'custom'
|
||||
type ExtendedPlatform = Platform | 'custom';
|
||||
|
||||
const props = defineProps({
|
||||
show: {
|
||||
type: Boolean,
|
||||
default: false
|
||||
},
|
||||
sources: {
|
||||
type: Array as () => Platform[],
|
||||
type: Array as () => ExtendedPlatform[],
|
||||
default: () => ['migu', 'kugou', 'pyncmd', 'bilibili']
|
||||
}
|
||||
});
|
||||
@@ -67,10 +88,13 @@ const props = defineProps({
|
||||
const emit = defineEmits(['update:show', 'update:sources']);
|
||||
|
||||
const { t } = useI18n();
|
||||
const settingsStore = useSettingsStore();
|
||||
const message = useMessage();
|
||||
const visible = ref(props.show);
|
||||
const selectedSources = ref<Platform[]>(props.sources);
|
||||
const selectedSources = ref<ExtendedPlatform[]>(props.sources);
|
||||
|
||||
const musicSourceOptions = ref([
|
||||
// 将常规音源和自定义音源分开定义
|
||||
const regularMusicSources = ref([
|
||||
{ label: 'MG', value: 'migu' },
|
||||
{ label: 'KG', value: 'kugou' },
|
||||
{ label: 'pyncmd', value: 'pyncmd' },
|
||||
@@ -78,6 +102,35 @@ const musicSourceOptions = ref([
|
||||
{ label: 'GD音乐台', value: 'gdmusic' }
|
||||
]);
|
||||
|
||||
const importPlugin = async () => {
|
||||
try {
|
||||
const result = await window.api.importCustomApiPlugin();
|
||||
if (result && result.name && result.content) {
|
||||
settingsStore.setCustomApiPlugin(result);
|
||||
message.success(`成功导入音源: ${result.name}`);
|
||||
// 导入成功后,如果用户还没勾选,则自动勾选上
|
||||
if (!selectedSources.value.includes('custom')) {
|
||||
selectedSources.value.push('custom');
|
||||
}
|
||||
}
|
||||
} catch (error: any) {
|
||||
message.error(`导入失败: ${error.message}`);
|
||||
}
|
||||
};
|
||||
|
||||
// 监听自定义插件内容的变化。如果用户清除了插件,要确保 'custom' 选项被取消勾选
|
||||
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,
|
||||
@@ -108,11 +161,9 @@ const handleConfirm = () => {
|
||||
const defaultPlatforms = ['migu', 'kugou', 'pyncmd', 'bilibili'];
|
||||
const valuesToEmit =
|
||||
selectedSources.value.length > 0 ? [...new Set(selectedSources.value)] : defaultPlatforms;
|
||||
|
||||
emit('update:sources', valuesToEmit);
|
||||
visible.value = false;
|
||||
};
|
||||
|
||||
const handleCancel = () => {
|
||||
// 取消时还原为props传入的初始值
|
||||
selectedSources.value = [...props.sources];
|
||||
|
||||
@@ -78,10 +78,14 @@ export const isBilibiliIdMatch = (id1: string | number, id2: string | number): b
|
||||
// 提取公共函数:获取B站视频URL
|
||||
|
||||
export const getSongUrl = async (
|
||||
id: string | number,
|
||||
songData: SongResult,
|
||||
isDownloaded: boolean = false
|
||||
id: string | number,
|
||||
songData: SongResult,
|
||||
isDownloaded: boolean = false
|
||||
) => {
|
||||
const numericId = typeof id === 'string' ? parseInt(id, 10) : id;
|
||||
const settingsStore = useSettingsStore();
|
||||
const { message } = createDiscreteApi(['message']); // 引入 message API 用于提示
|
||||
|
||||
try {
|
||||
if (songData.playMusicUrl) {
|
||||
return songData.playMusicUrl;
|
||||
@@ -92,8 +96,8 @@ export const getSongUrl = async (
|
||||
if (!songData.playMusicUrl && songData.bilibiliData.bvid && songData.bilibiliData.cid) {
|
||||
try {
|
||||
songData.playMusicUrl = await getBilibiliAudioUrl(
|
||||
songData.bilibiliData.bvid,
|
||||
songData.bilibiliData.cid
|
||||
songData.bilibiliData.bvid,
|
||||
songData.bilibiliData.cid
|
||||
);
|
||||
return songData.playMusicUrl;
|
||||
} catch (error) {
|
||||
@@ -104,14 +108,50 @@ export const getSongUrl = async (
|
||||
return songData.playMusicUrl || '';
|
||||
}
|
||||
|
||||
const numericId = typeof id === 'string' ? parseInt(id, 10) : id;
|
||||
|
||||
// 检查是否有自定义音源设置
|
||||
// ==================== 自定义API最优先 ====================
|
||||
// 检查用户是否在全局设置中启用了 'custom' 音源
|
||||
const globalSources = settingsStore.setData.enabledMusicSources || [];
|
||||
const useCustomApiGlobally = globalSources.includes('custom');
|
||||
|
||||
// 检查歌曲是否有专属的 'custom' 音源设置
|
||||
const songId = String(id);
|
||||
const savedSource = localStorage.getItem(`song_source_${songId}`);
|
||||
const savedSourceStr = localStorage.getItem(`song_source_${songId}`);
|
||||
let useCustomApiForSong = false;
|
||||
if (savedSourceStr) {
|
||||
try {
|
||||
const songSources = JSON.parse(savedSourceStr);
|
||||
useCustomApiForSong = songSources.includes('custom');
|
||||
} catch (e) {
|
||||
console.error('解析歌曲音源设置失败:', e);
|
||||
}
|
||||
}
|
||||
|
||||
// 如果全局或歌曲专属设置中启用了自定义API,则最优先尝试
|
||||
if ( (useCustomApiGlobally || useCustomApiForSong) && settingsStore.setData.customApiPlugin) {
|
||||
console.log(`优先级 1: 尝试使用自定义API解析歌曲 ${id}...`);
|
||||
try {
|
||||
// 直接从 api 目录导入 parseFromCustomApi 函数
|
||||
const { parseFromCustomApi } = await import('@/api/parseFromCustomApi');
|
||||
const customResult = await parseFromCustomApi(numericId, cloneDeep(songData), settingsStore.setData.musicQuality || 'higher');
|
||||
|
||||
if (customResult && customResult.data && customResult.data.data && customResult.data.data.url) {
|
||||
console.log('自定义API解析成功!');
|
||||
if (isDownloaded) return customResult.data.data as any;
|
||||
return customResult.data.data.url;
|
||||
} else {
|
||||
// 自定义API失败,给出提示,然后继续走默认流程
|
||||
console.log('自定义API解析失败,将使用默认降级流程...');
|
||||
message.warning(i18n.global.t('player.reparse.customApiFailed')); // 给用户一个提示
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('调用自定义API时发生错误:', error);
|
||||
message.error(i18n.global.t('player.reparse.customApiError'));
|
||||
}
|
||||
}
|
||||
// 如果自定义API失败或未启用,则执行【原有】的解析流程
|
||||
// 如果有自定义音源设置,直接使用getParsingMusicUrl获取URL
|
||||
if (savedSource && songData.source !== 'bilibili') {
|
||||
if (savedSourceStr && songData.source !== 'bilibili') {
|
||||
try {
|
||||
console.log(`使用自定义音源解析歌曲 ID: ${songId}`);
|
||||
const res = await getParsingMusicUrl(numericId, cloneDeep(songData));
|
||||
@@ -129,28 +169,33 @@ export const getSongUrl = async (
|
||||
|
||||
// 正常获取URL流程
|
||||
const { data } = await getMusicUrl(numericId, isDownloaded);
|
||||
let url = '';
|
||||
let songDetail = null;
|
||||
try {
|
||||
if (data.data[0].freeTrialInfo || !data.data[0].url) {
|
||||
if (data && data.data && data.data[0]) {
|
||||
const songDetail = data.data[0];
|
||||
const hasNoUrl = !songDetail.url;
|
||||
const isTrial = !!songDetail.freeTrialInfo;
|
||||
|
||||
if (hasNoUrl || isTrial) {
|
||||
console.log(`官方URL无效 (无URL: ${hasNoUrl}, 试听: ${isTrial}),进入内置备用解析...`);
|
||||
const res = await getParsingMusicUrl(numericId, cloneDeep(songData));
|
||||
url = res.data.data.url;
|
||||
songDetail = res.data.data;
|
||||
} else {
|
||||
songDetail = data.data[0] as any;
|
||||
if (isDownloaded) return res?.data?.data as any;
|
||||
return res?.data?.data?.url || null;
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('error', error);
|
||||
url = data.data[0].url || '';
|
||||
|
||||
console.log('官方API解析成功!');
|
||||
if (isDownloaded) return songDetail as any;
|
||||
return songDetail.url;
|
||||
}
|
||||
if (isDownloaded) {
|
||||
return songDetail;
|
||||
}
|
||||
url = url || data.data[0].url;
|
||||
return url;
|
||||
|
||||
console.log('官方API返回数据结构异常,进入内置备用解析...');
|
||||
const res = await getParsingMusicUrl(numericId, cloneDeep(songData));
|
||||
if (isDownloaded) return res?.data?.data as any;
|
||||
return res?.data?.data?.url || null;
|
||||
|
||||
} catch (error) {
|
||||
console.error('error', error);
|
||||
return null;
|
||||
console.error('官方API请求失败,进入内置备用解析流程:', error);
|
||||
const res = await getParsingMusicUrl(numericId, cloneDeep(songData));
|
||||
if (isDownloaded) return res?.data?.data as any;
|
||||
return res?.data?.data?.url || null;
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
@@ -64,6 +64,17 @@ export const useSettingsStore = defineStore('settings', () => {
|
||||
// 初始化 setData
|
||||
setData.value = getInitialSettings();
|
||||
|
||||
/**
|
||||
* 保存导入的自定义API插件
|
||||
* @param plugin 包含name和content的对象
|
||||
*/
|
||||
const setCustomApiPlugin = (plugin: { name: string; content: string }) => {
|
||||
setSetData({
|
||||
customApiPlugin: plugin.content,
|
||||
customApiPluginName: plugin.name
|
||||
});
|
||||
};
|
||||
|
||||
const toggleTheme = () => {
|
||||
if (setData.value.autoTheme) {
|
||||
// 如果是自动模式,切换到手动模式并设置相反的主题
|
||||
@@ -208,6 +219,7 @@ export const useSettingsStore = defineStore('settings', () => {
|
||||
setLanguage,
|
||||
initializeSettings,
|
||||
initializeTheme,
|
||||
initializeSystemFonts
|
||||
initializeSystemFonts,
|
||||
setCustomApiPlugin,
|
||||
};
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user