updated dependencies, added IPLS tag, rewrote stuff with epic itty-router features

This commit is contained in:
uh wot 2023-07-01 21:12:45 +02:00
parent c09a27e99b
commit 7f6c507926
Signed by: uhwot
GPG key ID: CB2454984587B781
7 changed files with 998 additions and 614 deletions

182
main.js
View file

@ -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
}