feat: 插件系统完全兼容PC/Android版格式

This commit is contained in:
QZMusic
2026-06-04 15:13:15 +00:00
parent c36b98fb90
commit a4368eb232
8 changed files with 341 additions and 221 deletions

View File

@@ -133,10 +133,13 @@
<div class="plugin-info">
<div class="plugin-name">{{ plugin.name }}</div>
<div class="plugin-desc">{{ plugin.description || '暂无描述' }}</div>
<div class="plugin-meta" v-if="plugin.version">
<span class="version">v{{ plugin.version }}</span>
<div class="plugin-meta">
<span class="version" v-if="plugin.version">v{{ plugin.version }}</span>
<span class="author" v-if="plugin.author">by {{ plugin.author }}</span>
</div>
<div class="quality-tags" v-if="plugin.quality && plugin.quality.length > 0">
<span class="quality-tag" v-for="q in plugin.quality" :key="q.id">{{ q.ui }}</span>
</div>
</div>
<div class="plugin-actions">
<button
@@ -192,12 +195,12 @@
<Icon icon="lucide:upload" />
本地文件
</div>
<div class="method-desc">上传音源插件的JSON配置文件</div>
<div class="method-desc">上传 PC/Android 版的音源插件文件.js</div>
<div class="file-upload">
<input
type="file"
ref="fileInput"
accept=".json"
accept=".js,.json"
@change="handleFileUpload"
class="file-input"
/>
@@ -215,12 +218,20 @@
<Icon icon="lucide:code" />
代码导入
</div>
<div class="method-desc">直接粘贴音源插件的JSON代码</div>
<div class="method-desc">粘贴 PC/Android 版插件的 JS 代码module.exports 格式</div>
<textarea
v-model="pluginCode"
placeholder='{"id": "my-plugin", "name": "我的音源", "searchFunction": "return {list: [], total: 0};"}'
placeholder="module.exports = {
pluginInfo: {
info: { id: 'my-plugin', name: '我的音源', ... },
quality: [...]
},
musicSearch: { search: (query, page, limit) => {...} },
getUrl: (id, quality) => '...',
getLyric: (id) => '...'
}"
class="code-textarea"
rows="6"
rows="8"
></textarea>
<button class="add-btn full-width" @click="importFromCode" :disabled="!pluginCode">
<Icon icon="lucide:play" />
@@ -252,7 +263,7 @@ import { ref, reactive, onBeforeMount, nextTick } from 'vue';
import { Icon } from '@iconify/vue';
import { usePlayerStore } from '../stores/player';
import { pluginManager } from '../plugins/index';
import type { PluginInfo } from '../types';
import type { PluginFullInfo } from '../types';
const playerStore = usePlayerStore();
@@ -286,7 +297,7 @@ const appearance = reactive({
});
// 插件管理相关
const installedPlugins = ref<PluginInfo[]>([]);
const installedPlugins = ref<PluginFullInfo[]>([]);
const activePluginId = ref('');
const pluginUrl = ref('');
const pluginCode = ref('');
@@ -305,6 +316,7 @@ const activatePlugin = (id: string) => {
const removePlugin = (id: string) => {
if (confirm('确定要删除这个音源吗?')) {
pluginManager.removeUserPlugin(id);
pluginManager.unregister(id);
refreshPlugins();
}
@@ -314,8 +326,8 @@ const importFromUrl = async () => {
if (!pluginUrl.value) return;
try {
const response = await fetch(pluginUrl.value);
const data = await response.json();
await importPluginData(data);
const code = await response.text();
importPluginCode(code);
pluginUrl.value = '';
} catch (e) {
alert('导入失败:' + (e as Error).message);
@@ -334,78 +346,36 @@ const handleFileUpload = async (event: Event) => {
selectedFile.value = file;
try {
const text = await file.text();
const data = JSON.parse(text);
await importPluginData(data);
const code = await file.text();
importPluginCode(code);
selectedFile.value = null;
target.value = '';
} catch (e) {
alert('文件解析失败:' + (e as Error).message);
alert('文件读取失败:' + (e as Error).message);
}
};
const importFromCode = async () => {
const importFromCode = () => {
if (!pluginCode.value) return;
try {
const data = JSON.parse(pluginCode.value);
await importPluginData(data);
pluginCode.value = '';
} catch (e) {
alert('代码解析失败:' + (e as Error).message);
}
importPluginCode(pluginCode.value);
pluginCode.value = '';
};
const importPluginData = async (data: any) => {
// 验证必要字段
if (!data.id || !data.name) {
alert('插件数据缺少必要字段id 和 name');
/** 导入插件代码(兼容 PC/Android 版 CommonJS module.exports 格式) */
const importPluginCode = (code: string) => {
const module = pluginManager.loadFromCode(code, 'user');
if (!module) {
alert('插件加载失败,请检查代码格式是否正确(需要 module.exports = {...}');
return;
}
// 标记为用户插件
data.source = 'user';
// 创建插件模块
const module = {
info: {
id: data.id,
name: data.name,
description: data.description || '',
version: data.version || '1.0.0',
author: data.author || '未知',
source: 'user' as const,
},
search: async (query: string, page: number, limit: number) => {
if (data.searchFunction) {
try {
const fn = new Function('query', 'page', 'limit', data.searchFunction);
return await fn(query, page, limit);
} catch (e) {
console.error('Search function error:', e);
return { list: [], total: 0 };
}
}
return { list: [], total: 0 };
},
getSongUrl: data.getSongUrlFunction ? async (songId: string) => {
const fn = new Function('songId', data.getSongUrlFunction);
return await fn(songId);
} : undefined,
getLyric: data.getLyricFunction ? async (songId: string) => {
const fn = new Function('songId', data.getLyricFunction);
return await fn(songId);
} : undefined,
};
pluginManager.registerModule(module);
// 保存到localStorage
const saved = JSON.parse(localStorage.getItem('qz-user-plugins') || '[]');
saved.push(data);
localStorage.setItem('qz-user-plugins', JSON.stringify(saved));
// 保存到 localStorage
pluginManager.saveUserPlugin(code);
refreshPlugins();
alert(`音源 "${data.name}" 导入成功!`);
const info = module.pluginInfo?.info || module.info;
alert(`音源 "${info?.name || '未知'}" 导入成功!`);
};
const applyTheme = (theme: 'dark' | 'light') => {
@@ -871,6 +841,23 @@ onBeforeMount(async () => {
color: var(--color-text-secondary);
}
.quality-tags {
display: flex;
gap: 4px;
margin-top: 6px;
flex-wrap: wrap;
}
.quality-tag {
font-size: 11px;
padding: 1px 6px;
border-radius: 3px;
background: var(--color-accent-soft);
color: var(--color-accent);
font-weight: 500;
letter-spacing: 0.5px;
}
.plugin-actions {
display: flex;
gap: 8px;

View File

@@ -1,52 +1,65 @@
import type { PluginModule, PluginSearchResult } from '../../types/plugin';
import type { PluginModule } from '../../types/plugin';
const mockSongs = [
{ id: '1', name: '晴天', artist: '周杰伦', albumName: '叶惠美', duration: '04:29', picUrl: 'https://picsum.photos/200/200?random=1' },
{ id: '2', name: '夜曲', artist: '周杰伦', albumName: '十一月的萧邦', duration: '04:23', picUrl: 'https://picsum.photos/200/200?random=2' },
{ id: '3', name: '稻香', artist: '周杰伦', albumName: '魔杰座', duration: '03:43', picUrl: 'https://picsum.photos/200/200?random=3' },
{ id: '4', name: '七里香', artist: '周杰伦', albumName: '七里香', duration: '04:59', picUrl: 'https://picsum.photos/200/200?random=4' },
{ id: '5', name: '告白气球', artist: '周杰伦', albumName: '周杰伦的床边故事', duration: '03:35', picUrl: 'https://picsum.photos/200/200?random=5' },
{ id: '6', name: '成都', artist: '赵雷', albumName: '无法长大', duration: '05:28', picUrl: 'https://picsum.photos/200/200?random=6' },
{ id: '7', name: '理想', artist: '赵雷', albumName: '无法长大', duration: '04:26', picUrl: 'https://picsum.photos/200/200?random=7' },
{ id: '8', name: '南方姑娘', artist: '赵雷', albumName: '赵小雷', duration: '05:34', picUrl: 'https://picsum.photos/200/200?random=8' },
{ id: '9', name: '平凡之路', artist: '朴树', albumName: '猎户星座', duration: '04:46', picUrl: 'https://picsum.photos/200/200?random=9' },
{ id: '10', name: '那些花儿', artist: '朴树', albumName: '我去2000年', duration: '04:57', picUrl: 'https://picsum.photos/200/200?random=10' },
];
const searchSongs = (query: string): typeof mockSongs => {
const q = query.toLowerCase();
return mockSongs.filter(song =>
song.name.toLowerCase().includes(q) ||
song.artist.toLowerCase().includes(q) ||
song.albumName.toLowerCase().includes(q)
);
};
export const defaultPlugin: PluginModule = {
info: {
id: 'default',
name: '默认音源',
description: '内置音乐搜索插件',
version: '1.0.0',
author: 'QZMusic',
source: 'built-in',
/**
* 默认音源插件PC 原版格式)
* 使用 module.exports 格式,与 PC/Android 版完全一致
*/
const defaultPluginCode = `
module.exports = {
pluginInfo: {
info: {
id: 'default',
name: '默认音源',
description: '内置音乐搜索插件(演示用)',
version: '1.0.0',
author: 'QZMusic'
},
quality: [
{ id: 'standard', name: '标准', ui: 'SQ' },
{ id: 'exhigh', name: '极高', ui: 'HQ' },
{ id: 'hires', name: 'Hi-Res', ui: 'HR' }
]
},
async search(query: string, page: number, limit: number): Promise<PluginSearchResult> {
const results = searchSongs(query);
const start = (page - 1) * limit;
const end = start + limit;
return {
list: results.slice(start, end),
total: results.length,
songCount: results.length,
};
musicSearch: {
search: function(query, page, limit) {
var songs = [
{ songmid: '1', name: '晴天', singer: '周杰伦', albumName: '叶惠美', interval: 269000, img: 'https://picsum.photos/200/200?random=1', source: 'default' },
{ songmid: '2', name: '夜曲', singer: '周杰伦', albumName: '十一月的萧邦', interval: 263000, img: 'https://picsum.photos/200/200?random=2', source: 'default' },
{ songmid: '3', name: '稻香', singer: '周杰伦', albumName: '魔杰座', interval: 223000, img: 'https://picsum.photos/200/200?random=3', source: 'default' },
{ songmid: '4', name: '七里香', singer: '周杰伦', albumName: '七里香', interval: 299000, img: 'https://picsum.photos/200/200?random=4', source: 'default' },
{ songmid: '5', name: '告白气球', singer: '周杰伦', albumName: '周杰伦的床边故事', interval: 215000, img: 'https://picsum.photos/200/200?random=5', source: 'default' },
{ songmid: '6', name: '成都', singer: '赵雷', albumName: '无法长大', interval: 328000, img: 'https://picsum.photos/200/200?random=6', source: 'default' },
{ songmid: '7', name: '理想', singer: '赵雷', albumName: '无法长大', interval: 266000, img: 'https://picsum.photos/200/200?random=7', source: 'default' },
{ songmid: '8', name: '南方姑娘', singer: '赵雷', albumName: '赵小雷', interval: 334000, img: 'https://picsum.photos/200/200?random=8', source: 'default' },
{ songmid: '9', name: '平凡之路', singer: '朴树', albumName: '猎户星座', interval: 286000, img: 'https://picsum.photos/200/200?random=9', source: 'default' },
{ songmid: '10', name: '那些花儿', singer: '朴树', albumName: '我去2000年', interval: 297000, img: 'https://picsum.photos/200/200?random=10', source: 'default' }
];
var q = query.toLowerCase();
var filtered = songs.filter(function(s) {
return s.name.toLowerCase().indexOf(q) !== -1 || s.singer.toLowerCase().indexOf(q) !== -1;
});
var start = (page - 1) * limit;
var end = start + limit;
return {
list: filtered.slice(start, end),
songCount: filtered.length,
total: filtered.length
};
}
},
async getSongUrl(_songId: string): Promise<string> {
getUrl: function(id, quality) {
return 'http://commondatastorage.googleapis.com/codeskulptor-demos/riceracer_assets/music/win.ogg';
},
async getLyric(_songId: string): Promise<string> {
return `[00:00.00]歌曲歌词
[00:05.00]暂无歌词数据
[00:10.00]---`;
},
getLyric: function(id) {
return '[00:00.00]歌曲歌词\\n[00:05.00]暂无歌词数据\\n[00:10.00]---';
}
};
`;
export const defaultPluginModule: PluginModule = (function() {
var module = { exports: {} };
eval(defaultPluginCode);
return module.exports;
})();
export { defaultPluginCode };

View File

@@ -1,3 +1,3 @@
export { pluginManager } from './pluginManager';
export { defaultPlugin } from './impl/defaultPlugin';
export { defaultPluginModule, defaultPluginCode } from './impl/defaultPlugin';
export * from '../types/plugin';

View File

@@ -1,10 +1,15 @@
import { pluginManager, defaultPlugin } from './index';
import { pluginManager, defaultPluginModule } from './index';
export const initPlugins = async (): Promise<void> => {
await pluginManager.register(async () => defaultPlugin);
// 注册内置默认插件
pluginManager.registerModule(defaultPluginModule);
// 恢复用户保存的插件
pluginManager.loadUserPlugins();
// 恢复上次选择的插件
const savedPlugin = sessionStorage.getItem('qz-active-plugin');
if (savedPlugin) {
if (savedPlugin && pluginManager.get(savedPlugin)) {
pluginManager.setActivePlugin(savedPlugin);
}
};

View File

@@ -1,56 +1,96 @@
import type { PluginInfo, PluginModule, PluginSearchResult } from '../types/plugin';
import type { PluginFullInfo, PluginModule, PluginSearchResult, UrlResponse } from '../types/plugin';
/**
* QZMusic Web 插件管理器
* 兼容 PC/Android 原版的 CommonJS 插件格式
*
* PC 原版插件格式:
* module.exports = {
* pluginInfo: { info: { id, name, ... }, quality: [...] },
* getUrl: (id, quality) => url,
* musicSearch: { search: (query, page, limit) => result },
* getLyric: (id) => lyric
* }
*/
class PluginManager {
private plugins: Map<string, PluginModule> = new Map();
private activePluginId: string = '';
async register(loader: () => Promise<PluginModule>): Promise<void> {
try {
const module = await loader();
this.plugins.set(module.info.id, module);
if (!this.activePluginId) {
this.activePluginId = module.info.id;
}
} catch (error) {
console.error('Failed to load plugin:', error);
}
}
// 直接注册插件模块
/** 注册插件模块(直接传入已解析的模块对象) */
registerModule(module: PluginModule): boolean {
if (!module.info || !module.info.id) {
console.error('Plugin module must have info.id');
const info = this.extractInfo(module);
if (!info || !info.id) {
console.error('[PluginManager] 插件缺少有效的 info 或 pluginInfo.info.id');
return false;
}
this.plugins.set(module.info.id, module);
this.plugins.set(info.id, module);
if (!this.activePluginId) {
this.activePluginId = module.info.id;
this.activePluginId = info.id;
}
return true;
}
// 卸载插件
/** 从 JS 代码字符串加载插件(兼容 PC 版 CommonJS 格式) */
loadFromCode(code: string, source: 'built-in' | 'user' = 'user'): PluginModule | null {
try {
// 将 CommonJS module.exports 转换为可执行的函数
// 支持 module.exports = { ... } 和 exports.xxx = ... 语法
const wrappedCode = `
(function() {
var module = { exports: {} };
var exports = module.exports;
${code}
return module.exports;
})()
`;
const module = new Function(wrappedCode)() as PluginModule;
if (!module) {
console.error('[PluginManager] 插件代码执行后返回空');
return null;
}
// 标记来源
if (module.pluginInfo?.info) {
(module.pluginInfo.info as any).__source = source;
}
if (module.info) {
(module.info as any).__source = source;
}
this.registerModule(module);
return module;
} catch (e) {
console.error('[PluginManager] 加载插件代码失败:', e);
return null;
}
}
/** 卸载插件 */
unregister(id: string): boolean {
if (this.activePluginId === id) {
// 如果卸载的是当前激活的插件,切换到第一个可用的
const remaining = this.getAll();
this.activePluginId = remaining.length > 0 && remaining[0].id !== id ? remaining[0].id : '';
}
return this.plugins.delete(id);
}
getAll(): PluginInfo[] {
return Array.from(this.plugins.values()).map(p => p.info);
/** 获取所有插件信息 */
getAll(): PluginFullInfo[] {
return Array.from(this.plugins.values()).map(p => this.extractInfo(p)!).filter(Boolean);
}
/** 获取插件模块 */
get(id: string): PluginModule | undefined {
return this.plugins.get(id);
}
/** 获取当前激活的插件 */
getActivePlugin(): PluginModule | undefined {
return this.plugins.get(this.activePluginId);
}
/** 设置激活插件 */
setActivePlugin(id: string): boolean {
if (this.plugins.has(id)) {
this.activePluginId = id;
@@ -60,84 +100,124 @@ class PluginManager {
return false;
}
/** 获取当前激活插件ID */
getActivePluginId(): string {
return this.activePluginId;
}
/** 搜索(兼容 PC 版 musicSearch.search 接口) */
async search(query: string, page: number, limit: number): Promise<PluginSearchResult> {
const plugin = this.getActivePlugin();
if (!plugin) {
return { list: [], total: 0 };
if (!plugin?.musicSearch?.search) {
return { list: [], total: 0, error: '当前插件不支持搜索' };
}
try {
return await plugin.musicSearch.search(query, page, limit);
} catch (e) {
console.error('[PluginManager] 搜索失败:', e);
return { list: [], total: 0, error: (e as Error).message };
}
return plugin.search(query, page, limit);
}
async getSongUrl(songId: string): Promise<string> {
/** 获取歌曲URL兼容 PC 版 getUrl 接口) */
async getSongUrl(id: string, quality: string = 'standard'): Promise<UrlResponse> {
const plugin = this.getActivePlugin();
if (!plugin || !plugin.getSongUrl) {
throw new Error('No active plugin or plugin does not support getSongUrl');
if (!plugin?.getUrl) {
return { success: false, error: '当前插件不支持获取URL' };
}
try {
const url = await plugin.getUrl(id, quality);
if (typeof url !== 'string' || !url.startsWith('http')) {
return { success: false, error: '无效的URL' };
}
return { success: true, url };
} catch (e) {
return { success: false, error: (e as Error).message || '插件错误' };
}
return plugin.getSongUrl(songId);
}
async getLyric(songId: string): Promise<string> {
/** 获取歌词(兼容 PC 版 getLyric 接口) */
async getLyric(id: string): Promise<any> {
const plugin = this.getActivePlugin();
if (!plugin || !plugin.getLyric) {
throw new Error('No active plugin or plugin does not support getLyric');
if (!plugin?.getLyric) {
return null;
}
try {
return await plugin.getLyric(id);
} catch (e) {
console.error('[PluginManager] 获取歌词失败:', e);
return null;
}
return plugin.getLyric(songId);
}
hasPlugins(): boolean {
return this.plugins.size > 0;
}
// 从localStorage加载用户添加的插件
/** localStorage 恢复用户插件 */
loadUserPlugins(): void {
try {
const saved = localStorage.getItem('qz-user-plugins');
if (saved) {
const pluginList = JSON.parse(saved);
pluginList.forEach((pluginData: any) => {
this.loadPluginFromData(pluginData);
const pluginList: string[] = JSON.parse(saved);
pluginList.forEach(code => {
this.loadFromCode(code, 'user');
});
}
} catch (e) {
console.error('Failed to load user plugins:', e);
console.error('[PluginManager] 恢复用户插件失败:', e);
}
}
// 保存用户插件到localStorage
saveUserPlugins(): void {
const userPlugins = this.getAll().filter(p => p.source === 'user');
localStorage.setItem('qz-user-plugins', JSON.stringify(userPlugins));
/** 保存用户插件到 localStorage */
saveUserPlugin(code: string): void {
const saved: string[] = JSON.parse(localStorage.getItem('qz-user-plugins') || '[]');
saved.push(code);
localStorage.setItem('qz-user-plugins', JSON.stringify(saved));
}
// 从数据加载插件(用于用户上传的插件)
private loadPluginFromData(data: any): void {
try {
// 创建一个简单的插件模块
const module: PluginModule = {
info: {
id: data.id,
name: data.name,
description: data.description,
version: data.version,
author: data.author,
},
search: async (query: string, page: number, limit: number) => {
// 如果插件有自定义搜索逻辑使用eval执行
if (data.searchFunction) {
const fn = new Function('query', 'page', 'limit', data.searchFunction);
return await fn(query, page, limit);
}
return { list: [], total: 0 };
},
/** 删除用户插件代码 */
removeUserPlugin(id: string): void {
const saved: string[] = JSON.parse(localStorage.getItem('qz-user-plugins') || '[]');
// 尝试从代码中提取id来匹配
const filtered = saved.filter(code => {
try {
const wrappedCode = `
(function() {
var module = { exports: {} };
var exports = module.exports;
${code}
return module.exports;
})()
`;
const mod = new Function(wrappedCode)() as any;
const info = mod?.pluginInfo?.info || mod?.info;
return info?.id !== id;
} catch {
return true; // 解析失败的保留
}
});
localStorage.setItem('qz-user-plugins', JSON.stringify(filtered));
}
/** 提取插件信息(兼容 PC 版两种格式) */
private extractInfo(module: PluginModule): PluginFullInfo | null {
// PC 原版格式pluginInfo.info
if (module.pluginInfo?.info) {
return {
...module.pluginInfo.info,
quality: module.pluginInfo.quality,
source: (module.pluginInfo.info as any).__source || 'built-in',
};
this.registerModule(module);
} catch (e) {
console.error('Failed to load plugin from data:', e);
}
// 简化格式info
if (module.info) {
return {
...module.info,
source: (module.info as any).__source || 'built-in',
};
}
return null;
}
}

View File

@@ -1,3 +1,13 @@
// 兼容 PC/Android 原版 QZMusic 插件系统的类型定义
/** 音质信息 */
export interface PluginQuality {
id: string;
name: string;
ui: string;
}
/** 插件元信息(兼容 PC 版 pluginInfo.info */
export interface PluginInfo {
id: string;
name: string;
@@ -8,24 +18,37 @@ export interface PluginInfo {
source?: 'built-in' | 'user';
}
/** 插件完整信息(包含音质列表) */
export interface PluginFullInfo extends PluginInfo {
quality?: PluginQuality[];
}
/** PC 原版插件模块接口 */
export interface PluginModule {
pluginInfo?: {
info: PluginInfo;
quality?: PluginQuality[];
};
info?: PluginInfo;
getUrl?: (id: string, quality: string) => Promise<string> | string;
musicSearch?: {
search: (query: string, page: number, limit: number) => Promise<PluginSearchResult> | PluginSearchResult;
};
getLyric?: (id: string) => Promise<string | object> | string | object;
}
/** 搜索结果 */
export interface PluginSearchResult {
list: any[];
total?: number;
songCount?: number;
allPage?: number;
error?: string;
}
export interface PluginAPI {
search: (pluginId: string, query: string, page: number, limit: number) => Promise<PluginSearchResult>;
getSongUrl: (pluginId: string, songId: string) => Promise<string>;
getLyric: (pluginId: string, songId: string) => Promise<string>;
getAll: () => Promise<PluginInfo[]>;
}
export type PluginLoader = () => Promise<PluginModule>;
export interface PluginModule {
info: PluginInfo;
search: (query: string, page: number, limit: number) => Promise<PluginSearchResult>;
getSongUrl?: (songId: string) => Promise<string>;
getLyric?: (songId: string) => Promise<string>;
/** URL 响应 */
export interface UrlResponse {
success: boolean;
url?: string;
error?: string;
}

View File

@@ -1,5 +1,6 @@
import type { Song } from '../types/song';
/** 将毫秒转换为 MM:SS 格式 */
export function formatDuration(ms: number): string {
const totalSeconds = Math.floor(ms / 1000);
const minutes = Math.floor(totalSeconds / 60);
@@ -7,19 +8,39 @@ export function formatDuration(ms: number): string {
return `${minutes.toString().padStart(2, '0')}:${seconds.toString().padStart(2, '0')}`;
}
/**
* 将 PC 原版插件搜索结果转换为 Web 版 Song 格式
* 兼容 PC 版 songUtils.ts 的 transformSearchSong
*
* PC 原版搜索结果字段:
* - songmid: 歌曲 ID
* - name: 歌曲名
* - singer: 歌手名
* - img / m_img / s_img: 封面图
* - interval: 时长(毫秒)
* - source: 来源标识
* - albumId: 专辑 ID
* - albumName: 专辑名
* - types: 音质映射
*/
export function transformSearchSong(raw: any): Song {
return {
id: String(raw.songmid),
name: raw.name,
artist: raw.singer,
picUrl: raw.img || raw.m_img || raw.s_img,
url: '', // Empty initially
id: String(raw.songmid || raw.id || ''),
name: raw.name || '未知歌曲',
artist: raw.singer || raw.artist || '未知歌手',
picUrl: raw.img || raw.m_img || raw.s_img || raw.picUrl || '',
url: '',
duration: formatDuration(raw.interval ? Number(raw.interval) : 0),
source: raw.source,
source: raw.source || '',
albumId: raw.albumId ? String(raw.albumId) : null,
albumName: raw.albumName,
albumName: raw.albumName || null,
type: 'Remote',
quality: 'auto',
types: raw.types // Store raw types for quality selection later
types: raw.types || undefined,
};
}
/** 批量转换搜索结果 */
export function transformSearchResults(results: any[]): Song[] {
return results.map(item => transformSearchSong(item));
}

View File

@@ -128,7 +128,8 @@ import { useRoute } from 'vue-router';
import { Icon } from '@iconify/vue';
import { usePlayerStore } from '../stores/player';
import { pluginManager } from '../plugins/index';
import type { PluginInfo, Song } from '../types';
import { transformSearchResults } from '../utils/songUtils';
import type { PluginFullInfo, Song } from '../types';
const route = useRoute();
const playerStore = usePlayerStore();
@@ -144,7 +145,7 @@ const loading = ref(false);
const error = ref(false);
const songs = ref<Song[]>([]);
const plugins = ref<PluginInfo[]>([]);
const plugins = ref<PluginFullInfo[]>([]);
const activePlugin = ref<string>('');
const isDropdownOpen = ref(false);
@@ -157,7 +158,7 @@ const toggleDropdown = () => {
isDropdownOpen.value = !isDropdownOpen.value;
};
const selectPlugin = (plugin: PluginInfo) => {
const selectPlugin = (plugin: PluginFullInfo) => {
if (activePlugin.value !== plugin.id) {
activePlugin.value = plugin.id;
pluginManager.setActivePlugin(plugin.id);
@@ -224,17 +225,7 @@ const fetchData = async () => {
const result = await pluginManager.search(query.value, currentPage.value, limit.value);
if (result && result.list) {
songs.value = result.list.map((item: any) => ({
id: String(item.id),
name: item.name,
artist: item.artist,
albumName: item.albumName,
duration: item.duration,
url: item.url || '',
picUrl: item.picUrl || '',
source: activePlugin.value,
type: 'Remote' as const
}));
songs.value = transformSearchResults(result.list);
total.value = result.songCount || result.total || 0;
} else {
total.value = 0;