mirror of
https://github.com/imsyy/SPlayer.git
synced 2025-11-25 11:29:26 +08:00
feat: 支持从本地读取歌词覆盖线上歌词
This commit is contained in:
@@ -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 {
|
||||
|
||||
@@ -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();
|
||||
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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 isSubfolder = await window.electron.ipcRenderer.invoke(
|
||||
"check-if-subfolder",
|
||||
allPath,
|
||||
selectedDir,
|
||||
);
|
||||
if (!isSubfolder) {
|
||||
settingStore.localFilesPath.push(selectedDir);
|
||||
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[settingsKey].push(selectedDir);
|
||||
} else {
|
||||
window.$message.error("添加的目录与现有目录有重叠,请重新选择");
|
||||
}
|
||||
} else {
|
||||
window.$message.error("添加的目录与现有目录有重叠,请重新选择");
|
||||
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)
|
||||
|
||||
@@ -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);
|
||||
};
|
||||
|
||||
Reference in New Issue
Block a user