initial commit
This commit is contained in:
279
src/main/services/database.ts
Normal file
279
src/main/services/database.ts
Normal file
@@ -0,0 +1,279 @@
|
||||
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()
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user