diff --git a/dzunlock.user.js b/dzunlock.user.js index bee93fe..64451e3 100644 --- a/dzunlock.user.js +++ b/dzunlock.user.js @@ -3,7 +3,7 @@ // @namespace io.github.uhwot.dzunlock // @description enables deezer hifi features lol // @author uh wot -// @version 1.1.6 +// @version 1.2 // @license GPL-3.0-only // @homepageURL https://git.freezer.life/uhwot/dzunlock // @downloadURL https://git.freezer.life/uhwot/dzunlock/raw/branch/master/dzunlock.user.js @@ -12,7 +12,6 @@ // @match https://www.deezer.com/search/* // @match https://www.deezer.com/account/* // @match https://www.deezer.com/smarttracklist/* -// @require https://cdnjs.cloudflare.com/ajax/libs/aes-js/3.1.2/index.min.js // @grant GM_getValue // @grant GM_setValue // @run-at document-start @@ -87,52 +86,12 @@ fetchIntercept = { const clientId = '119915' const clientSecret = '2f5b4c9785ddc367975b83d90dc46f5c' -const playerTokenKey = [102, 228, 95, 242, 215, 50, 122, 26, 57, 216, 206, 38, 164, 237, 200, 85] const formats = [ { api: '320', gw: 'MP3_320' }, { api: 'flac', gw: 'FLAC' } ] -function str2bin(str) { - return Array.from(str).map(function (item) { - return item.charCodeAt(0); - }) -} - -function bin2str(bin) { - return String.fromCharCode.apply(String, bin); -} - -function decryptHex(hex, key) { - hex = aesjs.utils.hex.toBytes(hex) - let aesEcb = new aesjs.ModeOfOperation.ecb(key) - return bin2str(aesEcb.decrypt(hex)).replace(/\0+$/, '') // removes zero-padding -} - -function encryptHex(str, key) { - // zero-padding - if (str.length % 16) { - str += '\x00'.repeat(16 - str.length % 16) - } - - let aesEcb = new aesjs.ModeOfOperation.ecb(key) - return aesjs.utils.hex.fromBytes(aesEcb.encrypt(str2bin(str))) -} - -function playerTokenPatch(playerToken) { - playerToken = JSON.parse(decryptHex(playerToken, playerTokenKey)) - - // enables 320/flac quality selection - playerToken.audio_qualities.wifi_streaming = ['low', 'standard', 'high', 'lossless'] - // disables previews - playerToken.streaming = true - - log(playerToken) - - return encryptHex(JSON.stringify(playerToken), playerTokenKey) -} - async function renewAccessToken() { let url = `https://connect.deezer.com/oauth/access_token.php?grant_type=client_credentials&client_id=${clientId}&client_secret=${clientSecret}&output=json` let resp = await fetch(url) @@ -147,16 +106,16 @@ async function renewAccessToken() { window.addEventListener('DOMContentLoaded', (_) => { unsafeWindow.dzPlayer.setTrackList = (function (old) { - return async function (...args) { + return async function (data, ...args) { // don't get filesizes if account is hifi if (unsafeWindow.dzPlayer.user_status.lossless) { - return old(...args) + return old(data, ...args) } let batchList = [] - for (let i = 0; i < args[0].data.length; i++) { - const id = Number(args[0].data[i].SNG_ID) + for (let i = 0; i < data.data.length; i++) { + const id = Number(data.data[i].SNG_ID) if (id >= 0) { // we don't need filesizes for user-upped tracks batchList.push({ 'relative_url': `/track/${id}` }) } @@ -164,7 +123,7 @@ window.addEventListener('DOMContentLoaded', (_) => { // return if all the tracks are user-upped if (batchList.length === 0) { - return old(...args) + return old(data, ...args) } batchList = JSON.stringify(batchList) @@ -190,20 +149,20 @@ window.addEventListener('DOMContentLoaded', (_) => { let userUppedSoFar = 0 - for (let i = 0; i < args[0].data.length; i++) { - const id = Number(args[0].data[i].SNG_ID) + for (let i = 0; i < data.data.length; i++) { + const id = Number(data.data[i].SNG_ID) if (id < 0) { // user-uploaded track userUppedSoFar++ continue } for (let j = 0; j < formats.length; j++) { - args[0].data[i]['FILESIZE_' + formats[j].gw] = json.batch_result[i - userUppedSoFar]['filesize_' + formats[j].api] + data.data[i]['FILESIZE_' + formats[j].gw] = json.batch_result[i - userUppedSoFar]['filesize_' + formats[j].api] } } - log(args) + log(data) - return old(...args) + return old(data, ...args) }; })(unsafeWindow.dzPlayer.setTrackList); }); @@ -237,23 +196,11 @@ fetchIntercept.register({ // disables call to get_url endpoint and enables track url gen json.results.__DZR_GATEKEEPS__.use_media_service = false - json.results.PLAYER_TOKEN = playerTokenPatch(json.results.PLAYER_TOKEN) - log(json) return new Response(JSON.stringify(json)) } - if (response.url.startsWith('https://www.deezer.com/ajax/gw-light.php?method=log.listen')) { - const json = await response.json() - - if (typeof json.results === 'string') { - json.results = playerTokenPatch(json.results) - } - - return new Response(JSON.stringify(json)) - } - return response; }, @@ -262,4 +209,75 @@ fetchIntercept.register({ return Promise.reject(error); } -}); \ No newline at end of file +}); + +worker_input = function(e) { + let json = e.data + if (typeof json !== 'object') { + json = JSON.parse(json) + } + //console.log("input", json) + input = json + + if (json.message.md5) { + // track url gen + + // decrypted track endpoint + json.message.cdn = 'https://cdns-proxy-{0}.dzcdn.net/api/1/' + worker.postMessage(JSON.stringify(json)) + return + } + if (json.message.buffer) { + // track buffer decryption + // this code skips decryption entirely, since the track from the CDN is already unencrypted + + let out = { message: { error: undefined } } + out['Symbol(PromisingWorker.id)'] = json['Symbol(PromisingWorker.id)'] + out.message.result = json.message.buffer + //console.log("track buffer output", out) + postMessage(out) + return + } + + worker.postMessage(e.data) +} + +worker_output = function(e) { + let json = e.data + if (typeof json !== 'object') { + json = JSON.parse(json) + } + + if (input) { + if (input.message.token && !input.message.id) { + // player token decryption + + // enables 320/flac quality selection + json.message.result.audio_qualities.wifi_streaming = ['low', 'standard', 'high', 'lossless'] + // disables previews + json.message.result.streaming = true + + //console.log("player token output", json) + postMessage(JSON.stringify(json)) + return + } + } + + //console.log("output", json) + postMessage(e.data) +} + +unsafeWindow.URL.createObjectURL = (function (old) { + return function (object) { + if (object instanceof Blob) { + let worker = ` + let worker = new Worker('${old(object)}') + let input + worker.onmessage = ${String(worker_output)} + onmessage = ${String(worker_input)} + ` + return old(new Blob([worker])) + } + return old(object); + }; +})(unsafeWindow.URL.createObjectURL); \ No newline at end of file