2023-11-23 18:28:53 +08:00
|
|
|
|
import { ipcMain, dialog, app, clipboard, shell } from "electron";
|
2023-11-27 18:23:01 +08:00
|
|
|
|
import { readDirAsync } from "@main/utils/readDirAsync";
|
2023-11-23 18:28:53 +08:00
|
|
|
|
import { parseFile } from "music-metadata";
|
|
|
|
|
|
import { download } from "electron-dl";
|
2023-11-27 18:23:01 +08:00
|
|
|
|
import getNeteaseMusicUrl from "@main/utils/getNeteaseMusicUrl";
|
2024-01-09 18:13:01 +08:00
|
|
|
|
import NodeID3 from "node-id3";
|
2023-11-23 18:28:53 +08:00
|
|
|
|
import axios from "axios";
|
|
|
|
|
|
import fs from "fs/promises";
|
|
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
|
* 监听主进程的 IPC 事件
|
|
|
|
|
|
* @param {BrowserWindow} win - 要监听 IPC 事件的程序窗口
|
|
|
|
|
|
*/
|
|
|
|
|
|
|
|
|
|
|
|
const mainIpcMain = (win) => {
|
|
|
|
|
|
// 窗口操作部分
|
|
|
|
|
|
ipcMain.on("window-min", (ev) => {
|
|
|
|
|
|
// 阻止最小化
|
|
|
|
|
|
ev.preventDefault();
|
|
|
|
|
|
// 最小化
|
|
|
|
|
|
win.minimize();
|
|
|
|
|
|
});
|
|
|
|
|
|
ipcMain.on("window-maxOrRestore", (ev) => {
|
|
|
|
|
|
const winSizeState = win.isMaximized();
|
|
|
|
|
|
winSizeState ? win.restore() : win.maximize();
|
2023-12-01 15:29:14 +08:00
|
|
|
|
ev.reply("windowState", win.isMaximized());
|
2023-11-23 18:28:53 +08:00
|
|
|
|
});
|
|
|
|
|
|
ipcMain.on("window-restore", () => {
|
|
|
|
|
|
win.restore();
|
|
|
|
|
|
});
|
|
|
|
|
|
ipcMain.on("window-hide", () => {
|
|
|
|
|
|
win.hide();
|
|
|
|
|
|
});
|
|
|
|
|
|
ipcMain.on("window-close", () => {
|
|
|
|
|
|
win.close();
|
|
|
|
|
|
app.isQuiting = true;
|
|
|
|
|
|
app.quit();
|
|
|
|
|
|
});
|
|
|
|
|
|
ipcMain.on("window-relaunch", () => {
|
|
|
|
|
|
app.isQuiting = true;
|
|
|
|
|
|
app.relaunch();
|
|
|
|
|
|
app.quit();
|
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
|
|
// 显示进度
|
|
|
|
|
|
ipcMain.on("setProgressBar", (_, val) => {
|
|
|
|
|
|
if (val === "close") {
|
|
|
|
|
|
win.setProgressBar(-1);
|
|
|
|
|
|
return false;
|
|
|
|
|
|
}
|
|
|
|
|
|
win.setProgressBar(val / 100);
|
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
|
|
// 解灰
|
|
|
|
|
|
ipcMain.handle("getMusicNumUrl", async (_, data) => {
|
|
|
|
|
|
// 解析传入数据
|
|
|
|
|
|
const songData = JSON.parse(data);
|
|
|
|
|
|
const songName = `${songData?.name}-${songData?.artists?.[0].name}`;
|
|
|
|
|
|
console.log("开始解灰:", songName);
|
|
|
|
|
|
const url = await getNeteaseMusicUrl(songName);
|
|
|
|
|
|
console.log("解灰地址:", url);
|
|
|
|
|
|
return url;
|
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
|
|
// bili 链接解析
|
|
|
|
|
|
ipcMain.handle("getBiliUrlData", async (_, url) => {
|
|
|
|
|
|
const data = await getBiliUrlBase64(url);
|
|
|
|
|
|
return data;
|
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
|
|
// 默认音乐文件夹
|
|
|
|
|
|
ipcMain.handle("getdefaultMusicPath", async () => {
|
|
|
|
|
|
const path = app.getPath("music");
|
|
|
|
|
|
return path;
|
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
|
|
// 选择文件夹
|
|
|
|
|
|
ipcMain.handle("selectDir", async (_, isChooseDl = false) => {
|
|
|
|
|
|
try {
|
|
|
|
|
|
const { canceled, filePaths } = await dialog.showOpenDialog({
|
|
|
|
|
|
title: isChooseDl ? "选择下载目录" : "选择添加目录",
|
|
|
|
|
|
defaultPath: isChooseDl ? app.getPath("downloads") : app.getPath("music"),
|
|
|
|
|
|
properties: ["openDirectory", "createDirectory"],
|
|
|
|
|
|
buttonLabel: "选择文件夹",
|
|
|
|
|
|
});
|
|
|
|
|
|
if (!canceled) {
|
|
|
|
|
|
const selectedDirectory = filePaths[0];
|
|
|
|
|
|
return selectedDirectory;
|
|
|
|
|
|
}
|
|
|
|
|
|
} catch (err) {
|
|
|
|
|
|
console.error("选择文件夹时发生错误:", err);
|
|
|
|
|
|
throw err;
|
|
|
|
|
|
}
|
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
|
|
// 读取文件夹内容
|
|
|
|
|
|
ipcMain.handle("getDirContents", async (_, selectedDir) => {
|
|
|
|
|
|
try {
|
|
|
|
|
|
// 使用 readDirAsync 函数递归地读取文件夹内容
|
|
|
|
|
|
const directoryContents = await readDirAsync(selectedDir);
|
|
|
|
|
|
return directoryContents;
|
|
|
|
|
|
} catch (err) {
|
|
|
|
|
|
console.error("读取文件夹内容时发生错误:", err);
|
|
|
|
|
|
throw err;
|
|
|
|
|
|
}
|
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
|
|
// 读取音乐歌词
|
|
|
|
|
|
ipcMain.handle("getMusicLyric", async (_, path) => {
|
|
|
|
|
|
try {
|
|
|
|
|
|
const data = await parseFile(path);
|
|
|
|
|
|
const lyric = data.common.lyrics;
|
|
|
|
|
|
if (lyric && lyric.length > 0) {
|
|
|
|
|
|
return lyric[0];
|
|
|
|
|
|
}
|
|
|
|
|
|
// 如果歌词数据不存在,尝试读取同名的 lrc 文件
|
|
|
|
|
|
else {
|
|
|
|
|
|
const lrcFilePath = path.replace(/\.[^.]+$/, ".lrc");
|
|
|
|
|
|
const lrcData = await fs.readFile(lrcFilePath, "utf-8");
|
|
|
|
|
|
// 返回读取的 lrc 数据,如果没有则返回 null
|
|
|
|
|
|
return lrcData || null;
|
|
|
|
|
|
}
|
|
|
|
|
|
} catch (error) {
|
|
|
|
|
|
console.error("读取音乐歌词出错:", error);
|
|
|
|
|
|
return null;
|
|
|
|
|
|
}
|
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
|
|
// 读取音乐封面
|
|
|
|
|
|
ipcMain.handle("getMusicCover", async (_, path) => {
|
|
|
|
|
|
try {
|
|
|
|
|
|
const data = await parseFile(path);
|
|
|
|
|
|
const picture = data.common.picture;
|
|
|
|
|
|
if (picture && picture.length > 0) {
|
|
|
|
|
|
const coverData = picture[0].data;
|
|
|
|
|
|
const coverFormat = picture[0].format;
|
|
|
|
|
|
return { coverData, coverFormat };
|
|
|
|
|
|
}
|
|
|
|
|
|
// 如果封面数据不存在,尝试读取同名的封面图片文件
|
|
|
|
|
|
else {
|
|
|
|
|
|
const coverFilePath = path.replace(/\.[^.]+$/, ".jpg");
|
|
|
|
|
|
const coverData = await fs.readFile(coverFilePath);
|
|
|
|
|
|
// 返回读取的封面图片数据,如果没有则返回 null
|
|
|
|
|
|
return coverData ? { coverData, coverFormat: "jpg" } : null;
|
|
|
|
|
|
}
|
|
|
|
|
|
} catch (error) {
|
|
|
|
|
|
console.error("读取音乐封面出错:", error);
|
|
|
|
|
|
return null;
|
|
|
|
|
|
}
|
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
|
|
// 执行复制操作
|
|
|
|
|
|
ipcMain.handle("copyData", async (_, data) => {
|
|
|
|
|
|
try {
|
|
|
|
|
|
clipboard.writeText(data);
|
|
|
|
|
|
return true;
|
|
|
|
|
|
} catch (error) {
|
|
|
|
|
|
console.error("复制操作出错:", error);
|
|
|
|
|
|
return false;
|
|
|
|
|
|
}
|
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
|
|
// 本地磁盘文件删除
|
|
|
|
|
|
ipcMain.handle("deleteFile", async (_, path) => {
|
|
|
|
|
|
try {
|
|
|
|
|
|
// 检查文件是否存在
|
|
|
|
|
|
if (fs.access(path)) {
|
|
|
|
|
|
// 尝试删除文件
|
|
|
|
|
|
fs.unlink(path);
|
|
|
|
|
|
console.log(`文件已删除:${path}`);
|
|
|
|
|
|
return true;
|
|
|
|
|
|
} else {
|
|
|
|
|
|
console.log(`文件不存在:${path}`);
|
|
|
|
|
|
return false;
|
|
|
|
|
|
}
|
|
|
|
|
|
} catch (err) {
|
|
|
|
|
|
console.error(`文件删除操作出错:${path}`, err);
|
|
|
|
|
|
return false;
|
|
|
|
|
|
}
|
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
|
|
// 打开歌曲目录
|
|
|
|
|
|
ipcMain.on("openSongLocal", (_, path) => {
|
|
|
|
|
|
try {
|
|
|
|
|
|
if (fs.access(path)) {
|
|
|
|
|
|
shell.showItemInFolder(path);
|
|
|
|
|
|
} else {
|
|
|
|
|
|
console.log(`文件不存在:${path}`);
|
|
|
|
|
|
}
|
|
|
|
|
|
} catch (error) {
|
|
|
|
|
|
console.error("打开歌曲目录时出错:", error);
|
|
|
|
|
|
}
|
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
|
|
// 下载文件至指定目录
|
|
|
|
|
|
ipcMain.handle("downloadFile", async (_, data, song, songName, songType, path) => {
|
|
|
|
|
|
try {
|
|
|
|
|
|
if (fs.access(path)) {
|
|
|
|
|
|
const songData = JSON.parse(song);
|
|
|
|
|
|
console.info("开始下载:", songData, data);
|
|
|
|
|
|
// 下载歌曲
|
|
|
|
|
|
const songDownload = await download(win, data.url, {
|
|
|
|
|
|
directory: path,
|
|
|
|
|
|
filename: `${songName}.${songType}`,
|
|
|
|
|
|
});
|
2024-01-09 18:13:01 +08:00
|
|
|
|
// 若不为 mp3,则不进行元信息写入
|
|
|
|
|
|
if (songType !== "mp3") return true;
|
2023-11-23 18:28:53 +08:00
|
|
|
|
// 下载封面
|
|
|
|
|
|
const coverDownload = await download(win, songData.cover, {
|
|
|
|
|
|
directory: path,
|
|
|
|
|
|
filename: `${songName}.jpg`,
|
|
|
|
|
|
});
|
|
|
|
|
|
// 生成歌曲文件的元数据
|
|
|
|
|
|
const songTag = {
|
|
|
|
|
|
title: songData.name,
|
|
|
|
|
|
artist: Array.isArray(songData.artists)
|
|
|
|
|
|
? songData.artists.map((ar) => ar.name).join(" / ")
|
|
|
|
|
|
: songData.artists || "未知歌手",
|
|
|
|
|
|
album: songData.album?.name || songData.album,
|
|
|
|
|
|
image: coverDownload.getSavePath(),
|
|
|
|
|
|
};
|
|
|
|
|
|
// 保存修改后的元数据
|
2024-01-09 18:13:01 +08:00
|
|
|
|
const isSuccess = NodeID3.write(songTag, songDownload.getSavePath());
|
2023-11-23 18:28:53 +08:00
|
|
|
|
// 删除封面
|
|
|
|
|
|
await fs.unlink(coverDownload.getSavePath());
|
2024-01-09 18:13:01 +08:00
|
|
|
|
return isSuccess;
|
2023-11-23 18:28:53 +08:00
|
|
|
|
} else {
|
|
|
|
|
|
console.log(`目录不存在:${path}`);
|
|
|
|
|
|
return false;
|
|
|
|
|
|
}
|
|
|
|
|
|
} catch (error) {
|
|
|
|
|
|
console.error("下载文件时出错:", error);
|
|
|
|
|
|
return false;
|
|
|
|
|
|
}
|
|
|
|
|
|
});
|
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
|
* 从 Bilibili 视频中获取文件的 Base64 数据
|
|
|
|
|
|
*
|
|
|
|
|
|
* @param {string} url - 要获取的文件的 URL
|
|
|
|
|
|
* @returns {Promise<string>} - 文件的 Base64 数据
|
|
|
|
|
|
*/
|
|
|
|
|
|
const getBiliUrlBase64 = async (url) => {
|
|
|
|
|
|
try {
|
|
|
|
|
|
const response = await axios.get(url, {
|
|
|
|
|
|
headers: {
|
|
|
|
|
|
Referer: "https://www.bilibili.com/",
|
|
|
|
|
|
"User-Agent": "okhttp/3.4.1",
|
|
|
|
|
|
},
|
|
|
|
|
|
responseType: "arraybuffer",
|
|
|
|
|
|
withCredentials: false,
|
|
|
|
|
|
});
|
|
|
|
|
|
// 将二进制数据转换为缓冲区
|
|
|
|
|
|
const buffer = toBuffer(response.data);
|
|
|
|
|
|
// 将缓冲区中的数据转换为 Base64 编码的字符串
|
|
|
|
|
|
const encodedData = buffer.toString("base64");
|
|
|
|
|
|
// 返回 Base64 编码的文件数据
|
|
|
|
|
|
return encodedData;
|
|
|
|
|
|
} catch (error) {
|
|
|
|
|
|
console.error("获取文件数据时发生错误:" + error);
|
|
|
|
|
|
return null;
|
|
|
|
|
|
}
|
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
|
* 将数据转换为缓冲区( Buffer )
|
|
|
|
|
|
*
|
|
|
|
|
|
* @param {ArrayBuffer|Buffer|Uint8Array} data - 要转换的数据
|
|
|
|
|
|
* @returns {Buffer} - 转换后的缓冲区
|
|
|
|
|
|
*/
|
|
|
|
|
|
const toBuffer = (data) => {
|
|
|
|
|
|
if (data instanceof Buffer) {
|
|
|
|
|
|
return data;
|
|
|
|
|
|
} else {
|
|
|
|
|
|
return Buffer.from(data);
|
|
|
|
|
|
}
|
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
|
|
export default mainIpcMain;
|