2022-03-30 00:53:05 +08:00
|
|
|
import path from 'path'
|
2022-04-09 00:28:37 +08:00
|
|
|
import { app } from 'electron'
|
2022-03-30 00:53:05 +08:00
|
|
|
import fs from 'fs'
|
|
|
|
import SQLite3 from 'better-sqlite3'
|
2022-04-16 13:30:25 +08:00
|
|
|
import log from './log'
|
2022-05-13 19:30:13 +08:00
|
|
|
import { createFileIfNotExist, dirname } from './utils'
|
|
|
|
import pkg from '../../../package.json'
|
2022-05-13 01:48:13 +08:00
|
|
|
import { compare, validate } from 'compare-versions'
|
2022-04-02 00:45:20 +08:00
|
|
|
|
2022-04-16 16:50:34 +08:00
|
|
|
export const enum Tables {
|
|
|
|
Track = 'Track',
|
|
|
|
Album = 'Album',
|
|
|
|
Artist = 'Artist',
|
|
|
|
Playlist = 'Playlist',
|
|
|
|
ArtistAlbum = 'ArtistAlbum',
|
|
|
|
Lyric = 'Lyric',
|
|
|
|
Audio = 'Audio',
|
|
|
|
AccountData = 'AccountData',
|
|
|
|
CoverColor = 'CoverColor',
|
2022-05-13 01:48:13 +08:00
|
|
|
AppData = 'AppData',
|
2022-03-30 00:53:05 +08:00
|
|
|
}
|
2022-04-16 16:50:34 +08:00
|
|
|
interface CommonTableStructure {
|
|
|
|
id: number
|
|
|
|
json: string
|
|
|
|
updatedAt: number
|
|
|
|
}
|
|
|
|
export interface TablesStructures {
|
|
|
|
[Tables.Track]: CommonTableStructure
|
|
|
|
[Tables.Album]: CommonTableStructure
|
|
|
|
[Tables.Artist]: CommonTableStructure
|
|
|
|
[Tables.Playlist]: CommonTableStructure
|
|
|
|
[Tables.ArtistAlbum]: CommonTableStructure
|
|
|
|
[Tables.Lyric]: CommonTableStructure
|
|
|
|
[Tables.AccountData]: {
|
|
|
|
id: string
|
|
|
|
json: string
|
|
|
|
updatedAt: number
|
|
|
|
}
|
|
|
|
[Tables.Audio]: {
|
|
|
|
id: number
|
|
|
|
br: number
|
2022-05-01 19:53:25 +08:00
|
|
|
type: 'mp3' | 'flac' | 'ogg' | 'wav' | 'm4a' | 'aac' | 'unknown' | 'opus'
|
|
|
|
source:
|
|
|
|
| 'unknown'
|
|
|
|
| 'netease'
|
|
|
|
| 'migu'
|
|
|
|
| 'kuwo'
|
|
|
|
| 'kugou'
|
|
|
|
| 'youtube'
|
|
|
|
| 'qq'
|
|
|
|
| 'bilibili'
|
|
|
|
| 'joox'
|
2022-04-16 16:50:34 +08:00
|
|
|
updatedAt: number
|
|
|
|
}
|
|
|
|
[Tables.CoverColor]: {
|
|
|
|
id: number
|
|
|
|
color: string
|
|
|
|
}
|
2022-05-13 01:48:13 +08:00
|
|
|
[Tables.AppData]: {
|
|
|
|
id: 'appVersion' | 'skippedVersion'
|
|
|
|
value: string
|
|
|
|
}
|
2022-04-16 16:50:34 +08:00
|
|
|
}
|
|
|
|
|
|
|
|
type TableNames = keyof TablesStructures
|
2022-03-30 00:53:05 +08:00
|
|
|
|
2022-05-13 01:48:13 +08:00
|
|
|
const readSqlFile = (filename: string) => {
|
|
|
|
return fs.readFileSync(path.join(dirname, `./migrations/${filename}`), 'utf8')
|
|
|
|
}
|
|
|
|
|
2022-04-12 01:48:14 +08:00
|
|
|
class DB {
|
|
|
|
sqlite: SQLite3.Database
|
|
|
|
dbFilePath: string = path.resolve(
|
|
|
|
app.getPath('userData'),
|
|
|
|
'./api_cache/db.sqlite'
|
2022-03-30 16:54:11 +08:00
|
|
|
)
|
2022-03-30 00:53:05 +08:00
|
|
|
|
2022-04-12 01:48:14 +08:00
|
|
|
constructor() {
|
2022-04-16 13:30:25 +08:00
|
|
|
log.info('[db] Initializing database...')
|
2022-04-12 01:48:14 +08:00
|
|
|
|
|
|
|
createFileIfNotExist(this.dbFilePath)
|
|
|
|
|
|
|
|
this.sqlite = new SQLite3(this.dbFilePath, {
|
|
|
|
nativeBinding: path.join(
|
|
|
|
__dirname,
|
2022-05-12 23:16:25 +08:00
|
|
|
`./binary/better_sqlite3_${process.arch}.node`
|
2022-04-12 01:48:14 +08:00
|
|
|
),
|
|
|
|
})
|
|
|
|
this.sqlite.pragma('auto_vacuum = FULL')
|
|
|
|
this.initTables()
|
2022-05-13 01:48:13 +08:00
|
|
|
this.migrate()
|
2022-04-12 01:48:14 +08:00
|
|
|
|
2022-04-16 13:30:25 +08:00
|
|
|
log.info('[db] Database initialized')
|
2022-04-12 01:48:14 +08:00
|
|
|
}
|
|
|
|
|
|
|
|
initTables() {
|
2022-05-13 01:48:13 +08:00
|
|
|
const init = readSqlFile('init.sql')
|
|
|
|
this.sqlite.exec(init)
|
|
|
|
}
|
|
|
|
|
|
|
|
migrate() {
|
|
|
|
const key = 'appVersion'
|
|
|
|
const appVersion = this.find(Tables.AppData, key)
|
|
|
|
const updateAppVersionInDB = () => {
|
|
|
|
this.upsert(Tables.AppData, {
|
|
|
|
id: key,
|
|
|
|
value: pkg.version,
|
|
|
|
})
|
|
|
|
}
|
|
|
|
|
|
|
|
if (!appVersion?.value) {
|
|
|
|
updateAppVersionInDB()
|
|
|
|
return
|
|
|
|
}
|
|
|
|
|
|
|
|
const sqlFiles = fs.readdirSync(path.join(dirname, './migrations'))
|
|
|
|
sqlFiles.forEach((sqlFile: string) => {
|
|
|
|
const version = sqlFile.split('.').shift() || ''
|
|
|
|
if (!validate(version)) return
|
|
|
|
if (compare(version, pkg.version, '>')) {
|
|
|
|
const file = readSqlFile(sqlFile)
|
|
|
|
this.sqlite.exec(file)
|
|
|
|
}
|
|
|
|
})
|
|
|
|
|
|
|
|
updateAppVersionInDB()
|
2022-04-12 01:48:14 +08:00
|
|
|
}
|
|
|
|
|
2022-04-16 16:50:34 +08:00
|
|
|
find<T extends TableNames>(
|
|
|
|
table: T,
|
|
|
|
key: TablesStructures[T]['id']
|
2022-05-13 01:48:13 +08:00
|
|
|
): TablesStructures[T] | undefined {
|
2022-04-12 01:48:14 +08:00
|
|
|
return this.sqlite
|
2022-03-30 00:53:05 +08:00
|
|
|
.prepare(`SELECT * FROM ${table} WHERE id = ? LIMIT 1`)
|
|
|
|
.get(key)
|
2022-04-12 01:48:14 +08:00
|
|
|
}
|
|
|
|
|
2022-04-16 16:50:34 +08:00
|
|
|
findMany<T extends TableNames>(
|
|
|
|
table: T,
|
|
|
|
keys: TablesStructures[T]['id'][]
|
|
|
|
): TablesStructures[T][] {
|
2022-03-30 00:53:05 +08:00
|
|
|
const idsQuery = keys.map(key => `id = ${key}`).join(' OR ')
|
2022-04-12 01:48:14 +08:00
|
|
|
return this.sqlite.prepare(`SELECT * FROM ${table} WHERE ${idsQuery}`).all()
|
|
|
|
}
|
|
|
|
|
2022-04-16 16:50:34 +08:00
|
|
|
findAll<T extends TableNames>(table: T): TablesStructures[T][] {
|
2022-04-12 01:48:14 +08:00
|
|
|
return this.sqlite.prepare(`SELECT * FROM ${table}`).all()
|
|
|
|
}
|
|
|
|
|
2022-04-16 16:50:34 +08:00
|
|
|
create<T extends TableNames>(
|
|
|
|
table: T,
|
|
|
|
data: TablesStructures[T],
|
|
|
|
skipWhenExist: boolean = true
|
|
|
|
) {
|
2022-03-30 16:54:11 +08:00
|
|
|
if (skipWhenExist && db.find(table, data.id)) return
|
2022-04-12 01:48:14 +08:00
|
|
|
return this.sqlite.prepare(`INSERT INTO ${table} VALUES (?)`).run(data)
|
|
|
|
}
|
|
|
|
|
2022-04-16 16:50:34 +08:00
|
|
|
createMany<T extends TableNames>(
|
|
|
|
table: T,
|
|
|
|
data: TablesStructures[T][],
|
|
|
|
skipWhenExist: boolean = true
|
|
|
|
) {
|
2022-03-30 16:54:11 +08:00
|
|
|
const valuesQuery = Object.keys(data[0])
|
|
|
|
.map(key => `:${key}`)
|
|
|
|
.join(', ')
|
2022-04-12 01:48:14 +08:00
|
|
|
const insert = this.sqlite.prepare(
|
2022-03-30 16:54:11 +08:00
|
|
|
`INSERT ${
|
|
|
|
skipWhenExist ? 'OR IGNORE' : ''
|
|
|
|
} INTO ${table} VALUES (${valuesQuery})`
|
|
|
|
)
|
2022-04-12 01:48:14 +08:00
|
|
|
const insertMany = this.sqlite.transaction((rows: any[]) => {
|
2022-03-30 16:54:11 +08:00
|
|
|
rows.forEach((row: any) => insert.run(row))
|
|
|
|
})
|
|
|
|
insertMany(data)
|
2022-04-12 01:48:14 +08:00
|
|
|
}
|
|
|
|
|
2022-04-16 16:50:34 +08:00
|
|
|
upsert<T extends TableNames>(table: T, data: TablesStructures[T]) {
|
2022-03-30 00:53:05 +08:00
|
|
|
const valuesQuery = Object.keys(data)
|
|
|
|
.map(key => `:${key}`)
|
|
|
|
.join(', ')
|
2022-04-12 01:48:14 +08:00
|
|
|
return this.sqlite
|
2022-03-30 00:53:05 +08:00
|
|
|
.prepare(`INSERT OR REPLACE INTO ${table} VALUES (${valuesQuery})`)
|
|
|
|
.run(data)
|
2022-04-12 01:48:14 +08:00
|
|
|
}
|
|
|
|
|
2022-04-16 16:50:34 +08:00
|
|
|
upsertMany<T extends TableNames>(table: T, data: TablesStructures[T][]) {
|
2022-03-30 00:53:05 +08:00
|
|
|
const valuesQuery = Object.keys(data[0])
|
|
|
|
.map(key => `:${key}`)
|
|
|
|
.join(', ')
|
2022-04-12 01:48:14 +08:00
|
|
|
const upsert = this.sqlite.prepare(
|
2022-03-30 00:53:05 +08:00
|
|
|
`INSERT OR REPLACE INTO ${table} VALUES (${valuesQuery})`
|
|
|
|
)
|
2022-04-12 01:48:14 +08:00
|
|
|
const upsertMany = this.sqlite.transaction((rows: any[]) => {
|
2022-03-30 00:53:05 +08:00
|
|
|
rows.forEach((row: any) => upsert.run(row))
|
|
|
|
})
|
|
|
|
upsertMany(data)
|
2022-04-12 01:48:14 +08:00
|
|
|
}
|
|
|
|
|
2022-04-16 21:14:03 +08:00
|
|
|
delete<T extends TableNames>(table: T, key: TablesStructures[T]['id']) {
|
2022-04-12 01:48:14 +08:00
|
|
|
return this.sqlite.prepare(`DELETE FROM ${table} WHERE id = ?`).run(key)
|
|
|
|
}
|
|
|
|
|
2022-04-16 16:50:34 +08:00
|
|
|
deleteMany<T extends TableNames>(
|
|
|
|
table: T,
|
|
|
|
keys: TablesStructures[T]['id'][]
|
|
|
|
) {
|
2022-03-30 00:53:05 +08:00
|
|
|
const idsQuery = keys.map(key => `id = ${key}`).join(' OR ')
|
2022-04-12 01:48:14 +08:00
|
|
|
return this.sqlite.prepare(`DELETE FROM ${table} WHERE ${idsQuery}`).run()
|
|
|
|
}
|
|
|
|
|
2022-04-16 16:50:34 +08:00
|
|
|
truncate<T extends TableNames>(table: T) {
|
2022-04-12 01:48:14 +08:00
|
|
|
return this.sqlite.prepare(`DELETE FROM ${table}`).run()
|
|
|
|
}
|
|
|
|
|
|
|
|
vacuum() {
|
|
|
|
return this.sqlite.prepare('VACUUM').run()
|
|
|
|
}
|
2022-03-30 00:53:05 +08:00
|
|
|
}
|
|
|
|
|
2022-04-12 01:48:14 +08:00
|
|
|
export const db = new DB()
|