mirror of
https://github.com/GiriNeko/YesPlayMusic.git
synced 2025-12-17 13:48:02 +00:00
feat: updates
This commit is contained in:
parent
a1b0bcf4d3
commit
884f3df41a
198 changed files with 4572 additions and 5336 deletions
|
|
@ -6,7 +6,7 @@ const headers = {
|
|||
Authority: 'amp-api.music.apple.com',
|
||||
Accept: '*/*',
|
||||
Authorization:
|
||||
'Bearer eyJhbGciOiJFUzI1NiIsInR5cCI6IkpXVCIsImtpZCI6IldlYlBsYXlLaWQifQ.eyJpc3MiOiJBTVBXZWJQbGF5IiwiaWF0IjoxNjQ2NjU1MDgwLCJleHAiOjE2NjIyMDcwODB9.pyOrt2FmP0cHkzYtO8KiEzQL2t1qpRszzxIYbLH7faXSddo6PQei771Ja3aGwGVU4hD99lZAw7bwat60iBcGiQ',
|
||||
'Bearer eyJhbGciOiJFUzI1NiIsInR5cCI6IkpXVCIsImtpZCI6IldlYlBsYXlLaWQifQ.eyJpc3MiOiJBTVBXZWJQbGF5IiwiaWF0IjoxNjYxNDQwNDMyLCJleHAiOjE2NzY5OTI0MzIsInJvb3RfaHR0cHNfb3JpZ2luIjpbImFwcGxlLmNvbSJdfQ.z4BMv9_O4MpMK2iFhYkDqPsx53soPSnlXXK3jm99pHqGOrZADvTgEUw2U7_B1W0MAtFiWBYhYcGvWrzaOig6Bw',
|
||||
Referer: 'https://music.apple.com/',
|
||||
'Sec-Fetch-Dest': 'empty',
|
||||
'Sec-Fetch-Mode': 'cors',
|
||||
|
|
@ -14,6 +14,7 @@ const headers = {
|
|||
'User-Agent':
|
||||
'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Cider/1.5.1 Chrome/100.0.4896.160 Electron/18.3.3 Safari/537.36',
|
||||
'Accept-Encoding': 'gzip',
|
||||
Origin: 'https://music.apple.com',
|
||||
}
|
||||
|
||||
export const getAlbum = async ({
|
||||
|
|
@ -43,6 +44,7 @@ export const getAlbum = async ({
|
|||
|
||||
const albums: AppleMusicAlbum[] | undefined =
|
||||
searchResult?.data?.results?.albums?.data
|
||||
|
||||
const album =
|
||||
albums?.find(
|
||||
a =>
|
||||
|
|
@ -72,12 +74,13 @@ export const getArtist = async (
|
|||
platform: 'web',
|
||||
limit: '1',
|
||||
l: 'en-us', // TODO: get from settings
|
||||
with: 'serverBubbles',
|
||||
},
|
||||
}).catch(e => {
|
||||
log.debug('[appleMusic] Search artist error', e)
|
||||
})
|
||||
|
||||
const artist = searchResult?.data?.results?.artists?.data?.[0]
|
||||
const artist = searchResult?.data?.results?.artist?.data?.[0]
|
||||
if (
|
||||
artist &&
|
||||
artist?.attributes?.name?.toLowerCase() === name.toLowerCase()
|
||||
|
|
|
|||
|
|
@ -5,7 +5,7 @@ import { Request, Response } from 'express'
|
|||
import log from './log'
|
||||
import fs from 'fs'
|
||||
import * as musicMetadata from 'music-metadata'
|
||||
import { APIs, APIsParams, APIsResponse } from '@/shared/CacheAPIs'
|
||||
import { APIs, APIsParams } from '@/shared/CacheAPIs'
|
||||
import { TablesStructures } from './db'
|
||||
|
||||
class Cache {
|
||||
|
|
|
|||
342
packages/desktop/main/cacheWithSQLite.ts
Normal file
342
packages/desktop/main/cacheWithSQLite.ts
Normal file
|
|
@ -0,0 +1,342 @@
|
|||
import { db, Tables } from './db'
|
||||
import type { FetchTracksResponse } from '@/shared/api/Track'
|
||||
import { app } from 'electron'
|
||||
import { Request, Response } from 'express'
|
||||
import log from './log'
|
||||
import fs from 'fs'
|
||||
import * as musicMetadata from 'music-metadata'
|
||||
import { APIs, APIsParams, APIsResponse } from '@/shared/CacheAPIs'
|
||||
import { TablesStructures } from './db'
|
||||
|
||||
class Cache {
|
||||
constructor() {
|
||||
//
|
||||
}
|
||||
|
||||
async set(api: string, data: any, query: any = {}) {
|
||||
switch (api) {
|
||||
case APIs.UserPlaylist:
|
||||
case APIs.UserAccount:
|
||||
case APIs.Personalized:
|
||||
case APIs.RecommendResource:
|
||||
case APIs.UserAlbums:
|
||||
case APIs.UserArtists:
|
||||
case APIs.ListenedRecords:
|
||||
case APIs.Likelist: {
|
||||
if (!data) return
|
||||
db.upsert(Tables.AccountData, {
|
||||
id: api,
|
||||
json: JSON.stringify(data),
|
||||
updatedAt: Date.now(),
|
||||
})
|
||||
break
|
||||
}
|
||||
case APIs.Track: {
|
||||
const res = data as FetchTracksResponse
|
||||
if (!res.songs) return
|
||||
const tracks = res.songs.map(t => ({
|
||||
id: t.id,
|
||||
json: JSON.stringify(t),
|
||||
updatedAt: Date.now(),
|
||||
}))
|
||||
db.upsertMany(Tables.Track, tracks)
|
||||
break
|
||||
}
|
||||
case APIs.Album: {
|
||||
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 APIs.Playlist: {
|
||||
if (!data.playlist) return
|
||||
db.upsert(Tables.Playlist, {
|
||||
id: data.playlist.id,
|
||||
json: JSON.stringify(data),
|
||||
updatedAt: Date.now(),
|
||||
})
|
||||
break
|
||||
}
|
||||
case APIs.Artist: {
|
||||
if (!data.artist) return
|
||||
db.upsert(Tables.Artist, {
|
||||
id: data.artist.id,
|
||||
json: JSON.stringify(data),
|
||||
updatedAt: Date.now(),
|
||||
})
|
||||
break
|
||||
}
|
||||
case APIs.ArtistAlbum: {
|
||||
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 APIs.Lyric: {
|
||||
if (!data.lrc) return
|
||||
db.upsert(Tables.Lyric, {
|
||||
id: query.id,
|
||||
json: JSON.stringify(data),
|
||||
updatedAt: Date.now(),
|
||||
})
|
||||
break
|
||||
}
|
||||
case APIs.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 APIs.AppleMusicAlbum: {
|
||||
if (!data.id) return
|
||||
db.upsert(Tables.AppleMusicAlbum, {
|
||||
id: data.id,
|
||||
json: data.album ? JSON.stringify(data.album) : 'no',
|
||||
updatedAt: Date.now(),
|
||||
})
|
||||
break
|
||||
}
|
||||
case APIs.AppleMusicArtist: {
|
||||
if (!data) return
|
||||
db.upsert(Tables.AppleMusicArtist, {
|
||||
id: data.id,
|
||||
json: data.artist ? JSON.stringify(data.artist) : 'no',
|
||||
updatedAt: Date.now(),
|
||||
})
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
get<T extends keyof APIsParams>(api: T, params: any): any {
|
||||
switch (api) {
|
||||
case APIs.UserPlaylist:
|
||||
case APIs.UserAccount:
|
||||
case APIs.Personalized:
|
||||
case APIs.RecommendResource:
|
||||
case APIs.UserArtists:
|
||||
case APIs.ListenedRecords:
|
||||
case APIs.Likelist: {
|
||||
const data = db.find(Tables.AccountData, api)
|
||||
if (data?.json) return JSON.parse(data.json)
|
||||
break
|
||||
}
|
||||
case APIs.Track: {
|
||||
const ids: number[] = params?.ids
|
||||
.split(',')
|
||||
.map((id: string) => Number(id))
|
||||
if (ids.length === 0) return
|
||||
|
||||
if (ids.includes(NaN)) return
|
||||
|
||||
const tracksRaw = db.findMany(Tables.Track, ids)
|
||||
|
||||
if (tracksRaw.length !== ids.length) {
|
||||
return
|
||||
}
|
||||
const tracks = ids.map(id => {
|
||||
const track = tracksRaw.find(t => t.id === Number(id)) as any
|
||||
return JSON.parse(track.json)
|
||||
})
|
||||
return {
|
||||
code: 200,
|
||||
songs: tracks,
|
||||
privileges: {},
|
||||
}
|
||||
}
|
||||
case APIs.Album: {
|
||||
if (isNaN(Number(params?.id))) return
|
||||
const data = db.find(Tables.Album, params.id)
|
||||
if (data?.json)
|
||||
return {
|
||||
resourceState: true,
|
||||
songs: [],
|
||||
code: 200,
|
||||
album: JSON.parse(data.json),
|
||||
}
|
||||
break
|
||||
}
|
||||
case APIs.Playlist: {
|
||||
if (isNaN(Number(params?.id))) return
|
||||
const data = db.find(Tables.Playlist, params.id)
|
||||
if (data?.json) return JSON.parse(data.json)
|
||||
break
|
||||
}
|
||||
case APIs.Artist: {
|
||||
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 APIs.ArtistAlbum: {
|
||||
if (isNaN(Number(params?.id))) return
|
||||
|
||||
const artistAlbumsRaw = db.find(Tables.ArtistAlbum, params.id)
|
||||
if (!artistAlbumsRaw?.json) return
|
||||
const artistAlbums = JSON.parse(artistAlbumsRaw.json)
|
||||
|
||||
const albumsRaw = db.findMany(Tables.Album, artistAlbums.hotAlbums)
|
||||
if (albumsRaw.length !== artistAlbums.hotAlbums.length) return
|
||||
const albums = albumsRaw.map(a => JSON.parse(a.json))
|
||||
|
||||
artistAlbums.hotAlbums = artistAlbums.hotAlbums.map((id: number) =>
|
||||
albums.find(a => a.id === id)
|
||||
)
|
||||
return artistAlbums
|
||||
}
|
||||
case APIs.Lyric: {
|
||||
if (isNaN(Number(params?.id))) return
|
||||
const data = db.find(Tables.Lyric, params.id)
|
||||
if (data?.json) return JSON.parse(data.json)
|
||||
break
|
||||
}
|
||||
case APIs.CoverColor: {
|
||||
if (isNaN(Number(params?.id))) return
|
||||
return db.find(Tables.CoverColor, params.id)?.color
|
||||
}
|
||||
case APIs.Artists: {
|
||||
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 APIs.AppleMusicAlbum: {
|
||||
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 APIs.AppleMusicArtist: {
|
||||
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
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
getForExpress(api: string, req: Request) {
|
||||
// Get track detail cache
|
||||
if (api === APIs.Track) {
|
||||
const cache = this.get(api, req.query)
|
||||
if (cache) {
|
||||
log.debug(`[cache] Cache hit for ${req.path}`)
|
||||
return cache
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
getAudio(fileName: string, res: Response) {
|
||||
if (!fileName) {
|
||||
return res.status(400).send({ error: 'No filename provided' })
|
||||
}
|
||||
const id = Number(fileName.split('-')[0])
|
||||
|
||||
try {
|
||||
const path = `${app.getPath('userData')}/audio_cache/${fileName}`
|
||||
const audio = fs.readFileSync(path)
|
||||
if (audio.byteLength === 0) {
|
||||
db.delete(Tables.Audio, id)
|
||||
fs.unlinkSync(path)
|
||||
return res.status(404).send({ error: 'Audio not found' })
|
||||
}
|
||||
res
|
||||
.status(206)
|
||||
.setHeader('Accept-Ranges', 'bytes')
|
||||
.setHeader('Connection', 'keep-alive')
|
||||
.setHeader(
|
||||
'Content-Range',
|
||||
`bytes 0-${audio.byteLength - 1}/${audio.byteLength}`
|
||||
)
|
||||
.send(audio)
|
||||
} catch (error) {
|
||||
res.status(500).send({ error })
|
||||
}
|
||||
}
|
||||
|
||||
async setAudio(buffer: Buffer, { id, url }: { id: number; url: string }) {
|
||||
const path = `${app.getPath('userData')}/audio_cache`
|
||||
|
||||
try {
|
||||
fs.statSync(path)
|
||||
} catch (e) {
|
||||
fs.mkdirSync(path)
|
||||
}
|
||||
|
||||
const meta = await musicMetadata.parseBuffer(buffer)
|
||||
const br =
|
||||
meta?.format?.codec === 'OPUS' ? 165000 : meta.format.bitrate ?? 0
|
||||
const type =
|
||||
{
|
||||
'MPEG 1 Layer 3': 'mp3',
|
||||
'Ogg Vorbis': 'ogg',
|
||||
AAC: 'm4a',
|
||||
FLAC: 'flac',
|
||||
OPUS: 'opus',
|
||||
}[meta.format.codec ?? ''] ?? 'unknown'
|
||||
|
||||
let source: TablesStructures[Tables.Audio]['source'] = 'unknown'
|
||||
if (url.includes('googlevideo.com')) source = 'youtube'
|
||||
if (url.includes('126.net')) source = 'netease'
|
||||
if (url.includes('migu.cn')) source = 'migu'
|
||||
if (url.includes('kuwo.cn')) source = 'kuwo'
|
||||
if (url.includes('bilivideo.com')) source = 'bilibili'
|
||||
// TODO: missing kugou qq joox
|
||||
|
||||
fs.writeFile(`${path}/${id}-${br}.${type}`, buffer, error => {
|
||||
if (error) {
|
||||
return log.error(`[cache] cacheAudio failed: ${error}`)
|
||||
}
|
||||
log.info(`Audio file ${id}-${br}.${type} cached!`)
|
||||
|
||||
db.upsert(Tables.Audio, {
|
||||
id,
|
||||
br,
|
||||
type: type as TablesStructures[Tables.Audio]['type'],
|
||||
source,
|
||||
queriedAt: Date.now(),
|
||||
})
|
||||
|
||||
log.info(`[cache] cacheAudio ${id}-${br}.${type}`)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
export default new Cache()
|
||||
344
packages/desktop/main/cacheWithSurreal.ts
Normal file
344
packages/desktop/main/cacheWithSurreal.ts
Normal file
|
|
@ -0,0 +1,344 @@
|
|||
import db from './surrealdb'
|
||||
import type { FetchTracksResponse } from '@/shared/api/Track'
|
||||
import { app } from 'electron'
|
||||
import { Request, Response } from 'express'
|
||||
import log from './log'
|
||||
import fs from 'fs'
|
||||
import * as musicMetadata from 'music-metadata'
|
||||
import { APIs, APIsParams } from '@/shared/CacheAPIs'
|
||||
// import { TablesStructures } from './db'
|
||||
|
||||
class Cache {
|
||||
constructor() {
|
||||
//
|
||||
}
|
||||
|
||||
async set(api: string, data: any, query: any = {}) {
|
||||
switch (api) {
|
||||
case APIs.UserPlaylist:
|
||||
case APIs.UserAccount:
|
||||
case APIs.Personalized:
|
||||
case APIs.RecommendResource:
|
||||
case APIs.UserAlbums:
|
||||
case APIs.UserArtists:
|
||||
case APIs.ListenedRecords:
|
||||
case APIs.Likelist: {
|
||||
if (!data) return
|
||||
db.upsert('netease', 'accountData', api, {
|
||||
json: JSON.stringify(data),
|
||||
})
|
||||
break
|
||||
}
|
||||
case APIs.Track: {
|
||||
const res = data as FetchTracksResponse
|
||||
if (!res.songs) return
|
||||
const tracks = res.songs.map(t => ({
|
||||
key: t.id,
|
||||
data: {
|
||||
json: JSON.stringify(t),
|
||||
},
|
||||
}))
|
||||
db.upsertMany('netease', 'track', tracks)
|
||||
break
|
||||
}
|
||||
case APIs.Album: {
|
||||
if (!data.album) return
|
||||
data.album.songs = data.song
|
||||
db.upsert('netease', 'album', data.album.id, data.album)
|
||||
break
|
||||
}
|
||||
case APIs.Playlist: {
|
||||
if (!data.playlist) return
|
||||
db.upsert('netease', 'playlist', data.playlist.id, {
|
||||
json: JSON.stringify(data),
|
||||
updatedAt: Date.now(),
|
||||
})
|
||||
break
|
||||
}
|
||||
case APIs.Artist: {
|
||||
if (!data.artist) return
|
||||
db.upsert('netease', 'artist', data.artist.id, {
|
||||
json: JSON.stringify(data),
|
||||
updatedAt: Date.now(),
|
||||
})
|
||||
break
|
||||
}
|
||||
case APIs.ArtistAlbum: {
|
||||
if (!data.hotAlbums) return
|
||||
db.upsertMany(
|
||||
'netease',
|
||||
'album',
|
||||
data.hotAlbums.map((a: Album) => ({
|
||||
key: a.id,
|
||||
data: {
|
||||
json: JSON.stringify(a),
|
||||
updatedAt: Date.now(),
|
||||
},
|
||||
}))
|
||||
)
|
||||
const modifiedData = {
|
||||
...data,
|
||||
hotAlbums: data.hotAlbums.map((a: Album) => a.id),
|
||||
}
|
||||
db.upsert('netease', 'artistAlbums', data.artist.id, {
|
||||
json: JSON.stringify(modifiedData),
|
||||
updatedAt: Date.now(),
|
||||
})
|
||||
break
|
||||
}
|
||||
case APIs.Lyric: {
|
||||
if (!data.lrc) return
|
||||
db.upsert('netease', 'lyric', query.id, {
|
||||
json: JSON.stringify(data),
|
||||
updatedAt: Date.now(),
|
||||
})
|
||||
break
|
||||
}
|
||||
// case APIs.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 APIs.AppleMusicAlbum: {
|
||||
if (!data.id) return
|
||||
db.upsert('appleMusic', 'album', data.id, {
|
||||
json: data.album ? JSON.stringify(data.album) : 'no',
|
||||
// updatedAt: Date.now(),
|
||||
})
|
||||
break
|
||||
}
|
||||
case APIs.AppleMusicArtist: {
|
||||
if (!data) return
|
||||
db.upsert('appleMusic', 'artist', data.id, {
|
||||
json: data.artist ? JSON.stringify(data.artist) : 'no',
|
||||
// updatedAt: Date.now(),
|
||||
})
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
async get<T extends keyof APIsParams>(api: T, params: any): Promise<any> {
|
||||
switch (api) {
|
||||
case APIs.UserPlaylist:
|
||||
case APIs.UserAccount:
|
||||
case APIs.Personalized:
|
||||
case APIs.RecommendResource:
|
||||
case APIs.UserArtists:
|
||||
case APIs.ListenedRecords:
|
||||
case APIs.Likelist: {
|
||||
const data = await db.find('netease', 'accountData', api)
|
||||
if (!data?.[0]?.json) return
|
||||
return JSON.parse(data[0].json)
|
||||
}
|
||||
case APIs.Track: {
|
||||
const ids: number[] = params?.ids
|
||||
.split(',')
|
||||
.map((id: string) => Number(id))
|
||||
if (ids.length === 0) return
|
||||
|
||||
if (ids.includes(NaN)) return
|
||||
|
||||
const tracksRaw = await db.findMany('netease', 'track', ids)
|
||||
|
||||
if (!tracksRaw || tracksRaw.length !== ids.length) {
|
||||
return
|
||||
}
|
||||
const tracks = ids.map(id => {
|
||||
const track = tracksRaw.find(t => t.id === Number(id)) as any
|
||||
return JSON.parse(track.json)
|
||||
})
|
||||
return {
|
||||
code: 200,
|
||||
songs: tracks,
|
||||
privileges: {},
|
||||
}
|
||||
}
|
||||
case APIs.Album: {
|
||||
if (isNaN(Number(params?.id))) return
|
||||
const data = await db.find('netease', 'album', params.id)
|
||||
const json = data?.[0]?.json
|
||||
if (!json) return
|
||||
return {
|
||||
resourceState: true,
|
||||
songs: [],
|
||||
code: 200,
|
||||
album: JSON.parse(json),
|
||||
}
|
||||
}
|
||||
case APIs.Playlist: {
|
||||
if (isNaN(Number(params?.id))) return
|
||||
const data = await db.find('netease', 'playlist', params.id)
|
||||
if (!data?.[0]?.json) return
|
||||
return JSON.parse(data[0].json)
|
||||
}
|
||||
case APIs.Artist: {
|
||||
if (isNaN(Number(params?.id))) return
|
||||
const data = await db.find('netease', 'artist', params.id)
|
||||
const fromAppleData = await db.find('appleMusic', 'artist', 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 || undefined
|
||||
}
|
||||
case APIs.ArtistAlbum: {
|
||||
if (isNaN(Number(params?.id))) return
|
||||
|
||||
const artistAlbumsRaw = await db.find(
|
||||
'netease',
|
||||
'artistAlbums',
|
||||
params.id
|
||||
)
|
||||
if (!artistAlbumsRaw?.json) return
|
||||
const artistAlbums = JSON.parse(artistAlbumsRaw.json)
|
||||
|
||||
const albumsRaw = await db.findMany(
|
||||
'netease',
|
||||
'album',
|
||||
artistAlbums.hotAlbums
|
||||
)
|
||||
if (albumsRaw?.length !== artistAlbums.hotAlbums.length) return
|
||||
const albums = albumsRaw.map(a => JSON.parse(a.json))
|
||||
|
||||
artistAlbums.hotAlbums = artistAlbums.hotAlbums.map((id: number) =>
|
||||
albums.find(a => a.id === id)
|
||||
)
|
||||
return artistAlbums
|
||||
}
|
||||
case APIs.Lyric: {
|
||||
if (isNaN(Number(params?.id))) return
|
||||
const data = await db.find('netease', 'lyric', params.id)
|
||||
if (data?.json) return JSON.parse(data.json)
|
||||
return
|
||||
}
|
||||
// case APIs.CoverColor: {
|
||||
// if (isNaN(Number(params?.id))) return
|
||||
// return await db.find(Tables.CoverColor, params.id)?.color
|
||||
// }
|
||||
case APIs.Artists: {
|
||||
if (!params.ids?.length) return
|
||||
const artists = await db.findMany('netease', '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 APIs.AppleMusicAlbum: {
|
||||
if (isNaN(Number(params?.id))) return
|
||||
const data = await db.find('appleMusic', 'album', params.id)
|
||||
if (data?.json && data.json !== 'no') return JSON.parse(data.json)
|
||||
return
|
||||
}
|
||||
case APIs.AppleMusicArtist: {
|
||||
if (isNaN(Number(params?.id))) return
|
||||
const data = await db.find('appleMusic', 'artist', params.id)
|
||||
if (data?.json && data.json !== 'no') return JSON.parse(data.json)
|
||||
return
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
getForExpress(api: string, req: Request) {
|
||||
// Get track detail cache
|
||||
if (api === APIs.Track) {
|
||||
const cache = this.get(api, req.query)
|
||||
if (cache) {
|
||||
log.debug(`[cache] Cache hit for ${req.path}`)
|
||||
return cache
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
getAudio(fileName: string, res: Response) {
|
||||
if (!fileName) {
|
||||
return res.status(400).send({ error: 'No filename provided' })
|
||||
}
|
||||
const id = Number(fileName.split('-')[0])
|
||||
|
||||
try {
|
||||
const path = `${app.getPath('userData')}/audio_cache/${fileName}`
|
||||
const audio = fs.readFileSync(path)
|
||||
// if audio file is empty, delete it
|
||||
if (audio.byteLength === 0) {
|
||||
db.delete('netease', 'audio', id)
|
||||
fs.unlinkSync(path)
|
||||
return res.status(404).send({ error: 'Audio not found' })
|
||||
}
|
||||
res
|
||||
.status(206)
|
||||
.setHeader('Accept-Ranges', 'bytes')
|
||||
.setHeader('Connection', 'keep-alive')
|
||||
.setHeader(
|
||||
'Content-Range',
|
||||
`bytes 0-${audio.byteLength - 1}/${audio.byteLength}`
|
||||
)
|
||||
.send(audio)
|
||||
} catch (error) {
|
||||
res.status(500).send({ error })
|
||||
}
|
||||
}
|
||||
|
||||
async setAudio(buffer: Buffer, { id, url }: { id: number; url: string }) {
|
||||
const path = `${app.getPath('userData')}/audio_cache`
|
||||
|
||||
try {
|
||||
fs.statSync(path)
|
||||
} catch (e) {
|
||||
fs.mkdirSync(path)
|
||||
}
|
||||
|
||||
const meta = await musicMetadata.parseBuffer(buffer)
|
||||
const br =
|
||||
meta?.format?.codec === 'OPUS' ? 165000 : meta.format.bitrate ?? 0
|
||||
const type =
|
||||
{
|
||||
'MPEG 1 Layer 3': 'mp3',
|
||||
'Ogg Vorbis': 'ogg',
|
||||
AAC: 'm4a',
|
||||
FLAC: 'flac',
|
||||
OPUS: 'opus',
|
||||
}[meta.format.codec ?? ''] ?? 'unknown'
|
||||
|
||||
// let source: TablesStructures[Tables.Audio]['source'] = 'unknown'
|
||||
let source = 'unknown'
|
||||
if (url.includes('googlevideo.com')) source = 'youtube'
|
||||
if (url.includes('126.net')) source = 'netease'
|
||||
if (url.includes('migu.cn')) source = 'migu'
|
||||
if (url.includes('kuwo.cn')) source = 'kuwo'
|
||||
if (url.includes('bilivideo.com')) source = 'bilibili'
|
||||
// TODO: missing kugou qq joox
|
||||
|
||||
fs.writeFile(`${path}/${id}-${br}.${type}`, buffer, error => {
|
||||
if (error) {
|
||||
return log.error(`[cache] cacheAudio failed: ${error}`)
|
||||
}
|
||||
log.info(`Audio file ${id}-${br}.${type} cached!`)
|
||||
|
||||
db.upsert('netease', 'audio', id, {
|
||||
id,
|
||||
br,
|
||||
type,
|
||||
source,
|
||||
queriedAt: Date.now(),
|
||||
})
|
||||
|
||||
log.info(`[cache] cacheAudio ${id}-${br}.${type}`)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
export default new Cache()
|
||||
|
|
@ -3,9 +3,10 @@ import { app } from 'electron'
|
|||
import fs from 'fs'
|
||||
import SQLite3 from 'better-sqlite3'
|
||||
import log from './log'
|
||||
import { createFileIfNotExist, dirname } from './utils'
|
||||
import { createFileIfNotExist, dirname, isProd } from './utils'
|
||||
import pkg from '../../../package.json'
|
||||
import { compare, validate } from 'compare-versions'
|
||||
import os from 'os'
|
||||
|
||||
export const enum Tables {
|
||||
Track = 'Track',
|
||||
|
|
@ -83,19 +84,43 @@ class DB {
|
|||
constructor() {
|
||||
log.info('[db] Initializing database...')
|
||||
|
||||
createFileIfNotExist(this.dbFilePath)
|
||||
try {
|
||||
createFileIfNotExist(this.dbFilePath)
|
||||
|
||||
this.sqlite = new SQLite3(this.dbFilePath, {
|
||||
nativeBinding: path.join(
|
||||
__dirname,
|
||||
`./binary/better_sqlite3_${process.arch}.node`
|
||||
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`
|
||||
),
|
||||
})
|
||||
this.sqlite.pragma('auto_vacuum = FULL')
|
||||
this.initTables()
|
||||
this.migrate()
|
||||
|
||||
log.info('[db] Database initialized.')
|
||||
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() {
|
||||
|
|
|
|||
|
|
@ -17,6 +17,7 @@ import { createTaskbar, Thumbar } from './windowsTaskbar'
|
|||
import { createMenu } from './menu'
|
||||
import { isDev, isWindows, isLinux, isMac } from './utils'
|
||||
import store from './store'
|
||||
// import './surrealdb'
|
||||
// import Airplay from './airplay'
|
||||
|
||||
class Main {
|
||||
|
|
@ -91,7 +92,7 @@ class Main {
|
|||
titleBarStyle: isMac ? 'customButtonsOnHover' : 'hidden',
|
||||
trafficLightPosition: { x: 24, y: 24 },
|
||||
frame: false,
|
||||
backgroundColor: '#000',
|
||||
transparent: true,
|
||||
show: false,
|
||||
}
|
||||
if (store.get('window')) {
|
||||
|
|
|
|||
|
|
@ -1,5 +1,5 @@
|
|||
import { BrowserWindow, ipcMain, app } from 'electron'
|
||||
import { db, Tables } from './db'
|
||||
// import { db, Tables } from './db'
|
||||
import { IpcChannels, IpcChannelsParams } from '@/shared/IpcChannels'
|
||||
import cache from './cache'
|
||||
import log from './log'
|
||||
|
|
@ -67,9 +67,9 @@ function initWindowIpcMain(win: BrowserWindow | null) {
|
|||
win?.setSize(1440, 1024, true)
|
||||
})
|
||||
|
||||
on(IpcChannels.IsMaximized, e => {
|
||||
handle(IpcChannels.IsMaximized, () => {
|
||||
if (!win) return
|
||||
e.returnValue = win.isMaximized()
|
||||
return win.isMaximized()
|
||||
})
|
||||
}
|
||||
|
||||
|
|
@ -118,23 +118,36 @@ function initOtherIpcMain() {
|
|||
* 清除API缓存
|
||||
*/
|
||||
on(IpcChannels.ClearAPICache, () => {
|
||||
db.truncate(Tables.Track)
|
||||
db.truncate(Tables.Album)
|
||||
db.truncate(Tables.Artist)
|
||||
db.truncate(Tables.Playlist)
|
||||
db.truncate(Tables.ArtistAlbum)
|
||||
db.truncate(Tables.AccountData)
|
||||
db.truncate(Tables.Audio)
|
||||
db.vacuum()
|
||||
// db.truncate(Tables.Track)
|
||||
// db.truncate(Tables.Album)
|
||||
// db.truncate(Tables.Artist)
|
||||
// db.truncate(Tables.Playlist)
|
||||
// db.truncate(Tables.ArtistAlbum)
|
||||
// db.truncate(Tables.AccountData)
|
||||
// db.truncate(Tables.Audio)
|
||||
// db.vacuum()
|
||||
})
|
||||
|
||||
/**
|
||||
* Get API cache
|
||||
*/
|
||||
on(IpcChannels.GetApiCacheSync, (event, args) => {
|
||||
// on(IpcChannels.GetApiCache, (event, args) => {
|
||||
// const { api, query } = args
|
||||
// const data = cache.get(api, query)
|
||||
// event.returnValue = data
|
||||
// })
|
||||
|
||||
handle(IpcChannels.GetApiCache, async (event, args) => {
|
||||
const { api, query } = args
|
||||
const data = cache.get(api, query)
|
||||
event.returnValue = data
|
||||
if (api !== 'user/account') {
|
||||
return null
|
||||
}
|
||||
try {
|
||||
const data = await cache.get(api, query)
|
||||
return data
|
||||
} catch {
|
||||
return null
|
||||
}
|
||||
})
|
||||
|
||||
/**
|
||||
|
|
@ -193,35 +206,42 @@ function initOtherIpcMain() {
|
|||
event.returnValue = artist === 'no' ? undefined : artist
|
||||
})
|
||||
|
||||
/**
|
||||
* 退出登陆
|
||||
*/
|
||||
handle(IpcChannels.Logout, async () => {
|
||||
// db.truncate(Tables.AccountData)
|
||||
return true
|
||||
})
|
||||
|
||||
/**
|
||||
* 导出tables到json文件,方便查看table大小(dev环境)
|
||||
*/
|
||||
if (process.env.NODE_ENV === 'development') {
|
||||
on(IpcChannels.DevDbExportJson, () => {
|
||||
const tables = [
|
||||
Tables.ArtistAlbum,
|
||||
Tables.Playlist,
|
||||
Tables.Album,
|
||||
Tables.Track,
|
||||
Tables.Artist,
|
||||
Tables.Audio,
|
||||
Tables.AccountData,
|
||||
Tables.Lyric,
|
||||
]
|
||||
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!')
|
||||
}
|
||||
)
|
||||
})
|
||||
})
|
||||
// on(IpcChannels.DevDbExportJson, () => {
|
||||
// const tables = [
|
||||
// Tables.ArtistAlbum,
|
||||
// Tables.Playlist,
|
||||
// Tables.Album,
|
||||
// Tables.Track,
|
||||
// Tables.Artist,
|
||||
// Tables.Audio,
|
||||
// Tables.AccountData,
|
||||
// Tables.Lyric,
|
||||
// ]
|
||||
// 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!')
|
||||
// }
|
||||
// )
|
||||
// })
|
||||
// })
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -12,7 +12,6 @@ if (isProd) {
|
|||
}
|
||||
|
||||
contextBridge.exposeInMainWorld('ipcRenderer', {
|
||||
sendSync: ipcRenderer.sendSync,
|
||||
invoke: ipcRenderer.invoke,
|
||||
send: ipcRenderer.send,
|
||||
on: (
|
||||
|
|
|
|||
|
|
@ -6,20 +6,20 @@ import cache from './cache'
|
|||
import fileUpload from 'express-fileupload'
|
||||
import path from 'path'
|
||||
import fs from 'fs'
|
||||
import { db, Tables } from './db'
|
||||
// import { db, Tables } from './db'
|
||||
import { app } from 'electron'
|
||||
import type { FetchAudioSourceResponse } from '@/shared/api/Track'
|
||||
import UNM from '@unblockneteasemusic/rust-napi'
|
||||
import { APIs as CacheAPIs } from '@/shared/CacheAPIs'
|
||||
import { isProd } from './utils'
|
||||
import { APIs } from '@/shared/CacheAPIs'
|
||||
import history from 'connect-history-api-fallback'
|
||||
import { db, Tables } from './db'
|
||||
|
||||
class Server {
|
||||
port = Number(
|
||||
isProd
|
||||
? process.env.ELECTRON_WEB_SERVER_PORT ?? 42710
|
||||
: process.env.ELECTRON_DEV_NETEASE_API_PORT ?? 3000
|
||||
? process.env.ELECTRON_WEB_SERVER_PORT || 42710
|
||||
: process.env.ELECTRON_DEV_NETEASE_API_PORT || 3000
|
||||
)
|
||||
app = express()
|
||||
// eslint-disable-next-line @typescript-eslint/no-var-requires
|
||||
|
|
@ -152,7 +152,7 @@ class Server {
|
|||
}
|
||||
}
|
||||
|
||||
const unmExecutor = new UNM.Executor()
|
||||
// const unmExecutor = new UNM.Executor()
|
||||
const getFromUNM = async (id: number, req: Request) => {
|
||||
log.debug('[server] Fetching audio url from UNM')
|
||||
let track: Track = cache.get(CacheAPIs.Track, { ids: String(id) })
|
||||
|
|
@ -269,15 +269,15 @@ class Server {
|
|||
return
|
||||
}
|
||||
|
||||
try {
|
||||
const fromUNM = await getFromUNM(id, req)
|
||||
if (fromUNM) {
|
||||
res.status(200).send(fromUNM)
|
||||
return
|
||||
}
|
||||
} catch (error) {
|
||||
log.error(`[server] getFromUNM failed: ${String(error)}`)
|
||||
}
|
||||
// try {
|
||||
// const fromUNM = await getFromUNM(id, req)
|
||||
// if (fromUNM) {
|
||||
// res.status(200).send(fromUNM)
|
||||
// return
|
||||
// }
|
||||
// } catch (error) {
|
||||
// log.error(`[server] getFromUNM failed: ${String(error)}`)
|
||||
// }
|
||||
|
||||
if (fromNetease?.data?.[0].freeTrialInfo) {
|
||||
fromNetease.data[0].url = ''
|
||||
|
|
|
|||
301
packages/desktop/main/surrealdb.ts
Normal file
301
packages/desktop/main/surrealdb.ts
Normal file
|
|
@ -0,0 +1,301 @@
|
|||
import axios, { AxiosInstance } from 'axios'
|
||||
import { app, ipcMain } from 'electron'
|
||||
import path from 'path'
|
||||
import { Get } from 'type-fest'
|
||||
import { $, fs } from 'zx'
|
||||
import log from './log'
|
||||
import { flatten } from 'lodash'
|
||||
|
||||
// surreal start --bind 127.0.0.1:37421 --user user --pass pass --log trace file:///Users/max/Developer/GitHub/replay/tmp/UserData/api_cache/surreal
|
||||
|
||||
interface Databases {
|
||||
appleMusic: {
|
||||
artist: {
|
||||
key: string
|
||||
data: {
|
||||
json: string
|
||||
}
|
||||
}
|
||||
album: {
|
||||
key: string
|
||||
data: {
|
||||
json: string
|
||||
}
|
||||
}
|
||||
}
|
||||
replay: {
|
||||
appData: {
|
||||
id: 'appVersion' | 'skippedVersion'
|
||||
value: string
|
||||
}
|
||||
}
|
||||
netease: {
|
||||
track: {
|
||||
id: number
|
||||
json: string
|
||||
updatedAt: number
|
||||
}
|
||||
artist: {
|
||||
id: number
|
||||
json: string
|
||||
updatedAt: number
|
||||
}
|
||||
album: {
|
||||
id: number
|
||||
json: string
|
||||
updatedAt: number
|
||||
}
|
||||
artistAlbums: {
|
||||
id: number
|
||||
json: string
|
||||
updatedAt: number
|
||||
}
|
||||
lyric: {
|
||||
id: number
|
||||
json: string
|
||||
updatedAt: number
|
||||
}
|
||||
playlist: {
|
||||
id: number
|
||||
json: string
|
||||
updatedAt: number
|
||||
}
|
||||
accountData: {
|
||||
id: string
|
||||
json: string
|
||||
updatedAt: number
|
||||
}
|
||||
audio: {
|
||||
id: number
|
||||
br: number
|
||||
type: 'mp3' | 'flac' | 'ogg' | 'wav' | 'm4a' | 'aac' | 'unknown' | 'opus'
|
||||
source:
|
||||
| 'unknown'
|
||||
| 'netease'
|
||||
| 'migu'
|
||||
| 'kuwo'
|
||||
| 'kugou'
|
||||
| 'youtube'
|
||||
| 'qq'
|
||||
| 'bilibili'
|
||||
| 'joox'
|
||||
queriedAt: number
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
interface SurrealSuccessResult<R> {
|
||||
time: string
|
||||
status: 'OK' | 'ERR'
|
||||
result?: R[]
|
||||
detail?: string
|
||||
}
|
||||
|
||||
interface SurrealErrorResult {
|
||||
code: 400
|
||||
details: string
|
||||
description: string
|
||||
information: string
|
||||
}
|
||||
|
||||
class Surreal {
|
||||
private port = 37421
|
||||
private username = 'user'
|
||||
private password = 'pass'
|
||||
private request: AxiosInstance
|
||||
|
||||
constructor() {
|
||||
this.start()
|
||||
this.request = axios.create({
|
||||
baseURL: `http://127.0.0.1:${this.port}`,
|
||||
timeout: 15000,
|
||||
auth: {
|
||||
username: this.username,
|
||||
password: this.password,
|
||||
},
|
||||
headers: {
|
||||
NS: 'replay',
|
||||
Accept: 'application/json',
|
||||
},
|
||||
responseType: 'json',
|
||||
})
|
||||
}
|
||||
|
||||
getSurrealBinPath() {
|
||||
return path.join(__dirname, `./binary/surreal`)
|
||||
}
|
||||
|
||||
getDatabasePath() {
|
||||
return path.resolve(app.getPath('userData'), './api_cache/surreal')
|
||||
}
|
||||
|
||||
getKey(table: string, key: string) {
|
||||
if (key.includes('/')) {
|
||||
return `${table}:⟨${key}⟩`
|
||||
}
|
||||
return `${table}:${key}`
|
||||
}
|
||||
|
||||
async query<R>(
|
||||
database: keyof Databases,
|
||||
query: string
|
||||
): Promise<R[] | undefined> {
|
||||
type DBResponse =
|
||||
| SurrealSuccessResult<R>
|
||||
| Array<SurrealSuccessResult<R>>
|
||||
| SurrealErrorResult
|
||||
|
||||
const result = await this.request
|
||||
.post<DBResponse | undefined>('/sql', query, {
|
||||
headers: { DB: database },
|
||||
})
|
||||
.catch(e => {
|
||||
log.error(
|
||||
`[surreal] Axios Error: ${e}, response: ${JSON.stringify(
|
||||
e.response.data,
|
||||
null,
|
||||
2
|
||||
)}`
|
||||
)
|
||||
})
|
||||
|
||||
if (!result?.data) {
|
||||
log.error(`[surreal] No result`)
|
||||
return []
|
||||
}
|
||||
const data = result.data
|
||||
|
||||
if (Array.isArray(data)) {
|
||||
return flatten(data.map(item => item?.result).filter(Boolean) as R[][])
|
||||
}
|
||||
|
||||
if ('status' in data) {
|
||||
if (data.status === 'OK') {
|
||||
return data.result
|
||||
}
|
||||
if (data.status === 'ERR') {
|
||||
log.error(`[surreal] ${data.detail}`)
|
||||
throw new Error(`[surreal] query error: ${data.detail}`)
|
||||
}
|
||||
}
|
||||
|
||||
if ('code' in data && data.code !== 400) {
|
||||
throw new Error(`[surreal] query error: ${data.description}`)
|
||||
}
|
||||
|
||||
throw new Error('[surreal] query error: unknown error')
|
||||
}
|
||||
|
||||
async start() {
|
||||
log.info(`[surreal] Starting surreal, listen on 127.0.0.1:${this.port}`)
|
||||
|
||||
await $`${this.getSurrealBinPath()} start --bind 127.0.0.1:${
|
||||
this.port
|
||||
} --user ${this.username} --pass ${
|
||||
this.password
|
||||
} --log warn file://${this.getDatabasePath()}`
|
||||
}
|
||||
|
||||
async create<D extends keyof Databases, T extends keyof Databases[D]>(
|
||||
database: D,
|
||||
table: T,
|
||||
key: Get<Databases[D][T], 'key'>,
|
||||
data: Get<Databases[D][T], 'data'>
|
||||
) {
|
||||
const result = await this.query<Get<Databases[D][T], 'data'>>(
|
||||
database,
|
||||
`CREATE ${String(table)}:(${String(key)}) CONTENT ${JSON.stringify(data)}`
|
||||
)
|
||||
return result?.[0]
|
||||
}
|
||||
|
||||
async upsert<D extends keyof Databases, T extends keyof Databases[D]>(
|
||||
database: D,
|
||||
table: T,
|
||||
key: Get<Databases[D][T], 'key'>,
|
||||
data: Get<Databases[D][T], 'data'>
|
||||
) {
|
||||
fs.writeFile(
|
||||
'tmp.json',
|
||||
`INSERT INTO ${String(table)} ${JSON.stringify({ ...data, id: key })}`
|
||||
)
|
||||
const result = await this.query<Get<Databases[D][T], 'data'>>(
|
||||
database,
|
||||
`INSERT INTO ${String(table)} ${JSON.stringify({ ...data, id: key })} `
|
||||
)
|
||||
return result?.[0]
|
||||
}
|
||||
|
||||
upsertMany<D extends keyof Databases, T extends keyof Databases[D]>(
|
||||
database: D,
|
||||
table: T,
|
||||
data: {
|
||||
key: Get<Databases[D][T], 'key'>
|
||||
data: Get<Databases[D][T], 'data'>
|
||||
}[]
|
||||
) {
|
||||
const queries = data.map(query => {
|
||||
return `INSERT INTO ${String(table)} ${JSON.stringify(query.data)};`
|
||||
})
|
||||
return this.query<Get<Databases[D][T], 'data'>>(database, queries.join(' '))
|
||||
}
|
||||
|
||||
async find<D extends keyof Databases, T extends keyof Databases[D]>(
|
||||
database: D,
|
||||
table: T,
|
||||
key: Get<Databases[D][T], 'key'>
|
||||
) {
|
||||
return this.query<Get<Databases[D][T], 'data'>>(
|
||||
database,
|
||||
`SELECT * FROM ${String(table)} WHERE id = "${this.getKey(
|
||||
String(table),
|
||||
String(key)
|
||||
)}" LIMIT 1`
|
||||
) as Promise<Get<Databases[D][T], 'data'>[]>
|
||||
}
|
||||
|
||||
async findMany<D extends keyof Databases, T extends keyof Databases[D]>(
|
||||
database: D,
|
||||
table: T,
|
||||
keys: Get<Databases[D][T], 'key'>[]
|
||||
) {
|
||||
const idsQuery = keys
|
||||
.map(key => `id = "${this.getKey(String(table), String(key))}"`)
|
||||
.join(' OR ')
|
||||
return this.query<Get<Databases[D][T], 'data'>>(
|
||||
database,
|
||||
`SELECT * FROM ${String(table)} WHERE ${idsQuery} TIMEOUT 5s`
|
||||
)
|
||||
}
|
||||
|
||||
async delete<D extends keyof Databases, T extends keyof Databases[D]>(
|
||||
database: D,
|
||||
table: T,
|
||||
key: Get<Databases[D][T], 'key'>
|
||||
) {
|
||||
try {
|
||||
await this.query(
|
||||
database,
|
||||
`SELECT ${this.getKey(String(table), String(key))}`
|
||||
)
|
||||
return true
|
||||
} catch (error) {
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
async deleteTable<D extends keyof Databases, T extends keyof Databases[D]>(
|
||||
database: D,
|
||||
table: T
|
||||
) {
|
||||
try {
|
||||
await this.query(database, `DELETE ${String(table)}`)
|
||||
return true
|
||||
} catch (error) {
|
||||
return false
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const surreal = new Surreal()
|
||||
export default surreal
|
||||
Loading…
Add table
Add a link
Reference in a new issue