chore: 数据库

This commit is contained in:
maotoumao
2025-06-29 14:05:37 +08:00
parent 8daa1dfc6e
commit 3059d06db6
4 changed files with 1487 additions and 13 deletions

View File

@@ -3,5 +3,10 @@
"files.associations": {
"*.html": "html",
"map": "cpp"
}
}
},
"editor.defaultFormatter": "dbaeumer.vscode-eslint",
"[typescript]": {
"editor.defaultFormatter": "vscode.typescript-language-features"
},
"eslint.format.enable": true
}

View File

@@ -0,0 +1,849 @@
/**
* 数据库模块
*
* 此模块负责加载数据库相关的功能,提供渲染进程需要的业务逻辑。
*/
import { app } from "electron";
import path from "node:path";
import Database from "better-sqlite3";
const appDbPath = path.resolve(app.getPath("userData"), "./app-database/database.db");
const database = new Database(appDbPath);
database.pragma("journal_mode = WAL");
database.pragma("foreign_keys = ON"); // 启用外键支持
database.pragma("synchronous = NORMAL"); // WAL模式下推荐设置
// 数据库版本号
const DATABASE_LATEST_VERSION = 1;
// 创建初始表结构
function createInitialTables() {
// 创建歌单表IMusicSheetModel
database.exec(`
CREATE TABLE IF NOT EXISTS LocalMusicSheets (
platform TEXT NOT NULL,
id TEXT NOT NULL,
title TEXT NOT NULL,
artwork TEXT,
description TEXT,
worksNum INTEGER DEFAULT 0,
playCount INTEGER DEFAULT 0,
createAt INTEGER,
artist TEXT,
_raw TEXT NOT NULL, -- (存储原始JSON数据)
_sortIndex REAL, -- $$sortIndex
-- 联合主键
PRIMARY KEY (platform, id)
);
`);
// 创建音乐项表IMusicItemModel
database.exec(`
CREATE TABLE IF NOT EXISTS LocalMusicItems (
_key INTEGER PRIMARY KEY AUTOINCREMENT,
platform TEXT NOT NULL,
id TEXT NOT NULL,
artist TEXT NOT NULL,
title TEXT NOT NULL,
duration REAL,
album TEXT,
artwork TEXT,
_timestamp INTEGER NOT NULL,
_raw TEXT NOT NULL, -- 替代 $$raw
_sortIndex REAL,
_musicSheetId TEXT NOT NULL, -- 替代 $$musicSheetId
_musicSheetPlatform TEXT NOT NULL, -- 替代 $$musicSheetPlatform
-- 添加复合唯一约束防止同一歌单重复添加相同歌曲
UNIQUE (_musicSheetPlatform, _musicSheetId, platform, id),
-- 外键引用歌单表的联合主键
FOREIGN KEY (_musicSheetPlatform, _musicSheetId)
REFERENCES LocalMusicSheets(platform, id)
ON DELETE CASCADE
ON UPDATE CASCADE
);
`);
// 创建索引优化查询性能
database.exec("CREATE INDEX IF NOT EXISTS idx_items_coreid ON LocalMusicItems(platform, id)");
database.exec("CREATE INDEX IF NOT EXISTS idx_items_sheet ON LocalMusicItems(_musicSheetPlatform, _musicSheetId)");
database.exec("CREATE INDEX IF NOT EXISTS idx_sheets_platform ON LocalMusicSheets(platform)");
database.exec("CREATE INDEX IF NOT EXISTS idx_items_artist ON LocalMusicItems(artist)");
database.exec("CREATE INDEX IF NOT EXISTS idx_sheets_sort ON LocalMusicSheets(_sortIndex)");
// 创建star的歌单表IMusicSheetModel
database.exec(`
CREATE TABLE IF NOT EXISTS StarredMusicSheets (
platform TEXT NOT NULL,
id TEXT NOT NULL,
title TEXT NOT NULL,
artwork TEXT,
description TEXT,
worksNum INTEGER DEFAULT 0,
playCount INTEGER DEFAULT 0,
createAt INTEGER,
artist TEXT,
_raw TEXT NOT NULL, -- $$raw (存储原始JSON数据)
_sortIndex REAL, -- $$sortIndex
-- 联合主键
PRIMARY KEY (platform, id)
);
`);
}
function migrateDatabase() {
let currentVersion = database.pragma("user_version", { simple: true }) as number;
if (currentVersion >= DATABASE_LATEST_VERSION) {
return;
}
if (!currentVersion) {
currentVersion = 0;
}
// 在事务中执行升级
const upgrade = database.transaction(() => {
for (let version = currentVersion + 1; version <= DATABASE_LATEST_VERSION; version++) {
switch (version) {
case 1:
createInitialTables();
break;
// 未来的版本升级可以在这里添加
// case 2:
// upgradeToVersion2();
// break;
default:
throw new Error(`Unknown database version: ${version}`);
}
database.pragma(`user_version = ${version}`);
}
});
upgrade();
}
migrateDatabase();
//////////////////// 歌单增删查改
class LocalMusicSheetDB {
private static readonly SORT_BASE = 1000; // 排序基础值
private static readonly SORT_INCREMENT = 1000; // 排序增量
private static readonly MIN_SORT_INTERVAL = 0.000001; // 最小排序间隔,低于此值需要重新均衡
/**
* 添加歌单
* @param musicSheet 歌单数据
* @returns 是否添加成功
*/
static addMusicSheet(musicSheet: IDataBaseModel.IMusicSheetModel): boolean {
try {
const insertStmt = database.prepare(`
INSERT INTO LocalMusicSheets
(platform, id, title, artwork, description, worksNum, playCount, createAt, artist, _raw, _sortIndex)
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
`);
// 如果没有指定排序索引,设置为当前最大值+增量
let sortIndex = musicSheet._sortIndex;
if (sortIndex === undefined || sortIndex === null) {
const maxSortStmt = database.prepare("SELECT MAX(_sortIndex) as maxSort FROM LocalMusicSheets");
const result = maxSortStmt.get() as { maxSort: number | null };
sortIndex = (result.maxSort || 0) + this.SORT_INCREMENT;
}
const result = insertStmt.run(
musicSheet.platform,
musicSheet.id,
musicSheet.title,
musicSheet.artwork || null,
musicSheet.description || null,
musicSheet.worksNum || 0,
musicSheet.playCount || 0,
musicSheet.createAt || Date.now(),
musicSheet.artist || null,
musicSheet._raw,
sortIndex,
);
return result.changes > 0;
} catch (error) {
console.error("添加歌单失败:", error);
return false;
}
}
/**
* 批量添加歌单
* @param musicSheets 歌单数据数组
* @returns 成功添加的数量
*/
static batchAddMusicSheets(musicSheets: IDataBaseModel.IMusicSheetModel[]): number {
if (!musicSheets.length) return 0;
try {
const transaction = database.transaction(() => {
let successCount = 0;
// 获取当前最大排序值
const maxSortStmt = database.prepare("SELECT MAX(_sortIndex) as maxSort FROM LocalMusicSheets");
const result = maxSortStmt.get() as { maxSort: number | null };
let currentMaxSort = result.maxSort || 0;
const insertStmt = database.prepare(`
INSERT INTO LocalMusicSheets
(platform, id, title, artwork, description, worksNum, playCount, createAt, artist, _raw, _sortIndex)
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
`);
for (const sheet of musicSheets) {
try {
let sortIndex = sheet._sortIndex;
if (sortIndex === undefined || sortIndex === null) {
currentMaxSort += this.SORT_INCREMENT;
sortIndex = currentMaxSort;
}
const insertResult = insertStmt.run(
sheet.platform,
sheet.id,
sheet.title,
sheet.artwork || null,
sheet.description || null,
sheet.worksNum || 0,
sheet.playCount || 0,
sheet.createAt || Date.now(),
sheet.artist || null,
sheet._raw,
sortIndex,
);
if (insertResult.changes > 0) {
successCount++;
}
} catch (error) {
console.warn(`批量添加歌单失败 (${sheet.platform}:${sheet.id}):`, error);
}
}
return successCount;
});
return transaction();
} catch (error) {
console.error("批量添加歌单失败:", error);
return 0;
}
}
/**
* 删除歌单
* @param platform 平台
* @param id 歌单ID
* @returns 是否删除成功
*/
static deleteMusicSheet(platform: string, id: string): boolean {
try {
const deleteStmt = database.prepare("DELETE FROM LocalMusicSheets WHERE platform = ? AND id = ?");
const result = deleteStmt.run(platform, id);
return result.changes > 0;
} catch (error) {
console.error("删除歌单失败:", error);
return false;
}
}
/**
* 批量删除歌单
* @param sheets 要删除的歌单标识数组 {platform, id}
* @returns 成功删除的数量
*/
static batchDeleteMusicSheets(sheets: Array<{ platform: string; id: string }>): number {
if (!sheets.length) return 0;
try {
const transaction = database.transaction(() => {
let successCount = 0;
const deleteStmt = database.prepare("DELETE FROM LocalMusicSheets WHERE platform = ? AND id = ?");
for (const sheet of sheets) {
try {
const result = deleteStmt.run(sheet.platform, sheet.id);
if (result.changes > 0) {
successCount++;
}
} catch (error) {
console.warn(`批量删除歌单失败 (${sheet.platform}:${sheet.id}):`, error);
}
}
return successCount;
});
return transaction();
} catch (error) {
console.error("批量删除歌单失败:", error);
return 0;
}
}
/**
* 查询单个歌单
* @param platform 平台
* @param id 歌单ID
* @returns 歌单数据或null
*/
static getMusicSheet(platform: string, id: string): IDataBaseModel.IMusicSheetModel | null {
try {
const selectStmt = database.prepare(`
SELECT * FROM LocalMusicSheets
WHERE platform = ? AND id = ?
`);
const result = selectStmt.get(platform, id) as any;
if (!result) return null;
return {
platform: result.platform,
id: result.id,
title: result.title,
artwork: result.artwork,
description: result.description,
worksNum: result.worksNum,
playCount: result.playCount,
createAt: result.createAt,
artist: result.artist,
_raw: result._raw,
_sortIndex: result._sortIndex,
};
} catch (error) {
console.error("查询歌单失败:", error);
return null;
}
}
/**
* 查询所有歌单
* @param orderBy 排序字段默认按_sortIndex排序
* @param order 排序方向,'ASC' 或 'DESC'默认ASC
* @returns 歌单数组
*/
static getAllMusicSheets(orderBy: string = "_sortIndex", order: "ASC" | "DESC" = "ASC"): IDataBaseModel.IMusicSheetModel[] {
try {
// 验证排序字段防止SQL注入
const allowedFields = ["_sortIndex", "title", "createAt", "artist", "playCount"];
if (!allowedFields.includes(orderBy)) {
orderBy = "_sortIndex";
}
const selectStmt = database.prepare(`
SELECT * FROM LocalMusicSheets
ORDER BY ${orderBy} ${order}
`);
const results = selectStmt.all() as any[];
return results.map(result => ({
platform: result.platform,
id: result.id,
title: result.title,
artwork: result.artwork,
description: result.description,
worksNum: result.worksNum,
playCount: result.playCount,
createAt: result.createAt,
artist: result.artist,
_raw: result._raw,
_sortIndex: result._sortIndex,
}));
} catch (error) {
console.error("查询所有歌单失败:", error);
return [];
}
}
/**
* 按平台查询歌单
* @param platform 平台名称
* @param orderBy 排序字段
* @param order 排序方向
* @returns 歌单数组
*/
static getMusicSheetsByPlatform(platform: string, orderBy: string = "_sortIndex", order: "ASC" | "DESC" = "ASC"): IDataBaseModel.IMusicSheetModel[] {
try {
const allowedFields = ["_sortIndex", "title", "createAt", "artist", "playCount"];
if (!allowedFields.includes(orderBy)) {
orderBy = "_sortIndex";
}
const selectStmt = database.prepare(`
SELECT * FROM LocalMusicSheets
WHERE platform = ?
ORDER BY ${orderBy} ${order}
`);
const results = selectStmt.all(platform) as any[];
return results.map(result => ({
platform: result.platform,
id: result.id,
title: result.title,
artwork: result.artwork,
description: result.description,
worksNum: result.worksNum,
playCount: result.playCount,
createAt: result.createAt,
artist: result.artist,
_raw: result._raw,
_sortIndex: result._sortIndex,
}));
} catch (error) {
console.error("按平台查询歌单失败:", error);
return [];
}
}
/**
* 更新歌单(部分更新)
* @param platform 平台
* @param id 歌单ID
* @param updates 要更新的字段
* @returns 是否更新成功
*/
static updateMusicSheet(
platform: string,
id: string,
updates: Partial<Omit<IDataBaseModel.IMusicSheetModel, "platform" | "id">>,
): boolean {
try {
if (Object.keys(updates).length === 0) {
return false;
}
// 构建动态更新SQL
const allowedFields = ["title", "artwork", "description", "worksNum", "playCount", "createAt", "artist", "_raw", "_sortIndex"];
const updateFields: string[] = [];
const values: any[] = [];
for (const [key, value] of Object.entries(updates)) {
if (allowedFields.includes(key) && value !== undefined) {
updateFields.push(`${key} = ?`);
values.push(value);
}
}
if (updateFields.length === 0) {
return false;
}
values.push(platform, id);
const updateStmt = database.prepare(`
UPDATE LocalMusicSheets
SET ${updateFields.join(", ")}
WHERE platform = ? AND id = ?
`);
const result = updateStmt.run(...values);
return result.changes > 0;
} catch (error) {
console.error("更新歌单失败:", error);
return false;
}
}
/**
* 批量更新歌单
* @param updates 更新数据数组,每个元素包含 platform, id 和要更新的字段
* @returns 成功更新的数量
*/
static batchUpdateMusicSheets(
updates: Array<{
platform: string;
id: string;
data: Partial<Omit<IDataBaseModel.IMusicSheetModel, "platform" | "id">>;
}>,
): number {
if (!updates.length) return 0;
try {
const transaction = database.transaction(() => {
let successCount = 0;
for (const update of updates) {
try {
if (this.updateMusicSheet(update.platform, update.id, update.data)) {
successCount++;
}
} catch (error) {
console.warn(`批量更新歌单失败 (${update.platform}:${update.id}):`, error);
}
}
return successCount;
});
return transaction();
} catch (error) {
console.error("批量更新歌单失败:", error);
return 0;
}
}
/**
* 更新歌单排序
* @param platform 平台
* @param id 歌单ID
* @param newSortIndex 新的排序索引
* @returns 是否更新成功
*/
static updateMusicSheetSort(platform: string, id: string, newSortIndex: number): boolean {
try {
const updateStmt = database.prepare(`
UPDATE LocalMusicSheets
SET _sortIndex = ?
WHERE platform = ? AND id = ?
`);
const result = updateStmt.run(newSortIndex, platform, id);
return result.changes > 0;
} catch (error) {
console.error("更新歌单排序失败:", error);
return false;
}
}
/**
* 在两个歌单之间插入新歌单(使用浮点数排序法)
* @param platform 平台
* @param id 歌单ID
* @param afterPlatform 插入位置前一个歌单的平台null表示插入到开头
* @param afterId 插入位置前一个歌单的IDnull表示插入到开头
* @param beforePlatform 插入位置后一个歌单的平台null表示插入到末尾
* @param beforeId 插入位置后一个歌单的IDnull表示插入到末尾
* @returns 是否更新成功
*/
static insertMusicSheetBetween(
platform: string,
id: string,
afterPlatform: string | null,
afterId: string | null,
beforePlatform: string | null,
beforeId: string | null,
): boolean {
try {
let newSortIndex: number;
if (!afterPlatform && !afterId) {
// 插入到开头
const firstStmt = database.prepare("SELECT MIN(_sortIndex) as minSort FROM LocalMusicSheets");
const result = firstStmt.get() as { minSort: number | null };
newSortIndex = (result.minSort || this.SORT_BASE) - this.SORT_INCREMENT;
} else if (!beforePlatform && !beforeId) {
// 插入到末尾
const lastStmt = database.prepare("SELECT MAX(_sortIndex) as maxSort FROM LocalMusicSheets");
const result = lastStmt.get() as { maxSort: number | null };
newSortIndex = (result.maxSort || this.SORT_BASE) + this.SORT_INCREMENT;
} else {
// 插入到中间
const afterStmt = database.prepare("SELECT _sortIndex FROM LocalMusicSheets WHERE platform = ? AND id = ?");
const beforeStmt = database.prepare("SELECT _sortIndex FROM LocalMusicSheets WHERE platform = ? AND id = ?");
const afterResult = afterStmt.get(afterPlatform, afterId) as { _sortIndex: number } | undefined;
const beforeResult = beforeStmt.get(beforePlatform, beforeId) as { _sortIndex: number } | undefined;
if (!afterResult || !beforeResult) {
return false;
}
newSortIndex = (afterResult._sortIndex + beforeResult._sortIndex) / 2;
// 检查是否需要重新均衡
if (Math.abs(beforeResult._sortIndex - afterResult._sortIndex) < this.MIN_SORT_INTERVAL) {
this.rebalanceSortIndexes();
// 重新计算新的排序值
const newAfterResult = afterStmt.get(afterPlatform, afterId) as { _sortIndex: number } | undefined;
const newBeforeResult = beforeStmt.get(beforePlatform, beforeId) as { _sortIndex: number } | undefined;
if (newAfterResult && newBeforeResult) {
newSortIndex = (newAfterResult._sortIndex + newBeforeResult._sortIndex) / 2;
}
}
}
return this.updateMusicSheetSort(platform, id, newSortIndex);
} catch (error) {
console.error("插入歌单排序失败:", error);
return false;
}
}
/**
* 重新均衡所有歌单的排序索引
* 当排序间隔过小时调用此方法
*/
static rebalanceSortIndexes(): boolean {
try {
const transaction = database.transaction(() => {
// 获取所有歌单按当前排序
const selectStmt = database.prepare("SELECT platform, id FROM LocalMusicSheets ORDER BY _sortIndex ASC");
const sheets = selectStmt.all() as Array<{ platform: string; id: string }>;
const updateStmt = database.prepare("UPDATE LocalMusicSheets SET _sortIndex = ? WHERE platform = ? AND id = ?");
// 重新分配排序索引每个间隔1000
sheets.forEach((sheet, index) => {
const newSortIndex = this.SORT_BASE + (index * this.SORT_INCREMENT);
updateStmt.run(newSortIndex, sheet.platform, sheet.id);
});
return true;
});
return transaction();
} catch (error) {
console.error("重新均衡排序索引失败:", error);
return false;
}
}
/**
* 获取歌单中的所有歌曲
* @param platform 歌单平台
* @param id 歌单ID
* @param orderBy 排序字段默认按_sortIndex排序
* @param order 排序方向
* @returns 歌曲数组
*/
static getMusicItemsInSheet(
platform: string,
id: string,
orderBy: string = "_sortIndex",
order: "ASC" | "DESC" = "ASC",
): IDataBaseModel.IMusicItemModel[] {
try {
const allowedFields = ["_sortIndex", "title", "artist", "album", "_timestamp"];
if (!allowedFields.includes(orderBy)) {
orderBy = "_sortIndex";
}
const selectStmt = database.prepare(`
SELECT * FROM LocalMusicItems
WHERE _musicSheetPlatform = ? AND _musicSheetId = ?
ORDER BY ${orderBy} ${order}
`);
const results = selectStmt.all(platform, id) as any[];
return results.map(result => ({
platform: result.platform,
id: result.id,
artist: result.artist,
title: result.title,
duration: result.duration,
album: result.album,
artwork: result.artwork,
_timestamp: result._timestamp,
_raw: result._raw,
_sortIndex: result._sortIndex,
_musicSheetId: result._musicSheetId,
_musicSheetPlatform: result._musicSheetPlatform,
}));
} catch (error) {
console.error("获取歌单歌曲失败:", error);
return [];
}
}
/**
* 获取歌单中歌曲的数量
* @param platform 歌单平台
* @param id 歌单ID
* @returns 歌曲数量
*/
static getMusicItemCountInSheet(platform: string, id: string): number {
try {
const countStmt = database.prepare(`
SELECT COUNT(*) as count FROM LocalMusicItems
WHERE _musicSheetPlatform = ? AND _musicSheetId = ?
`);
const result = countStmt.get(platform, id) as { count: number };
return result.count;
} catch (error) {
console.error("获取歌单歌曲数量失败:", error);
return 0;
}
}
/**
* 检查歌单是否存在
* @param platform 平台
* @param id 歌单ID
* @returns 是否存在
*/
static existsMusicSheet(platform: string, id: string): boolean {
try {
const countStmt = database.prepare("SELECT COUNT(*) as count FROM LocalMusicSheets WHERE platform = ? AND id = ?");
const result = countStmt.get(platform, id) as { count: number };
return result.count > 0;
} catch (error) {
console.error("检查歌单是否存在失败:", error);
return false;
}
}
/**
* 搜索歌单
* @param keyword 搜索关键词
* @param searchFields 搜索字段数组默认搜索title和artist
* @returns 匹配的歌单数组
*/
static searchMusicSheets(
keyword: string,
searchFields: string[] = ["title", "artist"],
): IDataBaseModel.IMusicSheetModel[] {
try {
if (!keyword.trim()) {
return this.getAllMusicSheets();
}
const allowedFields = ["title", "artist", "description"];
const validFields = searchFields.filter(field => allowedFields.includes(field));
if (validFields.length === 0) {
validFields.push("title");
}
const whereConditions = validFields.map(field => `${field} LIKE ?`).join(" OR ");
const searchPattern = `%${keyword}%`;
const params = validFields.map(() => searchPattern);
const searchStmt = database.prepare(`
SELECT * FROM LocalMusicSheets
WHERE ${whereConditions}
ORDER BY _sortIndex ASC
`);
const results = searchStmt.all(...params) as any[];
return results.map(result => ({
platform: result.platform,
id: result.id,
title: result.title,
artwork: result.artwork,
description: result.description,
worksNum: result.worksNum,
playCount: result.playCount,
createAt: result.createAt,
artist: result.artist,
_raw: result._raw,
_sortIndex: result._sortIndex,
}));
} catch (error) {
console.error("搜索歌单失败:", error);
return [];
}
}
/**
* 批量移动歌单到指定位置(支持所有拖拽和排序场景)
* @param selectedSheets 要移动的歌单标识数组
* @param targetPlatform 目标歌单的平台null表示移动到开头/末尾)
* @param targetId 目标歌单的IDnull表示移动到开头/末尾)
* @param position 相对于目标歌单的位置:"before" | "after",默认"after"
* @returns 成功移动的数量
*
* @example
* // 移动到开头
* batchMoveMusicSheets(sheets, null, null, "before")
*
* // 移动到末尾
* batchMoveMusicSheets(sheets, null, null, "after")
*
* // 移动到指定歌单之前
* batchMoveMusicSheets(sheets, "platform1", "id1", "before")
*
* // 移动到指定歌单之后
* batchMoveMusicSheets(sheets, "platform1", "id1", "after")
*/
static batchMoveMusicSheets(
selectedSheets: Array<{ platform: string; id: string }>,
targetPlatform: string | null = null,
targetId: string | null = null, position: "before" | "after" = "after",
): number {
if (!selectedSheets.length) return 0;
try {
const transaction = database.transaction(() => {
// 获取所有歌单的排序信息
const allSheetsStmt = database.prepare(`
SELECT platform, id, _sortIndex
FROM LocalMusicSheets
ORDER BY _sortIndex ASC
`);
const allSheets = allSheetsStmt.all() as Array<{
platform: string;
id: string;
_sortIndex: number;
}>;
// 分离要移动的歌单和剩余的歌单
const selectedSheetIds = new Set(selectedSheets.map(s => `${s.platform}:${s.id}`));
const sheetsToMove = allSheets.filter(s => selectedSheetIds.has(`${s.platform}:${s.id}`));
const remainingSheets = allSheets.filter(s => !selectedSheetIds.has(`${s.platform}:${s.id}`));
if (!sheetsToMove.length) return 0;
// 计算插入位置
let insertIndex = 0;
if (targetPlatform && targetId) {
const targetIndex = remainingSheets.findIndex(s =>
s.platform === targetPlatform && s.id === targetId,
);
insertIndex = targetIndex === -1 ? 0 :
(position === "after" ? targetIndex + 1 : targetIndex);
} else {
insertIndex = position === "before" ? 0 : remainingSheets.length;
}
// 构建新的排序数组并重新分配排序索引
const newOrderedSheets = [...remainingSheets];
newOrderedSheets.splice(insertIndex, 0, ...sheetsToMove);
const updateStmt = database.prepare(`
UPDATE LocalMusicSheets SET _sortIndex = ? WHERE platform = ? AND id = ?
`);
let successCount = 0;
newOrderedSheets.forEach((sheet, index) => {
const newSortIndex = this.SORT_BASE + (index * this.SORT_INCREMENT);
try {
const result = updateStmt.run(newSortIndex, sheet.platform, sheet.id);
if (result.changes > 0 && selectedSheetIds.has(`${sheet.platform}:${sheet.id}`)) {
successCount++;
}
} catch (error) {
console.warn(`批量移动歌单失败 (${sheet.platform}:${sheet.id}):`, error);
}
});
return successCount;
});
return transaction();
} catch (error) {
console.error("批量移动歌单失败:", error);
return 0;
}
}
}
// 导出数据库实例和类
export { database, LocalMusicSheetDB };

View File

@@ -4,12 +4,10 @@
* 此模块负责加载数据库相关的功能,提供渲染进程需要的业务逻辑。
*/
import { app } from "electron";
import path from "node:path";
import Database from "better-sqlite3";
const appDbPath = path.resolve(app.getPath("userData"), "./app-database/database.db");
const database = new Database(appDbPath);
@@ -17,8 +15,11 @@ database.pragma("journal_mode = WAL");
database.pragma("foreign_keys = ON"); // 启用外键支持
database.pragma("synchronous = NORMAL"); // WAL模式下推荐设置
// 初始化数据库
function setupDatabase() {
// 数据库版本号
const DATABASE_LATEST_VERSION = 1;
// 创建初始表结构
function createInitialTables() {
// 创建歌单表IMusicSheetModel
database.exec(`
CREATE TABLE IF NOT EXISTS LocalMusicSheets (
@@ -42,6 +43,7 @@ function setupDatabase() {
// 创建音乐项表IMusicItemModel
database.exec(`
CREATE TABLE IF NOT EXISTS LocalMusicItems (
_key INTEGER PRIMARY KEY AUTOINCREMENT,
platform TEXT NOT NULL,
id TEXT NOT NULL,
artist TEXT NOT NULL,
@@ -49,13 +51,14 @@ function setupDatabase() {
duration REAL,
album TEXT,
artwork TEXT,
_timestamp INTEGER NOT NULL,
_raw TEXT NOT NULL, -- 替代 $$raw
_sortOrder REAL,
_sortIndex REAL,
_musicSheetId TEXT NOT NULL, -- 替代 $$musicSheetId
_musicSheetPlatform TEXT NOT NULL, -- 替代 $$musicSheetPlatform
-- 联合主键
PRIMARY KEY (platform, id),
-- 添加复合唯一约束防止同一歌单重复添加相同歌曲
UNIQUE (_musicSheetPlatform, _musicSheetId, platform, id),
-- 外键引用歌单表的联合主键
FOREIGN KEY (_musicSheetPlatform, _musicSheetId)
@@ -66,6 +69,7 @@ function setupDatabase() {
`);
// 创建索引优化查询性能
database.exec("CREATE INDEX IF NOT EXISTS idx_items_coreid ON LocalMusicItems(platform, id)");
database.exec("CREATE INDEX IF NOT EXISTS idx_items_sheet ON LocalMusicItems(_musicSheetPlatform, _musicSheetId)");
database.exec("CREATE INDEX IF NOT EXISTS idx_sheets_platform ON LocalMusicSheets(platform)");
database.exec("CREATE INDEX IF NOT EXISTS idx_items_artist ON LocalMusicItems(artist)");
@@ -84,7 +88,7 @@ function setupDatabase() {
createAt INTEGER,
artist TEXT,
_raw TEXT NOT NULL, -- $$raw (存储原始JSON数据)
_sortIndex INTEGER DEFAULT 0, -- $$sortIndex
_sortIndex REAL, -- $$sortIndex
-- 联合主键
PRIMARY KEY (platform, id)
@@ -92,6 +96,620 @@ function setupDatabase() {
`);
}
setupDatabase();
function migrateDatabase() {
let currentVersion = database.pragma("user_version", { simple: true }) as number;
if (currentVersion >= DATABASE_LATEST_VERSION) {
return;
}
if (!currentVersion) {
currentVersion = 0;
}
// 在事务中执行升级
const upgrade = database.transaction(() => {
for (let version = currentVersion + 1; version <= DATABASE_LATEST_VERSION; version++) {
switch (version) {
case 1:
createInitialTables();
break;
// 未来的版本升级可以在这里添加
// case 2:
// upgradeToVersion2();
// break;
default:
throw new Error(`Unknown database version: ${version}`);
}
database.pragma(`user_version = ${version}`);
}
});
upgrade();
}
migrateDatabase();
//////////////////// 歌单增删查改
class LocalMusicSheetDB {
private static readonly SORT_BASE = 1000; // 排序基础值
private static readonly SORT_INCREMENT = 1000; // 排序增量
/**
* 添加歌单
* @param musicSheet 歌单数据
* @returns 是否添加成功
*/
static addMusicSheet(musicSheet: IDataBaseModel.IMusicSheetModel): boolean {
try {
const insertStmt = database.prepare(`
INSERT INTO LocalMusicSheets
(platform, id, title, artwork, description, worksNum, playCount, createAt, artist, _raw, _sortIndex)
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
`);
// 如果没有指定排序索引,设置为当前最大值+增量
let sortIndex = musicSheet._sortIndex;
if (sortIndex === undefined || sortIndex === null) {
const maxSortStmt = database.prepare("SELECT MAX(_sortIndex) as maxSort FROM LocalMusicSheets");
const result = maxSortStmt.get() as { maxSort: number | null };
sortIndex = (result.maxSort || 0) + this.SORT_INCREMENT;
}
const result = insertStmt.run(
musicSheet.platform,
musicSheet.id,
musicSheet.title,
musicSheet.artwork || null,
musicSheet.description || null,
musicSheet.worksNum || 0,
musicSheet.playCount || 0,
musicSheet.createAt || Date.now(),
musicSheet.artist || null,
musicSheet._raw,
sortIndex,
);
return result.changes > 0;
} catch {
return false;
}
}
/**
* 批量添加歌单
* @param musicSheets 歌单数据数组
* @returns 成功添加的数量
*/
static batchAddMusicSheets(musicSheets: IDataBaseModel.IMusicSheetModel[]): number {
if (!musicSheets.length) {
return 0;
}
try {
const transaction = database.transaction(() => {
let successCount = 0;
// 获取当前最大排序值
const maxSortStmt = database.prepare("SELECT MAX(_sortIndex) as maxSort FROM LocalMusicSheets");
const result = maxSortStmt.get() as { maxSort: number | null };
let currentMaxSort = result.maxSort || 0;
const insertStmt = database.prepare(`
INSERT INTO LocalMusicSheets
(platform, id, title, artwork, description, worksNum, playCount, createAt, artist, _raw, _sortIndex)
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
`);
for (const sheet of musicSheets) {
try {
let sortIndex = sheet._sortIndex;
if (sortIndex === undefined || sortIndex === null) {
currentMaxSort += this.SORT_INCREMENT;
sortIndex = currentMaxSort;
}
const insertResult = insertStmt.run(
sheet.platform,
sheet.id,
sheet.title,
sheet.artwork || null,
sheet.description || null,
sheet.worksNum || 0,
sheet.playCount || 0,
sheet.createAt || Date.now(),
sheet.artist || null,
sheet._raw,
sortIndex,
);
if (insertResult.changes > 0) {
successCount++;
}
} catch {
// 忽略单个添加失败的情况
}
}
return successCount;
});
return transaction();
} catch {
return 0;
}
}
/**
* 删除歌单
* @param platform 平台
* @param id 歌单ID
* @returns 是否删除成功
*/
static deleteMusicSheet(platform: string, id: string): boolean {
try {
const deleteStmt = database.prepare("DELETE FROM LocalMusicSheets WHERE platform = ? AND id = ?");
const result = deleteStmt.run(platform, id);
return result.changes > 0;
} catch {
return false;
}
}
/**
* 批量删除歌单
* @param sheets 要删除的歌单标识数组 {platform, id}
* @returns 成功删除的数量
*/
static batchDeleteMusicSheets(sheets: Array<{ platform: string; id: string }>): number {
if (!sheets.length) {
return 0;
}
try {
const transaction = database.transaction(() => {
let successCount = 0;
const deleteStmt = database.prepare("DELETE FROM LocalMusicSheets WHERE platform = ? AND id = ?");
for (const sheet of sheets) {
try {
const result = deleteStmt.run(sheet.platform, sheet.id);
if (result.changes > 0) {
successCount++;
}
} catch {
// 忽略单个删除失败的情况
}
}
return successCount;
});
return transaction();
} catch {
return 0;
}
}
/**
* 查询单个歌单
* @param platform 平台
* @param id 歌单ID
* @returns 歌单数据或null
*/
static getMusicSheet(platform: string, id: string): IDataBaseModel.IMusicSheetModel | null {
try {
const selectStmt = database.prepare(`
SELECT * FROM LocalMusicSheets
WHERE platform = ? AND id = ?
`);
const result = selectStmt.get(platform, id) as any;
if (!result) {
return null;
}
return {
platform: result.platform,
id: result.id,
title: result.title,
artwork: result.artwork,
description: result.description,
worksNum: result.worksNum,
playCount: result.playCount,
createAt: result.createAt,
artist: result.artist,
_raw: result._raw,
_sortIndex: result._sortIndex,
};
} catch {
return null;
}
}
/**
* 查询所有歌单
* @param orderBy 排序字段默认按_sortIndex排序
* @param order 排序方向,'ASC' 或 'DESC'默认ASC
* @returns 歌单数组
*/
static getAllMusicSheets(orderBy: string = "_sortIndex", order: "ASC" | "DESC" = "ASC"): IDataBaseModel.IMusicSheetModel[] {
try {
// 验证排序字段防止SQL注入
const allowedFields = ["_sortIndex", "title", "createAt", "artist", "playCount"];
if (!allowedFields.includes(orderBy)) {
orderBy = "_sortIndex";
}
const selectStmt = database.prepare(`
SELECT * FROM LocalMusicSheets
ORDER BY ${orderBy} ${order}
`);
const results = selectStmt.all() as any[];
return results.map(result => ({
platform: result.platform,
id: result.id,
title: result.title,
artwork: result.artwork,
description: result.description,
worksNum: result.worksNum,
playCount: result.playCount,
createAt: result.createAt,
artist: result.artist,
_raw: result._raw,
_sortIndex: result._sortIndex,
}));
} catch {
return [];
}
}
/**
* 按平台查询歌单
* @param platform 平台名称
* @param orderBy 排序字段
* @param order 排序方向
* @returns 歌单数组
*/
static getMusicSheetsByPlatform(platform: string, orderBy: string = "_sortIndex", order: "ASC" | "DESC" = "ASC"): IDataBaseModel.IMusicSheetModel[] {
try {
const allowedFields = ["_sortIndex", "title", "createAt", "artist", "playCount"];
if (!allowedFields.includes(orderBy)) {
orderBy = "_sortIndex";
}
const selectStmt = database.prepare(`
SELECT * FROM LocalMusicSheets
WHERE platform = ?
ORDER BY ${orderBy} ${order}
`);
const results = selectStmt.all(platform) as any[];
return results.map(result => ({
platform: result.platform,
id: result.id,
title: result.title,
artwork: result.artwork,
description: result.description,
worksNum: result.worksNum,
playCount: result.playCount,
createAt: result.createAt,
artist: result.artist,
_raw: result._raw,
_sortIndex: result._sortIndex,
}));
} catch {
return [];
}
}
/**
* 更新歌单(部分更新)
* @param platform 平台
* @param id 歌单ID
* @param updates 要更新的字段
* @returns 是否更新成功
*/
static updateMusicSheet(
platform: string,
id: string,
updates: Partial<Omit<IDataBaseModel.IMusicSheetModel, "platform" | "id">>,
): boolean {
try {
if (Object.keys(updates).length === 0) {
return false;
}
// 构建动态更新SQL
const allowedFields = ["title", "artwork", "description", "worksNum", "playCount", "createAt", "artist", "_raw", "_sortIndex"];
const updateFields: string[] = [];
const values: any[] = [];
for (const [key, value] of Object.entries(updates)) {
if (allowedFields.includes(key) && value !== undefined) {
updateFields.push(`${key} = ?`);
values.push(value);
}
}
if (updateFields.length === 0) {
return false;
}
values.push(platform, id);
const updateStmt = database.prepare(`
UPDATE LocalMusicSheets
SET ${updateFields.join(", ")}
WHERE platform = ? AND id = ?
`);
const result = updateStmt.run(...values);
return result.changes > 0;
} catch {
return false;
}
}
/**
* 重新均衡所有歌单的排序索引
* 当排序间隔过小时调用此方法
*/
static rebalanceSortIndexes(): boolean {
try {
const transaction = database.transaction(() => {
// 获取所有歌单按当前排序
const selectStmt = database.prepare("SELECT platform, id FROM LocalMusicSheets ORDER BY _sortIndex ASC");
const sheets = selectStmt.all() as Array<{ platform: string; id: string }>;
const updateStmt = database.prepare("UPDATE LocalMusicSheets SET _sortIndex = ? WHERE platform = ? AND id = ?");
// 重新分配排序索引每个间隔1000
sheets.forEach((sheet, index) => {
const newSortIndex = this.SORT_BASE + (index * this.SORT_INCREMENT);
updateStmt.run(newSortIndex, sheet.platform, sheet.id);
});
return true;
});
return transaction();
} catch {
return false;
}
}
/**
* 批量移动歌单到指定位置(支持所有拖拽和排序场景)
* @param selectedSheets 要移动的歌单标识数组
* @param targetPlatform 目标歌单的平台null表示移动到开头/末尾)
* @param targetId 目标歌单的IDnull表示移动到开头/末尾)
* @param position 相对于目标歌单的位置:"before" | "after",默认"after"
* @returns 成功移动的数量
*
* @example
* // 移动到开头
* batchMoveMusicSheets(sheets, null, null, "before")
*
* // 移动到末尾
* batchMoveMusicSheets(sheets, null, null, "after")
*
* // 移动到指定歌单之前
* batchMoveMusicSheets(sheets, "platform1", "id1", "before")
*
* // 移动到指定歌单之后
* batchMoveMusicSheets(sheets, "platform1", "id1", "after")
*/
static batchMoveMusicSheets(
selectedSheets: Array<{ platform: string; id: string }>,
targetPlatform: string | null = null,
targetId: string | null = null,
position: "before" | "after" = "after",
): number {
if (!selectedSheets.length) {
return 0;
}
try {
const transaction = database.transaction(() => {
// 获取所有歌单的排序信息
const allSheetsStmt = database.prepare(`
SELECT platform, id, _sortIndex
FROM LocalMusicSheets
ORDER BY _sortIndex ASC
`);
const allSheets = allSheetsStmt.all() as Array<{
platform: string;
id: string;
_sortIndex: number;
}>;
// 分离要移动的歌单和剩余的歌单
const selectedSheetIds = new Set(selectedSheets.map(s => `${s.platform}:${s.id}`));
const sheetsToMove = allSheets.filter(s => selectedSheetIds.has(`${s.platform}:${s.id}`));
const remainingSheets = allSheets.filter(s => !selectedSheetIds.has(`${s.platform}:${s.id}`));
if (!sheetsToMove.length) {
return 0;
}
// 计算插入位置
let insertIndex = 0;
if (targetPlatform && targetId) {
const targetIndex = remainingSheets.findIndex(s =>
s.platform === targetPlatform && s.id === targetId,
);
insertIndex = targetIndex === -1 ? 0 :
(position === "after" ? targetIndex + 1 : targetIndex);
} else {
insertIndex = position === "before" ? 0 : remainingSheets.length;
}
// 构建新的排序数组并重新分配排序索引
const newOrderedSheets = [...remainingSheets];
newOrderedSheets.splice(insertIndex, 0, ...sheetsToMove);
const updateStmt = database.prepare(`
UPDATE LocalMusicSheets SET _sortIndex = ? WHERE platform = ? AND id = ?
`);
let successCount = 0;
newOrderedSheets.forEach((sheet, index) => {
const newSortIndex = this.SORT_BASE + (index * this.SORT_INCREMENT);
try {
const result = updateStmt.run(newSortIndex, sheet.platform, sheet.id);
if (result.changes > 0 && selectedSheetIds.has(`${sheet.platform}:${sheet.id}`)) {
successCount++;
}
} catch {
// 静默处理单个歌单移动失败的情况
}
});
return successCount;
});
return transaction();
} catch {
return 0;
}
}
/**
* 搜索歌单
* @param keyword 搜索关键词
* @param searchFields 搜索字段数组默认搜索title和artist
* @returns 匹配的歌单数组
*/
static searchMusicSheets(
keyword: string,
searchFields: string[] = ["title", "artist"],
): IDataBaseModel.IMusicSheetModel[] {
try {
if (!keyword.trim()) {
return this.getAllMusicSheets();
}
const allowedFields = ["title", "artist", "description"];
const validFields = searchFields.filter(field => allowedFields.includes(field));
if (validFields.length === 0) {
validFields.push("title");
}
const whereConditions = validFields.map(field => `${field} LIKE ?`).join(" OR ");
const searchPattern = `%${keyword}%`;
const params = validFields.map(() => searchPattern);
const searchStmt = database.prepare(`
SELECT * FROM LocalMusicSheets
WHERE ${whereConditions}
ORDER BY _sortIndex ASC
`);
const results = searchStmt.all(...params) as any[];
return results.map(result => ({
platform: result.platform,
id: result.id,
title: result.title,
artwork: result.artwork,
description: result.description,
worksNum: result.worksNum,
playCount: result.playCount,
createAt: result.createAt,
artist: result.artist,
_raw: result._raw,
_sortIndex: result._sortIndex,
}));
} catch {
return [];
}
}
/**
* 获取歌单中的所有歌曲
* @param platform 歌单平台
* @param id 歌单ID
* @param orderBy 排序字段默认按_sortIndex排序
* @param order 排序方向
* @returns 歌曲数组
*/
static getMusicItemsInSheet(
platform: string,
id: string,
orderBy: string = "_sortIndex",
order: "ASC" | "DESC" = "ASC",
): IDataBaseModel.IMusicItemModel[] {
try {
const allowedFields = ["_sortIndex", "title", "artist", "album", "_timestamp"];
if (!allowedFields.includes(orderBy)) {
orderBy = "_sortIndex";
}
const selectStmt = database.prepare(`
SELECT * FROM LocalMusicItems
WHERE _musicSheetPlatform = ? AND _musicSheetId = ?
ORDER BY ${orderBy} ${order}
`);
const results = selectStmt.all(platform, id) as any[];
return results.map(result => ({
platform: result.platform,
id: result.id,
artist: result.artist,
title: result.title,
duration: result.duration,
album: result.album,
artwork: result.artwork,
_timestamp: result._timestamp,
_raw: result._raw,
_sortIndex: result._sortIndex,
_musicSheetId: result._musicSheetId,
_musicSheetPlatform: result._musicSheetPlatform,
}));
} catch {
return [];
}
}
/**
* 获取歌单中歌曲的数量
* @param platform 歌单平台
* @param id 歌单ID
* @returns 歌曲数量
*/
static getMusicItemCountInSheet(platform: string, id: string): number {
try {
const countStmt = database.prepare(`
SELECT COUNT(*) as count FROM LocalMusicItems
WHERE _musicSheetPlatform = ? AND _musicSheetId = ?
`);
const result = countStmt.get(platform, id) as { count: number };
return result.count;
} catch {
return 0;
}
}
/**
* 检查歌单是否存在
* @param platform 平台
* @param id 歌单ID
* @returns 是否存在
*/
static existsMusicSheet(platform: string, id: string): boolean {
try {
const countStmt = database.prepare("SELECT COUNT(*) as count FROM LocalMusicSheets WHERE platform = ? AND id = ?");
const result = countStmt.get(platform, id) as { count: number };
return result.count > 0;
} catch {
return false;
}
}
}
// 导出数据库实例和类
export { database, LocalMusicSheetDB };

View File

@@ -28,7 +28,7 @@ declare namespace IDataBaseModel {
platform: string;
id: string;
/** 作者 */
artist: string;
artist?: string;
/** 歌曲标题 */
title: string;
/** 时长(s) */
@@ -37,10 +37,12 @@ declare namespace IDataBaseModel {
album?: string;
/** 专辑封面图 */
artwork?: string;
/** 添加到歌单的时间 */
_timestamp: number;
// 完整信息
_raw: string;
// 在歌单内的顺序
_sortOrder: number;
_sortIndex: number;
// 歌单ID
_musicSheetId: string;
// 歌单