// ==UserScript== // @name dzunlock // @namespace io.github.uhwot.dzunlock // @description enables deezer hifi features lol // @author uh wot // @version 1.2.1 // @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 // @icon https://cdns-files.dzcdn.net/cache/images/common/favicon/favicon-96x96.852baf648e79894b668670e115e4a375.png // @include /^https:\/\/www\.deezer\.com\/[a-z]{2}\/($|track|album|artist|playlist|episode|show|profile|channels|podcasts|radio)/ // @match https://www.deezer.com/search/* // @match https://www.deezer.com/account/* // @match https://www.deezer.com/smarttracklist/* // @grant GM_getValue // @grant GM_setValue // @run-at document-start // ==/UserScript== const debug = false function log(...args) { if (debug) { return console.log(...args) } } // https://github.com/werk85/fetch-intercept/blob/develop/src/attach.js modified for browser support let interceptors = []; function interceptor(fetch, ...args) { const reversedInterceptors = interceptors.reduce((array, interceptor) => [interceptor].concat(array), []); let promise = Promise.resolve(args); // Register request interceptors reversedInterceptors.forEach(({ request, requestError }) => { if (request || requestError) { promise = promise.then(args => request(...args), requestError); } }); // Register fetch call promise = promise.then(args => { const request = new Request(...args); return fetch(request).then(response => { response.request = request; return response; }).catch(error => { error.request = request; return Promise.reject(error); }); }); // Register response interceptors reversedInterceptors.forEach(({ response, responseError }) => { if (response || responseError) { promise = promise.then(response, responseError); } }); return promise; } unsafeWindow.fetch = (function (fetch) { return function (...args) { return interceptor(fetch, ...args); }; })(unsafeWindow.fetch); fetchIntercept = { register: function (interceptor) { interceptors.push(interceptor); return () => { const index = interceptors.indexOf(interceptor); if (index >= 0) { interceptors.splice(index, 1); } }; }, clear: function () { interceptors = []; } }; // main code starts here const clientId = '119915' const clientSecret = '2f5b4c9785ddc367975b83d90dc46f5c' const formats = [ { api: '320', gw: 'MP3_320' }, { api: 'flac', gw: 'FLAC' } ] 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) let json = await resp.json() access_token = json['access_token'] // cache access token GM_setValue('access_token', access_token) GM_setValue('access_token_expiry', Math.floor(Date.now() / 1000) + Number(json['expires'])) return access_token } window.addEventListener('DOMContentLoaded', (_) => { unsafeWindow.dzPlayer.setTrackList = (function (old) { return async function (data, ...args) { // don't get filesizes if account is hifi if (unsafeWindow.dzPlayer.user_status.lossless) { return old(data, ...args) } let batchList = [] 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}` }) } } // return if all the tracks are user-upped if (batchList.length === 0) { return old(data, ...args) } batchList = JSON.stringify(batchList) let access_token = GM_getValue('access_token', false) const expiry = GM_getValue('access_token_expiry', 0) if (!access_token || Math.floor(Date.now() / 1000) >= expiry) { access_token = await renewAccessToken() } url = `https://api.deezer.com/batch?methods=${batchList}&access_token=${access_token}` let resp = await fetch(url) let json = await resp.json() // renew access token if token is invalid const error = json.batch_result[0].error if (error && error.code === 300) { access_token = await renewAccessToken() url = `https://api.deezer.com/batch?methods=${batchList}&access_token=${access_token}` resp = await fetch(url) json = await resp.json() } let userUppedSoFar = 0 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++) { data.data[i]['FILESIZE_' + formats[j].gw] = json.batch_result[i - userUppedSoFar]['filesize_' + formats[j].api] } } log(data) return old(data, ...args) }; })(unsafeWindow.dzPlayer.setTrackList); }); fetchIntercept.register({ request: function (url, config) { // Modify the url or config here return [url, config]; }, requestError: function (error) { // Called when an error occured during another 'request' interceptor call return Promise.reject(error); }, response: async function (response) { // Modify the response object if (response.url.startsWith('https://www.deezer.com/ajax/gw-light.php?method=deezer.getUserData')) { const json = await response.json() // removes upgrade popup stuff json.results.USER.ENTRYPOINTS = {} // needed to play premium-restricted albums like https://www.deezer.com/album/801279 json.results.OFFER_ID = 600 // disables ads json.results.USER.OPTIONS.ads_display = false json.results.USER.OPTIONS.ads_audio = false // disables call to get_url endpoint and enables track url gen json.results.__DZR_GATEKEEPS__.use_media_service = false log(json) return new Response(JSON.stringify(json)) } return response; }, responseError: function (error) { // Handle an fetch error return Promise.reject(error); } }); 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 json.message.result.limited = false //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);