feat: updates

This commit is contained in:
qier222 2023-01-24 16:29:33 +08:00
parent c6c59b2cd9
commit 7ce516877e
No known key found for this signature in database
63 changed files with 6591 additions and 1107 deletions

View file

@ -0,0 +1,31 @@
import fastify from 'fastify'
import fastifyStatic from '@fastify/static'
import path from 'path'
import fastifyCookie from '@fastify/cookie'
import { isProd } from '../env'
import log from '../log'
import netease from './routes/netease'
import appleMusic from './routes/r3play/appleMusic'
const server = fastify({
ignoreTrailingSlash: true,
})
server.register(fastifyCookie)
if (isProd) {
server.register(fastifyStatic, {
root: path.join(__dirname, '../web'),
})
}
server.register(netease, { prefix: '/netease' })
server.register(appleMusic)
const port = Number(
isProd
? process.env.ELECTRON_WEB_SERVER_PORT || 42710
: process.env.ELECTRON_DEV_NETEASE_API_PORT || 30001
)
server.listen({ port })
log.info(`[appServer] http server listening on port ${port}`)

View file

@ -0,0 +1,58 @@
import { APIs } from '@/shared/CacheAPIs'
import { pathCase, snakeCase } from 'change-case'
import { FastifyInstance, FastifyRequest, FastifyReply } from 'fastify'
import NeteaseCloudMusicApi from 'NeteaseCloudMusicApi'
import cache from '../../cache'
async function netease(fastify: FastifyInstance) {
const getHandler = (name: string, neteaseApi: (params: any) => any) => {
return async (
req: FastifyRequest<{ Querystring: { [key: string]: string } }>,
reply: FastifyReply
) => {
// Get track details from cache
if (name === APIs.Track) {
const cacheData = cache.get(name, req.query)
if (cacheData) {
return cache
}
}
// Request netease api
try {
const result = await neteaseApi({
...req.query,
cookie: req.cookies,
})
cache.set(name, result.body, req.query)
return reply.send(result.body)
} catch (error: any) {
if ([400, 301].includes(error.status)) {
return reply.status(error.status).send(error.body)
}
return reply.status(500)
}
}
}
// 循环注册NeteaseCloudMusicApi所有接口
Object.entries(NeteaseCloudMusicApi).forEach(([nameInSnakeCase, neteaseApi]: [string, any]) => {
// 例外
if (
['serveNcmApi', 'getModulesDefinitions', snakeCase(APIs.SongUrl)].includes(nameInSnakeCase)
) {
return
}
const name = pathCase(nameInSnakeCase)
const handler = getHandler(name, neteaseApi)
fastify.get(`/${name}`, handler)
fastify.post(`/${name}`, handler)
})
fastify.get('/', () => 'NeteaseCloudMusicApi')
}
export default netease

View file

@ -0,0 +1,12 @@
import { FastifyInstance } from 'fastify'
import proxy from '@fastify/http-proxy'
async function appleMusic(fastify: FastifyInstance) {
fastify.register(proxy, {
upstream: 'http://168.138.174.244:35530/',
prefix: '/r3play/apple-music',
rewritePrefix: '/apple-music',
})
}
export default appleMusic

View file

@ -1,90 +0,0 @@
import log from './log'
import axios from 'axios'
import { AppleMusicAlbum, AppleMusicArtist } from '@/shared/AppleMusic'
const headers = {
Authority: 'amp-api.music.apple.com',
Accept: '*/*',
Authorization:
'Bearer eyJhbGciOiJFUzI1NiIsInR5cCI6IkpXVCIsImtpZCI6IldlYlBsYXlLaWQifQ.eyJpc3MiOiJBTVBXZWJQbGF5IiwiaWF0IjoxNjYxNDQwNDMyLCJleHAiOjE2NzY5OTI0MzIsInJvb3RfaHR0cHNfb3JpZ2luIjpbImFwcGxlLmNvbSJdfQ.z4BMv9_O4MpMK2iFhYkDqPsx53soPSnlXXK3jm99pHqGOrZADvTgEUw2U7_B1W0MAtFiWBYhYcGvWrzaOig6Bw',
Referer: 'https://music.apple.com/',
'Sec-Fetch-Dest': 'empty',
'Sec-Fetch-Mode': 'cors',
'Sec-Fetch-Site': 'cross-site',
'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 ({
name,
artist,
}: {
name: string
artist: string
}): Promise<AppleMusicAlbum | undefined> => {
const keyword = `${artist} ${name}`
log.debug(`[appleMusic] getAlbum: ${keyword}`)
const searchResult = await axios({
method: 'GET',
headers,
url: 'https://amp-api.music.apple.com/v1/catalog/us/search',
params: {
term: keyword,
types: 'albums',
'fields[albums]': 'artistName,name,editorialVideo,editorialNotes',
platform: 'web',
limit: '5',
l: 'en-us', // TODO: get from settings
},
}).catch(e => {
log.debug('[appleMusic] Search album error', e)
})
const albums: AppleMusicAlbum[] | undefined =
searchResult?.data?.results?.albums?.data
const album =
albums?.find(
a =>
a.attributes.name.toLowerCase() === name.toLowerCase() &&
a.attributes.artistName.toLowerCase() === artist.toLowerCase()
) || albums?.[0]
if (!album) {
log.debug('[appleMusic] No album found on apple music')
return
}
return album
}
export const getArtist = async (
name: string
): Promise<AppleMusicArtist | undefined> => {
const searchResult = await axios({
method: 'GET',
url: 'https://amp-api.music.apple.com/v1/catalog/us/search',
headers,
params: {
term: name,
types: 'artists',
'fields[artists]': 'url,name,artwork,editorialVideo,artistBio',
'omit[resource:artists]': 'relationships',
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?.artist?.data?.[0]
if (
artist &&
artist?.attributes?.name?.toLowerCase() === name.toLowerCase()
) {
return artist
}
}

View file

@ -147,9 +147,7 @@ class Cache {
break
}
case APIs.Track: {
const ids: number[] = params?.ids
.split(',')
.map((id: string) => Number(id))
const ids: number[] = params?.ids.split(',').map((id: string) => Number(id))
if (ids.length === 0) return
if (ids.includes(NaN)) return
@ -252,17 +250,6 @@ class Cache {
}
}
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' })
@ -281,10 +268,7 @@ class Cache {
.status(206)
.setHeader('Accept-Ranges', 'bytes')
.setHeader('Connection', 'keep-alive')
.setHeader(
'Content-Range',
`bytes 0-${audio.byteLength - 1}/${audio.byteLength}`
)
.setHeader('Content-Range', `bytes 0-${audio.byteLength - 1}/${audio.byteLength}`)
.send(audio)
} catch (error) {
res.status(500).send({ error })
@ -301,8 +285,7 @@ class Cache {
}
const meta = await musicMetadata.parseBuffer(buffer)
const br =
meta?.format?.codec === 'OPUS' ? 165000 : meta.format.bitrate ?? 0
const br = meta?.format?.codec === 'OPUS' ? 165000 : meta.format.bitrate ?? 0
const type =
{
'MPEG 1 Layer 3': 'mp3',

View file

@ -77,10 +77,7 @@ const readSqlFile = (filename: string) => {
class DB {
sqlite: SQLite3.Database
dbFilePath: string = path.resolve(
app.getPath('userData'),
'./api_cache/db.sqlite'
)
dbFilePath: string = path.resolve(app.getPath('userData'), './api_cache/db.sqlite')
constructor() {
log.info('[db] Initializing database...')
@ -109,14 +106,8 @@ class DB {
`../../bin/better_sqlite3_${os.platform}_${os.arch}.node`
)
const prodBinPaths = {
darwin: path.resolve(
app.getPath('exe'),
`../../Resources/bin/better_sqlite3.node`
),
win32: path.resolve(
app.getPath('exe'),
`../resources/bin/better_sqlite3.node`
),
darwin: path.resolve(app.getPath('exe'), `../../Resources/bin/better_sqlite3.node`),
win32: path.resolve(app.getPath('exe'), `../resources/bin/better_sqlite3.node`),
linux: '',
}
return isProd
@ -128,6 +119,7 @@ class DB {
log.info('[db] Initializing database tables...')
const init = readSqlFile('init.sql')
this.sqlite.exec(init)
this.sqlite.pragma('journal_mode=WAL')
log.info('[db] Database tables initialized.')
}
@ -167,9 +159,7 @@ class DB {
table: T,
key: TablesStructures[T]['id']
): TablesStructures[T] | undefined {
return this.sqlite
.prepare(`SELECT * FROM ${table} WHERE id = ? LIMIT 1`)
.get(key)
return this.sqlite.prepare(`SELECT * FROM ${table} WHERE id = ? LIMIT 1`).get(key)
}
findMany<T extends TableNames>(
@ -184,11 +174,7 @@ class DB {
return this.sqlite.prepare(`SELECT * FROM ${table}`).all()
}
create<T extends TableNames>(
table: T,
data: TablesStructures[T],
skipWhenExist: boolean = true
) {
create<T extends TableNames>(table: T, data: TablesStructures[T], skipWhenExist: boolean = true) {
if (skipWhenExist && db.find(table, data.id)) return
return this.sqlite.prepare(`INSERT INTO ${table} VALUES (?)`).run(data)
}
@ -202,9 +188,7 @@ class DB {
.map(key => `:${key}`)
.join(', ')
const insert = this.sqlite.prepare(
`INSERT ${
skipWhenExist ? 'OR IGNORE' : ''
} INTO ${table} VALUES (${valuesQuery})`
`INSERT ${skipWhenExist ? 'OR IGNORE' : ''} INTO ${table} VALUES (${valuesQuery})`
)
const insertMany = this.sqlite.transaction((rows: any[]) => {
rows.forEach((row: any) => insert.run(row))
@ -216,18 +200,14 @@ class DB {
const valuesQuery = Object.keys(data)
.map(key => `:${key}`)
.join(', ')
return this.sqlite
.prepare(`INSERT OR REPLACE INTO ${table} VALUES (${valuesQuery})`)
.run(data)
return this.sqlite.prepare(`INSERT OR REPLACE INTO ${table} VALUES (${valuesQuery})`).run(data)
}
upsertMany<T extends TableNames>(table: T, data: TablesStructures[T][]) {
const valuesQuery = Object.keys(data[0])
.map(key => `:${key}`)
.join(', ')
const upsert = this.sqlite.prepare(
`INSERT OR REPLACE INTO ${table} VALUES (${valuesQuery})`
)
const upsert = this.sqlite.prepare(`INSERT OR REPLACE INTO ${table} VALUES (${valuesQuery})`)
const upsertMany = this.sqlite.transaction((rows: any[]) => {
rows.forEach((row: any) => upsert.run(row))
})
@ -238,10 +218,7 @@ class DB {
return this.sqlite.prepare(`DELETE FROM ${table} WHERE id = ?`).run(key)
}
deleteMany<T extends TableNames>(
table: T,
keys: TablesStructures[T]['id'][]
) {
deleteMany<T extends TableNames>(table: T, keys: TablesStructures[T]['id'][]) {
const idsQuery = keys.map(key => `id = ${key}`).join(' OR ')
return this.sqlite.prepare(`DELETE FROM ${table} WHERE ${idsQuery}`).run()
}

View file

@ -3,4 +3,4 @@ export const isProd = process.env.NODE_ENV === 'production'
export const isWindows = process.platform === 'win32'
export const isMac = process.platform === 'darwin'
export const isLinux = process.platform === 'linux'
export const appName = 'R3Play'
export const appName = 'R3PLAY'

View file

@ -1,6 +1,6 @@
import './preload' // must be first
import './sentry'
import './server'
// import './server'
import { BrowserWindow, BrowserWindowConstructorOptions, app, shell } from 'electron'
import { release } from 'os'
import { join } from 'path'
@ -12,8 +12,7 @@ import { createTaskbar, Thumbar } from './windowsTaskbar'
import { createMenu } from './menu'
import { isDev, isWindows, isLinux, isMac, appName } from './env'
import store from './store'
// import './surrealdb'
// import Airplay from './airplay'
import './appServer/appServer'
class Main {
win: BrowserWindow | null = null
@ -83,10 +82,12 @@ class Main {
width: store.get('window.width'),
height: store.get('window.height'),
minWidth: 1240,
minHeight: 848,
minHeight: 800,
titleBarStyle: isMac ? 'customButtonsOnHover' : 'hidden',
trafficLightPosition: { x: 24, y: 24 },
frame: false,
fullscreenable: true,
resizable: true,
transparent: true,
backgroundColor: 'rgba(0, 0, 0, 0)',
show: false,

View file

@ -12,7 +12,6 @@ import { Thumbar } from './windowsTaskbar'
import fastFolderSize from 'fast-folder-size'
import path from 'path'
import prettyBytes from 'pretty-bytes'
import { getArtist, getAlbum } from './appleMusic'
const on = <T extends keyof IpcChannelsParams>(
channel: T,
@ -23,10 +22,7 @@ const on = <T extends keyof IpcChannelsParams>(
const handle = <T extends keyof IpcChannelsParams>(
channel: T,
listener: (
event: Electron.IpcMainInvokeEvent,
params: IpcChannelsParams[T]
) => void
listener: (event: Electron.IpcMainInvokeEvent, params: IpcChannelsParams[T]) => void
) => {
return ipcMain.handle(channel, listener)
}
@ -162,49 +158,46 @@ function initOtherIpcMain() {
*
*/
on(IpcChannels.GetAudioCacheSize, event => {
fastFolderSize(
path.join(app.getPath('userData'), './audio_cache'),
(error, bytes) => {
if (error) throw error
fastFolderSize(path.join(app.getPath('userData'), './audio_cache'), (error, bytes) => {
if (error) throw error
event.returnValue = prettyBytes(bytes ?? 0)
}
)
event.returnValue = prettyBytes(bytes ?? 0)
})
})
/**
* Apple Music获取专辑信息
*/
handle(
IpcChannels.GetAlbumFromAppleMusic,
async (event, { id, name, artist }) => {
const fromCache = cache.get(APIs.AppleMusicAlbum, { id })
if (fromCache) {
return fromCache === 'no' ? undefined : fromCache
}
// handle(
// IpcChannels.GetAlbumFromAppleMusic,
// async (event, { id, name, artist }) => {
// const fromCache = cache.get(APIs.AppleMusicAlbum, { id })
// if (fromCache) {
// return fromCache === 'no' ? undefined : fromCache
// }
const fromApple = await getAlbum({ name, artist })
cache.set(APIs.AppleMusicAlbum, { id, album: fromApple })
return fromApple
}
)
// const fromApple = await getAlbum({ name, artist })
// cache.set(APIs.AppleMusicAlbum, { id, album: fromApple })
// return fromApple
// }
// )
/**
* Apple Music获取歌手信息
**/
handle(IpcChannels.GetArtistFromAppleMusic, async (event, { id, name }) => {
const fromApple = await getArtist(name)
cache.set(APIs.AppleMusicArtist, { id, artist: fromApple })
return fromApple
})
// /**
// * 从Apple Music获取歌手信息
// **/
// handle(IpcChannels.GetArtistFromAppleMusic, async (event, { id, name }) => {
// const fromApple = await getArtist(name)
// cache.set(APIs.AppleMusicArtist, { id, artist: fromApple })
// return fromApple
// })
/**
* Apple Music歌手信息
*/
on(IpcChannels.GetArtistFromAppleMusic, (event, { id }) => {
const artist = cache.get(APIs.AppleMusicArtist, id)
event.returnValue = artist === 'no' ? undefined : artist
})
// /**
// * 从缓存读取Apple Music歌手信息
// */
// on(IpcChannels.GetArtistFromAppleMusic, (event, { id }) => {
// const artist = cache.get(APIs.AppleMusicArtist, id)
// event.returnValue = artist === 'no' ? undefined : artist
// })
/**
* 退

View file

@ -14,6 +14,7 @@ import { appName, isProd } from './env'
import { APIs } from '@/shared/CacheAPIs'
import history from 'connect-history-api-fallback'
import { db, Tables } from './db'
import axios from 'axios'
class Server {
port = Number(
@ -30,12 +31,32 @@ class Server {
this.app.use(cookieParser())
this.app.use(fileUpload())
this.getAudioUrlHandler()
this.r3playApiHandler()
this.neteaseHandler()
this.cacheAudioHandler()
this.serveStaticForProduction()
this.listen()
}
r3playApiHandler() {
this.app.get(`/r3play/apple-music/:endpoint`, async (req: Request, res: Response) => {
log.debug(`[server] Handling request: ${req.path}`)
const { endpoint } = req.params
const { query } = req
axios
.get(`https://r3play.app/r3play/apple-music/${endpoint}`, {
params: query,
})
.then(response => {
console.log(response.data)
res.send(response.data)
})
.catch(error => {
console.log(error)
})
})
}
neteaseHandler() {
Object.entries(this.netease).forEach(([name, handler]: [string, any]) => {
// 例外处理