feat: updates

This commit is contained in:
qier222 2022-10-28 20:29:04 +08:00
parent a1b0bcf4d3
commit 884f3df41a
No known key found for this signature in database
198 changed files with 4572 additions and 5336 deletions

View file

@ -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()

View file

@ -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 {

View 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()

View 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()

View file

@ -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() {

View file

@ -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')) {

View file

@ -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!')
// }
// )
// })
// })
}
}

View file

@ -12,7 +12,6 @@ if (isProd) {
}
contextBridge.exposeInMainWorld('ipcRenderer', {
sendSync: ipcRenderer.sendSync,
invoke: ipcRenderer.invoke,
send: ipcRenderer.send,
on: (

View file

@ -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 = ''

View 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