dzunlock/dzunlock.user.js

260 lines
8.4 KiB
JavaScript
Raw Normal View History

2021-07-11 11:24:25 +00:00
// ==UserScript==
// @name dzunlock
// @namespace io.github.uhwot.dzunlock
2021-07-19 18:28:54 +00:00
// @description enables deezer premium features lol
2021-07-11 11:24:25 +00:00
// @author uh wot
2021-08-10 13:21:57 +00:00
// @version 1.2.6
2021-07-13 11:30:22 +00:00
// @license GPL-3.0-only
2021-07-11 16:19:38 +00:00
// @homepageURL https://git.freezer.life/uhwot/dzunlock
// @downloadURL https://git.freezer.life/uhwot/dzunlock/raw/branch/master/dzunlock.user.js
2021-07-11 11:24:25 +00:00
// @icon https://cdns-files.dzcdn.net/cache/images/common/favicon/favicon-96x96.852baf648e79894b668670e115e4a375.png
2021-07-13 12:43:33 +00:00
// @include /^https:\/\/www\.deezer\.com\/[a-z]{2}\/($|track|album|artist|playlist|episode|show|profile|channels|podcasts|radio)/
2021-07-11 11:24:25 +00:00
// @match https://www.deezer.com/search/*
// @match https://www.deezer.com/account/*
2021-07-13 12:43:33 +00:00
// @match https://www.deezer.com/smarttracklist/*
2021-08-10 13:21:57 +00:00
// @require https://cdnjs.cloudflare.com/ajax/libs/aes-js/3.1.2/index.min.js
2021-07-13 12:41:25 +00:00
// @grant GM_getValue
// @grant GM_setValue
2021-07-13 23:20:50 +00:00
// @run-at document-start
2021-07-11 11:24:25 +00:00
// ==/UserScript==
2021-07-11 16:26:04 +00:00
const debug = false
2021-07-11 11:24:25 +00:00
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'
2021-08-10 13:21:57 +00:00
const playerTokenKey = [102, 228, 95, 242, 215, 50, 122, 26, 57, 216, 206, 38, 164, 237, 200, 85]
const cipher = new aesjs.ModeOfOperation.ecb(playerTokenKey)
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) {
hex = aesjs.utils.hex.toBytes(hex)
return bin2str(cipher.decrypt(hex)).replace(/\0+$/, '') // removes zero-padding
}
function encryptHex(str) {
// zero-padding
if (str.length % 16) {
str += '\x00'.repeat(16 - str.length % 16)
}
return aesjs.utils.hex.fromBytes(cipher.encrypt(str2bin(str)))
}
function playerTokenPatch(playerToken) {
playerToken = JSON.parse(decryptHex(playerToken))
// enables 320/flac quality selection
playerToken.audio_qualities.wifi_streaming = ['low', 'standard', 'high']
// disables previews
playerToken.streaming = true
playerToken.limited = false
log(playerToken)
return encryptHex(JSON.stringify(playerToken))
}
2021-07-11 11:24:25 +00:00
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
}
2021-07-13 23:20:50 +00:00
window.addEventListener('DOMContentLoaded', (_) => {
unsafeWindow.dzPlayer.setTrackList = (function (old) {
2021-07-16 11:13:43 +00:00
return async function (data, ...args) {
2021-07-13 23:20:50 +00:00
// don't get filesizes if account is hifi
if (unsafeWindow.dzPlayer.user_status.lossless) {
2021-07-16 11:13:43 +00:00
return old(data, ...args)
2021-07-13 23:20:50 +00:00
}
2021-07-13 23:20:50 +00:00
let batchList = []
2021-07-16 11:13:43 +00:00
for (let i = 0; i < data.data.length; i++) {
const id = Number(data.data[i].SNG_ID)
2021-07-13 23:20:50 +00:00
if (id >= 0) { // we don't need filesizes for user-upped tracks
batchList.push({ 'relative_url': `/track/${id}` })
}
}
2021-07-13 23:20:50 +00:00
// return if all the tracks are user-upped
if (batchList.length === 0) {
2021-07-16 11:13:43 +00:00
return old(data, ...args)
2021-07-13 23:20:50 +00:00
}
2021-07-13 23:20:50 +00:00
batchList = JSON.stringify(batchList)
2021-07-13 23:20:50 +00:00
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}`
2021-07-13 23:20:50 +00:00
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()
}
2021-07-13 23:20:50 +00:00
let userUppedSoFar = 0
2021-07-16 11:13:43 +00:00
for (let i = 0; i < data.data.length; i++) {
const id = Number(data.data[i].SNG_ID)
2021-07-13 23:20:50 +00:00
if (id < 0) { // user-uploaded track
userUppedSoFar++
continue
}
2021-07-19 18:24:32 +00:00
data.data[i].FILESIZE_MP3_320 = json.batch_result[i - userUppedSoFar].filesize_320
2021-07-16 20:20:30 +00:00
// for streaming while unlogged
data.data[i].MD5_ORIGIN = json.batch_result[i - userUppedSoFar].md5_origin
}
2021-07-11 11:24:25 +00:00
2021-07-16 11:13:43 +00:00
log(data)
2021-07-11 11:24:25 +00:00
2021-07-16 11:13:43 +00:00
return old(data, ...args)
2021-07-13 23:20:50 +00:00
};
})(unsafeWindow.dzPlayer.setTrackList);
});
2021-07-11 11:24:25 +00:00
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')) {
2021-07-19 09:37:31 +00:00
let json = await response.json()
2021-07-11 11:24:25 +00:00
// 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
2021-07-11 11:24:25 +00:00
2021-08-10 13:21:57 +00:00
json.results.PLAYER_TOKEN = playerTokenPatch(json.results.PLAYER_TOKEN)
2021-07-11 11:24:25 +00:00
log(json)
return new Response(JSON.stringify(json))
}
2021-08-10 13:21:57 +00:00
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))
}
2021-07-11 11:24:25 +00:00
return response;
},
responseError: function (error) {
// Handle an fetch error
return Promise.reject(error);
}
2021-08-10 13:21:57 +00:00
});