feat: 使用sqlite3替换realm

This commit is contained in:
qier222 2022-03-30 00:53:05 +08:00
parent c4219afd3d
commit 1b86cbbee1
No known key found for this signature in database
GPG Key ID: 9C85007ED905F14D
20 changed files with 292 additions and 691 deletions

View File

@ -27,15 +27,16 @@
"@sentry/node": "^6.19.2",
"@sentry/tracing": "^6.19.2",
"NeteaseCloudMusicApi": "^4.5.8",
"better-sqlite3": "^7.5.0",
"change-case": "^4.1.2",
"cookie-parser": "^1.4.6",
"electron-log": "^4.4.6",
"electron-store": "^8.0.1",
"express": "^4.17.3",
"realm": "^10.13.0"
"express": "^4.17.3"
},
"devDependencies": {
"@sentry/react": "^6.19.2",
"@types/better-sqlite3": "^7.5.0",
"@types/cookie-parser": "^1.4.2",
"@types/express": "^4.17.13",
"@types/express-fileupload": "^1.2.2",

588
pnpm-lock.yaml generated

File diff suppressed because it is too large Load Diff

View File

@ -25,6 +25,7 @@ const options = {
),
'electron',
'NeteaseCloudMusicApi',
'better-sqlite3',
],
}

View File

@ -1,5 +1,5 @@
import { db, ModelNames, realm } from './database'
import type { FetchTracksResponse } from '../renderer/src/api/track'
import { db, Tables } from './db'
import type { FetchTracksResponse } from '../renderer/api/track'
import { app, ipcMain } from 'electron'
import { Request, Response } from 'express'
import logger from './logger'
@ -8,86 +8,82 @@ import * as musicMetadata from 'music-metadata'
export async function setCache(api: string, data: any, query: any) {
switch (api) {
case 'user/playlist':
case 'user/account':
case 'personalized':
case 'recommend/resource':
case 'likelist': {
if (!data) return
db.set(ModelNames.ACCOUNT_DATA, api, data)
break
}
case 'user/playlist': {
if (!data.playlist) return
db.set(ModelNames.USER_PLAYLISTS, Number(query.uid), data)
console.log(api)
db.upsert(Tables.ACCOUNT_DATA, {
id: api,
json: JSON.stringify(data),
updateAt: Date.now(),
})
break
}
case 'song/detail': {
console.log('dsdadasdas')
if (!data.songs) return
const tracks = (data as FetchTracksResponse).songs
db.batchSet(
ModelNames.TRACK,
tracks.map(t => ({
id: t.id,
json: JSON.stringify(t),
updateAt: Date.now(),
}))
)
const tracks = (data as FetchTracksResponse).songs.map(t => ({
id: t.id,
json: JSON.stringify(t),
updatedAt: Date.now(),
}))
db.upsertMany(Tables.TRACK, tracks)
break
}
case 'album': {
if (!data.album) return
data.album.songs = (data as FetchTracksResponse).songs
db.set(ModelNames.ALBUM, Number(data.album.id), data)
db.upsert(Tables.ALBUM, {
id: data.album.id,
json: JSON.stringify(data),
updatedAt: Date.now(),
})
break
}
case 'playlist/detail': {
if (!data.playlist) return
db.set(ModelNames.PLAYLIST, Number(data.playlist.id), data)
db.upsert(Tables.PLAYLIST, {
id: data.playlist.id,
json: JSON.stringify(data),
updatedAt: Date.now(),
})
break
}
case 'artists': {
if (!data.artist) return
db.set(ModelNames.ARTIST, Number(data.artist.id), data)
db.upsert(Tables.ARTIST, {
id: data.artist.id,
json: JSON.stringify(data),
updatedAt: Date.now(),
})
break
}
case 'artist/album': {
if (!data.hotAlbums) return
db.set(ModelNames.ARTIST_ALBUMS, Number(data.artist.id), data)
db.upsert(Tables.ARTIST_ALBUMS, {
id: data.artist.id,
json: JSON.stringify(data),
updatedAt: Date.now(),
})
break
}
}
}
/**
* Check if the cache is expired
* @param updateAt from database, milliseconds
* @param staleTime minutes
*/
const isCacheExpired = (updateAt: number, staleTime: number) => {
return Date.now() - updateAt > staleTime * 1000 * 60
}
export function getCache(
api: string,
query: any,
checkIsExpired: boolean = false
): any {
export function getCache(api: string, query: any): any {
switch (api) {
case 'user/account':
case 'user/playlist':
case 'personalized':
case 'recommend/resource':
case 'likelist': {
const data = db.get(ModelNames.ACCOUNT_DATA, api) as any
const data = db.find(Tables.ACCOUNT_DATA, api)
if (data?.json) return JSON.parse(data.json)
break
}
case 'user/playlist': {
if (isNaN(Number(query.uid))) return
const userPlaylists = db.get(
ModelNames.USER_PLAYLISTS,
Number(query?.uid)
) as any
if (userPlaylists?.json) return JSON.parse(userPlaylists.json)
break
}
case 'song/detail': {
const ids: string[] = query?.ids.split(',')
if (ids.length === 0) return
@ -98,10 +94,8 @@ export function getCache(
})
if (!isIDsValid) return
const idsQuery = ids.map(id => `id = ${id}`).join(' OR ')
const tracksRaw = realm
.objects(ModelNames.TRACK)
.filtered(`(${idsQuery})`)
const tracksRaw = db.findMany(Tables.TRACK, ids)
if (tracksRaw.length !== ids.length) {
return
}
@ -118,33 +112,27 @@ export function getCache(
}
case 'album': {
if (isNaN(Number(query?.id))) return
const album = db.get(ModelNames.ALBUM, Number(query?.id)) as any
if (checkIsExpired && isCacheExpired(album?.updateAt, 24 * 60)) return
if (album?.json) return JSON.parse(album.json)
const data = db.find(Tables.ALBUM, query.id)
console.log(data)
if (data?.json) return JSON.parse(data.json)
break
}
case 'playlist/detail': {
if (isNaN(Number(query?.id))) return
const playlist = db.get(ModelNames.PLAYLIST, Number(query?.id)) as any
if (checkIsExpired && isCacheExpired(playlist?.updateAt, 10)) return
if (playlist?.json) return JSON.parse(playlist.json)
const data = db.find(Tables.PLAYLIST, query.id)
if (data?.json) return JSON.parse(data.json)
break
}
case 'artists': {
if (isNaN(Number(query?.id))) return
const artist = db.get(ModelNames.ARTIST, Number(query?.id)) as any
if (checkIsExpired && isCacheExpired(artist?.updateAt, 30)) return
if (artist?.json) return JSON.parse(artist.json)
const data = db.find(Tables.ARTIST, query.id)
if (data?.json) return JSON.parse(data.json)
break
}
case 'artist/album': {
if (isNaN(Number(query?.id))) return
const artistAlbums = db.get(
ModelNames.ARTIST_ALBUMS,
Number(query?.id)
) as any
if (checkIsExpired && isCacheExpired(artistAlbums?.updateAt, 30)) return
if (artistAlbums?.json) return JSON.parse(artistAlbums.json)
const data = db.find(Tables.ARTIST_ALBUMS, query.id)
if (data?.json) return JSON.parse(data.json)
break
}
}
@ -162,7 +150,7 @@ export async function getCacheForExpress(api: string, req: Request) {
// Get audio cache if API is song/detail
if (api === 'song/url') {
const cache = db.get(ModelNames.AUDIO, Number(req.query.id)) as any
const cache = db.find(Tables.AUDIO, Number(req.query.id))
if (!cache) return
const audioFileName = `${cache.id}-${cache.br}.${cache.type}`
@ -224,7 +212,7 @@ export function getAudioCache(fileName: string, res: Response) {
const path = `${app.getPath('userData')}/audio_cache/${fileName}`
const audio = fs.readFileSync(path)
if (audio.byteLength === 0) {
db.delete(ModelNames.AUDIO, Number(id))
db.delete(Tables.AUDIO, id)
fs.unlinkSync(path)
return res.status(404).send({ error: 'Audio not found' })
}
@ -263,18 +251,12 @@ export async function cacheAudio(
}
logger.info(`Audio file ${id}-${br}.${type} cached!`)
realm.write(() => {
realm.create(
ModelNames.AUDIO,
{
id: Number(id),
type,
br,
source,
updateAt: Date.now(),
},
'modified'
)
db.upsert(Tables.AUDIO, {
id,
br,
type,
source,
updateAt: Date.now(),
})
logger.info(`[cache] cacheAudio ${id}-${br}.${type}`)
@ -283,6 +265,6 @@ export async function cacheAudio(
ipcMain.on('getApiCacheSync', (event, args) => {
const { api, query } = args
const data = getCache(api, query, false)
const data = getCache(api, query)
event.returnValue = data
})

View File

@ -122,10 +122,4 @@ ipcMain.on('test', () => {
console.log('The file was saved!')
})
})
// realm.write(() => {
// realm.deleteAll()
// })
realm.compact()
})

88
src/main/db.ts Normal file
View File

@ -0,0 +1,88 @@
import path from 'path'
import { app, ipcMain } from 'electron'
import fs from 'fs'
import SQLite3 from 'better-sqlite3'
export enum Tables {
TRACK = 'track',
ALBUM = 'album',
ARTIST = 'artist',
PLAYLIST = 'playlist',
ARTIST_ALBUMS = 'artist_album',
USER_PLAYLISTS = 'user_playlist',
// Special tables
ACCOUNT_DATA = 'account_data',
AUDIO = 'audio',
}
const sqlite = new SQLite3(
path.resolve(app.getPath('userData'), './api_cache/db.sqlite')
)
export const db = {
find: (table: Tables, key: number | string) => {
return sqlite
.prepare(`SELECT * FROM ${table} WHERE id = ? LIMIT 1`)
.get(key)
},
findMany: (table: Tables, keys: number[] | string[]) => {
const idsQuery = keys.map(key => `id = ${key}`).join(' OR ')
return sqlite.prepare(`SELECT * FROM ${table} WHERE ${idsQuery}`).all()
},
findAll: (table: Tables) => {
return sqlite.prepare(`SELECT * FROM ${table}`).all()
},
upsert: (table: Tables, data: any) => {
const valuesQuery = Object.keys(data)
.map(key => `:${key}`)
.join(', ')
return sqlite
.prepare(`INSERT OR REPLACE INTO ${table} VALUES (${valuesQuery})`)
.run(data)
},
upsertMany: (table: Tables, data: any[]) => {
const valuesQuery = Object.keys(data[0])
.map(key => `:${key}`)
.join(', ')
const upsert = sqlite.prepare(
`INSERT OR REPLACE INTO ${table} VALUES (${valuesQuery})`
)
const upsertMany = sqlite.transaction((rows: any[]) => {
rows.forEach((row: any) => upsert.run(row))
})
upsertMany(data)
},
delete: (table: Tables, key: number | string) => {
return sqlite.prepare(`DELETE FROM ${table} WHERE id = ?`).run(key)
},
deleteMany: (table: Tables, keys: number[] | string[]) => {
const idsQuery = keys.map(key => `id = ${key}`).join(' OR ')
return sqlite.prepare(`DELETE FROM ${table} WHERE ${idsQuery}`).run()
},
truncate: (table: Tables) => {
return sqlite.prepare(`DELETE FROM ${table}`).run()
},
}
ipcMain.on('db-export-json', () => {
const tables = [
Tables.ARTIST_ALBUMS,
Tables.PLAYLIST,
Tables.ALBUM,
Tables.TRACK,
Tables.ARTIST,
Tables.AUDIO,
Tables.ACCOUNT_DATA,
]
tables.forEach(table => {
const data = db.findAll(table)
fs.writeFile(`./tmp/${table}.json`, JSON.stringify(data), function (err) {
if (err) {
return console.log(err)
}
console.log('The file was saved!')
})
})
})

View File

@ -12,6 +12,7 @@ import path, { join } from 'path'
import logger from './logger'
import './server'
// import './database'
import './db'
const isWindows = process.platform === 'win32'
const isMac = process.platform === 'darwin'

View File

@ -3,7 +3,7 @@ import path from 'path'
import { app } from 'electron'
import fs from 'fs'
const isDev = !app.isPackaged
const isDev = process.env.NODE_ENV === 'development'
if (isDev) {
const devUserDataPath = path.resolve(process.cwd(), './tmp/userData')

View File

@ -8,7 +8,7 @@ logger.info(`[sentry] init sentry`)
Sentry.init({
dsn: 'https://2aaaa67f1c3d4d6baefafa5d58fcf340@o436528.ingest.sentry.io/6274637',
release: `yesplaymusic@${pkg.version}`,
// environment: import.meta.env.MODE,
environment: process.env.NODE_ENV,
// Set tracesSampleRate to 1.0 to capture 100%
// of transactions for performance monitoring.

View File

@ -2,12 +2,12 @@ import { pathCase } from 'change-case'
import cookieParser from 'cookie-parser'
import express, { Request, Response } from 'express'
import logger from './logger'
// import {
// setCache,
// getCacheForExpress,
// cacheAudio,
// getAudioCache,
// } from './cache'
import {
setCache,
getCacheForExpress,
cacheAudio,
getAudioCache,
} from './cache'
import fileUpload from 'express-fileupload'
// eslint-disable-next-line @typescript-eslint/no-var-requires
@ -26,8 +26,8 @@ Object.entries(neteaseApi).forEach(([name, handler]) => {
logger.info(`[server] Handling request: ${req.path}`)
// Get from cache
// const cache = await getCacheForExpress(name, req)
// if (cache) return res.json(cache)
const cache = await getCacheForExpress(name, req)
if (cache) return res.json(cache)
// Request netease api
try {
@ -36,7 +36,7 @@ Object.entries(neteaseApi).forEach(([name, handler]) => {
cookie: `MUSIC_U=${req.cookies['MUSIC_U']}`,
})
// setCache(name, result.body, req.query)
setCache(name, result.body, req.query)
return res.send(result.body)
} catch (error) {
return res.status(500).send(error)
@ -51,7 +51,7 @@ Object.entries(neteaseApi).forEach(([name, handler]) => {
app.get(
'/yesplaymusic/audio/:filename',
async (req: Request, res: Response) => {
// getAudioCache(req.params.filename, res)
getAudioCache(req.params.filename, res)
}
)
app.post('/yesplaymusic/audio/:id', async (req: Request, res: Response) => {
@ -72,10 +72,10 @@ app.post('/yesplaymusic/audio/:id', async (req: Request, res: Response) => {
}
try {
// await cacheAudio(req.files.file.data, {
// id: id,
// source: 'netease',
// })
await cacheAudio(req.files.file.data, {
id: id,
source: 'netease',
})
res.status(200).send('Audio cached!')
} catch (error) {
res.status(500).send({ error })

View File

@ -18,13 +18,13 @@ export default function useAlbum(params: FetchAlbumParams, noCache?: boolean) {
{
enabled: !!params.id,
staleTime: 24 * 60 * 60 * 1000, // 24 hours
// placeholderData: (): FetchAlbumResponse =>
// window.ipcRenderer.sendSync('getApiCacheSync', {
// api: 'album',
// query: {
// id: params.id,
// },
// }),
placeholderData: (): FetchAlbumResponse =>
window.ipcRenderer?.sendSync('getApiCacheSync', {
api: 'album',
query: {
id: params.id,
},
}),
}
)
}

View File

@ -12,13 +12,13 @@ export default function useArtist(
{
enabled: !!params.id && params.id > 0 && !isNaN(Number(params.id)),
staleTime: 5 * 60 * 1000, // 5 mins
// placeholderData: (): FetchArtistResponse =>
// window.ipcRenderer.sendSync('getApiCacheSync', {
// api: 'artists',
// query: {
// id: params.id,
// },
// }),
placeholderData: (): FetchArtistResponse =>
window.ipcRenderer?.sendSync('getApiCacheSync', {
api: 'artists',
query: {
id: params.id,
},
}),
}
)
}

View File

@ -15,13 +15,13 @@ export default function useUserAlbums(params: FetchArtistAlbumsParams) {
{
enabled: !!params.id && params.id !== 0,
staleTime: 3600000,
// placeholderData: (): FetchArtistAlbumsResponse =>
// window.ipcRenderer.sendSync('getApiCacheSync', {
// api: 'artist/album',
// query: {
// id: params.id,
// },
// }),
placeholderData: (): FetchArtistAlbumsResponse =>
window.ipcRenderer?.sendSync('getApiCacheSync', {
api: 'artist/album',
query: {
id: params.id,
},
}),
}
)
}

View File

@ -17,13 +17,13 @@ export default function usePlaylist(
{
enabled: !!(params.id && params.id > 0 && !isNaN(Number(params.id))),
refetchOnWindowFocus: true,
// placeholderData: (): FetchPlaylistResponse | undefined =>
// window.ipcRenderer.sendSync('getApiCacheSync', {
// api: 'playlist/detail',
// query: {
// id: params.id,
// },
// }),
placeholderData: (): FetchPlaylistResponse | undefined =>
window.ipcRenderer?.sendSync('getApiCacheSync', {
api: 'playlist/detail',
query: {
id: params.id,
},
}),
}
)
}

View File

@ -16,13 +16,13 @@ export default function useTracks(params: FetchTracksParams) {
enabled: params.ids.length !== 0,
refetchInterval: false,
staleTime: Infinity,
// initialData: (): FetchTracksResponse | undefined =>
// window.ipcRenderer.sendSync('getApiCacheSync', {
// api: 'song/detail',
// query: {
// ids: params.ids.join(','),
// },
// }),
initialData: (): FetchTracksResponse | undefined =>
window.ipcRenderer?.sendSync('getApiCacheSync', {
api: 'song/detail',
query: {
ids: params.ids.join(','),
},
}),
}
)
}

View File

@ -4,9 +4,9 @@ import { UserApiNames } from '@/api/user'
export default function useUser() {
return useQuery(UserApiNames.FETCH_USER_ACCOUNT, fetchUserAccount, {
refetchOnWindowFocus: true,
// placeholderData: (): fetchUserAccountResponse | undefined =>
// window.ipcRenderer.sendSync('getApiCacheSync', {
// api: 'user/account',
// }),
placeholderData: (): fetchUserAccountResponse | undefined =>
window.ipcRenderer?.sendSync('getApiCacheSync', {
api: 'user/account',
}),
})
}

View File

@ -13,13 +13,13 @@ export default function useUserLikedSongsIDs(
{
enabled: !!(params.uid && params.uid !== 0),
refetchOnWindowFocus: true,
// placeholderData: (): FetchUserLikedSongsIDsResponse | undefined =>
// window.ipcRenderer.sendSync('getApiCacheSync', {
// api: 'likelist',
// query: {
// uid: params.uid,
// },
// }),
placeholderData: (): FetchUserLikedSongsIDsResponse | undefined =>
window.ipcRenderer?.sendSync('getApiCacheSync', {
api: 'likelist',
query: {
uid: params.uid,
},
}),
}
)
}

View File

@ -17,13 +17,13 @@ export default function useUserPlaylists(params: FetchUserPlaylistsParams) {
params.uid !== 0 &&
params.offset !== undefined
),
// placeholderData: (): FetchUserPlaylistsResponse =>
// window.ipcRenderer.sendSync('getApiCacheSync', {
// api: 'user/playlist',
// query: {
// uid: params.uid,
// },
// }),
placeholderData: (): FetchUserPlaylistsResponse =>
window.ipcRenderer?.sendSync('getApiCacheSync', {
api: 'user/playlist',
query: {
uid: params.uid,
},
}),
}
)
}

View File

@ -12,7 +12,7 @@ Sentry.init({
dsn: 'https://7cc7879b42ba4bed9f66fb6752558475@o436528.ingest.sentry.io/6274630',
integrations: [new BrowserTracing()],
release: `yesplaymusic@${pkg.version}`,
// environment: import.meta.env.MODE,
environment: import.meta.env.MODE,
// Set tracesSampleRate to 1.0 to capture 100%
// of transactions for performance monitoring.

View File

@ -17,8 +17,10 @@ export default function Home() {
return fetchRecommendedPlaylists({})
},
{
// placeholderData: () =>
// window.ipcRenderer.sendSync('getApiCacheSync', { api: 'personalized' }),
placeholderData: () =>
window.ipcRenderer?.sendSync('getApiCacheSync', {
api: 'personalized',
}),
}
)
@ -27,7 +29,13 @@ export default function Home() {
isLoading: isLoadingDailyRecommendPlaylists,
} = useQuery(
PlaylistApiNames.FETCH_DAILY_RECOMMEND_PLAYLISTS,
fetchDailyRecommendPlaylists
fetchDailyRecommendPlaylists,
{
placeholderData: () =>
window.ipcRenderer?.sendSync('getApiCacheSync', {
api: 'recommend/resource',
}),
}
)
const playlists = [