switched to different method of getting stream urls cause of retarded quota
This commit is contained in:
parent
e28ad54c6b
commit
695fd9ebc2
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, flac, 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_ra3, sbc_256, misc</p>
|
||||||
<p>available formats depend on the track</p>
|
<p>available formats depend on the track</p>
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</html>
|
||||||
|
|
230
index.js
230
index.js
|
@ -16,12 +16,89 @@ const format_string_to_num = {
|
||||||
'mp4_ra3': '15',
|
'mp4_ra3': '15',
|
||||||
'mhm1_ra1': '16',
|
'mhm1_ra1': '16',
|
||||||
'mhm1_ra2': '17',
|
'mhm1_ra2': '17',
|
||||||
'mhm1': '18',
|
'mhm1_ra3': '18',
|
||||||
'sbc_256': '12',
|
'sbc_256': '12',
|
||||||
'misc': '0',
|
'misc': '0',
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const format_string_to_gw = {
|
||||||
|
'aac_96': 'AAC_96',
|
||||||
|
'64': 'MP3_64',
|
||||||
|
'128': 'MP3_128',
|
||||||
|
'320': 'MP3_320',
|
||||||
|
'flac': 'FLAC',
|
||||||
|
'mp4_ra1': 'MP4_RA1',
|
||||||
|
'mp4_ra2': 'MP4_RA2',
|
||||||
|
'mp4_ra3': 'MP4_RA3',
|
||||||
|
'mhm1_ra1': 'MHM1_RA1',
|
||||||
|
'mhm1_ra2': 'MHM1_RA2',
|
||||||
|
'mhm1_ra3': 'MHM1_RA3',
|
||||||
|
'sbc_256': 'SBC_256',
|
||||||
|
'misc': 'MP3_MISC',
|
||||||
|
}
|
||||||
|
|
||||||
|
async function gw_api_call(method, params) {
|
||||||
|
if (method === 'deezer.getUserData') {
|
||||||
|
checkForm = ''
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!params) {
|
||||||
|
params = {}
|
||||||
|
}
|
||||||
|
|
||||||
|
let cookies = `arl=${ARL}`
|
||||||
|
if (sid) {
|
||||||
|
cookies += `; sid=${sid}`
|
||||||
|
}
|
||||||
|
|
||||||
|
const headers = new Headers({ 'cookie': cookies })
|
||||||
|
|
||||||
|
const init = {
|
||||||
|
method: 'POST',
|
||||||
|
headers: headers,
|
||||||
|
body: JSON.stringify(params),
|
||||||
|
}
|
||||||
|
|
||||||
|
const response = await fetch(`https://www.deezer.com/ajax/gw-light.php?method=${method}&input=3&api_version=1.0&api_token=${checkForm}`, init)
|
||||||
|
const json = await response.json()
|
||||||
|
|
||||||
|
if (json.error.length !== 0) {
|
||||||
|
return new Response(JSON.stringify(json.error), { status: 500, headers: { 'content-type': 'application/json' } })
|
||||||
|
}
|
||||||
|
|
||||||
|
if (method === "deezer.getUserData") {
|
||||||
|
checkForm = json.results.checkForm
|
||||||
|
await KV.put('checkForm', checkForm)
|
||||||
|
|
||||||
|
sid = response.headers.get('set-cookie').split(',').map(v => v.trimStart())[0]
|
||||||
|
sid = sid.match(/^sid=(fr[\da-f]+)/)[1]
|
||||||
|
await KV.put('sid', sid)
|
||||||
|
}
|
||||||
|
|
||||||
|
return json.results
|
||||||
|
}
|
||||||
|
|
||||||
async function handler(type, request) {
|
async function handler(type, request) {
|
||||||
|
license_token = await KV.get('license_token')
|
||||||
|
checkForm = await KV.get('checkForm')
|
||||||
|
sid = await KV.get('sid')
|
||||||
|
|
||||||
|
if (license_token === null) {
|
||||||
|
const user_data = await gw_api_call('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' } })
|
||||||
|
}
|
||||||
|
|
||||||
|
license_token = user_data.USER.OPTIONS.license_token
|
||||||
|
const expiration = user_data.USER.OPTIONS.expiration_timestamp
|
||||||
|
|
||||||
|
await KV.put('license_token', license_token, { expiration: expiration })
|
||||||
|
}
|
||||||
|
|
||||||
const url = new URL(request.url)
|
const url = new URL(request.url)
|
||||||
const id = url.pathname.split('/')[2]
|
const id = url.pathname.split('/')[2]
|
||||||
|
|
||||||
|
@ -52,36 +129,60 @@ async function handler(type, request) {
|
||||||
}
|
}
|
||||||
|
|
||||||
async function track(id, format, tagging) {
|
async function track(id, format, tagging) {
|
||||||
const response = await fetch(`https://api.deezer.com/streaming_url.php?access_token=${ACCESS_TOKEN}&track_id=${id}`)
|
const json = await gw_api_call('song.getData', { 'SNG_ID': id })
|
||||||
const json = await response.json()
|
if (json.constructor.name === 'Response') {
|
||||||
if (json.error !== undefined) {
|
return json
|
||||||
return new Response(JSON.stringify(json.error), { status: 403, headers: { 'content-type': "application/json" } })
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if (json.id < 0) { // user-uploaded track
|
if (parseInt(json.SNG_ID) < 0) { // user-uploaded track
|
||||||
format = 'misc'
|
format = 'misc'
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (json['FILESIZE_' + format_string_to_gw[format]] == false) {
|
||||||
|
return new Response('Format unavailable', { status: 403, headers: { 'content-type': 'text/plain' } })
|
||||||
|
}
|
||||||
|
|
||||||
const wasm = await import('./pkg')
|
const wasm = await import('./pkg')
|
||||||
|
|
||||||
encrypted = !['320', 'flac'].includes(format)
|
legacy_url = !['320', 'flac'].includes(format)
|
||||||
if (!encrypted) { // server-side stream url
|
if (!legacy_url) { // server-side stream url
|
||||||
// needed if track has fallback, like https://www.deezer.com/track/11835714
|
// needed if track has fallback, like https://www.deezer.com/track/11835714
|
||||||
let urls
|
let track_token
|
||||||
if (!json.alternative) {
|
if (json.FALLBACK !== undefined) {
|
||||||
urls = json
|
track_token = json.FALLBACK.TRACK_TOKEN
|
||||||
} else {
|
} else {
|
||||||
const temp = await fetch(`https://api.deezer.com/streaming_url.php?access_token=${ACCESS_TOKEN}&track_id=${json.alternative.id}`)
|
track_token = json.TRACK_TOKEN
|
||||||
urls = await temp.json()
|
|
||||||
}
|
}
|
||||||
|
|
||||||
result = urls['url_' + format]
|
const body = {
|
||||||
|
license_token: license_token,
|
||||||
if (result === undefined) {
|
media: [
|
||||||
return new Response('Format unavailable', { status: 403, headers: { 'content-type': 'text/plain' } })
|
{
|
||||||
|
type: "FULL",
|
||||||
|
formats: [
|
||||||
|
{
|
||||||
|
cipher: "BF_CBC_STRIPE",
|
||||||
|
format: format_string_to_gw[format]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
],
|
||||||
|
track_tokens: [ track_token ]
|
||||||
}
|
}
|
||||||
|
|
||||||
result = wasm.decrypt_stream_url(result)
|
const init = {
|
||||||
|
method: 'POST',
|
||||||
|
body: JSON.stringify(body)
|
||||||
|
}
|
||||||
|
|
||||||
|
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' } })
|
||||||
|
}
|
||||||
|
|
||||||
|
const media_json = await resp.json()
|
||||||
|
|
||||||
|
result = media_json.data[0].media[0].sources[0].url
|
||||||
} else { // legacy stream url
|
} else { // legacy stream url
|
||||||
result = await legacy_track_url(json, format, wasm.legacy_stream_url)
|
result = await legacy_track_url(json, format, wasm.legacy_stream_url)
|
||||||
if (typeof result === 'object') {
|
if (typeof result === 'object') {
|
||||||
|
@ -89,55 +190,48 @@ async function track(id, format, tagging) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
let track
|
const track = await fetch(result)
|
||||||
if (tagging || encrypted) {
|
if (track.status !== 200) {
|
||||||
track = await fetch(result)
|
return new Response("Couldn't get track stream", { status: 403, headers: { 'content-type': 'text/plain' } })
|
||||||
if (track.status !== 200) {
|
|
||||||
return new Response("Couldn't get track stream", { status: 403, headers: { 'content-type': 'text/plain' } })
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
let id3
|
let id3
|
||||||
if (tagging) {
|
if (tagging) {
|
||||||
id3 = new ID3Writer(Buffer.alloc(0));
|
id3 = new ID3Writer(Buffer.alloc(0));
|
||||||
id3.padding = 0
|
id3.padding = 0
|
||||||
id3.setFrame('TIT2', json.title)
|
id3.setFrame('TIT2', json.SNG_TITLE)
|
||||||
.setFrame('TALB', json.album.title)
|
.setFrame('TALB', json.ALB_TITLE)
|
||||||
.setFrame('TPE2', json.artist.name)
|
.setFrame('TPE2', json.ART_NAME)
|
||||||
|
|
||||||
if (json.contributors !== undefined) {
|
if (json.ARTISTS !== undefined) {
|
||||||
contr_list = [];
|
artist_list = [];
|
||||||
for (const c of json.contributors) {
|
for (const a of json.ARTISTS) {
|
||||||
contr_list.push(c.name)
|
artist_list.push(a.ART_NAME)
|
||||||
}
|
}
|
||||||
|
|
||||||
id3.setFrame('TPE1', contr_list)
|
id3.setFrame('TPE1', artist_list)
|
||||||
}
|
}
|
||||||
|
|
||||||
if (json.track_position !== undefined) {
|
if (json.TRACK_NUMBER !== undefined) {
|
||||||
id3.setFrame('TRCK', json.track_position)
|
id3.setFrame('TRCK', json.TRACK_NUMBER)
|
||||||
}
|
}
|
||||||
|
|
||||||
if (json.disk_number !== undefined) {
|
if (json.DISK_NUMBER !== undefined) {
|
||||||
id3.setFrame('TPOS', json.disk_number)
|
id3.setFrame('TPOS', json.DISK_NUMBER)
|
||||||
}
|
}
|
||||||
|
|
||||||
if (json.isrc !== "") {
|
if (json.ISRC !== "") {
|
||||||
id3.setFrame('TSRC', json.isrc)
|
id3.setFrame('TSRC', json.ISRC)
|
||||||
}
|
}
|
||||||
|
|
||||||
if (json.bpm !== undefined) {
|
if (json.PHYSICAL_RELEASE_DATE !== undefined) {
|
||||||
id3.setFrame('TBPM', json.bpm)
|
const split = json.PHYSICAL_RELEASE_DATE.split('-')
|
||||||
}
|
|
||||||
|
|
||||||
if (json.release_date !== undefined) {
|
|
||||||
const split = json.release_date.split('-')
|
|
||||||
id3.setFrame('TYER', split[0])
|
id3.setFrame('TYER', split[0])
|
||||||
id3.setFrame('TDAT', split[2] + split[1])
|
id3.setFrame('TDAT', split[2] + split[1])
|
||||||
}
|
}
|
||||||
|
|
||||||
if (json.md5_image !== "") {
|
if (json.ALB_PICTURE !== "") {
|
||||||
const url = `https://cdns-images.dzcdn.net/images/cover/${json.md5_image}/1000x1000-000000-80-0-0.jpg`
|
const url = `https://cdns-images.dzcdn.net/images/cover/${json.ALB_PICTURE}/1000x1000-000000-80-0-0.jpg`
|
||||||
const cover = await fetch(url)
|
const cover = await fetch(url)
|
||||||
const coverBuffer = await cover.arrayBuffer()
|
const coverBuffer = await cover.arrayBuffer()
|
||||||
|
|
||||||
|
@ -152,31 +246,23 @@ async function track(id, format, tagging) {
|
||||||
}
|
}
|
||||||
|
|
||||||
let { readable, writable } = new TransformStream()
|
let { readable, writable } = new TransformStream()
|
||||||
let writer
|
const writer = writable.getWriter()
|
||||||
if (tagging || encrypted) {
|
|
||||||
writer = writable.getWriter()
|
|
||||||
}
|
|
||||||
|
|
||||||
if (tagging) {
|
if (tagging) {
|
||||||
writer.write(id3.arrayBuffer)
|
writer.write(id3.arrayBuffer)
|
||||||
}
|
}
|
||||||
|
|
||||||
// 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.FALLBACK) {
|
||||||
id = json.alternative.id.toString()
|
id = json.FALLBACK.SNG_ID
|
||||||
}
|
|
||||||
|
|
||||||
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 } })
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const cipher = new wasm.Cipher(id)
|
||||||
|
|
||||||
|
const length = parseInt(track.headers.get('Content-Length'))
|
||||||
|
|
||||||
|
pipeDecryptedStream(writer, track.body, length, cipher)
|
||||||
|
|
||||||
return new Response(readable, { status: 200, headers: { 'content-type': 'audio/mpeg' } })
|
return new Response(readable, { status: 200, headers: { 'content-type': 'audio/mpeg' } })
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -217,25 +303,21 @@ async function pipeDecryptedStream(writer, body, length, cipher) {
|
||||||
|
|
||||||
function legacy_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.FALLBACK) {
|
||||||
json = json.alternative
|
json = json.FALLBACK
|
||||||
}
|
}
|
||||||
|
|
||||||
const id = json.id
|
const id = json.SNG_ID.toString()
|
||||||
const md5_origin = json.md5_origin
|
const md5_origin = json.MD5_ORIGIN
|
||||||
const media_version = json.media_version
|
const media_version = json.MEDIA_VERSION
|
||||||
|
|
||||||
if (json['filesize_' + format] == false) {
|
|
||||||
return new Response('Format unavailable', { status: 403, headers: { 'content-type': 'text/plain' } })
|
|
||||||
}
|
|
||||||
|
|
||||||
format = format_string_to_num[format]
|
format = format_string_to_num[format]
|
||||||
|
|
||||||
return url_func(md5_origin, format, id.toString(), media_version)
|
return url_func(md5_origin, format, id, media_version)
|
||||||
}
|
}
|
||||||
|
|
||||||
async function m3u8(type, id, format, 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}?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" } })
|
||||||
|
|
11
src/lib.rs
11
src/lib.rs
|
@ -1,14 +1,13 @@
|
||||||
use wasm_bindgen::prelude::*;
|
use wasm_bindgen::prelude::*;
|
||||||
|
|
||||||
use blowfish::Blowfish;
|
use blowfish::Blowfish;
|
||||||
use aes::{Aes128, Aes256};
|
use aes::Aes128;
|
||||||
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];
|
||||||
|
@ -63,12 +62,4 @@ pub fn legacy_stream_url(md5_origin: &str, format: &str, id: &str, media_version
|
||||||
let ciphertext = cipher.encrypt_vec(&metadata_hash);
|
let ciphertext = cipher.encrypt_vec(&metadata_hash);
|
||||||
|
|
||||||
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()
|
|
||||||
}
|
}
|
|
@ -2,4 +2,8 @@ name = "dz"
|
||||||
type = "webpack"
|
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 = [
|
||||||
|
{ binding = "KV", id = "974c0967a84e415daa054bbbcc7f80c6", preview_id = "cfcc6491f3484cbca664913836635113" }
|
||||||
|
]
|
||||||
|
vars = { ARL = "haha nope" }
|
Loading…
Reference in New Issue