YesPlayMusic/packages/electron/main/server.ts

331 lines
8.9 KiB
TypeScript
Raw Normal View History

2022-03-13 14:40:38 +08:00
import { pathCase } from 'change-case'
import cookieParser from 'cookie-parser'
import express, { Request, Response } from 'express'
2022-04-16 13:30:25 +08:00
import log from './log'
2022-04-12 01:48:14 +08:00
import cache from './cache'
2022-03-19 17:03:29 +08:00
import fileUpload from 'express-fileupload'
2022-04-02 00:45:20 +08:00
import path from 'path'
2022-05-01 19:53:25 +08:00
import fs from 'fs'
2022-05-13 19:30:13 +08:00
import { db, Tables } from './db'
2022-05-01 19:53:25 +08:00
import { app } from 'electron'
import type { FetchAudioSourceResponse } from '@/shared/api/Track'
import UNM from '@unblockneteasemusic/rust-napi'
2022-05-13 19:30:13 +08:00
import { APIs as CacheAPIs } from '@/shared/CacheAPIs'
import { isProd } from './utils'
2022-03-13 14:40:38 +08:00
2022-04-16 21:14:03 +08:00
class Server {
port = Number(
isProd
? process.env.ELECTRON_WEB_SERVER_PORT ?? 42710
: process.env.ELECTRON_DEV_NETEASE_API_PORT ?? 3000
)
app = express()
constructor() {
log.info('[server] starting http server')
this.app.use(cookieParser())
this.app.use(fileUpload())
2022-05-01 19:53:25 +08:00
this.getAudioUrlHandler()
2022-04-16 21:14:03 +08:00
this.neteaseHandler()
this.cacheAudioHandler()
this.serveStaticForProd()
this.listen()
2022-03-13 14:40:38 +08:00
}
2022-04-16 21:14:03 +08:00
neteaseHandler() {
// eslint-disable-next-line @typescript-eslint/no-var-requires
const neteaseApi = require('NeteaseCloudMusicApi') as (params: any) => any[]
Object.entries(neteaseApi).forEach(([name, handler]) => {
2022-05-01 19:53:25 +08:00
if (['serveNcmApi', 'getModulesDefinitions', 'song_url'].includes(name)) {
return
}
2022-04-16 21:14:03 +08:00
name = pathCase(name)
const wrappedHandler = async (req: Request, res: Response) => {
log.debug(`[server] Handling request: ${req.path}`)
// Get from cache
const cacheData = await cache.getForExpress(name, req)
if (cacheData) return res.json(cacheData)
// Request netease api
try {
const result = await handler({
...req.query,
cookie: req.cookies,
})
cache.set(name, result.body, req.query)
return res.send(result.body)
} catch (error: any) {
if ([400, 301].includes(error.status)) {
return res.status(error.status).send(error.body)
}
2022-04-29 19:16:34 +08:00
return res.status(500)
2022-04-16 21:14:03 +08:00
}
}
this.app.get(`/netease/${name}`, wrappedHandler)
this.app.post(`/netease/${name}`, wrappedHandler)
})
2022-03-19 17:03:29 +08:00
}
2022-04-16 21:14:03 +08:00
serveStaticForProd() {
if (isProd) {
2022-05-12 02:45:43 +08:00
this.app.use('/', express.static(path.join(__dirname, '../web/')))
2022-04-16 21:14:03 +08:00
}
2022-03-19 17:03:29 +08:00
}
2022-04-16 21:14:03 +08:00
2022-05-01 19:53:25 +08:00
getAudioUrlHandler() {
const getFromCache = (id: number) => {
// get from cache
const cache = db.find(Tables.Audio, id)
if (!cache) return
const audioFileName = `${cache.id}-${cache.br}.${cache.type}`
const isAudioFileExists = fs.existsSync(
`${app.getPath('userData')}/audio_cache/${audioFileName}`
)
if (!isAudioFileExists) return
log.debug(`[server] Audio cache hit for song/url`)
return {
data: [
{
source: cache.source,
id: cache.id,
url: `http://127.0.0.1:42710/yesplaymusic/audio/${audioFileName}`,
br: cache.br,
size: 0,
md5: '',
code: 200,
expi: 0,
type: cache.type,
gain: 0,
fee: 8,
uf: null,
payed: 0,
flag: 4,
canExtend: false,
freeTrialInfo: null,
level: 'standard',
encodeType: cache.type,
freeTrialPrivilege: {
resConsumable: false,
userConsumable: false,
listenType: null,
},
freeTimeTrialPrivilege: {
resConsumable: false,
userConsumable: false,
type: 0,
remainTime: 0,
},
urlSource: 0,
},
],
code: 200,
}
}
const getFromNetease = async (
req: Request
): Promise<FetchAudioSourceResponse | undefined> => {
try {
// eslint-disable-next-line @typescript-eslint/no-var-requires
const getSongUrl = (require('NeteaseCloudMusicApi') as any).song_url
const result = await getSongUrl({ ...req.query, cookie: req.cookies })
return result.body
} catch (error: any) {
return
}
}
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) })
?.songs?.[0]
if (!track) {
// eslint-disable-next-line @typescript-eslint/no-var-requires
const getSongDetail = (require('NeteaseCloudMusicApi') as any)
.song_detail
track = await getSongDetail({ ...req.query, cookie: req.cookies })
}
if (!track) return
const trackForUNM = {
id: String(track.id),
name: track.name,
duration: track.dt,
album: {
id: String(track.al.id),
name: track.al.name,
},
artists: [
...track.ar.map((a: Artist) => ({
id: String(a.id),
name: a.name,
})),
],
}
const sourceList = ['ytdl']
const context = {}
const matchedAudio = await unmExecutor.search(
sourceList,
trackForUNM,
context
)
const retrievedSong = await unmExecutor.retrieve(matchedAudio, context)
const source =
retrievedSong.source === 'ytdl' ? 'youtube' : retrievedSong.source
if (retrievedSong.url) {
return {
data: [
{
source,
id,
url: retrievedSong.url,
br: 128000,
size: 0,
md5: '',
code: 200,
expi: 0,
type: 'unknown',
gain: 0,
fee: 8,
uf: null,
payed: 0,
flag: 4,
canExtend: false,
freeTrialInfo: null,
level: 'standard',
encodeType: 'unknown',
freeTrialPrivilege: {
resConsumable: false,
userConsumable: false,
listenType: null,
},
freeTimeTrialPrivilege: {
resConsumable: false,
userConsumable: false,
type: 0,
remainTime: 0,
},
urlSource: 0,
unm: {
source,
song: matchedAudio.song,
},
},
],
code: 200,
}
}
}
const handler = async (req: Request, res: Response) => {
const id = Number(req.query.id) || 0
if (id === 0) {
return res.status(400).send({
code: 400,
msg: 'id is required or id is invalid',
})
}
2022-05-13 19:30:13 +08:00
try {
const fromCache = await getFromCache(id)
if (fromCache) {
res.status(200).send(fromCache)
return
}
} catch (error) {
log.error(`[server] getFromCache failed: ${String(error)}`)
}
2022-05-01 19:53:25 +08:00
2022-05-13 19:30:13 +08:00
const fromNetease = await getFromNetease(req)
if (fromNetease?.code === 200 && !fromNetease?.data?.[0].freeTrialInfo) {
res.status(200).send(fromNetease)
return
}
2022-05-01 19:53:25 +08:00
try {
const fromUNM = await getFromUNM(id, req)
if (fromUNM) {
res.status(200).send(fromUNM)
return
}
} catch (error) {
log.error(`[server] getFromNetease failed: ${String(error)}`)
}
2022-05-13 19:30:13 +08:00
if (fromNetease?.data?.[0].freeTrialInfo) {
fromNetease.data[0].url = ''
}
2022-05-01 19:53:25 +08:00
2022-05-13 19:30:13 +08:00
res.status(fromNetease?.code ?? 500).send(fromNetease)
2022-05-01 19:53:25 +08:00
}
this.app.get('/netease/song/url', handler)
}
2022-04-16 21:14:03 +08:00
cacheAudioHandler() {
this.app.get(
'/yesplaymusic/audio/:filename',
async (req: Request, res: Response) => {
cache.getAudio(req.params.filename, res)
}
)
this.app.post(
'/yesplaymusic/audio/:id',
async (req: Request, res: Response) => {
const id = Number(req.params.id)
const { url } = req.query
if (isNaN(id)) {
return res.status(400).send({ error: 'Invalid param id' })
}
if (!url) {
return res.status(400).send({ error: 'Invalid query url' })
}
if (
!req.files ||
Object.keys(req.files).length === 0 ||
!req.files.file
) {
return res.status(400).send('No audio were uploaded.')
}
if ('length' in req.files.file) {
return res.status(400).send('Only can upload one audio at a time.')
}
try {
await cache.setAudio(req.files.file.data, {
2022-05-01 19:53:25 +08:00
id,
url: String(req.query.url) || '',
2022-04-16 21:14:03 +08:00
})
res.status(200).send('Audio cached!')
} catch (error) {
res.status(500).send({ error })
}
}
)
2022-03-19 17:03:29 +08:00
}
2022-04-16 21:14:03 +08:00
listen() {
this.app.listen(this.port, () => {
log.info(`[server] API server listening on port ${this.port}`)
2022-03-30 00:53:05 +08:00
})
2022-03-19 17:03:29 +08:00
}
2022-04-02 00:45:20 +08:00
}
2022-04-16 21:14:03 +08:00
export default new Server()