mirror of
https://github.com/GiriNeko/YesPlayMusic.git
synced 2025-12-17 21:58:03 +00:00
feat: updates
This commit is contained in:
parent
840a5b8e9b
commit
cb0a809b16
29 changed files with 1550 additions and 449 deletions
|
|
@ -1,6 +1,5 @@
|
|||
import { FastifyInstance, FastifyRequest, FastifyReply } from 'fastify'
|
||||
import NeteaseCloudMusicApi, { SoundQualityType } from 'NeteaseCloudMusicApi'
|
||||
import prisma from '@/desktop/main/prisma'
|
||||
import { app } from 'electron'
|
||||
import log from '@/desktop/main/log'
|
||||
import { appName } from '@/desktop/main/env'
|
||||
|
|
@ -10,10 +9,11 @@ import youtube from '@/desktop/main/youtube'
|
|||
import { CacheAPIs } from '@/shared/CacheAPIs'
|
||||
import { FetchTracksResponse } from '@/shared/api/Track'
|
||||
import store from '@/desktop/main/store'
|
||||
import { db, Tables } from '@/desktop/main/db'
|
||||
|
||||
const getAudioFromCache = async (id: number) => {
|
||||
// get from cache
|
||||
const cache = await prisma.audio.findUnique({ where: { id } })
|
||||
const cache = await db.find(Tables.Audio, id)
|
||||
if (!cache) return
|
||||
|
||||
const audioFileName = `${cache.id}-${cache.bitRate}.${cache.format}`
|
||||
|
|
|
|||
|
|
@ -1,9 +1,11 @@
|
|||
import prisma from './prisma'
|
||||
import { db, Tables } from './db'
|
||||
import type { FetchTracksResponse } from '@/shared/api/Track'
|
||||
import { app } from 'electron'
|
||||
import log from './log'
|
||||
import fs from 'fs'
|
||||
import * as musicMetadata from 'music-metadata'
|
||||
import { CacheAPIs, CacheAPIsParams, CacheAPIsResponse } from '@/shared/CacheAPIs'
|
||||
import { CacheAPIs, CacheAPIsParams } from '@/shared/CacheAPIs'
|
||||
import { TablesStructures } from './db'
|
||||
import { FastifyReply } from 'fastify'
|
||||
|
||||
class Cache {
|
||||
|
|
@ -11,12 +13,7 @@ class Cache {
|
|||
//
|
||||
}
|
||||
|
||||
async set<T extends CacheAPIs>(
|
||||
api: T,
|
||||
data: CacheAPIsResponse[T],
|
||||
query: { [key: string]: string } = {}
|
||||
) {
|
||||
if (!data) return
|
||||
async set(api: string, data: any, query: any = {}) {
|
||||
switch (api) {
|
||||
case CacheAPIs.UserPlaylist:
|
||||
case CacheAPIs.UserAccount:
|
||||
|
|
@ -26,104 +23,117 @@ class Cache {
|
|||
case CacheAPIs.UserArtists:
|
||||
case CacheAPIs.ListenedRecords:
|
||||
case CacheAPIs.Likelist: {
|
||||
const id = api
|
||||
const row = { id, json: JSON.stringify(data) }
|
||||
await prisma.accountData.upsert({ where: { id }, create: row, update: row })
|
||||
if (!data) return
|
||||
db.upsert(Tables.AccountData, {
|
||||
id: api,
|
||||
json: JSON.stringify(data),
|
||||
updatedAt: Date.now(),
|
||||
})
|
||||
break
|
||||
}
|
||||
case CacheAPIs.Track: {
|
||||
const res = data as CacheAPIsResponse[CacheAPIs.Track]
|
||||
const res = data as FetchTracksResponse
|
||||
if (!res.songs) return
|
||||
await Promise.all(
|
||||
res.songs.map(t => {
|
||||
const id = t.id
|
||||
const row = { id, json: JSON.stringify(t) }
|
||||
return prisma.track.upsert({ where: { id }, create: row, update: row })
|
||||
})
|
||||
)
|
||||
const tracks = res.songs.map(t => ({
|
||||
id: t.id,
|
||||
json: JSON.stringify(t),
|
||||
updatedAt: Date.now(),
|
||||
}))
|
||||
db.upsertMany(Tables.Track, tracks)
|
||||
break
|
||||
}
|
||||
case CacheAPIs.Album: {
|
||||
const res = data as CacheAPIsResponse[CacheAPIs.Album]
|
||||
if (!res.album) return
|
||||
res.album.songs = data.songs
|
||||
const id = data.album.id
|
||||
const row = { id, json: JSON.stringify(data.album) }
|
||||
await prisma.album.upsert({ where: { id }, update: row, create: row })
|
||||
if (!data.album) return
|
||||
data.album.songs = data.songs
|
||||
db.upsert(Tables.Album, {
|
||||
id: data.album.id,
|
||||
json: JSON.stringify(data.album),
|
||||
updatedAt: Date.now(),
|
||||
})
|
||||
break
|
||||
}
|
||||
case CacheAPIs.Playlist: {
|
||||
if (!data.playlist) return
|
||||
const id = data.playlist.id
|
||||
const row = { id, json: JSON.stringify(data) }
|
||||
await prisma.playlist.upsert({ where: { id }, update: row, create: row })
|
||||
db.upsert(Tables.Playlist, {
|
||||
id: data.playlist.id,
|
||||
json: JSON.stringify(data),
|
||||
updatedAt: Date.now(),
|
||||
})
|
||||
break
|
||||
}
|
||||
case CacheAPIs.Artist: {
|
||||
if (!data.artist) return
|
||||
const id = data.artist.id
|
||||
const row = { id, json: JSON.stringify(data) }
|
||||
await prisma.artist.upsert({ where: { id }, update: row, create: row })
|
||||
db.upsert(Tables.Artist, {
|
||||
id: data.artist.id,
|
||||
json: JSON.stringify(data),
|
||||
updatedAt: Date.now(),
|
||||
})
|
||||
break
|
||||
}
|
||||
case CacheAPIs.ArtistAlbum: {
|
||||
const res = data as CacheAPIsResponse[CacheAPIs.ArtistAlbum]
|
||||
if (!res.hotAlbums) return
|
||||
|
||||
const id = data.artist.id
|
||||
const row = { id, hotAlbums: res.hotAlbums.map(a => a.id).join(',') }
|
||||
await prisma.artistAlbum.upsert({ where: { id }, update: row, create: row })
|
||||
await Promise.all(
|
||||
res.hotAlbums.map(async album => {
|
||||
const id = album.id
|
||||
const existAlbum = await prisma.album.findUnique({ where: { id } })
|
||||
if (!existAlbum) {
|
||||
await prisma.album.create({ data: { id, json: JSON.stringify(album) } })
|
||||
}
|
||||
})
|
||||
if (!data.hotAlbums) return
|
||||
db.createMany(
|
||||
Tables.Album,
|
||||
data.hotAlbums.map((a: Album) => ({
|
||||
id: a.id,
|
||||
json: JSON.stringify(a),
|
||||
updatedAt: Date.now(),
|
||||
}))
|
||||
)
|
||||
const modifiedData = {
|
||||
...data,
|
||||
hotAlbums: data.hotAlbums.map((a: Album) => a.id),
|
||||
}
|
||||
db.upsert(Tables.ArtistAlbum, {
|
||||
id: data.artist.id,
|
||||
json: JSON.stringify(modifiedData),
|
||||
updatedAt: Date.now(),
|
||||
})
|
||||
break
|
||||
}
|
||||
case CacheAPIs.Lyric: {
|
||||
if (!data.lrc) return
|
||||
const id = Number(query.id)
|
||||
const row = { id, json: JSON.stringify(data) }
|
||||
await prisma.lyrics.upsert({ where: { id }, update: row, create: row })
|
||||
db.upsert(Tables.Lyrics, {
|
||||
id: query.id,
|
||||
json: JSON.stringify(data),
|
||||
updatedAt: Date.now(),
|
||||
})
|
||||
break
|
||||
}
|
||||
case CacheAPIs.CoverColor: {
|
||||
if (!data.id || !data.color) return
|
||||
if (/^#([a-fA-F0-9]){3}$|[a-fA-F0-9]{6}$/.test(data.color) === false) {
|
||||
return
|
||||
}
|
||||
db.upsert(Tables.CoverColor, {
|
||||
id: data.id,
|
||||
color: data.color,
|
||||
queriedAt: Date.now(),
|
||||
})
|
||||
break
|
||||
}
|
||||
// case CacheAPIs.CoverColor: {
|
||||
// if (!data.id || !data.color) return
|
||||
// if (/^#([a-fA-F0-9]){3}$|[a-fA-F0-9]{6}$/.test(data.color) === false) {
|
||||
// return
|
||||
// }
|
||||
// db.upsert(Tables.CoverColor, {
|
||||
// id: data.id,
|
||||
// color: data.color,
|
||||
// queriedAt: Date.now(),
|
||||
// })
|
||||
// break
|
||||
// }
|
||||
case CacheAPIs.AppleMusicAlbum: {
|
||||
if (!data.id) return
|
||||
const id = data.id
|
||||
const row = { id, json: JSON.stringify(data) }
|
||||
await prisma.appleMusicAlbum.upsert({ where: { id }, update: row, create: row })
|
||||
db.upsert(Tables.AppleMusicAlbum, {
|
||||
id: data.id,
|
||||
json: data.album ? JSON.stringify(data.album) : 'no',
|
||||
updatedAt: Date.now(),
|
||||
})
|
||||
break
|
||||
}
|
||||
case CacheAPIs.AppleMusicArtist: {
|
||||
if (!data) return
|
||||
const id = data.id
|
||||
const row = { id, json: JSON.stringify(data) }
|
||||
await prisma.artist.upsert({ where: { id }, update: row, create: row })
|
||||
db.upsert(Tables.AppleMusicArtist, {
|
||||
id: data.id,
|
||||
json: data.artist ? JSON.stringify(data.artist) : 'no',
|
||||
updatedAt: Date.now(),
|
||||
})
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
async get<T extends CacheAPIs>(
|
||||
api: T,
|
||||
query: CacheAPIsParams[T]
|
||||
): Promise<CacheAPIsResponse[T] | undefined> {
|
||||
get<T extends keyof CacheAPIsParams>(api: T, params: any): any {
|
||||
switch (api) {
|
||||
case CacheAPIs.UserPlaylist:
|
||||
case CacheAPIs.UserAccount:
|
||||
|
|
@ -132,18 +142,17 @@ class Cache {
|
|||
case CacheAPIs.UserArtists:
|
||||
case CacheAPIs.ListenedRecords:
|
||||
case CacheAPIs.Likelist: {
|
||||
const data = await prisma.accountData.findUnique({ where: { id: api } })
|
||||
const data = db.find(Tables.AccountData, api)
|
||||
if (data?.json) return JSON.parse(data.json)
|
||||
break
|
||||
}
|
||||
case CacheAPIs.Track: {
|
||||
const typedQuery = query as CacheAPIsParams[CacheAPIs.Track]
|
||||
const ids: number[] = typedQuery?.ids.split(',').map((id: string) => Number(id))
|
||||
const ids: number[] = params?.ids.split(',').map((id: string) => Number(id))
|
||||
if (ids.length === 0) return
|
||||
|
||||
if (ids.includes(NaN)) return
|
||||
|
||||
const tracksRaw = await prisma.track.findMany({ where: { id: { in: ids } } })
|
||||
const tracksRaw = db.findMany(Tables.Track, ids)
|
||||
|
||||
if (tracksRaw.length !== ids.length) {
|
||||
return
|
||||
|
|
@ -159,10 +168,8 @@ class Cache {
|
|||
}
|
||||
}
|
||||
case CacheAPIs.Album: {
|
||||
const typedQuery = query as CacheAPIsParams[CacheAPIs.Album]
|
||||
const id = Number(typedQuery?.id)
|
||||
if (isNaN(id)) return
|
||||
const data = await prisma.album.findUnique({ where: { id } })
|
||||
if (isNaN(Number(params?.id))) return
|
||||
const data = db.find(Tables.Album, params.id)
|
||||
if (data?.json)
|
||||
return {
|
||||
resourceState: true,
|
||||
|
|
@ -173,86 +180,91 @@ class Cache {
|
|||
break
|
||||
}
|
||||
case CacheAPIs.Playlist: {
|
||||
const typedQuery = query as CacheAPIsParams[CacheAPIs.Playlist]
|
||||
const id = Number(typedQuery?.id)
|
||||
if (isNaN(id)) return
|
||||
const data = await prisma.playlist.findUnique({ where: { id } })
|
||||
if (isNaN(Number(params?.id))) return
|
||||
const data = db.find(Tables.Playlist, params.id)
|
||||
if (data?.json) return JSON.parse(data.json)
|
||||
break
|
||||
}
|
||||
case CacheAPIs.Artist: {
|
||||
const typedQuery = query as CacheAPIsParams[CacheAPIs.Artist]
|
||||
const id = Number(typedQuery?.id)
|
||||
if (isNaN(id)) return
|
||||
const data = await prisma.artist.findUnique({ where: { id } })
|
||||
if (data?.json) return JSON.parse(data.json)
|
||||
break
|
||||
if (isNaN(Number(params?.id))) return
|
||||
const data = db.find(Tables.Artist, params.id)
|
||||
const fromAppleData = db.find(Tables.AppleMusicArtist, params.id)
|
||||
const fromApple = fromAppleData?.json && JSON.parse(fromAppleData.json)
|
||||
const fromNetease = data?.json && JSON.parse(data.json)
|
||||
if (fromNetease && fromApple && fromApple !== 'no') {
|
||||
fromNetease.artist.img1v1Url = fromApple.attributes.artwork.url
|
||||
fromNetease.artist.briefDesc = fromApple.attributes.artistBio
|
||||
}
|
||||
return fromNetease ? fromNetease : undefined
|
||||
}
|
||||
case CacheAPIs.ArtistAlbum: {
|
||||
const typedQuery = query as CacheAPIsParams[CacheAPIs.ArtistAlbum]
|
||||
const id = Number(typedQuery?.id)
|
||||
if (isNaN(id)) return
|
||||
if (isNaN(Number(params?.id))) return
|
||||
|
||||
const artistAlbums = await prisma.artistAlbum.findUnique({ where: { id } })
|
||||
if (!artistAlbums?.hotAlbums) return
|
||||
const ids = artistAlbums.hotAlbums.split(',').map(Number)
|
||||
const artistAlbumsRaw = db.find(Tables.ArtistAlbum, params.id)
|
||||
if (!artistAlbumsRaw?.json) return
|
||||
const artistAlbums = JSON.parse(artistAlbumsRaw.json)
|
||||
|
||||
const albumsRaw = await prisma.album.findMany({
|
||||
where: { id: { in: ids } },
|
||||
})
|
||||
if (albumsRaw.length !== ids.length) return
|
||||
const albumsRaw = db.findMany(Tables.Album, artistAlbums.hotAlbums)
|
||||
if (albumsRaw.length !== artistAlbums.hotAlbums.length) return
|
||||
const albums = albumsRaw.map(a => JSON.parse(a.json))
|
||||
return {
|
||||
hotAlbums: ids.map((id: number) => albums.find(a => a.id === id)),
|
||||
}
|
||||
|
||||
artistAlbums.hotAlbums = artistAlbums.hotAlbums.map((id: number) =>
|
||||
albums.find(a => a.id === id)
|
||||
)
|
||||
return artistAlbums
|
||||
}
|
||||
case CacheAPIs.Lyric: {
|
||||
const typedQuery = query as CacheAPIsParams[CacheAPIs.Lyric]
|
||||
const id = Number(typedQuery?.id)
|
||||
if (isNaN(id)) return
|
||||
const data = await prisma.lyrics.findUnique({ where: { id } })
|
||||
if (isNaN(Number(params?.id))) return
|
||||
const data = db.find(Tables.Lyrics, params.id)
|
||||
if (data?.json) return JSON.parse(data.json)
|
||||
break
|
||||
}
|
||||
case CacheAPIs.CoverColor: {
|
||||
// if (isNaN(Number(params?.id))) return
|
||||
// return db.find(Tables.CoverColor, params.id)?.color
|
||||
if (isNaN(Number(params?.id))) return
|
||||
return db.find(Tables.CoverColor, params.id)?.color
|
||||
}
|
||||
case CacheAPIs.Artist: {
|
||||
if (!params.ids?.length) return
|
||||
const artists = db.findMany(Tables.Artist, params.ids)
|
||||
if (artists.length !== params.ids.length) return
|
||||
const result = artists.map(a => JSON.parse(a.json))
|
||||
result.sort((a, b) => {
|
||||
const indexA: number = params.ids.indexOf(a.artist.id)
|
||||
const indexB: number = params.ids.indexOf(b.artist.id)
|
||||
return indexA - indexB
|
||||
})
|
||||
return result
|
||||
}
|
||||
case CacheAPIs.AppleMusicAlbum: {
|
||||
const typedQuery = query as CacheAPIsParams[CacheAPIs.AppleMusicAlbum]
|
||||
const id = Number(typedQuery?.id)
|
||||
if (isNaN(id)) return
|
||||
const data = await prisma.appleMusicAlbum.findUnique({ where: { id } })
|
||||
if (data?.json) return JSON.parse(data.json)
|
||||
if (isNaN(Number(params?.id))) return
|
||||
const data = db.find(Tables.AppleMusicAlbum, params.id)
|
||||
if (data?.json && data.json !== 'no') return JSON.parse(data.json)
|
||||
break
|
||||
}
|
||||
case CacheAPIs.AppleMusicArtist: {
|
||||
const typedQuery = query as CacheAPIsParams[CacheAPIs.AppleMusicArtist]
|
||||
const id = Number(typedQuery?.id)
|
||||
if (isNaN(id)) return
|
||||
const data = await prisma.appleMusicArtist.findUnique({ where: { id } })
|
||||
if (data?.json) return JSON.parse(data.json)
|
||||
if (isNaN(Number(params?.id))) return
|
||||
const data = db.find(Tables.AppleMusicArtist, params.id)
|
||||
if (data?.json && data.json !== 'no') return JSON.parse(data.json)
|
||||
break
|
||||
}
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
async getAudio(filename: string, reply: FastifyReply) {
|
||||
if (!filename) {
|
||||
getAudio(fileName: string, reply: FastifyReply) {
|
||||
if (!fileName) {
|
||||
return reply.status(400).send({ error: 'No filename provided' })
|
||||
}
|
||||
const id = Number(filename.split('-')[0])
|
||||
const id = Number(fileName.split('-')[0])
|
||||
|
||||
try {
|
||||
const path = `${app.getPath('userData')}/audio_cache/${filename}`
|
||||
const path = `${app.getPath('userData')}/audio_cache/${fileName}`
|
||||
const audio = fs.readFileSync(path)
|
||||
if (audio.byteLength === 0) {
|
||||
prisma.audio.delete({ where: { id } })
|
||||
db.delete(Tables.Audio, id)
|
||||
fs.unlinkSync(path)
|
||||
return reply.status(404).send({ error: 'Audio not found' })
|
||||
}
|
||||
await prisma.audio.update({ where: { id }, data: { updatedAt: new Date() } })
|
||||
db.update(Tables.Audio, id, { queriedAt: Date.now() })
|
||||
reply
|
||||
.status(206)
|
||||
.header('Accept-Ranges', 'bytes')
|
||||
|
|
@ -264,10 +276,7 @@ class Cache {
|
|||
}
|
||||
}
|
||||
|
||||
async setAudio(
|
||||
buffer: Buffer,
|
||||
{ id, url, bitrate }: { id: number; url: string; bitrate: number }
|
||||
) {
|
||||
async setAudio(buffer: Buffer, { id, url }: { id: number; url: string }) {
|
||||
const path = `${app.getPath('userData')}/audio_cache`
|
||||
|
||||
try {
|
||||
|
|
@ -277,8 +286,8 @@ class Cache {
|
|||
}
|
||||
|
||||
const meta = await musicMetadata.parseBuffer(buffer)
|
||||
const bitRate = ~~((meta.format.bitrate || bitrate || 0) / 1000)
|
||||
const format =
|
||||
const bitRate = meta?.format?.codec === 'OPUS' ? 165000 : meta.format.bitrate ?? 0
|
||||
const type =
|
||||
{
|
||||
'MPEG 1 Layer 3': 'mp3',
|
||||
'Ogg Vorbis': 'ogg',
|
||||
|
|
@ -287,23 +296,25 @@ class Cache {
|
|||
OPUS: 'opus',
|
||||
}[meta.format.codec ?? ''] ?? 'unknown'
|
||||
|
||||
let source = 'unknown'
|
||||
let source: TablesStructures[Tables.Audio]['source'] = 'unknown'
|
||||
if (url.includes('googlevideo.com')) source = 'youtube'
|
||||
if (url.includes('126.net')) source = 'netease'
|
||||
|
||||
fs.writeFile(`${path}/${id}-${bitRate}.${format}`, buffer, async error => {
|
||||
fs.writeFile(`${path}/${id}-${bitRate}.${type}`, buffer, error => {
|
||||
if (error) {
|
||||
return log.error(`[cache] cacheAudio failed: ${error}`)
|
||||
}
|
||||
log.info(`Audio file ${id}-${bitRate}.${type} cached!`)
|
||||
|
||||
const row = { id, bitRate, format, source }
|
||||
await prisma.audio.upsert({
|
||||
where: { id },
|
||||
create: row,
|
||||
update: row,
|
||||
db.upsert(Tables.Audio, {
|
||||
id,
|
||||
bitRate,
|
||||
format: type as TablesStructures[Tables.Audio]['format'],
|
||||
source,
|
||||
queriedAt: Date.now(),
|
||||
})
|
||||
|
||||
log.info(`Audio file ${id}-${bitRate}.${format} cached!`)
|
||||
log.info(`[cache] cacheAudio ${id}-${bitRate}.${type}`)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
|
|
|||
243
packages/desktop/main/db.ts
Normal file
243
packages/desktop/main/db.ts
Normal file
|
|
@ -0,0 +1,243 @@
|
|||
import path from 'path'
|
||||
import { app } from 'electron'
|
||||
import fs from 'fs'
|
||||
import SQLite3 from 'better-sqlite3'
|
||||
import log from './log'
|
||||
import { createFileIfNotExist, dirname } from './utils'
|
||||
import { isProd } from './env'
|
||||
import pkg from '../../../package.json'
|
||||
import { compare, validate } from 'compare-versions'
|
||||
import os from 'os'
|
||||
|
||||
export const enum Tables {
|
||||
Track = 'Track',
|
||||
Album = 'Album',
|
||||
Artist = 'Artist',
|
||||
Playlist = 'Playlist',
|
||||
ArtistAlbum = 'ArtistAlbum',
|
||||
Lyrics = 'Lyrics',
|
||||
Audio = 'Audio',
|
||||
AccountData = 'AccountData',
|
||||
CoverColor = 'CoverColor',
|
||||
AppData = 'AppData',
|
||||
AppleMusicAlbum = 'AppleMusicAlbum',
|
||||
AppleMusicArtist = 'AppleMusicArtist',
|
||||
}
|
||||
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.Lyrics]: CommonTableStructure
|
||||
[Tables.AccountData]: {
|
||||
id: string
|
||||
json: string
|
||||
updatedAt: number
|
||||
}
|
||||
[Tables.Audio]: {
|
||||
id: number
|
||||
bitRate: number
|
||||
format: 'mp3' | 'flac' | 'ogg' | 'wav' | 'm4a' | 'aac' | 'unknown' | 'opus'
|
||||
source:
|
||||
| 'unknown'
|
||||
| 'netease'
|
||||
| 'migu'
|
||||
| 'kuwo'
|
||||
| 'kugou'
|
||||
| 'youtube'
|
||||
| 'qq'
|
||||
| 'bilibili'
|
||||
| 'joox'
|
||||
queriedAt: number
|
||||
}
|
||||
[Tables.CoverColor]: {
|
||||
id: number
|
||||
color: string
|
||||
queriedAt: number
|
||||
}
|
||||
[Tables.AppData]: {
|
||||
id: 'appVersion' | 'skippedVersion'
|
||||
value: string
|
||||
}
|
||||
[Tables.AppleMusicAlbum]: CommonTableStructure
|
||||
[Tables.AppleMusicArtist]: CommonTableStructure
|
||||
}
|
||||
|
||||
type TableNames = keyof TablesStructures
|
||||
|
||||
const readSqlFile = (filename: string) => {
|
||||
return fs.readFileSync(path.join(dirname, `./migrations/${filename}`), 'utf8')
|
||||
}
|
||||
|
||||
class DB {
|
||||
sqlite: SQLite3.Database
|
||||
dbFilePath: string = path.resolve(app.getPath('userData'), './api_cache/db.sqlite')
|
||||
|
||||
constructor() {
|
||||
log.info('[db] Initializing database...')
|
||||
|
||||
try {
|
||||
createFileIfNotExist(this.dbFilePath)
|
||||
|
||||
this.sqlite = new SQLite3(this.dbFilePath, {
|
||||
nativeBinding: this.getBinPath(),
|
||||
})
|
||||
this.sqlite.pragma('auto_vacuum = FULL')
|
||||
this.initTables()
|
||||
this.migrate()
|
||||
|
||||
log.info('[db] Database initialized.')
|
||||
} catch (e) {
|
||||
log.error('[db] Database initialization failed.')
|
||||
log.error(e)
|
||||
}
|
||||
}
|
||||
|
||||
private getBinPath() {
|
||||
console
|
||||
const devBinPath = path.resolve(
|
||||
app.getPath('userData'),
|
||||
`../../bin/better_sqlite3_${os.platform}_${os.arch}.node`
|
||||
)
|
||||
const prodBinPaths = {
|
||||
darwin: path.resolve(app.getPath('exe'), `../../Resources/bin/better_sqlite3.node`),
|
||||
win32: path.resolve(app.getPath('exe'), `../resources/bin/better_sqlite3.node`),
|
||||
linux: '',
|
||||
}
|
||||
return isProd
|
||||
? prodBinPaths[os.platform as unknown as 'darwin' | 'win32' | 'linux']
|
||||
: devBinPath
|
||||
}
|
||||
|
||||
initTables() {
|
||||
log.info('[db] Initializing database tables...')
|
||||
const init = readSqlFile('init.sql')
|
||||
this.sqlite.exec(init)
|
||||
this.sqlite.pragma('journal_mode=WAL')
|
||||
log.info('[db] Database tables initialized.')
|
||||
}
|
||||
|
||||
migrate() {
|
||||
log.info('[db] Migrating database..')
|
||||
|
||||
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()
|
||||
|
||||
log.info('[db] Database migrated.')
|
||||
}
|
||||
|
||||
find<T extends TableNames>(
|
||||
table: T,
|
||||
key: TablesStructures[T]['id']
|
||||
): TablesStructures[T] | undefined {
|
||||
return this.sqlite.prepare(`SELECT * FROM ${table} WHERE id = ? LIMIT 1`).get(key)
|
||||
}
|
||||
|
||||
findMany<T extends TableNames>(
|
||||
table: T,
|
||||
keys: TablesStructures[T]['id'][]
|
||||
): TablesStructures[T][] {
|
||||
const idsQuery = keys.map(key => `id = ${key}`).join(' OR ')
|
||||
return this.sqlite.prepare(`SELECT * FROM ${table} WHERE ${idsQuery}`).all()
|
||||
}
|
||||
|
||||
findAll<T extends TableNames>(table: T): TablesStructures[T][] {
|
||||
return this.sqlite.prepare(`SELECT * FROM ${table}`).all()
|
||||
}
|
||||
|
||||
create<T extends TableNames>(table: T, data: TablesStructures[T], skipWhenExist: boolean = true) {
|
||||
if (skipWhenExist && db.find(table, data.id)) return
|
||||
return this.sqlite.prepare(`INSERT INTO ${table} VALUES (?)`).run(data)
|
||||
}
|
||||
|
||||
createMany<T extends TableNames>(
|
||||
table: T,
|
||||
data: TablesStructures[T][],
|
||||
skipWhenExist: boolean = true
|
||||
) {
|
||||
const valuesQuery = Object.keys(data[0])
|
||||
.map(key => `:${key}`)
|
||||
.join(', ')
|
||||
const insert = this.sqlite.prepare(
|
||||
`INSERT ${skipWhenExist ? 'OR IGNORE' : ''} INTO ${table} VALUES (${valuesQuery})`
|
||||
)
|
||||
const insertMany = this.sqlite.transaction((rows: any[]) => {
|
||||
rows.forEach((row: any) => insert.run(row))
|
||||
})
|
||||
insertMany(data)
|
||||
}
|
||||
|
||||
update<T extends TableNames>(
|
||||
table: T,
|
||||
key: TablesStructures[T]['id'],
|
||||
data: Partial<TablesStructures[T]>
|
||||
) {
|
||||
// TODO:
|
||||
}
|
||||
|
||||
upsert<T extends TableNames>(table: T, data: TablesStructures[T]) {
|
||||
const valuesQuery = Object.keys(data)
|
||||
.map(key => `:${key}`)
|
||||
.join(', ')
|
||||
return this.sqlite.prepare(`INSERT OR REPLACE INTO ${table} VALUES (${valuesQuery})`).run(data)
|
||||
}
|
||||
|
||||
upsertMany<T extends TableNames>(table: T, data: TablesStructures[T][]) {
|
||||
const valuesQuery = Object.keys(data[0])
|
||||
.map(key => `:${key}`)
|
||||
.join(', ')
|
||||
const upsert = this.sqlite.prepare(`INSERT OR REPLACE INTO ${table} VALUES (${valuesQuery})`)
|
||||
const upsertMany = this.sqlite.transaction((rows: any[]) => {
|
||||
rows.forEach((row: any) => upsert.run(row))
|
||||
})
|
||||
upsertMany(data)
|
||||
}
|
||||
|
||||
delete<T extends TableNames>(table: T, key: TablesStructures[T]['id']) {
|
||||
return this.sqlite.prepare(`DELETE FROM ${table} WHERE id = ?`).run(key)
|
||||
}
|
||||
|
||||
deleteMany<T extends TableNames>(table: T, keys: TablesStructures[T]['id'][]) {
|
||||
const idsQuery = keys.map(key => `id = ${key}`).join(' OR ')
|
||||
return this.sqlite.prepare(`DELETE FROM ${table} WHERE ${idsQuery}`).run()
|
||||
}
|
||||
|
||||
truncate<T extends TableNames>(table: T) {
|
||||
return this.sqlite.prepare(`DELETE FROM ${table}`).run()
|
||||
}
|
||||
|
||||
vacuum() {
|
||||
return this.sqlite.prepare('VACUUM').run()
|
||||
}
|
||||
}
|
||||
|
||||
export const db = new DB()
|
||||
|
|
@ -12,7 +12,6 @@ import { createMenu } from './menu'
|
|||
import { isDev, isWindows, isLinux, isMac, appName } from './env'
|
||||
import store from './store'
|
||||
import initAppServer from './appServer/appServer'
|
||||
import { initDatabase } from './prisma'
|
||||
|
||||
class Main {
|
||||
win: BrowserWindow | null = null
|
||||
|
|
@ -35,7 +34,6 @@ class Main {
|
|||
|
||||
app.whenReady().then(async () => {
|
||||
log.info('[index] App ready')
|
||||
await initDatabase()
|
||||
await initAppServer()
|
||||
this.createWindow()
|
||||
this.handleAppEvents()
|
||||
|
|
|
|||
|
|
@ -12,7 +12,7 @@ import { Thumbar } from './windowsTaskbar'
|
|||
import fastFolderSize from 'fast-folder-size'
|
||||
import path from 'path'
|
||||
import prettyBytes from 'pretty-bytes'
|
||||
import prisma from './prisma'
|
||||
import { db, Tables } from './db'
|
||||
|
||||
const on = <T extends keyof IpcChannelsParams>(
|
||||
channel: T,
|
||||
|
|
@ -204,7 +204,7 @@ function initOtherIpcMain() {
|
|||
* 退出登陆
|
||||
*/
|
||||
handle(IpcChannels.Logout, async () => {
|
||||
await prisma.accountData.deleteMany({})
|
||||
await db.truncate(Tables.AccountData)
|
||||
return true
|
||||
})
|
||||
|
||||
|
|
|
|||
|
|
@ -1,102 +0,0 @@
|
|||
import { app } from 'electron'
|
||||
import path from 'path'
|
||||
import { PrismaClient } from '../prisma/client'
|
||||
import { isDev, isWindows } from './env'
|
||||
import log from './log'
|
||||
import { createFileIfNotExist, dirname, isFileExist } from './utils'
|
||||
import fs from 'fs'
|
||||
import { dialog } from 'electron'
|
||||
|
||||
export const dbPath = path.join(app.getPath('userData'), 'r3play.db')
|
||||
export const dbUrl = 'file:' + (isWindows ? '' : '//') + dbPath
|
||||
log.info('[prisma] dbUrl', dbUrl)
|
||||
|
||||
const extraResourcesPath = app.getAppPath().replace('app.asar', '') // impacted by extraResources setting in electron-builder.yml
|
||||
function getPlatformName(): string {
|
||||
const isDarwin = process.platform === 'darwin'
|
||||
if (isDarwin && process.arch === 'arm64') {
|
||||
return process.platform + 'Arm64'
|
||||
}
|
||||
|
||||
return process.platform
|
||||
}
|
||||
const platformName = getPlatformName()
|
||||
export const platformToExecutables: any = {
|
||||
win32: {
|
||||
migrationEngine: 'node_modules/@prisma/engines/migration-engine-windows.exe',
|
||||
queryEngine: 'node_modules/@prisma/engines/query_engine-windows.dll.node',
|
||||
},
|
||||
linux: {
|
||||
migrationEngine: 'node_modules/@prisma/engines/migration-engine-debian-openssl-1.1.x',
|
||||
queryEngine: 'node_modules/@prisma/engines/libquery_engine-debian-openssl-1.1.x.so.node',
|
||||
},
|
||||
darwin: {
|
||||
migrationEngine: 'node_modules/@prisma/engines/migration-engine-darwin',
|
||||
queryEngine: 'node_modules/@prisma/engines/libquery_engine-darwin.dylib.node',
|
||||
},
|
||||
darwinArm64: {
|
||||
migrationEngine: 'node_modules/@prisma/engines/migration-engine-darwin-arm64',
|
||||
queryEngine: 'node_modules/@prisma/engines/libquery_engine-darwin-arm64.dylib.node',
|
||||
},
|
||||
}
|
||||
export const queryEnginePath = path.join(
|
||||
extraResourcesPath,
|
||||
platformToExecutables[platformName].queryEngine
|
||||
)
|
||||
|
||||
log.info('[prisma] dbUrl', dbUrl)
|
||||
|
||||
// Hacky, but putting this here because otherwise at query time the Prisma client
|
||||
// gives an error "Environment variable not found: DATABASE_URL" despite us passing
|
||||
// the dbUrl into the prisma client constructor in datasources.db.url
|
||||
process.env.DATABASE_URL = dbUrl
|
||||
|
||||
createFileIfNotExist(dbPath)
|
||||
|
||||
// @ts-expect-error
|
||||
let prisma: PrismaClient = null
|
||||
try {
|
||||
prisma = new PrismaClient({
|
||||
log: isDev ? ['info', 'warn', 'error'] : ['error'],
|
||||
datasources: {
|
||||
db: {
|
||||
url: dbUrl,
|
||||
},
|
||||
},
|
||||
// see https://github.com/prisma/prisma/discussions/5200
|
||||
// @ts-expect-error internal prop
|
||||
// __internal: {
|
||||
// engine: {
|
||||
// binaryPath: queryEnginePath,
|
||||
// },
|
||||
// },
|
||||
})
|
||||
log.info('[prisma] prisma initialized')
|
||||
} catch (e) {
|
||||
log.error('[prisma] failed to init prisma', e)
|
||||
dialog.showErrorBox('Failed to init prisma', String(e))
|
||||
app.exit()
|
||||
}
|
||||
|
||||
export const initDatabase = async () => {
|
||||
try {
|
||||
const initSQLFile = fs
|
||||
.readFileSync(path.join(dirname, 'migrations/init.sql'), 'utf-8')
|
||||
.toString()
|
||||
const tables = initSQLFile.split(';')
|
||||
await Promise.all(
|
||||
tables.map(sql => {
|
||||
if (!sql.trim()) return
|
||||
return prisma.$executeRawUnsafe(sql.trim()).catch(() => {
|
||||
log.error('[prisma] failed to execute init sql >>> ', sql.trim())
|
||||
})
|
||||
})
|
||||
)
|
||||
} catch (e) {
|
||||
dialog.showErrorBox('Failed to init prisma database', String(e))
|
||||
app.exit()
|
||||
}
|
||||
log.info('[prisma] database initialized')
|
||||
}
|
||||
|
||||
export default prisma
|
||||
Loading…
Add table
Add a link
Reference in a new issue