mirror of
https://github.com/qier222/YesPlayMusic.git
synced 2025-03-01 14:51:11 +08:00
302 lines
6.8 KiB
TypeScript
302 lines
6.8 KiB
TypeScript
![]() |
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
|