284 lines
8.9 KiB
JavaScript
284 lines
8.9 KiB
JavaScript
// ==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); |