1. 支持暗黑主题 2. 调整插件页面ui

This commit is contained in:
sqj
2025-10-03 22:34:47 +08:00
parent 3c0be7a20f
commit e1e2d88c67
7 changed files with 6 additions and 760 deletions

View File

@@ -5,6 +5,11 @@ export default defineConfig({
lang: 'zh-CN',
title: 'Ceru Music',
base: '/',
head: [
['link', { rel: 'icon', href: '/logo.svg' }],
['meta', { name: 'author', href: '时迁酱无聊的霜霜star' }],
['meta', { name: 'keywords', content: 'Ceru Music,音乐播放器,音乐播放器工具,音乐播放器软件,音乐播放器下载,音乐播放器下载地址,澜音播放器,免费的音乐播放器,cerumusic,时迁酱,周晨鹭,无聊的霜霜,star,洛雪音乐,洛雪'}]
],
description:
'Ceru Music 是基于 Electron 和 Vue 开发的跨平台桌面音乐播放器工具,一个跨平台的音乐播放器应用,支持基于合规插件获取公开音乐信息与播放功能。',
markdown: {
@@ -77,8 +82,7 @@ export default defineConfig({
sitemap: {
hostname: 'https://ceru.docs.shiqianjiang.cn'
},
lastUpdated: true,
head: [['link', { rel: 'icon', href: '/logo.svg' }]]
lastUpdated: true
})
console.log(process.env.BASE_URL_DOCS)
// Smooth scrolling functions

View File

@@ -4,7 +4,6 @@ import { join } from 'path'
import { electronApp, optimizer, is } from '@electron-toolkit/utils'
import icon from '../../resources/logo.png?asset'
import path from 'node:path'
import musicService from './services/music'
import pluginService from './services/plugin'
import aiEvents from './events/ai'
import './services/musicSdk/index'
@@ -270,10 +269,6 @@ ipcMain.handle('service-plugin-uninstallPlugin', async (_, pluginId): Promise<an
}
})
ipcMain.handle('service-music-request', async (_, api, args) => {
return await musicService.request(api, args)
})
// 获取应用版本号
ipcMain.handle('get-app-version', () => {
return app.getVersion()

View File

@@ -1,77 +0,0 @@
import { MusicServiceBase, ServiceNamesType, ServiceArgsType } from './service-base'
import {
GetToplistArgs,
SearchArgs,
GetLyricArgs,
GetSongDetailArgs,
GetSongUrlArgs,
GetToplistDetailArgs,
GetListSongsArgs,
DownloadSingleSongArgs
} from './service-base'
import { netEaseService } from './net-ease-service'
import { AxiosError } from 'axios'
const musicService: MusicServiceBase = netEaseService
type Response = {
success: boolean
data?: any
error?: any
}
async function request(api: ServiceNamesType, args: ServiceArgsType): Promise<any> {
const res: Response = { success: false }
try {
switch (api) {
case 'search':
res.data = await musicService.search(args as SearchArgs)
break
case 'getSongDetail':
res.data = await musicService.getSongDetail(args as GetSongDetailArgs)
break
case 'getSongUrl':
res.data = await musicService.getSongUrl(args as GetSongUrlArgs)
break
case 'getLyric':
res.data = await musicService.getLyric(args as GetLyricArgs)
break
case 'getToplist':
res.data = await musicService.getToplist(args as GetToplistArgs)
break
case 'getToplistDetail':
res.data = await musicService.getToplistDetail(args as GetToplistDetailArgs)
break
case 'getListSongs':
res.data = await musicService.getListSongs(args as GetListSongsArgs)
break
case 'downloadSingleSong':
res.data = await musicService.downloadSingleSong(args as DownloadSingleSongArgs)
break
default:
throw new Error(`未知的方法: ${api}`)
}
res.success = true
} catch (error: any) {
if (error instanceof AxiosError) {
error.message = '网络错误'
}
console.error('请求失败: ', error)
res.error = error
}
return res
}
export default { request }
// netEaseService
// .search({
// keyword: '稻香',
// type: 1,
// limit: 25
// })
// .then((res) => {
// console.log(res)
// })

View File

@@ -1,398 +0,0 @@
import path from 'path'
import fs from 'fs'
import fsPromise from 'fs/promises'
import NeteaseCloudMusicApi from 'NeteaseCloudMusicApi'
import { axiosClient, MusicServiceBase } from './service-base'
import { pipeline } from 'node:stream/promises'
import pluginService from '../plugin'
import musicSdk from '../../utils/musicSdk'
import {
SearchArgs,
GetSongDetailArgs,
GetSongUrlArgs,
GetToplistDetailArgs,
GetListSongsArgs,
GetLyricArgs,
GetToplistArgs,
DownloadSingleSongArgs
} from './service-base'
import { SongDetailResponse, SongResponse } from './service-base'
import { fieldsSelector } from '../../utils/object'
import { getAppDirPath } from '../../utils/path'
// 音乐源映射
const MUSIC_SOURCES = {
kg: 'kg', // 酷狗音乐
wy: 'wy', // 网易云音乐
tx: 'tx', // QQ音乐
kw: 'kw', // 酷我音乐
mg: 'mg' // 咪咕音乐
}
// 扩展搜索参数接口
interface ExtendedSearchArgs extends SearchArgs {
source?: string // 音乐源参数 kg|wy|tx|kw|mg
}
// 扩展歌曲详情参数接口
interface ExtendedGetSongDetailArgs extends GetSongDetailArgs {
source?: string
}
// 扩展歌词参数接口
interface ExtendedGetLyricArgs extends GetLyricArgs {
source?: string
}
const baseUrl: string = 'https://music.163.com'
const baseTwoUrl: string = 'https://www.lihouse.xyz/coco_widget'
const fileLock: Record<string, boolean> = {}
/**
* 获取支持的音乐源列表
*/
export const getSupportedSources = () => {
return Object.keys(MUSIC_SOURCES).map((key) => ({
id: key,
name: getSourceName(key),
available: !!musicSdk[key]
}))
}
/**
* 获取音乐源名称
*/
const getSourceName = (source: string): string => {
const sourceNames = {
kg: '酷狗音乐',
wy: '网易云音乐',
tx: 'QQ音乐',
kw: '酷我音乐',
mg: '咪咕音乐'
}
return sourceNames[source] || source
}
/**
* 智能音乐匹配使用musicSdk的findMusic功能
*/
export const findMusic = async (musicInfo: {
name: string
singer?: string
albumName?: string
interval?: string
source?: string
}) => {
try {
return await musicSdk.findMusic(musicInfo)
} catch (error) {
console.error('智能音乐匹配失败:', error)
return []
}
}
export const netEaseService: MusicServiceBase = {
async search({
type,
keyword,
offset,
limit,
source
}: ExtendedSearchArgs): Promise<SongResponse> {
// 如果指定了音乐源且不是网易云使用对应的musicSdk
if (source && source !== 'wy' && MUSIC_SOURCES[source]) {
try {
const sourceModule = musicSdk[source]
if (sourceModule && sourceModule.musicSearch) {
const page = Math.floor((offset || 0) / (limit || 25)) + 1
const result = await sourceModule.musicSearch.search(keyword, page, limit || 25)
// 转换为统一格式
return {
songs: result.list || [],
songCount: result.total || result.list?.length || 0
}
} else {
throw new Error(`不支持的音乐源: ${source}`)
}
} catch (error: any) {
console.error(`${source}音乐源搜索失败:`, error)
// 如果指定源失败,回退到网易云
console.log('回退到网易云音乐搜索')
}
}
// 默认使用网易云音乐搜索
return await axiosClient
.get(`${baseUrl}/api/search/get/web`, {
params: {
s: keyword,
type: type,
limit: limit,
offset: offset ?? 0
}
})
.then(({ data }) => {
if (data.code !== 200) {
console.error(data)
throw new Error(data.msg)
}
return data.result
})
},
async getSongDetail({ ids, source }: ExtendedGetSongDetailArgs): Promise<SongDetailResponse> {
// 如果指定了音乐源且不是网易云使用对应的musicSdk
if (source && source !== 'wy' && MUSIC_SOURCES[source]) {
try {
const sourceModule = musicSdk[source]
if (sourceModule && sourceModule.musicInfo) {
// 对于多个ID并行获取详情
const promises = ids.map((id) => sourceModule.musicInfo.getMusicInfo(id))
const results = await Promise.all(promises)
return results.filter((result: any) => result) // 过滤掉失败的结果
} else {
throw new Error(`不支持的音乐源: ${source}`)
}
} catch (error: any) {
console.error(`${source}音乐源获取歌曲详情失败:`, error)
// 如果指定源失败,回退到网易云
console.log('回退到网易云音乐获取歌曲详情')
}
}
// 默认使用网易云音乐
return await axiosClient
.get(`${baseUrl}/api/song/detail?ids=[${ids.join(',')}]`)
.then(({ data }) => {
if (data.code !== 200) {
console.error(data)
throw new Error(data.msg)
}
return data.songs
})
},
async getSongUrl({ id, pluginId, quality, source }: GetSongUrlArgs): Promise<any> {
// 如果提供了插件ID、音质和音乐源则使用插件获取音乐URL
if (pluginId && (quality || source)) {
try {
// 获取插件实例
const plugin = pluginService.getPluginById(pluginId)
if (!plugin) {
throw new Error(`未找到ID为 ${pluginId} 的插件`)
}
// 准备音乐信息对象确保符合MusicInfo类型要求
const musicInfo = {
songmid: id as unknown as number,
singer: '',
name: '',
albumName: '',
albumId: 0,
source: source || 'wy',
interval: '',
img: '',
lrc: null,
types: [],
_types: {},
typeUrl: {}
}
// 调用插件的getMusicUrl方法获取音乐URL
const url: string = await plugin.getMusicUrl(
source || 'wy',
musicInfo,
quality || 'standard'
)
// 构建返回对象
return { url }
} catch (error: any) {
console.error('通过插件获取音乐URL失败:', error)
throw new Error(`插件获取音乐URL失败: ${error.message}`)
}
}
// 如果没有提供插件信息或插件调用失败,则使用默认方法获取
return await axiosClient.get(`${baseTwoUrl}/music_resource/id/${id}`).then(({ data }) => {
if (!data.status) {
throw new Error('歌曲不存在')
}
return data.song_data
})
},
async getLyric({ id, lv, yv, tv, source }: ExtendedGetLyricArgs): Promise<any> {
// 如果指定了音乐源且不是网易云使用对应的musicSdk
if (source && source !== 'wy' && MUSIC_SOURCES[source]) {
try {
const sourceModule = musicSdk[source]
if (sourceModule && sourceModule.getLyric) {
// 构建歌曲信息对象,不同源可能需要不同的参数
const songInfo = { id, songmid: id, hash: id }
const result = await sourceModule.getLyric(songInfo)
// 转换为统一格式
return {
lrc: { lyric: result.lyric || '' },
tlyric: { lyric: result.tlyric || '' },
yrc: { lyric: result.yrc || '' }
}
} else {
throw new Error(`不支持的音乐源: ${source}`)
}
} catch (error: any) {
console.error(`${source}音乐源获取歌词失败:`, error)
// 如果指定源失败,回退到网易云
console.log('回退到网易云音乐获取歌词')
}
}
// 默认使用网易云音乐
const optionalParams: any = {}
if (lv) {
optionalParams.lv = -1
}
if (yv) {
optionalParams.yv = -1
}
if (tv) {
optionalParams.tv = -1
}
return await axiosClient
.get(`${baseUrl}/api/song/lyric`, {
params: {
id: id,
...optionalParams
}
})
.then(({ data }) => {
if (data.code !== 200) {
console.error(data)
throw Error(data.msg)
}
const requiredFields = ['lyricUser', 'lrc', 'tlyric', 'yrc']
return fieldsSelector(data, requiredFields)
})
},
async getToplist({}: GetToplistArgs): Promise<any> {
return await NeteaseCloudMusicApi.toplist({})
.then(({ body: data }) => {
return data.list
})
.catch((err: any) => {
console.error({
code: err.body?.code,
msg: err.body?.msg?.message
})
throw err.body?.msg ?? err
})
},
async getToplistDetail({}: GetToplistDetailArgs): Promise<any> {
return await NeteaseCloudMusicApi.toplist_detail({})
.then(({ body: data }) => {
return data.list
})
.catch((err: any) => {
console.error({
code: err.body?.code,
msg: err.body?.msg?.message
})
throw err.body?.msg ?? err
})
},
async getListSongs(args: GetListSongsArgs): Promise<any> {
return await NeteaseCloudMusicApi.playlist_track_all(args)
.then(({ body: data }) => {
const requiredFields = ['songs', 'privileges']
return fieldsSelector(data, requiredFields)
})
.catch((err: any) => {
console.error({
code: err.body?.code,
msg: err.body?.msg?.message
})
throw err.body?.msg ?? err
})
},
async downloadSingleSong({
id,
name,
artist,
pluginId,
source,
quality
}: DownloadSingleSongArgs) {
const { url } = await this.getSongUrl({ id, pluginId, source, quality })
// 从URL中提取文件扩展名如果没有则默认为mp3
const getFileExtension = (url: string): string => {
try {
const urlObj = new URL(url)
const pathname = urlObj.pathname
const lastDotIndex = pathname.lastIndexOf('.')
if (lastDotIndex !== -1 && lastDotIndex < pathname.length - 1) {
const extension = pathname.substring(lastDotIndex + 1).toLowerCase()
// 验证是否为常见的音频格式
const validExtensions = ['mp3', 'flac', 'wav', 'aac', 'm4a', 'ogg', 'wma']
if (validExtensions.includes(extension)) {
return extension
}
}
} catch (error) {
console.warn('解析URL失败使用默认扩展名:', error)
}
return 'mp3' // 默认扩展名
}
const fileExtension = getFileExtension(url)
const songPath = path.join(
getAppDirPath(),
'download',
'songs',
`${name}-${artist}-${id}.${fileExtension}`
.replace(/[/\\:*?"<>|]/g, '')
.replace(/^\.+/, '')
.replace(/\.+$/, '')
.trim()
)
if (fileLock[songPath]) {
throw new Error('歌曲正在下载中')
} else {
fileLock[songPath] = true
}
try {
if (fs.existsSync(songPath)) {
return {
message: '歌曲已存在'
}
}
await fsPromise.mkdir(path.dirname(songPath), { recursive: true })
const songDataRes = await axiosClient({
method: 'GET',
url: url,
responseType: 'stream'
})
await pipeline(songDataRes.data, fs.createWriteStream(songPath))
} finally {
delete fileLock[songPath]
}
return {
message: '下载成功',
path: songPath
}
}
}

View File

@@ -1,276 +0,0 @@
import axios, { AxiosInstance } from 'axios'
const timeout: number = 5000
const mobileHeaders = {
'User-Agent':
'Mozilla/5.0 (iPhone; CPU iPhone OS 14_0 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/14.0 Mobile/15E148'
}
const axiosClient: AxiosInstance = axios.create({
timeout: timeout
})
type SearchArgs = {
type: number
keyword: string
offset?: number
limit: number
source?: string
}
type GetSongDetailArgs = {
ids: string[]
}
type GetSongUrlArgs = {
id: string
pluginId?: string // 插件ID
quality?: string // 音质
source?: string // 音乐源wy, tx等
}
type GetLyricArgs = {
id: string
lv?: boolean
yv?: boolean // 获取逐字歌词
tv?: boolean // 获取歌词翻译
}
type GetToplistArgs = Record<string, never>
type GetToplistDetailArgs = Record<string, never>
type GetListSongsArgs = {
id: string
limit?: number
offset?: number
}
type DownloadSingleSongArgs = {
id: string
name: string
artist: string
pluginId?: string
quality?: string
source?: string
}
type ServiceNamesType =
| 'search'
| 'getSongDetail'
| 'getSongUrl'
| 'getLyric'
| 'getToplist'
| 'getToplistDetail'
| 'getListSongs'
| 'downloadSingleSong'
type ServiceArgsType =
| SearchArgs
| GetSongDetailArgs
| GetSongUrlArgs
| GetLyricArgs
| GetToplistArgs
| GetToplistDetailArgs
| GetListSongsArgs
| DownloadSingleSongArgs
interface Artist {
id: number
name: string
picUrl: string | null
alias: string[]
albumSize: number
picId: number
fansGroup: null
img1v1Url: string
img1v1: number
trans: null
}
interface Album {
id: number
name: string
artist: {
id: number
name: string
picUrl: string | null
alias: string[]
albumSize: number
picId: number
fansGroup: null
img1v1Url: string
img1v1: number
trans: null
}
publishTime: number
size: number
copyrightId: number
status: number
picId: number
alia?: string[]
mark: number
}
interface Song {
id: number
name: string
artists: Artist[]
album: Album
duration: number
copyrightId: number
status: number
alias: string[]
rtype: number
ftype: number
mvid: number
fee: number
rUrl: null
mark: number
transNames?: string[]
}
interface SongResponse {
songs: Song[]
songCount: number
}
interface AlbumDetail {
name: string
id: number
type: string
size: number
picId: number
blurPicUrl: string
companyId: number
pic: number
picUrl: string
publishTime: number
description: string
tags: string
company: string
briefDesc: string
artist: {
name: string
id: number
picId: number
img1v1Id: number
briefDesc: string
picUrl: string
img1v1Url: string
albumSize: number
alias: string[]
trans: string
musicSize: number
topicPerson: number
}
songs: any[]
alias: string[]
status: number
copyrightId: number
commentThreadId: string
artists: Artist[]
subType: string
transName: null
onSale: boolean
mark: number
gapless: number
dolbyMark: number
}
interface MusicQuality {
name: null
id: number
size: number
extension: string
sr: number
dfsId: number
bitrate: number
playTime: number
volumeDelta: number
}
interface SongDetail {
name: string
id: number
position: number
alias: string[]
status: number
fee: number
copyrightId: number
disc: string
no: number
artists: Artist[]
album: AlbumDetail
starred: boolean
popularity: number
score: number
starredNum: number
duration: number
playedNum: number
dayPlays: number
hearTime: number
sqMusic: MusicQuality
hrMusic: null
ringtone: null
crbt: null
audition: null
copyFrom: string
commentThreadId: string
rtUrl: null
ftype: number
rtUrls: any[]
copyright: number
transName: null
sign: null
mark: number
originCoverType: number
originSongSimpleData: null
single: number
noCopyrightRcmd: null
hMusic: MusicQuality
mMusic: MusicQuality
lMusic: MusicQuality
bMusic: MusicQuality
mvid: number
mp3Url: null
rtype: number
rurl: null
}
interface SongDetailResponse {
songs: SongDetail[]
equalizers: Record<string, unknown>
code: number
}
interface SongUrlResponse {
id: number
url: string // 歌曲地址
name: string
artist: string
pic: string //封面图片
}
interface MusicServiceBase {
search({ type, keyword, offset, limit }: SearchArgs): Promise<SongResponse>
getSongDetail({ ids }: GetSongDetailArgs): Promise<SongDetailResponse>
getSongUrl({ id, pluginId, quality, source }: GetSongUrlArgs): Promise<SongUrlResponse>
getLyric({ id, lv, yv, tv }: GetLyricArgs): Promise<any>
getToplist({}: GetToplistArgs): Promise<any>
getToplistDetail({}: GetToplistDetailArgs): Promise<any>
getListSongs({ id, limit, offset }: GetListSongsArgs): Promise<any>
downloadSingleSong({ id }: DownloadSingleSongArgs): Promise<any>
}
export type { MusicServiceBase, ServiceNamesType, ServiceArgsType }
export type {
SearchArgs,
GetSongDetailArgs,
GetSongUrlArgs,
GetLyricArgs,
GetToplistArgs,
GetToplistDetailArgs,
GetListSongsArgs,
DownloadSingleSongArgs
}
export type { SongResponse, SongDetailResponse, SongUrlResponse }
export { mobileHeaders, axiosClient }

View File

@@ -11,7 +11,6 @@ interface CustomAPI {
onMusicCtrl: (callback: (event: Event, args: any) => void) => () => void
music: {
request: (api: string, args: any) => Promise<any>
requestSdk: <T extends keyof MainApi>(
method: T,
args: {

View File

@@ -29,7 +29,6 @@ const api = {
},
// 音乐相关方法
music: {
request: (api: string, args: any) => ipcRenderer.invoke('service-music-request', api, args),
requestSdk: (api: string, args: any) =>
ipcRenderer.invoke('service-music-sdk-request', api, args)
},