updated dependencies, added IPLS tag, rewrote stuff with epic itty-router features
This commit is contained in:
parent
c09a27e99b
commit
7f6c507926
7 changed files with 998 additions and 614 deletions
182
main.js
182
main.js
|
|
@ -1,10 +1,8 @@
|
|||
import html from './index.html'
|
||||
import index_html from './index.html'
|
||||
import { Cipher, legacy_stream_url } from './wasm/pkg'
|
||||
|
||||
import { Router } from 'itty-router'
|
||||
import ID3Writer from 'browser-id3-writer'
|
||||
|
||||
const router = Router()
|
||||
import { error, json as jsonResp, html, Router, createResponse } from 'itty-router'
|
||||
import { ID3Writer } from 'browser-id3-writer'
|
||||
|
||||
const client_id = "447462"
|
||||
const client_secret = "a83bf7f38ad2f137e444727cfc3775cf"
|
||||
|
|
@ -25,7 +23,49 @@ const formats = {
|
|||
misc: { num: '0', gw: 'MP3_MISC', mime: 'audio/mpeg', ext: 'mp3' },
|
||||
}
|
||||
|
||||
async function gw_api_call(method, params) {
|
||||
async function track_req(request, env) {
|
||||
const { query } = request
|
||||
let { type, id } = request.params
|
||||
|
||||
if (!['track', 'album', 'playlist'].includes(type)) {
|
||||
return error(404)
|
||||
}
|
||||
|
||||
id = parseInt(id)
|
||||
if (isNaN(id) || (type !== 'track' && id < 0)) {
|
||||
return error(400, 'Invalid ID')
|
||||
}
|
||||
|
||||
let format = query.f
|
||||
if (format === undefined) {
|
||||
format = '320'
|
||||
} else {
|
||||
format = format.toLowerCase()
|
||||
if (formats[format] === undefined) {
|
||||
let nums = []
|
||||
Object.values(formats).forEach(f => nums.push(f.num))
|
||||
|
||||
let index = nums.indexOf(format)
|
||||
if (index === -1) {
|
||||
return error(400, 'Invalid format')
|
||||
}
|
||||
format = Object.keys(formats)[index]
|
||||
}
|
||||
}
|
||||
|
||||
let tagging = query.t
|
||||
tagging = (tagging === 'true' || tagging === '1') && (['misc', '128', '320'].includes(format) || type !== 'track')
|
||||
|
||||
switch (type) {
|
||||
case 'track':
|
||||
return await track(env, id, format, tagging, request.headers.get('range'))
|
||||
case 'album':
|
||||
case 'playlist':
|
||||
return await m3u8(type, id, format, tagging, request.headers.get('host'))
|
||||
}
|
||||
}
|
||||
|
||||
async function gw_api_call(env, method, params) {
|
||||
if (method === 'deezer.getUserData') {
|
||||
checkForm = ''
|
||||
}
|
||||
|
|
@ -34,7 +74,7 @@ async function gw_api_call(method, params) {
|
|||
params = {}
|
||||
}
|
||||
|
||||
let cookies = `arl=${ARL}`
|
||||
let cookies = `arl=${env.ARL}`
|
||||
if (sid) {
|
||||
cookies += `; sid=${sid}`
|
||||
}
|
||||
|
|
@ -54,116 +94,74 @@ async function gw_api_call(method, params) {
|
|||
const json = await response.json()
|
||||
|
||||
if (json.error.length !== 0) {
|
||||
return new Response(JSON.stringify(json.error), { status: 500, headers: { 'content-type': 'application/json' } })
|
||||
return error(500, {error: json.error})
|
||||
}
|
||||
|
||||
if (method === 'deezer.getUserData') {
|
||||
checkForm = json.results.checkForm
|
||||
await KV.put('checkForm', checkForm)
|
||||
await env.KV.put('checkForm', checkForm)
|
||||
|
||||
sid = json.results.SESSION_ID
|
||||
await KV.put('sid', sid)
|
||||
await env.KV.put('sid', sid)
|
||||
}
|
||||
|
||||
return json.results
|
||||
}
|
||||
|
||||
router.get('/:type/:id', async request => {
|
||||
const { query } = request
|
||||
let { type, id } = request.params
|
||||
|
||||
if (!['track', 'album', 'playlist'].includes(type)) {
|
||||
return new Response("not found", { status: 404, headers: { 'content-type': 'text/plain' } })
|
||||
}
|
||||
|
||||
id = parseInt(id)
|
||||
if (isNaN(id) || (type !== 'track' && id < 0)) {
|
||||
return new Response("Invalid ID", { status: 400, headers: { 'content-type': 'text/plain' } })
|
||||
}
|
||||
|
||||
let format = query.f
|
||||
if (format === undefined) {
|
||||
format = '320'
|
||||
} else {
|
||||
format = format.toLowerCase()
|
||||
if (formats[format] === undefined) {
|
||||
let nums = []
|
||||
Object.values(formats).forEach(f => nums.push(f.num))
|
||||
|
||||
let index = nums.indexOf(format)
|
||||
if (index === -1) {
|
||||
return new Response('Invalid format', { status: 400, headers: { 'content-type': 'text/plain' } })
|
||||
}
|
||||
format = Object.keys(formats)[index]
|
||||
}
|
||||
}
|
||||
|
||||
let tagging = query.t
|
||||
tagging = (tagging === 'true' || tagging === '1') && (['misc', '128', '320'].includes(format) || type !== 'track')
|
||||
|
||||
switch (type) {
|
||||
case 'track':
|
||||
return await track(id, format, tagging, request.headers.get('range'))
|
||||
case 'album':
|
||||
case 'playlist':
|
||||
return await m3u8(type, id, format, tagging, request.headers.get('host'))
|
||||
}
|
||||
})
|
||||
|
||||
let license_token
|
||||
let checkForm
|
||||
let sid
|
||||
async function auth_gw() {
|
||||
license_token = await KV.get('license_token')
|
||||
checkForm = await KV.get('checkForm')
|
||||
sid = await KV.get('sid')
|
||||
async function auth_gw(env) {
|
||||
license_token = await env.KV.get('license_token')
|
||||
checkForm = await env.KV.get('checkForm')
|
||||
sid = await env.KV.get('sid')
|
||||
|
||||
if (license_token === null) {
|
||||
const user_data = await gw_api_call('deezer.getUserData')
|
||||
const user_data = await gw_api_call(env, 'deezer.getUserData')
|
||||
if (user_data.constructor.name === 'Response') {
|
||||
return user_data
|
||||
}
|
||||
|
||||
if (user_data.USER.USER_ID === 0) {
|
||||
return new Response('Invalid arl', { status: 500, headers: { 'content-type': 'text/plain' } })
|
||||
return error(500, 'Invalid arl')
|
||||
}
|
||||
|
||||
license_token = user_data.USER.OPTIONS.license_token
|
||||
|
||||
await KV.put('license_token', license_token, { expirationTtl: 3600 })
|
||||
await env.KV.put('license_token', license_token, { expirationTtl: 3600 })
|
||||
}
|
||||
}
|
||||
|
||||
let access_token
|
||||
async function auth_dzapi() {
|
||||
access_token = await KV.get('access_token')
|
||||
async function auth_dzapi(env) {
|
||||
access_token = await env.KV.get('access_token')
|
||||
if (access_token === null) {
|
||||
const response = await fetch(`https://connect.deezer.com/oauth/access_token.php?grant_type=client_credentials&client_id=${client_id}&client_secret=${client_secret}&output=json`)
|
||||
const json = await response.json()
|
||||
if (json.error !== undefined) {
|
||||
return new Response("Couldn't get access token from Deezer", { status: 500, headers: { 'content-type': 'text/plain' } })
|
||||
return error(500, "Couldn't get access token from Deezer")
|
||||
}
|
||||
|
||||
access_token = json.access_token
|
||||
await KV.put('access_token', access_token, { expirationTtl: parseInt(json.expires) })
|
||||
await env.KV.put('access_token', access_token, { expirationTtl: parseInt(json.expires) })
|
||||
}
|
||||
}
|
||||
|
||||
async function track(id, format, tagging, range_header) {
|
||||
async function track(env, id, format, tagging, range_header) {
|
||||
// other users' user-upped tracks cannot be downloaded with the gw-light API
|
||||
let json;
|
||||
if (id >= 0) {
|
||||
let err_auth = await auth_gw()
|
||||
let err_auth = await auth_gw(env)
|
||||
if (err_auth) {
|
||||
return err_auth
|
||||
}
|
||||
|
||||
json = await gw_api_call('song.getData', { 'SNG_ID': id })
|
||||
json = await gw_api_call(env, 'song.getData', { 'SNG_ID': id })
|
||||
if (json.constructor.name === 'Response') {
|
||||
return json
|
||||
}
|
||||
} else { // user-upped track
|
||||
let err_auth = await auth_dzapi()
|
||||
let err_auth = await auth_dzapi(env)
|
||||
if (err_auth) {
|
||||
return err_auth
|
||||
}
|
||||
|
|
@ -171,7 +169,7 @@ async function track(id, format, tagging, range_header) {
|
|||
const response = await fetch(`https://api.deezer.com/track/${id}?access_token=${access_token}`)
|
||||
json = await response.json()
|
||||
if (json.error !== undefined) {
|
||||
return new Response(JSON.stringify(json.error), { status: 403, headers: { 'content-type': "application/json" } })
|
||||
return error(403, {error: json.error})
|
||||
}
|
||||
|
||||
json = {
|
||||
|
|
@ -191,7 +189,7 @@ async function track(id, format, tagging, range_header) {
|
|||
}
|
||||
|
||||
if (json['FILESIZE_' + formats[format].gw] == false) {
|
||||
return new Response('Format unavailable', { status: 403, headers: { 'content-type': 'text/plain' } })
|
||||
return error(403, 'Format unavailable')
|
||||
}
|
||||
|
||||
let range_req = false
|
||||
|
|
@ -241,7 +239,7 @@ async function track(id, format, tagging, range_header) {
|
|||
|
||||
const resp = await fetch('https://media.deezer.com/v1/get_url', init)
|
||||
if (resp.status !== 200) {
|
||||
return new Response("Couldn't get stream URL", { status: 403, headers: { 'content-type': 'text/plain' } })
|
||||
return error(403, "Couldn't get stream URL")
|
||||
}
|
||||
|
||||
const media_json = await resp.json()
|
||||
|
|
@ -249,7 +247,7 @@ async function track(id, format, tagging, range_header) {
|
|||
if (media_json.data[0].media !== undefined) {
|
||||
track_url = media_json.data[0].media[0].sources[0].url
|
||||
} else {
|
||||
return new Response("Couldn't get stream URL", { status: 403, headers: { 'content-type': 'text/plain' } })
|
||||
return error(403, "Couldn't get stream URL")
|
||||
}
|
||||
} else { // legacy stream url
|
||||
track_url = legacy_track_url(json, format)
|
||||
|
|
@ -287,6 +285,17 @@ async function track(id, format, tagging, range_header) {
|
|||
id3.setFrame('TPE1', artist_list)
|
||||
}
|
||||
|
||||
if (json.SNG_CONTRIBUTORS !== undefined) {
|
||||
let involved_people = []
|
||||
for (const [role, contributors] of Object.entries(json.SNG_CONTRIBUTORS)) {
|
||||
for (const contributor of contributors) {
|
||||
involved_people.push([role, contributor])
|
||||
}
|
||||
}
|
||||
|
||||
id3.setFrame('IPLS', involved_people)
|
||||
}
|
||||
|
||||
if (json.TRACK_NUMBER !== undefined) {
|
||||
id3.setFrame('TRCK', json.TRACK_NUMBER)
|
||||
}
|
||||
|
|
@ -338,7 +347,7 @@ async function track(id, format, tagging, range_header) {
|
|||
}
|
||||
|
||||
if (range_start < 0 || range_end < range_start) {
|
||||
return new Response('Invalid range', { status: 416, headers: { 'content-type': 'text/plain' } })
|
||||
return error(416, 'Invalid range')
|
||||
}
|
||||
|
||||
trk_range_start = range_start
|
||||
|
|
@ -387,9 +396,9 @@ async function track(id, format, tagging, range_header) {
|
|||
track = await fetch(track_url, init)
|
||||
if (![200, 206].includes(track.status)) {
|
||||
if (track.status === 416) {
|
||||
return new Response('Invalid range', { status: 416, headers: { 'content-type': 'text/plain' } })
|
||||
return error(416, 'Invalid range')
|
||||
}
|
||||
return new Response("Couldn't get track stream", { status: 403, headers: { 'content-type': 'text/plain' } })
|
||||
return error(403, "Couldn't get track stream")
|
||||
}
|
||||
|
||||
let track_size
|
||||
|
|
@ -484,11 +493,13 @@ function legacy_track_url(json, format) {
|
|||
return legacy_stream_url(md5_origin, format, id, media_version)
|
||||
}
|
||||
|
||||
const m3u8Resp = createResponse('audio/mpegurl')
|
||||
|
||||
async function m3u8(type, id, format, tagging, host) {
|
||||
const response = await fetch(`https://api.deezer.com/${type}/${id}?limit=-1`)
|
||||
const json = await response.json()
|
||||
if (json.error !== undefined) {
|
||||
return new Response(JSON.stringify(json.error), { status: 403, headers: { 'content-type': 'application/json' } })
|
||||
return error(403, {error: json.error})
|
||||
}
|
||||
|
||||
let list = '#EXTM3U\n'
|
||||
|
|
@ -502,15 +513,20 @@ async function m3u8(type, id, format, tagging, host) {
|
|||
list += `#EXTINF:${track.duration},${track.title}\n${result}\n`
|
||||
}
|
||||
|
||||
return new Response(list, { status: 200, headers: { 'content-type': 'audio/mpegurl' } })
|
||||
return m3u8Resp(list)
|
||||
}
|
||||
|
||||
router.get('/', () => {
|
||||
return new Response(html, { status: 200, headers: { 'content-type': 'text/html' } })
|
||||
})
|
||||
const router = Router()
|
||||
|
||||
router.all("*", () => new Response("not found", { status: 404, headers: { 'content-type': 'text/plain' } }))
|
||||
router
|
||||
.get('/:type/:id', track_req)
|
||||
.get('/', () => html(index_html))
|
||||
.all("*", () => error(404))
|
||||
|
||||
addEventListener('fetch', event =>
|
||||
event.respondWith(router.handle(event.request))
|
||||
)
|
||||
export default {
|
||||
fetch: (req, env, ctx) =>
|
||||
router
|
||||
.handle(req, env, ctx)
|
||||
.then(jsonResp)
|
||||
.catch(error), // catch errors
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue