diff --git a/docs/songlist-api.md b/docs/songlist-api.md new file mode 100644 index 0000000..fc7dcd3 --- /dev/null +++ b/docs/songlist-api.md @@ -0,0 +1,422 @@ +# 歌单管理 API 文档 + +本文档介绍了 CeruMusic 中歌单管理功能的使用方法,包括后端服务类和前端 API 接口。 + +## 概述 + +歌单管理系统提供了完整的歌单和歌曲管理功能,包括: + +- 📁 **歌单管理**:创建、删除、编辑、搜索歌单 +- 🎵 **歌曲管理**:添加、移除、搜索歌单中的歌曲 +- 📊 **统计分析**:获取歌单和歌曲的统计信息 +- 🔧 **数据维护**:验证和修复歌单数据完整性 +- ⚡ **批量操作**:支持批量删除和批量移除操作 + +## 架构设计 + +``` +前端 (Renderer Process) +├── src/renderer/src/api/songList.ts # 前端 API 封装 +├── src/renderer/src/examples/songListUsage.ts # 使用示例 +└── src/types/songList.ts # TypeScript 类型定义 + +主进程 (Main Process) +├── src/main/events/songList.ts # IPC 事件处理 +├── src/main/services/songList/ManageSongList.ts # 歌单管理服务 +└── src/main/services/songList/PlayListSongs.ts # 歌曲管理基类 +``` + +## 快速开始 + +### 1. 前端使用 + +```typescript +import songListAPI from '@/api/songList' + +// 创建歌单 +const result = await songListAPI.create('我的收藏', '我最喜欢的歌曲') +if (result.success) { + console.log('歌单创建成功,ID:', result.data?.id) +} + +// 获取所有歌单 +const playlists = await songListAPI.getAll() +if (playlists.success) { + console.log('歌单列表:', playlists.data) +} + +// 添加歌曲到歌单 +const songs = [/* 歌曲数据 */] +await songListAPI.addSongs(playlistId, songs) +``` + +### 2. 类型安全 + +所有 API 都提供了完整的 TypeScript 类型支持: + +```typescript +import type { IPCResponse, SongListStatistics } from '@/types/songList' + +const stats: IPCResponse = await songListAPI.getStatistics() +``` + +## API 参考 + +### 歌单管理 + +#### `create(name, description?, source?)` +创建新歌单 + +```typescript +const result = await songListAPI.create('我的收藏', '描述', 'local') +// 返回: { success: boolean, data?: { id: string }, error?: string } +``` + +#### `getAll()` +获取所有歌单 + +```typescript +const result = await songListAPI.getAll() +// 返回: { success: boolean, data?: SongList[], error?: string } +``` + +#### `getById(hashId)` +根据ID获取歌单 + +```typescript +const result = await songListAPI.getById('playlist-id') +// 返回: { success: boolean, data?: SongList | null, error?: string } +``` + +#### `delete(hashId)` +删除歌单 + +```typescript +const result = await songListAPI.delete('playlist-id') +// 返回: { success: boolean, error?: string } +``` + +#### `batchDelete(hashIds)` +批量删除歌单 + +```typescript +const result = await songListAPI.batchDelete(['id1', 'id2']) +// 返回: { success: boolean, data?: { success: string[], failed: string[] } } +``` + +#### `edit(hashId, updates)` +编辑歌单信息 + +```typescript +const result = await songListAPI.edit('playlist-id', { + name: '新名称', + description: '新描述' +}) +``` + +#### `search(keyword, source?)` +搜索歌单 + +```typescript +const result = await songListAPI.search('关键词', 'local') +// 返回: { success: boolean, data?: SongList[], error?: string } +``` + +### 歌曲管理 + +#### `addSongs(hashId, songs)` +添加歌曲到歌单 + +```typescript +const songs: Songs[] = [/* 歌曲数据 */] +const result = await songListAPI.addSongs('playlist-id', songs) +``` + +#### `removeSong(hashId, songmid)` +移除单首歌曲 + +```typescript +const result = await songListAPI.removeSong('playlist-id', 'song-id') +// 返回: { success: boolean, data?: boolean, error?: string } +``` + +#### `removeSongs(hashId, songmids)` +批量移除歌曲 + +```typescript +const result = await songListAPI.removeSongs('playlist-id', ['song1', 'song2']) +// 返回: { success: boolean, data?: { removed: number, notFound: number } } +``` + +#### `getSongs(hashId)` +获取歌单中的歌曲 + +```typescript +const result = await songListAPI.getSongs('playlist-id') +// 返回: { success: boolean, data?: readonly Songs[], error?: string } +``` + +#### `searchSongs(hashId, keyword)` +搜索歌单中的歌曲 + +```typescript +const result = await songListAPI.searchSongs('playlist-id', '关键词') +// 返回: { success: boolean, data?: Songs[], error?: string } +``` + +### 统计信息 + +#### `getStatistics()` +获取歌单统计信息 + +```typescript +const result = await songListAPI.getStatistics() +// 返回: { +// success: boolean, +// data?: { +// total: number, +// bySource: Record, +// lastUpdated: string +// } +// } +``` + +#### `getSongStatistics(hashId)` +获取歌单歌曲统计信息 + +```typescript +const result = await songListAPI.getSongStatistics('playlist-id') +// 返回: { +// success: boolean, +// data?: { +// total: number, +// bySinger: Record, +// byAlbum: Record, +// lastModified: string +// } +// } +``` + +### 数据维护 + +#### `validateIntegrity(hashId)` +验证歌单数据完整性 + +```typescript +const result = await songListAPI.validateIntegrity('playlist-id') +// 返回: { success: boolean, data?: { isValid: boolean, issues: string[] } } +``` + +#### `repairData(hashId)` +修复歌单数据 + +```typescript +const result = await songListAPI.repairData('playlist-id') +// 返回: { success: boolean, data?: { fixed: boolean, changes: string[] } } +``` + +### 便捷方法 + +#### `getPlaylistDetail(hashId)` +获取歌单详细信息(包含歌曲列表) + +```typescript +const result = await songListAPI.getPlaylistDetail('playlist-id') +// 返回: { +// playlist: SongList | null, +// songs: readonly Songs[], +// success: boolean, +// error?: string +// } +``` + +#### `checkAndRepair(hashId)` +检查并修复歌单数据 + +```typescript +const result = await songListAPI.checkAndRepair('playlist-id') +// 返回: { +// needsRepair: boolean, +// repairResult?: RepairResult, +// success: boolean, +// error?: string +// } +``` + +## 错误处理 + +所有 API 都返回统一的响应格式: + +```typescript +interface IPCResponse { + success: boolean // 操作是否成功 + data?: T // 返回的数据 + error?: string // 错误信息 + message?: string // 附加消息 + code?: string // 错误码 +} +``` + +### 错误码说明 + +| 错误码 | 说明 | +|--------|------| +| `INVALID_HASH_ID` | 无效的歌单ID | +| `PLAYLIST_NOT_FOUND` | 歌单不存在 | +| `EMPTY_NAME` | 歌单名称为空 | +| `CREATE_FAILED` | 创建失败 | +| `DELETE_FAILED` | 删除失败 | +| `EDIT_FAILED` | 编辑失败 | +| `READ_FAILED` | 读取失败 | +| `WRITE_FAILED` | 写入失败 | + +## 使用示例 + +### 完整的歌单管理流程 + +```typescript +import songListAPI from '@/api/songList' + +async function managePlaylist() { + try { + // 1. 创建歌单 + const createResult = await songListAPI.create('我的收藏', '我最喜欢的歌曲') + if (!createResult.success) { + throw new Error(createResult.error) + } + + const playlistId = createResult.data!.id + + // 2. 添加歌曲 + const songs = [ + { + songmid: 'song1', + name: '歌曲1', + singer: '歌手1', + albumName: '专辑1', + albumId: 'album1', + duration: 240, + source: 'local' + } + ] + + await songListAPI.addSongs(playlistId, songs) + + // 3. 获取歌单详情 + const detail = await songListAPI.getPlaylistDetail(playlistId) + console.log('歌单信息:', detail.playlist) + console.log('歌曲列表:', detail.songs) + + // 4. 搜索歌曲 + const searchResult = await songListAPI.searchSongs(playlistId, '歌曲') + console.log('搜索结果:', searchResult.data) + + // 5. 获取统计信息 + const stats = await songListAPI.getSongStatistics(playlistId) + console.log('统计信息:', stats.data) + + } catch (error) { + console.error('操作失败:', error) + } +} +``` + +### React 组件中的使用 + +```typescript +import React, { useState, useEffect } from 'react' +import songListAPI from '@/api/songList' +import type { SongList } from '@common/types/songList' + +const PlaylistManager: React.FC = () => { + const [playlists, setPlaylists] = useState([]) + const [loading, setLoading] = useState(false) + + // 加载歌单列表 + const loadPlaylists = async () => { + setLoading(true) + try { + const result = await songListAPI.getAll() + if (result.success) { + setPlaylists(result.data || []) + } + } catch (error) { + console.error('加载歌单失败:', error) + } finally { + setLoading(false) + } + } + + // 创建新歌单 + const createPlaylist = async (name: string) => { + const result = await songListAPI.create(name) + if (result.success) { + await loadPlaylists() // 重新加载列表 + } + } + + // 删除歌单 + const deletePlaylist = async (id: string) => { + const result = await songListAPI.safeDelete(id, async () => { + return confirm('确定要删除这个歌单吗?') + }) + + if (result.success) { + await loadPlaylists() // 重新加载列表 + } + } + + useEffect(() => { + loadPlaylists() + }, []) + + return ( +
+ {loading ? ( +
加载中...
+ ) : ( +
+ {playlists.map(playlist => ( +
+

{playlist.name}

+

{playlist.description}

+ +
+ ))} +
+ )} +
+ ) +} +``` + +## 性能优化建议 + +1. **批量操作**:使用 `batchDelete` 和 `removeSongs` 进行批量操作 +2. **数据缓存**:在前端适当缓存歌单列表,避免频繁请求 +3. **懒加载**:歌曲列表可以按需加载,不必一次性加载所有数据 +4. **错误恢复**:使用 `checkAndRepair` 定期检查数据完整性 + +## 注意事项 + +1. 所有 API 都是异步的,需要使用 `await` 或 `.then()` +2. 歌单 ID (`hashId`) 是唯一标识符,不要与数组索引混淆 +3. 歌曲 ID (`songmid`) 可能是字符串或数字类型 +4. 删除操作是不可逆的,建议使用 `safeDelete` 方法 +5. 大量数据操作时注意性能影响 + +## 更新日志 + +### v1.0.0 (2024-01-10) +- ✨ 初始版本发布 +- ✨ 完整的歌单管理功能 +- ✨ 批量操作支持 +- ✨ 数据完整性检查 +- ✨ TypeScript 类型支持 +- ✨ 详细的使用文档和示例 + +--- + +如有问题或建议,请提交 Issue 或 Pull Request。 \ No newline at end of file diff --git a/package.json b/package.json index 28d3fd5..7549c7f 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "ceru-music", - "version": "1.2.5", + "version": "1.2.6", "description": "一款简洁优雅的音乐播放器", "main": "./out/main/index.js", "author": "sqj,wldss,star", diff --git a/src/common/types/playList.ts b/src/common/types/playList.ts new file mode 100644 index 0000000..96c3c0a --- /dev/null +++ b/src/common/types/playList.ts @@ -0,0 +1,15 @@ +export default interface PlayList { + songmid: string|number + hash?: string + singer: string + name: string + albumName: string + albumId: string|number + source: string + interval: string + img: string + lrc: null | string + types: string[] + _types: Record + typeUrl: Record +} \ No newline at end of file diff --git a/src/common/types/songList.ts b/src/common/types/songList.ts new file mode 100644 index 0000000..3e5719a --- /dev/null +++ b/src/common/types/songList.ts @@ -0,0 +1,12 @@ +import PlayList from "./playList"; +export type Songs = PlayList; + +export type SongList = { + id: string //hashId 对应歌单文件名.json + name: string; // 歌单名 + createTime: string; + updateTime: string; + description: string; // 歌单描述 + coverImgUrl: string; //歌单封面 默认第一首歌的图片 + source: 'local'|'wy'|'tx'|'mg'|'kg'|'kw'; // 来源 +}; diff --git a/src/main/events/songList.ts b/src/main/events/songList.ts new file mode 100644 index 0000000..0f1470d --- /dev/null +++ b/src/main/events/songList.ts @@ -0,0 +1,300 @@ +import { ipcMain } from 'electron' +import ManageSongList, { SongListError } from '../services/songList/ManageSongList' +import type { SongList, Songs } from '@common/types/songList' + +// 创建新歌单 +ipcMain.handle('songlist:create', async (_, name: string, description: string = '', source: SongList['source']) => { + try { + const result = ManageSongList.createPlaylist(name, description, source) + return { success: true, data: result, message: '歌单创建成功' } + } catch (error) { + console.error('创建歌单失败:', error) + const message = error instanceof SongListError ? error.message : '创建歌单失败' + return { success: false, error: message, code: error instanceof SongListError ? error.code : 'UNKNOWN_ERROR' } + } +}) + +// 获取所有歌单 +ipcMain.handle('songlist:get-all', async () => { + try { + const songLists = ManageSongList.Read() + return { success: true, data: songLists } + } catch (error) { + console.error('获取歌单列表失败:', error) + const message = error instanceof SongListError ? error.message : '获取歌单列表失败' + return { success: false, error: message, code: error instanceof SongListError ? error.code : 'UNKNOWN_ERROR' } + } +}) + +// 根据ID获取歌单信息 +ipcMain.handle('songlist:get-by-id', async (_, hashId: string) => { + try { + const songList = ManageSongList.getById(hashId) + return { success: true, data: songList } + } catch (error) { + console.error('获取歌单信息失败:', error) + return { success: false, error: '获取歌单信息失败' } + } +}) + +// 删除歌单 +ipcMain.handle('songlist:delete', async (_, hashId: string) => { + try { + ManageSongList.deleteById(hashId) + return { success: true, message: '歌单删除成功' } + } catch (error) { + console.error('删除歌单失败:', error) + const message = error instanceof SongListError ? error.message : '删除歌单失败' + return { success: false, error: message, code: error instanceof SongListError ? error.code : 'UNKNOWN_ERROR' } + } +}) + +// 批量删除歌单 +ipcMain.handle('songlist:batch-delete', async (_, hashIds: string[]) => { + try { + const result = ManageSongList.batchDelete(hashIds) + return { + success: true, + data: result, + message: `成功删除 ${result.success.length} 个歌单,失败 ${result.failed.length} 个` + } + } catch (error) { + console.error('批量删除歌单失败:', error) + return { success: false, error: '批量删除歌单失败' } + } +}) + +// 编辑歌单信息 +ipcMain.handle('songlist:edit', async (_, hashId: string, updates: Partial>) => { + try { + ManageSongList.editById(hashId, updates) + return { success: true, message: '歌单信息更新成功' } + } catch (error) { + console.error('编辑歌单失败:', error) + const message = error instanceof SongListError ? error.message : '编辑歌单失败' + return { success: false, error: message, code: error instanceof SongListError ? error.code : 'UNKNOWN_ERROR' } + } +}) + +// 更新歌单封面 +ipcMain.handle('songlist:update-cover', async (_, hashId: string, coverImgUrl: string) => { + try { + ManageSongList.updateCoverImgById(hashId, coverImgUrl) + return { success: true, message: '封面更新成功' } + } catch (error) { + console.error('更新封面失败:', error) + const message = error instanceof SongListError ? error.message : '更新封面失败' + return { success: false, error: message, code: error instanceof SongListError ? error.code : 'UNKNOWN_ERROR' } + } +}) + +// 搜索歌单 +ipcMain.handle('songlist:search', async (_, keyword: string, source?: SongList['source']) => { + try { + const results = ManageSongList.search(keyword, source) + return { success: true, data: results } + } catch (error) { + console.error('搜索歌单失败:', error) + return { success: false, error: '搜索歌单失败', data: [] } + } +}) + +// 获取歌单统计信息 +ipcMain.handle('songlist:get-statistics', async () => { + try { + const statistics = ManageSongList.getStatistics() + return { success: true, data: statistics } + } catch (error) { + console.error('获取统计信息失败:', error) + return { success: false, error: '获取统计信息失败' } + } +}) + +// 检查歌单是否存在 +ipcMain.handle('songlist:exists', async (_, hashId: string) => { + try { + const exists = ManageSongList.exists(hashId) + return { success: true, data: exists } + } catch (error) { + console.error('检查歌单存在性失败:', error) + return { success: false, error: '检查歌单存在性失败', data: false } + } +}) + +// === 歌曲管理相关 IPC 事件 === + +// 添加歌曲到歌单 +ipcMain.handle('songlist:add-songs', async (_, hashId: string, songs: Songs[]) => { + try { + const instance = new ManageSongList(hashId) + instance.addSongs(songs) + return { success: true, message: `成功添加 ${songs.length} 首歌曲` } + } catch (error) { + console.error('添加歌曲失败:', error) + const message = error instanceof SongListError ? error.message : '添加歌曲失败' + return { success: false, error: message, code: error instanceof SongListError ? error.code : 'UNKNOWN_ERROR' } + } +}) + +// 从歌单移除歌曲 +ipcMain.handle('songlist:remove-song', async (_, hashId: string, songmid: string | number) => { + try { + const instance = new ManageSongList(hashId) + const removed = instance.removeSong(songmid) + return { + success: true, + data: removed, + message: removed ? '歌曲移除成功' : '歌曲不存在' + } + } catch (error) { + console.error('移除歌曲失败:', error) + const message = error instanceof SongListError ? error.message : '移除歌曲失败' + return { success: false, error: message, code: error instanceof SongListError ? error.code : 'UNKNOWN_ERROR' } + } +}) + +// 批量移除歌曲 +ipcMain.handle('songlist:remove-songs', async (_, hashId: string, songmids: (string | number)[]) => { + try { + const instance = new ManageSongList(hashId) + const result = instance.removeSongs(songmids) + return { + success: true, + data: result, + message: `成功移除 ${result.removed} 首歌曲,${result.notFound} 首未找到` + } + } catch (error) { + console.error('批量移除歌曲失败:', error) + const message = error instanceof SongListError ? error.message : '批量移除歌曲失败' + return { success: false, error: message, code: error instanceof SongListError ? error.code : 'UNKNOWN_ERROR' } + } +}) + +// 清空歌单 +ipcMain.handle('songlist:clear-songs', async (_, hashId: string) => { + try { + const instance = new ManageSongList(hashId) + instance.clearSongs() + return { success: true, message: '歌单已清空' } + } catch (error) { + console.error('清空歌单失败:', error) + const message = error instanceof SongListError ? error.message : '清空歌单失败' + return { success: false, error: message, code: error instanceof SongListError ? error.code : 'UNKNOWN_ERROR' } + } +}) + +// 获取歌单中的歌曲列表 +ipcMain.handle('songlist:get-songs', async (_, hashId: string) => { + try { + const instance = new ManageSongList(hashId) + const songs = instance.getSongs() + return { success: true, data: songs } + } catch (error) { + console.error('获取歌曲列表失败:', error) + const message = error instanceof SongListError ? error.message : '获取歌曲列表失败' + return { success: false, error: message, code: error instanceof SongListError ? error.code : 'UNKNOWN_ERROR' } + } +}) + +// 获取歌单歌曲数量 +ipcMain.handle('songlist:get-song-count', async (_, hashId: string) => { + try { + const instance = new ManageSongList(hashId) + const count = instance.getCount() + return { success: true, data: count } + } catch (error) { + console.error('获取歌曲数量失败:', error) + const message = error instanceof SongListError ? error.message : '获取歌曲数量失败' + return { success: false, error: message, code: error instanceof SongListError ? error.code : 'UNKNOWN_ERROR' } + } +}) + +// 检查歌曲是否在歌单中 +ipcMain.handle('songlist:has-song', async (_, hashId: string, songmid: string | number) => { + try { + const instance = new ManageSongList(hashId) + const hasSong = instance.hasSong(songmid) + return { success: true, data: hasSong } + } catch (error) { + console.error('检查歌曲存在性失败:', error) + return { success: false, error: '检查歌曲存在性失败', data: false } + } +}) + +// 根据ID获取歌曲 +ipcMain.handle('songlist:get-song', async (_, hashId: string, songmid: string | number) => { + try { + const instance = new ManageSongList(hashId) + const song = instance.getSong(songmid) + return { success: true, data: song } + } catch (error) { + console.error('获取歌曲失败:', error) + return { success: false, error: '获取歌曲失败', data: null } + } +}) + +// 搜索歌单中的歌曲 +ipcMain.handle('songlist:search-songs', async (_, hashId: string, keyword: string) => { + try { + const instance = new ManageSongList(hashId) + const results = instance.searchSongs(keyword) + return { success: true, data: results } + } catch (error) { + console.error('搜索歌曲失败:', error) + return { success: false, error: '搜索歌曲失败', data: [] } + } +}) + +// 获取歌单歌曲统计信息 +ipcMain.handle('songlist:get-song-statistics', async (_, hashId: string) => { + try { + const instance = new ManageSongList(hashId) + const statistics = instance.getStatistics() + return { success: true, data: statistics } + } catch (error) { + console.error('获取歌曲统计信息失败:', error) + return { success: false, error: '获取歌曲统计信息失败' } + } +}) + +// 验证歌单完整性 +ipcMain.handle('songlist:validate-integrity', async (_, hashId: string) => { + try { + const instance = new ManageSongList(hashId) + const result = instance.validateIntegrity() + return { success: true, data: result } + } catch (error) { + console.error('验证歌单完整性失败:', error) + return { success: false, error: '验证歌单完整性失败' } + } +}) + +// 修复歌单数据 +ipcMain.handle('songlist:repair-data', async (_, hashId: string) => { + try { + const instance = new ManageSongList(hashId) + const result = instance.repairData() + return { + success: true, + data: result, + message: result.fixed ? `数据修复完成: ${result.changes.join(', ')}` : '数据无需修复' + } + } catch (error) { + console.error('修复歌单数据失败:', error) + const message = error instanceof SongListError ? error.message : '修复歌单数据失败' + return { success: false, error: message, code: error instanceof SongListError ? error.code : 'UNKNOWN_ERROR' } + } +}) + +// 强制保存歌单 +ipcMain.handle('songlist:force-save', async (_, hashId: string) => { + try { + const instance = new ManageSongList(hashId) + instance.forceSave() + return { success: true, message: '歌单保存成功' } + } catch (error) { + console.error('强制保存歌单失败:', error) + const message = error instanceof SongListError ? error.message : '强制保存歌单失败' + return { success: false, error: message, code: error instanceof SongListError ? error.code : 'UNKNOWN_ERROR' } + } +}) \ No newline at end of file diff --git a/src/main/index.ts b/src/main/index.ts index 1e8e246..3617bab 100644 --- a/src/main/index.ts +++ b/src/main/index.ts @@ -218,6 +218,7 @@ ipcMain.handle('get-app-version', () => { aiEvents(mainWindow) import './events/musicCache' +import './events/songList' import { registerAutoUpdateEvents, initAutoUpdateForWindow } from './events/autoUpdate' // This method will be called when Electron has finished diff --git a/src/main/services/musicSdk/service.ts b/src/main/services/musicSdk/service.ts index 7191488..790047b 100644 --- a/src/main/services/musicSdk/service.ts +++ b/src/main/services/musicSdk/service.ts @@ -161,6 +161,26 @@ function main(source: string) { message: '下载成功', path: songPath } + }, + + async parsePlaylistId({url}: {url: string}) { + try { + return await Api.songList.handleParseId(url) + } catch (e: any) { + return { + error: '解析歌单链接失败 ' + (e.error || e.message || e) + } + } + }, + + async getPlaylistDetailById(id: string, page: number = 1) { + try { + return await Api.songList.getListDetail(id, page) + } catch (e: any) { + return { + error: '获取歌单详情失败 ' + (e.error || e.message || e) + } + } } } } diff --git a/src/main/services/songList/ManageSongList.ts b/src/main/services/songList/ManageSongList.ts new file mode 100644 index 0000000..269a3e5 --- /dev/null +++ b/src/main/services/songList/ManageSongList.ts @@ -0,0 +1,730 @@ +import type { SongList, Songs } from "@common/types/songList"; +import PlayListSongs from "./PlayListSongs"; +import fs from "fs"; +import path from "path"; +import crypto from "crypto"; +import { getAppDirPath } from "../../utils/path"; + +// 常量定义 +const DEFAULT_COVER_IDENTIFIER = 'default-cover'; +const SONGLIST_DIR = 'songList'; +const INDEX_FILE = 'index.json'; + +// 错误类型定义 +class SongListError extends Error { + constructor(message: string, public code?: string) { + super(message); + this.name = 'SongListError'; + } +} + +// 工具函数类 +class SongListUtils { + /** + * 获取默认封面标识符 + */ + static getDefaultCoverUrl(): string { + return DEFAULT_COVER_IDENTIFIER; + } + + /** + * 获取歌单管理入口文件路径 + */ + static getSongListIndexPath(): string { + return path.join(getAppDirPath('userData'), SONGLIST_DIR, INDEX_FILE); + } + + /** + * 获取歌单文件路径 + */ + static getSongListFilePath(hashId: string): string { + return path.join(getAppDirPath('userData'), SONGLIST_DIR, `${hashId}.json`); + } + + /** + * 确保目录存在 + */ + static ensureDirectoryExists(dirPath: string): void { + if (!fs.existsSync(dirPath)) { + fs.mkdirSync(dirPath, { recursive: true }); + } + } + + /** + * 生成唯一hashId + */ + static generateUniqueId(name: string): string { + return crypto.createHash('md5') + .update(`${name}_${Date.now()}_${Math.random()}`) + .digest('hex'); + } + + /** + * 验证歌曲封面URL是否有效 + */ + static isValidCoverUrl(url: string | undefined | null): boolean { + return Boolean(url && url.trim() !== '' && url !== DEFAULT_COVER_IDENTIFIER); + } + + /** + * 验证hashId格式 + */ + static isValidHashId(hashId: string): boolean { + return Boolean(hashId && typeof hashId === 'string' && hashId.trim().length > 0); + } + + /** + * 安全的JSON解析 + */ + static safeJsonParse(content: string, defaultValue: T): T { + try { + return JSON.parse(content) as T; + } catch { + return defaultValue; + } + } +} + +export default class ManageSongList extends PlayListSongs { + private readonly hashId: string; + + constructor(hashId: string) { + if (!SongListUtils.isValidHashId(hashId)) { + throw new SongListError('无效的歌单ID', 'INVALID_HASH_ID'); + } + + super(hashId); + this.hashId = hashId.trim(); + } + + /** + * 静态方法:创建新歌单 + * @param name 歌单名称 + * @param description 歌单描述 + * @param source 歌单来源 + * @returns 包含hashId的对象 (id字段就是hashId) + */ + static createPlaylist( + name: string, + description: string = '', + source: SongList['source'] + ): { id: string } { + // 参数验证 + if (!name?.trim()) { + throw new SongListError('歌单名称不能为空', 'EMPTY_NAME'); + } + + if (!source) { + throw new SongListError('歌单来源不能为空', 'EMPTY_SOURCE'); + } + + try { + const id = SongListUtils.generateUniqueId(name); + const now = new Date().toISOString(); + + const songListInfo: SongList = { + id, + name: name.trim(), + createTime: now, + updateTime: now, + description: description?.trim() || '', + coverImgUrl: SongListUtils.getDefaultCoverUrl(), + source + }; + + // 创建歌单文件 + ManageSongList.createSongListFile(id); + + // 更新入口文件 + ManageSongList.updateIndexFile(songListInfo, 'add'); + + // 验证歌单可以正常实例化 + try { + new ManageSongList(id); + // 如果能成功创建实例,说明文件创建成功 + } catch (verifyError) { + console.error('歌单创建验证失败:', verifyError); + // 清理已创建的文件 + try { + const filePath = SongListUtils.getSongListFilePath(id); + if (fs.existsSync(filePath)) { + fs.unlinkSync(filePath); + } + } catch (cleanupError) { + console.error('清理失败的歌单文件时出错:', cleanupError); + } + throw new SongListError('歌单创建后验证失败', 'CREATION_VERIFICATION_FAILED'); + } + + return { id }; + } catch (error) { + console.error('创建歌单失败:', error); + if (error instanceof SongListError) { + throw error; + } + throw new SongListError(`创建歌单失败: ${error instanceof Error ? error.message : '未知错误'}`, 'CREATE_FAILED'); + } + } + + /** + * 创建歌单文件 + * @param hashId 歌单hashId + */ + private static createSongListFile(hashId: string): void { + const songListFilePath = SongListUtils.getSongListFilePath(hashId); + const dir = path.dirname(songListFilePath); + + SongListUtils.ensureDirectoryExists(dir); + + try { + // 使用原子性写入确保文件完整性 + const tempPath = `${songListFilePath}.tmp`; + const content = JSON.stringify([], null, 2); + + fs.writeFileSync(tempPath, content); + fs.renameSync(tempPath, songListFilePath); + + // 确保文件确实存在且可读 + if (!fs.existsSync(songListFilePath)) { + throw new Error('文件创建后验证失败'); + } + + // 验证文件内容 + const verifyContent = fs.readFileSync(songListFilePath, 'utf-8'); + JSON.parse(verifyContent); // 确保内容是有效的JSON + + } catch (error) { + throw new SongListError(`创建歌单文件失败: ${error instanceof Error ? error.message : '未知错误'}`, 'FILE_CREATE_FAILED'); + } + } + + /** + * 删除当前歌单 + */ + delete(): void { + const hashId = this.getHashId(); + + try { + // 检查歌单是否存在 + if (!ManageSongList.exists(hashId)) { + throw new SongListError('歌单不存在', 'PLAYLIST_NOT_FOUND'); + } + + // 删除歌单文件 + const filePath = SongListUtils.getSongListFilePath(hashId); + if (fs.existsSync(filePath)) { + fs.unlinkSync(filePath); + } + + // 从入口文件中移除 + ManageSongList.updateIndexFile({ id: hashId } as SongList, 'remove'); + } catch (error) { + console.error('删除歌单失败:', error); + if (error instanceof SongListError) { + throw error; + } + throw new SongListError(`删除歌单失败: ${error instanceof Error ? error.message : '未知错误'}`, 'DELETE_FAILED'); + } + } + + /** + * 修改当前歌单信息 + * @param updates 要更新的字段 + */ + edit(updates: Partial>): void { + if (!updates || Object.keys(updates).length === 0) { + throw new SongListError('更新内容不能为空', 'EMPTY_UPDATES'); + } + + const hashId = this.getHashId(); + + try { + const songLists = ManageSongList.readIndexFile(); + const index = songLists.findIndex(item => item.id === hashId); + + if (index === -1) { + throw new SongListError('歌单不存在', 'PLAYLIST_NOT_FOUND'); + } + + // 验证和清理更新数据 + const cleanUpdates = ManageSongList.validateAndCleanUpdates(updates); + + // 更新歌单信息 + songLists[index] = { + ...songLists[index], + ...cleanUpdates, + updateTime: new Date().toISOString() + }; + + // 保存到入口文件 + ManageSongList.writeIndexFile(songLists); + } catch (error) { + console.error('修改歌单失败:', error); + if (error instanceof SongListError) { + throw error; + } + throw new SongListError(`修改歌单失败: ${error instanceof Error ? error.message : '未知错误'}`, 'EDIT_FAILED'); + } + } + + /** + * 获取当前歌单的hashId + * @returns hashId + */ + private getHashId(): string { + return this.hashId; + } + + /** + * 验证和清理更新数据 + * @param updates 原始更新数据 + * @returns 清理后的更新数据 + */ + private static validateAndCleanUpdates( + updates: Partial> + ): Partial> { + const cleanUpdates: Partial> = {}; + + // 验证歌单名称 + if (updates.name !== undefined) { + const trimmedName = updates.name.trim(); + if (!trimmedName) { + throw new SongListError('歌单名称不能为空', 'EMPTY_NAME'); + } + cleanUpdates.name = trimmedName; + } + + // 处理描述 + if (updates.description !== undefined) { + cleanUpdates.description = updates.description?.trim() || ''; + } + + // 处理封面URL + if (updates.coverImgUrl !== undefined) { + cleanUpdates.coverImgUrl = updates.coverImgUrl || SongListUtils.getDefaultCoverUrl(); + } + + // 处理来源 + if (updates.source !== undefined) { + if (!updates.source) { + throw new SongListError('歌单来源不能为空', 'EMPTY_SOURCE'); + } + cleanUpdates.source = updates.source; + } + + return cleanUpdates; + } + + /** + * 读取歌单列表 + * @returns 歌单列表数组 + */ + static Read(): SongList[] { + try { + return ManageSongList.readIndexFile(); + } catch (error) { + console.error('读取歌单列表失败:', error); + if (error instanceof SongListError) { + throw error; + } + throw new SongListError(`读取歌单列表失败: ${error instanceof Error ? error.message : '未知错误'}`, 'READ_FAILED'); + } + } + + /** + * 根据hashId获取单个歌单信息 + * @param hashId 歌单hashId + * @returns 歌单信息或null + */ + static getById(hashId: string): SongList | null { + if (!SongListUtils.isValidHashId(hashId)) { + return null; + } + + try { + const songLists = ManageSongList.readIndexFile(); + return songLists.find(item => item.id === hashId) || null; + } catch (error) { + console.error('获取歌单信息失败:', error); + return null; + } + } + + /** + * 读取入口文件 + * @returns 歌单列表数组 + */ + private static readIndexFile(): SongList[] { + const indexPath = SongListUtils.getSongListIndexPath(); + + if (!fs.existsSync(indexPath)) { + ManageSongList.initializeIndexFile(); + return []; + } + + try { + const content = fs.readFileSync(indexPath, 'utf-8'); + const parsed = SongListUtils.safeJsonParse(content, []); + + // 验证数据格式 + if (!Array.isArray(parsed)) { + console.warn('入口文件格式错误,重新初始化'); + ManageSongList.initializeIndexFile(); + return []; + } + + return parsed as SongList[]; + } catch (error) { + console.error('解析入口文件失败:', error); + // 备份损坏的文件并重新初始化 + ManageSongList.backupCorruptedFile(indexPath); + ManageSongList.initializeIndexFile(); + return []; + } + } + + /** + * 备份损坏的文件 + * @param filePath 文件路径 + */ + private static backupCorruptedFile(filePath: string): void { + try { + const backupPath = `${filePath}.backup.${Date.now()}`; + fs.copyFileSync(filePath, backupPath); + console.log(`已备份损坏的文件到: ${backupPath}`); + } catch (error) { + console.error('备份损坏文件失败:', error); + } + } + + /** + * 初始化入口文件 + */ + private static initializeIndexFile(): void { + const indexPath = SongListUtils.getSongListIndexPath(); + const dir = path.dirname(indexPath); + + SongListUtils.ensureDirectoryExists(dir); + + try { + fs.writeFileSync(indexPath, JSON.stringify([], null, 2)); + } catch (error) { + throw new SongListError(`初始化入口文件失败: ${error instanceof Error ? error.message : '未知错误'}`, 'INIT_FAILED'); + } + } + + /** + * 写入入口文件 + * @param songLists 歌单列表 + */ + private static writeIndexFile(songLists: SongList[]): void { + if (!Array.isArray(songLists)) { + throw new SongListError('歌单列表必须是数组格式', 'INVALID_DATA_FORMAT'); + } + + const indexPath = SongListUtils.getSongListIndexPath(); + const dir = path.dirname(indexPath); + + SongListUtils.ensureDirectoryExists(dir); + + try { + // 先写入临时文件,再重命名,确保原子性操作 + const tempPath = `${indexPath}.tmp`; + fs.writeFileSync(tempPath, JSON.stringify(songLists, null, 2)); + fs.renameSync(tempPath, indexPath); + } catch (error) { + console.error('写入入口文件失败:', error); + throw new SongListError(`写入入口文件失败: ${error instanceof Error ? error.message : '未知错误'}`, 'WRITE_FAILED'); + } + } + + /** + * 更新入口文件 + * @param songListInfo 歌单信息 + * @param action 操作类型 + */ + private static updateIndexFile( + songListInfo: SongList, + action: 'add' | 'remove' + ): void { + const songLists = ManageSongList.readIndexFile(); + + switch (action) { + case 'add': + // 检查是否已存在,避免重复添加 + if (!songLists.some(item => item.id === songListInfo.id)) { + songLists.push(songListInfo); + } + break; + + case 'remove': + const index = songLists.findIndex(item => item.id === songListInfo.id); + if (index !== -1) { + songLists.splice(index, 1); + } + break; + + default: + throw new SongListError(`不支持的操作类型: ${action}`, 'INVALID_ACTION'); + } + + ManageSongList.writeIndexFile(songLists); + } + + /** + * 更新当前歌单封面图片URL + * @param coverImgUrl 封面图片URL + */ + updateCoverImg(coverImgUrl: string): void { + try { + const finalCoverUrl = coverImgUrl || SongListUtils.getDefaultCoverUrl(); + this.edit({ coverImgUrl: finalCoverUrl }); + } catch (error) { + console.error('更新封面失败:', error); + if (error instanceof SongListError) { + throw error; + } + throw new SongListError(`更新封面失败: ${error instanceof Error ? error.message : '未知错误'}`, 'UPDATE_COVER_FAILED'); + } + } + + /** + * 重写父类的addSongs方法,添加自动设置封面功能 + * @param songs 要添加的歌曲列表 + */ + addSongs(songs: Songs[]): void { + if (!Array.isArray(songs) || songs.length === 0) { + return; + } + + // 调用父类方法添加歌曲 + super.addSongs(songs); + + // 异步更新封面,不阻塞主要功能 + setImmediate(() => { + this.updateCoverIfNeeded(songs); + }); + } + + /** + * 检查并更新封面图片 + * @param newSongs 新添加的歌曲列表 + */ + private updateCoverIfNeeded(newSongs: Songs[]): void { + try { + const currentPlaylist = ManageSongList.getById(this.hashId); + + if (!currentPlaylist) { + console.warn(`歌单 ${this.hashId} 不存在,跳过封面更新`); + return; + } + + const shouldUpdateCover = this.shouldUpdateCover(currentPlaylist.coverImgUrl); + + if (shouldUpdateCover) { + const validCoverUrl = this.findValidCoverFromSongs(newSongs); + + if (validCoverUrl) { + this.updateCoverImg(validCoverUrl); + } else if (!currentPlaylist.coverImgUrl || currentPlaylist.coverImgUrl === SongListUtils.getDefaultCoverUrl()) { + // 如果没有找到有效封面且当前也没有封面,设置默认封面 + this.updateCoverImg(SongListUtils.getDefaultCoverUrl()); + } + } + } catch (error) { + console.error('更新封面失败:', error); + // 不抛出错误,避免影响添加歌曲的主要功能 + } + } + + /** + * 判断是否应该更新封面 + * @param currentCoverUrl 当前封面URL + * @returns 是否应该更新 + */ + private shouldUpdateCover(currentCoverUrl: string): boolean { + return !currentCoverUrl || currentCoverUrl === SongListUtils.getDefaultCoverUrl(); + } + + /** + * 从歌曲列表中查找有效的封面图片 + * @param songs 歌曲列表 + * @returns 有效的封面URL或null + */ + private findValidCoverFromSongs(songs: Songs[]): string | null { + // 优先检查新添加的歌曲 + for (const song of songs) { + if (SongListUtils.isValidCoverUrl(song.img)) { + return song.img; + } + } + + // 如果新添加的歌曲都没有封面,检查当前歌单中的所有歌曲 + try { + for (const song of this.list) { + if (SongListUtils.isValidCoverUrl(song.img)) { + return song.img; + } + } + } catch (error) { + console.error('获取歌单歌曲列表失败:', error); + } + + return null; + } + + /** + * 检查歌单是否存在 + * @param hashId 歌单hashId + * @returns 是否存在 + */ + static exists(hashId: string): boolean { + if (!SongListUtils.isValidHashId(hashId)) { + return false; + } + + try { + const songLists = ManageSongList.readIndexFile(); + return songLists.some(item => item.id === hashId); + } catch (error) { + console.error('检查歌单存在性失败:', error); + return false; + } + } + + /** + * 获取歌单统计信息 + * @returns 统计信息 + */ + static getStatistics(): { total: number; bySource: Record; lastUpdated: string } { + try { + const songLists = ManageSongList.readIndexFile(); + const bySource: Record = {}; + + songLists.forEach(playlist => { + const source = playlist.source || 'unknown'; + bySource[source] = (bySource[source] || 0) + 1; + }); + + return { + total: songLists.length, + bySource, + lastUpdated: new Date().toISOString() + }; + } catch (error) { + console.error('获取统计信息失败:', error); + return { + total: 0, + bySource: {}, + lastUpdated: new Date().toISOString() + }; + } + } + + /** + * 获取当前歌单信息 + * @returns 歌单信息或null + */ + getPlaylistInfo(): SongList | null { + return ManageSongList.getById(this.hashId); + } + + /** + * 批量操作:删除多个歌单 + * @param hashIds 歌单ID数组 + * @returns 操作结果 + */ + static batchDelete(hashIds: string[]): { success: string[]; failed: string[] } { + const result = { success: [] as string[], failed: [] as string[] }; + + for (const hashId of hashIds) { + try { + ManageSongList.deleteById(hashId); + result.success.push(hashId); + } catch (error) { + console.error(`删除歌单 ${hashId} 失败:`, error); + result.failed.push(hashId); + } + } + + return result; + } + + /** + * 搜索歌单 + * @param keyword 搜索关键词 + * @param source 可选的来源筛选 + * @returns 匹配的歌单列表 + */ + static search(keyword: string, source?: SongList['source']): SongList[] { + if (!keyword?.trim()) { + return []; + } + + try { + const songLists = ManageSongList.readIndexFile(); + const lowerKeyword = keyword.toLowerCase(); + + return songLists.filter(playlist => { + const matchesKeyword = playlist.name.toLowerCase().includes(lowerKeyword) || + playlist.description.toLowerCase().includes(lowerKeyword); + const matchesSource = !source || playlist.source === source; + + return matchesKeyword && matchesSource; + }); + } catch (error) { + console.error('搜索歌单失败:', error); + return []; + } + } + + // 静态方法别名,用于删除和编辑指定hashId的歌单 + /** + * 静态方法:删除指定歌单 + * @param hashId 歌单hashId + */ + static deleteById(hashId: string): void { + if (!SongListUtils.isValidHashId(hashId)) { + throw new SongListError('无效的歌单ID', 'INVALID_HASH_ID'); + } + + const instance = new ManageSongList(hashId); + instance.delete(); + } + + /** + * 静态方法:编辑指定歌单 + * @param hashId 歌单hashId + * @param updates 要更新的字段 + */ + static editById(hashId: string, updates: Partial>): void { + if (!SongListUtils.isValidHashId(hashId)) { + throw new SongListError('无效的歌单ID', 'INVALID_HASH_ID'); + } + + const instance = new ManageSongList(hashId); + instance.edit(updates); + } + + /** + * 静态方法:更新指定歌单封面 + * @param hashId 歌单hashId + * @param coverImgUrl 封面图片URL + */ + static updateCoverImgById(hashId: string, coverImgUrl: string): void { + if (!SongListUtils.isValidHashId(hashId)) { + throw new SongListError('无效的歌单ID', 'INVALID_HASH_ID'); + } + + const instance = new ManageSongList(hashId); + instance.updateCoverImg(coverImgUrl); + } + + // 保持向后兼容的别名方法 + static Delete = ManageSongList.deleteById; + static Edit = ManageSongList.editById; + static read = ManageSongList.Read; +} + +// 导出错误类供外部使用 +export { SongListError }; \ No newline at end of file diff --git a/src/main/services/songList/PlayListSongs.ts b/src/main/services/songList/PlayListSongs.ts new file mode 100644 index 0000000..c2fbfeb --- /dev/null +++ b/src/main/services/songList/PlayListSongs.ts @@ -0,0 +1,435 @@ +import type { Songs as SongItem } from "@common/types/songList"; +import fs from "fs"; +import path from "path"; +import { getAppDirPath } from "../../utils/path"; + +// 错误类定义 +class PlayListError extends Error { + constructor(message: string, public code?: string) { + super(message); + this.name = 'PlayListError'; + } +} + +// 工具函数类 +class PlayListUtils { + /** + * 获取歌单文件路径 + */ + static getFilePath(hashId: string): string { + if (!hashId || typeof hashId !== 'string' || !hashId.trim()) { + throw new PlayListError('无效的歌单ID', 'INVALID_HASH_ID'); + } + return path.join(getAppDirPath('userData'), 'songList', `${hashId.trim()}.json`); + } + + /** + * 确保目录存在 + */ + static ensureDirectoryExists(dirPath: string): void { + if (!fs.existsSync(dirPath)) { + fs.mkdirSync(dirPath, { recursive: true }); + } + } + + /** + * 安全的JSON解析 + */ + static safeJsonParse(content: string, defaultValue: T): T { + try { + const parsed = JSON.parse(content); + return parsed as T; + } catch { + return defaultValue; + } + } + + /** + * 安全的JSON解析(专门用于数组) + */ + static safeJsonParseArray(content: string, defaultValue: T[]): T[] { + try { + const parsed = JSON.parse(content); + return Array.isArray(parsed) ? parsed : defaultValue; + } catch { + return defaultValue; + } + } + + /** + * 验证歌曲对象 + */ + static isValidSong(song: any): song is SongItem { + return song && + typeof song === 'object' && + (typeof song.songmid === 'string' || typeof song.songmid === 'number') && + String(song.songmid).trim().length > 0; + } + + /** + * 去重歌曲列表 + */ + static deduplicateSongs(songs: SongItem[]): SongItem[] { + const seen = new Set(); + return songs.filter(song => { + const songmidStr = String(song.songmid); + if (seen.has(songmidStr)) { + return false; + } + seen.add(songmidStr); + return true; + }); + } +} + +export default class PlayListSongs { + protected readonly filePath: string; + protected list: SongItem[]; + private isDirty: boolean = false; + + constructor(hashId: string) { + this.filePath = PlayListUtils.getFilePath(hashId); + this.list = []; + this.initList(); + } + + /** + * 初始化歌单列表 + */ + private initList(): void { + // 增加重试机制,处理文件创建的时序问题 + const maxRetries = 3; + const retryDelay = 100; // 100ms + + for (let attempt = 0; attempt < maxRetries; attempt++) { + try { + if (!fs.existsSync(this.filePath)) { + if (attempt < maxRetries - 1) { + // 等待一段时间后重试 + const start = Date.now(); + while (Date.now() - start < retryDelay) { + // 简单的同步等待 + } + continue; + } + throw new PlayListError('歌单文件不存在', 'FILE_NOT_FOUND'); + } + + const content = fs.readFileSync(this.filePath, 'utf-8'); + const parsed = PlayListUtils.safeJsonParseArray(content, []); + + // 验证和清理数据 + this.list = parsed.filter(PlayListUtils.isValidSong); + + // 如果数据被清理过,标记为需要保存 + if (this.list.length !== parsed.length) { + this.isDirty = true; + console.warn(`歌单文件包含无效数据,已自动清理 ${parsed.length - this.list.length} 条无效记录`); + } + + // 成功读取,退出重试循环 + return; + + } catch (error) { + if (attempt < maxRetries - 1) { + console.warn(`读取歌单文件失败,第 ${attempt + 1} 次重试:`, error); + // 等待一段时间后重试 + const start = Date.now(); + while (Date.now() - start < retryDelay) { + // 简单的同步等待 + } + continue; + } + + console.error('读取歌单文件失败:', error); + throw new PlayListError(`读取歌单失败: ${error instanceof Error ? error.message : '未知错误'}`, 'READ_FAILED'); + } + } + } + + /** + * 检查歌单文件是否存在 + */ + static hasListFile(hashId: string): boolean { + try { + const filePath = PlayListUtils.getFilePath(hashId); + return fs.existsSync(filePath); + } catch { + return false; + } + } + + /** + * 添加歌曲到歌单 + */ + addSongs(songs: SongItem[]): void { + if (!Array.isArray(songs) || songs.length === 0) { + return; + } + + // 验证和过滤有效歌曲 + const validSongs = songs.filter(PlayListUtils.isValidSong); + if (validSongs.length === 0) { + console.warn('没有有效的歌曲可添加'); + return; + } + + // 使用 Set 提高查重性能,统一转换为字符串进行比较 + const existingSongMids = new Set(this.list.map(song => String(song.songmid))); + + // 添加不重复的歌曲 + const newSongs = validSongs.filter(song => !existingSongMids.has(String(song.songmid))); + + if (newSongs.length > 0) { + this.list.push(...newSongs); + this.isDirty = true; + this.saveToFile(); + + console.log(`成功添加 ${newSongs.length} 首歌曲,跳过 ${validSongs.length - newSongs.length} 首重复歌曲`); + } else { + console.log('所有歌曲都已存在,未添加任何歌曲'); + } + } + + /** + * 从歌单中移除歌曲 + */ + removeSong(songmid: string | number): boolean { + if (!songmid && songmid !== 0) { + throw new PlayListError('无效的歌曲ID', 'INVALID_SONG_ID'); + } + + const songmidStr = String(songmid); + const index = this.list.findIndex(item => String(item.songmid) === songmidStr); + if (index !== -1) { + this.list.splice(index, 1); + this.isDirty = true; + this.saveToFile(); + return true; + } + return false; + } + + /** + * 批量移除歌曲 + */ + removeSongs(songmids: (string | number)[]): { removed: number; notFound: number } { + if (!Array.isArray(songmids) || songmids.length === 0) { + return { removed: 0, notFound: 0 }; + } + + const validSongMids = songmids.filter(id => (id || id === 0) && (typeof id === 'string' || typeof id === 'number')); + const songMidSet = new Set(validSongMids.map(id => String(id))); + + const initialLength = this.list.length; + this.list = this.list.filter(song => !songMidSet.has(String(song.songmid))); + + const removedCount = initialLength - this.list.length; + const notFoundCount = validSongMids.length - removedCount; + + if (removedCount > 0) { + this.isDirty = true; + this.saveToFile(); + } + + return { removed: removedCount, notFound: notFoundCount }; + } + + /** + * 清空歌单 + */ + clearSongs(): void { + if (this.list.length > 0) { + this.list = []; + this.isDirty = true; + this.saveToFile(); + } + } + + /** + * 保存到文件 + */ + private saveToFile(): void { + if (!this.isDirty) { + return; + } + + try { + const dir = path.dirname(this.filePath); + PlayListUtils.ensureDirectoryExists(dir); + + // 原子性写入:先写临时文件,再重命名 + const tempPath = `${this.filePath}.tmp`; + const content = JSON.stringify(this.list, null, 2); + + fs.writeFileSync(tempPath, content); + fs.renameSync(tempPath, this.filePath); + + this.isDirty = false; + } catch (error) { + console.error('保存歌单文件失败:', error); + throw new PlayListError(`保存歌单失败: ${error instanceof Error ? error.message : '未知错误'}`, 'SAVE_FAILED'); + } + } + + /** + * 强制保存到文件 + */ + forceSave(): void { + this.isDirty = true; + this.saveToFile(); + } + + /** + * 获取歌曲列表 + */ + getSongs(): readonly SongItem[] { + return Object.freeze([...this.list]); + } + + /** + * 获取歌曲数量 + */ + getCount(): number { + return this.list.length; + } + + /** + * 检查歌曲是否存在 + */ + hasSong(songmid: string | number): boolean { + if (!songmid && songmid !== 0) { + return false; + } + const songmidStr = String(songmid); + return this.list.some(song => String(song.songmid) === songmidStr); + } + + /** + * 根据songmid获取歌曲 + */ + getSong(songmid: string | number): SongItem | null { + if (!songmid && songmid !== 0) { + return null; + } + const songmidStr = String(songmid); + return this.list.find(song => String(song.songmid) === songmidStr) || null; + } + + /** + * 搜索歌曲 + */ + searchSongs(keyword: string): SongItem[] { + if (!keyword || typeof keyword !== 'string') { + return []; + } + + const lowerKeyword = keyword.toLowerCase(); + return this.list.filter(song => + song.name?.toLowerCase().includes(lowerKeyword) || + song.singer?.toLowerCase().includes(lowerKeyword) || + song.albumName?.toLowerCase().includes(lowerKeyword) + ); + } + + /** + * 获取歌单统计信息 + */ + getStatistics(): { + total: number; + bySinger: Record; + byAlbum: Record; + lastModified: string; + } { + const bySinger: Record = {}; + const byAlbum: Record = {}; + + this.list.forEach(song => { + // 统计歌手 + if (song.singer) { + const singerName = String(song.singer); + bySinger[singerName] = (bySinger[singerName] || 0) + 1; + } + + // 统计专辑 + if (song.albumName) { + const albumName = String(song.albumName); + byAlbum[albumName] = (byAlbum[albumName] || 0) + 1; + } + }); + + return { + total: this.list.length, + bySinger, + byAlbum, + lastModified: new Date().toISOString() + }; + } + + /** + * 验证歌单完整性 + */ + validateIntegrity(): { isValid: boolean; issues: string[] } { + const issues: string[] = []; + + // 检查文件是否存在 + if (!fs.existsSync(this.filePath)) { + issues.push('歌单文件不存在'); + } + + // 检查数据完整性 + const invalidSongs = this.list.filter(song => !PlayListUtils.isValidSong(song)); + if (invalidSongs.length > 0) { + issues.push(`发现 ${invalidSongs.length} 首无效歌曲`); + } + + // 检查重复歌曲 + const songMids = this.list.map(song => String(song.songmid)); + const uniqueSongMids = new Set(songMids); + if (songMids.length !== uniqueSongMids.size) { + issues.push(`发现 ${songMids.length - uniqueSongMids.size} 首重复歌曲`); + } + + return { + isValid: issues.length === 0, + issues + }; + } + + /** + * 修复歌单数据 + */ + repairData(): { fixed: boolean; changes: string[] } { + const changes: string[] = []; + let hasChanges = false; + + // 移除无效歌曲 + const validSongs = this.list.filter(PlayListUtils.isValidSong); + if (validSongs.length !== this.list.length) { + changes.push(`移除了 ${this.list.length - validSongs.length} 首无效歌曲`); + this.list = validSongs; + hasChanges = true; + } + + // 去重 + const deduplicatedSongs = PlayListUtils.deduplicateSongs(this.list); + if (deduplicatedSongs.length !== this.list.length) { + changes.push(`移除了 ${this.list.length - deduplicatedSongs.length} 首重复歌曲`); + this.list = deduplicatedSongs; + hasChanges = true; + } + + if (hasChanges) { + this.isDirty = true; + this.saveToFile(); + } + + return { + fixed: hasChanges, + changes + }; + } +} + +// 导出错误类供外部使用 +export { PlayListError }; \ No newline at end of file diff --git a/src/preload/index.d.ts b/src/preload/index.d.ts index e5c285d..707f2ce 100644 --- a/src/preload/index.d.ts +++ b/src/preload/index.d.ts @@ -26,6 +26,36 @@ interface CustomAPI { getSize: () => Promise }, + // 歌单管理 API + songList: { + // === 歌单管理 === + create: (name: string, description?: string, source?: string) => Promise + getAll: () => Promise + getById: (hashId: string) => Promise + delete: (hashId: string) => Promise + batchDelete: (hashIds: string[]) => Promise + edit: (hashId: string, updates: any) => Promise + updateCover: (hashId: string, coverImgUrl: string) => Promise + search: (keyword: string, source?: string) => Promise + getStatistics: () => Promise + exists: (hashId: string) => Promise + + // === 歌曲管理 === + addSongs: (hashId: string, songs: any[]) => Promise + removeSong: (hashId: string, songmid: string | number) => Promise + removeSongs: (hashId: string, songmids: (string | number)[]) => Promise + clearSongs: (hashId: string) => Promise + getSongs: (hashId: string) => Promise + getSongCount: (hashId: string) => Promise + hasSong: (hashId: string, songmid: string | number) => Promise + getSong: (hashId: string, songmid: string | number) => Promise + searchSongs: (hashId: string, keyword: string) => Promise + getSongStatistics: (hashId: string) => Promise + validateIntegrity: (hashId: string) => Promise + repairData: (hashId: string) => Promise + forceSave: (hashId: string) => Promise + }, + ai: { ask: (prompt: string) => Promise askStream: (prompt: string, streamId: string) => Promise diff --git a/src/preload/index.ts b/src/preload/index.ts index e1f7dd8..fe7084d 100644 --- a/src/preload/index.ts +++ b/src/preload/index.ts @@ -70,6 +70,47 @@ const api = { getSize: () => ipcRenderer.invoke('music-cache:get-size') }, + // 歌单管理 API + songList: { + // === 歌单管理 === + create: (name: string, description?: string, source?: string) => + ipcRenderer.invoke('songlist:create', name, description, source), + getAll: () => ipcRenderer.invoke('songlist:get-all'), + getById: (hashId: string) => ipcRenderer.invoke('songlist:get-by-id', hashId), + delete: (hashId: string) => ipcRenderer.invoke('songlist:delete', hashId), + batchDelete: (hashIds: string[]) => ipcRenderer.invoke('songlist:batch-delete', hashIds), + edit: (hashId: string, updates: any) => ipcRenderer.invoke('songlist:edit', hashId, updates), + updateCover: (hashId: string, coverImgUrl: string) => + ipcRenderer.invoke('songlist:update-cover', hashId, coverImgUrl), + search: (keyword: string, source?: string) => + ipcRenderer.invoke('songlist:search', keyword, source), + getStatistics: () => ipcRenderer.invoke('songlist:get-statistics'), + exists: (hashId: string) => ipcRenderer.invoke('songlist:exists', hashId), + + // === 歌曲管理 === + addSongs: (hashId: string, songs: any[]) => + ipcRenderer.invoke('songlist:add-songs', hashId, songs), + removeSong: (hashId: string, songmid: string | number) => + ipcRenderer.invoke('songlist:remove-song', hashId, songmid), + removeSongs: (hashId: string, songmids: (string | number)[]) => + ipcRenderer.invoke('songlist:remove-songs', hashId, songmids), + clearSongs: (hashId: string) => ipcRenderer.invoke('songlist:clear-songs', hashId), + getSongs: (hashId: string) => ipcRenderer.invoke('songlist:get-songs', hashId), + getSongCount: (hashId: string) => ipcRenderer.invoke('songlist:get-song-count', hashId), + hasSong: (hashId: string, songmid: string | number) => + ipcRenderer.invoke('songlist:has-song', hashId, songmid), + getSong: (hashId: string, songmid: string | number) => + ipcRenderer.invoke('songlist:get-song', hashId, songmid), + searchSongs: (hashId: string, keyword: string) => + ipcRenderer.invoke('songlist:search-songs', hashId, keyword), + getSongStatistics: (hashId: string) => + ipcRenderer.invoke('songlist:get-song-statistics', hashId), + validateIntegrity: (hashId: string) => + ipcRenderer.invoke('songlist:validate-integrity', hashId), + repairData: (hashId: string) => ipcRenderer.invoke('songlist:repair-data', hashId), + forceSave: (hashId: string) => ipcRenderer.invoke('songlist:force-save', hashId) + }, + getUserConfig: () => ipcRenderer.invoke('get-user-config'), // 自动更新相关 diff --git a/src/renderer/components.d.ts b/src/renderer/components.d.ts index 6c63873..575bfac 100644 --- a/src/renderer/components.d.ts +++ b/src/renderer/components.d.ts @@ -26,21 +26,18 @@ declare module 'vue' { TAside: typeof import('tdesign-vue-next')['Aside'] TBadge: typeof import('tdesign-vue-next')['Badge'] TButton: typeof import('tdesign-vue-next')['Button'] - TCard: typeof import('tdesign-vue-next')['Card'] TContent: typeof import('tdesign-vue-next')['Content'] TDialog: typeof import('tdesign-vue-next')['Dialog'] TDropdown: typeof import('tdesign-vue-next')['Dropdown'] + TForm: typeof import('tdesign-vue-next')['Form'] + TFormItem: typeof import('tdesign-vue-next')['FormItem'] ThemeSelector: typeof import('./src/components/ThemeSelector.vue')['default'] - TIcon: typeof import('tdesign-vue-next')['Icon'] TImage: typeof import('tdesign-vue-next')['Image'] TInput: typeof import('tdesign-vue-next')['Input'] TitleBarControls: typeof import('./src/components/TitleBarControls.vue')['default'] TLayout: typeof import('tdesign-vue-next')['Layout'] TLoading: typeof import('tdesign-vue-next')['Loading'] - TRadioButton: typeof import('tdesign-vue-next')['RadioButton'] - TRadioGroup: typeof import('tdesign-vue-next')['RadioGroup'] - TSlider: typeof import('tdesign-vue-next')['Slider'] - TSwitch: typeof import('tdesign-vue-next')['Switch'] + TTextarea: typeof import('tdesign-vue-next')['Textarea'] TTooltip: typeof import('tdesign-vue-next')['Tooltip'] UpdateExample: typeof import('./src/components/UpdateExample.vue')['default'] UpdateProgress: typeof import('./src/components/UpdateProgress.vue')['default'] diff --git a/src/renderer/src/api/songList.ts b/src/renderer/src/api/songList.ts new file mode 100644 index 0000000..626213e --- /dev/null +++ b/src/renderer/src/api/songList.ts @@ -0,0 +1,468 @@ +import type { + SongListAPI, + IPCResponse, + BatchOperationResult, + RemoveSongsResult, + SongListStatistics, + SongStatistics, + IntegrityCheckResult, + RepairResult +} from '../../../types/songList' +import type { SongList, Songs } from '@common/types/songList' + +// 检查是否在 Electron 环境中 +const isElectron = typeof window !== 'undefined' && window.api && window.api.songList + +/** + * 歌单管理 API 封装类 + */ +class SongListService implements SongListAPI { + private get songListAPI() { + if (!isElectron) { + throw new Error('当前环境不支持 Electron API 调用') + } + return window.api.songList + } + + // === 歌单管理方法 === + + /** + * 创建新歌单 + */ + async create(name: string, description: string = '', source: SongList['source'] = 'local'): Promise> { + try { + return await this.songListAPI.create(name, description, source) + } catch (error) { + return { + success: false, + error: error instanceof Error ? error.message : '创建歌单失败' + } + } + } + + /** + * 获取所有歌单 + */ + async getAll(): Promise> { + try { + return await this.songListAPI.getAll() + } catch (error) { + return { + success: false, + error: error instanceof Error ? error.message : '获取歌单列表失败' + } + } + } + + /** + * 根据ID获取歌单信息 + */ + async getById(hashId: string): Promise> { + try { + return await this.songListAPI.getById(hashId) + } catch (error) { + return { + success: false, + error: error instanceof Error ? error.message : '获取歌单信息失败' + } + } + } + + /** + * 删除歌单 + */ + async delete(hashId: string): Promise { + try { + return await this.songListAPI.delete(hashId) + } catch (error) { + return { + success: false, + error: error instanceof Error ? error.message : '删除歌单失败' + } + } + } + + /** + * 批量删除歌单 + */ + async batchDelete(hashIds: string[]): Promise> { + try { + return await this.songListAPI.batchDelete(hashIds) + } catch (error) { + return { + success: false, + error: error instanceof Error ? error.message : '批量删除歌单失败' + } + } + } + + /** + * 编辑歌单信息 + */ + async edit(hashId: string, updates: Partial>): Promise { + try { + return await this.songListAPI.edit(hashId, updates) + } catch (error) { + return { + success: false, + error: error instanceof Error ? error.message : '编辑歌单失败' + } + } + } + + /** + * 更新歌单封面 + */ + async updateCover(hashId: string, coverImgUrl: string): Promise { + try { + return await this.songListAPI.updateCover(hashId, coverImgUrl) + } catch (error) { + return { + success: false, + error: error instanceof Error ? error.message : '更新封面失败' + } + } + } + + /** + * 搜索歌单 + */ + async search(keyword: string, source?: SongList['source']): Promise> { + try { + return await this.songListAPI.search(keyword, source) + } catch (error) { + return { + success: false, + error: error instanceof Error ? error.message : '搜索歌单失败' + } + } + } + + /** + * 获取歌单统计信息 + */ + async getStatistics(): Promise> { + try { + return await this.songListAPI.getStatistics() + } catch (error) { + return { + success: false, + error: error instanceof Error ? error.message : '获取统计信息失败' + } + } + } + + /** + * 检查歌单是否存在 + */ + async exists(hashId: string): Promise> { + try { + return await this.songListAPI.exists(hashId) + } catch (error) { + return { + success: false, + error: error instanceof Error ? error.message : '检查歌单存在性失败' + } + } + } + + // === 歌曲管理方法 === + + /** + * 添加歌曲到歌单 + */ + async addSongs(hashId: string, songs: Songs[]): Promise { + try { + return await this.songListAPI.addSongs(hashId, songs) + } catch (error) { + return { + success: false, + error: error instanceof Error ? error.message : '添加歌曲失败' + } + } + } + + /** + * 从歌单移除歌曲 + */ + async removeSong(hashId: string, songmid: string | number): Promise> { + try { + return await this.songListAPI.removeSong(hashId, songmid) + } catch (error) { + return { + success: false, + error: error instanceof Error ? error.message : '移除歌曲失败' + } + } + } + + /** + * 批量移除歌曲 + */ + async removeSongs(hashId: string, songmids: (string | number)[]): Promise> { + try { + return await this.songListAPI.removeSongs(hashId, songmids) + } catch (error) { + return { + success: false, + error: error instanceof Error ? error.message : '批量移除歌曲失败' + } + } + } + + /** + * 清空歌单 + */ + async clearSongs(hashId: string): Promise { + try { + return await this.songListAPI.clearSongs(hashId) + } catch (error) { + return { + success: false, + error: error instanceof Error ? error.message : '清空歌单失败' + } + } + } + + /** + * 获取歌单中的歌曲列表 + */ + async getSongs(hashId: string): Promise> { + try { + return await this.songListAPI.getSongs(hashId) + } catch (error) { + return { + success: false, + error: error instanceof Error ? error.message : '获取歌曲列表失败' + } + } + } + + /** + * 获取歌单歌曲数量 + */ + async getSongCount(hashId: string): Promise> { + try { + return await this.songListAPI.getSongCount(hashId) + } catch (error) { + return { + success: false, + error: error instanceof Error ? error.message : '获取歌曲数量失败' + } + } + } + + /** + * 检查歌曲是否在歌单中 + */ + async hasSong(hashId: string, songmid: string | number): Promise> { + try { + return await this.songListAPI.hasSong(hashId, songmid) + } catch (error) { + return { + success: false, + error: error instanceof Error ? error.message : '检查歌曲存在性失败' + } + } + } + + /** + * 根据ID获取歌曲 + */ + async getSong(hashId: string, songmid: string | number): Promise> { + try { + return await this.songListAPI.getSong(hashId, songmid) + } catch (error) { + return { + success: false, + error: error instanceof Error ? error.message : '获取歌曲信息失败' + } + } + } + + /** + * 搜索歌单中的歌曲 + */ + async searchSongs(hashId: string, keyword: string): Promise> { + try { + return await this.songListAPI.searchSongs(hashId, keyword) + } catch (error) { + return { + success: false, + error: error instanceof Error ? error.message : '搜索歌曲失败' + } + } + } + + /** + * 获取歌单歌曲统计信息 + */ + async getSongStatistics(hashId: string): Promise> { + try { + return await this.songListAPI.getSongStatistics(hashId) + } catch (error) { + return { + success: false, + error: error instanceof Error ? error.message : '获取歌曲统计信息失败' + } + } + } + + /** + * 验证歌单完整性 + */ + async validateIntegrity(hashId: string): Promise> { + try { + return await this.songListAPI.validateIntegrity(hashId) + } catch (error) { + return { + success: false, + error: error instanceof Error ? error.message : '验证数据完整性失败' + } + } + } + + /** + * 修复歌单数据 + */ + async repairData(hashId: string): Promise> { + try { + return await this.songListAPI.repairData(hashId) + } catch (error) { + return { + success: false, + error: error instanceof Error ? error.message : '修复数据失败' + } + } + } + + /** + * 强制保存歌单 + */ + async forceSave(hashId: string): Promise { + try { + return await this.songListAPI.forceSave(hashId) + } catch (error) { + return { + success: false, + error: error instanceof Error ? error.message : '强制保存失败' + } + } + } + + // === 便捷方法 === + + /** + * 创建本地歌单的便捷方法 + */ + async createLocal(name: string, description?: string): Promise> { + return this.create(name, description, 'local') + } + + /** + * 获取歌单详细信息(包含歌曲列表) + */ + async getPlaylistDetail(hashId: string): Promise<{ + playlist: SongList | null + songs: readonly Songs[] + success: boolean + error?: string + }> { + try { + const [playlistRes, songsRes] = await Promise.all([ + this.getById(hashId), + this.getSongs(hashId) + ]) + + if (!playlistRes.success) { + return { + playlist: null, + songs: [], + success: false, + error: playlistRes.error + } + } + + return { + playlist: playlistRes.data || null, + songs: songsRes.success ? songsRes.data || [] : [], + success: true + } + } catch (error) { + return { + playlist: null, + songs: [], + success: false, + error: error instanceof Error ? error.message : '获取歌单详情失败' + } + } + } + + /** + * 安全删除歌单(带确认) + */ + async safeDelete(hashId: string, confirmCallback?: () => Promise): Promise { + if (confirmCallback) { + const confirmed = await confirmCallback() + if (!confirmed) { + return { + success: false, + error: '用户取消删除操作' + } + } + } + return this.delete(hashId) + } + + /** + * 检查并修复歌单数据 + */ + async checkAndRepair(hashId: string): Promise<{ + needsRepair: boolean + repairResult?: RepairResult + success: boolean + error?: string + }> { + try { + const integrityRes = await this.validateIntegrity(hashId) + if (!integrityRes.success) { + return { + needsRepair: false, + success: false, + error: integrityRes.error + } + } + + const { isValid } = integrityRes.data! + if (isValid) { + return { + needsRepair: false, + success: true + } + } + + const repairRes = await this.repairData(hashId) + return { + needsRepair: true, + repairResult: repairRes.data, + success: repairRes.success, + error: repairRes.error + } + } catch (error) { + return { + needsRepair: false, + success: false, + error: error instanceof Error ? error.message : '检查修复失败' + } + } + } +} + +// 创建单例实例 +export const songListAPI = new SongListService() + +// 默认导出 +export default songListAPI + +// 导出类型 +export type { SongListAPI, IPCResponse } \ No newline at end of file diff --git a/src/renderer/src/components/Play/FullPlay.vue b/src/renderer/src/components/Play/FullPlay.vue index b5f875f..6b9f3b8 100644 --- a/src/renderer/src/components/Play/FullPlay.vue +++ b/src/renderer/src/components/Play/FullPlay.vue @@ -24,7 +24,7 @@ interface Props { show?: boolean coverImage?: string songId?: string | null - songInfo: SongList | { songmid: number | null }, + songInfo: SongList | { songmid: number | null | string }, mainColor:string } diff --git a/src/renderer/src/components/Play/PlayMusic.vue b/src/renderer/src/components/Play/PlayMusic.vue index 29adc5f..2ce60f8 100644 --- a/src/renderer/src/components/Play/PlayMusic.vue +++ b/src/renderer/src/components/Play/PlayMusic.vue @@ -8,7 +8,8 @@ import { nextTick, onActivated, onDeactivated, - toRaw + toRaw, + provide } from 'vue' import { ControlAudioStore } from '@renderer/store/ControlAudio' import { LocalUserDetailStore } from '@renderer/store/LocalUserDetail' @@ -119,7 +120,7 @@ const waitForAudioReady = (): Promise => { // 存储待恢复的播放位置 let pendingRestorePosition = 0 -let pendingRestoreSongId: number | null = null +let pendingRestoreSongId: number | string | null = null // 记录组件被停用前的播放状态 let wasPlaying = false @@ -227,7 +228,7 @@ const playSong = async (song: SongList) => { MessagePlugin.error('播放失败,原因:' + error.message) } } - +provide('PlaySong', playSong) // 歌曲信息 // const playMode = ref(userInfo.value.playMode || PlayMode.SEQUENCE) const playMode = ref(PlayMode.SEQUENCE) @@ -671,13 +672,13 @@ const handleProgressDragStart = (event: MouseEvent) => { } // 歌曲信息 -const songInfo = ref & { songmid: null | number }>({ +const songInfo = ref & { songmid: null | number | string }>({ songmid: null, hash: '', name: '欢迎使用CeruMusic 🎉', singer: '可以配置音源插件来播放你的歌曲', albumName: '', - albumId: 0, + albumId: '0', source: '', interval: '00:00', img: '', diff --git a/src/renderer/src/store/LocalUserDetail.ts b/src/renderer/src/store/LocalUserDetail.ts index 2830b22..2ca5a15 100644 --- a/src/renderer/src/store/LocalUserDetail.ts +++ b/src/renderer/src/store/LocalUserDetail.ts @@ -79,12 +79,16 @@ export const LocalUserDetailStore = defineStore('Local', () => { return list.value } - function removeSong(songId: number) { + function removeSong(songId: number|string) { const index = list.value.findIndex((item) => item.songmid === songId) if (index !== -1) { list.value.splice(index, 1) } } + + function clearList() { + list.value = [] + } const userSource = computed(() => { return { pluginId: userInfo.value.pluginId, @@ -100,6 +104,7 @@ export const LocalUserDetailStore = defineStore('Local', () => { addSong, addSongToFirst, removeSong, + clearList, userSource } }) diff --git a/src/renderer/src/types/audio.ts b/src/renderer/src/types/audio.ts index bcef112..9d2932f 100644 --- a/src/renderer/src/types/audio.ts +++ b/src/renderer/src/types/audio.ts @@ -1,3 +1,5 @@ +import playList from '@common/types/playList' + // 音频事件相关类型定义 // 事件回调函数类型定义 @@ -44,18 +46,5 @@ export type ControlAudioState = { url: string } -export type SongList = { - songmid: number - hash?: string - singer: string - name: string - albumName: string - albumId: number - source: string - interval: string - img: string - lrc: null | string - types: string[] - _types: Record - typeUrl: Record -} + +export type SongList = playList \ No newline at end of file diff --git a/src/renderer/src/types/userInfo.ts b/src/renderer/src/types/userInfo.ts index d1685a7..1c1a850 100644 --- a/src/renderer/src/types/userInfo.ts +++ b/src/renderer/src/types/userInfo.ts @@ -2,7 +2,7 @@ import { PlayMode } from './audio' import { Sources } from './Sources' export interface UserInfo { - lastPlaySongId?: number | null + lastPlaySongId?: number | string | null currentTime?: number volume?: number topBarStyle?: boolean diff --git a/src/renderer/src/utils/playlistManager.ts b/src/renderer/src/utils/playlistManager.ts index a1b9a2d..1affc75 100644 --- a/src/renderer/src/utils/playlistManager.ts +++ b/src/renderer/src/utils/playlistManager.ts @@ -7,6 +7,7 @@ import { LocalUserDetailStore } from '@renderer/store/LocalUserDetail' type PlaylistEvents = { addToPlaylistAndPlay: SongList addToPlaylistEnd: SongList + replacePlaylist: SongList[] } // 创建全局事件总线 @@ -46,7 +47,7 @@ export async function getSongRealUrl(song: SongList): Promise { const urlData = await window.api.music.requestSdk('getMusicUrl', { pluginId: LocalUserDetail.userSource.pluginId as unknown as string, source: song.source, - songInfo: song, + songInfo: song as any, quality }) console.log(urlData) @@ -124,6 +125,52 @@ export async function addToPlaylistEnd(song: SongList, localUserStore: any) { } } +/** + * 替换整个播放列表 + * @param songs 要替换的歌曲列表 + * @param localUserStore LocalUserDetail store实例 + * @param playSongCallback 播放歌曲的回调函数 + */ +export async function replacePlaylist( + songs: SongList[], + localUserStore: any, + playSongCallback: (song: SongList) => Promise +) { + try { + if (songs.length === 0) { + await MessagePlugin.warning('歌曲列表为空') + return + } + + // 清空当前播放列表 + localUserStore.list.length = 0 + + // 添加所有歌曲到播放列表 + songs.forEach(song => { + localUserStore.addSong(song) + }) + + // 播放第一首歌曲 + if (songs[0]) { + await getSongRealUrl(songs[0]) + const playResult = playSongCallback(songs[0]) + + if (playResult && typeof playResult.then === 'function') { + await playResult + } + } + + await MessagePlugin.success(`已用 ${songs.length} 首歌曲替换播放列表`) + } catch (error: any) { + console.error('替换播放列表失败:', error) + if (error.message) { + await MessagePlugin.error('替换失败: ' + error.message) + return + } + await MessagePlugin.error('替换播放列表失败,请重试') + } +} + /** * 初始化播放列表事件监听器 * @param localUserStore LocalUserDetail store实例 @@ -142,6 +189,11 @@ export function initPlaylistEventListeners( emitter.on('addToPlaylistEnd', async (song: SongList) => { await addToPlaylistEnd(song, localUserStore) }) + + // 监听替换播放列表的事件 + emitter.on('replacePlaylist', async (songs: SongList[]) => { + await replacePlaylist(songs, localUserStore, playSongCallback) + }) } /** @@ -150,6 +202,7 @@ export function initPlaylistEventListeners( export function destroyPlaylistEventListeners() { emitter.off('addToPlaylistAndPlay') emitter.off('addToPlaylistEnd') + emitter.off('replacePlaylist') } /** diff --git a/src/renderer/src/views/music/list.vue b/src/renderer/src/views/music/list.vue index 47d40b0..00963d7 100644 --- a/src/renderer/src/views/music/list.vue +++ b/src/renderer/src/views/music/list.vue @@ -1,6 +1,7 @@ diff --git a/src/renderer/src/views/music/search.vue b/src/renderer/src/views/music/search.vue index 9f61a9e..6284f82 100644 --- a/src/renderer/src/views/music/search.vue +++ b/src/renderer/src/views/music/search.vue @@ -108,7 +108,7 @@ async function setPic(offset: number, source: string) { searchResults.value[i].img = '' } } catch (e) { - searchResults.value[i].img = 'logo.svg' + searchResults.value[i].img = '' console.log('获取失败 index' + i, e) } } diff --git a/src/types/songList.ts b/src/types/songList.ts new file mode 100644 index 0000000..6337a19 --- /dev/null +++ b/src/types/songList.ts @@ -0,0 +1,205 @@ +import type { SongList, Songs } from '@common/types/songList' + +// IPC 响应基础类型 +export interface IPCResponse { + success: boolean + data?: T + error?: string + message?: string + code?: string +} + +// 歌单管理相关类型定义 +export interface SongListAPI { + // === 歌单管理 === + + /** + * 创建新歌单 + */ + create(name: string, description?: string, source?: SongList['source']): Promise> + + /** + * 获取所有歌单 + */ + getAll(): Promise> + + /** + * 根据ID获取歌单信息 + */ + getById(hashId: string): Promise> + + /** + * 删除歌单 + */ + delete(hashId: string): Promise + + /** + * 批量删除歌单 + */ + batchDelete(hashIds: string[]): Promise> + + /** + * 编辑歌单信息 + */ + edit(hashId: string, updates: Partial>): Promise + + /** + * 更新歌单封面 + */ + updateCover(hashId: string, coverImgUrl: string): Promise + + /** + * 搜索歌单 + */ + search(keyword: string, source?: SongList['source']): Promise> + + /** + * 获取歌单统计信息 + */ + getStatistics(): Promise + lastUpdated: string + }>> + + /** + * 检查歌单是否存在 + */ + exists(hashId: string): Promise> + + // === 歌曲管理 === + + /** + * 添加歌曲到歌单 + */ + addSongs(hashId: string, songs: Songs[]): Promise + + /** + * 从歌单移除歌曲 + */ + removeSong(hashId: string, songmid: string | number): Promise> + + /** + * 批量移除歌曲 + */ + removeSongs(hashId: string, songmids: (string | number)[]): Promise> + + /** + * 清空歌单 + */ + clearSongs(hashId: string): Promise + + /** + * 获取歌单中的歌曲列表 + */ + getSongs(hashId: string): Promise> + + /** + * 获取歌单歌曲数量 + */ + getSongCount(hashId: string): Promise> + + /** + * 检查歌曲是否在歌单中 + */ + hasSong(hashId: string, songmid: string | number): Promise> + + /** + * 根据ID获取歌曲 + */ + getSong(hashId: string, songmid: string | number): Promise> + + /** + * 搜索歌单中的歌曲 + */ + searchSongs(hashId: string, keyword: string): Promise> + + /** + * 获取歌单歌曲统计信息 + */ + getSongStatistics(hashId: string): Promise + byAlbum: Record + lastModified: string + }>> + + /** + * 验证歌单完整性 + */ + validateIntegrity(hashId: string): Promise> + + /** + * 修复歌单数据 + */ + repairData(hashId: string): Promise> + + /** + * 强制保存歌单 + */ + forceSave(hashId: string): Promise +} + +// 错误码枚举 +export enum SongListErrorCode { + INVALID_HASH_ID = 'INVALID_HASH_ID', + EMPTY_NAME = 'EMPTY_NAME', + EMPTY_SOURCE = 'EMPTY_SOURCE', + CREATE_FAILED = 'CREATE_FAILED', + FILE_CREATE_FAILED = 'FILE_CREATE_FAILED', + PLAYLIST_NOT_FOUND = 'PLAYLIST_NOT_FOUND', + DELETE_FAILED = 'DELETE_FAILED', + EMPTY_UPDATES = 'EMPTY_UPDATES', + EDIT_FAILED = 'EDIT_FAILED', + READ_FAILED = 'READ_FAILED', + INIT_FAILED = 'INIT_FAILED', + WRITE_FAILED = 'WRITE_FAILED', + INVALID_ACTION = 'INVALID_ACTION', + UPDATE_COVER_FAILED = 'UPDATE_COVER_FAILED', + INVALID_DATA_FORMAT = 'INVALID_DATA_FORMAT', + FILE_NOT_FOUND = 'FILE_NOT_FOUND', + INVALID_SONG_ID = 'INVALID_SONG_ID', + SAVE_FAILED = 'SAVE_FAILED', + UNKNOWN_ERROR = 'UNKNOWN_ERROR' +} + +// 歌单来源类型 +export type SongListSource = 'local' | 'wy' | 'tx' | 'mg' | 'kg' | 'kw' + +// 批量操作结果类型 +export interface BatchOperationResult { + success: string[] + failed: string[] +} + +// 歌曲移除结果类型 +export interface RemoveSongsResult { + removed: number + notFound: number +} + +// 统计信息类型 +export interface SongListStatistics { + total: number + bySource: Record + lastUpdated: string +} + +export interface SongStatistics { + total: number + bySinger: Record + byAlbum: Record + lastModified: string +} + +// 数据完整性检查结果 +export interface IntegrityCheckResult { + isValid: boolean + issues: string[] +} + +// 数据修复结果 +export interface RepairResult { + fixed: boolean + changes: string[] +} \ No newline at end of file diff --git a/tsconfig.node.json b/tsconfig.node.json index 19dab4f..79d0eec 100644 --- a/tsconfig.node.json +++ b/tsconfig.node.json @@ -3,7 +3,13 @@ "include": ["electron.vite.config.*", "src/main/**/*", "src/preload/**/*", "src/common/**/*", "src/types/**/*"], "compilerOptions": { "composite": true, + "baseUrl": ".", "types": ["electron-vite/node"], - "moduleResolution": "bundler" + "moduleResolution": "bundler", + "paths": { + "@common/*":[ + "src/common/*" + ], + } } } diff --git a/tsconfig.web.json b/tsconfig.web.json index cc965ce..2045bc6 100644 --- a/tsconfig.web.json +++ b/tsconfig.web.json @@ -5,7 +5,8 @@ "src/renderer/src/**/*", "src/renderer/src/**/*.vue", "src/preload/*.d.ts", - "src/types/**/*" + "src/types/**/*", + "src/common/**/*" ], "compilerOptions": { "composite": true,