mirror of
https://github.com/GiriNeko/YesPlayMusic.git
synced 2025-12-15 20:58:01 +00:00
feat: updates
This commit is contained in:
parent
7ce516877e
commit
ccebe0a67a
74 changed files with 56065 additions and 2810 deletions
|
|
@ -5,12 +5,13 @@
|
|||
|
||||
const pkg = require('./package.json')
|
||||
const electronVersion = pkg.devDependencies.electron.replaceAll('^', '')
|
||||
const isDev = process.env.NODE_ENV === 'development'
|
||||
|
||||
module.exports = {
|
||||
appId: 'app.r3play',
|
||||
productName: pkg.productName,
|
||||
copyright: 'Copyright © 2022 qier222',
|
||||
asar: true,
|
||||
asar: isDev ? true : false,
|
||||
directories: {
|
||||
output: 'release',
|
||||
buildResources: 'build',
|
||||
|
|
@ -119,6 +120,17 @@ module.exports = {
|
|||
'!**/{npm-debug.log,yarn.lock,.yarn-integrity,.yarn-metadata.json,pnpm-lock.yaml}',
|
||||
'!**/*.{map,debug.min.js}',
|
||||
|
||||
// copy prisma
|
||||
{
|
||||
from: './prisma',
|
||||
to: 'main/prisma',
|
||||
},
|
||||
{
|
||||
from: './prisma',
|
||||
to: 'main',
|
||||
filter: '*.prisma' // only copy prisma schema
|
||||
},
|
||||
|
||||
{
|
||||
from: './dist',
|
||||
to: './main',
|
||||
|
|
|
|||
|
|
@ -1,283 +0,0 @@
|
|||
import { $ } from 'zx'
|
||||
import store from './store'
|
||||
import { ipcMain, BrowserWindow } from 'electron'
|
||||
import { getNetworkInfo, sleep } from './utils'
|
||||
import { spawn, ChildProcessWithoutNullStreams } from 'child_process'
|
||||
import log from './log'
|
||||
|
||||
type Protocol = 'dmap' | 'mrp' | 'airplay' | 'companion' | 'raop'
|
||||
interface Device {
|
||||
name: string
|
||||
address: string
|
||||
identifier: string
|
||||
services: { protocol: Protocol; port: number }[]
|
||||
}
|
||||
|
||||
class Airplay {
|
||||
devices: Device[] = []
|
||||
pairProcess: ChildProcessWithoutNullStreams | null = null
|
||||
window: BrowserWindow
|
||||
|
||||
constructor(window: BrowserWindow) {
|
||||
log.debug('[airplay] ini')
|
||||
this.window = window
|
||||
this.initIpc()
|
||||
}
|
||||
|
||||
async checkIsInstalled() {
|
||||
const help = (await $`atvscript -h`).toString()
|
||||
return String(help).includes('usage: atvscript')
|
||||
}
|
||||
|
||||
async scanDevices(excludeThisDevice: boolean = true) {
|
||||
if (!this.checkIsInstalled()) {
|
||||
return {
|
||||
result: 'failure',
|
||||
error: 'pyatv is not installed',
|
||||
}
|
||||
}
|
||||
|
||||
let scanResult = null
|
||||
try {
|
||||
scanResult = await $`atvscript scan`
|
||||
} catch (p: any) {
|
||||
return {
|
||||
result: 'failure',
|
||||
error: p.stderr,
|
||||
}
|
||||
}
|
||||
|
||||
let json = null
|
||||
try {
|
||||
json = JSON.parse(scanResult.toString())
|
||||
} catch (e) {
|
||||
return {
|
||||
result: 'failure',
|
||||
error: String(e),
|
||||
}
|
||||
}
|
||||
|
||||
if (excludeThisDevice) {
|
||||
const macAddress = getNetworkInfo()?.mac
|
||||
if (macAddress) {
|
||||
json.devices = json.devices.filter(
|
||||
(device: Device) =>
|
||||
device.identifier.toLowerCase() !== macAddress.toLowerCase()
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
if (json.result === 'success') {
|
||||
this.devices = json.devices
|
||||
}
|
||||
|
||||
return json
|
||||
}
|
||||
|
||||
async pairDevice(deviceID: string, protocol: Protocol) {
|
||||
this.pairProcess = spawn('atvremote', [
|
||||
'--id',
|
||||
deviceID,
|
||||
'--protocol',
|
||||
protocol,
|
||||
'pair',
|
||||
])
|
||||
|
||||
let paired = false
|
||||
let done = false
|
||||
this.pairProcess.stdout.on('data', (data: any) => {
|
||||
console.log('stdout', String(data))
|
||||
if (data.includes('You may now use these credentials:')) {
|
||||
store.set(
|
||||
`airplay.credentials.${deviceID}`,
|
||||
String(data).split('credentials:')[1].trim()
|
||||
)
|
||||
paired = true
|
||||
done = true
|
||||
}
|
||||
if (data.includes('Pairing failed')) {
|
||||
paired = false
|
||||
done = true
|
||||
}
|
||||
})
|
||||
|
||||
while (!done) {
|
||||
console.log('not done yet')
|
||||
await sleep(1000)
|
||||
}
|
||||
|
||||
return paired
|
||||
}
|
||||
|
||||
async enterPairPin(pin: string) {
|
||||
if (!this.pairProcess) {
|
||||
return false
|
||||
}
|
||||
this.pairProcess.stdin.write(`${pin}\n`)
|
||||
return true
|
||||
}
|
||||
|
||||
async playUrl(deviceID: string, url: string) {
|
||||
log.debug(`[airplay] playUrl ${url}`)
|
||||
const credentials: string = store.get(`airplay.credentials.${deviceID}`)
|
||||
if (url.includes('127.0.0.1')) {
|
||||
const ip = getNetworkInfo()?.address
|
||||
if (ip) url = url.replace('127.0.0.1', ip)
|
||||
}
|
||||
|
||||
try {
|
||||
spawn('atvremote', [
|
||||
'--id',
|
||||
deviceID,
|
||||
'--airplay-credentials',
|
||||
credentials,
|
||||
`play_url=${url}`,
|
||||
])
|
||||
} catch (p: any) {
|
||||
return {
|
||||
result: 'failure',
|
||||
error: p.stderr,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
async getPlaying(deviceID: string) {
|
||||
if (!this.checkIsInstalled()) {
|
||||
return {
|
||||
result: 'failure',
|
||||
error: 'pyatv is not installed',
|
||||
}
|
||||
}
|
||||
|
||||
const credentials = store.get(`airplay.credentials.${deviceID}`)
|
||||
|
||||
let playing = null
|
||||
try {
|
||||
playing =
|
||||
await $`atvscript --id ${deviceID} --airplay-credentials=${credentials} playing`
|
||||
} catch (p: any) {
|
||||
return {
|
||||
result: 'failure',
|
||||
error: p.stderr,
|
||||
}
|
||||
}
|
||||
|
||||
let json = null
|
||||
try {
|
||||
json = JSON.parse(playing.toString())
|
||||
} catch (e) {
|
||||
return {
|
||||
result: 'failure',
|
||||
error: String(e),
|
||||
}
|
||||
}
|
||||
|
||||
return json
|
||||
}
|
||||
|
||||
async play(deviceID: string) {
|
||||
const credentials = store.get(`airplay.credentials.${deviceID}`)
|
||||
try {
|
||||
$`atvscript --id ${deviceID} --airplay-credentials ${credentials} play`
|
||||
} catch (p: any) {
|
||||
return {
|
||||
result: 'failure',
|
||||
error: p.stderr,
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
result: 'success',
|
||||
}
|
||||
}
|
||||
|
||||
async pause(deviceID: string) {
|
||||
const credentials = store.get(`airplay.credentials.${deviceID}`)
|
||||
try {
|
||||
$`atvscript --id ${deviceID} --airplay-credentials ${credentials} pause`
|
||||
} catch (p: any) {
|
||||
return {
|
||||
result: 'failure',
|
||||
error: p.stderr,
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
result: 'success',
|
||||
}
|
||||
}
|
||||
|
||||
async playOrPause(deviceID: string) {
|
||||
const credentials = store.get(`airplay.credentials.${deviceID}`)
|
||||
try {
|
||||
$`atvscript --id ${deviceID} --airplay-credentials ${credentials} play_pause`
|
||||
} catch (p: any) {
|
||||
return {
|
||||
result: 'failure',
|
||||
error: p.stderr,
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
result: 'success',
|
||||
}
|
||||
}
|
||||
|
||||
async setProgress(deviceID: string, progress: number) {
|
||||
const credentials = store.get(`airplay.credentials.${deviceID}`)
|
||||
try {
|
||||
$`atvremote --id ${deviceID} --airplay-credentials ${credentials} set_position=${progress}`
|
||||
} catch (p: any) {
|
||||
return {
|
||||
result: 'failure',
|
||||
error: p.stderr,
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
result: 'success',
|
||||
}
|
||||
}
|
||||
|
||||
async pushUpdates(deviceID: string) {
|
||||
const credentials = store.get(`airplay.credentials.${deviceID}`)
|
||||
let updates = null
|
||||
try {
|
||||
updates = $`atvscript --id ${deviceID} --airplay-credentials ${credentials} push_updates`
|
||||
} catch (p: any) {
|
||||
return {
|
||||
result: 'failure',
|
||||
error: p.stderr,
|
||||
}
|
||||
}
|
||||
|
||||
for await (const chunk of updates.stdout) {
|
||||
this.window.webContents.send('airplay-updates', chunk)
|
||||
}
|
||||
}
|
||||
|
||||
async initIpc() {
|
||||
ipcMain.handle('airplay-scan-devices', () => {
|
||||
return this.scanDevices()
|
||||
})
|
||||
|
||||
ipcMain.handle('airplay-pair', async () => {
|
||||
console.log('airplay-pair')
|
||||
return this.pairDevice('58:D3:49:F0:C9:71', 'airplay')
|
||||
})
|
||||
|
||||
ipcMain.handle('airplay-pair-enter-pin', async (e, pin) => {
|
||||
return this.enterPairPin(pin)
|
||||
})
|
||||
|
||||
ipcMain.handle('airplay-play-url', async (e, { deviceID, url }) => {
|
||||
return this.playUrl(deviceID, url)
|
||||
})
|
||||
|
||||
ipcMain.handle('airplay-get-playing', async (e, { deviceID }) => {
|
||||
return this.getPlaying(deviceID)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
export default Airplay
|
||||
|
|
@ -1,24 +1,28 @@
|
|||
import fastify from 'fastify'
|
||||
import fastifyStatic from '@fastify/static'
|
||||
import path from 'path'
|
||||
import fastifyCookie from '@fastify/cookie'
|
||||
import fastifyMultipart from '@fastify/multipart'
|
||||
import fastifyStatic from '@fastify/static'
|
||||
import fastify from 'fastify'
|
||||
import path from 'path'
|
||||
import { isProd } from '../env'
|
||||
import log from '../log'
|
||||
import netease from './routes/netease'
|
||||
import netease from './routes/netease/netease'
|
||||
import appleMusic from './routes/r3play/appleMusic'
|
||||
import audio from './routes/r3play/audio'
|
||||
|
||||
const server = fastify({
|
||||
ignoreTrailingSlash: true,
|
||||
})
|
||||
|
||||
server.register(fastifyCookie)
|
||||
server.register(fastifyMultipart)
|
||||
if (isProd) {
|
||||
server.register(fastifyStatic, {
|
||||
root: path.join(__dirname, '../web'),
|
||||
})
|
||||
}
|
||||
|
||||
server.register(netease, { prefix: '/netease' })
|
||||
server.register(netease)
|
||||
server.register(audio)
|
||||
server.register(appleMusic)
|
||||
|
||||
const port = Number(
|
||||
|
|
|
|||
|
|
@ -1,8 +1,9 @@
|
|||
import { APIs } from '@/shared/CacheAPIs'
|
||||
import log from '@/desktop/main/log'
|
||||
import { CacheAPIs } from '@/shared/CacheAPIs'
|
||||
import { pathCase, snakeCase } from 'change-case'
|
||||
import { FastifyInstance, FastifyRequest, FastifyReply } from 'fastify'
|
||||
import NeteaseCloudMusicApi from 'NeteaseCloudMusicApi'
|
||||
import cache from '../../cache'
|
||||
import cache from '../../../cache'
|
||||
|
||||
async function netease(fastify: FastifyInstance) {
|
||||
const getHandler = (name: string, neteaseApi: (params: any) => any) => {
|
||||
|
|
@ -10,11 +11,11 @@ async function netease(fastify: FastifyInstance) {
|
|||
req: FastifyRequest<{ Querystring: { [key: string]: string } }>,
|
||||
reply: FastifyReply
|
||||
) => {
|
||||
// Get track details from cache
|
||||
if (name === APIs.Track) {
|
||||
const cacheData = cache.get(name, req.query)
|
||||
// // Get track details from cache
|
||||
if (name === CacheAPIs.Track) {
|
||||
const cacheData = await cache.get(name, req.query as any)
|
||||
if (cacheData) {
|
||||
return cache
|
||||
return cacheData
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -25,7 +26,8 @@ async function netease(fastify: FastifyInstance) {
|
|||
cookie: req.cookies,
|
||||
})
|
||||
|
||||
cache.set(name, result.body, req.query)
|
||||
cache.set(name as CacheAPIs, result.body, req.query)
|
||||
|
||||
return reply.send(result.body)
|
||||
} catch (error: any) {
|
||||
if ([400, 301].includes(error.status)) {
|
||||
|
|
@ -40,7 +42,9 @@ async function netease(fastify: FastifyInstance) {
|
|||
Object.entries(NeteaseCloudMusicApi).forEach(([nameInSnakeCase, neteaseApi]: [string, any]) => {
|
||||
// 例外
|
||||
if (
|
||||
['serveNcmApi', 'getModulesDefinitions', snakeCase(APIs.SongUrl)].includes(nameInSnakeCase)
|
||||
['serveNcmApi', 'getModulesDefinitions', snakeCase(CacheAPIs.SongUrl)].includes(
|
||||
nameInSnakeCase
|
||||
)
|
||||
) {
|
||||
return
|
||||
}
|
||||
|
|
@ -48,11 +52,11 @@ async function netease(fastify: FastifyInstance) {
|
|||
const name = pathCase(nameInSnakeCase)
|
||||
const handler = getHandler(name, neteaseApi)
|
||||
|
||||
fastify.get(`/${name}`, handler)
|
||||
fastify.post(`/${name}`, handler)
|
||||
fastify.get(`/netease/${name}`, handler)
|
||||
fastify.post(`/netease/${name}`, handler)
|
||||
})
|
||||
|
||||
fastify.get('/', () => 'NeteaseCloudMusicApi')
|
||||
fastify.get('/netease', () => 'NeteaseCloudMusicApi')
|
||||
}
|
||||
|
||||
export default netease
|
||||
212
packages/desktop/main/appServer/routes/r3play/audio.ts
Normal file
212
packages/desktop/main/appServer/routes/r3play/audio.ts
Normal file
|
|
@ -0,0 +1,212 @@
|
|||
import { FastifyInstance, FastifyRequest, FastifyReply } from 'fastify'
|
||||
import NeteaseCloudMusicApi, { SoundQualityType } from 'NeteaseCloudMusicApi'
|
||||
import prisma from '@/desktop/main/prisma'
|
||||
import { app } from 'electron'
|
||||
import log from '@/desktop/main/log'
|
||||
import { appName } from '@/desktop/main/env'
|
||||
import cache from '@/desktop/main/cache'
|
||||
import fs from 'fs'
|
||||
import youtube from '@/desktop/main/youtube'
|
||||
import { CacheAPIs } from '@/shared/CacheAPIs'
|
||||
import { FetchTracksResponse } from '@/shared/api/Track'
|
||||
|
||||
const getAudioFromCache = async (id: number) => {
|
||||
// get from cache
|
||||
const cache = await prisma.audio.findUnique({ where: { id } })
|
||||
if (!cache) return
|
||||
|
||||
const audioFileName = `${cache.id}-${cache.bitRate}.${cache.format}`
|
||||
|
||||
const isAudioFileExists = fs.existsSync(`${app.getPath('userData')}/audio_cache/${audioFileName}`)
|
||||
if (!isAudioFileExists) return
|
||||
|
||||
log.debug(`[server] Audio cache hit ${id}`)
|
||||
|
||||
return {
|
||||
data: [
|
||||
{
|
||||
source: cache.source,
|
||||
id: cache.id,
|
||||
url: `http://127.0.0.1:${
|
||||
process.env.ELECTRON_WEB_SERVER_PORT
|
||||
}/${appName.toLowerCase()}/audio/${audioFileName}`,
|
||||
br: cache.bitRate,
|
||||
size: 0,
|
||||
md5: '',
|
||||
code: 200,
|
||||
expi: 0,
|
||||
type: cache.format,
|
||||
gain: 0,
|
||||
fee: 8,
|
||||
uf: null,
|
||||
payed: 0,
|
||||
flag: 4,
|
||||
canExtend: false,
|
||||
freeTrialInfo: null,
|
||||
level: 'standard',
|
||||
encodeType: cache.format,
|
||||
freeTrialPrivilege: {
|
||||
resConsumable: false,
|
||||
userConsumable: false,
|
||||
listenType: null,
|
||||
},
|
||||
freeTimeTrialPrivilege: {
|
||||
resConsumable: false,
|
||||
userConsumable: false,
|
||||
type: 0,
|
||||
remainTime: 0,
|
||||
},
|
||||
urlSource: 0,
|
||||
},
|
||||
],
|
||||
code: 200,
|
||||
}
|
||||
}
|
||||
|
||||
const getAudioFromYouTube = async (id: number) => {
|
||||
let fetchTrackResult: FetchTracksResponse | undefined = await cache.get(CacheAPIs.Track, {
|
||||
ids: String(id),
|
||||
})
|
||||
if (!fetchTrackResult) {
|
||||
log.info(`[audio] getAudioFromYouTube no fetchTrackResult, fetch from netease api`)
|
||||
fetchTrackResult = (await NeteaseCloudMusicApi.song_detail({
|
||||
ids: String(id),
|
||||
})) as unknown as FetchTracksResponse
|
||||
}
|
||||
const track = fetchTrackResult?.songs?.[0]
|
||||
if (!track) return
|
||||
|
||||
const data = await youtube.matchTrack(track.ar[0].name, track.name)
|
||||
if (!data) return
|
||||
return {
|
||||
data: [
|
||||
{
|
||||
source: 'youtube',
|
||||
id,
|
||||
url: data.url,
|
||||
br: data.bitRate,
|
||||
size: 0,
|
||||
md5: '',
|
||||
code: 200,
|
||||
expi: 0,
|
||||
type: 'opus',
|
||||
gain: 0,
|
||||
fee: 8,
|
||||
uf: null,
|
||||
payed: 0,
|
||||
flag: 4,
|
||||
canExtend: false,
|
||||
freeTrialInfo: null,
|
||||
level: 'standard',
|
||||
encodeType: 'opus',
|
||||
freeTrialPrivilege: {
|
||||
resConsumable: false,
|
||||
userConsumable: false,
|
||||
listenType: null,
|
||||
},
|
||||
freeTimeTrialPrivilege: {
|
||||
resConsumable: false,
|
||||
userConsumable: false,
|
||||
type: 0,
|
||||
remainTime: 0,
|
||||
},
|
||||
urlSource: 0,
|
||||
r3play: {
|
||||
youtube: data,
|
||||
},
|
||||
},
|
||||
],
|
||||
code: 200,
|
||||
}
|
||||
}
|
||||
|
||||
async function audio(fastify: FastifyInstance) {
|
||||
// 劫持网易云的song/url api,将url替换成缓存的音频文件url
|
||||
fastify.get(
|
||||
'/netease/song/url/v1',
|
||||
async (
|
||||
req: FastifyRequest<{ Querystring: { id: string | number; level: SoundQualityType } }>,
|
||||
reply
|
||||
) => {
|
||||
const id = Number(req.query.id) || 0
|
||||
if (!id || isNaN(id)) {
|
||||
return reply.status(400).send({
|
||||
code: 400,
|
||||
msg: 'id is required or id is invalid',
|
||||
})
|
||||
}
|
||||
|
||||
const cache = await getAudioFromCache(id)
|
||||
if (cache) {
|
||||
return cache
|
||||
}
|
||||
|
||||
const { body: fromNetease }: { body: any } = await NeteaseCloudMusicApi.song_url_v1({
|
||||
...req.query,
|
||||
cookie: req.cookies as unknown as any,
|
||||
})
|
||||
if (
|
||||
fromNetease?.code === 200 &&
|
||||
!fromNetease?.data?.[0]?.freeTrialInfo &&
|
||||
fromNetease?.data?.[0]?.url
|
||||
) {
|
||||
reply.status(200).send(fromNetease)
|
||||
return
|
||||
}
|
||||
|
||||
const fromYoutube = getAudioFromYouTube(id)
|
||||
if (fromYoutube) {
|
||||
return fromYoutube
|
||||
}
|
||||
|
||||
// 是试听歌曲就把url删掉
|
||||
if (fromNetease?.data?.[0].freeTrialInfo) {
|
||||
fromNetease.data[0].url = ''
|
||||
}
|
||||
|
||||
reply.status(fromNetease?.code ?? 500).send(fromNetease)
|
||||
}
|
||||
)
|
||||
|
||||
// 获取缓存的音频数据
|
||||
fastify.get(
|
||||
`/${appName.toLowerCase()}/audio/:filename`,
|
||||
(req: FastifyRequest<{ Params: { filename: string } }>, reply) => {
|
||||
const filename = req.params.filename
|
||||
cache.getAudio(filename, reply)
|
||||
}
|
||||
)
|
||||
|
||||
// 缓存音频数据
|
||||
fastify.post(
|
||||
`/${appName.toLowerCase()}/audio/:id`,
|
||||
async (
|
||||
req: FastifyRequest<{ Params: { id: string }; Querystring: { url: string } }>,
|
||||
reply
|
||||
) => {
|
||||
const id = Number(req.params.id)
|
||||
const { url } = req.query
|
||||
if (isNaN(id)) {
|
||||
return reply.status(400).send({ error: 'Invalid param id' })
|
||||
}
|
||||
if (!url) {
|
||||
return reply.status(400).send({ error: 'Invalid query url' })
|
||||
}
|
||||
|
||||
const data = await req.file()
|
||||
|
||||
if (!data?.file) {
|
||||
return reply.status(400).send({ error: 'No file' })
|
||||
}
|
||||
|
||||
try {
|
||||
await cache.setAudio(await data.toBuffer(), { id, url })
|
||||
reply.status(200).send('Audio cached!')
|
||||
} catch (error) {
|
||||
reply.status(500).send({ error })
|
||||
}
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
export default audio
|
||||
|
|
@ -1,158 +1,149 @@
|
|||
import { db, Tables } from './db'
|
||||
import type { FetchTracksResponse } from '@/shared/api/Track'
|
||||
import prisma from './prisma'
|
||||
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'
|
||||
import { CacheAPIs, CacheAPIsParams, CacheAPIsResponse } from '@/shared/CacheAPIs'
|
||||
import { FastifyReply } from 'fastify'
|
||||
|
||||
class Cache {
|
||||
constructor() {
|
||||
//
|
||||
}
|
||||
|
||||
async set(api: string, data: any, query: any = {}) {
|
||||
async set<T extends CacheAPIs>(
|
||||
api: T,
|
||||
data: CacheAPIsResponse[T],
|
||||
query: { [key: string]: string } = {}
|
||||
) {
|
||||
if (!data) return
|
||||
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(),
|
||||
})
|
||||
case CacheAPIs.UserPlaylist:
|
||||
case CacheAPIs.UserAccount:
|
||||
case CacheAPIs.Personalized:
|
||||
case CacheAPIs.RecommendResource:
|
||||
case CacheAPIs.UserAlbums:
|
||||
case CacheAPIs.UserArtists:
|
||||
case CacheAPIs.ListenedRecords:
|
||||
case CacheAPIs.Likelist: {
|
||||
const id = api
|
||||
const row = { id, json: JSON.stringify(data) }
|
||||
await prisma.accountData.upsert({ where: { id }, create: row, update: row })
|
||||
break
|
||||
}
|
||||
case APIs.Track: {
|
||||
const res = data as FetchTracksResponse
|
||||
case CacheAPIs.Track: {
|
||||
const res = data as CacheAPIsResponse[CacheAPIs.Track]
|
||||
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(),
|
||||
}))
|
||||
await Promise.all(
|
||||
res.songs.map(t => {
|
||||
const id = t.id
|
||||
const row = { id, json: JSON.stringify(t) }
|
||||
return prisma.track.upsert({ where: { id }, create: row, update: row })
|
||||
})
|
||||
)
|
||||
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: {
|
||||
case CacheAPIs.Album: {
|
||||
const res = data as CacheAPIsResponse[CacheAPIs.Album]
|
||||
if (!res.album) return
|
||||
res.album.songs = data.songs
|
||||
const id = data.album.id
|
||||
const row = { id, json: JSON.stringify(data.album) }
|
||||
await prisma.album.upsert({ where: { id }, update: row, create: row })
|
||||
break
|
||||
}
|
||||
case CacheAPIs.Playlist: {
|
||||
if (!data.playlist) return
|
||||
const id = data.playlist.id
|
||||
const row = { id, json: JSON.stringify(data) }
|
||||
await prisma.playlist.upsert({ where: { id }, update: row, create: row })
|
||||
break
|
||||
}
|
||||
case CacheAPIs.Artist: {
|
||||
if (!data.artist) return
|
||||
const id = data.artist.id
|
||||
const row = { id, json: JSON.stringify(data) }
|
||||
await prisma.artist.upsert({ where: { id }, update: row, create: row })
|
||||
break
|
||||
}
|
||||
case CacheAPIs.ArtistAlbum: {
|
||||
const res = data as CacheAPIsResponse[CacheAPIs.ArtistAlbum]
|
||||
if (!res.hotAlbums) return
|
||||
|
||||
const id = data.artist.id
|
||||
const row = { id, hotAlbums: res.hotAlbums.map(a => a.id).join(',') }
|
||||
await prisma.artistAlbum.upsert({ where: { id }, update: row, create: row })
|
||||
await Promise.all(
|
||||
res.hotAlbums.map(async album => {
|
||||
const id = album.id
|
||||
const existAlbum = await prisma.album.findUnique({ where: { id } })
|
||||
if (!existAlbum) {
|
||||
await prisma.album.create({ data: { id, json: JSON.stringify(album) } })
|
||||
}
|
||||
})
|
||||
)
|
||||
break
|
||||
}
|
||||
case CacheAPIs.Lyric: {
|
||||
if (!data.lrc) return
|
||||
db.upsert(Tables.Lyric, {
|
||||
id: query.id,
|
||||
json: JSON.stringify(data),
|
||||
updatedAt: Date.now(),
|
||||
})
|
||||
const id = Number(query.id)
|
||||
const row = { id, json: JSON.stringify(data) }
|
||||
await prisma.lyrics.upsert({ where: { id }, update: row, create: row })
|
||||
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: {
|
||||
// case CacheAPIs.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 CacheAPIs.AppleMusicAlbum: {
|
||||
if (!data.id) return
|
||||
db.upsert(Tables.AppleMusicAlbum, {
|
||||
id: data.id,
|
||||
json: data.album ? JSON.stringify(data.album) : 'no',
|
||||
updatedAt: Date.now(),
|
||||
})
|
||||
const id = data.id
|
||||
const row = { id, json: JSON.stringify(data) }
|
||||
await prisma.appleMusicAlbum.upsert({ where: { id }, update: row, create: row })
|
||||
break
|
||||
}
|
||||
case APIs.AppleMusicArtist: {
|
||||
case CacheAPIs.AppleMusicArtist: {
|
||||
if (!data) return
|
||||
db.upsert(Tables.AppleMusicArtist, {
|
||||
id: data.id,
|
||||
json: data.artist ? JSON.stringify(data.artist) : 'no',
|
||||
updatedAt: Date.now(),
|
||||
})
|
||||
const id = data.id
|
||||
const row = { id, json: JSON.stringify(data) }
|
||||
await prisma.artist.upsert({ where: { id }, update: row, create: row })
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
get<T extends keyof APIsParams>(api: T, params: any): any {
|
||||
async get<T extends CacheAPIs>(
|
||||
api: T,
|
||||
query: CacheAPIsParams[T]
|
||||
): Promise<CacheAPIsResponse[T] | undefined> {
|
||||
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)
|
||||
case CacheAPIs.UserPlaylist:
|
||||
case CacheAPIs.UserAccount:
|
||||
case CacheAPIs.Personalized:
|
||||
case CacheAPIs.RecommendResource:
|
||||
case CacheAPIs.UserArtists:
|
||||
case CacheAPIs.ListenedRecords:
|
||||
case CacheAPIs.Likelist: {
|
||||
const data = await prisma.accountData.findUnique({ where: { id: api } })
|
||||
if (data?.json) return JSON.parse(data.json)
|
||||
break
|
||||
}
|
||||
case APIs.Track: {
|
||||
const ids: number[] = params?.ids.split(',').map((id: string) => Number(id))
|
||||
case CacheAPIs.Track: {
|
||||
const typedQuery = query as CacheAPIsParams[CacheAPIs.Track]
|
||||
const ids: number[] = typedQuery?.ids.split(',').map((id: string) => Number(id))
|
||||
if (ids.length === 0) return
|
||||
|
||||
if (ids.includes(NaN)) return
|
||||
|
||||
const tracksRaw = db.findMany(Tables.Track, ids)
|
||||
const tracksRaw = await prisma.track.findMany({ where: { id: { in: ids } } })
|
||||
|
||||
if (tracksRaw.length !== ids.length) {
|
||||
return
|
||||
|
|
@ -167,9 +158,11 @@ class Cache {
|
|||
privileges: {},
|
||||
}
|
||||
}
|
||||
case APIs.Album: {
|
||||
if (isNaN(Number(params?.id))) return
|
||||
const data = db.find(Tables.Album, params.id)
|
||||
case CacheAPIs.Album: {
|
||||
const typedQuery = query as CacheAPIsParams[CacheAPIs.Album]
|
||||
const id = Number(typedQuery?.id)
|
||||
if (isNaN(id)) return
|
||||
const data = await prisma.album.findUnique({ where: { id } })
|
||||
if (data?.json)
|
||||
return {
|
||||
resourceState: true,
|
||||
|
|
@ -179,99 +172,94 @@ class Cache {
|
|||
}
|
||||
break
|
||||
}
|
||||
case APIs.Playlist: {
|
||||
if (isNaN(Number(params?.id))) return
|
||||
const data = db.find(Tables.Playlist, params.id)
|
||||
case CacheAPIs.Playlist: {
|
||||
const typedQuery = query as CacheAPIsParams[CacheAPIs.Playlist]
|
||||
const id = Number(typedQuery?.id)
|
||||
if (isNaN(id)) return
|
||||
const data = await prisma.playlist.findUnique({ where: { 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)
|
||||
case CacheAPIs.Artist: {
|
||||
const typedQuery = query as CacheAPIsParams[CacheAPIs.Artist]
|
||||
const id = Number(typedQuery?.id)
|
||||
if (isNaN(id)) return
|
||||
const data = await prisma.artist.findUnique({ where: { 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
|
||||
case CacheAPIs.ArtistAlbum: {
|
||||
const typedQuery = query as CacheAPIsParams[CacheAPIs.ArtistAlbum]
|
||||
const id = Number(typedQuery?.id)
|
||||
if (isNaN(id)) return
|
||||
|
||||
const artistAlbums = await prisma.artistAlbum.findUnique({ where: { id } })
|
||||
if (!artistAlbums?.hotAlbums) return
|
||||
const ids = artistAlbums.hotAlbums.split(',').map(Number)
|
||||
|
||||
const albumsRaw = await prisma.album.findMany({
|
||||
where: { id: { in: ids } },
|
||||
})
|
||||
return result
|
||||
if (albumsRaw.length !== ids.length) return
|
||||
const albums = albumsRaw.map(a => JSON.parse(a.json))
|
||||
return {
|
||||
hotAlbums: ids.map((id: number) => albums.find(a => a.id === id)),
|
||||
}
|
||||
}
|
||||
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)
|
||||
case CacheAPIs.Lyric: {
|
||||
const typedQuery = query as CacheAPIsParams[CacheAPIs.Lyric]
|
||||
const id = Number(typedQuery?.id)
|
||||
if (isNaN(id)) return
|
||||
const data = await prisma.lyrics.findUnique({ where: { id } })
|
||||
if (data?.json) 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)
|
||||
case CacheAPIs.CoverColor: {
|
||||
// if (isNaN(Number(params?.id))) return
|
||||
// return db.find(Tables.CoverColor, params.id)?.color
|
||||
}
|
||||
case CacheAPIs.AppleMusicAlbum: {
|
||||
const typedQuery = query as CacheAPIsParams[CacheAPIs.AppleMusicAlbum]
|
||||
const id = Number(typedQuery?.id)
|
||||
if (isNaN(id)) return
|
||||
const data = await prisma.appleMusicAlbum.findUnique({ where: { id } })
|
||||
if (data?.json) return JSON.parse(data.json)
|
||||
break
|
||||
}
|
||||
case CacheAPIs.AppleMusicArtist: {
|
||||
const typedQuery = query as CacheAPIsParams[CacheAPIs.AppleMusicArtist]
|
||||
const id = Number(typedQuery?.id)
|
||||
if (isNaN(id)) return
|
||||
const data = await prisma.appleMusicArtist.findUnique({ where: { id } })
|
||||
if (data?.json) return JSON.parse(data.json)
|
||||
break
|
||||
}
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
getAudio(fileName: string, res: Response) {
|
||||
if (!fileName) {
|
||||
return res.status(400).send({ error: 'No filename provided' })
|
||||
getAudio(filename: string, reply: FastifyReply) {
|
||||
if (!filename) {
|
||||
return reply.status(400).send({ error: 'No filename provided' })
|
||||
}
|
||||
const id = Number(fileName.split('-')[0])
|
||||
const id = Number(filename.split('-')[0])
|
||||
|
||||
try {
|
||||
const path = `${app.getPath('userData')}/audio_cache/${fileName}`
|
||||
const path = `${app.getPath('userData')}/audio_cache/${filename}`
|
||||
const audio = fs.readFileSync(path)
|
||||
if (audio.byteLength === 0) {
|
||||
db.delete(Tables.Audio, id)
|
||||
prisma.audio.delete({ where: { id } })
|
||||
fs.unlinkSync(path)
|
||||
return res.status(404).send({ error: 'Audio not found' })
|
||||
return reply.status(404).send({ error: 'Audio not found' })
|
||||
}
|
||||
res
|
||||
reply
|
||||
.status(206)
|
||||
.setHeader('Accept-Ranges', 'bytes')
|
||||
.setHeader('Connection', 'keep-alive')
|
||||
.setHeader('Content-Range', `bytes 0-${audio.byteLength - 1}/${audio.byteLength}`)
|
||||
.header('Accept-Ranges', 'bytes')
|
||||
.header('Connection', 'keep-alive')
|
||||
.header('Content-Range', `bytes 0-${audio.byteLength - 1}/${audio.byteLength}`)
|
||||
.send(audio)
|
||||
} catch (error) {
|
||||
res.status(500).send({ error })
|
||||
reply.status(500).send({ error })
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -285,8 +273,8 @@ class Cache {
|
|||
}
|
||||
|
||||
const meta = await musicMetadata.parseBuffer(buffer)
|
||||
const br = meta?.format?.codec === 'OPUS' ? 165000 : meta.format.bitrate ?? 0
|
||||
const type =
|
||||
const bitRate = (meta?.format?.codec === 'OPUS' ? 165000 : meta.format.bitrate ?? 0) / 1000
|
||||
const format =
|
||||
{
|
||||
'MPEG 1 Layer 3': 'mp3',
|
||||
'Ogg Vorbis': 'ogg',
|
||||
|
|
@ -295,29 +283,23 @@ class Cache {
|
|||
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 => {
|
||||
fs.writeFile(`${path}/${id}-${bitRate}.${format}`, buffer, async 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(),
|
||||
const row = { id, bitRate, format, source }
|
||||
await prisma.audio.upsert({
|
||||
where: { id },
|
||||
create: row,
|
||||
update: row,
|
||||
})
|
||||
|
||||
log.info(`[cache] cacheAudio ${id}-${br}.${type}`)
|
||||
log.info(`Audio file ${id}-${bitRate}.${format} cached!`)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,342 +0,0 @@
|
|||
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()
|
||||
|
|
@ -1,344 +0,0 @@
|
|||
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()
|
||||
|
|
@ -1,235 +0,0 @@
|
|||
import path from 'path'
|
||||
import { app } from 'electron'
|
||||
import fs from 'fs'
|
||||
import SQLite3 from 'better-sqlite3'
|
||||
import log from './log'
|
||||
import { createFileIfNotExist, dirname } from './utils'
|
||||
import { isProd } from './env'
|
||||
import pkg from '../../../package.json'
|
||||
import { compare, validate } from 'compare-versions'
|
||||
import os from 'os'
|
||||
|
||||
export const enum Tables {
|
||||
Track = 'Track',
|
||||
Album = 'Album',
|
||||
Artist = 'Artist',
|
||||
Playlist = 'Playlist',
|
||||
ArtistAlbum = 'ArtistAlbum',
|
||||
Lyric = 'Lyric',
|
||||
Audio = 'Audio',
|
||||
AccountData = 'AccountData',
|
||||
CoverColor = 'CoverColor',
|
||||
AppData = 'AppData',
|
||||
AppleMusicAlbum = 'AppleMusicAlbum',
|
||||
AppleMusicArtist = 'AppleMusicArtist',
|
||||
}
|
||||
interface CommonTableStructure {
|
||||
id: number
|
||||
json: string
|
||||
updatedAt: number
|
||||
}
|
||||
export interface TablesStructures {
|
||||
[Tables.Track]: CommonTableStructure
|
||||
[Tables.Album]: CommonTableStructure
|
||||
[Tables.Artist]: CommonTableStructure
|
||||
[Tables.Playlist]: CommonTableStructure
|
||||
[Tables.ArtistAlbum]: CommonTableStructure
|
||||
[Tables.Lyric]: CommonTableStructure
|
||||
[Tables.AccountData]: {
|
||||
id: string
|
||||
json: string
|
||||
updatedAt: number
|
||||
}
|
||||
[Tables.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
|
||||
}
|
||||
[Tables.CoverColor]: {
|
||||
id: number
|
||||
color: string
|
||||
queriedAt: number
|
||||
}
|
||||
[Tables.AppData]: {
|
||||
id: 'appVersion' | 'skippedVersion'
|
||||
value: string
|
||||
}
|
||||
[Tables.AppleMusicAlbum]: CommonTableStructure
|
||||
[Tables.AppleMusicArtist]: CommonTableStructure
|
||||
}
|
||||
|
||||
type TableNames = keyof TablesStructures
|
||||
|
||||
const readSqlFile = (filename: string) => {
|
||||
return fs.readFileSync(path.join(dirname, `./migrations/${filename}`), 'utf8')
|
||||
}
|
||||
|
||||
class DB {
|
||||
sqlite: SQLite3.Database
|
||||
dbFilePath: string = path.resolve(app.getPath('userData'), './api_cache/db.sqlite')
|
||||
|
||||
constructor() {
|
||||
log.info('[db] Initializing database...')
|
||||
|
||||
try {
|
||||
createFileIfNotExist(this.dbFilePath)
|
||||
|
||||
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`),
|
||||
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() {
|
||||
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.')
|
||||
}
|
||||
|
||||
migrate() {
|
||||
log.info('[db] Migrating database..')
|
||||
|
||||
const key = 'appVersion'
|
||||
const appVersion = this.find(Tables.AppData, key)
|
||||
const updateAppVersionInDB = () => {
|
||||
this.upsert(Tables.AppData, {
|
||||
id: key,
|
||||
value: pkg.version,
|
||||
})
|
||||
}
|
||||
|
||||
if (!appVersion?.value) {
|
||||
updateAppVersionInDB()
|
||||
return
|
||||
}
|
||||
|
||||
const sqlFiles = fs.readdirSync(path.join(dirname, './migrations'))
|
||||
sqlFiles.forEach((sqlFile: string) => {
|
||||
const version = sqlFile.split('.').shift() || ''
|
||||
if (!validate(version)) return
|
||||
if (compare(version, pkg.version, '>')) {
|
||||
const file = readSqlFile(sqlFile)
|
||||
this.sqlite.exec(file)
|
||||
}
|
||||
})
|
||||
|
||||
updateAppVersionInDB()
|
||||
|
||||
log.info('[db] Database migrated.')
|
||||
}
|
||||
|
||||
find<T extends TableNames>(
|
||||
table: T,
|
||||
key: TablesStructures[T]['id']
|
||||
): TablesStructures[T] | undefined {
|
||||
return this.sqlite.prepare(`SELECT * FROM ${table} WHERE id = ? LIMIT 1`).get(key)
|
||||
}
|
||||
|
||||
findMany<T extends TableNames>(
|
||||
table: T,
|
||||
keys: TablesStructures[T]['id'][]
|
||||
): TablesStructures[T][] {
|
||||
const idsQuery = keys.map(key => `id = ${key}`).join(' OR ')
|
||||
return this.sqlite.prepare(`SELECT * FROM ${table} WHERE ${idsQuery}`).all()
|
||||
}
|
||||
|
||||
findAll<T extends TableNames>(table: T): TablesStructures[T][] {
|
||||
return this.sqlite.prepare(`SELECT * FROM ${table}`).all()
|
||||
}
|
||||
|
||||
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)
|
||||
}
|
||||
|
||||
createMany<T extends TableNames>(
|
||||
table: T,
|
||||
data: TablesStructures[T][],
|
||||
skipWhenExist: boolean = true
|
||||
) {
|
||||
const valuesQuery = Object.keys(data[0])
|
||||
.map(key => `:${key}`)
|
||||
.join(', ')
|
||||
const insert = this.sqlite.prepare(
|
||||
`INSERT ${skipWhenExist ? 'OR IGNORE' : ''} INTO ${table} VALUES (${valuesQuery})`
|
||||
)
|
||||
const insertMany = this.sqlite.transaction((rows: any[]) => {
|
||||
rows.forEach((row: any) => insert.run(row))
|
||||
})
|
||||
insertMany(data)
|
||||
}
|
||||
|
||||
upsert<T extends TableNames>(table: T, data: TablesStructures[T]) {
|
||||
const valuesQuery = Object.keys(data)
|
||||
.map(key => `:${key}`)
|
||||
.join(', ')
|
||||
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 upsertMany = this.sqlite.transaction((rows: any[]) => {
|
||||
rows.forEach((row: any) => upsert.run(row))
|
||||
})
|
||||
upsertMany(data)
|
||||
}
|
||||
|
||||
delete<T extends TableNames>(table: T, key: TablesStructures[T]['id']) {
|
||||
return this.sqlite.prepare(`DELETE FROM ${table} WHERE id = ?`).run(key)
|
||||
}
|
||||
|
||||
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()
|
||||
}
|
||||
|
||||
truncate<T extends TableNames>(table: T) {
|
||||
return this.sqlite.prepare(`DELETE FROM ${table}`).run()
|
||||
}
|
||||
|
||||
vacuum() {
|
||||
return this.sqlite.prepare('VACUUM').run()
|
||||
}
|
||||
}
|
||||
|
||||
export const db = new DB()
|
||||
|
|
@ -1,6 +1,5 @@
|
|||
import './preload' // must be first
|
||||
import './sentry'
|
||||
// import './server'
|
||||
import { BrowserWindow, BrowserWindowConstructorOptions, app, shell } from 'electron'
|
||||
import { release } from 'os'
|
||||
import { join } from 'path'
|
||||
|
|
@ -39,11 +38,10 @@ class Main {
|
|||
this.handleAppEvents()
|
||||
this.handleWindowEvents()
|
||||
this.createTray()
|
||||
createMenu(this.win)
|
||||
createMenu(this.win!)
|
||||
this.createThumbar()
|
||||
initIpcMain(this.win, this.tray, this.thumbar, store)
|
||||
this.initDevTools()
|
||||
// new Airplay(this.win)
|
||||
})
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -6,7 +6,7 @@ import log from './log'
|
|||
import fs from 'fs'
|
||||
import Store from 'electron-store'
|
||||
import { TypedElectronStore } from './store'
|
||||
import { APIs } from '@/shared/CacheAPIs'
|
||||
import { CacheAPIs } from '@/shared/CacheAPIs'
|
||||
import { YPMTray } from './tray'
|
||||
import { Thumbar } from './windowsTaskbar'
|
||||
import fastFolderSize from 'fast-folder-size'
|
||||
|
|
@ -151,7 +151,7 @@ function initOtherIpcMain() {
|
|||
*/
|
||||
on(IpcChannels.CacheCoverColor, (event, args) => {
|
||||
const { id, color } = args
|
||||
cache.set(APIs.CoverColor, { id, color })
|
||||
cache.set(CacheAPIs.CoverColor, { id, color })
|
||||
})
|
||||
|
||||
/**
|
||||
|
|
|
|||
|
|
@ -1,7 +1,7 @@
|
|||
/** By default, it writes logs to the following locations:
|
||||
* on Linux: ~/.config/{app name}/logs/{process type}.log
|
||||
* on macOS: ~/Library/Logs/{app name}/{process type}.log
|
||||
* on Windows: %USERPROFILE%\AppData\Roaming\{app name}\logs\{process type}.log
|
||||
* on Linux: ~/.config/r3play/logs/main.log
|
||||
* on macOS: ~/Library/Logs/r3play/main.log
|
||||
* on Windows: %USERPROFILE%\AppData\Roaming\r3play\logs\main.log
|
||||
* @see https://www.npmjs.com/package/electron-log
|
||||
*/
|
||||
|
||||
|
|
@ -12,9 +12,7 @@ import { isDev } from './env'
|
|||
Object.assign(console, log.functions)
|
||||
log.variables.process = 'main'
|
||||
if (log.transports.ipc) log.transports.ipc.level = false
|
||||
log.transports.console.format = `${
|
||||
isDev ? '' : pc.dim('{h}:{i}:{s}{scope} ')
|
||||
}{level} › {text}`
|
||||
log.transports.console.format = `${isDev ? '' : pc.dim('{h}:{i}:{s}{scope} ')}{level} › {text}`
|
||||
log.transports.file.level = 'info'
|
||||
|
||||
log.info(
|
||||
|
|
|
|||
|
|
@ -1,11 +1,7 @@
|
|||
import log from './log'
|
||||
import { app } from 'electron'
|
||||
import { isDev } from './env'
|
||||
import {
|
||||
createDirIfNotExist,
|
||||
portableUserDataPath,
|
||||
devUserDataPath,
|
||||
} from './utils'
|
||||
import { createDirIfNotExist, portableUserDataPath, devUserDataPath, dirname } from './utils'
|
||||
|
||||
if (isDev) {
|
||||
createDirIfNotExist(devUserDataPath)
|
||||
|
|
@ -16,4 +12,6 @@ if (process.env.PORTABLE_EXECUTABLE_DIR) {
|
|||
app.setPath('appData', portableUserDataPath)
|
||||
}
|
||||
|
||||
log.info('[preload] dirname', dirname)
|
||||
|
||||
log.info(`[preload] userData path: ${app.getPath('userData')}`)
|
||||
|
|
|
|||
84
packages/desktop/main/prisma.ts
Normal file
84
packages/desktop/main/prisma.ts
Normal file
|
|
@ -0,0 +1,84 @@
|
|||
import { app } from 'electron'
|
||||
import path from 'path'
|
||||
import { PrismaClient } from '../prisma/client'
|
||||
import { isDev } from './env'
|
||||
import log from './log'
|
||||
import { createFileIfNotExist, dirname, isFileExist } from './utils'
|
||||
import fs from 'fs'
|
||||
|
||||
export const dbPath = path.join(app.getPath('userData'), 'r3play.db')
|
||||
export const dbUrl = 'file://' + dbPath
|
||||
|
||||
const extraResourcesPath = app.getAppPath().replace('app.asar', '') // impacted by extraResources setting in electron-builder.yml
|
||||
function getPlatformName(): string {
|
||||
const isDarwin = process.platform === 'darwin'
|
||||
if (isDarwin && process.arch === 'arm64') {
|
||||
return process.platform + 'Arm64'
|
||||
}
|
||||
|
||||
return process.platform
|
||||
}
|
||||
const platformName = getPlatformName()
|
||||
export const platformToExecutables: any = {
|
||||
win32: {
|
||||
migrationEngine: 'node_modules/@prisma/engines/migration-engine-windows.exe',
|
||||
queryEngine: 'node_modules/@prisma/engines/query_engine-windows.dll.node',
|
||||
},
|
||||
linux: {
|
||||
migrationEngine: 'node_modules/@prisma/engines/migration-engine-debian-openssl-1.1.x',
|
||||
queryEngine: 'node_modules/@prisma/engines/libquery_engine-debian-openssl-1.1.x.so.node',
|
||||
},
|
||||
darwin: {
|
||||
migrationEngine: 'node_modules/@prisma/engines/migration-engine-darwin',
|
||||
queryEngine: 'node_modules/@prisma/engines/libquery_engine-darwin.dylib.node',
|
||||
},
|
||||
darwinArm64: {
|
||||
migrationEngine: 'node_modules/@prisma/engines/migration-engine-darwin-arm64',
|
||||
queryEngine: 'node_modules/@prisma/engines/libquery_engine-darwin-arm64.dylib.node',
|
||||
},
|
||||
}
|
||||
export const queryEnginePath = path.join(
|
||||
extraResourcesPath,
|
||||
platformToExecutables[platformName].queryEngine
|
||||
)
|
||||
|
||||
log.info('[prisma] dbUrl', dbUrl)
|
||||
|
||||
// Hacky, but putting this here because otherwise at query time the Prisma client
|
||||
// gives an error "Environment variable not found: DATABASE_URL" despite us passing
|
||||
// the dbUrl into the prisma client constructor in datasources.db.url
|
||||
process.env.DATABASE_URL = dbUrl
|
||||
|
||||
const isInitialized = isFileExist(dbPath)
|
||||
if (!isInitialized) {
|
||||
const from = isDev ? path.join(dirname, './prisma/r3play.db') : path.join(dirname, './r3play.db')
|
||||
log.info(`[prisma] copy r3play.db file from ${from} to ${dbPath}`)
|
||||
fs.copyFileSync(from, dbPath)
|
||||
log.info('[prisma] Database tables initialized.')
|
||||
} else {
|
||||
log.info('[prisma] Database tables already initialized before.')
|
||||
}
|
||||
|
||||
let prisma: PrismaClient = null
|
||||
try {
|
||||
prisma = new PrismaClient({
|
||||
log: isDev ? ['info', 'warn', 'error'] : ['error'],
|
||||
datasources: {
|
||||
db: {
|
||||
url: dbUrl,
|
||||
},
|
||||
},
|
||||
// see https://github.com/prisma/prisma/discussions/5200
|
||||
// @ts-expect-error internal prop
|
||||
// __internal: {
|
||||
// engine: {
|
||||
// binaryPath: queryEnginePath,
|
||||
// },
|
||||
// },
|
||||
})
|
||||
log.info('[prisma] prisma initialized')
|
||||
} catch (e) {
|
||||
log.error('[prisma] failed to init prisma', e)
|
||||
}
|
||||
|
||||
export default prisma
|
||||
|
|
@ -1,345 +0,0 @@
|
|||
import { pathCase } from 'change-case'
|
||||
import cookieParser from 'cookie-parser'
|
||||
import express, { Request, Response } from 'express'
|
||||
import log from './log'
|
||||
import cache from './cache'
|
||||
import fileUpload from 'express-fileupload'
|
||||
import path from 'path'
|
||||
import fs from 'fs'
|
||||
// import { db, Tables } from './db'
|
||||
import { app } from 'electron'
|
||||
import type { FetchAudioSourceResponse } from '@/shared/api/Track'
|
||||
import { APIs as CacheAPIs } from '@/shared/CacheAPIs'
|
||||
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(
|
||||
isProd
|
||||
? process.env.ELECTRON_WEB_SERVER_PORT || 42710
|
||||
: process.env.ELECTRON_DEV_NETEASE_API_PORT || 30001
|
||||
)
|
||||
app = express()
|
||||
// eslint-disable-next-line @typescript-eslint/no-var-requires
|
||||
netease = require('NeteaseCloudMusicApi') as any
|
||||
|
||||
constructor() {
|
||||
log.info('[server] starting http 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]) => {
|
||||
// 例外处理
|
||||
if (['serveNcmApi', 'getModulesDefinitions', APIs.SongUrl].includes(name)) {
|
||||
return
|
||||
}
|
||||
|
||||
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)
|
||||
}
|
||||
return res.status(500)
|
||||
}
|
||||
}
|
||||
|
||||
this.app.get(`/netease/${name}`, wrappedHandler)
|
||||
this.app.post(`/netease/${name}`, wrappedHandler)
|
||||
})
|
||||
}
|
||||
|
||||
serveStaticForProduction() {
|
||||
if (isProd) {
|
||||
this.app.use(history())
|
||||
this.app.use(express.static(path.join(__dirname, '../web')))
|
||||
}
|
||||
}
|
||||
|
||||
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/${appName.toLowerCase()}/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) {
|
||||
log.debug(
|
||||
`[server] UMN match: ${matchedAudio.song?.name} (https://youtube.com/v/${matchedAudio.song?.id})`
|
||||
)
|
||||
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',
|
||||
})
|
||||
}
|
||||
|
||||
try {
|
||||
const fromCache = await getFromCache(id)
|
||||
if (fromCache) {
|
||||
res.status(200).send(fromCache)
|
||||
return
|
||||
}
|
||||
} catch (error) {
|
||||
log.error(`[server] getFromCache failed: ${String(error)}`)
|
||||
}
|
||||
|
||||
const fromNetease = await getFromNetease(req)
|
||||
if (
|
||||
fromNetease?.code === 200 &&
|
||||
!fromNetease?.data?.[0]?.freeTrialInfo &&
|
||||
fromNetease?.data?.[0]?.url
|
||||
) {
|
||||
res.status(200).send(fromNetease)
|
||||
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)}`)
|
||||
// }
|
||||
|
||||
if (fromNetease?.data?.[0].freeTrialInfo) {
|
||||
fromNetease.data[0].url = ''
|
||||
}
|
||||
|
||||
res.status(fromNetease?.code ?? 500).send(fromNetease)
|
||||
}
|
||||
|
||||
this.app.get('/netease/song/url', handler)
|
||||
}
|
||||
|
||||
cacheAudioHandler() {
|
||||
this.app.get(
|
||||
`/${appName.toLowerCase()}/audio/:filename`,
|
||||
async (req: Request, res: Response) => {
|
||||
cache.getAudio(req.params.filename, res)
|
||||
}
|
||||
)
|
||||
this.app.post(`/${appName.toLowerCase()}/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, {
|
||||
id,
|
||||
url: String(req.query.url) || '',
|
||||
})
|
||||
res.status(200).send('Audio cached!')
|
||||
} catch (error) {
|
||||
res.status(500).send({ error })
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
listen() {
|
||||
this.app.listen(this.port, '127.0.0.1', () => {
|
||||
log.info(`[server] API server listening on port ${this.port}`)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
export default new Server()
|
||||
|
|
@ -8,11 +8,6 @@ export interface TypedElectronStore {
|
|||
y?: number
|
||||
}
|
||||
// settings: State['settings']
|
||||
airplay: {
|
||||
credentials: {
|
||||
[key: string]: string
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const store = new Store<TypedElectronStore>({
|
||||
|
|
@ -22,9 +17,6 @@ const store = new Store<TypedElectronStore>({
|
|||
height: 1024,
|
||||
},
|
||||
// settings: initialState.settings,
|
||||
airplay: {
|
||||
credentials: {},
|
||||
},
|
||||
},
|
||||
})
|
||||
|
||||
|
|
|
|||
|
|
@ -1,301 +0,0 @@
|
|||
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
|
||||
|
|
@ -16,15 +16,19 @@ export const logsPath = {
|
|||
win32: `%USERPROFILE%\\AppData\\Roaming\\${pkg.productName}\\logs`,
|
||||
}[process.platform as 'darwin' | 'win32' | 'linux']
|
||||
|
||||
export const isFileExist = (file: string) => {
|
||||
return fs.existsSync(file)
|
||||
}
|
||||
|
||||
export const createDirIfNotExist = (dir: string) => {
|
||||
if (!fs.existsSync(dir)) {
|
||||
if (!isFileExist(dir)) {
|
||||
fs.mkdirSync(dir, { recursive: true })
|
||||
}
|
||||
}
|
||||
|
||||
export const createFileIfNotExist = (file: string) => {
|
||||
createDirIfNotExist(path.dirname(file))
|
||||
if (!fs.existsSync(file)) {
|
||||
if (!isFileExist(file)) {
|
||||
fs.writeFileSync(file, '')
|
||||
}
|
||||
}
|
||||
|
|
|
|||
71
packages/desktop/main/youtube.ts
Normal file
71
packages/desktop/main/youtube.ts
Normal file
|
|
@ -0,0 +1,71 @@
|
|||
import log from './log'
|
||||
import youtube, { Scraper, Video } from '@yimura/scraper'
|
||||
import ytdl from 'ytdl-core'
|
||||
|
||||
class YoutubeDownloader {
|
||||
yt: Scraper
|
||||
|
||||
constructor() {
|
||||
// @ts-ignore
|
||||
this.yt = new youtube.default()
|
||||
}
|
||||
|
||||
async search(keyword: string) {
|
||||
const result = await this.yt.search(keyword)
|
||||
return result?.videos
|
||||
}
|
||||
|
||||
async matchTrack(artist: string, trackName: string) {
|
||||
console.time('[youtube] search')
|
||||
const videos = await this.search(`${artist} ${trackName} lyric audio`)
|
||||
console.timeEnd('[youtube] search')
|
||||
let video: Video | null = null
|
||||
|
||||
// 找官方频道最匹配的
|
||||
// videos.forEach(v => {
|
||||
// if (video) return
|
||||
// const channelName = v.channel.name.toLowerCase()
|
||||
// if (channelName !== artist.toLowerCase()) return
|
||||
// const title = v.title.toLowerCase()
|
||||
// if (!title.includes(trackName.toLowerCase())) return
|
||||
// if (!title.includes('audio') && !title.includes('lyric')) return
|
||||
// video = v
|
||||
// })
|
||||
|
||||
// TODO:找时长误差不超过2秒的
|
||||
|
||||
// 最后方案选搜索的第一个
|
||||
if (!video) {
|
||||
video = videos[0]
|
||||
}
|
||||
|
||||
console.time('[youtube] getInfo')
|
||||
const info = await ytdl.getInfo('http://www.youtube.com/watch?v=' + video.id)
|
||||
console.timeEnd('[youtube] getInfo')
|
||||
let url = ''
|
||||
let bitRate = 0
|
||||
info.formats.forEach(video => {
|
||||
if (
|
||||
video.mimeType === `audio/webm; codecs="opus"` &&
|
||||
video.bitrate &&
|
||||
video.bitrate > bitRate
|
||||
) {
|
||||
url = video.url
|
||||
bitRate = video.bitrate
|
||||
}
|
||||
})
|
||||
const data = {
|
||||
url,
|
||||
bitRate,
|
||||
title: info.videoDetails.title,
|
||||
videoId: info.videoDetails.videoId,
|
||||
duration: info.videoDetails.lengthSeconds,
|
||||
channel: info.videoDetails.ownerChannelName,
|
||||
}
|
||||
log.info(`[youtube] matched `, data)
|
||||
return data
|
||||
}
|
||||
}
|
||||
|
||||
const youtubeDownloader = new YoutubeDownloader()
|
||||
export default youtubeDownloader
|
||||
|
|
@ -1,13 +1,120 @@
|
|||
CREATE TABLE IF NOT EXISTS "AccountData" ("id" text NOT NULL,"json" text NOT NULL,"updatedAt" int NOT NULL, PRIMARY KEY (id));
|
||||
CREATE TABLE IF NOT EXISTS "Album" ("id" integer NOT NULL,"json" text NOT NULL,"updatedAt" int NOT NULL, PRIMARY KEY (id));
|
||||
CREATE TABLE IF NOT EXISTS "ArtistAlbum" ("id" integer NOT NULL,"json" text NOT NULL,"updatedAt" int NOT NULL, PRIMARY KEY (id));
|
||||
CREATE TABLE IF NOT EXISTS "Artist" ("id" integer NOT NULL,"json" text NOT NULL,"updatedAt" int NOT NULL, PRIMARY KEY (id));
|
||||
CREATE TABLE IF NOT EXISTS "Lyric" ("id" integer NOT NULL,"json" text NOT NULL,"updatedAt" integer NOT NULL, PRIMARY KEY (id));
|
||||
CREATE TABLE IF NOT EXISTS "Playlist" ("id" integer NOT NULL,"json" text NOT NULL,"updatedAt" int NOT NULL, PRIMARY KEY (id));
|
||||
CREATE TABLE IF NOT EXISTS "Track" ("id" integer NOT NULL,"json" text NOT NULL,"updatedAt" int NOT NULL, PRIMARY KEY (id));
|
||||
-- CreateTable
|
||||
CREATE TABLE "AccountData" IF NOT EXISTS (
|
||||
"id" TEXT NOT NULL PRIMARY KEY,
|
||||
"json" TEXT NOT NULL,
|
||||
"updatedAt" DATETIME NOT NULL
|
||||
);
|
||||
|
||||
CREATE TABLE IF NOT EXISTS "AppData" ("id" text NOT NULL,"value" text, PRIMARY KEY (id));
|
||||
CREATE TABLE IF NOT EXISTS "CoverColor" ("id" integer NOT NULL,"color" text NOT NULL, "queriedAt" int NOT NULL, PRIMARY KEY (id));
|
||||
CREATE TABLE IF NOT EXISTS "Audio" ("id" integer NOT NULL,"br" int NOT NULL,"type" text NOT NULL,"source" text NOT NULL, "queriedAt" int NOT NULL, PRIMARY KEY (id));
|
||||
CREATE TABLE IF NOT EXISTS "AppleMusicArtist" ("id" integer NOT NULL,"json" text NOT NULL,"updatedAt" int NOT NULL, PRIMARY KEY (id));
|
||||
CREATE TABLE IF NOT EXISTS "AppleMusicAlbum" ("id" integer NOT NULL,"json" text NOT NULL,"updatedAt" int NOT NULL, PRIMARY KEY (id));
|
||||
-- CreateTable
|
||||
CREATE TABLE "AppData" IF NOT EXISTS (
|
||||
"id" TEXT NOT NULL PRIMARY KEY,
|
||||
"value" TEXT NOT NULL
|
||||
);
|
||||
|
||||
-- CreateTable
|
||||
CREATE TABLE "Track" IF NOT EXISTS (
|
||||
"id" INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT,
|
||||
"json" TEXT NOT NULL,
|
||||
"createdAt" DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
"updatedAt" DATETIME NOT NULL
|
||||
);
|
||||
|
||||
-- CreateTable
|
||||
CREATE TABLE "Album" IF NOT EXISTS (
|
||||
"id" INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT,
|
||||
"json" TEXT NOT NULL,
|
||||
"createdAt" DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
"updatedAt" DATETIME NOT NULL
|
||||
);
|
||||
|
||||
-- CreateTable
|
||||
CREATE TABLE "Artist" IF NOT EXISTS (
|
||||
"id" INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT,
|
||||
"json" TEXT NOT NULL,
|
||||
"createdAt" DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
"updatedAt" DATETIME NOT NULL
|
||||
);
|
||||
|
||||
-- CreateTable
|
||||
CREATE TABLE "ArtistAlbum" IF NOT EXISTS (
|
||||
"id" INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT,
|
||||
"hotAlbums" TEXT NOT NULL,
|
||||
"createdAt" DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
"updatedAt" DATETIME NOT NULL
|
||||
);
|
||||
|
||||
-- CreateTable
|
||||
CREATE TABLE "Playlist" IF NOT EXISTS (
|
||||
"id" INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT,
|
||||
"json" TEXT NOT NULL,
|
||||
"createdAt" DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
"updatedAt" DATETIME NOT NULL
|
||||
);
|
||||
|
||||
-- CreateTable
|
||||
CREATE TABLE "Audio" IF NOT EXISTS (
|
||||
"id" INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT,
|
||||
"bitRate" INTEGER NOT NULL,
|
||||
"format" TEXT NOT NULL,
|
||||
"source" TEXT NOT NULL,
|
||||
"createdAt" DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
"updatedAt" DATETIME NOT NULL,
|
||||
"queriedAt" DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP
|
||||
);
|
||||
|
||||
-- CreateTable
|
||||
CREATE TABLE "Lyrics" IF NOT EXISTS (
|
||||
"id" INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT,
|
||||
"json" TEXT NOT NULL,
|
||||
"createdAt" DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
"updatedAt" DATETIME NOT NULL
|
||||
);
|
||||
|
||||
-- CreateTable
|
||||
CREATE TABLE "AppleMusicAlbum" IF NOT EXISTS (
|
||||
"id" INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT,
|
||||
"json" TEXT NOT NULL,
|
||||
"createdAt" DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
"updatedAt" DATETIME NOT NULL
|
||||
);
|
||||
|
||||
-- CreateTable
|
||||
CREATE TABLE "AppleMusicArtist" IF NOT EXISTS (
|
||||
"id" INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT,
|
||||
"json" TEXT NOT NULL,
|
||||
"createdAt" DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
"updatedAt" DATETIME NOT NULL
|
||||
);
|
||||
|
||||
-- CreateIndex
|
||||
CREATE UNIQUE INDEX IF NOT EXISTS "AccountData_id_key" ON "AccountData"("id");
|
||||
|
||||
-- CreateIndex
|
||||
CREATE UNIQUE INDEX IF NOT EXISTS "AppData_id_key" ON "AppData"("id");
|
||||
|
||||
-- CreateIndex
|
||||
CREATE UNIQUE INDEX IF NOT EXISTS "Track_id_key" ON "Track"("id");
|
||||
|
||||
-- CreateIndex
|
||||
CREATE UNIQUE INDEX IF NOT EXISTS "Album_id_key" ON "Album"("id");
|
||||
|
||||
-- CreateIndex
|
||||
CREATE UNIQUE INDEX IF NOT EXISTS "Artist_id_key" ON "Artist"("id");
|
||||
|
||||
-- CreateIndex
|
||||
CREATE UNIQUE INDEX IF NOT EXISTS "ArtistAlbum_id_key" ON "ArtistAlbum"("id");
|
||||
|
||||
-- CreateIndex
|
||||
CREATE UNIQUE INDEX IF NOT EXISTS "Playlist_id_key" ON "Playlist"("id");
|
||||
|
||||
-- CreateIndex
|
||||
CREATE UNIQUE INDEX IF NOT EXISTS "Audio_id_key" ON "Audio"("id");
|
||||
|
||||
-- CreateIndex
|
||||
CREATE UNIQUE INDEX IF NOT EXISTS "Lyrics_id_key" ON "Lyrics"("id");
|
||||
|
||||
-- CreateIndex
|
||||
CREATE UNIQUE INDEX IF NOT EXISTS "AppleMusicAlbum_id_key" ON "AppleMusicAlbum"("id");
|
||||
|
||||
-- CreateIndex
|
||||
CREATE UNIQUE INDEX IF NOT EXISTS "AppleMusicArtist_id_key" ON "AppleMusicArtist"("id");
|
||||
|
|
|
|||
|
|
@ -14,7 +14,9 @@
|
|||
"test:types": "tsc --noEmit --project ./tsconfig.json",
|
||||
"test": "vitest",
|
||||
"test:ui": "vitest --ui",
|
||||
"test:coverage": "vitest run --coverage"
|
||||
"test:coverage": "vitest run --coverage",
|
||||
"prisma:generate": "prisma generate",
|
||||
"prisma:db-push": "prisma db push"
|
||||
},
|
||||
"engines": {
|
||||
"node": "^14.13.1 || >=16.0.0"
|
||||
|
|
@ -22,26 +24,22 @@
|
|||
"dependencies": {
|
||||
"@fastify/cookie": "^8.3.0",
|
||||
"@fastify/http-proxy": "^8.4.0",
|
||||
"@fastify/multipart": "^7.4.0",
|
||||
"@prisma/client": "^4.8.1",
|
||||
"@prisma/engines": "^4.9.0",
|
||||
"@sentry/electron": "^3.0.7",
|
||||
"NeteaseCloudMusicApi": "^4.8.7",
|
||||
"better-sqlite3": "8.0.1",
|
||||
"@yimura/scraper": "^1.2.4",
|
||||
"NeteaseCloudMusicApi": "^4.8.9",
|
||||
"change-case": "^4.1.2",
|
||||
"compare-versions": "^4.1.3",
|
||||
"connect-history-api-fallback": "^2.0.0",
|
||||
"cookie-parser": "^1.4.6",
|
||||
"electron-log": "^4.4.8",
|
||||
"electron-store": "^8.1.0",
|
||||
"express": "^4.18.2",
|
||||
"fast-folder-size": "^1.7.1",
|
||||
"pretty-bytes": "^6.0.0",
|
||||
"type-fest": "^3.5.0",
|
||||
"zx": "^7.1.1"
|
||||
"prisma": "^4.8.1",
|
||||
"ytdl-core": "^4.11.2"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/better-sqlite3": "^7.6.3",
|
||||
"@types/cookie-parser": "^1.4.3",
|
||||
"@types/express": "^4.17.15",
|
||||
"@types/express-fileupload": "^1.2.3",
|
||||
"@vitest/ui": "^0.20.3",
|
||||
"axios": "^1.2.1",
|
||||
"cross-env": "^7.0.3",
|
||||
|
|
@ -52,7 +50,6 @@
|
|||
"electron-rebuild": "^3.2.9",
|
||||
"electron-releases": "^3.1171.0",
|
||||
"esbuild": "^0.16.10",
|
||||
"express-fileupload": "^1.4.0",
|
||||
"minimist": "^1.2.7",
|
||||
"music-metadata": "^8.1.0",
|
||||
"open-cli": "^7.1.0",
|
||||
|
|
|
|||
206
packages/desktop/prisma/client/index-browser.js
Normal file
206
packages/desktop/prisma/client/index-browser.js
Normal file
|
|
@ -0,0 +1,206 @@
|
|||
|
||||
Object.defineProperty(exports, "__esModule", { value: true });
|
||||
|
||||
const {
|
||||
Decimal,
|
||||
objectEnumValues,
|
||||
makeStrictEnum
|
||||
} = require('./runtime/index-browser')
|
||||
|
||||
|
||||
const Prisma = {}
|
||||
|
||||
exports.Prisma = Prisma
|
||||
|
||||
/**
|
||||
* Prisma Client JS version: 4.8.1
|
||||
* Query Engine version: d6e67a83f971b175a593ccc12e15c4a757f93ffe
|
||||
*/
|
||||
Prisma.prismaVersion = {
|
||||
client: "4.8.1",
|
||||
engine: "d6e67a83f971b175a593ccc12e15c4a757f93ffe"
|
||||
}
|
||||
|
||||
Prisma.PrismaClientKnownRequestError = () => {
|
||||
throw new Error(`PrismaClientKnownRequestError is unable to be run in the browser.
|
||||
In case this error is unexpected for you, please report it in https://github.com/prisma/prisma/issues`,
|
||||
)};
|
||||
Prisma.PrismaClientUnknownRequestError = () => {
|
||||
throw new Error(`PrismaClientUnknownRequestError is unable to be run in the browser.
|
||||
In case this error is unexpected for you, please report it in https://github.com/prisma/prisma/issues`,
|
||||
)}
|
||||
Prisma.PrismaClientRustPanicError = () => {
|
||||
throw new Error(`PrismaClientRustPanicError is unable to be run in the browser.
|
||||
In case this error is unexpected for you, please report it in https://github.com/prisma/prisma/issues`,
|
||||
)}
|
||||
Prisma.PrismaClientInitializationError = () => {
|
||||
throw new Error(`PrismaClientInitializationError is unable to be run in the browser.
|
||||
In case this error is unexpected for you, please report it in https://github.com/prisma/prisma/issues`,
|
||||
)}
|
||||
Prisma.PrismaClientValidationError = () => {
|
||||
throw new Error(`PrismaClientValidationError is unable to be run in the browser.
|
||||
In case this error is unexpected for you, please report it in https://github.com/prisma/prisma/issues`,
|
||||
)}
|
||||
Prisma.NotFoundError = () => {
|
||||
throw new Error(`NotFoundError is unable to be run in the browser.
|
||||
In case this error is unexpected for you, please report it in https://github.com/prisma/prisma/issues`,
|
||||
)}
|
||||
Prisma.Decimal = Decimal
|
||||
|
||||
/**
|
||||
* Re-export of sql-template-tag
|
||||
*/
|
||||
Prisma.sql = () => {
|
||||
throw new Error(`sqltag is unable to be run in the browser.
|
||||
In case this error is unexpected for you, please report it in https://github.com/prisma/prisma/issues`,
|
||||
)}
|
||||
Prisma.empty = () => {
|
||||
throw new Error(`empty is unable to be run in the browser.
|
||||
In case this error is unexpected for you, please report it in https://github.com/prisma/prisma/issues`,
|
||||
)}
|
||||
Prisma.join = () => {
|
||||
throw new Error(`join is unable to be run in the browser.
|
||||
In case this error is unexpected for you, please report it in https://github.com/prisma/prisma/issues`,
|
||||
)}
|
||||
Prisma.raw = () => {
|
||||
throw new Error(`raw is unable to be run in the browser.
|
||||
In case this error is unexpected for you, please report it in https://github.com/prisma/prisma/issues`,
|
||||
)}
|
||||
Prisma.validator = () => (val) => val
|
||||
|
||||
|
||||
/**
|
||||
* Shorthand utilities for JSON filtering
|
||||
*/
|
||||
Prisma.DbNull = objectEnumValues.instances.DbNull
|
||||
Prisma.JsonNull = objectEnumValues.instances.JsonNull
|
||||
Prisma.AnyNull = objectEnumValues.instances.AnyNull
|
||||
|
||||
Prisma.NullTypes = {
|
||||
DbNull: objectEnumValues.classes.DbNull,
|
||||
JsonNull: objectEnumValues.classes.JsonNull,
|
||||
AnyNull: objectEnumValues.classes.AnyNull
|
||||
}
|
||||
|
||||
/**
|
||||
* Enums
|
||||
*/
|
||||
// Based on
|
||||
// https://github.com/microsoft/TypeScript/issues/3192#issuecomment-261720275
|
||||
function makeEnum(x) { return x; }
|
||||
|
||||
exports.Prisma.AccountDataScalarFieldEnum = makeEnum({
|
||||
id: 'id',
|
||||
json: 'json',
|
||||
updatedAt: 'updatedAt'
|
||||
});
|
||||
|
||||
exports.Prisma.AlbumScalarFieldEnum = makeEnum({
|
||||
id: 'id',
|
||||
json: 'json',
|
||||
createdAt: 'createdAt',
|
||||
updatedAt: 'updatedAt'
|
||||
});
|
||||
|
||||
exports.Prisma.AppDataScalarFieldEnum = makeEnum({
|
||||
id: 'id',
|
||||
value: 'value'
|
||||
});
|
||||
|
||||
exports.Prisma.AppleMusicAlbumScalarFieldEnum = makeEnum({
|
||||
id: 'id',
|
||||
json: 'json',
|
||||
createdAt: 'createdAt',
|
||||
updatedAt: 'updatedAt'
|
||||
});
|
||||
|
||||
exports.Prisma.AppleMusicArtistScalarFieldEnum = makeEnum({
|
||||
id: 'id',
|
||||
json: 'json',
|
||||
createdAt: 'createdAt',
|
||||
updatedAt: 'updatedAt'
|
||||
});
|
||||
|
||||
exports.Prisma.ArtistAlbumScalarFieldEnum = makeEnum({
|
||||
id: 'id',
|
||||
hotAlbums: 'hotAlbums',
|
||||
createdAt: 'createdAt',
|
||||
updatedAt: 'updatedAt'
|
||||
});
|
||||
|
||||
exports.Prisma.ArtistScalarFieldEnum = makeEnum({
|
||||
id: 'id',
|
||||
json: 'json',
|
||||
createdAt: 'createdAt',
|
||||
updatedAt: 'updatedAt'
|
||||
});
|
||||
|
||||
exports.Prisma.AudioScalarFieldEnum = makeEnum({
|
||||
id: 'id',
|
||||
bitRate: 'bitRate',
|
||||
format: 'format',
|
||||
source: 'source',
|
||||
createdAt: 'createdAt',
|
||||
updatedAt: 'updatedAt',
|
||||
queriedAt: 'queriedAt'
|
||||
});
|
||||
|
||||
exports.Prisma.LyricsScalarFieldEnum = makeEnum({
|
||||
id: 'id',
|
||||
json: 'json',
|
||||
createdAt: 'createdAt',
|
||||
updatedAt: 'updatedAt'
|
||||
});
|
||||
|
||||
exports.Prisma.PlaylistScalarFieldEnum = makeEnum({
|
||||
id: 'id',
|
||||
json: 'json',
|
||||
createdAt: 'createdAt',
|
||||
updatedAt: 'updatedAt'
|
||||
});
|
||||
|
||||
exports.Prisma.SortOrder = makeEnum({
|
||||
asc: 'asc',
|
||||
desc: 'desc'
|
||||
});
|
||||
|
||||
exports.Prisma.TrackScalarFieldEnum = makeEnum({
|
||||
id: 'id',
|
||||
json: 'json',
|
||||
createdAt: 'createdAt',
|
||||
updatedAt: 'updatedAt'
|
||||
});
|
||||
|
||||
exports.Prisma.TransactionIsolationLevel = makeStrictEnum({
|
||||
Serializable: 'Serializable'
|
||||
});
|
||||
|
||||
|
||||
exports.Prisma.ModelName = makeEnum({
|
||||
AccountData: 'AccountData',
|
||||
AppData: 'AppData',
|
||||
Track: 'Track',
|
||||
Album: 'Album',
|
||||
Artist: 'Artist',
|
||||
ArtistAlbum: 'ArtistAlbum',
|
||||
Playlist: 'Playlist',
|
||||
Audio: 'Audio',
|
||||
Lyrics: 'Lyrics',
|
||||
AppleMusicAlbum: 'AppleMusicAlbum',
|
||||
AppleMusicArtist: 'AppleMusicArtist'
|
||||
});
|
||||
|
||||
/**
|
||||
* Create the Client
|
||||
*/
|
||||
class PrismaClient {
|
||||
constructor() {
|
||||
throw new Error(
|
||||
`PrismaClient is unable to be run in the browser.
|
||||
In case this error is unexpected for you, please report it in https://github.com/prisma/prisma/issues`,
|
||||
)
|
||||
}
|
||||
}
|
||||
exports.PrismaClient = PrismaClient
|
||||
|
||||
Object.assign(exports, Prisma)
|
||||
12729
packages/desktop/prisma/client/index.d.ts
vendored
Normal file
12729
packages/desktop/prisma/client/index.d.ts
vendored
Normal file
File diff suppressed because it is too large
Load diff
271
packages/desktop/prisma/client/index.js
Normal file
271
packages/desktop/prisma/client/index.js
Normal file
File diff suppressed because one or more lines are too long
BIN
packages/desktop/prisma/client/libquery_engine-darwin-arm64.dylib.node
Executable file
BIN
packages/desktop/prisma/client/libquery_engine-darwin-arm64.dylib.node
Executable file
Binary file not shown.
BIN
packages/desktop/prisma/client/libquery_engine-darwin.dylib.node
Executable file
BIN
packages/desktop/prisma/client/libquery_engine-darwin.dylib.node
Executable file
Binary file not shown.
6
packages/desktop/prisma/client/package.json
Normal file
6
packages/desktop/prisma/client/package.json
Normal file
|
|
@ -0,0 +1,6 @@
|
|||
{
|
||||
"name": ".prisma/client",
|
||||
"main": "index.js",
|
||||
"types": "index.d.ts",
|
||||
"browser": "index-browser.js"
|
||||
}
|
||||
98
packages/desktop/prisma/client/runtime/edge-esm.js
Normal file
98
packages/desktop/prisma/client/runtime/edge-esm.js
Normal file
File diff suppressed because one or more lines are too long
98
packages/desktop/prisma/client/runtime/edge.js
Normal file
98
packages/desktop/prisma/client/runtime/edge.js
Normal file
File diff suppressed because one or more lines are too long
323
packages/desktop/prisma/client/runtime/index-browser.d.ts
vendored
Normal file
323
packages/desktop/prisma/client/runtime/index-browser.d.ts
vendored
Normal file
|
|
@ -0,0 +1,323 @@
|
|||
declare class AnyNull extends NullTypesEnumValue {
|
||||
}
|
||||
|
||||
declare class DbNull extends NullTypesEnumValue {
|
||||
}
|
||||
|
||||
export declare namespace Decimal {
|
||||
export type Constructor = typeof Decimal;
|
||||
export type Instance = Decimal;
|
||||
export type Rounding = 0 | 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8;
|
||||
export type Modulo = Rounding | 9;
|
||||
export type Value = string | number | Decimal;
|
||||
|
||||
// http://mikemcl.github.io/decimal.js/#constructor-properties
|
||||
export interface Config {
|
||||
precision?: number;
|
||||
rounding?: Rounding;
|
||||
toExpNeg?: number;
|
||||
toExpPos?: number;
|
||||
minE?: number;
|
||||
maxE?: number;
|
||||
crypto?: boolean;
|
||||
modulo?: Modulo;
|
||||
defaults?: boolean;
|
||||
}
|
||||
}
|
||||
|
||||
export declare class Decimal {
|
||||
readonly d: number[];
|
||||
readonly e: number;
|
||||
readonly s: number;
|
||||
private readonly toStringTag: string;
|
||||
|
||||
constructor(n: Decimal.Value);
|
||||
|
||||
absoluteValue(): Decimal;
|
||||
abs(): Decimal;
|
||||
|
||||
ceil(): Decimal;
|
||||
|
||||
clampedTo(min: Decimal.Value, max: Decimal.Value): Decimal;
|
||||
clamp(min: Decimal.Value, max: Decimal.Value): Decimal;
|
||||
|
||||
comparedTo(n: Decimal.Value): number;
|
||||
cmp(n: Decimal.Value): number;
|
||||
|
||||
cosine(): Decimal;
|
||||
cos(): Decimal;
|
||||
|
||||
cubeRoot(): Decimal;
|
||||
cbrt(): Decimal;
|
||||
|
||||
decimalPlaces(): number;
|
||||
dp(): number;
|
||||
|
||||
dividedBy(n: Decimal.Value): Decimal;
|
||||
div(n: Decimal.Value): Decimal;
|
||||
|
||||
dividedToIntegerBy(n: Decimal.Value): Decimal;
|
||||
divToInt(n: Decimal.Value): Decimal;
|
||||
|
||||
equals(n: Decimal.Value): boolean;
|
||||
eq(n: Decimal.Value): boolean;
|
||||
|
||||
floor(): Decimal;
|
||||
|
||||
greaterThan(n: Decimal.Value): boolean;
|
||||
gt(n: Decimal.Value): boolean;
|
||||
|
||||
greaterThanOrEqualTo(n: Decimal.Value): boolean;
|
||||
gte(n: Decimal.Value): boolean;
|
||||
|
||||
hyperbolicCosine(): Decimal;
|
||||
cosh(): Decimal;
|
||||
|
||||
hyperbolicSine(): Decimal;
|
||||
sinh(): Decimal;
|
||||
|
||||
hyperbolicTangent(): Decimal;
|
||||
tanh(): Decimal;
|
||||
|
||||
inverseCosine(): Decimal;
|
||||
acos(): Decimal;
|
||||
|
||||
inverseHyperbolicCosine(): Decimal;
|
||||
acosh(): Decimal;
|
||||
|
||||
inverseHyperbolicSine(): Decimal;
|
||||
asinh(): Decimal;
|
||||
|
||||
inverseHyperbolicTangent(): Decimal;
|
||||
atanh(): Decimal;
|
||||
|
||||
inverseSine(): Decimal;
|
||||
asin(): Decimal;
|
||||
|
||||
inverseTangent(): Decimal;
|
||||
atan(): Decimal;
|
||||
|
||||
isFinite(): boolean;
|
||||
|
||||
isInteger(): boolean;
|
||||
isInt(): boolean;
|
||||
|
||||
isNaN(): boolean;
|
||||
|
||||
isNegative(): boolean;
|
||||
isNeg(): boolean;
|
||||
|
||||
isPositive(): boolean;
|
||||
isPos(): boolean;
|
||||
|
||||
isZero(): boolean;
|
||||
|
||||
lessThan(n: Decimal.Value): boolean;
|
||||
lt(n: Decimal.Value): boolean;
|
||||
|
||||
lessThanOrEqualTo(n: Decimal.Value): boolean;
|
||||
lte(n: Decimal.Value): boolean;
|
||||
|
||||
logarithm(n?: Decimal.Value): Decimal;
|
||||
log(n?: Decimal.Value): Decimal;
|
||||
|
||||
minus(n: Decimal.Value): Decimal;
|
||||
sub(n: Decimal.Value): Decimal;
|
||||
|
||||
modulo(n: Decimal.Value): Decimal;
|
||||
mod(n: Decimal.Value): Decimal;
|
||||
|
||||
naturalExponential(): Decimal;
|
||||
exp(): Decimal;
|
||||
|
||||
naturalLogarithm(): Decimal;
|
||||
ln(): Decimal;
|
||||
|
||||
negated(): Decimal;
|
||||
neg(): Decimal;
|
||||
|
||||
plus(n: Decimal.Value): Decimal;
|
||||
add(n: Decimal.Value): Decimal;
|
||||
|
||||
precision(includeZeros?: boolean): number;
|
||||
sd(includeZeros?: boolean): number;
|
||||
|
||||
round(): Decimal;
|
||||
|
||||
sine() : Decimal;
|
||||
sin() : Decimal;
|
||||
|
||||
squareRoot(): Decimal;
|
||||
sqrt(): Decimal;
|
||||
|
||||
tangent() : Decimal;
|
||||
tan() : Decimal;
|
||||
|
||||
times(n: Decimal.Value): Decimal;
|
||||
mul(n: Decimal.Value) : Decimal;
|
||||
|
||||
toBinary(significantDigits?: number): string;
|
||||
toBinary(significantDigits: number, rounding: Decimal.Rounding): string;
|
||||
|
||||
toDecimalPlaces(decimalPlaces?: number): Decimal;
|
||||
toDecimalPlaces(decimalPlaces: number, rounding: Decimal.Rounding): Decimal;
|
||||
toDP(decimalPlaces?: number): Decimal;
|
||||
toDP(decimalPlaces: number, rounding: Decimal.Rounding): Decimal;
|
||||
|
||||
toExponential(decimalPlaces?: number): string;
|
||||
toExponential(decimalPlaces: number, rounding: Decimal.Rounding): string;
|
||||
|
||||
toFixed(decimalPlaces?: number): string;
|
||||
toFixed(decimalPlaces: number, rounding: Decimal.Rounding): string;
|
||||
|
||||
toFraction(max_denominator?: Decimal.Value): Decimal[];
|
||||
|
||||
toHexadecimal(significantDigits?: number): string;
|
||||
toHexadecimal(significantDigits: number, rounding: Decimal.Rounding): string;
|
||||
toHex(significantDigits?: number): string;
|
||||
toHex(significantDigits: number, rounding?: Decimal.Rounding): string;
|
||||
|
||||
toJSON(): string;
|
||||
|
||||
toNearest(n: Decimal.Value, rounding?: Decimal.Rounding): Decimal;
|
||||
|
||||
toNumber(): number;
|
||||
|
||||
toOctal(significantDigits?: number): string;
|
||||
toOctal(significantDigits: number, rounding: Decimal.Rounding): string;
|
||||
|
||||
toPower(n: Decimal.Value): Decimal;
|
||||
pow(n: Decimal.Value): Decimal;
|
||||
|
||||
toPrecision(significantDigits?: number): string;
|
||||
toPrecision(significantDigits: number, rounding: Decimal.Rounding): string;
|
||||
|
||||
toSignificantDigits(significantDigits?: number): Decimal;
|
||||
toSignificantDigits(significantDigits: number, rounding: Decimal.Rounding): Decimal;
|
||||
toSD(significantDigits?: number): Decimal;
|
||||
toSD(significantDigits: number, rounding: Decimal.Rounding): Decimal;
|
||||
|
||||
toString(): string;
|
||||
|
||||
truncated(): Decimal;
|
||||
trunc(): Decimal;
|
||||
|
||||
valueOf(): string;
|
||||
|
||||
static abs(n: Decimal.Value): Decimal;
|
||||
static acos(n: Decimal.Value): Decimal;
|
||||
static acosh(n: Decimal.Value): Decimal;
|
||||
static add(x: Decimal.Value, y: Decimal.Value): Decimal;
|
||||
static asin(n: Decimal.Value): Decimal;
|
||||
static asinh(n: Decimal.Value): Decimal;
|
||||
static atan(n: Decimal.Value): Decimal;
|
||||
static atanh(n: Decimal.Value): Decimal;
|
||||
static atan2(y: Decimal.Value, x: Decimal.Value): Decimal;
|
||||
static cbrt(n: Decimal.Value): Decimal;
|
||||
static ceil(n: Decimal.Value): Decimal;
|
||||
static clamp(n: Decimal.Value, min: Decimal.Value, max: Decimal.Value): Decimal;
|
||||
static clone(object?: Decimal.Config): Decimal.Constructor;
|
||||
static config(object: Decimal.Config): Decimal.Constructor;
|
||||
static cos(n: Decimal.Value): Decimal;
|
||||
static cosh(n: Decimal.Value): Decimal;
|
||||
static div(x: Decimal.Value, y: Decimal.Value): Decimal;
|
||||
static exp(n: Decimal.Value): Decimal;
|
||||
static floor(n: Decimal.Value): Decimal;
|
||||
static hypot(...n: Decimal.Value[]): Decimal;
|
||||
static isDecimal(object: any): object is Decimal;
|
||||
static ln(n: Decimal.Value): Decimal;
|
||||
static log(n: Decimal.Value, base?: Decimal.Value): Decimal;
|
||||
static log2(n: Decimal.Value): Decimal;
|
||||
static log10(n: Decimal.Value): Decimal;
|
||||
static max(...n: Decimal.Value[]): Decimal;
|
||||
static min(...n: Decimal.Value[]): Decimal;
|
||||
static mod(x: Decimal.Value, y: Decimal.Value): Decimal;
|
||||
static mul(x: Decimal.Value, y: Decimal.Value): Decimal;
|
||||
static noConflict(): Decimal.Constructor; // Browser only
|
||||
static pow(base: Decimal.Value, exponent: Decimal.Value): Decimal;
|
||||
static random(significantDigits?: number): Decimal;
|
||||
static round(n: Decimal.Value): Decimal;
|
||||
static set(object: Decimal.Config): Decimal.Constructor;
|
||||
static sign(n: Decimal.Value): number;
|
||||
static sin(n: Decimal.Value): Decimal;
|
||||
static sinh(n: Decimal.Value): Decimal;
|
||||
static sqrt(n: Decimal.Value): Decimal;
|
||||
static sub(x: Decimal.Value, y: Decimal.Value): Decimal;
|
||||
static sum(...n: Decimal.Value[]): Decimal;
|
||||
static tan(n: Decimal.Value): Decimal;
|
||||
static tanh(n: Decimal.Value): Decimal;
|
||||
static trunc(n: Decimal.Value): Decimal;
|
||||
|
||||
static readonly default?: Decimal.Constructor;
|
||||
static readonly Decimal?: Decimal.Constructor;
|
||||
|
||||
static readonly precision: number;
|
||||
static readonly rounding: Decimal.Rounding;
|
||||
static readonly toExpNeg: number;
|
||||
static readonly toExpPos: number;
|
||||
static readonly minE: number;
|
||||
static readonly maxE: number;
|
||||
static readonly crypto: boolean;
|
||||
static readonly modulo: Decimal.Modulo;
|
||||
|
||||
static readonly ROUND_UP: 0;
|
||||
static readonly ROUND_DOWN: 1;
|
||||
static readonly ROUND_CEIL: 2;
|
||||
static readonly ROUND_FLOOR: 3;
|
||||
static readonly ROUND_HALF_UP: 4;
|
||||
static readonly ROUND_HALF_DOWN: 5;
|
||||
static readonly ROUND_HALF_EVEN: 6;
|
||||
static readonly ROUND_HALF_CEIL: 7;
|
||||
static readonly ROUND_HALF_FLOOR: 8;
|
||||
static readonly EUCLID: 9;
|
||||
}
|
||||
|
||||
declare class JsonNull extends NullTypesEnumValue {
|
||||
}
|
||||
|
||||
/**
|
||||
* Generates more strict variant of an enum which, unlike regular enum,
|
||||
* throws on non-existing property access. This can be useful in following situations:
|
||||
* - we have an API, that accepts both `undefined` and `SomeEnumType` as an input
|
||||
* - enum values are generated dynamically from DMMF.
|
||||
*
|
||||
* In that case, if using normal enums and no compile-time typechecking, using non-existing property
|
||||
* will result in `undefined` value being used, which will be accepted. Using strict enum
|
||||
* in this case will help to have a runtime exception, telling you that you are probably doing something wrong.
|
||||
*
|
||||
* Note: if you need to check for existence of a value in the enum you can still use either
|
||||
* `in` operator or `hasOwnProperty` function.
|
||||
*
|
||||
* @param definition
|
||||
* @returns
|
||||
*/
|
||||
export declare function makeStrictEnum<T extends Record<PropertyKey, string | number>>(definition: T): T;
|
||||
|
||||
declare class NullTypesEnumValue extends ObjectEnumValue {
|
||||
_getNamespace(): string;
|
||||
}
|
||||
|
||||
/**
|
||||
* Base class for unique values of object-valued enums.
|
||||
*/
|
||||
declare abstract class ObjectEnumValue {
|
||||
constructor(arg?: symbol);
|
||||
abstract _getNamespace(): string;
|
||||
_getName(): string;
|
||||
toString(): string;
|
||||
}
|
||||
|
||||
export declare const objectEnumValues: {
|
||||
classes: {
|
||||
DbNull: typeof DbNull;
|
||||
JsonNull: typeof JsonNull;
|
||||
AnyNull: typeof AnyNull;
|
||||
};
|
||||
instances: {
|
||||
DbNull: DbNull;
|
||||
JsonNull: JsonNull;
|
||||
AnyNull: AnyNull;
|
||||
};
|
||||
};
|
||||
|
||||
export { }
|
||||
2479
packages/desktop/prisma/client/runtime/index-browser.js
Normal file
2479
packages/desktop/prisma/client/runtime/index-browser.js
Normal file
File diff suppressed because it is too large
Load diff
2075
packages/desktop/prisma/client/runtime/index.d.ts
vendored
Normal file
2075
packages/desktop/prisma/client/runtime/index.d.ts
vendored
Normal file
File diff suppressed because it is too large
Load diff
36419
packages/desktop/prisma/client/runtime/index.js
Normal file
36419
packages/desktop/prisma/client/runtime/index.js
Normal file
File diff suppressed because one or more lines are too long
87
packages/desktop/prisma/client/schema.prisma
Normal file
87
packages/desktop/prisma/client/schema.prisma
Normal file
|
|
@ -0,0 +1,87 @@
|
|||
generator client {
|
||||
provider = "prisma-client-js"
|
||||
output = "./client"
|
||||
binaryTargets = ["native", "darwin", "darwin-arm64"]
|
||||
}
|
||||
|
||||
datasource db {
|
||||
provider = "sqlite"
|
||||
url = env("DATABASE_URL")
|
||||
}
|
||||
|
||||
model AccountData {
|
||||
id String @id @unique
|
||||
json String
|
||||
updatedAt DateTime @updatedAt
|
||||
}
|
||||
|
||||
model AppData {
|
||||
id String @id @unique
|
||||
value String
|
||||
}
|
||||
|
||||
model Track {
|
||||
id Int @id @unique
|
||||
json String
|
||||
createdAt DateTime @default(now())
|
||||
updatedAt DateTime @updatedAt
|
||||
}
|
||||
|
||||
model Album {
|
||||
id Int @id @unique
|
||||
json String
|
||||
createdAt DateTime @default(now())
|
||||
updatedAt DateTime @updatedAt
|
||||
}
|
||||
|
||||
model Artist {
|
||||
id Int @id @unique
|
||||
json String
|
||||
createdAt DateTime @default(now())
|
||||
updatedAt DateTime @updatedAt
|
||||
}
|
||||
|
||||
model ArtistAlbum {
|
||||
id Int @id @unique
|
||||
hotAlbums String
|
||||
createdAt DateTime @default(now())
|
||||
updatedAt DateTime @updatedAt
|
||||
}
|
||||
|
||||
model Playlist {
|
||||
id Int @id @unique
|
||||
json String
|
||||
createdAt DateTime @default(now())
|
||||
updatedAt DateTime @updatedAt
|
||||
}
|
||||
|
||||
model Audio {
|
||||
id Int @id @unique
|
||||
bitRate Int
|
||||
format String
|
||||
source String
|
||||
createdAt DateTime @default(now())
|
||||
updatedAt DateTime @updatedAt
|
||||
queriedAt DateTime @default(now())
|
||||
}
|
||||
|
||||
model Lyrics {
|
||||
id Int @id @unique
|
||||
json String
|
||||
createdAt DateTime @default(now())
|
||||
updatedAt DateTime @updatedAt
|
||||
}
|
||||
|
||||
model AppleMusicAlbum {
|
||||
id Int @id @unique
|
||||
json String
|
||||
createdAt DateTime @default(now())
|
||||
updatedAt DateTime @updatedAt
|
||||
}
|
||||
|
||||
model AppleMusicArtist {
|
||||
id Int @id @unique
|
||||
json String
|
||||
createdAt DateTime @default(now())
|
||||
updatedAt DateTime @updatedAt
|
||||
}
|
||||
BIN
packages/desktop/prisma/r3play.db
Normal file
BIN
packages/desktop/prisma/r3play.db
Normal file
Binary file not shown.
87
packages/desktop/prisma/schema.prisma
Normal file
87
packages/desktop/prisma/schema.prisma
Normal file
|
|
@ -0,0 +1,87 @@
|
|||
generator client {
|
||||
provider = "prisma-client-js"
|
||||
output = "./client"
|
||||
binaryTargets = ["native", "darwin", "darwin-arm64"]
|
||||
}
|
||||
|
||||
datasource db {
|
||||
provider = "sqlite"
|
||||
url = env("DATABASE_URL")
|
||||
}
|
||||
|
||||
model AccountData {
|
||||
id String @id @unique
|
||||
json String
|
||||
updatedAt DateTime @updatedAt
|
||||
}
|
||||
|
||||
model AppData {
|
||||
id String @id @unique
|
||||
value String
|
||||
}
|
||||
|
||||
model Track {
|
||||
id Int @id @unique
|
||||
json String
|
||||
createdAt DateTime @default(now())
|
||||
updatedAt DateTime @updatedAt
|
||||
}
|
||||
|
||||
model Album {
|
||||
id Int @id @unique
|
||||
json String
|
||||
createdAt DateTime @default(now())
|
||||
updatedAt DateTime @updatedAt
|
||||
}
|
||||
|
||||
model Artist {
|
||||
id Int @id @unique
|
||||
json String
|
||||
createdAt DateTime @default(now())
|
||||
updatedAt DateTime @updatedAt
|
||||
}
|
||||
|
||||
model ArtistAlbum {
|
||||
id Int @id @unique
|
||||
hotAlbums String
|
||||
createdAt DateTime @default(now())
|
||||
updatedAt DateTime @updatedAt
|
||||
}
|
||||
|
||||
model Playlist {
|
||||
id Int @id @unique
|
||||
json String
|
||||
createdAt DateTime @default(now())
|
||||
updatedAt DateTime @updatedAt
|
||||
}
|
||||
|
||||
model Audio {
|
||||
id Int @id @unique
|
||||
bitRate Int
|
||||
format String
|
||||
source String
|
||||
createdAt DateTime @default(now())
|
||||
updatedAt DateTime @updatedAt
|
||||
queriedAt DateTime @default(now())
|
||||
}
|
||||
|
||||
model Lyrics {
|
||||
id Int @id @unique
|
||||
json String
|
||||
createdAt DateTime @default(now())
|
||||
updatedAt DateTime @updatedAt
|
||||
}
|
||||
|
||||
model AppleMusicAlbum {
|
||||
id Int @id @unique
|
||||
json String
|
||||
createdAt DateTime @default(now())
|
||||
updatedAt DateTime @updatedAt
|
||||
}
|
||||
|
||||
model AppleMusicArtist {
|
||||
id Int @id @unique
|
||||
json String
|
||||
createdAt DateTime @default(now())
|
||||
updatedAt DateTime @updatedAt
|
||||
}
|
||||
|
|
@ -1,10 +0,0 @@
|
|||
import { expect, test, describe, vi } from 'vitest'
|
||||
import { pairDevice, scanDevices } from '../main/airplay'
|
||||
|
||||
// test('scanDevices', async () => {
|
||||
// console.log(await scanDevices())
|
||||
// }, 10000000)
|
||||
|
||||
// test('pairDevice', async () => {
|
||||
// console.log(await pairDevice('58:D3:49:F0:C9:71', 'airplay'))
|
||||
// }, 100000)
|
||||
|
|
@ -12,18 +12,11 @@ import {
|
|||
FetchUserLikedTracksIDsResponse,
|
||||
FetchUserPlaylistsResponse,
|
||||
} from './api/User'
|
||||
import {
|
||||
FetchAudioSourceResponse,
|
||||
FetchLyricResponse,
|
||||
FetchTracksResponse,
|
||||
} from './api/Track'
|
||||
import {
|
||||
FetchPlaylistResponse,
|
||||
FetchRecommendedPlaylistsResponse,
|
||||
} from './api/Playlists'
|
||||
import { FetchAudioSourceResponse, FetchLyricResponse, FetchTracksResponse } from './api/Track'
|
||||
import { FetchPlaylistResponse, FetchRecommendedPlaylistsResponse } from './api/Playlists'
|
||||
import { AppleMusicAlbum, AppleMusicArtist } from 'AppleMusic'
|
||||
|
||||
export const enum APIs {
|
||||
export enum CacheAPIs {
|
||||
Album = 'album',
|
||||
Artist = 'artists',
|
||||
ArtistAlbum = 'artist/album',
|
||||
|
|
@ -32,7 +25,7 @@ export const enum APIs {
|
|||
Personalized = 'personalized',
|
||||
Playlist = 'playlist/detail',
|
||||
RecommendResource = 'recommend/resource',
|
||||
SongUrl = 'song/url',
|
||||
SongUrl = 'song/url/v1',
|
||||
Track = 'song/detail',
|
||||
UserAccount = 'user/account',
|
||||
UserAlbums = 'album/sublist',
|
||||
|
|
@ -42,56 +35,53 @@ export const enum APIs {
|
|||
ListenedRecords = 'user/record',
|
||||
|
||||
// not netease api
|
||||
Artists = 'artistsNotNetease',
|
||||
CoverColor = 'cover_color',
|
||||
AppleMusicAlbum = 'apple_music_album',
|
||||
AppleMusicArtist = 'apple_music_artist',
|
||||
}
|
||||
|
||||
export interface APIsParams {
|
||||
[APIs.Album]: { id: number }
|
||||
[APIs.Artist]: { id: number }
|
||||
[APIs.ArtistAlbum]: { id: number }
|
||||
[APIs.Likelist]: void
|
||||
[APIs.Lyric]: { id: number }
|
||||
[APIs.Personalized]: void
|
||||
[APIs.Playlist]: { id: number }
|
||||
[APIs.RecommendResource]: void
|
||||
[APIs.SongUrl]: { id: string }
|
||||
[APIs.Track]: { ids: string }
|
||||
[APIs.UserAccount]: void
|
||||
[APIs.UserAlbums]: void
|
||||
[APIs.UserArtists]: void
|
||||
[APIs.UserPlaylist]: void
|
||||
[APIs.SimilarArtist]: { id: number }
|
||||
[APIs.ListenedRecords]: { id: number; type: number }
|
||||
export interface CacheAPIsParams {
|
||||
[CacheAPIs.Album]: { id: number }
|
||||
[CacheAPIs.Artist]: { id: number }
|
||||
[CacheAPIs.ArtistAlbum]: { id: number }
|
||||
[CacheAPIs.Likelist]: void
|
||||
[CacheAPIs.Lyric]: { id: number }
|
||||
[CacheAPIs.Personalized]: void
|
||||
[CacheAPIs.Playlist]: { id: number }
|
||||
[CacheAPIs.RecommendResource]: void
|
||||
[CacheAPIs.SongUrl]: { id: string }
|
||||
[CacheAPIs.Track]: { ids: string }
|
||||
[CacheAPIs.UserAccount]: void
|
||||
[CacheAPIs.UserAlbums]: void
|
||||
[CacheAPIs.UserArtists]: void
|
||||
[CacheAPIs.UserPlaylist]: void
|
||||
[CacheAPIs.SimilarArtist]: { id: number }
|
||||
[CacheAPIs.ListenedRecords]: { id: number; type: number }
|
||||
|
||||
[APIs.Artists]: { ids: number[] }
|
||||
[APIs.CoverColor]: { id: number }
|
||||
[APIs.AppleMusicAlbum]: { id: number }
|
||||
[APIs.AppleMusicArtist]: { id: number }
|
||||
[CacheAPIs.CoverColor]: { id: number }
|
||||
[CacheAPIs.AppleMusicAlbum]: { id: number }
|
||||
[CacheAPIs.AppleMusicArtist]: { id: number }
|
||||
}
|
||||
|
||||
export interface APIsResponse {
|
||||
[APIs.Album]: FetchAlbumResponse
|
||||
[APIs.Artist]: FetchArtistResponse
|
||||
[APIs.ArtistAlbum]: FetchArtistAlbumsResponse
|
||||
[APIs.Likelist]: FetchUserLikedTracksIDsResponse
|
||||
[APIs.Lyric]: FetchLyricResponse
|
||||
[APIs.Personalized]: FetchRecommendedPlaylistsResponse
|
||||
[APIs.Playlist]: FetchPlaylistResponse
|
||||
[APIs.RecommendResource]: FetchRecommendedPlaylistsResponse
|
||||
[APIs.SongUrl]: FetchAudioSourceResponse
|
||||
[APIs.Track]: FetchTracksResponse
|
||||
[APIs.UserAccount]: FetchUserAccountResponse
|
||||
[APIs.UserAlbums]: FetchUserAlbumsResponse
|
||||
[APIs.UserArtists]: FetchUserArtistsResponse
|
||||
[APIs.UserPlaylist]: FetchUserPlaylistsResponse
|
||||
[APIs.SimilarArtist]: FetchSimilarArtistsResponse
|
||||
[APIs.ListenedRecords]: FetchListenedRecordsResponse
|
||||
export interface CacheAPIsResponse {
|
||||
[CacheAPIs.Album]: FetchAlbumResponse
|
||||
[CacheAPIs.Artist]: FetchArtistResponse
|
||||
[CacheAPIs.ArtistAlbum]: FetchArtistAlbumsResponse
|
||||
[CacheAPIs.Likelist]: FetchUserLikedTracksIDsResponse
|
||||
[CacheAPIs.Lyric]: FetchLyricResponse
|
||||
[CacheAPIs.Personalized]: FetchRecommendedPlaylistsResponse
|
||||
[CacheAPIs.Playlist]: FetchPlaylistResponse
|
||||
[CacheAPIs.RecommendResource]: FetchRecommendedPlaylistsResponse
|
||||
[CacheAPIs.SongUrl]: FetchAudioSourceResponse
|
||||
[CacheAPIs.Track]: FetchTracksResponse
|
||||
[CacheAPIs.UserAccount]: FetchUserAccountResponse
|
||||
[CacheAPIs.UserAlbums]: FetchUserAlbumsResponse
|
||||
[CacheAPIs.UserArtists]: FetchUserArtistsResponse
|
||||
[CacheAPIs.UserPlaylist]: FetchUserPlaylistsResponse
|
||||
[CacheAPIs.SimilarArtist]: FetchSimilarArtistsResponse
|
||||
[CacheAPIs.ListenedRecords]: FetchListenedRecordsResponse
|
||||
|
||||
[APIs.Artists]: FetchArtistResponse[]
|
||||
[APIs.CoverColor]: string | undefined
|
||||
[APIs.AppleMusicAlbum]: AppleMusicAlbum | 'no'
|
||||
[APIs.AppleMusicArtist]: AppleMusicArtist | 'no'
|
||||
[CacheAPIs.CoverColor]: string | undefined
|
||||
[CacheAPIs.AppleMusicAlbum]: AppleMusicAlbum | 'no'
|
||||
[CacheAPIs.AppleMusicArtist]: AppleMusicArtist | 'no'
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,5 +1,5 @@
|
|||
import { AppleMusicAlbum, AppleMusicArtist } from './AppleMusic'
|
||||
import { APIs } from './CacheAPIs'
|
||||
import { CacheAPIs } from './CacheAPIs'
|
||||
import { RepeatMode } from './playerDataTypes'
|
||||
|
||||
export const enum IpcChannels {
|
||||
|
|
@ -38,7 +38,7 @@ export interface IpcChannelsParams {
|
|||
[IpcChannels.IsMaximized]: void
|
||||
[IpcChannels.FullscreenStateChange]: void
|
||||
[IpcChannels.GetApiCache]: {
|
||||
api: APIs
|
||||
api: CacheAPIs
|
||||
query?: any
|
||||
}
|
||||
[IpcChannels.DevDbExportJson]: void
|
||||
|
|
|
|||
|
|
@ -1,5 +1,6 @@
|
|||
interface FetchAppleMusicAlbumParams {
|
||||
neteaseId: number | string
|
||||
lang?: 'zh-CN' | 'en-US'
|
||||
}
|
||||
|
||||
interface FetchAppleMusicAlbumResponse {
|
||||
|
|
@ -17,6 +18,7 @@ interface FetchAppleMusicAlbumResponse {
|
|||
|
||||
interface FetchAppleMusicArtistParams {
|
||||
neteaseId: number | string
|
||||
lang?: 'zh-CN' | 'en-US'
|
||||
}
|
||||
|
||||
interface FetchAppleMusicArtistResponse {
|
||||
|
|
|
|||
|
|
@ -1,12 +1,8 @@
|
|||
import { fetchAlbum } from '@/web/api/album'
|
||||
import reactQueryClient from '@/web/utils/reactQueryClient'
|
||||
import { IpcChannels } from '@/shared/IpcChannels'
|
||||
import { APIs } from '@/shared/CacheAPIs'
|
||||
import {
|
||||
FetchAlbumParams,
|
||||
AlbumApiNames,
|
||||
FetchAlbumResponse,
|
||||
} from '@/shared/api/Album'
|
||||
import { CacheAPIs } from '@/shared/CacheAPIs'
|
||||
import { FetchAlbumParams, AlbumApiNames, FetchAlbumResponse } from '@/shared/api/Album'
|
||||
import { useQuery } from '@tanstack/react-query'
|
||||
|
||||
const fetch = async (params: FetchAlbumParams) => {
|
||||
|
|
@ -17,11 +13,9 @@ const fetch = async (params: FetchAlbumParams) => {
|
|||
return album
|
||||
}
|
||||
|
||||
const fetchFromCache = async (
|
||||
params: FetchAlbumParams
|
||||
): Promise<FetchAlbumResponse | undefined> =>
|
||||
const fetchFromCache = async (params: FetchAlbumParams): Promise<FetchAlbumResponse | undefined> =>
|
||||
window.ipcRenderer?.invoke(IpcChannels.GetApiCache, {
|
||||
api: APIs.Album,
|
||||
api: CacheAPIs.Album,
|
||||
query: params,
|
||||
})
|
||||
|
||||
|
|
@ -48,22 +42,14 @@ export default function useAlbum(params: FetchAlbumParams) {
|
|||
}
|
||||
|
||||
export function fetchAlbumWithReactQuery(params: FetchAlbumParams) {
|
||||
return reactQueryClient.fetchQuery(
|
||||
[AlbumApiNames.FetchAlbum, params],
|
||||
() => fetch(params),
|
||||
{
|
||||
staleTime: Infinity,
|
||||
}
|
||||
)
|
||||
return reactQueryClient.fetchQuery([AlbumApiNames.FetchAlbum, params], () => fetch(params), {
|
||||
staleTime: Infinity,
|
||||
})
|
||||
}
|
||||
|
||||
export async function prefetchAlbum(params: FetchAlbumParams) {
|
||||
if (await fetchFromCache(params)) return
|
||||
await reactQueryClient.prefetchQuery(
|
||||
[AlbumApiNames.FetchAlbum, params],
|
||||
() => fetch(params),
|
||||
{
|
||||
staleTime: Infinity,
|
||||
}
|
||||
)
|
||||
await reactQueryClient.prefetchQuery([AlbumApiNames.FetchAlbum, params], () => fetch(params), {
|
||||
staleTime: Infinity,
|
||||
})
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,11 +1,7 @@
|
|||
import { fetchArtist } from '@/web/api/artist'
|
||||
import { IpcChannels } from '@/shared/IpcChannels'
|
||||
import { APIs } from '@/shared/CacheAPIs'
|
||||
import {
|
||||
FetchArtistParams,
|
||||
ArtistApiNames,
|
||||
FetchArtistResponse,
|
||||
} from '@/shared/api/Artist'
|
||||
import { CacheAPIs } from '@/shared/CacheAPIs'
|
||||
import { FetchArtistParams, ArtistApiNames, FetchArtistResponse } from '@/shared/api/Artist'
|
||||
import { useQuery } from '@tanstack/react-query'
|
||||
import reactQueryClient from '@/web/utils/reactQueryClient'
|
||||
|
||||
|
|
@ -13,7 +9,7 @@ const fetchFromCache = async (
|
|||
params: FetchArtistParams
|
||||
): Promise<FetchArtistResponse | undefined> =>
|
||||
window.ipcRenderer?.invoke(IpcChannels.GetApiCache, {
|
||||
api: APIs.Artist,
|
||||
api: CacheAPIs.Artist,
|
||||
query: params,
|
||||
})
|
||||
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
import { fetchArtistAlbums } from '@/web/api/artist'
|
||||
import { IpcChannels } from '@/shared/IpcChannels'
|
||||
import { APIs } from '@/shared/CacheAPIs'
|
||||
import { CacheAPIs } from '@/shared/CacheAPIs'
|
||||
import { FetchArtistAlbumsParams, ArtistApiNames } from '@/shared/api/Artist'
|
||||
import { useQuery } from '@tanstack/react-query'
|
||||
import reactQueryClient from '@/web/utils/reactQueryClient'
|
||||
|
|
@ -13,7 +13,7 @@ export default function useArtistAlbums(params: FetchArtistAlbumsParams) {
|
|||
// fetch from cache as placeholder
|
||||
window.ipcRenderer
|
||||
?.invoke(IpcChannels.GetApiCache, {
|
||||
api: APIs.ArtistAlbum,
|
||||
api: CacheAPIs.ArtistAlbum,
|
||||
query: {
|
||||
id: params.id,
|
||||
},
|
||||
|
|
|
|||
|
|
@ -1,11 +1,7 @@
|
|||
import { fetchArtistMV } from '@/web/api/artist'
|
||||
import { IpcChannels } from '@/shared/IpcChannels'
|
||||
import { APIs } from '@/shared/CacheAPIs'
|
||||
import {
|
||||
FetchArtistMVParams,
|
||||
ArtistApiNames,
|
||||
FetchArtistMVResponse,
|
||||
} from '@/shared/api/Artist'
|
||||
import { CacheAPIs } from '@/shared/CacheAPIs'
|
||||
import { FetchArtistMVParams, ArtistApiNames, FetchArtistMVResponse } from '@/shared/api/Artist'
|
||||
import { useQuery } from '@tanstack/react-query'
|
||||
|
||||
export default function useArtistMV(params: FetchArtistMVParams) {
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
import { fetchArtist } from '@/web/api/artist'
|
||||
import { IpcChannels } from '@/shared/IpcChannels'
|
||||
import { APIs } from '@/shared/CacheAPIs'
|
||||
import { CacheAPIs } from '@/shared/CacheAPIs'
|
||||
import { ArtistApiNames } from '@/shared/api/Artist'
|
||||
import { useQuery } from '@tanstack/react-query'
|
||||
import reactQueryClient from '@/web/utils/reactQueryClient'
|
||||
|
|
@ -11,21 +11,15 @@ export default function useArtists(ids: number[]) {
|
|||
() =>
|
||||
Promise.all(
|
||||
ids.map(async id => {
|
||||
const queryData = reactQueryClient.getQueryData([
|
||||
ArtistApiNames.FetchArtist,
|
||||
{ id },
|
||||
])
|
||||
const queryData = reactQueryClient.getQueryData([ArtistApiNames.FetchArtist, { id }])
|
||||
if (queryData) return queryData
|
||||
|
||||
const cache = await window.ipcRenderer?.invoke(
|
||||
IpcChannels.GetApiCache,
|
||||
{
|
||||
api: APIs.Artist,
|
||||
query: {
|
||||
id,
|
||||
},
|
||||
}
|
||||
)
|
||||
const cache = await window.ipcRenderer?.invoke(IpcChannels.GetApiCache, {
|
||||
api: CacheAPIs.Artist,
|
||||
query: {
|
||||
id,
|
||||
},
|
||||
})
|
||||
if (cache) return cache
|
||||
|
||||
return fetchArtist({ id })
|
||||
|
|
|
|||
|
|
@ -1,7 +1,7 @@
|
|||
import { fetchLyric } from '@/web/api/track'
|
||||
import reactQueryClient from '@/web/utils/reactQueryClient'
|
||||
import { FetchLyricParams, TrackApiNames } from '@/shared/api/Track'
|
||||
import { APIs } from '@/shared/CacheAPIs'
|
||||
import { CacheAPIs } from '@/shared/CacheAPIs'
|
||||
import { IpcChannels } from '@/shared/IpcChannels'
|
||||
import { useQuery } from '@tanstack/react-query'
|
||||
|
||||
|
|
@ -12,7 +12,7 @@ export default function useLyric(params: FetchLyricParams) {
|
|||
async () => {
|
||||
// fetch from cache as initial data
|
||||
const cache = window.ipcRenderer?.invoke(IpcChannels.GetApiCache, {
|
||||
api: APIs.Lyric,
|
||||
api: CacheAPIs.Lyric,
|
||||
query: {
|
||||
id: params.id,
|
||||
},
|
||||
|
|
|
|||
|
|
@ -1,12 +1,6 @@
|
|||
import { fetchMV, fetchMVUrl } from '@/web/api/mv'
|
||||
import { IpcChannels } from '@/shared/IpcChannels'
|
||||
import { APIs } from '@/shared/CacheAPIs'
|
||||
import {
|
||||
MVApiNames,
|
||||
FetchMVParams,
|
||||
FetchMVResponse,
|
||||
FetchMVUrlParams,
|
||||
} from '@/shared/api/MV'
|
||||
import { MVApiNames, FetchMVParams, FetchMVResponse, FetchMVUrlParams } from '@/shared/api/MV'
|
||||
import { useQuery } from '@tanstack/react-query'
|
||||
|
||||
export default function useMV(params: FetchMVParams) {
|
||||
|
|
|
|||
|
|
@ -1,7 +1,7 @@
|
|||
import { fetchPlaylist } from '@/web/api/playlist'
|
||||
import reactQueryClient from '@/web/utils/reactQueryClient'
|
||||
import { IpcChannels } from '@/shared/IpcChannels'
|
||||
import { APIs } from '@/shared/CacheAPIs'
|
||||
import { CacheAPIs } from '@/shared/CacheAPIs'
|
||||
import {
|
||||
FetchPlaylistParams,
|
||||
PlaylistApiNames,
|
||||
|
|
@ -17,7 +17,7 @@ export const fetchFromCache = async (
|
|||
params: FetchPlaylistParams
|
||||
): Promise<FetchPlaylistResponse | undefined> =>
|
||||
window.ipcRenderer?.invoke(IpcChannels.GetApiCache, {
|
||||
api: APIs.Playlist,
|
||||
api: CacheAPIs.Playlist,
|
||||
query: params,
|
||||
})
|
||||
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
import { fetchSimilarArtists } from '@/web/api/artist'
|
||||
import { IpcChannels } from '@/shared/IpcChannels'
|
||||
import { APIs } from '@/shared/CacheAPIs'
|
||||
import { CacheAPIs } from '@/shared/CacheAPIs'
|
||||
import { FetchSimilarArtistsParams, ArtistApiNames } from '@/shared/api/Artist'
|
||||
import { useQuery } from '@tanstack/react-query'
|
||||
import reactQueryClient from '@/web/utils/reactQueryClient'
|
||||
|
|
@ -12,7 +12,7 @@ export default function useSimilarArtists(params: FetchSimilarArtistsParams) {
|
|||
() => {
|
||||
window.ipcRenderer
|
||||
?.invoke(IpcChannels.GetApiCache, {
|
||||
api: APIs.SimilarArtist,
|
||||
api: CacheAPIs.SimilarArtist,
|
||||
query: {
|
||||
id: params.id,
|
||||
},
|
||||
|
|
|
|||
|
|
@ -8,7 +8,7 @@ import {
|
|||
FetchTracksResponse,
|
||||
TrackApiNames,
|
||||
} from '@/shared/api/Track'
|
||||
import { APIs } from '@/shared/CacheAPIs'
|
||||
import { CacheAPIs } from '@/shared/CacheAPIs'
|
||||
import { useQuery } from '@tanstack/react-query'
|
||||
|
||||
export default function useTracks(params: FetchTracksParams) {
|
||||
|
|
@ -17,7 +17,7 @@ export default function useTracks(params: FetchTracksParams) {
|
|||
async () => {
|
||||
// fetch from cache as initial data
|
||||
const cache = await window.ipcRenderer?.invoke(IpcChannels.GetApiCache, {
|
||||
api: APIs.Track,
|
||||
api: CacheAPIs.Track,
|
||||
query: {
|
||||
ids: params.ids.join(','),
|
||||
},
|
||||
|
|
@ -40,7 +40,7 @@ export function fetchTracksWithReactQuery(params: FetchTracksParams) {
|
|||
[TrackApiNames.FetchTracks, params],
|
||||
async () => {
|
||||
const cache = await window.ipcRenderer?.invoke(IpcChannels.GetApiCache, {
|
||||
api: APIs.Track,
|
||||
api: CacheAPIs.Track,
|
||||
query: {
|
||||
ids: params.ids.join(','),
|
||||
},
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
import { fetchUserAccount } from '@/web/api/user'
|
||||
import { UserApiNames, FetchUserAccountResponse } from '@/shared/api/User'
|
||||
import { APIs } from '@/shared/CacheAPIs'
|
||||
import { CacheAPIs } from '@/shared/CacheAPIs'
|
||||
import { IpcChannels } from '@/shared/IpcChannels'
|
||||
import { useMutation, useQuery } from '@tanstack/react-query'
|
||||
import { logout } from '../auth'
|
||||
|
|
@ -16,7 +16,7 @@ export default function useUser() {
|
|||
if (!existsQueryData) {
|
||||
window.ipcRenderer
|
||||
?.invoke(IpcChannels.GetApiCache, {
|
||||
api: APIs.UserAccount,
|
||||
api: CacheAPIs.UserAccount,
|
||||
})
|
||||
.then(cache => {
|
||||
if (cache) reactQueryClient.setQueryData(key, cache)
|
||||
|
|
|
|||
|
|
@ -2,12 +2,8 @@ import { likeAAlbum } from '@/web/api/album'
|
|||
import { useMutation, useQuery } from '@tanstack/react-query'
|
||||
import useUser from './useUser'
|
||||
import { IpcChannels } from '@/shared/IpcChannels'
|
||||
import { APIs } from '@/shared/CacheAPIs'
|
||||
import {
|
||||
FetchUserAlbumsParams,
|
||||
UserApiNames,
|
||||
FetchUserAlbumsResponse,
|
||||
} from '@/shared/api/User'
|
||||
import { CacheAPIs } from '@/shared/CacheAPIs'
|
||||
import { FetchUserAlbumsParams, UserApiNames, FetchUserAlbumsResponse } from '@/shared/api/User'
|
||||
import { fetchUserAlbums } from '../user'
|
||||
import toast from 'react-hot-toast'
|
||||
import reactQueryClient from '@/web/utils/reactQueryClient'
|
||||
|
|
@ -25,7 +21,7 @@ export default function useUserAlbums(params: FetchUserAlbumsParams = {}) {
|
|||
if (!existsQueryData) {
|
||||
window.ipcRenderer
|
||||
?.invoke(IpcChannels.GetApiCache, {
|
||||
api: APIs.UserAlbums,
|
||||
api: CacheAPIs.UserAlbums,
|
||||
query: params,
|
||||
})
|
||||
.then(cache => {
|
||||
|
|
@ -72,9 +68,7 @@ export const useMutationLikeAAlbum = () => {
|
|||
}
|
||||
|
||||
// Snapshot the previous value
|
||||
const previousData = reactQueryClient.getQueryData(
|
||||
key
|
||||
) as FetchUserAlbumsResponse
|
||||
const previousData = reactQueryClient.getQueryData(key) as FetchUserAlbumsResponse
|
||||
|
||||
const isLiked = !!previousData?.data.find(a => a.id === albumID)
|
||||
const newAlbums = cloneDeep(previousData!)
|
||||
|
|
@ -88,21 +82,17 @@ export const useMutationLikeAAlbum = () => {
|
|||
|
||||
console.log({ albumID })
|
||||
|
||||
const albumFromCache: FetchAlbumResponse | undefined =
|
||||
reactQueryClient.getQueryData([
|
||||
AlbumApiNames.FetchAlbum,
|
||||
{ id: albumID },
|
||||
])
|
||||
const albumFromCache: FetchAlbumResponse | undefined = reactQueryClient.getQueryData([
|
||||
AlbumApiNames.FetchAlbum,
|
||||
{ id: albumID },
|
||||
])
|
||||
|
||||
console.log({ albumFromCache })
|
||||
|
||||
// 从api获取专辑
|
||||
const album: FetchAlbumResponse | undefined = albumFromCache
|
||||
? albumFromCache
|
||||
: await reactQueryClient.fetchQuery([
|
||||
AlbumApiNames.FetchAlbum,
|
||||
{ id: albumID },
|
||||
])
|
||||
: await reactQueryClient.fetchQuery([AlbumApiNames.FetchAlbum, { id: albumID }])
|
||||
|
||||
if (!album?.album) {
|
||||
toast.error('Failed to like album: unable to fetch album info')
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
import { fetchUserArtists } from '@/web/api/user'
|
||||
import { UserApiNames, FetchUserArtistsResponse } from '@/shared/api/User'
|
||||
import { APIs } from '@/shared/CacheAPIs'
|
||||
import { CacheAPIs } from '@/shared/CacheAPIs'
|
||||
import { IpcChannels } from '@/shared/IpcChannels'
|
||||
import { useMutation, useQuery, useQueryClient } from '@tanstack/react-query'
|
||||
import toast from 'react-hot-toast'
|
||||
|
|
@ -18,7 +18,7 @@ export default function useUserArtists() {
|
|||
if (!existsQueryData) {
|
||||
window.ipcRenderer
|
||||
?.invoke(IpcChannels.GetApiCache, {
|
||||
api: APIs.UserArtists,
|
||||
api: CacheAPIs.UserArtists,
|
||||
})
|
||||
.then(cache => {
|
||||
if (cache) reactQueryClient.setQueryData(key, cache)
|
||||
|
|
@ -59,32 +59,24 @@ export const useMutationLikeAArtist = () => {
|
|||
}
|
||||
|
||||
// Snapshot the previous value
|
||||
const previousData = reactQueryClient.getQueryData(
|
||||
key
|
||||
) as FetchUserArtistsResponse
|
||||
const previousData = reactQueryClient.getQueryData(key) as FetchUserArtistsResponse
|
||||
|
||||
const isLiked = !!previousData?.data.find(a => a.id === artistID)
|
||||
const newLikedArtists = cloneDeep(previousData!)
|
||||
|
||||
if (isLiked) {
|
||||
newLikedArtists.data = previousData.data.filter(
|
||||
a => a.id !== artistID
|
||||
)
|
||||
newLikedArtists.data = previousData.data.filter(a => a.id !== artistID)
|
||||
} else {
|
||||
// 从react-query缓存获取歌手信息
|
||||
const artistFromCache: FetchArtistResponse | undefined =
|
||||
reactQueryClient.getQueryData([
|
||||
ArtistApiNames.FetchArtist,
|
||||
{ id: artistID },
|
||||
])
|
||||
const artistFromCache: FetchArtistResponse | undefined = reactQueryClient.getQueryData([
|
||||
ArtistApiNames.FetchArtist,
|
||||
{ id: artistID },
|
||||
])
|
||||
|
||||
// 从api获取歌手信息
|
||||
const artist: FetchArtistResponse | undefined = artistFromCache
|
||||
? artistFromCache
|
||||
: await reactQueryClient.fetchQuery([
|
||||
ArtistApiNames.FetchArtist,
|
||||
{ id: artistID },
|
||||
])
|
||||
: await reactQueryClient.fetchQuery([ArtistApiNames.FetchArtist, { id: artistID }])
|
||||
|
||||
if (!artist?.artist) {
|
||||
toast.error('Failed to like artist: unable to fetch artist info')
|
||||
|
|
|
|||
|
|
@ -2,12 +2,9 @@ import { likeATrack } from '@/web/api/track'
|
|||
import useUser from './useUser'
|
||||
import { useMutation, useQueryClient } from '@tanstack/react-query'
|
||||
import { IpcChannels } from '@/shared/IpcChannels'
|
||||
import { APIs } from '@/shared/CacheAPIs'
|
||||
import { CacheAPIs } from '@/shared/CacheAPIs'
|
||||
import { fetchUserLikedTracksIDs } from '../user'
|
||||
import {
|
||||
FetchUserLikedTracksIDsResponse,
|
||||
UserApiNames,
|
||||
} from '@/shared/api/User'
|
||||
import { FetchUserLikedTracksIDsResponse, UserApiNames } from '@/shared/api/User'
|
||||
import { useQuery } from '@tanstack/react-query'
|
||||
import toast from 'react-hot-toast'
|
||||
import reactQueryClient from '@/web/utils/reactQueryClient'
|
||||
|
|
@ -24,7 +21,7 @@ export default function useUserLikedTracksIDs() {
|
|||
if (!existsQueryData) {
|
||||
window.ipcRenderer
|
||||
?.invoke(IpcChannels.GetApiCache, {
|
||||
api: APIs.Likelist,
|
||||
api: CacheAPIs.Likelist,
|
||||
query: {
|
||||
uid,
|
||||
},
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
import { fetchListenedRecords } from '@/web/api/user'
|
||||
import { UserApiNames, FetchListenedRecordsResponse } from '@/shared/api/User'
|
||||
import { APIs } from '@/shared/CacheAPIs'
|
||||
import { CacheAPIs } from '@/shared/CacheAPIs'
|
||||
import { IpcChannels } from '@/shared/IpcChannels'
|
||||
import { useQuery } from '@tanstack/react-query'
|
||||
import useUser from './useUser'
|
||||
|
|
@ -18,7 +18,7 @@ export default function useUserListenedRecords(params: { type: 'week' | 'all' })
|
|||
if (!existsQueryData) {
|
||||
window.ipcRenderer
|
||||
?.invoke(IpcChannels.GetApiCache, {
|
||||
api: APIs.ListenedRecords,
|
||||
api: CacheAPIs.ListenedRecords,
|
||||
})
|
||||
.then(cache => {
|
||||
if (cache) reactQueryClient.setQueryData(key, cache)
|
||||
|
|
|
|||
|
|
@ -2,7 +2,7 @@ import { likeAPlaylist } from '@/web/api/playlist'
|
|||
import { useMutation, useQuery, useQueryClient } from '@tanstack/react-query'
|
||||
import useUser from './useUser'
|
||||
import { IpcChannels } from '@/shared/IpcChannels'
|
||||
import { APIs } from '@/shared/CacheAPIs'
|
||||
import { CacheAPIs } from '@/shared/CacheAPIs'
|
||||
import { fetchUserPlaylists } from '@/web/api/user'
|
||||
import { FetchUserPlaylistsResponse, UserApiNames } from '@/shared/api/User'
|
||||
import toast from 'react-hot-toast'
|
||||
|
|
@ -33,7 +33,7 @@ export default function useUserPlaylists() {
|
|||
if (!existsQueryData) {
|
||||
window.ipcRenderer
|
||||
?.invoke(IpcChannels.GetApiCache, {
|
||||
api: APIs.UserPlaylist,
|
||||
api: CacheAPIs.UserPlaylist,
|
||||
query: {
|
||||
uid: params.uid,
|
||||
},
|
||||
|
|
@ -46,11 +46,7 @@ export default function useUserPlaylists() {
|
|||
return fetchUserPlaylists(params)
|
||||
},
|
||||
{
|
||||
enabled: !!(
|
||||
!!params.uid &&
|
||||
params.uid !== 0 &&
|
||||
params.offset !== undefined
|
||||
),
|
||||
enabled: !!(!!params.uid && params.uid !== 0 && params.offset !== undefined),
|
||||
refetchOnWindowFocus: true,
|
||||
}
|
||||
)
|
||||
|
|
@ -69,10 +65,7 @@ export const useMutationLikeAPlaylist = () => {
|
|||
}
|
||||
const response = await likeAPlaylist({
|
||||
id: playlistID,
|
||||
t:
|
||||
userPlaylists.playlist.findIndex(p => p.id === playlistID) > -1
|
||||
? 2
|
||||
: 1,
|
||||
t: userPlaylists.playlist.findIndex(p => p.id === playlistID) > -1 ? 2 : 1,
|
||||
})
|
||||
if (response.code !== 200) throw new Error((response as any).msg)
|
||||
return response
|
||||
|
|
@ -90,9 +83,7 @@ export const useMutationLikeAPlaylist = () => {
|
|||
}
|
||||
|
||||
// Snapshot the previous value
|
||||
const previousData = reactQueryClient.getQueryData(
|
||||
key
|
||||
) as FetchUserPlaylistsResponse
|
||||
const previousData = reactQueryClient.getQueryData(key) as FetchUserPlaylistsResponse
|
||||
|
||||
const isLiked = !!previousData?.playlist.find(p => p.id === playlistID)
|
||||
const newPlaylists = cloneDeep(previousData!)
|
||||
|
|
@ -100,17 +91,12 @@ export const useMutationLikeAPlaylist = () => {
|
|||
console.log({ isLiked })
|
||||
|
||||
if (isLiked) {
|
||||
newPlaylists.playlist = previousData.playlist.filter(
|
||||
p => p.id !== playlistID
|
||||
)
|
||||
newPlaylists.playlist = previousData.playlist.filter(p => p.id !== playlistID)
|
||||
} else {
|
||||
// 从react-query缓存获取歌单信息
|
||||
|
||||
const playlistFromCache: FetchPlaylistResponse | undefined =
|
||||
reactQueryClient.getQueryData([
|
||||
PlaylistApiNames.FetchPlaylist,
|
||||
{ id: playlistID },
|
||||
])
|
||||
reactQueryClient.getQueryData([PlaylistApiNames.FetchPlaylist, { id: playlistID }])
|
||||
|
||||
// 从api获取歌单信息
|
||||
const playlist: FetchPlaylistResponse | undefined = playlistFromCache
|
||||
|
|
@ -121,9 +107,7 @@ export const useMutationLikeAPlaylist = () => {
|
|||
])
|
||||
|
||||
if (!playlist?.playlist) {
|
||||
toast.error(
|
||||
'Failed to like playlist: unable to fetch playlist info'
|
||||
)
|
||||
toast.error('Failed to like playlist: unable to fetch playlist info')
|
||||
throw new Error('unable to fetch playlist info')
|
||||
}
|
||||
newPlaylists.playlist.splice(1, 0, playlist.playlist)
|
||||
|
|
|
|||
|
|
@ -3,7 +3,7 @@ import {
|
|||
FetchListenedRecordsResponse,
|
||||
FetchUserVideosResponse,
|
||||
} from '@/shared/api/User'
|
||||
import { APIs } from '@/shared/CacheAPIs'
|
||||
import { CacheAPIs } from '@/shared/CacheAPIs'
|
||||
import { IpcChannels } from '@/shared/IpcChannels'
|
||||
import { useMutation, useQuery } from '@tanstack/react-query'
|
||||
import useUser from './useUser'
|
||||
|
|
@ -25,7 +25,7 @@ export default function useUserVideos() {
|
|||
// if (!existsQueryData) {
|
||||
// window.ipcRenderer
|
||||
// ?.invoke(IpcChannels.GetApiCache, {
|
||||
// api: APIs.Likelist,
|
||||
// api: CacheAPIs.Likelist,
|
||||
// query: {
|
||||
// uid,
|
||||
// },
|
||||
|
|
|
|||
|
|
@ -1,67 +0,0 @@
|
|||
import player from '@/web/states/player'
|
||||
import { css, cx } from '@emotion/css'
|
||||
import { useQuery } from '@tanstack/react-query'
|
||||
import { useState } from 'react'
|
||||
import { useSnapshot } from 'valtio'
|
||||
|
||||
const useAirplayDevices = () => {
|
||||
return useQuery(['useAirplayDevices'], () =>
|
||||
window.ipcRenderer?.invoke('airplay-scan-devices')
|
||||
)
|
||||
}
|
||||
|
||||
const Airplay = () => {
|
||||
const [showPanel, setShowPanel] = useState(false)
|
||||
const { data: devices, isLoading } = useAirplayDevices()
|
||||
const { remoteDevice } = useSnapshot(player)
|
||||
const selectedAirplayDeviceID =
|
||||
remoteDevice?.protocol === 'airplay' ? remoteDevice?.id : ''
|
||||
|
||||
return (
|
||||
<div
|
||||
className={cx(
|
||||
'fixed z-20',
|
||||
css`
|
||||
top: 46px;
|
||||
right: 256px;
|
||||
`
|
||||
)}
|
||||
>
|
||||
<div
|
||||
onClick={() => setShowPanel(!showPanel)}
|
||||
className='flex h-12 w-12 items-center justify-center rounded-full bg-white/20 text-24 text-white'
|
||||
>
|
||||
A
|
||||
</div>
|
||||
|
||||
{showPanel && (
|
||||
<div
|
||||
className={cx(
|
||||
'absolute rounded-24 border border-white/10 bg-black/60 p-2 backdrop-blur-xl',
|
||||
css`
|
||||
width: 256px;
|
||||
height: 256px;
|
||||
`
|
||||
)}
|
||||
>
|
||||
{devices?.devices?.map(device => (
|
||||
<div
|
||||
key={device.identifier}
|
||||
className={cx(
|
||||
'rounded-12 p-2 hover:bg-white/10',
|
||||
device.identifier === selectedAirplayDeviceID
|
||||
? 'text-brand-500'
|
||||
: 'text-white'
|
||||
)}
|
||||
onClick={() => player.switchToAirplayDevice(device.identifier)}
|
||||
>
|
||||
{device.name}
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export default Airplay
|
||||
|
|
@ -87,7 +87,10 @@ const AlbumContextMenu = () => {
|
|||
type: 'item',
|
||||
label: t`context-menu.copy-r3play-link`,
|
||||
onClick: () => {
|
||||
copyToClipboard(`${window.location.origin}/album/${dataSourceID}`)
|
||||
const baseUrl = window.env?.isElectron
|
||||
? 'https://r3play.app'
|
||||
: window.location.origin
|
||||
copyToClipboard(`${baseUrl}/album/${dataSourceID}`)
|
||||
toast.success(t`toasts.copied`)
|
||||
},
|
||||
},
|
||||
|
|
|
|||
100
packages/web/components/DescriptionViewer.tsx
Normal file
100
packages/web/components/DescriptionViewer.tsx
Normal file
|
|
@ -0,0 +1,100 @@
|
|||
import { css, cx } from '@emotion/css'
|
||||
import { AnimatePresence, motion } from 'framer-motion'
|
||||
import { useEffect } from 'react'
|
||||
import { createPortal } from 'react-dom'
|
||||
import uiStates from '../states/uiStates'
|
||||
import { ease } from '../utils/const'
|
||||
import Icon from './Icon'
|
||||
|
||||
function DescriptionViewer({
|
||||
description,
|
||||
title,
|
||||
isOpen,
|
||||
onClose,
|
||||
}: {
|
||||
description: string
|
||||
title: string
|
||||
isOpen: boolean
|
||||
onClose: () => void
|
||||
}) {
|
||||
useEffect(() => {
|
||||
uiStates.isPauseVideos = isOpen
|
||||
}, [isOpen])
|
||||
|
||||
return createPortal(
|
||||
<>
|
||||
{/* Blur bg */}
|
||||
<AnimatePresence>
|
||||
{isOpen && (
|
||||
<motion.div
|
||||
className='fixed inset-0 z-30 bg-black/70 backdrop-blur-3xl lg:rounded-24'
|
||||
initial={{ opacity: 0 }}
|
||||
animate={{ opacity: 1, transition: { duration: 0.3 } }}
|
||||
exit={{ opacity: 0, transition: { duration: 0.3, delay: 0.3 } }}
|
||||
transition={{ ease }}
|
||||
></motion.div>
|
||||
)}
|
||||
</AnimatePresence>
|
||||
|
||||
{/* Content */}
|
||||
<AnimatePresence>
|
||||
{isOpen && (
|
||||
<motion.div
|
||||
initial={{ opacity: 0 }}
|
||||
animate={{ opacity: 1, transition: { duration: 0.3, delay: 0.3 } }}
|
||||
exit={{ opacity: 0, transition: { duration: 0.3 } }}
|
||||
transition={{ ease }}
|
||||
className={cx('fixed inset-0 z-30 flex flex-col items-center justify-center')}
|
||||
>
|
||||
<div className='relative'>
|
||||
{/* Title */}
|
||||
<div className='line-clamp-1 absolute -top-8 mx-44 max-w-2xl select-none text-32 font-extrabold text-neutral-100'>
|
||||
{title}
|
||||
</div>
|
||||
|
||||
{/* Description */}
|
||||
|
||||
<div
|
||||
className={css`
|
||||
mask-image: linear-gradient(to top, transparent 0px, black 32px); // 底部渐变遮罩
|
||||
`}
|
||||
>
|
||||
<div
|
||||
className={cx(
|
||||
'no-scrollbar relative mx-44 max-w-2xl overflow-scroll',
|
||||
css`
|
||||
max-height: 60vh;
|
||||
mask-image: linear-gradient(
|
||||
to bottom,
|
||||
transparent 12px,
|
||||
black 32px
|
||||
); // 顶部渐变遮罩
|
||||
`
|
||||
)}
|
||||
>
|
||||
<p
|
||||
dangerouslySetInnerHTML={{ __html: description + description }}
|
||||
className='mt-8 whitespace-pre-wrap pb-8 text-16 font-bold leading-6 text-neutral-200'
|
||||
></p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Close button */}
|
||||
<div className='absolute -bottom-24 flex w-full justify-center'>
|
||||
<div
|
||||
onClick={onClose}
|
||||
className='flex h-14 w-14 items-center justify-center rounded-full bg-white/10 text-white/50 transition-colors duration-300 hover:bg-white/20 hover:text-white/70'
|
||||
>
|
||||
<Icon name='x' className='h-6 w-6' />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</motion.div>
|
||||
)}
|
||||
</AnimatePresence>
|
||||
</>,
|
||||
document.body
|
||||
)
|
||||
}
|
||||
|
||||
export default DescriptionViewer
|
||||
|
|
@ -6,48 +6,41 @@ import { useAnimation, motion } from 'framer-motion'
|
|||
import { useState, useRef, useEffect } from 'react'
|
||||
import { useNavigate } from 'react-router-dom'
|
||||
import { useSnapshot } from 'valtio'
|
||||
import { subscribeKey } from 'valtio/utils'
|
||||
|
||||
const Cover = () => {
|
||||
const { track } = useSnapshot(player)
|
||||
const [cover, setCover] = useState(track?.al.picUrl)
|
||||
const animationStartTime = useRef(0)
|
||||
const controls = useAnimation()
|
||||
const duration = 150 // ms
|
||||
const navigate = useNavigate()
|
||||
|
||||
useEffect(() => {
|
||||
const resizedCover = resizeImage(track?.al.picUrl || '', 'lg')
|
||||
const animate = async () => {
|
||||
animationStartTime.current = Date.now()
|
||||
await controls.start({ opacity: 0 })
|
||||
setCover(resizedCover)
|
||||
}
|
||||
animate()
|
||||
}, [controls, track?.al.picUrl])
|
||||
|
||||
// 防止狂点下一首或上一首造成封面与歌曲不匹配的问题
|
||||
useEffect(() => {
|
||||
const realCover = resizeImage(track?.al.picUrl ?? '', 'lg')
|
||||
if (cover !== realCover) setCover(realCover)
|
||||
}, [cover, track?.al.picUrl])
|
||||
|
||||
const onLoad = () => {
|
||||
const passedTime = Date.now() - animationStartTime.current
|
||||
controls.start({
|
||||
opacity: 1,
|
||||
transition: {
|
||||
delay: passedTime > duration ? 0 : (duration - passedTime) / 1000,
|
||||
},
|
||||
const unsubscribe = subscribeKey(player, 'track', async () => {
|
||||
const coverUrl = player.track?.al.picUrl
|
||||
await controls.start({
|
||||
opacity: 0,
|
||||
transition: { duration: 0.2 },
|
||||
})
|
||||
setCover(coverUrl)
|
||||
if (!coverUrl) return
|
||||
const img = new Image()
|
||||
img.onload = () => {
|
||||
controls.start({
|
||||
opacity: 1,
|
||||
transition: { duration: 0.2 },
|
||||
})
|
||||
}
|
||||
img.src = coverUrl
|
||||
})
|
||||
}
|
||||
return unsubscribe
|
||||
}, [])
|
||||
|
||||
return (
|
||||
<motion.img
|
||||
animate={controls}
|
||||
transition={{ duration: duration / 1000, ease }}
|
||||
className={cx('absolute inset-0 w-full')}
|
||||
transition={{ ease }}
|
||||
className='absolute inset-0 w-full'
|
||||
src={cover}
|
||||
onLoad={onLoad}
|
||||
onClick={() => {
|
||||
const id = track?.al.id
|
||||
if (id) navigate(`/album/${id}`)
|
||||
|
|
|
|||
|
|
@ -23,7 +23,7 @@ const Header = () => {
|
|||
)}
|
||||
>
|
||||
<div className='flex'>
|
||||
<div className='mr-2 h-4 w-1 bg-brand-700'></div>
|
||||
<div className='mr-2 h-4 w-1 rounded-full bg-brand-700'></div>
|
||||
{t`player.queue`}
|
||||
</div>
|
||||
<div className='flex'>
|
||||
|
|
@ -117,11 +117,7 @@ const TrackList = ({ className }: { className?: string }) => {
|
|||
<>
|
||||
<div
|
||||
className={css`
|
||||
mask-image: linear-gradient(
|
||||
to bottom,
|
||||
transparent 22px,
|
||||
black 42px
|
||||
); // 顶部渐变遮罩
|
||||
mask-image: linear-gradient(to bottom, transparent 22px, black 42px); // 顶部渐变遮罩
|
||||
`}
|
||||
>
|
||||
<Virtuoso
|
||||
|
|
@ -133,11 +129,7 @@ const TrackList = ({ className }: { className?: string }) => {
|
|||
'no-scrollbar relative z-10 w-full overflow-auto',
|
||||
className,
|
||||
css`
|
||||
mask-image: linear-gradient(
|
||||
to top,
|
||||
transparent 8px,
|
||||
black 42px
|
||||
); // 底部渐变遮罩
|
||||
mask-image: linear-gradient(to top, transparent 8px, black 42px); // 底部渐变遮罩
|
||||
`
|
||||
)}
|
||||
fixedItemHeight={76}
|
||||
|
|
|
|||
5
packages/web/components/Tooltip.tsx
Normal file
5
packages/web/components/Tooltip.tsx
Normal file
|
|
@ -0,0 +1,5 @@
|
|||
function Tooltip() {
|
||||
return <></>
|
||||
}
|
||||
|
||||
export default Tooltip
|
||||
|
|
@ -128,13 +128,15 @@ const SearchBox = () => {
|
|||
const [searchText, setSearchText] = useState('')
|
||||
const [isFocused, setIsFocused] = useState(false)
|
||||
const { t } = useTranslation()
|
||||
const inputRef = useRef<HTMLInputElement>(null)
|
||||
|
||||
return (
|
||||
<div className='relative'>
|
||||
{/* Input */}
|
||||
<div
|
||||
onClick={() => inputRef.current?.focus()}
|
||||
className={cx(
|
||||
'app-region-no-drag flex items-center rounded-full bg-white/10 p-2.5 text-white/40 backdrop-blur-3xl',
|
||||
'app-region-no-drag flex cursor-text items-center rounded-full bg-white/10 p-2.5 text-white/40 backdrop-blur-3xl',
|
||||
css`
|
||||
${bp.lg} {
|
||||
min-width: 284px;
|
||||
|
|
@ -144,6 +146,7 @@ const SearchBox = () => {
|
|||
>
|
||||
<Icon name='search' className='mr-2.5 h-7 w-7' />
|
||||
<input
|
||||
ref={inputRef}
|
||||
placeholder={t`search.search`}
|
||||
className={cx(
|
||||
'flex-shrink bg-transparent font-medium placeholder:text-white/40 dark:text-white/80',
|
||||
|
|
|
|||
|
|
@ -3,8 +3,9 @@ import Icon from '@/web/components/Icon'
|
|||
import dayjs from 'dayjs'
|
||||
import { useNavigate } from 'react-router-dom'
|
||||
import useIsMobile from '@/web/hooks/useIsMobile'
|
||||
import { ReactNode } from 'react'
|
||||
import { AnimatePresence, motion } from 'framer-motion'
|
||||
import { ReactNode, useState } from 'react'
|
||||
import { motion } from 'framer-motion'
|
||||
import DescriptionViewer from '../DescriptionViewer'
|
||||
|
||||
const Info = ({
|
||||
title,
|
||||
|
|
@ -23,6 +24,7 @@ const Info = ({
|
|||
}) => {
|
||||
const navigate = useNavigate()
|
||||
const isMobile = useIsMobile()
|
||||
const [isOpenDescription, setIsOpenDescription] = useState(false)
|
||||
|
||||
return (
|
||||
<div>
|
||||
|
|
@ -72,12 +74,20 @@ const Info = ({
|
|||
initial={{ opacity: 0 }}
|
||||
animate={{ opacity: 1 }}
|
||||
transition={{ duration: 0.3 }}
|
||||
className='line-clamp-3 mt-6 whitespace-pre-wrap text-14 font-bold dark:text-white/40'
|
||||
className='line-clamp-3 mt-6 whitespace-pre-wrap text-14 font-bold transition-colors duration-300 dark:text-white/40 dark:hover:text-white/60'
|
||||
dangerouslySetInnerHTML={{
|
||||
__html: description,
|
||||
}}
|
||||
onClick={() => setIsOpenDescription(true)}
|
||||
></motion.div>
|
||||
)}
|
||||
|
||||
<DescriptionViewer
|
||||
description={description || ''}
|
||||
isOpen={isOpenDescription}
|
||||
onClose={() => setIsOpenDescription(false)}
|
||||
title={title || ''}
|
||||
/>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -22,14 +22,14 @@ const VideoCover = ({ source, onPlay }: { source?: string; onPlay?: () => void }
|
|||
}, [source])
|
||||
|
||||
// Pause video cover when playing another video
|
||||
const { playingVideoID } = useSnapshot(uiStates)
|
||||
const { playingVideoID, isPauseVideos } = useSnapshot(uiStates)
|
||||
useEffect(() => {
|
||||
if (playingVideoID) {
|
||||
if (playingVideoID || isPauseVideos) {
|
||||
videoRef?.current?.pause()
|
||||
} else {
|
||||
videoRef?.current?.play()
|
||||
}
|
||||
}, [playingVideoID])
|
||||
}, [playingVideoID, isPauseVideos])
|
||||
|
||||
return (
|
||||
<motion.div
|
||||
|
|
|
|||
|
|
@ -3,6 +3,8 @@ import useAppleMusicArtist from '@/web/api/hooks/useAppleMusicArtist'
|
|||
import { cx, css } from '@emotion/css'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import i18next from 'i18next'
|
||||
import { useState } from 'react'
|
||||
import DescriptionViewer from '@/web/components/DescriptionViewer'
|
||||
|
||||
const ArtistInfo = ({ artist, isLoading }: { artist?: Artist; isLoading: boolean }) => {
|
||||
const { t, i18n } = useTranslation()
|
||||
|
|
@ -12,6 +14,10 @@ const ArtistInfo = ({ artist, isLoading }: { artist?: Artist; isLoading: boolean
|
|||
artist?.id || 0
|
||||
)
|
||||
|
||||
const [isOpenDescription, setIsOpenDescription] = useState(false)
|
||||
const description =
|
||||
artistFromApple?.artistBio?.[i18n.language.replace('-', '_')] || artist?.briefDesc
|
||||
|
||||
return (
|
||||
<div>
|
||||
{/* Name */}
|
||||
|
|
@ -61,15 +67,23 @@ const ArtistInfo = ({ artist, isLoading }: { artist?: Artist; isLoading: boolean
|
|||
) : (
|
||||
<div
|
||||
className={cx(
|
||||
'line-clamp-5 mt-6 text-14 font-bold text-white/40',
|
||||
css`
|
||||
height: 86px;
|
||||
`
|
||||
'line-clamp-5 mt-6 overflow-hidden whitespace-pre-wrap text-14 font-bold text-white/40 transition-colors duration-500 hover:text-white/60'
|
||||
// css`
|
||||
// height: 86px;
|
||||
// `
|
||||
)}
|
||||
onClick={() => setIsOpenDescription(true)}
|
||||
>
|
||||
{artistFromApple?.artistBio?.[i18n.language.replace('-', '_')] || artist?.briefDesc}
|
||||
{description}
|
||||
</div>
|
||||
))}
|
||||
|
||||
<DescriptionViewer
|
||||
description={description || ''}
|
||||
isOpen={isOpenDescription}
|
||||
onClose={() => setIsOpenDescription(false)}
|
||||
title={artist?.name || ''}
|
||||
/>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -79,7 +79,7 @@ const CollectionTabs = ({ showBg }: { showBg: boolean }) => {
|
|||
animate={{ opacity: 1 }}
|
||||
exit={{ opacity: 0 }}
|
||||
className={cx(
|
||||
'pointer-events-none fixed top-0 right-0 left-10 z-10 hidden lg:block',
|
||||
'pointer-events-none fixed top-0 right-0 left-10 z-10',
|
||||
css`
|
||||
height: 230px;
|
||||
background-repeat: repeat;
|
||||
|
|
|
|||
|
|
@ -10,6 +10,7 @@ interface UIStates {
|
|||
blurBackgroundImage: string | null
|
||||
fullscreen: boolean
|
||||
playingVideoID: number | null
|
||||
isPauseVideos: boolean
|
||||
}
|
||||
|
||||
const initUIStates: UIStates = {
|
||||
|
|
@ -21,6 +22,7 @@ const initUIStates: UIStates = {
|
|||
blurBackgroundImage: null,
|
||||
fullscreen: false,
|
||||
playingVideoID: null,
|
||||
isPauseVideos: false,
|
||||
}
|
||||
|
||||
window.ipcRenderer
|
||||
|
|
|
|||
|
|
@ -112,8 +112,14 @@ input {
|
|||
@apply outline-none;
|
||||
}
|
||||
|
||||
*::selection {
|
||||
@apply bg-brand-500 text-neutral-800;
|
||||
}
|
||||
|
||||
a,
|
||||
button {
|
||||
button,
|
||||
p,
|
||||
div {
|
||||
@apply cursor-default;
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -9,7 +9,7 @@ import {
|
|||
storage,
|
||||
} from '@/web/utils/common'
|
||||
import { IpcChannels } from '@/shared/IpcChannels'
|
||||
import { APIs } from '@/shared/CacheAPIs'
|
||||
import { CacheAPIs } from '@/shared/CacheAPIs'
|
||||
|
||||
test('resizeImage', () => {
|
||||
expect(resizeImage('https://test.com/test.jpg', 'xs')).toBe(
|
||||
|
|
@ -50,12 +50,8 @@ test('formatDuration', () => {
|
|||
expect(formatDuration(3600000, 'zh-CN', 'hh[hr] mm[min]')).toBe('1 小时')
|
||||
expect(formatDuration(3600000, 'zh-TW', 'hh[hr] mm[min]')).toBe('1 小時')
|
||||
expect(formatDuration(3700000, 'en', 'hh[hr] mm[min]')).toBe('1 hr 1 min')
|
||||
expect(formatDuration(3700000, 'zh-CN', 'hh[hr] mm[min]')).toBe(
|
||||
'1 小时 1 分钟'
|
||||
)
|
||||
expect(formatDuration(3700000, 'zh-TW', 'hh[hr] mm[min]')).toBe(
|
||||
'1 小時 1 分鐘'
|
||||
)
|
||||
expect(formatDuration(3700000, 'zh-CN', 'hh[hr] mm[min]')).toBe('1 小时 1 分钟')
|
||||
expect(formatDuration(3700000, 'zh-TW', 'hh[hr] mm[min]')).toBe('1 小時 1 分鐘')
|
||||
|
||||
expect(formatDuration(0)).toBe('0:00')
|
||||
expect(formatDuration(0, 'en', 'hh[hr] mm[min]')).toBe('0 min')
|
||||
|
|
@ -67,7 +63,7 @@ describe('cacheCoverColor', () => {
|
|||
vi.stubGlobal('ipcRenderer', {
|
||||
send: (channel: IpcChannels, ...args: any[]) => {
|
||||
expect(channel).toBe(IpcChannels.CacheCoverColor)
|
||||
expect(args[0].api).toBe(APIs.CoverColor)
|
||||
expect(args[0].api).toBe(CacheAPIs.CoverColor)
|
||||
expect(args[0].query).toEqual({
|
||||
id: '109951165911363',
|
||||
color: '#fff',
|
||||
|
|
@ -169,7 +165,7 @@ describe('getCoverColor', () => {
|
|||
vi.stubGlobal('ipcRenderer', {
|
||||
sendSync: (channel: IpcChannels, ...args: any[]) => {
|
||||
expect(channel).toBe(IpcChannels.GetApiCache)
|
||||
expect(args[0].api).toBe(APIs.CoverColor)
|
||||
expect(args[0].api).toBe(CacheAPIs.CoverColor)
|
||||
expect(args[0].query).toEqual({
|
||||
id: '109951165911363',
|
||||
})
|
||||
|
|
|
|||
|
|
@ -1,7 +1,7 @@
|
|||
import { IpcChannels } from '@/shared/IpcChannels'
|
||||
import dayjs from 'dayjs'
|
||||
import duration from 'dayjs/plugin/duration'
|
||||
import { APIs } from '@/shared/CacheAPIs'
|
||||
import { CacheAPIs } from '@/shared/CacheAPIs'
|
||||
import { average } from 'color.js'
|
||||
import { colord } from 'colord'
|
||||
import { supportedLanguages } from '../i18n/i18n'
|
||||
|
|
@ -11,10 +11,7 @@ import { supportedLanguages } from '../i18n/i18n'
|
|||
* @param {string} url 封面图片URL
|
||||
* @param {'xs'|'sm'|'md'|'lg'} size - 大小,值对应为 128px | 256px | 512px | 1024px
|
||||
*/
|
||||
export function resizeImage(
|
||||
url: string,
|
||||
size: 'xs' | 'sm' | 'md' | 'lg'
|
||||
): string {
|
||||
export function resizeImage(url: string, size: 'xs' | 'sm' | 'md' | 'lg'): string {
|
||||
if (!url) return ''
|
||||
|
||||
const sizeMap = {
|
||||
|
|
@ -87,9 +84,7 @@ export function formatDuration(
|
|||
const seconds = time.seconds().toString().padStart(2, '0')
|
||||
|
||||
if (format === 'hh:mm:ss') {
|
||||
return hours !== '0'
|
||||
? `${hours}:${mins.padStart(2, '0')}:${seconds}`
|
||||
: `${mins}:${seconds}`
|
||||
return hours !== '0' ? `${hours}:${mins.padStart(2, '0')}:${seconds}` : `${mins}:${seconds}`
|
||||
} else {
|
||||
const units = {
|
||||
'en-US': {
|
||||
|
|
@ -107,23 +102,17 @@ export function formatDuration(
|
|||
} as const
|
||||
|
||||
return hours !== '0'
|
||||
? `${hours} ${units[locale].hours}${
|
||||
mins === '0' ? '' : ` ${mins} ${units[locale].mins}`
|
||||
}`
|
||||
? `${hours} ${units[locale].hours}${mins === '0' ? '' : ` ${mins} ${units[locale].mins}`}`
|
||||
: `${mins} ${units[locale].mins}`
|
||||
}
|
||||
}
|
||||
|
||||
export function scrollToTop(smooth = false) {
|
||||
document
|
||||
.querySelector('#main')
|
||||
?.scrollTo({ top: 0, behavior: smooth ? 'smooth' : 'auto' })
|
||||
document.querySelector('#main')?.scrollTo({ top: 0, behavior: smooth ? 'smooth' : 'auto' })
|
||||
}
|
||||
|
||||
export function scrollToBottom(smooth = false) {
|
||||
document
|
||||
.querySelector('#main')
|
||||
?.scrollTo({ top: 100000, behavior: smooth ? 'smooth' : 'auto' })
|
||||
document.querySelector('#main')?.scrollTo({ top: 100000, behavior: smooth ? 'smooth' : 'auto' })
|
||||
}
|
||||
|
||||
export async function getCoverColor(coverUrl: string) {
|
||||
|
|
@ -137,7 +126,7 @@ export async function getCoverColor(coverUrl: string) {
|
|||
const colorFromCache: string | undefined = await window.ipcRenderer?.invoke(
|
||||
IpcChannels.GetApiCache,
|
||||
{
|
||||
api: APIs.CoverColor,
|
||||
api: CacheAPIs.CoverColor,
|
||||
query: {
|
||||
id,
|
||||
},
|
||||
|
|
@ -177,12 +166,9 @@ export async function calcCoverColor(coverUrl: string) {
|
|||
}
|
||||
|
||||
export const isIOS = /iPad|iPhone|iPod/.test(navigator.userAgent)
|
||||
export const isSafari = /^((?!chrome|android).)*safari/i.test(
|
||||
navigator.userAgent
|
||||
)
|
||||
export const isSafari = /^((?!chrome|android).)*safari/i.test(navigator.userAgent)
|
||||
export const isPWA =
|
||||
(navigator as any).standalone ||
|
||||
window.matchMedia('(display-mode: standalone)').matches
|
||||
(navigator as any).standalone || window.matchMedia('(display-mode: standalone)').matches
|
||||
export const isIosPwa = isIOS && isPWA && isSafari
|
||||
|
||||
export const sleep = (ms: number) => new Promise(r => setTimeout(r, ms))
|
||||
|
|
|
|||
|
|
@ -28,10 +28,7 @@ interface TrackListSource {
|
|||
type: TrackListSourceType
|
||||
id: number
|
||||
}
|
||||
interface RemoteDevice {
|
||||
protocol: 'airplay' | 'chromecast'
|
||||
id: string
|
||||
}
|
||||
|
||||
export enum Mode {
|
||||
TrackList = 'trackList',
|
||||
FM = 'fm',
|
||||
|
|
@ -62,7 +59,6 @@ export class Player {
|
|||
fmTrackList: TrackID[] = []
|
||||
shuffle: boolean = false
|
||||
fmTrack: Track | null = null
|
||||
remoteDevice: RemoteDevice | null = null
|
||||
|
||||
init(params: { [key: string]: any }) {
|
||||
if (params._track) this._track = params._track
|
||||
|
|
@ -173,10 +169,6 @@ export class Player {
|
|||
window.ipcRenderer?.send(IpcChannels.Repeat, { mode: this._repeatMode })
|
||||
}
|
||||
|
||||
get _isAirplay() {
|
||||
return this.remoteDevice?.protocol === 'airplay'
|
||||
}
|
||||
|
||||
private async _initFM() {
|
||||
if (this.fmTrackList.length === 0) await this._loadMoreFMTracks()
|
||||
|
||||
|
|
@ -194,27 +186,9 @@ export class Player {
|
|||
}
|
||||
|
||||
private async _setupProgressInterval() {
|
||||
if (this.remoteDevice === null) {
|
||||
// Howler
|
||||
this._progressInterval = setInterval(() => {
|
||||
if (this.state === State.Playing) this._progress = _howler.seek()
|
||||
}, 1000)
|
||||
} else if (this._isAirplay) {
|
||||
// Airplay
|
||||
// let isFetchAirplayPlayingInfo = false
|
||||
// this._progressInterval = setInterval(async () => {
|
||||
// if (isFetchAirplayPlayingInfo) return
|
||||
// isFetchAirplayPlayingInfo = true
|
||||
// const playingInfo = await window?.ipcRenderer?.invoke(
|
||||
// 'airplay-get-playing',
|
||||
// { deviceID: this.remoteDevice?.id }
|
||||
// )
|
||||
// if (playingInfo) {
|
||||
// this._progress = playingInfo.position || 0
|
||||
// }
|
||||
// isFetchAirplayPlayingInfo = false
|
||||
// }, 1000)
|
||||
}
|
||||
this._progressInterval = setInterval(() => {
|
||||
if (this.state === State.Playing) this._progress = _howler.seek()
|
||||
}, 1000)
|
||||
}
|
||||
|
||||
private async _scrobble() {
|
||||
|
|
@ -285,23 +259,12 @@ export class Player {
|
|||
return
|
||||
}
|
||||
if (this.trackID !== id) return
|
||||
if (this._isAirplay) {
|
||||
this._playAudioViaAirplay(audio)
|
||||
return
|
||||
} else {
|
||||
this._playAudioViaHowler(audio, id, autoplay)
|
||||
}
|
||||
this._playAudioViaHowler(audio, id, autoplay)
|
||||
}
|
||||
|
||||
private async _playAudioViaHowler(
|
||||
audio: string,
|
||||
id: number,
|
||||
autoplay: boolean = true
|
||||
) {
|
||||
private async _playAudioViaHowler(audio: string, id: number, autoplay: boolean = true) {
|
||||
Howler.unload()
|
||||
const url = audio.includes('?')
|
||||
? `${audio}&dash-id=${id}`
|
||||
: `${audio}?dash-id=${id}`
|
||||
const url = audio.includes('?') ? `${audio}&dash-id=${id}` : `${audio}?dash-id=${id}`
|
||||
const howler = new Howl({
|
||||
src: [url],
|
||||
format: ['mp3', 'flac', 'webm'],
|
||||
|
|
@ -325,18 +288,6 @@ export class Player {
|
|||
}
|
||||
}
|
||||
|
||||
private async _playAudioViaAirplay(audio: string) {
|
||||
// if (!this._isAirplay) {
|
||||
// console.log('No airplay device selected')
|
||||
// return
|
||||
// }
|
||||
// const result = await window.ipcRenderer?.invoke('airplay-play-url', {
|
||||
// deviceID: this.remoteDevice?.id,
|
||||
// url: audio,
|
||||
// })
|
||||
// console.log(result)
|
||||
}
|
||||
|
||||
private _howlerOnEndCallback() {
|
||||
if (this.mode !== Mode.FM && this.repeatMode === RepeatMode.One) {
|
||||
_howler.seek(0)
|
||||
|
|
@ -367,18 +318,14 @@ export class Player {
|
|||
if (this.fmTrackList.length === 0) await this._loadMoreFMTracks()
|
||||
this._playTrack()
|
||||
|
||||
this.fmTrackList.length <= 1
|
||||
? await this._loadMoreFMTracks()
|
||||
: this._loadMoreFMTracks()
|
||||
this.fmTrackList.length <= 1 ? await this._loadMoreFMTracks() : this._loadMoreFMTracks()
|
||||
prefetchNextTrack()
|
||||
}
|
||||
|
||||
private async _loadMoreFMTracks() {
|
||||
if (this.fmTrackList.length <= 5) {
|
||||
const response = await fetchPersonalFMWithReactQuery()
|
||||
const ids = (response?.data?.map(r => r.id) ?? []).filter(
|
||||
r => !this.fmTrackList.includes(r)
|
||||
)
|
||||
const ids = (response?.data?.map(r => r.id) ?? []).filter(r => !this.fmTrackList.includes(r))
|
||||
this.fmTrackList.push(...ids)
|
||||
}
|
||||
}
|
||||
|
|
@ -476,9 +423,7 @@ export class Player {
|
|||
this._setStateToLoading()
|
||||
this.mode = Mode.TrackList
|
||||
this.trackList = list
|
||||
this._trackIndex = autoPlayTrackID
|
||||
? list.findIndex(t => t === autoPlayTrackID)
|
||||
: 0
|
||||
this._trackIndex = autoPlayTrackID ? list.findIndex(t => t === autoPlayTrackID) : 0
|
||||
this._playTrack()
|
||||
}
|
||||
|
||||
|
|
@ -552,10 +497,7 @@ export class Player {
|
|||
async playFM() {
|
||||
this._setStateToLoading()
|
||||
this.mode = Mode.FM
|
||||
if (
|
||||
this.fmTrackList.length > 0 &&
|
||||
this.fmTrack?.id === this.fmTrackList[0]
|
||||
) {
|
||||
if (this.fmTrackList.length > 0 && this.fmTrack?.id === this.fmTrackList[0]) {
|
||||
this._track = this.fmTrack
|
||||
this._playAudio()
|
||||
} else {
|
||||
|
|
@ -584,29 +526,12 @@ export class Player {
|
|||
this._playTrack()
|
||||
}
|
||||
|
||||
async switchToThisComputer() {
|
||||
this.remoteDevice = null
|
||||
clearInterval(this._progressInterval)
|
||||
this._setupProgressInterval()
|
||||
}
|
||||
|
||||
async switchToAirplayDevice(deviceID: string) {
|
||||
this.remoteDevice = {
|
||||
protocol: 'airplay',
|
||||
id: deviceID,
|
||||
}
|
||||
clearInterval(this._progressInterval)
|
||||
this._setupProgressInterval()
|
||||
}
|
||||
|
||||
private async _initMediaSession() {
|
||||
console.log('init')
|
||||
if ('mediaSession' in navigator === false) return
|
||||
navigator.mediaSession.setActionHandler('play', () => this.play())
|
||||
navigator.mediaSession.setActionHandler('pause', () => this.pause())
|
||||
navigator.mediaSession.setActionHandler('previoustrack', () =>
|
||||
this.prevTrack()
|
||||
)
|
||||
navigator.mediaSession.setActionHandler('previoustrack', () => this.prevTrack())
|
||||
navigator.mediaSession.setActionHandler('nexttrack', () => this.nextTrack())
|
||||
navigator.mediaSession.setActionHandler('seekto', event => {
|
||||
if (event.seekTime) this.progress = event.seekTime
|
||||
|
|
|
|||
365
pnpm-lock.yaml
generated
365
pnpm-lock.yaml
generated
|
|
@ -24,19 +24,16 @@ importers:
|
|||
specifiers:
|
||||
'@fastify/cookie': ^8.3.0
|
||||
'@fastify/http-proxy': ^8.4.0
|
||||
'@fastify/multipart': ^7.4.0
|
||||
'@prisma/client': ^4.8.1
|
||||
'@prisma/engines': ^4.9.0
|
||||
'@sentry/electron': ^3.0.7
|
||||
'@types/better-sqlite3': ^7.6.3
|
||||
'@types/cookie-parser': ^1.4.3
|
||||
'@types/express': ^4.17.15
|
||||
'@types/express-fileupload': ^1.2.3
|
||||
'@vitest/ui': ^0.20.3
|
||||
NeteaseCloudMusicApi: ^4.8.7
|
||||
'@yimura/scraper': ^1.2.4
|
||||
NeteaseCloudMusicApi: ^4.8.9
|
||||
axios: ^1.2.1
|
||||
better-sqlite3: 8.0.1
|
||||
change-case: ^4.1.2
|
||||
compare-versions: ^4.1.3
|
||||
connect-history-api-fallback: ^2.0.0
|
||||
cookie-parser: ^1.4.6
|
||||
cross-env: ^7.0.3
|
||||
dotenv: ^16.0.3
|
||||
electron: ^22.0.0
|
||||
|
|
@ -47,8 +44,6 @@ importers:
|
|||
electron-releases: ^3.1171.0
|
||||
electron-store: ^8.1.0
|
||||
esbuild: ^0.16.10
|
||||
express: ^4.18.2
|
||||
express-fileupload: ^1.4.0
|
||||
fast-folder-size: ^1.7.1
|
||||
minimist: ^1.2.7
|
||||
music-metadata: ^8.1.0
|
||||
|
|
@ -57,34 +52,34 @@ importers:
|
|||
picocolors: ^1.0.0
|
||||
prettier: '*'
|
||||
pretty-bytes: ^6.0.0
|
||||
prisma: ^4.8.1
|
||||
tsx: '*'
|
||||
type-fest: ^3.5.0
|
||||
typescript: '*'
|
||||
vitest: ^0.20.3
|
||||
wait-on: ^7.0.1
|
||||
ytdl-core: ^4.11.2
|
||||
ytsr: ^3.8.0
|
||||
zx: ^7.1.1
|
||||
dependencies:
|
||||
'@fastify/cookie': 8.3.0
|
||||
'@fastify/http-proxy': 8.4.0
|
||||
'@fastify/multipart': 7.4.0
|
||||
'@prisma/client': 4.8.1_prisma@4.8.1
|
||||
'@prisma/engines': 4.9.0
|
||||
'@sentry/electron': 3.0.7
|
||||
NeteaseCloudMusicApi: 4.8.7
|
||||
better-sqlite3: 8.0.1
|
||||
'@yimura/scraper': 1.2.4
|
||||
NeteaseCloudMusicApi: 4.8.9
|
||||
change-case: 4.1.2
|
||||
compare-versions: 4.1.3
|
||||
connect-history-api-fallback: 2.0.0
|
||||
cookie-parser: 1.4.6
|
||||
electron-log: 4.4.8
|
||||
electron-store: 8.1.0
|
||||
express: 4.18.2
|
||||
fast-folder-size: 1.7.1
|
||||
pretty-bytes: 6.0.0
|
||||
type-fest: 3.5.0
|
||||
prisma: 4.8.1
|
||||
ytdl-core: 4.11.2
|
||||
ytsr: 3.8.0
|
||||
zx: 7.1.1
|
||||
devDependencies:
|
||||
'@types/better-sqlite3': 7.6.3
|
||||
'@types/cookie-parser': 1.4.3
|
||||
'@types/express': 4.17.15
|
||||
'@types/express-fileupload': 1.2.3
|
||||
'@vitest/ui': 0.20.3
|
||||
axios: 1.2.1
|
||||
cross-env: 7.0.3
|
||||
|
|
@ -95,7 +90,6 @@ importers:
|
|||
electron-rebuild: 3.2.9
|
||||
electron-releases: 3.1171.0
|
||||
esbuild: 0.16.10
|
||||
express-fileupload: 1.4.0
|
||||
minimist: 1.2.7
|
||||
music-metadata: 8.1.0
|
||||
open-cli: 7.1.0
|
||||
|
|
@ -3180,6 +3174,13 @@ packages:
|
|||
pkg-up: 3.1.0
|
||||
dev: false
|
||||
|
||||
/@fastify/busboy/1.2.1:
|
||||
resolution: {integrity: sha512-7PQA7EH43S0CxcOa9OeAnaeA0oQ+e/DHNPZwSQM9CQHW76jle5+OvLdibRp/Aafs9KXbLhxyjOTkRjWUbQEd3Q==}
|
||||
engines: {node: '>=14'}
|
||||
dependencies:
|
||||
text-decoding: 1.0.0
|
||||
dev: false
|
||||
|
||||
/@fastify/cookie/8.3.0:
|
||||
resolution: {integrity: sha512-P9hY9GO11L20TnZ33XN3i0bt+3x0zaT7S0ohAzWO950E9PB2xnNhLYzPFJIGFi5AVN0yr5+/iZhWxeYvR6KCzg==}
|
||||
dependencies:
|
||||
|
|
@ -3211,6 +3212,19 @@ packages:
|
|||
- utf-8-validate
|
||||
dev: false
|
||||
|
||||
/@fastify/multipart/7.4.0:
|
||||
resolution: {integrity: sha512-jl8KCMOjzniAMnF2/VdYFhGB03Oqtl24plxcnsKpdnRLu/ihVz4cNPz9bPn8mLUQW4r3dUlh6emINtZdJczkbg==}
|
||||
dependencies:
|
||||
'@fastify/busboy': 1.2.1
|
||||
'@fastify/deepmerge': 1.1.0
|
||||
'@fastify/error': 3.0.0
|
||||
end-of-stream: 1.4.4
|
||||
fastify-plugin: 4.5.0
|
||||
hexoid: 1.0.0
|
||||
secure-json-parse: 2.5.0
|
||||
stream-wormhole: 1.1.0
|
||||
dev: false
|
||||
|
||||
/@fastify/reply-from/8.3.1:
|
||||
resolution: {integrity: sha512-fRByAvTMXuBuYIimcinukOB3YdmqtYPeoybXIBNY0aPVgetHkmCVffBo/M4pEOib9Pes8wuoYL4VawI65aHl4w==}
|
||||
dependencies:
|
||||
|
|
@ -3681,6 +3695,11 @@ packages:
|
|||
resolution: {integrity: sha512-93tctjNXcIS+i/e552IO6tqw17sX8liivv8WX9lDMCpEEe3ci+nT9F+1oHtAafqruXLepKF80i/D20Mm+ESlOw==}
|
||||
requiresBuild: true
|
||||
|
||||
/@prisma/engines/4.9.0:
|
||||
resolution: {integrity: sha512-t1pt0Gsp+HcgPJrHFc+d/ZSAaKKWar2G/iakrE07yeKPNavDP3iVKPpfXP22OTCHZUWf7OelwKJxQgKAm5hkgw==}
|
||||
requiresBuild: true
|
||||
dev: false
|
||||
|
||||
/@remix-run/router/1.2.1:
|
||||
resolution: {integrity: sha512-XiY0IsyHR+DXYS5vBxpoBe/8veTeoRpMHP+vDosLZxL5bnpetzI0igkxkLZS235ldLzyfkxF+2divEwWHP3vMQ==}
|
||||
engines: {node: '>=14'}
|
||||
|
|
@ -5571,25 +5590,6 @@ packages:
|
|||
resolution: {integrity: sha512-HnYpAE1Y6kRyKM/XkEuiRQhTHvkzMBurTHnpFLYLBGPIylZNPs9jJcuOOYWxPLJCSEtmZT0Y8rHDokKN7rRTig==}
|
||||
dev: true
|
||||
|
||||
/@types/better-sqlite3/7.6.3:
|
||||
resolution: {integrity: sha512-YS64N9SNDT/NAvou3QNdzAu3E2om/W/0dhORimtPGLef+zSK5l1vDzfsWb4xgXOgfhtOI5ZDTRxnvRPb22AIVQ==}
|
||||
dependencies:
|
||||
'@types/node': 18.6.4
|
||||
dev: true
|
||||
|
||||
/@types/body-parser/1.19.2:
|
||||
resolution: {integrity: sha512-ALYone6pm6QmwZoAgeyNksccT9Q4AWZQ6PvfwR37GT6r6FWUPguq6sUmNGSMV2Wr761oQoBxwGGa6DR5o1DC9g==}
|
||||
dependencies:
|
||||
'@types/connect': 3.4.35
|
||||
'@types/node': 18.6.4
|
||||
dev: true
|
||||
|
||||
/@types/busboy/0.3.2:
|
||||
resolution: {integrity: sha512-iEvdm9Z9KdSs/ozuh1Z7ZsXrOl8F4M/CLMXPZHr3QuJ4d6Bjn+HBMC5EMKpwpAo8oi8iK9GZfFoHaIMrrZgwVw==}
|
||||
dependencies:
|
||||
'@types/node': 18.6.4
|
||||
dev: true
|
||||
|
||||
/@types/cacheable-request/6.0.2:
|
||||
resolution: {integrity: sha512-B3xVo+dlKM6nnKTcmm5ZtY/OL8bOAOd2Olee9M1zft65ox50OzjEHW91sDiU9j6cvW8Ejg1/Qkf4xd2kugApUA==}
|
||||
dependencies:
|
||||
|
|
@ -5613,18 +5613,6 @@ packages:
|
|||
resolution: {integrity: sha512-KnRanxnpfpjUTqTCXslZSEdLfXExwgNxYPdiO2WGUj8+HDjFi8R3k5RVKPeSCzLjCcshCAtVO2QBbVuAV4kTnw==}
|
||||
dev: true
|
||||
|
||||
/@types/connect/3.4.35:
|
||||
resolution: {integrity: sha512-cdeYyv4KWoEgpBISTxWvqYsVy444DOqehiF3fM3ne10AmJ62RSyNkUnxMJXHQWRQQX2eR94m5y1IZyDwBjV9FQ==}
|
||||
dependencies:
|
||||
'@types/node': 18.6.4
|
||||
dev: true
|
||||
|
||||
/@types/cookie-parser/1.4.3:
|
||||
resolution: {integrity: sha512-CqSKwFwefj4PzZ5n/iwad/bow2hTCh0FlNAeWLtQM3JA/NX/iYagIpWG2cf1bQKQ2c9gU2log5VUCrn7LDOs0w==}
|
||||
dependencies:
|
||||
'@types/express': 4.17.15
|
||||
dev: true
|
||||
|
||||
/@types/debug/4.1.7:
|
||||
resolution: {integrity: sha512-9AonUzyTjXXhEOa0DnqpzZi6VHlqKMswga9EXjpXnnqxwLtdvPPtlO8evrI5D9S6asFRCQ6v+wpiUKbw+vKqyg==}
|
||||
dependencies:
|
||||
|
|
@ -5657,30 +5645,6 @@ packages:
|
|||
resolution: {integrity: sha512-WulqXMDUTYAXCjZnk6JtIHPigp55cVtDgDrO2gHRwhyJto21+1zbVCtOYB2L1F9w4qCQ0rOGWBnBe0FNTiEJIQ==}
|
||||
dev: true
|
||||
|
||||
/@types/express-fileupload/1.2.3:
|
||||
resolution: {integrity: sha512-TaMmhgnWr5Nb/bJ7zPSNSTBMQTX7vwKzv8+HqX1vZtytXw/DzcvX/IzjENO2BdzttJfmWofzi0aRio6cXWPuAw==}
|
||||
dependencies:
|
||||
'@types/busboy': 0.3.2
|
||||
'@types/express': 4.17.15
|
||||
dev: true
|
||||
|
||||
/@types/express-serve-static-core/4.17.31:
|
||||
resolution: {integrity: sha512-DxMhY+NAsTwMMFHBTtJFNp5qiHKJ7TeqOo23zVEM9alT1Ml27Q3xcTH0xwxn7Q0BbMcVEJOs/7aQtUWupUQN3Q==}
|
||||
dependencies:
|
||||
'@types/node': 18.6.4
|
||||
'@types/qs': 6.9.7
|
||||
'@types/range-parser': 1.2.4
|
||||
dev: true
|
||||
|
||||
/@types/express/4.17.15:
|
||||
resolution: {integrity: sha512-Yv0k4bXGOH+8a+7bELd2PqHQsuiANB+A8a4gnQrkRWzrkKlb6KHaVvyXhqs04sVW/OWlbPyYxRgYlIXLfrufMQ==}
|
||||
dependencies:
|
||||
'@types/body-parser': 1.19.2
|
||||
'@types/express-serve-static-core': 4.17.31
|
||||
'@types/qs': 6.9.7
|
||||
'@types/serve-static': 1.15.0
|
||||
dev: true
|
||||
|
||||
/@types/fs-extra/9.0.13:
|
||||
resolution: {integrity: sha512-nEnwB++1u5lVDM2UI4c1+5R+FYaKfaAzS4OococimjVm3nQw3TuzH5UNsocrcTBbhnerblyHj4A49qXbIiZdpA==}
|
||||
dependencies:
|
||||
|
|
@ -5691,7 +5655,7 @@ packages:
|
|||
requiresBuild: true
|
||||
dependencies:
|
||||
'@types/minimatch': 3.0.5
|
||||
'@types/node': 18.6.4
|
||||
'@types/node': 18.11.17
|
||||
dev: true
|
||||
|
||||
/@types/graceful-fs/4.1.5:
|
||||
|
|
@ -5780,10 +5744,6 @@ packages:
|
|||
'@types/unist': 2.0.6
|
||||
dev: true
|
||||
|
||||
/@types/mime/3.0.1:
|
||||
resolution: {integrity: sha512-Y4XFY5VJAuw0FgAqPNd6NNoV44jbq9Bz2L7Rh/J6jLTiHBSBJa9fxqQIvkIld4GsoDOcCbvzOUAbLPsSKKg+uA==}
|
||||
dev: true
|
||||
|
||||
/@types/minimatch/3.0.5:
|
||||
resolution: {integrity: sha512-Klz949h02Gz2uZCMGwDUSDS1YBlTdDDgbWHi+81l29tQALUtvz4rAYi5uoVhE5Lagoq6DeqAUlbrHvW/mXDgdQ==}
|
||||
dev: true
|
||||
|
|
@ -5858,10 +5818,6 @@ packages:
|
|||
resolution: {integrity: sha512-FGa1F62FT09qcrueBA6qYTrJPVDzah9a+493+o2PCXsesWHIn27G98TsSMs3WPNbZIEj4+VJf6saSFpvD+3Zsw==}
|
||||
dev: true
|
||||
|
||||
/@types/range-parser/1.2.4:
|
||||
resolution: {integrity: sha512-EEhsLsD6UsDM1yFhAvy0Cjr6VwmpMWqFBCb9w07wVugF7w9nfajxLuVmngTIpgS6svCnm6Vaw+MZhoDCKnOfsw==}
|
||||
dev: true
|
||||
|
||||
/@types/react-dom/18.0.6:
|
||||
resolution: {integrity: sha512-/5OFZgfIPSwy+YuIBP/FgJnQnsxhZhjjrnxudMddeblOouIodEQ75X14Rr4wGSG/bknL+Omy9iWlLo1u/9GzAA==}
|
||||
dependencies:
|
||||
|
|
@ -5892,13 +5848,6 @@ packages:
|
|||
resolution: {integrity: sha512-hppQEBDmlwhFAXKJX2KnWLYu5yMfi91yazPb2l+lbJiwW+wdo1gNeRA+3RgNSO39WYX2euey41KEwnqesU2Jew==}
|
||||
dev: true
|
||||
|
||||
/@types/serve-static/1.15.0:
|
||||
resolution: {integrity: sha512-z5xyF6uh8CbjAu9760KDKsH2FcDxZ2tFCsA4HIMWE6IkiYMXfVoa+4f9KX+FN0ZLsaMw1WNG2ETLA6N+/YA+cg==}
|
||||
dependencies:
|
||||
'@types/mime': 3.0.1
|
||||
'@types/node': 18.6.4
|
||||
dev: true
|
||||
|
||||
/@types/source-list-map/0.1.2:
|
||||
resolution: {integrity: sha512-K5K+yml8LTo9bWJI/rECfIPrGgxdpeNbj+d53lwN4QjW1MCwlkhUms+gtdzigTeUyBr09+u8BwOIY3MXvHdcsA==}
|
||||
dev: true
|
||||
|
|
@ -6287,6 +6236,10 @@ packages:
|
|||
resolution: {integrity: sha512-NuHqBY1PB/D8xU6s/thBgOAiAP7HOYDQ32+BFZILJ8ivkUkAHQnWfn6WhL79Owj1qmUnoN/YPhktdIoucipkAQ==}
|
||||
dev: true
|
||||
|
||||
/@yimura/scraper/1.2.4:
|
||||
resolution: {integrity: sha512-lHx/NqqZBOuDyYdUbXIigLGKKm2+Wtrx4rxGJ1dObZhOyth3CzFZbb1/SykVVzxxGYkFkhD9HHf74UWelaP2mw==}
|
||||
dev: false
|
||||
|
||||
/NeteaseCloudMusicApi/4.8.7:
|
||||
resolution: {integrity: sha512-f+Z/lar+IOcrzGeDKHNQaTnlNE7nLRiD9Nb/KoLmKC32aUnnCerFTFV2/ZWlEC4YokP/4mXOZXvo8wXA8PQhVA==}
|
||||
engines: {node: '>=12'}
|
||||
|
|
@ -6307,6 +6260,26 @@ packages:
|
|||
- supports-color
|
||||
dev: false
|
||||
|
||||
/NeteaseCloudMusicApi/4.8.9:
|
||||
resolution: {integrity: sha512-3YcbljD8nGn0KsjoPq6pBxZSuu3Sk4M3c8ZZhdvEAL+P+1noYftq/ZhA+CY0uQXLMkgbHRpLapBdGXMXhhB6/Q==}
|
||||
engines: {node: '>=12'}
|
||||
hasBin: true
|
||||
dependencies:
|
||||
axios: 1.2.2
|
||||
express: 4.18.2
|
||||
express-fileupload: 1.4.0
|
||||
md5: 2.3.0
|
||||
music-metadata: 7.12.6
|
||||
pac-proxy-agent: 5.0.0
|
||||
qrcode: 1.5.1
|
||||
safe-decode-uri-component: 1.2.1
|
||||
tunnel: 0.0.6
|
||||
yargs: 17.5.1
|
||||
transitivePeerDependencies:
|
||||
- debug
|
||||
- supports-color
|
||||
dev: false
|
||||
|
||||
/abab/2.0.6:
|
||||
resolution: {integrity: sha512-j2afSsaIENvHZN2B8GOpF566vZ5WVk5opAiMTvWgaQT8DkbOqsTfvNAvHoRGU2zzP8cPoqys+xHTRDWW8L+/BA==}
|
||||
dev: true
|
||||
|
|
@ -7172,6 +7145,7 @@ packages:
|
|||
|
||||
/base64-js/1.5.1:
|
||||
resolution: {integrity: sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA==}
|
||||
dev: true
|
||||
|
||||
/better-opn/2.1.1:
|
||||
resolution: {integrity: sha512-kIPXZS5qwyKiX/HcRvDYfmBQUa8XP17I0mYZZ0y4UhpYOSvtsLHDYqmomS+Mj20aDvD3knEiQ0ecQy2nhio3yA==}
|
||||
|
|
@ -7180,14 +7154,6 @@ packages:
|
|||
open: 7.4.2
|
||||
dev: true
|
||||
|
||||
/better-sqlite3/8.0.1:
|
||||
resolution: {integrity: sha512-JhTZjpyapA1icCEjIZB4TSSgkGdFgpWZA2Wszg7Cf4JwJwKQmbvuNnJBeR+EYG/Z29OXvR4G//Rbg31BW/Z7Yg==}
|
||||
requiresBuild: true
|
||||
dependencies:
|
||||
bindings: 1.5.0
|
||||
prebuild-install: 7.1.1
|
||||
dev: false
|
||||
|
||||
/big-integer/1.6.51:
|
||||
resolution: {integrity: sha512-GPEid2Y9QU1Exl1rpO9B2IPJGHPSupF5GnVIP0blYvNOMer2bTvSWs1jGOUg04hTmu67nmLsQ9TBo1puaotBHg==}
|
||||
engines: {node: '>=0.6'}
|
||||
|
|
@ -7217,6 +7183,8 @@ packages:
|
|||
resolution: {integrity: sha512-p2q/t/mhvuOj/UeLlV6566GD/guowlr0hHxClI0W9m7MWYkL1F0hLo+0Aexs9HSPCtR1SXQ0TD3MMKrXZajbiQ==}
|
||||
dependencies:
|
||||
file-uri-to-path: 1.0.0
|
||||
dev: true
|
||||
optional: true
|
||||
|
||||
/bl/4.1.0:
|
||||
resolution: {integrity: sha512-1W07cM9gS6DcLperZfFSj+bWLtaPGSOHWhPiGzXmvVJbRLdG82sH/Kn8EtW1VqWVA54AKf2h5k5BbnIbwF3h6w==}
|
||||
|
|
@ -7224,6 +7192,7 @@ packages:
|
|||
buffer: 5.7.1
|
||||
inherits: 2.0.4
|
||||
readable-stream: 3.6.0
|
||||
dev: true
|
||||
|
||||
/bl/5.0.0:
|
||||
resolution: {integrity: sha512-8vxFNZ0pflFfi0WXA3WQXlj6CaMEwsmh63I1CNp0q+wWv8sD0ARx1KovSQd0l2GkwrMIOyedq0EF1FxI+RCZLQ==}
|
||||
|
|
@ -7465,6 +7434,7 @@ packages:
|
|||
dependencies:
|
||||
base64-js: 1.5.1
|
||||
ieee754: 1.2.1
|
||||
dev: true
|
||||
|
||||
/buffer/6.0.3:
|
||||
resolution: {integrity: sha512-FTiCpNxtwiZZHEZbcbTIcZjERVICn9yq/pDFkTl95/AxzD1naBctN7YO68riM/gLSDY7sdrMby8hofADYuuqOA==}
|
||||
|
|
@ -7526,6 +7496,7 @@ packages:
|
|||
engines: {node: '>=10.16.0'}
|
||||
dependencies:
|
||||
streamsearch: 1.1.0
|
||||
dev: false
|
||||
|
||||
/bytes/3.0.0:
|
||||
resolution: {integrity: sha512-pMhOfFDPiv9t5jjIXkHosWmkSyQbvsgEVNkz0ERHbuLh2T/7j4Mqqpz523Fe8MVY89KC6Sh/QfS2sM+SjgFDcw==}
|
||||
|
|
@ -7884,6 +7855,7 @@ packages:
|
|||
|
||||
/chownr/1.1.4:
|
||||
resolution: {integrity: sha512-jJ0bqzaylmJtVnNgzTeSOs8DPavpbYgEr/b0YL8/2GO3xJEhInFmhKMUnEJQjZumK7KXGFhUy89PrsJWlakBVg==}
|
||||
dev: true
|
||||
|
||||
/chownr/2.0.0:
|
||||
resolution: {integrity: sha512-bIomtDF5KGpdogkLd9VspvFzk9KfpyyGlS8YFVZl7TGPBHL5snIOnxeshwVgPteQ9b4Eydl+pVbIyE1DcvCWgQ==}
|
||||
|
|
@ -8227,11 +8199,6 @@ packages:
|
|||
semver: 7.3.7
|
||||
dev: false
|
||||
|
||||
/connect-history-api-fallback/2.0.0:
|
||||
resolution: {integrity: sha512-U73+6lQFmfiNPrYbXqr6kZ1i1wiRqXnp2nhMsINseWXO8lDau0LGEffJ8kQi4EjLZympVgRdvqjAgiZ1tgzDDA==}
|
||||
engines: {node: '>=0.8'}
|
||||
dev: false
|
||||
|
||||
/console-browserify/1.2.0:
|
||||
resolution: {integrity: sha512-ZMkYO/LkF17QvCPqM0gxw8yUzigAOZOSWSHg91FH6orS7vcEj5dVZTidN2fQ14yBSdg97RqhSNwLUXInd52OTA==}
|
||||
dev: true
|
||||
|
|
@ -8267,22 +8234,9 @@ packages:
|
|||
dependencies:
|
||||
safe-buffer: 5.1.2
|
||||
|
||||
/cookie-parser/1.4.6:
|
||||
resolution: {integrity: sha512-z3IzaNjdwUC2olLIB5/ITd0/setiaFMLYiZJle7xg5Fe9KWAceil7xszYfHHBtDFYLSgJduS2Ty0P1uJdPDJeA==}
|
||||
engines: {node: '>= 0.8.0'}
|
||||
dependencies:
|
||||
cookie: 0.4.1
|
||||
cookie-signature: 1.0.6
|
||||
dev: false
|
||||
|
||||
/cookie-signature/1.0.6:
|
||||
resolution: {integrity: sha512-QADzlaHc8icV8I7vbaJXJwod9HWYp8uCqf1xa4OfNu1T7JVxQIrUgOWtHdNDtPiywmFbiS12VjotIXLrKM3orQ==}
|
||||
|
||||
/cookie/0.4.1:
|
||||
resolution: {integrity: sha512-ZwrFkGJxUR3EIoXtO+yVE69Eb7KlixbaeAWfBQB9vVsNn/o+Yw69gBWSSDK825hQNdN+wF8zELf3dFNl/kxkUA==}
|
||||
engines: {node: '>= 0.6'}
|
||||
dev: false
|
||||
|
||||
/cookie/0.4.2:
|
||||
resolution: {integrity: sha512-aSWTXFzaKWkvHO1Ny/s+ePFpvKsPnjc551iI41v3ny/ow6tBG5Vd+FuqGNhh1LxOmVzOlGUriIlOaokOvhaStA==}
|
||||
engines: {node: '>= 0.6'}
|
||||
|
|
@ -8722,6 +8676,7 @@ packages:
|
|||
engines: {node: '>=10'}
|
||||
dependencies:
|
||||
mimic-response: 3.1.0
|
||||
dev: true
|
||||
|
||||
/dedent/0.7.0:
|
||||
resolution: {integrity: sha512-Q6fKUPqnAHAyhiUgFU7BUzLiv0kd8saH9al7tnu5Q/okj6dnupxyTgFIBjVzJATdfIAm9NAsvXNzjaKa+bxVyA==}
|
||||
|
|
@ -8741,11 +8696,6 @@ packages:
|
|||
type-detect: 4.0.8
|
||||
dev: true
|
||||
|
||||
/deep-extend/0.6.0:
|
||||
resolution: {integrity: sha512-LOHxIOaPYdHlJRtCQfDIVZtfw/ufM8+rVj649RIHzcm/vGwQRXFt6OPqIFWsm2XEMrNIEtWR64sY1LEKD2vAOA==}
|
||||
engines: {node: '>=4.0.0'}
|
||||
dev: false
|
||||
|
||||
/deep-is/0.1.4:
|
||||
resolution: {integrity: sha512-oIPzksmTg4/MriiaYGO+okXDT7ztn/w3Eptv/+gSIdMdKsJo0u4CfYNFJPy+4SKMuCqGw2wxnA+URMg3t8a/bQ==}
|
||||
|
||||
|
|
@ -8876,6 +8826,7 @@ packages:
|
|||
/detect-libc/2.0.1:
|
||||
resolution: {integrity: sha512-463v3ZeIrcWtdgIg6vI6XUncguvr2TnGl4SzDXinkt9mSLpBJKXT3mW6xT3VQdDN11+WVs29pgvivTc4Lp8v+w==}
|
||||
engines: {node: '>=8'}
|
||||
dev: true
|
||||
|
||||
/detect-node/2.1.0:
|
||||
resolution: {integrity: sha512-T0NIuQpnTvFDATNuHN5roPwSBG83rFsuO+MXXH9/3N1eFbn4wcPjttvjMLEPWJ0RGUYgQE7cGgS3tNxbqCGM7g==}
|
||||
|
|
@ -10236,16 +10187,12 @@ packages:
|
|||
- supports-color
|
||||
dev: true
|
||||
|
||||
/expand-template/2.0.3:
|
||||
resolution: {integrity: sha512-XYfuKMvj4O35f/pOXLObndIRvyQ+/+6AhODh+OKWj9S9498pHHn/IMszH+gt0fBCRWMNfk1ZSp5x3AifmnI2vg==}
|
||||
engines: {node: '>=6'}
|
||||
dev: false
|
||||
|
||||
/express-fileupload/1.4.0:
|
||||
resolution: {integrity: sha512-RjzLCHxkv3umDeZKeFeMg8w7qe0V09w3B7oGZprr/oO2H/ISCgNzuqzn7gV3HRWb37GjRk429CCpSLS2KNTqMQ==}
|
||||
engines: {node: '>=12.0.0'}
|
||||
dependencies:
|
||||
busboy: 1.6.0
|
||||
dev: false
|
||||
|
||||
/express/4.18.2:
|
||||
resolution: {integrity: sha512-5/PsL6iGPdfQ/lKM1UuielYgv3BUoJfz1aUwU9vHZ+J7gyvwdQXFEBIEIaxeGf0GIcreATNyBExtalisDbuMqQ==}
|
||||
|
|
@ -10373,17 +10320,6 @@ packages:
|
|||
- supports-color
|
||||
dev: true
|
||||
|
||||
/fast-glob/3.2.11:
|
||||
resolution: {integrity: sha512-xrO3+1bxSo3ZVHAnqzyuewYT6aMFHRAd4Kcs92MAonjwQZLsK9d0SF1IyQ3k5PoirxTW0Oe/RqFgMQ6TcNE5Ew==}
|
||||
engines: {node: '>=8.6.0'}
|
||||
dependencies:
|
||||
'@nodelib/fs.stat': 2.0.5
|
||||
'@nodelib/fs.walk': 1.2.8
|
||||
glob-parent: 5.1.2
|
||||
merge2: 1.4.1
|
||||
micromatch: 4.0.5
|
||||
dev: false
|
||||
|
||||
/fast-glob/3.2.12:
|
||||
resolution: {integrity: sha512-DVj4CQIYYow0BlaelwK1pHl5n5cRSJfM60UA0zK891sVInoPri2Ekj7+e1CT3/3qxXenpI+nBBmQAcJPJgaj4w==}
|
||||
engines: {node: '>=8.6.0'}
|
||||
|
|
@ -10393,7 +10329,6 @@ packages:
|
|||
glob-parent: 5.1.2
|
||||
merge2: 1.4.1
|
||||
micromatch: 4.0.5
|
||||
dev: true
|
||||
|
||||
/fast-json-parse/1.0.3:
|
||||
resolution: {integrity: sha512-FRWsaZRWEJ1ESVNbDWmsAlqDk96gPQezzLghafp5J4GUKjbCz3OkAHuZs5TuPEtkbVQERysLp9xv6c24fBm8Aw==}
|
||||
|
|
@ -10579,6 +10514,8 @@ packages:
|
|||
|
||||
/file-uri-to-path/1.0.0:
|
||||
resolution: {integrity: sha512-0Zt+s3L7Vf1biwWZ29aARiVYLx7iMGnEUl9x33fbB/j3jR81u/O2LbqK+Bm1CDSNDKVtJ/YjwY7TUd5SkeLQLw==}
|
||||
dev: true
|
||||
optional: true
|
||||
|
||||
/file-uri-to-path/2.0.0:
|
||||
resolution: {integrity: sha512-hjPFI8oE/2iQPVe4gbrJ73Pp+Xfub2+WI2LlXDbsaJBwT5wuMh35WNWVYYTpnz895shtwfyutMFLFywpQAFdLg==}
|
||||
|
|
@ -10856,10 +10793,6 @@ packages:
|
|||
readable-stream: 2.3.7
|
||||
dev: true
|
||||
|
||||
/fs-constants/1.0.0:
|
||||
resolution: {integrity: sha512-y6OAwoSIf7FyjMIv94u+b5rdheZEjzR63GTyZJm5qh4Bi+2YgwLCcI/fPFZkL5PSixOt6ZNKm+w+Hfp/Bciwow==}
|
||||
dev: false
|
||||
|
||||
/fs-extra/10.1.0:
|
||||
resolution: {integrity: sha512-oRXApq54ETRj4eMiFzGnHWGy+zo5raudjuxN0b8H7s/RU2oW0Wvsx9O0ACRN/kRq9E8Vu/ReskGB5o3ji+FzHQ==}
|
||||
engines: {node: '>=12'}
|
||||
|
|
@ -11096,10 +11029,6 @@ packages:
|
|||
engines: {node: '>=0.10.0'}
|
||||
dev: true
|
||||
|
||||
/github-from-package/0.0.0:
|
||||
resolution: {integrity: sha512-SyHy3T1v2NUXn29OsWdxmK6RwHD+vkj3v8en8AOBZ1wBQ/hCAQ5bAQTD02kW4W9tUp/3Qh6J8r9EvntiyCmOOw==}
|
||||
dev: false
|
||||
|
||||
/github-slugger/1.4.0:
|
||||
resolution: {integrity: sha512-w0dzqw/nt51xMVmlaV1+JRzN+oCa1KfcgGEWhxUG16wbdA+Xnt/yoFO8Z8x/V82ZcZ0wy6ln9QDup5avbhiDhQ==}
|
||||
dev: true
|
||||
|
|
@ -11229,7 +11158,7 @@ packages:
|
|||
engines: {node: ^12.20.0 || ^14.13.1 || >=16.0.0}
|
||||
dependencies:
|
||||
dir-glob: 3.0.1
|
||||
fast-glob: 3.2.11
|
||||
fast-glob: 3.2.12
|
||||
ignore: 5.2.0
|
||||
merge2: 1.4.1
|
||||
slash: 4.0.0
|
||||
|
|
@ -11496,6 +11425,11 @@ packages:
|
|||
readable-stream: 3.6.0
|
||||
dev: false
|
||||
|
||||
/hexoid/1.0.0:
|
||||
resolution: {integrity: sha512-QFLV0taWQOZtvIRIAdBChesmogZrtuXvVWsFHZTk2SU+anspqZ2vMnoLg7IE1+Uk16N19APic1BuF8bC8c2m5g==}
|
||||
engines: {node: '>=8'}
|
||||
dev: false
|
||||
|
||||
/hey-listen/1.0.8:
|
||||
resolution: {integrity: sha512-COpmrF2NOg4TBWUJ5UVyaCU2A88wEMkUPK4hNqyCkqHbxT92BbvfjoSozkAIIm6XhicGlJHhFdullInrdhwU8Q==}
|
||||
dev: false
|
||||
|
|
@ -11813,10 +11747,6 @@ packages:
|
|||
/inherits/2.0.4:
|
||||
resolution: {integrity: sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==}
|
||||
|
||||
/ini/1.3.8:
|
||||
resolution: {integrity: sha512-JV/yugV2uzW5iMRSiZAyDtQd+nxtUnjeLt0acNdw98kKLrvuRVyB80tsREOE7yvGVgalhZ6RNXCmEHkUKBKxew==}
|
||||
dev: false
|
||||
|
||||
/inline-style-parser/0.1.1:
|
||||
resolution: {integrity: sha512-7NXolsK4CAS5+xvdj5OMMbI962hU/wvwoxk+LWR9Ek9bVtyuuYScDN6eS0rUm6TxApFpw7CX1o4uJzcd4AyD3Q==}
|
||||
dev: true
|
||||
|
|
@ -12915,6 +12845,14 @@ packages:
|
|||
readable-stream: 3.6.0
|
||||
dev: true
|
||||
|
||||
/m3u8stream/0.8.6:
|
||||
resolution: {integrity: sha512-LZj8kIVf9KCphiHmH7sbFQTVe4tOemb202fWwvJwR9W5ENW/1hxJN6ksAWGhQgSBSa3jyWhnjKU1Fw1GaOdbyA==}
|
||||
engines: {node: '>=12'}
|
||||
dependencies:
|
||||
miniget: 4.2.2
|
||||
sax: 1.2.4
|
||||
dev: false
|
||||
|
||||
/magic-string/0.25.9:
|
||||
resolution: {integrity: sha512-RmF0AsMzgt25qzqqLc1+MbHmhdx0ojF2Fvs4XnOqz2ZOBXzzkEwc/dJQZCYHAn7v1jbVOjAZfK8msRn4BxO4VQ==}
|
||||
dependencies:
|
||||
|
|
@ -13299,6 +13237,7 @@ packages:
|
|||
/mimic-response/3.1.0:
|
||||
resolution: {integrity: sha512-z0yWI+4FDrrweS8Zmt4Ej5HdJmky15+L2e6Wgn3+iK5fWzb6T3fhNFq2+MeTRb064c6Wr4N/wv0DzQTjNzHNGQ==}
|
||||
engines: {node: '>=10'}
|
||||
dev: true
|
||||
|
||||
/min-document/2.19.0:
|
||||
resolution: {integrity: sha512-9Wy1B3m3f66bPPmU5hdA4DR4PB2OfDU/+GS3yAB7IQozE3tqXaVv2zOjgla7MEGSRv95+ILmOuvhLkOK6wJtCQ==}
|
||||
|
|
@ -13311,6 +13250,11 @@ packages:
|
|||
engines: {node: '>=4'}
|
||||
dev: true
|
||||
|
||||
/miniget/4.2.2:
|
||||
resolution: {integrity: sha512-a7voNL1N5lDMxvTMExOkg+Fq89jM2vY8pAi9ZEWzZtfNmdfP6RXkvUtFnCAXoCv2T9k1v/fUJVaAEuepGcvLYA==}
|
||||
engines: {node: '>=12'}
|
||||
dev: false
|
||||
|
||||
/minimalistic-assert/1.0.1:
|
||||
resolution: {integrity: sha512-UtJcAD4yEaGtjPezWuO9wC4nwUnVH/8/Im3yEHQP4b67cXlD/Qr9hdITCU1xDbSEXg2XKNaP8jsReV7vQd00/A==}
|
||||
dev: true
|
||||
|
|
@ -13426,10 +13370,6 @@ packages:
|
|||
is-extendable: 1.0.1
|
||||
dev: true
|
||||
|
||||
/mkdirp-classic/0.5.3:
|
||||
resolution: {integrity: sha512-gKLcREMhtuZRwRAfqP3RFW+TK4JqApVBtOIftVgjuABpAtpxhPGaDcfvbhNvD0B8iD1oUr/txX35NjcaY6Ns/A==}
|
||||
dev: false
|
||||
|
||||
/mkdirp/0.5.6:
|
||||
resolution: {integrity: sha512-FP+p8RB8OWpF3YZBCrP5gtADmtXApB5AMLn+vdyA+PyxCjrCs00mjyUozssO33cwDeT3wNGdLxJ5M//YqtHAJw==}
|
||||
hasBin: true
|
||||
|
|
@ -13557,10 +13497,6 @@ packages:
|
|||
- supports-color
|
||||
dev: true
|
||||
|
||||
/napi-build-utils/1.0.2:
|
||||
resolution: {integrity: sha512-ONmRUqK7zj7DWX0D9ADe03wbwOBZxNAfF20PlGfCWQcD3+/MakShIHrMqx9YwPTfxDdF1zLeL+RGZiR9kGMLdg==}
|
||||
dev: false
|
||||
|
||||
/natural-compare/1.4.0:
|
||||
resolution: {integrity: sha512-OWND8ei3VtNC9h7V60qff3SVobHr996CTwgxubgyQYEpg290h9J0buyECNNJexkFm5sOajh5G116RYA1c8ZMSw==}
|
||||
dev: true
|
||||
|
|
@ -13597,6 +13533,7 @@ packages:
|
|||
engines: {node: '>=10'}
|
||||
dependencies:
|
||||
semver: 7.3.7
|
||||
dev: true
|
||||
|
||||
/node-addon-api/1.7.2:
|
||||
resolution: {integrity: sha512-ibPK3iA+vaY1eEjESkQkM0BbCqFOaZMiXRTtdB0u7b4djtY6JnsjvPdUHVMg6xQt3B8fpTTWHI9A+ADjM9frzg==}
|
||||
|
|
@ -14709,25 +14646,6 @@ packages:
|
|||
posthtml-render: 1.4.0
|
||||
dev: true
|
||||
|
||||
/prebuild-install/7.1.1:
|
||||
resolution: {integrity: sha512-jAXscXWMcCK8GgCoHOfIr0ODh5ai8mj63L2nWrjuAgXE6tDyYGnx4/8o/rCgU+B4JSyZBKbeZqzhtwtC3ovxjw==}
|
||||
engines: {node: '>=10'}
|
||||
hasBin: true
|
||||
dependencies:
|
||||
detect-libc: 2.0.1
|
||||
expand-template: 2.0.3
|
||||
github-from-package: 0.0.0
|
||||
minimist: 1.2.7
|
||||
mkdirp-classic: 0.5.3
|
||||
napi-build-utils: 1.0.2
|
||||
node-abi: 3.24.0
|
||||
pump: 3.0.0
|
||||
rc: 1.2.8
|
||||
simple-get: 4.0.1
|
||||
tar-fs: 2.1.1
|
||||
tunnel-agent: 0.6.0
|
||||
dev: false
|
||||
|
||||
/prelude-ls/1.1.2:
|
||||
resolution: {integrity: sha512-ESF23V4SKG6lVSGZgYNpbsiaAkdab6ZgOxe52p7+Kid3W3u3bxR4Vfd/o21dmN7jSt0IwgZ4v5MUd26FEtXE9w==}
|
||||
engines: {node: '>= 0.8.0'}
|
||||
|
|
@ -15072,16 +14990,6 @@ packages:
|
|||
webpack: 4.46.0
|
||||
dev: true
|
||||
|
||||
/rc/1.2.8:
|
||||
resolution: {integrity: sha512-y3bGgqKj3QBdxLbLkomlohkvsA8gdAiUQlSBJnBhfn+BPxg4bc62d8TcBW15wavDfgexCgccckhcZvywyQYPOw==}
|
||||
hasBin: true
|
||||
dependencies:
|
||||
deep-extend: 0.6.0
|
||||
ini: 1.3.8
|
||||
minimist: 1.2.7
|
||||
strip-json-comments: 2.0.1
|
||||
dev: false
|
||||
|
||||
/react-docgen-typescript/2.2.2_typescript@4.7.4:
|
||||
resolution: {integrity: sha512-tvg2ZtOpOi6QDwsb3GZhOjDkkX0h8Z2gipvTg6OVMUyoYoURhEiRNePT8NZItTVCDh39JJHnLdfCOkzoLbFnTg==}
|
||||
peerDependencies:
|
||||
|
|
@ -15894,7 +15802,6 @@ packages:
|
|||
|
||||
/sax/1.2.4:
|
||||
resolution: {integrity: sha512-NqVDv9TpANUjFm0N8uM5GxL36UgKi9/atZw+x7YFnQ8ckwFGKrl4xX4yWtrey3UJm5nP1kUbnYgLopqWNSRhWw==}
|
||||
dev: true
|
||||
|
||||
/saxes/6.0.0:
|
||||
resolution: {integrity: sha512-xAg7SOnEhrm5zI3puOOKyy1OMcMlIJZYNJY7xLBwSze0UjhPLnWfj2GF2EpT0jmzaJKIWKHLsaSSajf35bcYnA==}
|
||||
|
|
@ -16138,18 +16045,6 @@ packages:
|
|||
resolution: {integrity: sha512-wnD2ZE+l+SPC/uoS0vXeE9L1+0wuaMqKlfz9AMUo38JsyLSBWSFcHR1Rri62LZc12vLr1gb3jl7iwQhgwpAbGQ==}
|
||||
dev: true
|
||||
|
||||
/simple-concat/1.0.1:
|
||||
resolution: {integrity: sha512-cSFtAPtRhljv69IK0hTVZQ+OfE9nePi/rtJmw5UjHeVyVroEqJXP1sFztKUy1qU+xvz3u/sfYJLa947b7nAN2Q==}
|
||||
dev: false
|
||||
|
||||
/simple-get/4.0.1:
|
||||
resolution: {integrity: sha512-brv7p5WgH0jmQJr1ZDDfKDOSeWWg+OVypG99A/5vYGPqJ6pxiaHLy8nxtFjBA7oMa01ebA9gfh1uMCFqOuXxvA==}
|
||||
dependencies:
|
||||
decompress-response: 6.0.0
|
||||
once: 1.4.0
|
||||
simple-concat: 1.0.1
|
||||
dev: false
|
||||
|
||||
/simple-update-notifier/1.1.0:
|
||||
resolution: {integrity: sha512-VpsrsJSUcJEseSbMHkrsrAVSdvVS5I96Qo1QAQ4FxQ9wXFcB+pjj7FB7/us9+GcgfW4ziHtYMc1J0PLczb55mg==}
|
||||
engines: {node: '>=8.10.0'}
|
||||
|
|
@ -16525,9 +16420,15 @@ packages:
|
|||
resolution: {integrity: sha512-AiisoFqQ0vbGcZgQPY1cdP2I76glaVA/RauYR4G4thNFgkTqr90yXTo4LYX60Jl+sIlPNHHdGSwo01AvbKUSVQ==}
|
||||
dev: true
|
||||
|
||||
/stream-wormhole/1.1.0:
|
||||
resolution: {integrity: sha512-gHFfL3px0Kctd6Po0M8TzEvt3De/xu6cnRrjlfYNhwbhLPLwigI2t1nc6jrzNuaYg5C4YF78PPFuQPzRiqn9ew==}
|
||||
engines: {node: '>=4.0.0'}
|
||||
dev: false
|
||||
|
||||
/streamsearch/1.1.0:
|
||||
resolution: {integrity: sha512-Mcc5wHehp9aXz1ax6bZUyY5afg9u2rv5cqQI3mRrYkGC8rW2hM02jWuwjtL++LS5qinSyhj2QfLyNsuc+VsExg==}
|
||||
engines: {node: '>=10.0.0'}
|
||||
dev: false
|
||||
|
||||
/strict-uri-encode/1.1.0:
|
||||
resolution: {integrity: sha512-R3f198pcvnB+5IpnBlRkphuE9n46WyVl8I39W/ZUTZLz4nqSP/oLYUrcnJrw462Ds8he4YKMov2efsTIw1BDGQ==}
|
||||
|
|
@ -16678,11 +16579,6 @@ packages:
|
|||
min-indent: 1.0.1
|
||||
dev: true
|
||||
|
||||
/strip-json-comments/2.0.1:
|
||||
resolution: {integrity: sha512-4gB8na07fecVVkOI6Rs4e7T6NOTki5EmL7TUduTs6bu3EdnSycntVJ4re8kgZA+wx9IueI2Y11bfbgwtzuE0KQ==}
|
||||
engines: {node: '>=0.10.0'}
|
||||
dev: false
|
||||
|
||||
/strip-json-comments/3.1.1:
|
||||
resolution: {integrity: sha512-6fPc+R4ihwqP6N/aIv2f1gMH8lOVtWQHoqC4yK6oSDVVocumAsfCqjkXnqiYMhmMwS/mEHLp7Vehlt3ql6lEig==}
|
||||
engines: {node: '>=8'}
|
||||
|
|
@ -16901,26 +16797,6 @@ packages:
|
|||
engines: {node: '>=6'}
|
||||
dev: true
|
||||
|
||||
/tar-fs/2.1.1:
|
||||
resolution: {integrity: sha512-V0r2Y9scmbDRLCNex/+hYzvp/zyYjvFbHPNgVTKfQvVrb6guiE/fxP+XblDNR011utopbkex2nM4dHNV6GDsng==}
|
||||
dependencies:
|
||||
chownr: 1.1.4
|
||||
mkdirp-classic: 0.5.3
|
||||
pump: 3.0.0
|
||||
tar-stream: 2.2.0
|
||||
dev: false
|
||||
|
||||
/tar-stream/2.2.0:
|
||||
resolution: {integrity: sha512-ujeqbceABgwMZxEJnk2HDY2DlnUZ+9oEcb1KzTVfYHio0UE6dG71n60d8D2I4qNvleWrrXpmjpt7vZeF1LnMZQ==}
|
||||
engines: {node: '>=6'}
|
||||
dependencies:
|
||||
bl: 4.1.0
|
||||
end-of-stream: 1.4.4
|
||||
fs-constants: 1.0.0
|
||||
inherits: 2.0.4
|
||||
readable-stream: 3.6.0
|
||||
dev: false
|
||||
|
||||
/tar/6.1.11:
|
||||
resolution: {integrity: sha512-an/KZQzQUkZCkuoAA64hM92X0Urb6VpRhAFllDzz44U2mcD5scmT3zBc4VgVpkugF580+DQn8eAFSyoQt0tznA==}
|
||||
engines: {node: '>= 10'}
|
||||
|
|
@ -17082,6 +16958,10 @@ packages:
|
|||
minimatch: 3.1.2
|
||||
dev: true
|
||||
|
||||
/text-decoding/1.0.0:
|
||||
resolution: {integrity: sha512-/0TJD42KDnVwKmDK6jj3xP7E2MG7SHAOG4tyTgyUCRPdHwvkquYNLEQltmdMa3owq3TkddCVcTsoctJI8VQNKA==}
|
||||
dev: false
|
||||
|
||||
/text-table/0.2.0:
|
||||
resolution: {integrity: sha512-N+8UisAXDGk8PFXP4HAzVR9nbfmVJ3zYLAWiTIoqC5v5isinhr+r5uaO8+7r3BMfuNIufIsA7RdpVgacC2cSpw==}
|
||||
dev: true
|
||||
|
|
@ -17378,12 +17258,6 @@ packages:
|
|||
resolution: {integrity: sha512-JVa5ijo+j/sOoHGjw0sxw734b1LhBkQ3bvUGNdxnVXDCX81Yx7TFgnZygxrIIWn23hbfTaMYLwRmAxFyDuFmIw==}
|
||||
dev: true
|
||||
|
||||
/tunnel-agent/0.6.0:
|
||||
resolution: {integrity: sha512-McnNiV1l8RYeY8tBgEpuodCC1mLUdbSN+CYBL7kJsJNInOP8UjDDEwdk6Mw60vdLLrr5NHKZhMAOSrR2NZuQ+w==}
|
||||
dependencies:
|
||||
safe-buffer: 5.2.1
|
||||
dev: false
|
||||
|
||||
/tunnel/0.0.6:
|
||||
resolution: {integrity: sha512-1h/Lnq9yajKY2PEbBadPXj3VxsDDu844OnaAo52UVmIzIvwwtBPIuNvkjuzBlTWpfJyUbG3ez0KSBibQkj4ojg==}
|
||||
engines: {node: '>=0.6.11 <=0.7.0 || >=0.7.3'}
|
||||
|
|
@ -17503,11 +17377,6 @@ packages:
|
|||
resolution: {integrity: sha512-pRS+/yrW5TjPPHNOvxhbNZexr2bS63WjrMU8a+VzEBhUi9Tz1pZeD+vQz3ut0svZ46P+SRqMEPnJmk2XnvNzTw==}
|
||||
engines: {node: '>=12.20'}
|
||||
|
||||
/type-fest/3.5.0:
|
||||
resolution: {integrity: sha512-bI3zRmZC8K0tUz1HjbIOAGQwR2CoPQG68N5IF7gm0LBl8QSNXzkmaWnkWccCUL5uG9mCsp4sBwC8SBrNSISWew==}
|
||||
engines: {node: '>=14.16'}
|
||||
dev: false
|
||||
|
||||
/type-is/1.6.18:
|
||||
resolution: {integrity: sha512-TkRKr9sUTxEH8MdfuCSP7VizJyzRNMjj2J2do2Jr3Kym598JVdEksuzPQCnlFPW4ky9Q+iA+ma9BGm06XQBy8g==}
|
||||
engines: {node: '>= 0.6'}
|
||||
|
|
@ -18876,6 +18745,22 @@ packages:
|
|||
resolution: {integrity: sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q==}
|
||||
engines: {node: '>=10'}
|
||||
|
||||
/ytdl-core/4.11.2:
|
||||
resolution: {integrity: sha512-D939t9b4ZzP3v0zDvehR2q+KgG97UTgrTKju3pOPGQcXtl4W6W5z0EpznzcJFu+OOpl7S7Ge8hv8zU65QnxYug==}
|
||||
engines: {node: '>=12'}
|
||||
dependencies:
|
||||
m3u8stream: 0.8.6
|
||||
miniget: 4.2.2
|
||||
sax: 1.2.4
|
||||
dev: false
|
||||
|
||||
/ytsr/3.8.0:
|
||||
resolution: {integrity: sha512-R+RfYXvBBMAr2e4OxrQ5SBv5x/Mdhmcj1Q8TH0f2HK5d2jbhHOtK4BdzPvLriA6MDoMwqqX04GD8Rpf9UNtSTg==}
|
||||
engines: {node: '>=8'}
|
||||
dependencies:
|
||||
miniget: 4.2.2
|
||||
dev: false
|
||||
|
||||
/zwitch/1.0.5:
|
||||
resolution: {integrity: sha512-V50KMwwzqJV0NpZIZFwfOD5/lyny3WlSzRiXgA0G7VUnRlqttta1L6UQIHzd6EuBY/cHGfwTIck7w1yH6Q5zUw==}
|
||||
dev: true
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue