Files
LoveLiveMusicPlayer-Next/src/main/services/database.ts
2026-08-08 18:34:12 +08:00

280 lines
9.5 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
import { app } from 'electron'
import { join } from 'path'
import Database from 'better-sqlite3'
import type { Album, Music, Lyric, Menu, Love, History } from '@shared/models'
/**
* 本地数据库better-sqlite3替代旧 NeDB
* 表结构对齐移动端 Floor 实体,降低双端同步映射成本。
*/
let db: Database.Database | null = null
export function initDatabase(): Database.Database {
if (db) return db
const dbPath = join(app.getPath('userData'), 'llmp.db')
db = new Database(dbPath)
db.pragma('journal_mode = WAL')
createTables(db)
migrate(db)
return db
}
/** 轻量迁移:为已存在的库补齐新增列 */
function migrate(d: Database.Database): void {
const musicCols = d.prepare('PRAGMA table_info(music)').all() as { name: string }[]
const albumCols = d.prepare('PRAGMA table_info(album)').all() as { name: string }[]
const ensureMusic = (name: string, ddl: string): void => {
if (!musicCols.some((c) => c.name === name)) d.exec(ddl)
}
const ensureAlbum = (name: string, ddl: string): void => {
if (!albumCols.some((c) => c.name === name)) d.exec(ddl)
}
ensureMusic('recommend', 'ALTER TABLE music ADD COLUMN recommend INTEGER DEFAULT 0')
ensureMusic('artistBin', "ALTER TABLE music ADD COLUMN artistBin TEXT NOT NULL DEFAULT ''")
ensureMusic('time', "ALTER TABLE music ADD COLUMN time TEXT NOT NULL DEFAULT ''")
ensureMusic('musicId', 'ALTER TABLE music ADD COLUMN musicId INTEGER')
ensureAlbum('albumId', 'ALTER TABLE album ADD COLUMN albumId INTEGER')
}
export function getDb(): Database.Database {
if (!db) return initDatabase()
return db
}
function createTables(d: Database.Database): void {
d.exec(`
CREATE TABLE IF NOT EXISTS album (
albumUId TEXT PRIMARY KEY,
albumName TEXT NOT NULL DEFAULT '',
cover TEXT NOT NULL DEFAULT '',
category TEXT NOT NULL DEFAULT '',
"group" TEXT NOT NULL DEFAULT '',
releaseDate TEXT,
baseUrl TEXT NOT NULL DEFAULT '',
albumId INTEGER
);
CREATE TABLE IF NOT EXISTS music (
musicUId TEXT PRIMARY KEY,
albumUId TEXT NOT NULL DEFAULT '',
musicName TEXT NOT NULL DEFAULT '',
artist TEXT NOT NULL DEFAULT '',
musicPath TEXT NOT NULL DEFAULT '',
coverPath TEXT NOT NULL DEFAULT '',
baseUrl TEXT NOT NULL DEFAULT '',
"group" TEXT NOT NULL DEFAULT '',
neteaseId TEXT,
duration INTEGER,
"index" INTEGER,
local INTEGER DEFAULT 0,
recommend INTEGER DEFAULT 0,
artistBin TEXT NOT NULL DEFAULT '',
time TEXT NOT NULL DEFAULT '',
musicId INTEGER
);
CREATE TABLE IF NOT EXISTS artist (
artistId TEXT PRIMARY KEY,
name TEXT NOT NULL DEFAULT '',
avatar TEXT
);
CREATE TABLE IF NOT EXISTS lyric (
musicUId TEXT PRIMARY KEY,
lyricJp TEXT,
lyricZh TEXT,
lyricRoma TEXT
);
CREATE TABLE IF NOT EXISTS menu (
id INTEGER PRIMARY KEY,
title TEXT NOT NULL DEFAULT '',
cover TEXT,
createTime INTEGER NOT NULL DEFAULT 0
);
CREATE TABLE IF NOT EXISTS playlist_music (
menuId INTEGER NOT NULL,
musicUId TEXT NOT NULL,
"order" INTEGER NOT NULL DEFAULT 0,
PRIMARY KEY (menuId, musicUId)
);
CREATE TABLE IF NOT EXISTS love (
musicUId TEXT PRIMARY KEY,
createTime INTEGER NOT NULL DEFAULT 0
);
CREATE TABLE IF NOT EXISTS history (
musicUId TEXT PRIMARY KEY,
playTime INTEGER NOT NULL DEFAULT 0
);
`)
}
/* -------------------- 通用查询(供 IPC 调用) -------------------- */
export function dbQuery<T = unknown>(sql: string, params: unknown[] = []): T[] {
return getDb().prepare(sql).all(...params) as T[]
}
export function dbExec(sql: string, params: unknown[] = []): Database.RunResult {
return getDb().prepare(sql).run(...params)
}
/* -------------------- 领域 DAO 封装 -------------------- */
export const AlbumDao = {
all: (): Album[] => dbQuery<Album>('SELECT * FROM album'),
upsert: (a: Album): void => {
dbExec(
`INSERT INTO album (albumUId, albumName, cover, category, "group", releaseDate, baseUrl, albumId)
VALUES (?,?,?,?,?,?,?,?)
ON CONFLICT(albumUId) DO UPDATE SET
albumName=excluded.albumName, cover=excluded.cover, category=excluded.category,
"group"=excluded."group", releaseDate=excluded.releaseDate, baseUrl=excluded.baseUrl,
albumId=excluded.albumId`,
[
a.albumUId,
a.albumName,
a.cover,
a.category,
a.group,
a.releaseDate ?? null,
a.baseUrl,
a.albumId ?? null
]
)
}
}
export const MusicDao = {
all: (): Music[] => dbQuery<Music>('SELECT * FROM music'),
byIds: (ids: string[]): Music[] => {
if (ids.length === 0) return []
const placeholders = ids.map(() => '?').join(',')
return dbQuery<Music>(`SELECT * FROM music WHERE musicUId IN (${placeholders})`, ids)
},
byAlbum: (albumUId: string): Music[] =>
dbQuery<Music>('SELECT * FROM music WHERE albumUId = ? ORDER BY "index" ASC', [albumUId]),
upsert: (m: Music): void => {
dbExec(
`INSERT INTO music (musicUId, albumUId, musicName, artist, musicPath, coverPath, baseUrl, "group", neteaseId, duration, "index", local, recommend, artistBin, time, musicId)
VALUES (?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?)
ON CONFLICT(musicUId) DO UPDATE SET
albumUId=excluded.albumUId, musicName=excluded.musicName, artist=excluded.artist,
musicPath=excluded.musicPath, coverPath=excluded.coverPath, baseUrl=excluded.baseUrl,
"group"=excluded."group", neteaseId=excluded.neteaseId, duration=excluded.duration,
"index"=excluded."index", local=excluded.local, recommend=excluded.recommend,
artistBin=excluded.artistBin, time=excluded.time, musicId=excluded.musicId`,
[
m.musicUId,
m.albumUId,
m.musicName,
m.artist,
m.musicPath,
m.coverPath,
m.baseUrl,
m.group,
m.neteaseId ?? null,
m.duration ?? null,
m.index ?? null,
m.local ? 1 : 0,
m.recommend ? 1 : 0,
m.artistBin ?? '',
m.time ?? '',
m.musicId ?? m.index ?? null
]
)
}
}
export const LyricDao = {
byId: (musicUId: string): Lyric | undefined =>
dbQuery<Lyric>('SELECT * FROM lyric WHERE musicUId = ?', [musicUId])[0],
upsert: (l: Lyric): void => {
dbExec(
`INSERT INTO lyric (musicUId, lyricJp, lyricZh, lyricRoma) VALUES (?,?,?,?)
ON CONFLICT(musicUId) DO UPDATE SET
lyricJp=excluded.lyricJp, lyricZh=excluded.lyricZh, lyricRoma=excluded.lyricRoma`,
[l.musicUId, l.lyricJp ?? null, l.lyricZh ?? null, l.lyricRoma ?? null]
)
}
}
export const LoveDao = {
all: (): Love[] => dbQuery<Love>('SELECT * FROM love ORDER BY createTime DESC'),
add: (musicUId: string): void =>
void dbExec('INSERT OR IGNORE INTO love (musicUId, createTime) VALUES (?, ?)', [
musicUId,
Date.now()
]),
remove: (musicUId: string): void =>
void dbExec('DELETE FROM love WHERE musicUId = ?', [musicUId]),
has: (musicUId: string): boolean =>
dbQuery('SELECT 1 FROM love WHERE musicUId = ?', [musicUId]).length > 0,
replaceAll: (loves: Love[]): void => {
const d = getDb()
const tx = d.transaction((items: Love[]) => {
d.prepare('DELETE FROM love').run()
const stmt = d.prepare('INSERT OR IGNORE INTO love (musicUId, createTime) VALUES (?, ?)')
for (const it of items) stmt.run(it.musicUId, it.createTime ?? Date.now())
})
tx(loves)
},
merge: (loves: Love[]): void => {
const d = getDb()
const stmt = d.prepare('INSERT OR IGNORE INTO love (musicUId, createTime) VALUES (?, ?)')
const tx = d.transaction((items: Love[]) => {
for (const it of items) stmt.run(it.musicUId, it.createTime ?? Date.now())
})
tx(loves)
}
}
export const HistoryDao = {
recent: (limit = 100): History[] =>
dbQuery<History>('SELECT * FROM history ORDER BY playTime DESC LIMIT ?', [limit]),
touch: (musicUId: string): void =>
void dbExec(
`INSERT INTO history (musicUId, playTime) VALUES (?, ?)
ON CONFLICT(musicUId) DO UPDATE SET playTime=excluded.playTime`,
[musicUId, Date.now()]
)
}
export const MenuDao = {
all: (): Menu[] => dbQuery<Menu>('SELECT * FROM menu ORDER BY createTime DESC'),
pcMenus: (): Menu[] => dbQuery<Menu>('SELECT * FROM menu WHERE id <= 100'),
phoneMenus: (): Menu[] => dbQuery<Menu>('SELECT * FROM menu WHERE id > 100'),
musicIds: (menuId: number): string[] =>
dbQuery<{ musicUId: string }>(
'SELECT musicUId FROM playlist_music WHERE menuId = ? ORDER BY "order" ASC',
[menuId]
).map((r) => r.musicUId),
upsert: (m: Menu, musicUIds: string[]): void => {
const d = getDb()
const tx = d.transaction(() => {
d.prepare(
`INSERT INTO menu (id, title, cover, createTime) VALUES (?,?,?,?)
ON CONFLICT(id) DO UPDATE SET title=excluded.title, cover=excluded.cover`
).run(m.id, m.title, m.cover ?? null, m.createTime)
d.prepare('DELETE FROM playlist_music WHERE menuId = ?').run(m.id)
const stmt = d.prepare(
'INSERT OR IGNORE INTO playlist_music (menuId, musicUId, "order") VALUES (?,?,?)'
)
musicUIds.forEach((id, i) => stmt.run(m.id, id, i))
})
tx()
},
remove: (id: number): void => {
const d = getDb()
const tx = d.transaction(() => {
d.prepare('DELETE FROM menu WHERE id = ?').run(id)
d.prepare('DELETE FROM playlist_music WHERE menuId = ?').run(id)
})
tx()
}
}