320/flac now epically bacc?!?!?!?1?1
This commit is contained in:
parent
90f3d6bf4e
commit
a92876194a
File diff suppressed because one or more lines are too long
|
@ -31,7 +31,7 @@
|
||||||
<p>params are:</p>
|
<p>params are:</p>
|
||||||
<p><b>t: tagging</b>, boolean</p>
|
<p><b>t: tagging</b>, boolean</p>
|
||||||
<p><b>f: format</b>, can be one of these:</p>
|
<p><b>f: format</b>, can be one of these:</p>
|
||||||
<p>aac_96, 64, 128, 320, mp4_ra1, mp4_ra2, mp4_ra3, mhm1_ra1, mhm1_ra2, mhm1, sbc_256, misc</p>
|
<p>aac_96, 64, 128, 320, flac, mp4_ra1, mp4_ra2, mp4_ra3, mhm1_ra1, mhm1_ra2, mhm1, sbc_256, misc</p>
|
||||||
<p>available formats depend on the track</p>
|
<p>available formats depend on the track</p>
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</html>
|
||||||
|
|
64
index.js
64
index.js
|
@ -10,6 +10,7 @@ const format_string_to_num = {
|
||||||
'64': '10',
|
'64': '10',
|
||||||
'128': '1',
|
'128': '1',
|
||||||
'320': '3',
|
'320': '3',
|
||||||
|
'flac': '9',
|
||||||
'mp4_ra1': '13',
|
'mp4_ra1': '13',
|
||||||
'mp4_ra2': '14',
|
'mp4_ra2': '14',
|
||||||
'mp4_ra3': '15',
|
'mp4_ra3': '15',
|
||||||
|
@ -21,18 +22,6 @@ const format_string_to_num = {
|
||||||
}
|
}
|
||||||
|
|
||||||
async function handler(type, request) {
|
async function handler(type, request) {
|
||||||
access_token = await 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=447462&client_secret=a83bf7f38ad2f137e444727cfc3775cf&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' } })
|
|
||||||
}
|
|
||||||
|
|
||||||
access_token = json.access_token
|
|
||||||
await KV.put('access_token', access_token, { expirationTtl: parseInt(json.expires) })
|
|
||||||
}
|
|
||||||
|
|
||||||
const url = new URL(request.url)
|
const url = new URL(request.url)
|
||||||
const id = url.pathname.split('/')[2]
|
const id = url.pathname.split('/')[2]
|
||||||
|
|
||||||
|
@ -51,19 +40,19 @@ async function handler(type, request) {
|
||||||
}
|
}
|
||||||
|
|
||||||
let tagging = url.searchParams.get('t')
|
let tagging = url.searchParams.get('t')
|
||||||
tagging = tagging === 'true' || tagging === '1'
|
tagging = (tagging === 'true' || tagging === '1') && ['misc', '128', '320'].includes(format)
|
||||||
|
|
||||||
switch (type) {
|
switch (type) {
|
||||||
case 'track':
|
case 'track':
|
||||||
return await track(id, format, access_token, tagging)
|
return await track(id, format, tagging)
|
||||||
case 'album':
|
case 'album':
|
||||||
case 'playlist':
|
case 'playlist':
|
||||||
return await m3u8(type, id, format, access_token, tagging, url.host)
|
return await m3u8(type, id, format, tagging, url.host)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async function track(id, format, access_token, tagging) {
|
async function track(id, format, tagging) {
|
||||||
const response = await fetch(`https://api.deezer.com/track/${id}?access_token=${access_token}`)
|
const response = await fetch(`https://api.deezer.com/streaming_url.php?access_token=${ACCESS_TOKEN}&track_id=${id}`)
|
||||||
const json = await response.json()
|
const json = await response.json()
|
||||||
if (json.error !== undefined) {
|
if (json.error !== undefined) {
|
||||||
return new Response(JSON.stringify(json.error), { status: 403, headers: { 'content-type': "application/json" } })
|
return new Response(JSON.stringify(json.error), { status: 403, headers: { 'content-type': "application/json" } })
|
||||||
|
@ -71,15 +60,30 @@ async function track(id, format, access_token, tagging) {
|
||||||
|
|
||||||
const wasm = await import('./pkg')
|
const wasm = await import('./pkg')
|
||||||
|
|
||||||
result = await track_url(json, format, wasm.stream_url)
|
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') {
|
if (typeof result === 'object') {
|
||||||
return result
|
return result
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
const track = await fetch(result)
|
let track
|
||||||
|
if (tagging || encrypted) {
|
||||||
|
track = await fetch(result)
|
||||||
if (track.status !== 200) {
|
if (track.status !== 200) {
|
||||||
return new Response("Couldn't get track stream", { status: 403, headers: { 'content-type': 'text/plain' } })
|
return new Response("Couldn't get track stream", { status: 403, headers: { 'content-type': 'text/plain' } })
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
let id3
|
let id3
|
||||||
if (tagging) {
|
if (tagging) {
|
||||||
|
@ -136,9 +140,12 @@ async function track(id, format, access_token, tagging) {
|
||||||
}
|
}
|
||||||
|
|
||||||
let { readable, writable } = new TransformStream()
|
let { readable, writable } = new TransformStream()
|
||||||
const writer = writable.getWriter()
|
let writer
|
||||||
|
if (tagging || encrypted) {
|
||||||
|
writer = writable.getWriter()
|
||||||
|
}
|
||||||
|
|
||||||
if (id3) {
|
if (tagging) {
|
||||||
writer.write(id3.arrayBuffer)
|
writer.write(id3.arrayBuffer)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -147,11 +154,16 @@ async function track(id, format, access_token, tagging) {
|
||||||
id = json.alternative.id.toString()
|
id = json.alternative.id.toString()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (encrypted) {
|
||||||
const cipher = new wasm.Cipher(id)
|
const cipher = new wasm.Cipher(id)
|
||||||
|
|
||||||
const length = parseInt(track.headers.get('Content-Length'))
|
const length = parseInt(track.headers.get('Content-Length'))
|
||||||
|
|
||||||
pipeDecryptedStream(writer, track.body, length, cipher)
|
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' } })
|
return new Response(readable, { status: 200, headers: { 'content-type': 'audio/mpeg' } })
|
||||||
}
|
}
|
||||||
|
@ -191,7 +203,7 @@ async function pipeDecryptedStream(writer, body, length, cipher) {
|
||||||
await writer.close()
|
await writer.close()
|
||||||
}
|
}
|
||||||
|
|
||||||
function track_url(json, format, url_func) {
|
function legacy_track_url(json, format, url_func) {
|
||||||
// needed if track has fallback, like https://www.deezer.com/track/11835714
|
// needed if track has fallback, like https://www.deezer.com/track/11835714
|
||||||
if (json.alternative) {
|
if (json.alternative) {
|
||||||
json = json.alternative
|
json = json.alternative
|
||||||
|
@ -214,8 +226,8 @@ function track_url(json, format, url_func) {
|
||||||
return url_func(md5_origin, format, id.toString(), media_version)
|
return url_func(md5_origin, format, id.toString(), media_version)
|
||||||
}
|
}
|
||||||
|
|
||||||
async function m3u8(type, id, format, access_token, tagging, host) {
|
async function m3u8(type, id, format, tagging, host) {
|
||||||
const response = await fetch(`https://api.deezer.com/${type}/${id}?access_token=${access_token}&limit=-1`)
|
const response = await fetch(`https://api.deezer.com/${type}/${id}?access_token=${ACCESS_TOKEN}&limit=-1`)
|
||||||
const json = await response.json()
|
const json = await response.json()
|
||||||
if (json.error !== undefined) {
|
if (json.error !== undefined) {
|
||||||
return new Response(JSON.stringify(json.error), { status: 403, headers: { 'content-type': "application/json" } })
|
return new Response(JSON.stringify(json.error), { status: 403, headers: { 'content-type': "application/json" } })
|
||||||
|
|
13
src/lib.rs
13
src/lib.rs
|
@ -1,13 +1,14 @@
|
||||||
use wasm_bindgen::prelude::*;
|
use wasm_bindgen::prelude::*;
|
||||||
|
|
||||||
use blowfish::Blowfish;
|
use blowfish::Blowfish;
|
||||||
use aes::Aes128;
|
use aes::{Aes128, Aes256};
|
||||||
use block_modes::{BlockMode, Cbc, Ecb};
|
use block_modes::{BlockMode, Cbc, Ecb};
|
||||||
use block_modes::block_padding::{NoPadding, ZeroPadding};
|
use block_modes::block_padding::{NoPadding, ZeroPadding};
|
||||||
use md5::{Md5, Digest};
|
use md5::{Md5, Digest};
|
||||||
|
|
||||||
type BfCbc = Cbc<Blowfish, NoPadding>;
|
type BfCbc = Cbc<Blowfish, NoPadding>;
|
||||||
type Aes128Ecb = Ecb<Aes128, ZeroPadding>;
|
type Aes128Ecb = Ecb<Aes128, ZeroPadding>;
|
||||||
|
type Aes256Ecb = Ecb<Aes256, ZeroPadding>;
|
||||||
|
|
||||||
const TRACK_CDN_KEY: [u8; 16] = [106, 111, 54, 97, 101, 121, 54, 104, 97, 105, 100, 50, 84, 101, 105, 104];
|
const TRACK_CDN_KEY: [u8; 16] = [106, 111, 54, 97, 101, 121, 54, 104, 97, 105, 100, 50, 84, 101, 105, 104];
|
||||||
const BF_SECRET: [u8; 16] = [103, 52, 101, 108, 53, 56, 119, 99, 48, 122, 118, 102, 57, 110, 97, 49];
|
const BF_SECRET: [u8; 16] = [103, 52, 101, 108, 53, 56, 119, 99, 48, 122, 118, 102, 57, 110, 97, 49];
|
||||||
|
@ -36,7 +37,7 @@ impl Cipher {
|
||||||
}
|
}
|
||||||
|
|
||||||
#[wasm_bindgen]
|
#[wasm_bindgen]
|
||||||
pub fn stream_url(md5_origin: &str, format: &str, id: &str, media_version: u8) -> String {
|
pub fn legacy_stream_url(md5_origin: &str, format: &str, id: &str, media_version: u8) -> String {
|
||||||
// md5 origin + format num + id + media version
|
// md5 origin + format num + id + media version
|
||||||
let metadata = [
|
let metadata = [
|
||||||
md5_origin.as_bytes(),
|
md5_origin.as_bytes(),
|
||||||
|
@ -63,3 +64,11 @@ pub fn stream_url(md5_origin: &str, format: &str, id: &str, media_version: u8) -
|
||||||
|
|
||||||
format!("https://cdns-proxy-{}.dzcdn.net/mobile/1/{}", md5_origin.chars().next().unwrap(), hex::encode(ciphertext))
|
format!("https://cdns-proxy-{}.dzcdn.net/mobile/1/{}", md5_origin.chars().next().unwrap(), hex::encode(ciphertext))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[wasm_bindgen]
|
||||||
|
pub fn decrypt_stream_url(url: &str) -> String {
|
||||||
|
let url = hex::decode(url).unwrap();
|
||||||
|
let cipher = Aes256Ecb::new_from_slices("2f5b4c9785ddc367975b83d90dc46f5c".as_bytes(), Default::default()).unwrap();
|
||||||
|
let url = cipher.decrypt_vec(&url).unwrap();
|
||||||
|
String::from_utf8_lossy(&url).into_owned()
|
||||||
|
}
|
|
@ -3,6 +3,4 @@ type = "webpack"
|
||||||
webpack_config = "webpack.config.js"
|
webpack_config = "webpack.config.js"
|
||||||
account_id = "03479b0523a52b140e0dabac40cb0fc8"
|
account_id = "03479b0523a52b140e0dabac40cb0fc8"
|
||||||
workers_dev = true
|
workers_dev = true
|
||||||
kv_namespaces = [
|
vars = { ACCESS_TOKEN = "haha nope" }
|
||||||
{ binding = "KV", id = "974c0967a84e415daa054bbbcc7f80c6", preview_id = "cfcc6491f3484cbca664913836635113" }
|
|
||||||
]
|
|
Loading…
Reference in New Issue