feat: updates

This commit is contained in:
qier222 2023-03-03 03:12:27 +08:00
parent 9a52681687
commit 840a5b8e9b
No known key found for this signature in database
104 changed files with 1645 additions and 13494 deletions

View file

@ -1,9 +1,10 @@
import { FastifyInstance } from 'fastify'
import proxy from '@fastify/http-proxy'
import { isDev } from '@/desktop/main/env'
async function appleMusic(fastify: FastifyInstance) {
fastify.register(proxy, {
upstream: 'http://168.138.174.244:35530/',
upstream: isDev ? 'http://127.0.0.1:35530/' : 'http://168.138.174.244:35530/',
prefix: '/r3play/apple-music',
rewritePrefix: '/apple-music',
})

View file

@ -9,6 +9,7 @@ import fs from 'fs'
import youtube from '@/desktop/main/youtube'
import { CacheAPIs } from '@/shared/CacheAPIs'
import { FetchTracksResponse } from '@/shared/api/Track'
import store from '@/desktop/main/store'
const getAudioFromCache = async (id: number) => {
// get from cache
@ -76,47 +77,51 @@ const getAudioFromYouTube = async (id: number) => {
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,
try {
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,
},
},
freeTimeTrialPrivilege: {
resConsumable: false,
userConsumable: false,
type: 0,
remainTime: 0,
},
urlSource: 0,
r3play: {
youtube: data,
},
},
],
code: 200,
],
code: 200,
}
} catch (e) {
log.error('getAudioFromYouTube error', id, e)
}
}
@ -154,9 +159,11 @@ async function audio(fastify: FastifyInstance) {
return
}
const fromYoutube = getAudioFromYouTube(id)
if (fromYoutube) {
return fromYoutube
if (store.get('settings.enableFindTrackOnYouTube')) {
const fromYoutube = getAudioFromYouTube(id)
if (fromYoutube) {
return fromYoutube
}
}
// 是试听歌曲就把url删掉
@ -181,11 +188,14 @@ async function audio(fastify: FastifyInstance) {
fastify.post(
`/${appName.toLowerCase()}/audio/:id`,
async (
req: FastifyRequest<{ Params: { id: string }; Querystring: { url: string } }>,
req: FastifyRequest<{
Params: { id: string }
Querystring: { url: string; bitrate: number }
}>,
reply
) => {
const id = Number(req.params.id)
const { url } = req.query
const { url, bitrate } = req.query
if (isNaN(id)) {
return reply.status(400).send({ error: 'Invalid param id' })
}
@ -200,7 +210,7 @@ async function audio(fastify: FastifyInstance) {
}
try {
await cache.setAudio(await data.toBuffer(), { id, url })
await cache.setAudio(await data.toBuffer(), { id, url, bitrate })
reply.status(200).send('Audio cached!')
} catch (error) {
reply.status(500).send({ error })

View file

@ -238,7 +238,7 @@ class Cache {
return
}
getAudio(filename: string, reply: FastifyReply) {
async getAudio(filename: string, reply: FastifyReply) {
if (!filename) {
return reply.status(400).send({ error: 'No filename provided' })
}
@ -252,6 +252,7 @@ class Cache {
fs.unlinkSync(path)
return reply.status(404).send({ error: 'Audio not found' })
}
await prisma.audio.update({ where: { id }, data: { updatedAt: new Date() } })
reply
.status(206)
.header('Accept-Ranges', 'bytes')
@ -263,7 +264,10 @@ class Cache {
}
}
async setAudio(buffer: Buffer, { id, url }: { id: number; url: string }) {
async setAudio(
buffer: Buffer,
{ id, url, bitrate }: { id: number; url: string; bitrate: number }
) {
const path = `${app.getPath('userData')}/audio_cache`
try {
@ -273,7 +277,7 @@ class Cache {
}
const meta = await musicMetadata.parseBuffer(buffer)
const bitRate = (meta?.format?.codec === 'OPUS' ? 165000 : meta.format.bitrate ?? 0) / 1000
const bitRate = ~~((meta.format.bitrate || bitrate || 0) / 1000)
const format =
{
'MPEG 1 Layer 3': 'mp3',

View file

@ -12,6 +12,7 @@ import { Thumbar } from './windowsTaskbar'
import fastFolderSize from 'fast-folder-size'
import path from 'path'
import prettyBytes from 'pretty-bytes'
import prisma from './prisma'
const on = <T extends keyof IpcChannelsParams>(
channel: T,
@ -203,7 +204,7 @@ function initOtherIpcMain() {
* 退
*/
handle(IpcChannels.Logout, async () => {
// db.truncate(Tables.AccountData)
await prisma.accountData.deleteMany({})
return true
})

View file

@ -1,69 +1,218 @@
import log from './log'
import youtube, { Scraper, Video } from '@yimura/scraper'
import ytdl from 'ytdl-core'
import axios, { AxiosProxyConfig } from 'axios'
import store from './store'
import httpProxyAgent from 'http-proxy-agent'
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 search(keyword: string): Promise<
{
duration: number
id: string
title: string
}[]
> {
let proxy: AxiosProxyConfig | false = false
if (store.get('settings.httpProxyForYouTube')) {
const host = store.get('settings.httpProxyForYouTube.host') as string | undefined
const port = store.get('settings.httpProxyForYouTube.port') as number | undefined
const auth = store.get('settings.httpProxyForYouTube.auth') as any | undefined
const protocol = store.get('settings.httpProxyForYouTube.protocol') as string | undefined
if (host && port) {
proxy = { host, port, auth, protocol }
}
}
// proxy = { host: '127.0.0.1', port: 8888, protocol: 'http' }
const webPage = await axios.get(`https://www.youtube.com/results`, {
params: {
search_query: keyword,
sp: 'EgIQAQ==',
},
headers: { 'Accept-Language': 'en-US' },
timeout: 5000,
proxy,
})
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]
if (webPage.status !== 200) {
return []
}
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
// @credit https://www.npmjs.com/package/@yimura/scraper
function _parseData(data) {
const results = {
channels: [],
playlists: [],
streams: [],
videos: [],
}
const isVideo = item => item.videoRenderer && item.videoRenderer.lengthText
const getVideoData = item => {
const vRender = item.videoRenderer
const compress = key => {
return (key && key['runs'] ? key['runs'].map(v => v.text) : []).join('')
}
const parseDuration = vRender => {
if (!vRender.lengthText?.simpleText) return 0
const nums = vRender.lengthText.simpleText.split(':')
let time = nums.reduce((a, t) => 60 * a + +t) * 1e3
return time
}
return {
duration: parseDuration(vRender),
id: vRender.videoId,
title: compress(vRender.title),
}
}
for (const item of data) {
if (isVideo(item)) results.videos.push(getVideoData(item))
}
return results
}
function _extractData(json) {
json = json.contents.twoColumnSearchResultsRenderer.primaryContents
let contents = []
if (json.sectionListRenderer) {
contents = json.sectionListRenderer.contents
.filter(item =>
item?.itemSectionRenderer?.contents.filter(
x => x.videoRenderer || x.playlistRenderer || x.channelRenderer
)
)
.shift().itemSectionRenderer.contents
}
if (json.richGridRenderer) {
contents = json.richGridRenderer.contents
.filter(item => item.richItemRenderer && item.richItemRenderer.content)
.map(item => item.richItemRenderer.content)
}
return contents
}
function _getSearchData(webPage: string) {
const startString = 'var ytInitialData = '
const start = webPage.indexOf(startString)
const end = webPage.indexOf(';</script>', start)
const data = webPage.substring(start + startString.length, end)
try {
return JSON.parse(data)
} catch (e) {
throw new Error(
'Failed to parse YouTube search data. YouTube might have updated their site or no results returned.'
)
}
}
const parsedJson = _getSearchData(webPage.data)
const extracted = _extractData(parsedJson)
const parsed = _parseData(extracted)
return parsed?.videos
}
async matchTrack(
artist: string,
trackName: string
): Promise<{
url: string
bitRate: number
title: string
videoId: string
duration: string
channel: string
}> {
const match = async () => {
console.time('[youtube] search')
const videos = await this.search(`${artist} ${trackName} audio`)
console.timeEnd('[youtube] search')
let video: {
duration: number
id: string
title: string
} | 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]
}
if (!video) return null
console.time('[youtube] getInfo')
const proxy = 'http://127.0.0.1:8888'
const agent = httpProxyAgent(proxy)
const info = await ytdl.getInfo(video.id, {
// requestOptions: { agent },
})
console.timeEnd('[youtube] getInfo')
if (!info) return null
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
}
return new Promise(async (resolve, reject) => {
setTimeout(() => reject('youtube match timeout'), 10000)
try {
const result = await match()
if (result) resolve(result)
} catch (e) {
log.error(`[youtube] matchTrack error`, e)
reject(e)
}
})
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
}
async testConnection() {
return axios.get('https://www.youtube.com', { timeout: 5000 })
}
}