mirror of
https://github.com/algerkong/AlgerMusicPlayer.git
synced 2026-04-03 14:20:50 +08:00
feat: 优化音源解析
This commit is contained in:
4
DEV.md
4
DEV.md
@@ -15,7 +15,7 @@
|
|||||||
- **国际化**:vue-i18n
|
- **国际化**:vue-i18n
|
||||||
- **HTTP 客户端**:axios
|
- **HTTP 客户端**:axios
|
||||||
- **本地存储**:electron-store localstorage
|
- **本地存储**:electron-store localstorage
|
||||||
- **网易云音乐 API**:netease-cloud-music-api
|
- **音乐 API**:netease-cloud-music-api
|
||||||
- **音乐解锁**:@unblockneteasemusic/server
|
- **音乐解锁**:@unblockneteasemusic/server
|
||||||
|
|
||||||
### 项目结构
|
### 项目结构
|
||||||
@@ -93,7 +93,7 @@ AlgerMusicPlayer/
|
|||||||
|
|
||||||
- **index.ts**: 应用主入口,负责创建窗口和应用生命周期管理
|
- **index.ts**: 应用主入口,负责创建窗口和应用生命周期管理
|
||||||
- **lyric.ts**: 歌词解析和处理
|
- **lyric.ts**: 歌词解析和处理
|
||||||
- **unblockMusic.ts**: 网易云音乐解锁功能
|
- **unblockMusic.ts**: 音乐解锁功能
|
||||||
- **server.ts**: 本地服务器
|
- **server.ts**: 本地服务器
|
||||||
|
|
||||||
#### 预加载脚本 (src/preload)
|
#### 预加载脚本 (src/preload)
|
||||||
|
|||||||
@@ -27,7 +27,7 @@
|
|||||||
主要功能如下
|
主要功能如下
|
||||||
|
|
||||||
- 🎵 音乐推荐
|
- 🎵 音乐推荐
|
||||||
- 🔐 网易云账号登录与同步
|
- 🔐 账号登录与同步
|
||||||
- 📝 功能
|
- 📝 功能
|
||||||
- 播放历史记录
|
- 播放历史记录
|
||||||
- 歌曲收藏管理
|
- 歌曲收藏管理
|
||||||
|
|||||||
@@ -105,9 +105,13 @@ export default {
|
|||||||
sourceLabels: {
|
sourceLabels: {
|
||||||
migu: 'Migu',
|
migu: 'Migu',
|
||||||
kugou: 'Kugou',
|
kugou: 'Kugou',
|
||||||
|
kuwo: 'Kuwo',
|
||||||
pyncmd: 'NetEase (Built-in)',
|
pyncmd: 'NetEase (Built-in)',
|
||||||
|
qq: 'QQ Music',
|
||||||
|
joox: 'JOOX',
|
||||||
bilibili: 'Bilibili',
|
bilibili: 'Bilibili',
|
||||||
gdmusic: 'GD Music',
|
gdmusic: 'GD Music',
|
||||||
|
lxMusic: 'LX Music',
|
||||||
custom: 'Custom API'
|
custom: 'Custom API'
|
||||||
},
|
},
|
||||||
|
|
||||||
|
|||||||
@@ -102,9 +102,13 @@ export default {
|
|||||||
sourceLabels: {
|
sourceLabels: {
|
||||||
migu: 'Migu',
|
migu: 'Migu',
|
||||||
kugou: 'Kugou',
|
kugou: 'Kugou',
|
||||||
|
kuwo: 'Kuwo',
|
||||||
pyncmd: 'NetEase (内蔵)',
|
pyncmd: 'NetEase (内蔵)',
|
||||||
|
qq: 'QQ Music',
|
||||||
|
joox: 'JOOX',
|
||||||
bilibili: 'Bilibili',
|
bilibili: 'Bilibili',
|
||||||
gdmusic: 'GD 音楽台',
|
gdmusic: 'GD 音楽台',
|
||||||
|
lxMusic: 'LX Music',
|
||||||
custom: 'カスタム API'
|
custom: 'カスタム API'
|
||||||
},
|
},
|
||||||
customApi: {
|
customApi: {
|
||||||
|
|||||||
@@ -103,9 +103,13 @@ export default {
|
|||||||
sourceLabels: {
|
sourceLabels: {
|
||||||
migu: 'Migu',
|
migu: 'Migu',
|
||||||
kugou: 'Kugou',
|
kugou: 'Kugou',
|
||||||
|
kuwo: 'Kuwo',
|
||||||
pyncmd: 'NetEase (내장)',
|
pyncmd: 'NetEase (내장)',
|
||||||
|
qq: 'QQ Music',
|
||||||
|
joox: 'JOOX',
|
||||||
bilibili: 'Bilibili',
|
bilibili: 'Bilibili',
|
||||||
gdmusic: 'GD Music',
|
gdmusic: 'GD Music',
|
||||||
|
lxMusic: 'LX Music',
|
||||||
custom: '사용자 지정 API'
|
custom: '사용자 지정 API'
|
||||||
},
|
},
|
||||||
|
|
||||||
|
|||||||
@@ -5,14 +5,14 @@ export default {
|
|||||||
cookie: 'Cookie登录',
|
cookie: 'Cookie登录',
|
||||||
uid: 'UID登录'
|
uid: 'UID登录'
|
||||||
},
|
},
|
||||||
qrTip: '使用网易云APP扫码登录',
|
qrTip: '使用APP扫码登录',
|
||||||
phoneTip: '使用网易云账号登录',
|
phoneTip: '使用账号登录',
|
||||||
tokenTip: '输入有效的网易云音乐Cookie即可登录',
|
tokenTip: '输入有效的音乐Cookie即可登录',
|
||||||
uidTip: '输入用户ID快速登录',
|
uidTip: '输入用户ID快速登录',
|
||||||
placeholder: {
|
placeholder: {
|
||||||
phone: '手机号',
|
phone: '手机号',
|
||||||
password: '密码',
|
password: '密码',
|
||||||
cookie: '请输入网易云音乐Cookie(token)',
|
cookie: '请输入音乐Cookie(token)',
|
||||||
uid: '请输入用户ID(UID)'
|
uid: '请输入用户ID(UID)'
|
||||||
},
|
},
|
||||||
button: {
|
button: {
|
||||||
@@ -45,7 +45,7 @@ export default {
|
|||||||
phoneLoginFailed: '手机号登录失败,请检查手机号和密码是否正确',
|
phoneLoginFailed: '手机号登录失败,请检查手机号和密码是否正确',
|
||||||
autoGetCookieSuccess: '自动获取Cookie成功',
|
autoGetCookieSuccess: '自动获取Cookie成功',
|
||||||
autoGetCookieFailed: '自动获取Cookie失败',
|
autoGetCookieFailed: '自动获取Cookie失败',
|
||||||
autoGetCookieTip: '将打开网易云音乐登录页面,请完成登录后关闭窗口',
|
autoGetCookieTip: '将打开音乐登录页面,请完成登录后关闭窗口',
|
||||||
qrCheckFailed: '检查二维码状态失败,请刷新重试',
|
qrCheckFailed: '检查二维码状态失败,请刷新重试',
|
||||||
qrLoading: '正在加载二维码...',
|
qrLoading: '正在加载二维码...',
|
||||||
qrExpired: '二维码已过期,请点击刷新',
|
qrExpired: '二维码已过期,请点击刷新',
|
||||||
@@ -57,6 +57,6 @@ export default {
|
|||||||
qrConfirmed: '登录成功,正在跳转...',
|
qrConfirmed: '登录成功,正在跳转...',
|
||||||
qrGenerating: '正在生成二维码...'
|
qrGenerating: '正在生成二维码...'
|
||||||
},
|
},
|
||||||
qrTitle: '扫码登录网易云音乐',
|
qrTitle: '扫码登录',
|
||||||
uidWarning: '注意:UID登录仅用于查看用户公开信息,无法访问需要登录权限的功能'
|
uidWarning: '注意:UID登录仅用于查看用户公开信息,无法访问需要登录权限的功能'
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -20,7 +20,7 @@ export default {
|
|||||||
language: '语言设置',
|
language: '语言设置',
|
||||||
languageDesc: '切换显示语言',
|
languageDesc: '切换显示语言',
|
||||||
tokenManagement: 'Cookie管理',
|
tokenManagement: 'Cookie管理',
|
||||||
tokenManagementDesc: '管理网易云音乐登录Cookie',
|
tokenManagementDesc: '管理音乐登录Cookie',
|
||||||
tokenStatus: '当前Cookie状态',
|
tokenStatus: '当前Cookie状态',
|
||||||
tokenSet: '已设置',
|
tokenSet: '已设置',
|
||||||
tokenNotSet: '未设置',
|
tokenNotSet: '未设置',
|
||||||
@@ -61,7 +61,7 @@ export default {
|
|||||||
},
|
},
|
||||||
playback: {
|
playback: {
|
||||||
quality: '音质设置',
|
quality: '音质设置',
|
||||||
qualityDesc: '选择音乐播放音质(网易云VIP)',
|
qualityDesc: '选择音乐播放音质(不确保有效)',
|
||||||
qualityOptions: {
|
qualityOptions: {
|
||||||
standard: '标准',
|
standard: '标准',
|
||||||
higher: '较高',
|
higher: '较高',
|
||||||
@@ -99,11 +99,15 @@ export default {
|
|||||||
|
|
||||||
// 音源标签
|
// 音源标签
|
||||||
sourceLabels: {
|
sourceLabels: {
|
||||||
migu: '咪咕音乐',
|
migu: 'migu',
|
||||||
kugou: '酷狗音乐',
|
kugou: 'kugou',
|
||||||
pyncmd: '网易云(内置)',
|
kuwo: 'kuwo',
|
||||||
|
pyncmd: 'pyncmd',
|
||||||
|
qq: 'qq',
|
||||||
|
joox: 'JOOX',
|
||||||
bilibili: 'Bilibili',
|
bilibili: 'Bilibili',
|
||||||
gdmusic: 'GD音乐台',
|
gdmusic: 'gdmusic',
|
||||||
|
lxMusic: 'lxMusic',
|
||||||
custom: '自定义 API'
|
custom: '自定义 API'
|
||||||
},
|
},
|
||||||
|
|
||||||
@@ -414,7 +418,7 @@ export default {
|
|||||||
},
|
},
|
||||||
cookie: {
|
cookie: {
|
||||||
title: 'Cookie设置',
|
title: 'Cookie设置',
|
||||||
description: '请输入网易云音乐的Cookie:',
|
description: '请输入音乐的Cookie:',
|
||||||
placeholder: '请粘贴完整的Cookie...',
|
placeholder: '请粘贴完整的Cookie...',
|
||||||
help: {
|
help: {
|
||||||
format: 'Cookie通常以 "MUSIC_U=" 开头',
|
format: 'Cookie通常以 "MUSIC_U=" 开头',
|
||||||
|
|||||||
@@ -98,14 +98,17 @@ export default {
|
|||||||
|
|
||||||
// 音源標籤
|
// 音源標籤
|
||||||
sourceLabels: {
|
sourceLabels: {
|
||||||
migu: '咪咕音樂',
|
migu: 'migu',
|
||||||
kugou: '酷狗音樂',
|
kugou: 'kugou',
|
||||||
pyncmd: '網易雲(內建)',
|
kuwo: 'kuwo',
|
||||||
|
pyncmd: 'pyncmd',
|
||||||
|
qq: 'qq',
|
||||||
|
joox: 'JOOX',
|
||||||
bilibili: 'Bilibili',
|
bilibili: 'Bilibili',
|
||||||
gdmusic: 'GD音樂台',
|
gdmusic: 'gdmusic',
|
||||||
|
lxMusic: 'lxMusic',
|
||||||
custom: '自訂 API'
|
custom: '自訂 API'
|
||||||
},
|
},
|
||||||
|
|
||||||
customApi: {
|
customApi: {
|
||||||
sectionTitle: '自訂 API 設定',
|
sectionTitle: '自訂 API 設定',
|
||||||
importConfig: '匯入 JSON 設定',
|
importConfig: '匯入 JSON 設定',
|
||||||
|
|||||||
@@ -42,7 +42,7 @@ const openLoginWindow = async (mainWin: BrowserWindow) => {
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
// 打开网易云登录页面
|
// 打开登录页面
|
||||||
loginWindow.loadURL(loginUrl);
|
loginWindow.loadURL(loginUrl);
|
||||||
|
|
||||||
// 阻止新窗口创建
|
// 阻止新窗口创建
|
||||||
|
|||||||
@@ -5,7 +5,7 @@ import { ipcMain } from 'electron';
|
|||||||
* 初始化其他杂项 API(如搜索建议等)
|
* 初始化其他杂项 API(如搜索建议等)
|
||||||
*/
|
*/
|
||||||
export function initializeOtherApi() {
|
export function initializeOtherApi() {
|
||||||
// 搜索建议(从酷狗获取)
|
// 搜索建议
|
||||||
ipcMain.handle('get-search-suggestions', async (_, keyword: string) => {
|
ipcMain.handle('get-search-suggestions', async (_, keyword: string) => {
|
||||||
if (!keyword || !keyword.trim()) {
|
if (!keyword || !keyword.trim()) {
|
||||||
return [];
|
return [];
|
||||||
|
|||||||
@@ -108,7 +108,7 @@ const convertToLxMusicInfo = (songResult: SongResult): LxMusicInfo => {
|
|||||||
singer: artistName,
|
singer: artistName,
|
||||||
album: albumName,
|
album: albumName,
|
||||||
albumId,
|
albumId,
|
||||||
source: 'wy', // 默认使用网易云作为源,因为我们的数据来自网易云
|
source: 'wy',
|
||||||
interval,
|
interval,
|
||||||
img: songResult.picUrl || songResult.al?.picUrl || ''
|
img: songResult.picUrl || songResult.al?.picUrl || ''
|
||||||
};
|
};
|
||||||
@@ -116,13 +116,11 @@ const convertToLxMusicInfo = (songResult: SongResult): LxMusicInfo => {
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* 获取最佳匹配的落雪音源
|
* 获取最佳匹配的落雪音源
|
||||||
* 因为我们的数据来自网易云,优先尝试 wy 音源
|
|
||||||
*/
|
*/
|
||||||
const getBestMatchingSource = (
|
const getBestMatchingSource = (
|
||||||
availableSources: LxSourceKey[],
|
availableSources: LxSourceKey[],
|
||||||
_songSource?: string
|
_songSource?: string
|
||||||
): LxSourceKey | null => {
|
): LxSourceKey | null => {
|
||||||
// 优先级顺序:网易云 > 酷我 > 咪咕 > 酷狗 > QQ音乐
|
|
||||||
const priority: LxSourceKey[] = ['wy', 'kw', 'mg', 'kg', 'tx'];
|
const priority: LxSourceKey[] = ['wy', 'kw', 'mg', 'kg', 'tx'];
|
||||||
|
|
||||||
for (const source of priority) {
|
for (const source of priority) {
|
||||||
@@ -196,7 +194,9 @@ export class LxMusicStrategy {
|
|||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
console.log(`[LxMusicStrategy] 使用激活的音源: ${activeScript.name} (ID: ${activeScript.id})`);
|
console.log(
|
||||||
|
`[LxMusicStrategy] 使用激活的音源: ${activeScript.name} (ID: ${activeScript.id})`
|
||||||
|
);
|
||||||
|
|
||||||
// 获取或初始化执行器
|
// 获取或初始化执行器
|
||||||
let runner = getLxMusicRunner();
|
let runner = getLxMusicRunner();
|
||||||
|
|||||||
@@ -26,7 +26,6 @@ export const getMusicUrl = async (id: number, isDownloaded: boolean = false) =>
|
|||||||
id,
|
id,
|
||||||
level: settingStore.setData.musicQuality || 'higher',
|
level: settingStore.setData.musicQuality || 'higher',
|
||||||
encodeType: settingStore.setData.musicQuality == 'lossless' ? 'aac' : 'flac',
|
encodeType: settingStore.setData.musicQuality == 'lossless' ? 'aac' : 'flac',
|
||||||
// level为lossless时,encodeType=flac时网易云会返回hires音质,encodeType=aac时网易云会返回lossless音质
|
|
||||||
cookie: `${localStorage.getItem('token')} os=pc;`
|
cookie: `${localStorage.getItem('token')} os=pc;`
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -25,7 +25,7 @@ interface KugouSuggestionResponse {
|
|||||||
data: Suggestion[];
|
data: Suggestion[];
|
||||||
}
|
}
|
||||||
|
|
||||||
// 网易云搜索建议返回的数据结构(部分字段)
|
// 搜索建议返回的数据结构(部分字段)
|
||||||
interface NeteaseSuggestResult {
|
interface NeteaseSuggestResult {
|
||||||
result?: {
|
result?: {
|
||||||
songs?: Array<{ name: string }>;
|
songs?: Array<{ name: string }>;
|
||||||
@@ -36,7 +36,7 @@ interface NeteaseSuggestResult {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 从酷狗获取搜索建议
|
* 获取搜索建议
|
||||||
* @param keyword 搜索关键词
|
* @param keyword 搜索关键词
|
||||||
*/
|
*/
|
||||||
export const getSearchSuggestions = async (keyword: string) => {
|
export const getSearchSuggestions = async (keyword: string) => {
|
||||||
@@ -54,7 +54,7 @@ export const getSearchSuggestions = async (keyword: string) => {
|
|||||||
console.log('[API] Running in Electron, using IPC proxy.');
|
console.log('[API] Running in Electron, using IPC proxy.');
|
||||||
responseData = await window.api.getSearchSuggestions(keyword);
|
responseData = await window.api.getSearchSuggestions(keyword);
|
||||||
} else {
|
} else {
|
||||||
// 非 Electron 环境下,使用网易云接口
|
// 非 Electron 环境下,使用接口
|
||||||
const res = await request.get<NeteaseSuggestResult>('/search/suggest', {
|
const res = await request.get<NeteaseSuggestResult>('/search/suggest', {
|
||||||
params: { keywords: keyword }
|
params: { keywords: keyword }
|
||||||
});
|
});
|
||||||
@@ -67,7 +67,7 @@ export const getSearchSuggestions = async (keyword: string) => {
|
|||||||
|
|
||||||
// 去重并截取前10个
|
// 去重并截取前10个
|
||||||
const unique = Array.from(new Set(names)).slice(0, 10);
|
const unique = Array.from(new Set(names)).slice(0, 10);
|
||||||
console.log('[API] getSearchSuggestions: 网易云建议解析成功:', unique);
|
console.log('[API] getSearchSuggestions: 解析成功:', unique);
|
||||||
return unique;
|
return unique;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -22,37 +22,58 @@
|
|||||||
<div class="reparse-popover bg-light-100 dark:bg-dark-100 p-4 rounded-xl max-w-60">
|
<div class="reparse-popover bg-light-100 dark:bg-dark-100 p-4 rounded-xl max-w-60">
|
||||||
<div class="text-base font-medium mb-2">{{ t('player.reparse.title') }}</div>
|
<div class="text-base font-medium mb-2">{{ t('player.reparse.title') }}</div>
|
||||||
<div class="text-sm opacity-70 mb-3">{{ t('player.reparse.desc') }}</div>
|
<div class="text-sm opacity-70 mb-3">{{ t('player.reparse.desc') }}</div>
|
||||||
<div class="mb-3">
|
<div class="mb-3 max-h-80 overflow-y-auto">
|
||||||
<div class="flex flex-col space-y-2">
|
<div class="flex flex-col space-y-2">
|
||||||
<div
|
<template v-for="(group, groupIndex) in groupedSources" :key="group.key">
|
||||||
v-for="source in musicSourceOptions"
|
<!-- 分组分隔线 -->
|
||||||
:key="source.value"
|
|
||||||
class="source-button flex items-center p-2 rounded-lg cursor-pointer transition-all duration-200 bg-light-200 dark:bg-dark-200 hover:bg-light-300 dark:hover:bg-dark-300"
|
|
||||||
:class="{
|
|
||||||
'bg-green-50 dark:bg-green-900/20 text-green-500': isCurrentSource(source.value),
|
|
||||||
'opacity-50 cursor-not-allowed': isReparsing
|
|
||||||
}"
|
|
||||||
@click="directReparseMusic(source.value)"
|
|
||||||
>
|
|
||||||
<div class="flex items-center justify-center w-6 h-6 mr-3 text-lg">
|
|
||||||
<i :class="getSourceIcon(source.value)"></i>
|
|
||||||
</div>
|
|
||||||
<div class="flex-1 text-sm whitespace-nowrap overflow-hidden text-ellipsis">
|
|
||||||
{{ source.label }}
|
|
||||||
</div>
|
|
||||||
<div
|
<div
|
||||||
v-if="isReparsing && currentReparsingSource === source.value"
|
v-if="groupIndex > 0"
|
||||||
class="w-5 h-5 flex items-center justify-center"
|
class="border-t border-gray-200 dark:border-gray-700 my-1"
|
||||||
>
|
></div>
|
||||||
<i class="ri-loader-4-line animate-spin"></i>
|
|
||||||
</div>
|
|
||||||
<div
|
<div
|
||||||
v-else-if="isCurrentSource(source.value)"
|
v-for="source in group.sources"
|
||||||
class="w-5 h-5 flex items-center justify-center"
|
:key="source.id"
|
||||||
|
class="source-button flex items-center p-2 rounded-lg transition-all duration-200"
|
||||||
|
:class="[
|
||||||
|
source.available
|
||||||
|
? 'cursor-pointer bg-light-200 dark:bg-dark-200 hover:bg-light-300 dark:hover:bg-dark-300'
|
||||||
|
: 'opacity-40 cursor-not-allowed bg-light-200 dark:bg-dark-200',
|
||||||
|
{
|
||||||
|
'bg-green-50 dark:bg-green-900/20 text-green-500': isCurrentSource(source.id),
|
||||||
|
'opacity-50 cursor-not-allowed': isReparsing && source.available
|
||||||
|
}
|
||||||
|
]"
|
||||||
|
@click="source.available && handleSourceClick(source)"
|
||||||
>
|
>
|
||||||
<i class="ri-check-line"></i>
|
<div
|
||||||
|
class="flex items-center justify-center w-6 h-6 mr-3 text-lg"
|
||||||
|
:style="{ color: source.color }"
|
||||||
|
>
|
||||||
|
<i :class="source.icon"></i>
|
||||||
|
</div>
|
||||||
|
<div class="flex-1 text-sm whitespace-nowrap overflow-hidden text-ellipsis">
|
||||||
|
<span>{{ source.label }}</span>
|
||||||
|
<n-tooltip v-if="!source.available && source.configHint" trigger="hover">
|
||||||
|
<template #trigger>
|
||||||
|
<i class="ri-information-line text-xs ml-1 opacity-60"></i>
|
||||||
|
</template>
|
||||||
|
{{ t(source.configHint) }}
|
||||||
|
</n-tooltip>
|
||||||
|
</div>
|
||||||
|
<div
|
||||||
|
v-if="isReparsing && currentReparsingId === source.id"
|
||||||
|
class="w-5 h-5 flex items-center justify-center"
|
||||||
|
>
|
||||||
|
<i class="ri-loader-4-line animate-spin"></i>
|
||||||
|
</div>
|
||||||
|
<div
|
||||||
|
v-else-if="isCurrentSource(source.id)"
|
||||||
|
class="w-5 h-5 flex items-center justify-center"
|
||||||
|
>
|
||||||
|
<i class="ri-check-line"></i>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</template>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<!-- 清除自定义音源 -->
|
<!-- 清除自定义音源 -->
|
||||||
@@ -78,50 +99,101 @@ import { useI18n } from 'vue-i18n';
|
|||||||
|
|
||||||
import { CacheManager } from '@/api/musicParser';
|
import { CacheManager } from '@/api/musicParser';
|
||||||
import { playMusic } from '@/hooks/MusicHook';
|
import { playMusic } from '@/hooks/MusicHook';
|
||||||
|
import { initLxMusicRunner, setLxMusicRunner } from '@/services/LxMusicSourceRunner';
|
||||||
import { SongSourceConfigManager } from '@/services/SongSourceConfigManager';
|
import { SongSourceConfigManager } from '@/services/SongSourceConfigManager';
|
||||||
|
import { useSettingsStore } from '@/store';
|
||||||
import { usePlayerStore } from '@/store/modules/player';
|
import { usePlayerStore } from '@/store/modules/player';
|
||||||
|
import type { LxMusicScriptConfig } from '@/types/lxMusic';
|
||||||
import type { Platform } from '@/types/music';
|
import type { Platform } from '@/types/music';
|
||||||
|
import { type MusicSourceGroup, useMusicSources } from '@/utils/musicSourceConfig';
|
||||||
|
|
||||||
|
type ReparseSourceItem = {
|
||||||
|
id: string;
|
||||||
|
platform: Platform;
|
||||||
|
label: string;
|
||||||
|
icon: string;
|
||||||
|
color: string;
|
||||||
|
group: MusicSourceGroup;
|
||||||
|
available: boolean;
|
||||||
|
configHint?: string;
|
||||||
|
lxScriptId?: string;
|
||||||
|
};
|
||||||
|
|
||||||
const playerStore = usePlayerStore();
|
const playerStore = usePlayerStore();
|
||||||
|
const settingsStore = useSettingsStore();
|
||||||
const { t } = useI18n();
|
const { t } = useI18n();
|
||||||
const message = useMessage();
|
const message = useMessage();
|
||||||
|
const { allSources } = useMusicSources();
|
||||||
|
|
||||||
// 音源重新解析状态
|
// 音源重新解析状态
|
||||||
const isReparsing = ref(false);
|
const isReparsing = ref(false);
|
||||||
const currentReparsingSource = ref<Platform | null>(null);
|
const currentReparsingId = ref<string | null>(null);
|
||||||
|
|
||||||
// 实际存储选中音源的值
|
// 当前选中的音源条目 id(唯一标识,区分不同 lxMusic 脚本)
|
||||||
const selectedSourcesValue = ref<Platform[]>([]);
|
const selectedSourceId = ref<string | null>(null);
|
||||||
|
|
||||||
const isReparse = computed(() => selectedSourcesValue.value.length > 0);
|
const isReparse = computed(() => selectedSourceId.value !== null);
|
||||||
|
|
||||||
// 可选音源列表
|
// 构建重解析音源列表:将 lxMusic 展开为每个导入的脚本
|
||||||
const musicSourceOptions = ref([
|
const reparseSourceList = computed<ReparseSourceItem[]>(() => {
|
||||||
{ label: 'MiGu', value: 'migu' as Platform },
|
const result: ReparseSourceItem[] = [];
|
||||||
{ label: 'KuGou', value: 'kugou' as Platform },
|
for (const source of allSources.value) {
|
||||||
{ label: 'pyncmd', value: 'pyncmd' as Platform },
|
if (source.key === 'lxMusic') {
|
||||||
{ label: 'GdMuisc', value: 'gdmusic' as Platform }
|
const scripts: LxMusicScriptConfig[] = settingsStore.setData.lxMusicScripts || [];
|
||||||
]);
|
for (const script of scripts) {
|
||||||
|
result.push({
|
||||||
|
id: `lxMusic:${script.id}`,
|
||||||
|
platform: 'lxMusic',
|
||||||
|
label: script.name,
|
||||||
|
icon: source.icon,
|
||||||
|
color: source.color,
|
||||||
|
group: source.group,
|
||||||
|
available: true,
|
||||||
|
lxScriptId: script.id
|
||||||
|
});
|
||||||
|
}
|
||||||
|
// 没有导入任何脚本时显示占位
|
||||||
|
if (scripts.length === 0) {
|
||||||
|
result.push({
|
||||||
|
id: 'lxMusic',
|
||||||
|
platform: 'lxMusic',
|
||||||
|
label: 'lxMusic',
|
||||||
|
icon: source.icon,
|
||||||
|
color: source.color,
|
||||||
|
group: source.group,
|
||||||
|
available: false,
|
||||||
|
configHint: 'settings.playback.lxMusic.scripts.notConfigured'
|
||||||
|
});
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
result.push({
|
||||||
|
id: source.key,
|
||||||
|
platform: source.key,
|
||||||
|
label: source.key,
|
||||||
|
icon: source.icon,
|
||||||
|
color: source.color,
|
||||||
|
group: source.group,
|
||||||
|
available: source.available,
|
||||||
|
configHint: source.configHint
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return result;
|
||||||
|
});
|
||||||
|
|
||||||
// 检查音源是否被选中
|
// 按分组排列音源
|
||||||
const isCurrentSource = (source: Platform) => {
|
const GROUP_ORDER: MusicSourceGroup[] = ['unblock', 'extended', 'plugin'];
|
||||||
return selectedSourcesValue.value.includes(source);
|
|
||||||
};
|
|
||||||
|
|
||||||
// 获取音源图标
|
const groupedSources = computed(() => {
|
||||||
const getSourceIcon = (source: Platform) => {
|
return GROUP_ORDER.map((groupKey) => ({
|
||||||
const iconMap: Record<Platform, string> = {
|
key: groupKey,
|
||||||
migu: 'ri-music-2-fill',
|
sources: reparseSourceList.value.filter((s) => s.group === groupKey)
|
||||||
kugou: 'ri-music-fill',
|
})).filter((g) => g.sources.length > 0);
|
||||||
qq: 'ri-qq-fill',
|
});
|
||||||
joox: 'ri-disc-fill',
|
|
||||||
pyncmd: 'ri-netease-cloud-music-fill',
|
|
||||||
gdmusic: 'ri-google-fill',
|
|
||||||
kuwo: 'ri-music-fill',
|
|
||||||
lxMusic: 'ri-leaf-fill'
|
|
||||||
};
|
|
||||||
|
|
||||||
return iconMap[source] || 'ri-music-2-fill';
|
// 检查音源条目是否被选中
|
||||||
|
const isCurrentSource = (sourceId: string) => {
|
||||||
|
return selectedSourceId.value === sourceId;
|
||||||
};
|
};
|
||||||
|
|
||||||
// 初始化选中的音源
|
// 初始化选中的音源
|
||||||
@@ -129,40 +201,59 @@ const initSelectedSources = () => {
|
|||||||
const songId = playMusic.value.id;
|
const songId = playMusic.value.id;
|
||||||
const config = SongSourceConfigManager.getConfig(songId);
|
const config = SongSourceConfigManager.getConfig(songId);
|
||||||
|
|
||||||
if (config) {
|
if (config && config.sources.length > 0) {
|
||||||
selectedSourcesValue.value = config.sources;
|
const platform = config.sources[0];
|
||||||
|
if (platform === 'lxMusic') {
|
||||||
|
// lxMusic 需要结合当前激活的脚本 id 来定位
|
||||||
|
const activeId = settingsStore.setData.activeLxMusicApiId;
|
||||||
|
selectedSourceId.value = activeId ? `lxMusic:${activeId}` : null;
|
||||||
|
} else {
|
||||||
|
selectedSourceId.value = platform;
|
||||||
|
}
|
||||||
} else {
|
} else {
|
||||||
selectedSourcesValue.value = [];
|
selectedSourceId.value = null;
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
// 清除自定义音源
|
// 清除自定义音源
|
||||||
const clearCustomSource = () => {
|
const clearCustomSource = () => {
|
||||||
SongSourceConfigManager.clearConfig(playMusic.value.id);
|
SongSourceConfigManager.clearConfig(playMusic.value.id);
|
||||||
selectedSourcesValue.value = [];
|
selectedSourceId.value = null;
|
||||||
};
|
};
|
||||||
|
|
||||||
// 直接重新解析当前歌曲
|
// 点击音源条目
|
||||||
const directReparseMusic = async (source: Platform) => {
|
const handleSourceClick = async (source: ReparseSourceItem) => {
|
||||||
if (isReparsing.value) {
|
if (source.lxScriptId) {
|
||||||
return;
|
await reparseWithLxScript(source);
|
||||||
|
} else {
|
||||||
|
await directReparseMusic(source);
|
||||||
}
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// 使用指定 lxMusic 脚本重新解析
|
||||||
|
const reparseWithLxScript = async (source: ReparseSourceItem) => {
|
||||||
|
if (isReparsing.value || !source.lxScriptId) return;
|
||||||
|
|
||||||
|
const scripts: LxMusicScriptConfig[] = settingsStore.setData.lxMusicScripts || [];
|
||||||
|
const script = scripts.find((s) => s.id === source.lxScriptId);
|
||||||
|
if (!script) return;
|
||||||
|
|
||||||
try {
|
try {
|
||||||
isReparsing.value = true;
|
isReparsing.value = true;
|
||||||
currentReparsingSource.value = source;
|
currentReparsingId.value = source.id;
|
||||||
|
|
||||||
|
// 激活该脚本的 runner
|
||||||
|
setLxMusicRunner(null);
|
||||||
|
await initLxMusicRunner(script.script);
|
||||||
|
settingsStore.setSetData({ activeLxMusicApiId: script.id });
|
||||||
|
|
||||||
const songId = Number(playMusic.value.id);
|
const songId = Number(playMusic.value.id);
|
||||||
|
|
||||||
await CacheManager.clearMusicCache(songId);
|
await CacheManager.clearMusicCache(songId);
|
||||||
|
|
||||||
// 更新选中的音源值为当前点击的音源
|
selectedSourceId.value = source.id;
|
||||||
selectedSourcesValue.value = [source];
|
SongSourceConfigManager.setConfig(songId, ['lxMusic'], 'manual');
|
||||||
|
|
||||||
// 使用 SongSourceConfigManager 保存配置(手动选择)
|
const success = await playerStore.reparseCurrentSong('lxMusic', false);
|
||||||
SongSourceConfigManager.setConfig(songId, [source], 'manual');
|
|
||||||
|
|
||||||
const success = await playerStore.reparseCurrentSong(source, false);
|
|
||||||
|
|
||||||
if (success) {
|
if (success) {
|
||||||
message.success(t('player.reparse.success'));
|
message.success(t('player.reparse.success'));
|
||||||
@@ -174,7 +265,37 @@ const directReparseMusic = async (source: Platform) => {
|
|||||||
message.error(t('player.reparse.failed'));
|
message.error(t('player.reparse.failed'));
|
||||||
} finally {
|
} finally {
|
||||||
isReparsing.value = false;
|
isReparsing.value = false;
|
||||||
currentReparsingSource.value = null;
|
currentReparsingId.value = null;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// 直接重新解析当前歌曲(非 lxMusic)
|
||||||
|
const directReparseMusic = async (source: ReparseSourceItem) => {
|
||||||
|
if (isReparsing.value) return;
|
||||||
|
|
||||||
|
try {
|
||||||
|
isReparsing.value = true;
|
||||||
|
currentReparsingId.value = source.id;
|
||||||
|
|
||||||
|
const songId = Number(playMusic.value.id);
|
||||||
|
await CacheManager.clearMusicCache(songId);
|
||||||
|
|
||||||
|
selectedSourceId.value = source.id;
|
||||||
|
SongSourceConfigManager.setConfig(songId, [source.platform], 'manual');
|
||||||
|
|
||||||
|
const success = await playerStore.reparseCurrentSong(source.platform, false);
|
||||||
|
|
||||||
|
if (success) {
|
||||||
|
message.success(t('player.reparse.success'));
|
||||||
|
} else {
|
||||||
|
message.error(t('player.reparse.failed'));
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error('解析失败:', error);
|
||||||
|
message.error(t('player.reparse.failed'));
|
||||||
|
} finally {
|
||||||
|
isReparsing.value = false;
|
||||||
|
currentReparsingId.value = null;
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -209,7 +330,7 @@ watch(
|
|||||||
}
|
}
|
||||||
|
|
||||||
.source-button {
|
.source-button {
|
||||||
&:hover:not(.opacity-50) {
|
&:hover:not(.opacity-50):not(.opacity-40) {
|
||||||
@apply transform -translate-y-0.5 shadow-sm;
|
@apply transform -translate-y-0.5 shadow-sm;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -33,15 +33,15 @@
|
|||||||
</p>
|
</p>
|
||||||
|
|
||||||
<div class="grid grid-cols-2 md:grid-cols-3 gap-2">
|
<div class="grid grid-cols-2 md:grid-cols-3 gap-2">
|
||||||
<!-- Standard Sources -->
|
|
||||||
<div
|
<div
|
||||||
v-for="source in MUSIC_SOURCES"
|
v-for="source in allSources"
|
||||||
:key="source.key"
|
:key="source.key"
|
||||||
class="group relative flex items-center p-2.5 rounded-xl border transition-all duration-200 cursor-pointer"
|
class="group relative flex items-center p-2.5 rounded-xl border transition-all duration-200 cursor-pointer"
|
||||||
:class="[
|
:class="[
|
||||||
isSourceSelected(source.key)
|
isSourceSelected(source.key)
|
||||||
? 'bg-emerald-50/50 dark:bg-emerald-500/10 border-emerald-200 dark:border-emerald-500/20'
|
? 'bg-emerald-50/50 dark:bg-emerald-500/10 border-emerald-200 dark:border-emerald-500/20'
|
||||||
: 'bg-white dark:bg-white/5 border-gray-100 dark:border-white/5 hover:bg-gray-50 dark:hover:bg-white/10'
|
: 'bg-white dark:bg-white/5 border-gray-100 dark:border-white/5 hover:bg-gray-50 dark:hover:bg-white/10',
|
||||||
|
{ 'opacity-60 cursor-not-allowed': !source.available }
|
||||||
]"
|
]"
|
||||||
@click="toggleSource(source.key)"
|
@click="toggleSource(source.key)"
|
||||||
>
|
>
|
||||||
@@ -53,7 +53,7 @@
|
|||||||
}"
|
}"
|
||||||
:class="{ 'bg-gray-100 dark:bg-white/10': !isSourceSelected(source.key) }"
|
:class="{ 'bg-gray-100 dark:bg-white/10': !isSourceSelected(source.key) }"
|
||||||
>
|
>
|
||||||
<i class="ri-music-2-fill text-base"></i>
|
<i :class="source.icon" class="text-base"></i>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="flex-1 min-w-0">
|
<div class="flex-1 min-w-0">
|
||||||
@@ -75,102 +75,22 @@
|
|||||||
></i>
|
></i>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
<!-- lxMusic 子描述 -->
|
||||||
</div>
|
<p
|
||||||
|
v-if="source.key === 'lxMusic'"
|
||||||
<!-- LX Music Source -->
|
class="text-[10px] text-gray-500 mt-0.5 truncate"
|
||||||
<div
|
>
|
||||||
class="group relative flex items-center p-2.5 rounded-xl border transition-all duration-200 cursor-pointer"
|
|
||||||
:class="[
|
|
||||||
isSourceSelected('lxMusic')
|
|
||||||
? 'bg-emerald-50/50 dark:bg-emerald-500/10 border-emerald-200 dark:border-emerald-500/20'
|
|
||||||
: 'bg-white dark:bg-white/5 border-gray-100 dark:border-white/5 hover:bg-gray-50 dark:hover:bg-white/10',
|
|
||||||
{ 'opacity-60 cursor-not-allowed': !activeLxApiId || lxMusicApis.length === 0 }
|
|
||||||
]"
|
|
||||||
@click="toggleSource('lxMusic')"
|
|
||||||
>
|
|
||||||
<div
|
|
||||||
class="flex items-center justify-center w-8 h-8 rounded-full mr-2.5 transition-colors shrink-0"
|
|
||||||
:class="[
|
|
||||||
isSourceSelected('lxMusic')
|
|
||||||
? 'bg-emerald-500 text-white'
|
|
||||||
: 'bg-gray-100 dark:bg-white/10 text-emerald-500'
|
|
||||||
]"
|
|
||||||
>
|
|
||||||
<i class="ri-netease-cloud-music-fill text-base"></i>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="flex-1 min-w-0">
|
|
||||||
<div class="flex items-center justify-between">
|
|
||||||
<span class="font-semibold text-gray-900 dark:text-white text-sm truncate"
|
|
||||||
>落雪音源</span
|
|
||||||
>
|
|
||||||
<div
|
|
||||||
class="w-4 h-4 rounded-full border flex items-center justify-center transition-colors shrink-0 ml-1"
|
|
||||||
:class="[
|
|
||||||
isSourceSelected('lxMusic')
|
|
||||||
? 'bg-emerald-500 border-emerald-500'
|
|
||||||
: 'border-gray-300 dark:border-gray-600'
|
|
||||||
]"
|
|
||||||
>
|
|
||||||
<i
|
|
||||||
v-if="isSourceSelected('lxMusic')"
|
|
||||||
class="ri-check-line text-white text-xs scale-75"
|
|
||||||
></i>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<p class="text-[10px] text-gray-500 mt-0.5 truncate">
|
|
||||||
{{
|
{{
|
||||||
activeLxApiId && lxMusicScriptInfo
|
activeLxApiId && lxMusicScriptInfo
|
||||||
? lxMusicScriptInfo.name
|
? lxMusicScriptInfo.name
|
||||||
: t('settings.playback.lxMusic.scripts.notConfigured')
|
: t('settings.playback.lxMusic.scripts.notConfigured')
|
||||||
}}
|
}}
|
||||||
</p>
|
</p>
|
||||||
</div>
|
<!-- custom 子描述 -->
|
||||||
</div>
|
<p
|
||||||
|
v-else-if="source.key === 'custom'"
|
||||||
<!-- Custom API Source -->
|
class="text-[10px] text-gray-500 mt-0.5 truncate"
|
||||||
<div
|
>
|
||||||
class="group relative flex items-center p-2.5 rounded-xl border transition-all duration-200 cursor-pointer"
|
|
||||||
:class="[
|
|
||||||
isSourceSelected('custom')
|
|
||||||
? 'bg-emerald-50/50 dark:bg-emerald-500/10 border-emerald-200 dark:border-emerald-500/20'
|
|
||||||
: 'bg-white dark:bg-white/5 border-gray-100 dark:border-white/5 hover:bg-gray-50 dark:hover:bg-white/10',
|
|
||||||
{ 'opacity-60 cursor-not-allowed': !settingsStore.setData.customApiPlugin }
|
|
||||||
]"
|
|
||||||
@click="toggleSource('custom')"
|
|
||||||
>
|
|
||||||
<div
|
|
||||||
class="flex items-center justify-center w-8 h-8 rounded-full mr-2.5 transition-colors shrink-0"
|
|
||||||
:class="[
|
|
||||||
isSourceSelected('custom')
|
|
||||||
? 'bg-violet-500 text-white'
|
|
||||||
: 'bg-gray-100 dark:bg-white/10 text-violet-500'
|
|
||||||
]"
|
|
||||||
>
|
|
||||||
<i class="ri-plug-fill text-base"></i>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="flex-1 min-w-0">
|
|
||||||
<div class="flex items-center justify-between">
|
|
||||||
<span class="font-semibold text-gray-900 dark:text-white text-sm truncate">{{
|
|
||||||
t('settings.playback.sourceLabels.custom')
|
|
||||||
}}</span>
|
|
||||||
<div
|
|
||||||
class="w-4 h-4 rounded-full border flex items-center justify-center transition-colors shrink-0 ml-1"
|
|
||||||
:class="[
|
|
||||||
isSourceSelected('custom')
|
|
||||||
? 'bg-emerald-500 border-emerald-500'
|
|
||||||
: 'border-gray-300 dark:border-gray-600'
|
|
||||||
]"
|
|
||||||
>
|
|
||||||
<i
|
|
||||||
v-if="isSourceSelected('custom')"
|
|
||||||
class="ri-check-line text-white text-xs scale-75"
|
|
||||||
></i>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<p class="text-[10px] text-gray-500 mt-0.5 truncate">
|
|
||||||
{{
|
{{
|
||||||
settingsStore.setData.customApiPlugin
|
settingsStore.setData.customApiPlugin
|
||||||
? t('settings.playback.customApi.status.imported')
|
? t('settings.playback.customApi.status.imported')
|
||||||
@@ -377,24 +297,7 @@ import {
|
|||||||
import { useSettingsStore } from '@/store';
|
import { useSettingsStore } from '@/store';
|
||||||
import type { LxMusicScriptConfig, LxScriptInfo, LxSourceKey } from '@/types/lxMusic';
|
import type { LxMusicScriptConfig, LxScriptInfo, LxSourceKey } from '@/types/lxMusic';
|
||||||
import { type Platform } from '@/types/music';
|
import { type Platform } from '@/types/music';
|
||||||
|
import { useMusicSources } from '@/utils/musicSourceConfig';
|
||||||
// ==================== 类型定义 ====================
|
|
||||||
type ExtendedPlatform = Platform | 'custom' | 'lxMusic';
|
|
||||||
|
|
||||||
interface MusicSourceConfig {
|
|
||||||
key: string;
|
|
||||||
description?: string;
|
|
||||||
color: string;
|
|
||||||
disabled?: boolean;
|
|
||||||
}
|
|
||||||
|
|
||||||
// ==================== 音源配置 ====================
|
|
||||||
const MUSIC_SOURCES: MusicSourceConfig[] = [
|
|
||||||
{ key: 'migu', color: '#ff6600' },
|
|
||||||
{ key: 'kugou', color: '#2979ff' },
|
|
||||||
{ key: 'kuwo', color: '#ff8c00' },
|
|
||||||
{ key: 'pyncmd', color: '#ec4141' }
|
|
||||||
];
|
|
||||||
|
|
||||||
// ==================== Props & Emits ====================
|
// ==================== Props & Emits ====================
|
||||||
const props = defineProps({
|
const props = defineProps({
|
||||||
@@ -403,8 +306,8 @@ const props = defineProps({
|
|||||||
default: false
|
default: false
|
||||||
},
|
},
|
||||||
sources: {
|
sources: {
|
||||||
type: Array as () => ExtendedPlatform[],
|
type: Array as () => Platform[],
|
||||||
default: () => ['migu', 'kugou', 'kuwo', 'pyncmd'] as ExtendedPlatform[]
|
default: () => ['migu', 'kugou', 'kuwo', 'pyncmd'] as Platform[]
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -415,8 +318,9 @@ const { t } = useI18n();
|
|||||||
const settingsStore = useSettingsStore();
|
const settingsStore = useSettingsStore();
|
||||||
const message = useMessage();
|
const message = useMessage();
|
||||||
const visible = ref(props.show);
|
const visible = ref(props.show);
|
||||||
const selectedSources = ref<ExtendedPlatform[]>([...props.sources]);
|
const selectedSources = ref<Platform[]>([...props.sources]);
|
||||||
const activeTab = ref('sources');
|
const activeTab = ref('sources');
|
||||||
|
const { allSources } = useMusicSources();
|
||||||
|
|
||||||
const tabs = computed(() => [
|
const tabs = computed(() => [
|
||||||
{ key: 'sources', label: t('settings.playback.lxMusic.tabs.sources') },
|
{ key: 'sources', label: t('settings.playback.lxMusic.tabs.sources') },
|
||||||
@@ -459,7 +363,7 @@ const renameInputRef = ref<HTMLInputElement | null>(null);
|
|||||||
|
|
||||||
// ==================== 计算属性 ====================
|
// ==================== 计算属性 ====================
|
||||||
const isSourceSelected = (sourceKey: string): boolean => {
|
const isSourceSelected = (sourceKey: string): boolean => {
|
||||||
return selectedSources.value.includes(sourceKey as ExtendedPlatform);
|
return selectedSources.value.includes(sourceKey as Platform);
|
||||||
};
|
};
|
||||||
|
|
||||||
// ==================== 方法 ====================
|
// ==================== 方法 ====================
|
||||||
@@ -488,7 +392,7 @@ const toggleSource = (sourceKey: string) => {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const index = selectedSources.value.indexOf(sourceKey as ExtendedPlatform);
|
const index = selectedSources.value.indexOf(sourceKey as Platform);
|
||||||
if (index > -1) {
|
if (index > -1) {
|
||||||
// 至少保留一个音源
|
// 至少保留一个音源
|
||||||
if (selectedSources.value.length <= 1) {
|
if (selectedSources.value.length <= 1) {
|
||||||
@@ -497,7 +401,7 @@ const toggleSource = (sourceKey: string) => {
|
|||||||
}
|
}
|
||||||
selectedSources.value.splice(index, 1);
|
selectedSources.value.splice(index, 1);
|
||||||
} else {
|
} else {
|
||||||
selectedSources.value.push(sourceKey as ExtendedPlatform);
|
selectedSources.value.push(sourceKey as Platform);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -749,7 +653,7 @@ const saveScriptName = (apiId: string) => {
|
|||||||
* 确认选择
|
* 确认选择
|
||||||
*/
|
*/
|
||||||
const handleConfirm = () => {
|
const handleConfirm = () => {
|
||||||
const defaultPlatforms: ExtendedPlatform[] = ['migu', 'kugou', 'kuwo', 'pyncmd'];
|
const defaultPlatforms: Platform[] = ['migu', 'kugou', 'kuwo', 'pyncmd'];
|
||||||
const valuesToEmit =
|
const valuesToEmit =
|
||||||
selectedSources.value.length > 0 ? [...new Set(selectedSources.value)] : defaultPlatforms;
|
selectedSources.value.length > 0 ? [...new Set(selectedSources.value)] : defaultPlatforms;
|
||||||
emit('update:sources', valuesToEmit);
|
emit('update:sources', valuesToEmit);
|
||||||
@@ -812,7 +716,7 @@ watch(
|
|||||||
// 同步外部sources属性变化
|
// 同步外部sources属性变化
|
||||||
watch(
|
watch(
|
||||||
() => props.sources,
|
() => props.sources,
|
||||||
(newVal: ExtendedPlatform[]) => {
|
(newVal: Platform[]) => {
|
||||||
selectedSources.value = [...newVal];
|
selectedSources.value = [...newVal];
|
||||||
},
|
},
|
||||||
{ deep: true }
|
{ deep: true }
|
||||||
|
|||||||
@@ -23,12 +23,6 @@ export type LxQuality = '128k' | '320k' | 'flac' | 'flac24bit';
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* 支持的音源 key
|
* 支持的音源 key
|
||||||
* - kw: 酷我
|
|
||||||
* - kg: 酷狗
|
|
||||||
* - tx: QQ音乐
|
|
||||||
* - wy: 网易云
|
|
||||||
* - mg: 咪咕
|
|
||||||
* - local: 本地音乐
|
|
||||||
*/
|
*/
|
||||||
export type LxSourceKey = 'kw' | 'kg' | 'tx' | 'wy' | 'mg' | 'local';
|
export type LxSourceKey = 'kw' | 'kg' | 'tx' | 'wy' | 'mg' | 'local';
|
||||||
|
|
||||||
@@ -122,12 +116,12 @@ export const LX_EVENT_NAMES = {
|
|||||||
* 落雪音源 key 到平台名称的映射
|
* 落雪音源 key 到平台名称的映射
|
||||||
*/
|
*/
|
||||||
export const LX_SOURCE_NAMES: Record<LxSourceKey, string> = {
|
export const LX_SOURCE_NAMES: Record<LxSourceKey, string> = {
|
||||||
kw: '酷我',
|
kw: 'kw',
|
||||||
kg: '酷狗',
|
kg: 'kg',
|
||||||
tx: 'QQ音乐',
|
tx: 'tx',
|
||||||
wy: '网易云',
|
wy: 'wy',
|
||||||
mg: '咪咕',
|
mg: 'mg',
|
||||||
local: '本地'
|
local: 'local'
|
||||||
};
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
@@ -1,5 +1,14 @@
|
|||||||
// 音乐平台类型
|
// 音乐平台类型
|
||||||
export type Platform = 'qq' | 'migu' | 'kugou' | 'kuwo' | 'pyncmd' | 'joox' | 'gdmusic' | 'lxMusic';
|
export type Platform =
|
||||||
|
| 'qq'
|
||||||
|
| 'migu'
|
||||||
|
| 'kugou'
|
||||||
|
| 'kuwo'
|
||||||
|
| 'pyncmd'
|
||||||
|
| 'joox'
|
||||||
|
| 'gdmusic'
|
||||||
|
| 'lxMusic'
|
||||||
|
| 'custom';
|
||||||
|
|
||||||
// 默认平台列表
|
// 默认平台列表
|
||||||
export const DEFAULT_PLATFORMS: Platform[] = ['lxMusic', 'migu', 'kugou', 'kuwo', 'pyncmd'];
|
export const DEFAULT_PLATFORMS: Platform[] = ['lxMusic', 'migu', 'kugou', 'kuwo', 'pyncmd'];
|
||||||
|
|||||||
62
src/renderer/utils/musicSourceConfig.ts
Normal file
62
src/renderer/utils/musicSourceConfig.ts
Normal file
@@ -0,0 +1,62 @@
|
|||||||
|
import { computed } from 'vue';
|
||||||
|
|
||||||
|
import { useSettingsStore } from '@/store';
|
||||||
|
import type { Platform } from '@/types/music';
|
||||||
|
|
||||||
|
// ==================== 类型定义 ====================
|
||||||
|
|
||||||
|
export type MusicSourceGroup = 'unblock' | 'extended' | 'plugin';
|
||||||
|
|
||||||
|
export type MusicSourceMeta = {
|
||||||
|
key: Platform;
|
||||||
|
icon: string;
|
||||||
|
color: string;
|
||||||
|
group: MusicSourceGroup;
|
||||||
|
};
|
||||||
|
|
||||||
|
export type MusicSourceInfo = MusicSourceMeta & {
|
||||||
|
available: boolean;
|
||||||
|
configHint?: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
// ==================== 静态注册表 ====================
|
||||||
|
|
||||||
|
export const MUSIC_SOURCE_REGISTRY: MusicSourceMeta[] = [
|
||||||
|
// 内置解锁音源 (UnblockMusicStrategy)
|
||||||
|
{ key: 'migu', icon: 'ri-music-2-fill', color: '#ff6600', group: 'unblock' },
|
||||||
|
{ key: 'kugou', icon: 'ri-music-fill', color: '#2979ff', group: 'unblock' },
|
||||||
|
{ key: 'kuwo', icon: 'ri-music-fill', color: '#ff8c00', group: 'unblock' },
|
||||||
|
{ key: 'pyncmd', icon: 'ri-netease-cloud-music-fill', color: '#ec4141', group: 'unblock' },
|
||||||
|
// 扩展音源 (GDMusicStrategy)
|
||||||
|
{ key: 'gdmusic', icon: 'ri-google-fill', color: '#4285f4', group: 'extended' },
|
||||||
|
// 插件音源 (需要用户配置)
|
||||||
|
{ key: 'lxMusic', icon: 'ri-leaf-fill', color: '#22c55e', group: 'plugin' },
|
||||||
|
{ key: 'custom', icon: 'ri-plug-fill', color: '#8b5cf6', group: 'plugin' }
|
||||||
|
];
|
||||||
|
|
||||||
|
// ==================== Composable ====================
|
||||||
|
|
||||||
|
export const useMusicSources = () => {
|
||||||
|
const settingsStore = useSettingsStore();
|
||||||
|
|
||||||
|
const allSources = computed<MusicSourceInfo[]>(() => {
|
||||||
|
return MUSIC_SOURCE_REGISTRY.map((source) => {
|
||||||
|
let available = true;
|
||||||
|
let configHint: string | undefined;
|
||||||
|
|
||||||
|
if (source.key === 'lxMusic') {
|
||||||
|
available =
|
||||||
|
(settingsStore.setData.lxMusicScripts?.length ?? 0) > 0 &&
|
||||||
|
Boolean(settingsStore.setData.activeLxMusicApiId);
|
||||||
|
if (!available) configHint = 'settings.playback.lxMusic.scripts.notConfigured';
|
||||||
|
} else if (source.key === 'custom') {
|
||||||
|
available = Boolean(settingsStore.setData.customApiPlugin);
|
||||||
|
if (!available) configHint = 'settings.playback.customApi.notImported';
|
||||||
|
}
|
||||||
|
|
||||||
|
return { ...source, available, configHint };
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
return { allSources };
|
||||||
|
};
|
||||||
@@ -257,10 +257,9 @@ const getFavoriteSongs = async () => {
|
|||||||
try {
|
try {
|
||||||
const currentIds = getCurrentPageIds();
|
const currentIds = getCurrentPageIds();
|
||||||
|
|
||||||
// 分离网易云音乐ID和B站视频ID
|
|
||||||
const musicIds = currentIds.filter((id) => typeof id === 'number') as number[];
|
const musicIds = currentIds.filter((id) => typeof id === 'number') as number[];
|
||||||
|
|
||||||
// 处理网易云音乐数据
|
// 处理音乐数据
|
||||||
let neteaseSongs: SongResult[] = [];
|
let neteaseSongs: SongResult[] = [];
|
||||||
if (musicIds.length > 0) {
|
if (musicIds.length > 0) {
|
||||||
const res = await getMusicDetail(musicIds);
|
const res = await getMusicDetail(musicIds);
|
||||||
@@ -282,7 +281,7 @@ const getFavoriteSongs = async () => {
|
|||||||
.map((id) => {
|
.map((id) => {
|
||||||
const strId = String(id);
|
const strId = String(id);
|
||||||
|
|
||||||
// 查找网易云音乐
|
// 查找音乐
|
||||||
const found = neteaseSongs.find((song) => String(song.id) === strId);
|
const found = neteaseSongs.find((song) => String(song.id) === strId);
|
||||||
return found;
|
return found;
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -555,7 +555,7 @@ const loadHistoryData = async () => {
|
|||||||
|
|
||||||
// 根据分类处理不同的数据
|
// 根据分类处理不同的数据
|
||||||
if (currentCategory.value === 'songs') {
|
if (currentCategory.value === 'songs') {
|
||||||
// 区分本地歌曲和网易云歌曲
|
// 区分本地歌曲和云歌曲
|
||||||
const localItems: any[] = [];
|
const localItems: any[] = [];
|
||||||
const neteaseItems: any[] = [];
|
const neteaseItems: any[] = [];
|
||||||
|
|
||||||
@@ -567,7 +567,7 @@ const loadHistoryData = async () => {
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
// 获取网易云歌曲详情
|
// 获取歌曲详情
|
||||||
let neteaseSongs: SongResult[] = [];
|
let neteaseSongs: SongResult[] = [];
|
||||||
if (neteaseItems.length > 0) {
|
if (neteaseItems.length > 0) {
|
||||||
try {
|
try {
|
||||||
@@ -585,7 +585,7 @@ const loadHistoryData = async () => {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('获取网易云歌曲详情失败:', error);
|
console.error('获取歌曲详情失败:', error);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -83,7 +83,7 @@
|
|||||||
{{ item.name }}
|
{{ item.name }}
|
||||||
</h3>
|
</h3>
|
||||||
<p class="text-xs text-neutral-500 dark:text-neutral-400 line-clamp-1">
|
<p class="text-xs text-neutral-500 dark:text-neutral-400 line-clamp-1">
|
||||||
{{ item.updateFrequency || '网易云音乐榜单' }}
|
{{ item.updateFrequency }}
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
Reference in New Issue
Block a user