dzserver/index.js

263 lines
7.9 KiB
JavaScript
Raw Normal View History

2021-01-24 14:41:29 +00:00
const Router = require('./router')
2021-05-25 19:00:16 +00:00
const ID3Writer = require('browser-id3-writer');
2021-01-24 14:41:29 +00:00
addEventListener('fetch', event => {
event.respondWith(handleRequest(event.request))
})
const format_string_to_num = {
2021-08-03 19:08:46 +00:00
'aac_96': '8',
'64': '10',
'128': '1',
'320': '3',
2021-09-01 13:01:56 +00:00
'flac': '9',
2021-08-16 20:00:34 +00:00
'mp4_ra1': '13',
'mp4_ra2': '14',
'mp4_ra3': '15',
'mhm1_ra1': '16',
'mhm1_ra2': '17',
'mhm1': '18',
2021-08-03 19:08:46 +00:00
'sbc_256': '12',
'misc': '0',
}
2021-01-26 11:55:28 +00:00
async function handler(type, request) {
const url = new URL(request.url)
const id = url.pathname.split('/')[2]
format = url.searchParams.get('f')
if (format === null) {
format = '320'
} else {
format = format.toLowerCase()
if (format_string_to_num[format] === undefined) {
2021-08-19 14:49:56 +00:00
index = Object.values(format_string_to_num).indexOf(format)
if (index === -1) {
return new Response('Invalid format', { status: 400, headers: { 'content-type': 'text/plain' } })
}
2021-08-19 14:49:56 +00:00
format = Object.keys(format_string_to_num)[index]
}
}
2021-08-13 21:20:43 +00:00
let tagging = url.searchParams.get('t')
2021-09-01 13:01:56 +00:00
tagging = (tagging === 'true' || tagging === '1') && ['misc', '128', '320'].includes(format)
2021-05-25 19:00:16 +00:00
2021-01-26 11:55:28 +00:00
switch (type) {
case 'track':
2021-09-01 13:01:56 +00:00
return await track(id, format, tagging)
2021-01-26 11:55:28 +00:00
case 'album':
case 'playlist':
2021-09-01 13:01:56 +00:00
return await m3u8(type, id, format, tagging, url.host)
2021-01-26 11:55:28 +00:00
}
}
2021-09-01 13:01:56 +00:00
async function track(id, format, tagging) {
const response = await fetch(`https://api.deezer.com/streaming_url.php?access_token=${ACCESS_TOKEN}&track_id=${id}`)
2021-01-26 11:55:28 +00:00
const json = await response.json()
if (json.error !== undefined) {
return new Response(JSON.stringify(json.error), { status: 403, headers: { 'content-type': "application/json" } })
2021-01-26 11:55:28 +00:00
}
2021-09-01 13:11:17 +00:00
if (json.id < 0) { // user-uploaded track
format = 'misc'
}
2021-08-02 15:26:47 +00:00
const wasm = await import('./pkg')
2021-09-01 13:01:56 +00:00
encrypted = !['320', 'flac'].includes(format)
if (!encrypted) { // server-side stream url
// TODO: handle alternatives
result = json['url_' + format]
if (result === undefined) {
return new Response('Format unavailable', { status: 403, headers: { 'content-type': 'text/plain' } })
}
result = wasm.decrypt_stream_url(result)
} else { // legacy stream url
result = await legacy_track_url(json, format, wasm.legacy_stream_url)
if (typeof result === 'object') {
return result
}
2021-01-26 11:55:28 +00:00
}
2021-09-01 13:01:56 +00:00
let track
if (tagging || encrypted) {
track = await fetch(result)
if (track.status !== 200) {
return new Response("Couldn't get track stream", { status: 403, headers: { 'content-type': 'text/plain' } })
}
}
2021-05-25 19:00:16 +00:00
let id3
if (tagging) {
id3 = new ID3Writer(Buffer.alloc(0));
2021-05-25 21:42:00 +00:00
id3.padding = 0
id3.setFrame('TIT2', json.title)
2021-05-25 19:00:16 +00:00
.setFrame('TALB', json.album.title)
2021-06-02 22:55:29 +00:00
.setFrame('TPE2', json.artist.name)
2021-05-25 19:00:16 +00:00
if (json.contributors !== undefined) {
contr_list = [];
for (const c of json.contributors) {
contr_list.push(c.name)
}
2021-05-25 21:42:00 +00:00
id3.setFrame('TPE1', contr_list)
2021-05-25 19:00:16 +00:00
}
2021-06-02 22:55:29 +00:00
if (json.track_position !== undefined) {
id3.setFrame('TRCK', json.track_position)
}
if (json.disk_number !== undefined) {
id3.setFrame('TPOS', json.disk_number)
}
if (json.isrc !== "") {
id3.setFrame('TSRC', json.isrc)
}
if (json.bpm !== undefined) {
id3.setFrame('TBPM', json.bpm)
}
2021-05-25 19:00:16 +00:00
if (json.release_date !== undefined) {
2021-06-02 22:55:29 +00:00
const split = json.release_date.split('-')
id3.setFrame('TYER', split[0])
id3.setFrame('TDAT', split[2] + split[1])
2021-05-25 19:00:16 +00:00
}
if (json.md5_image !== "") {
const url = `https://cdns-images.dzcdn.net/images/cover/${json.md5_image}/1000x1000-000000-80-0-0.jpg`
const cover = await fetch(url)
2021-05-25 19:00:16 +00:00
const coverBuffer = await cover.arrayBuffer()
2021-05-25 21:42:00 +00:00
id3.setFrame('APIC', {
2021-05-25 19:00:16 +00:00
type: 3,
data: coverBuffer,
description: 'cover'
});
}
2021-05-25 21:42:00 +00:00
id3.addTag();
}
2021-05-25 19:00:16 +00:00
let { readable, writable } = new TransformStream()
2021-09-01 13:01:56 +00:00
let writer
if (tagging || encrypted) {
writer = writable.getWriter()
}
2021-05-25 21:42:00 +00:00
2021-09-01 13:01:56 +00:00
if (tagging) {
2021-05-25 21:42:00 +00:00
writer.write(id3.arrayBuffer)
}
2021-05-25 21:42:00 +00:00
2021-08-01 18:35:56 +00:00
// needed if track has fallback, like https://www.deezer.com/track/11835714
if (json.alternative) {
id = json.alternative.id.toString()
}
2021-09-01 13:01:56 +00:00
if (encrypted) {
const cipher = new wasm.Cipher(id)
const length = parseInt(track.headers.get('Content-Length'))
pipeDecryptedStream(writer, track.body, length, cipher)
} else if (tagging) {
writer.releaseLock()
track.body.pipeTo(writable)
} else {
return new Response(null, { status: 302, headers: { 'location': result } })
}
return new Response(readable, { status: 200, headers: { 'content-type': 'audio/mpeg' } })
}
2021-08-02 15:26:47 +00:00
async function pipeDecryptedStream(writer, body, length, cipher) {
const reader = body.getReader({ mode: 'byob' })
let byteCount = 0
let end = false
while (!end) {
end = byteCount + 2048 > length
let chunk
if (!end) {
chunk = new Int8Array(2048)
} else {
chunk = new Int8Array(length - byteCount)
}
// if read chunk isn't 2048 bytes, read until it is
// cause of retarded readable streams not having an option to specify min bytes
let tempLength = 0
while (tempLength !== chunk.length) {
let read = (await reader.read(new Int8Array(chunk.length - tempLength))).value
chunk.set(read, tempLength)
tempLength += read.length
}
if (byteCount % 6144 === 0 && !end) {
// encrypted chunk
2021-08-23 16:55:33 +00:00
cipher.decrypt_chunk(chunk)
}
2021-08-02 15:26:47 +00:00
await writer.write(chunk)
byteCount += 2048
2021-05-25 19:00:16 +00:00
}
await reader.cancel()
await writer.close()
2021-01-26 11:55:28 +00:00
}
2021-09-01 13:01:56 +00:00
function legacy_track_url(json, format, url_func) {
2021-08-01 18:35:56 +00:00
// needed if track has fallback, like https://www.deezer.com/track/11835714
2021-07-19 13:43:07 +00:00
if (json.alternative) {
json = json.alternative
}
2021-01-26 11:55:28 +00:00
const id = json.id
const md5_origin = json.md5_origin
const media_version = json.media_version
2021-08-24 15:36:22 +00:00
if (json['filesize_' + format] == false) {
return new Response('Format unavailable', { status: 403, headers: { 'content-type': 'text/plain' } })
}
format = format_string_to_num[format]
2021-01-25 00:39:01 +00:00
2021-08-02 18:27:42 +00:00
return url_func(md5_origin, format, id.toString(), media_version)
2021-01-26 11:55:28 +00:00
}
2021-09-01 13:01:56 +00:00
async function m3u8(type, id, format, tagging, host) {
const response = await fetch(`https://api.deezer.com/${type}/${id}?access_token=${ACCESS_TOKEN}&limit=-1`)
2021-01-26 11:55:28 +00:00
const json = await response.json()
if (json.error !== undefined) {
return new Response(JSON.stringify(json.error), { status: 403, headers: { 'content-type': "application/json" } })
2021-01-26 11:55:28 +00:00
}
2021-08-13 23:18:05 +00:00
let list = '#EXTM3U\n'
2021-01-26 11:55:28 +00:00
for (const track of json.tracks.data) {
if (track.id < 0) { // user-uploaded track
format = 'misc'
2021-01-26 11:55:28 +00:00
}
2021-08-13 21:20:43 +00:00
let result = `https://${host}/track/${track.id}?f=${format}&t=${+ tagging}`
2021-01-26 11:55:28 +00:00
list += `#EXTINF:${track.duration},${track.title}\n${result}\n`
}
return new Response(list, { status: 200, headers: { 'content-type': 'audio/mpegurl' } })
2021-01-24 14:41:29 +00:00
}
2021-08-16 17:52:51 +00:00
async function indexHandler() {
return new Response(require('./index.html'), { status: 200, headers: { 'content-type': 'text/html' } })
}
2021-01-24 14:41:29 +00:00
async function handleRequest(request) {
const r = new Router()
2021-08-16 17:52:51 +00:00
r.get('/', () => indexHandler())
2021-08-16 16:03:20 +00:00
r.get('/track/-?\\d+', () => handler('track', request))
r.get('/album/\\d+', () => handler('album', request))
r.get('/playlist/\\d+', () => handler('playlist', request))
2021-01-24 14:41:29 +00:00
const resp = await r.route(request)
return resp
}