dzserver/index.js

204 lines
6.6 KiB
JavaScript
Raw Normal View History

2021-01-24 14:41:29 +00:00
const Router = require('./router')
2021-01-25 00:39:01 +00:00
const aesjs = require('aes-js');
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))
})
2021-01-25 00:39:01 +00:00
const AESKEY = [106, 111, 54, 97, 101, 121, 54, 104, 97, 105, 100, 50, 84, 101, 105, 104]
async function url_gen(md5_origin, format, id, media_version) {
const cipher = new aesjs.ModeOfOperation.ecb(AESKEY)
result = [md5_origin, format, id, media_version].join('\xa4')
buf = new ArrayBuffer(result.length);
result_hash = new Uint8Array(buf);
for (i = 0; i < result.length; i++) {
result_hash[i] = result.charCodeAt(i);
2021-01-24 14:41:29 +00:00
}
result_hash = await crypto.subtle.digest('MD5', result_hash)
result_hash = Array.from(new Uint8Array(result_hash))
result_hash = result_hash.reduce(function(previous, current) {
return previous + '0'.concat(current.toString(16)).substr(-2, 2);
}, ''),
result = result_hash + '\xa4' + result + '\xa4'
// padding
result += '\x00'.repeat(result.length % 16 ? 16 - result.length % 16 : 0)
// converting to array for encryption / hex string
result = Array.from(result).map(function(item) {
return item.charCodeAt(0);
})
// encryption / hex string
result = (result = cipher.encrypt(result)).reduce(function(previous, current) {
return previous + '0'.concat(current.toString(16)).substr(-2, 2);
}, '')
// cdn template with first character of md5 string + hash
2021-01-26 16:16:54 +00:00
return `https://cdns-proxy-${md5_origin[0]}.dzcdn.net/api/1/${result}`
2021-01-25 00:39:01 +00:00
}
const format_string_to_num = {
'64': '10',
'128': '1',
'320': '3',
'flac': '9',
'misc': '0',
}
2021-01-26 11:55:28 +00:00
async function handler(type, request) {
access_token = await KV.get('access_token')
2021-01-25 00:39:01 +00:00
if (access_token === null) {
const response = await fetch('https://connect.deezer.com/oauth/access_token.php?grant_type=client_credentials&client_id=447462&client_secret=a83bf7f38ad2f137e444727cfc3775cf&output=json')
2021-01-25 00:39:01 +00:00
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'}})
2021-01-25 00:39:01 +00:00
}
access_token = json.access_token
await KV.put('access_token', access_token, {expirationTtl: Number(json.expires)})
2021-01-25 00:39:01 +00:00
}
2021-01-26 11:55:28 +00:00
const url = new URL(request.url)
const id = url.pathname.split('/')[2]
if (id === "") {
return new Response('ID needs to be specified', {status: 400, headers: {'content-type': 'text/plain'}})
2021-01-26 11:55:28 +00:00
}
2021-01-25 00:39:01 +00:00
format = url.searchParams.get('f')
if (format === null) {
format = '320'
} else {
format = format.toLowerCase()
if (format_string_to_num[format] === undefined) {
index = Object.values(format_string_to_num).indexOf(format)
if (index === -1) {
return new Response('Invalid format', {status: 400, headers: {'content-type': 'text/plain'}})
}
format = Object.keys(format_string_to_num)[index]
}
}
2021-05-25 19:07:27 +00:00
tagging = (url.searchParams.get('t') === 'true') && ((format === '128') || (format === '320') || (format === 'misc'))
2021-05-25 19:00:16 +00:00
2021-01-26 11:55:28 +00:00
switch (type) {
case 'track':
2021-05-25 19:00:16 +00:00
return await track(id, format, access_token, tagging)
2021-01-26 11:55:28 +00:00
case 'album':
case 'playlist':
2021-01-26 11:57:00 +00:00
return await m3u8(type, id, format, access_token)
2021-01-26 11:55:28 +00:00
}
}
2021-05-25 19:00:16 +00:00
async function track(id, format, access_token, tagging) {
2021-01-26 11:55:28 +00:00
const response = await fetch(`https://api.deezer.com/track/${id}?access_token=${access_token}`)
const json = await response.json()
if (json.error !== undefined) {
return new Response(JSON.stringify(json.error), {status: 403, headers: {'content-type': "application/json"}})
}
result = await track_url(json, format)
if (typeof result === 'object') {
return result
}
2021-05-25 19:00:16 +00:00
if (!tagging) {
return new Response(null, {status: 302, headers: {'location': result}})
} else {
const track = await fetch(result)
2021-05-25 21:42:00 +00:00
const id3 = new ID3Writer(Buffer.alloc(0));
id3.padding = 0
id3.setFrame('TIT2', json.title)
2021-05-25 19:00:16 +00:00
.setFrame('TALB', json.album.title)
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
}
if (json.release_date !== undefined) {
2021-05-25 21:42:00 +00:00
id3.setFrame('TYER', json.release_date.split('-')[0])
2021-05-25 19:00:16 +00:00
}
if (json.album.cover_xl !== undefined) {
const cover = await fetch(json.album.cover_xl)
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
2021-05-25 21:42:00 +00:00
let { readable, writable } = new TransformStream()
const writer = writable.getWriter()
writer.write(id3.arrayBuffer)
writer.releaseLock()
track.body.pipeTo(writable)
return new Response(readable, {status: 200, headers: {'content-type': 'audio/mpeg'}})
2021-05-25 19:00:16 +00:00
}
2021-01-26 11:55:28 +00:00
}
async function track_url(json, format) {
const id = json.id
const md5_origin = json.md5_origin
const media_version = json.media_version
if (id < 0) {
format = 'misc'
}
if (json['filesize_' + format] === '0') {
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-01-26 11:55:28 +00:00
return await url_gen(md5_origin, format, id, media_version)
}
2021-01-26 11:57:00 +00:00
async function m3u8(type, id, format, access_token) {
2021-01-26 11:55:28 +00:00
const response = await fetch(`https://api.deezer.com/${type}/${id}?access_token=${access_token}&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"}})
}
var list = '#EXTM3U\n'
2021-01-26 11:55:28 +00:00
for (const track of json.tracks.data) {
const result = await track_url(track, format)
if (typeof result === 'object') {
return result
}
list += `#EXTINF:${track.duration},${track.title}\n${result}\n`
}
2021-01-26 11:55:28 +00:00
return new Response(list, {status: 200, headers: {'content-type': 'audio/mpegurl'}})
2021-01-24 14:41:29 +00:00
}
async function handleRequest(request) {
const r = new Router()
2021-01-26 11:55:28 +00:00
r.get('/track/.*', () => handler('track', request))
r.get('/album/.*', () => handler('album', request))
r.get('/playlist/.*', () => handler('playlist', request))
2021-01-24 14:41:29 +00:00
const resp = await r.route(request)
return resp
}