feat: 支持从本地读取歌词覆盖线上歌词

This commit is contained in:
MoYingJi
2025-10-22 13:33:33 +08:00
parent 103bf7948d
commit 9fc1746495
5 changed files with 114 additions and 22 deletions

View File

@@ -25,6 +25,7 @@ import log from "../main/logger";
import Store from "electron-store";
import fg from "fast-glob";
import openLoginWin from "./loginWin";
import path from "node:path";
// 注册 ipcMain
const initIpcMain = (
@@ -305,6 +306,18 @@ const initWinIpcMain = (
},
);
// 读取本地歌词
ipcMain.handle("read-local-lyric", async (_, lyricDir: string, id: number, ext: string): Promise<string> => {
const lyricPath = path.join(lyricDir, `${id}.${ext}`);
try {
await fs.access(lyricPath);
const lyric = await fs.readFile(lyricPath, "utf-8")
if (lyric) return lyric;
} catch {}
return "";
})
// 删除文件
ipcMain.handle("delete-file", async (_, path: string) => {
try {

View File

@@ -22,7 +22,7 @@
<n-text class="name">本地歌曲目录</n-text>
<n-text class="tip" :depth="3">可在此增删本地歌曲目录歌曲增删实时同步</n-text>
</div>
<n-button strong secondary @click="changeLocalPath()">
<n-button strong secondary @click="changeLocalMusicPath()">
<template #icon>
<SvgIcon name="Folder" />
</template>
@@ -38,7 +38,40 @@
<div class="label">
<n-text class="name">{{ item }}</n-text>
</div>
<n-button strong secondary @click="changeLocalPath(index)">
<n-button strong secondary @click="changeLocalMusicPath(index)">
<template #icon>
<SvgIcon name="Delete" />
</template>
</n-button>
</n-card>
</n-collapse-transition>
</n-card>
<n-card class="set-item" id="local-list-choose" content-style="flex-direction: column">
<n-flex justify="space-between">
<div class="label">
<n-text class="name">本地歌词覆盖在线歌词</n-text>
<n-text class="tip" :depth="3"
>可在这些文件夹内覆盖在线歌曲的歌词将歌词文件命名为 `歌曲ID.后缀名` 即可支持 LRC
TTML 格式</n-text
>
</div>
<n-button strong secondary @click="changeLocalLyricPath()">
<template #icon>
<SvgIcon name="Folder" />
</template>
更改
</n-button>
</n-flex>
<n-collapse-transition :show="settingStore.localLyricPath.length > 0">
<n-card
v-for="(item, index) in settingStore.localLyricPath"
:key="index"
class="set-item"
>
<div class="label">
<n-text class="name">{{ item }}</n-text>
</div>
<n-button strong secondary @click="changeLocalLyricPath(index)">
<template #icon>
<SvgIcon name="Delete" />
</template>
@@ -125,7 +158,7 @@
<script setup lang="ts">
import { useSettingStore } from "@/stores";
import { changeLocalPath } from "@/utils/helper";
import { changeLocalLyricPath, changeLocalMusicPath, changeLocalPath } from "@/utils/helper";
const settingStore = useSettingStore();

View File

@@ -135,6 +135,8 @@ interface SettingState {
preventSleep: boolean;
/** 本地文件路径 */
localFilesPath: string[];
/** 本地歌词路径 */
localLyricPath: string[];
/** 本地文件分隔符 */
localSeparators: string[];
/** 显示本地封面 */
@@ -216,6 +218,7 @@ export const useSettingStore = defineStore("setting", {
lrcMousePause: false,
excludeKeywords: keywords,
localFilesPath: [],
localLyricPath: [],
showDefaultLocalPath: true,
localSeparators: ["/", "&"],
showLocalCover: true,

View File

@@ -302,37 +302,71 @@ export const getUpdateLog = async (): Promise<UpdateLogType[]> => {
};
/**
* 更改本地目录
* @param delIndex 删除文件夹路径的索引
* 获取 更改本地目录 函数
* @param settingsKey 设置项 key
* @param includeSubFolders 是否包含子文件夹
* @param errorConsole 控制台输出的错误信息
* @param errorMessage 错误信息
* @param defaultPath 默认路径
*/
export const changeLocalPath = async (delIndex?: number) => {
const changeLocalPath = (
settingsKey: string, includeSubFolders: boolean, errorConsole: string, errorMessage: string, defaultPath?: string
) => async (delIndex?: number) => {
try {
if (!isElectron) return;
const settingStore = useSettingStore();
if (typeof delIndex === "number" && delIndex >= 0) {
settingStore.localFilesPath.splice(delIndex, 1);
settingStore[settingsKey].splice(delIndex, 1);
} else {
const selectedDir = await window.electron.ipcRenderer.invoke("choose-path");
if (!selectedDir) return;
// 检查是否为子文件夹
const defaultMusicPath = await window.electron.ipcRenderer.invoke("get-default-dir", "music");
const allPath = [defaultMusicPath, ...settingStore.localFilesPath];
const allPath = defaultPath ? [defaultPath, ...settingStore[settingsKey]] : settingStore[settingsKey];
if (includeSubFolders) {
const isSubfolder = await window.electron.ipcRenderer.invoke(
"check-if-subfolder",
allPath,
selectedDir,
);
if (!isSubfolder) {
settingStore.localFilesPath.push(selectedDir);
settingStore[settingsKey].push(selectedDir);
} else {
window.$message.error("添加的目录与现有目录有重叠,请重新选择");
}
} else {
if (allPath.includes(selectedDir)) {
window.$message.error("添加的目录已存在");
} else {
settingStore[settingsKey].push(selectedDir);
}
}
}
} catch (error) {
console.error("Error changing local path:", error);
window.$message.error("更改本地歌曲文件夹出错,请重试");
console.error(`${errorConsole}: `, error);
window.$message.error(errorMessage);
}
};
}
/**
* 更改本地音乐目录
* @param delIndex 删除文件夹路径的索引
*/
export const changeLocalMusicPath = changeLocalPath(
"localFilesPath", true,
"Error changing local path",
"更改本地歌曲文件夹出错,请重试",
await window.electron.ipcRenderer.invoke("get-default-dir", "music")
);
/**
* 更改本地歌词目录
* @param delIndex 删除文件夹路径的索引
*/
export const changeLocalLyricPath = changeLocalPath(
"localLyricPath", false,
"Error changing local lyric path",
"更改本地歌词文件夹出错,请重试",
)
/**
* 洗牌数组Fisher-Yates

View File

@@ -18,9 +18,10 @@ export const getLyricData = async (id: number) => {
try {
const musicStore = useMusicStore();
const settingStore = useSettingStore();
const getLyric = getLyricFun(settingStore.localLyricPath, id);
const [lyricRes, ttmlContent] = await Promise.all([
songLyric(id),
settingStore.enableTTMLLyric && songLyricTTML(id),
getLyric("lrc", songLyric),
settingStore.enableTTMLLyric && getLyric("ttml", songLyricTTML),
]);
parsedLyricsData(lyricRes);
if (ttmlContent) {
@@ -51,3 +52,11 @@ export const getLyricData = async (id: number) => {
resetSongLyric();
}
};
const getLyricFun = (paths: string[], id: number) => async (ext: string, getOnline: (id: number) => Promise<string | null>): Promise<string | null> => {
for (let path of paths) {
const lyric = await window.electron.ipcRenderer.invoke("read-local-lyric", path, id, ext);
if (lyric) return lyric;
}
return await getOnline(id);
};