280 lines
9.5 KiB
TypeScript
280 lines
9.5 KiB
TypeScript
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()
|
||
}
|
||
}
|