Skip to content

Instantly share code, notes, and snippets.

@aleandroid
Last active September 27, 2023 15:39
Show Gist options
  • Save aleandroid/cbd675e1655e22b2727ef02dedfe3dfb to your computer and use it in GitHub Desktop.
Save aleandroid/cbd675e1655e22b2727ef02dedfe3dfb to your computer and use it in GitHub Desktop.
Deezer Downloader Monkey Script
//---CONFIGURATION---//
const showMp3_128 = true; // Show MP3 @128k download (default: true)
const showMp3_320 = true; // Show MP3 @320k download (default: true)
const showFLAC = true; // Show FLAC download (default: true)
const showAzLyrics = true; // Show azLyrics checkbox (default: true)
const showListDownloader = true; // Show bulk download option (default: true)
const coverSize = 600; // JPEG cover size in px (default: 600)
const coverQuality = 80; // JPEG cover quality from 0 to 100 (default: 80)
//---DEBUG---//
const SHOW_KEYS = !true; // Show pressed keys (default: !true)
const DEBUG = !true; // Debug ON/OFF (default: !true)
const L10nDEBUG = !true; // Enable localization override (default: !true)
const L10nOVERRIDE = 'en'; // Override browser language with selected (default: 'en')
// ==UserScript==
// @name DeezRevived
// @description Download music from Deezer in any chosen format. Supports HQ/HiFi even without Premium subscription.
// @version 1.1.8.1
// @author Deki Haker, Kawashi666 and some others developers
// devs Jonathan Tavares (revisions, fixes, proofreading, compatibility)
// devs K-mik@Z (fixes, proofreading, style, translation, compatibility)
// devs AHOHNMYC (code refractoring, fixes, translation)
// devs AleX (fixes, style, compatibility)
// @namespace Script from original 'Deezer:Download', revisited by some developers and contributors
// @icon https://i.imgur.com/kFReGp3.jpg
// screenshot https://i.imgur.com/7FogWtb.jpg
// license Beerware License https://paste.debian.net/hidden/plainh/f360edb6
// @homepageURL https://gist.github.com/aleandroid/cbd675e1655e22b2727ef02dedfe3dfb
// @downloadURL https://gist.github.com/aleandroid/cbd675e1655e22b2727ef02dedfe3dfb/raw/deez.revived.user.js
// @updateURL https://gist.github.com/aleandroid/cbd675e1655e22b2727ef02dedfe3dfb/raw/deez.revived.user.js
// @grant unsafeWindow
// @grant GM_xmlhttpRequest
// @grant GM.xmlHttpRequest
// @connect azlyrics.com
// @connect api.deezer.com
// @require https://gist.githubusercontent.com/aleandroid/ca7b7fd58a4a05751f5608107f87a7f0/raw/PajHome.MD5.min.js
// license BSD
// attribution Paul Johnston http://pajhome.org.uk/crypt/md5/
// @require https://cdnjs.cloudflare.com/ajax/libs/aes-js/3.1.2/index.min.js
// license MIT
// attribution ricmoo https://github.com/ricmoo/aes-js
// @match https://www.deezer.com/*
// @description:de Laden Sie Musik von Deezer im MP3 oder FLAC format herunter, auch ohne Abonnement!
// @description:en Download music from Deezer in MP3 or FLAC format, even without a subscription!
// @description:es ¡Descargue música de Deezer en formato MP3 o FLAC, incluso sin suscripción!
// @description:fr Téléchargez de la musique de Deezer au format MP3 ou FLAC, même sans abonnement!
// @description:it Scarica musica da Deezer in formato MP3 o FLAC, anche senza abbonamento!
// @description:pt Baixe músicas do Deezer em formato MP3 ou FLAC, mesmo sem uma assinatura!
// @description:ru Скачивайте музыку с Deezer в формате MP3 или FLAC, даже без подписки!
// @noframes
// compatible Chrome (TamperMonkey, ViolentMonkey)
// compatible Opera (TamperMonkey)
// compatible Firefox (TamperMonkey, GreaseMonkey, ViolentMonkey)
// compatible Edge (TamperMonkey)
// ==/UserScript==
const _getI18nMap = { // Default lang is 'en'
en: {
downloading: 'Downloading',
decrypting: 'Decrypting',
waiting: 'Waiting',
choose: 'Choose',
fileNaming: 'file naming',
title: 'Title',
artist: 'Artist',
refresh: 'Refresh',
album: 'Album',
mb: 'МB',
duration: 'Duration',
coverDownloading: 'Downloading cover',
downloaderHotkey: 'Open "Deezer:D\➲wnloader"',
tracklist: 'Tracklist',
clickToOpen: 'Click to open\nor D key (keyboard shortcut)',
downloadList: 'Download tracklist',
lyricsDownloading: 'Downloading lyrics',
gettingAlbumInfo: 'Getting album info'
},
de: {
downloading: 'Herunterladen',
decrypting: 'Entschlüsselung',
waiting: 'Warten',
choose: 'Wählen',
fileNaming: 'Sie die Dateibenennung',
title: 'Titel',
artist: 'Künstler',
refresh: 'Aktualisiere die',
album: 'Album',
duration: 'Dauer',
coverDownloading: 'Frontabdeckung Herunterladen',
mb: 'МB',
downloaderHotkey: 'Öffnen Sie das Panel "Deezer:D\➲wnloader"',
tracklist: 'Liste der Tracks',
clickToOpen: 'Klicken Sie auf Öffnen\noder D-Taste (Tastaturkürzel)',
downloadList: 'Laden Sie die Liste herunter',
lyricsDownloading: 'Herunterladen der Liedtexte',
gettingAlbumInfo: 'Informationen zum Album erhalten'
},
es: {
downloading: 'Descargan',
decrypting: 'Descifrado',
waiting: 'Esperando la descarga',
choose: 'Elege',
fileNaming: 'el nombre del archivo',
title: 'Título',
artist: 'Artista',
refresh: 'actualizar la',
album: 'Álbum',
duration: 'Duración',
coverDownloading: 'Descargando la portada',
mb: 'МB',
downloaderHotkey: 'Abra el panel "Deezer:D\➲wnloader"',
tracklist: 'Lista de pistas',
clickToOpen: 'Haga clic para abrir\no la tecla D (atajo de teclado)',
downloadList: 'Descargue la lista',
lyricsDownloading: 'Descargando la letra',
gettingAlbumInfo: 'Obtener información del álbum'
},
fr: {
downloading: 'Téléchargement',
decrypting: 'Décryptage',
waiting: 'En attente',
choose: 'choisir',
fileNaming: 'le nom de fichier souhaité',
title: 'Titre',
artist: 'Artiste',
refresh: 'Rafraîchir la',
album: 'Album',
duration: 'Durée',
coverDownloading: 'Téléchargement de la couverture',
mb: 'MB',
downloaderHotkey: 'Ouvrir "Deezer:D\➲wnloader"',
tracklist: 'Liste des pistes',
clickToOpen: 'Cliquer pour ouvrir\nou touche D (raccourcis clavier)',
downloadList: 'Télécharger la liste',
lyricsDownloading: 'Télécharger les paroles',
gettingAlbumInfo: 'Obtenir des informations sur l\'album'
},
it: {
downloading: 'In download',
decrypting: 'Decrypting',
waiting: 'In attesa',
choose: 'Imposta',
fileNaming: 'denominazione file',
title: 'Titolo',
artist: 'Artista',
refresh: 'Aggiorna',
album: 'Album',
duration: 'Durata',
coverDownloading: 'Download copertina',
mb: 'МB',
downloaderHotkey: 'Apri "Deezer:D\➲wnloader"',
tracklist: 'Tracklist',
clickToOpen: 'Fai click per aprire\no premi il tasto D',
downloadList: 'Scarica tracklist',
lyricsDownloading: 'Download testi...',
gettingAlbumInfo: 'Download informazioni album...'
},
pt: {
downloading: 'Baixar',
decrypting: 'Descriptografar',
waiting: 'Carregando',
choose: 'Escolha',
fileNaming: 'o nome do arquivo',
title: 'Título',
artist: 'Artista',
refresh: 'Atualize a',
album: 'Álbum',
duration: 'Duração',
coverDownloading: 'Baixando capa',
mb: 'МB',
downloaderHotkey: 'Abra o painel "Deezer\:D\➲wnloader"',
tracklist: 'Lista de faixas',
clickToOpen: 'Clique para abrir\nou tecla D (atalho do teclado)',
downloadList: 'Baixe a lista',
lyricsDownloading: 'Download das letras',
gettingAlbumInfo: 'Obtendo informações sobre o álbum'
},
'pt-BR': {
downloading: 'Baixar',
decrypting: 'Descriptografar',
waiting: 'Carregando',
choose: 'Escolha',
fileNaming: 'o nome do arquivo',
title: 'Título',
artist: 'Artista',
refresh: 'Atualize a',
album: 'Álbum',
duration: 'Duração',
coverDownloading: 'Baixando capa',
mb: 'МB',
downloaderHotkey: 'Abra o painel "Deezer:D\➲wnloader"',
tracklist: 'Lista de faixas',
clickToOpen: 'Clique para abrir\nou tecla D (atalho do teclado)',
downloadList: 'Baixe a lista',
lyricsDownloading: 'Download das letras',
gettingAlbumInfo: 'Obtendo informações sobre o álbum'
},
ru: {
downloading: 'Загрузка',
decrypting: 'Расшифровка',
waiting: 'Ожидание загрузки',
choose: 'Выбор',
fileNaming: 'имени файла',
title: 'Название',
artist: 'Исполнитель',
refresh: 'Обновить',
album: 'Альбом',
duration: 'Длительность',
coverDownloading: 'Загрузка обложки',
mb: 'МБ',
downloaderHotkey: 'Открыть панель "Deezer:D\➲wnloader"',
tracklist: 'Список треков',
clickToOpen: 'Клик здесь или нажатие клавиши D\nоткроют панель',
downloadList: 'Загрузить список',
lyricsDownloading: 'Загрузка текста',
gettingAlbumInfo: 'Получение информации об альбоме'
}
};
//[ °°° SECTION GUI LANGUAGE °°° ]
let L10n = L10nDEBUG ? L10nOVERRIDE : navigator.language;
if (!_getI18nMap[L10n]) L10n = L10n.substring(0, 2);
if (!_getI18nMap[L10n]) L10n = 'en';
const translate = _getI18nMap[L10n]; // Now, use translate.whatYouWant ( better than translate[whatYouWant] ).
/* Replaces `10 ^mb' to `10 MB' */
function translateCircumstring(data) {
return data.replace(/\^(\w+)/g, (full, part) => translate[part] ? translate[part] : full);
}
// FETCH SUBSTITUTE
if (GM_info.scriptHandler) {
if (!localStorage.gmFetchWarningShown) {
unsafeWindow.localStorage.gmFetchWarningShown = 'false';
alert('We cannot safely replace `fetch()\' in GreaseMonkey. If troubles starts, replace GreaseMonkey with ViolentMonkey or TamperMonkey. This message will not be shown anymore');
}
} else {
const orig_fetch = unsafeWindow.fetch;
unsafeWindow.fetch = (input, init = {}) => {
if (DEBUG) console.info('fetch() to:', input);
if (input.startsWith('https://www.deezer.com/ajax/gw-light.php?method=log.listen'))
return;
if (input.startsWith('https://www.deezer.com/ajax/gw-light.php')) {
orig_fetch(input, init)
.then(response => response.json())
.then(trackRecurseLookup);
}
return orig_fetch(input, init);
};
}
function trackRecurseLookup(key) {
if (key !== null && typeof key === 'object') {
if (key.SNG_ID)
trackDB[key.SNG_ID] = key;
else if (key.forEach)
key.forEach(trackRecurseLookup);
else
Object.keys(key).forEach(subKey => trackRecurseLookup(key[subKey]));
}
}
// config: url, responseType, onProgress, onSuccess, anyway
function niceXhr(config) {
const data = {
responseType: config.responseType,
method: 'GET',
url: config.url,
onload: e => {
if (200 === e.status)
config.onSuccess(e.response);
else
console.warn('Error with getting data from', e.finalUrl);
config.anyway();
},
onerror: config.anyway,
onabort: config.anyway,
onprogress: config.onProgress,
};
// Greasemonkey with their 4.0 API became a monster ~
if (typeof GM !== 'undefined' && typeof GM.xmlHttpRequest !== 'undefined')
GM.xmlHttpRequest(data);
else
GM_xmlhttpRequest(data);
}
// WORKER
function createWorker(code) {
const blobURL = URL.createObjectURL(new Blob(
['(', code.toString(), ')()'], {
type: 'text/javascript'
}
));
const worker = new Worker(blobURL);
URL.revokeObjectURL(blobURL);
return worker;
}
const mainWk = createWorker(function() {
// BLOWFISH LIBRARY, adapted from https://github.com/agorlov/javascript-blowfish (MIT-licensed)
// Modified to work with byte arrays, and also supports encryption / decryption in-place for buffers.
// Now it works ONLY with byte arrays and can ONLY decrypt data ONLY in CBC mode.
// Cannot be @require-d, as it is part of worker code. Workers share NO data so everything must be embedded.
const Blowfish = function(key) {
this.key = key;
this.sBox0 = new Uint32Array([0xd1310ba6, 0x98dfb5ac, 0x2ffd72db, 0xd01adfb7, 0xb8e1afed, 0x6a267e96, 0xba7c9045, 0xf12c7f99, 0x24a19947, 0xb3916cf7, 0x0801f2e2, 0x858efc16, 0x636920d8, 0x71574e69, 0xa458fea3, 0xf4933d7e, 0x0d95748f, 0x728eb658, 0x718bcd58, 0x82154aee, 0x7b54a41d, 0xc25a59b5, 0x9c30d539, 0x2af26013, 0xc5d1b023, 0x286085f0, 0xca417918, 0xb8db38ef, 0x8e79dcb0, 0x603a180e, 0x6c9e0e8b, 0xb01e8a3e, 0xd71577c1, 0xbd314b27, 0x78af2fda, 0x55605c60, 0xe65525f3, 0xaa55ab94, 0x57489862, 0x63e81440, 0x55ca396a, 0x2aab10b6, 0xb4cc5c34, 0x1141e8ce, 0xa15486af, 0x7c72e993, 0xb3ee1411, 0x636fbc2a, 0x2ba9c55d, 0x741831f6, 0xce5c3e16, 0x9b87931e, 0xafd6ba33, 0x6c24cf5c, 0x7a325381, 0x28958677, 0x3b8f4898, 0x6b4bb9af, 0xc4bfe81b, 0x66282193, 0x61d809cc, 0xfb21a991, 0x487cac60, 0x5dec8032, 0xef845d5d, 0xe98575b1, 0xdc262302, 0xeb651b88, 0x23893e81, 0xd396acc5, 0x0f6d6ff3, 0x83f44239, 0x2e0b4482, 0xa4842004, 0x69c8f04a, 0x9e1f9b5e, 0x21c66842, 0xf6e96c9a, 0x670c9c61, 0xabd388f0, 0x6a51a0d2, 0xd8542f68, 0x960fa728, 0xab5133a3, 0x6eef0b6c, 0x137a3be4, 0xba3bf050, 0x7efb2a98, 0xa1f1651d, 0x39af0176, 0x66ca593e, 0x82430e88, 0x8cee8619, 0x456f9fb4, 0x7d84a5c3, 0x3b8b5ebe, 0xe06f75d8, 0x85c12073, 0x401a449f, 0x56c16aa6, 0x4ed3aa62, 0x363f7706, 0x1bfedf72, 0x429b023d, 0x37d0d724, 0xd00a1248, 0xdb0fead3, 0x49f1c09b, 0x075372c9, 0x80991b7b, 0x25d479d8, 0xf6e8def7, 0xe3fe501a, 0xb6794c3b, 0x976ce0bd, 0x04c006ba, 0xc1a94fb6, 0x409f60c4, 0x5e5c9ec2, 0x196a2463, 0x68fb6faf, 0x3e6c53b5, 0x1339b2eb, 0x3b52ec6f, 0x6dfc511f, 0x9b30952c, 0xcc814544, 0xaf5ebd09, 0xbee3d004, 0xde334afd, 0x660f2807, 0x192e4bb3, 0xc0cba857, 0x45c8740f, 0xd20b5f39, 0xb9d3fbdb, 0x5579c0bd, 0x1a60320a, 0xd6a100c6, 0x402c7279, 0x679f25fe, 0xfb1fa3cc, 0x8ea5e9f8, 0xdb3222f8, 0x3c7516df, 0xfd616b15, 0x2f501ec8, 0xad0552ab, 0x323db5fa, 0xfd238760, 0x53317b48, 0x3e00df82, 0x9e5c57bb, 0xca6f8ca0, 0x1a87562e, 0xdf1769db, 0xd542a8f6, 0x287effc3, 0xac6732c6, 0x8c4f5573, 0x695b27b0, 0xbbca58c8, 0xe1ffa35d, 0xb8f011a0, 0x10fa3d98, 0xfd2183b8, 0x4afcb56c, 0x2dd1d35b, 0x9a53e479, 0xb6f84565, 0xd28e49bc, 0x4bfb9790, 0xe1ddf2da, 0xa4cb7e33, 0x62fb1341, 0xcee4c6e8, 0xef20cada, 0x36774c01, 0xd07e9efe, 0x2bf11fb4, 0x95dbda4d, 0xae909198, 0xeaad8e71, 0x6b93d5a0, 0xd08ed1d0, 0xafc725e0, 0x8e3c5b2f, 0x8e7594b7, 0x8ff6e2fb, 0xf2122b64, 0x8888b812, 0x900df01c, 0x4fad5ea0, 0x688fc31c, 0xd1cff191, 0xb3a8c1ad, 0x2f2f2218, 0xbe0e1777, 0xea752dfe, 0x8b021fa1, 0xe5a0cc0f, 0xb56f74e8, 0x18acf3d6, 0xce89e299, 0xb4a84fe0, 0xfd13e0b7, 0x7cc43b81, 0xd2ada8d9, 0x165fa266, 0x80957705, 0x93cc7314, 0x211a1477, 0xe6ad2065, 0x77b5fa86, 0xc75442f5, 0xfb9d35cf, 0xebcdaf0c, 0x7b3e89a0, 0xd6411bd3, 0xae1e7e49, 0x00250e2d, 0x2071b35e, 0x226800bb, 0x57b8e0af, 0x2464369b, 0xf009b91e, 0x5563911d, 0x59dfa6aa, 0x78c14389, 0xd95a537f, 0x207d5ba2, 0x02e5b9c5, 0x83260376, 0x6295cfa9, 0x11c81968, 0x4e734a41, 0xb3472dca, 0x7b14a94a, 0x1b510052, 0x9a532915, 0xd60f573f, 0xbc9bc6e4, 0x2b60a476, 0x81e67400, 0x08ba6fb5, 0x571be91f, 0xf296ec6b, 0x2a0dd915, 0xb6636521, 0xe7b9f9b6, 0xff34052e, 0xc5855664, 0x53b02d5d, 0xa99f8fa1, 0x08ba4799, 0x6e85076a]);
this.sBox1 = new Uint32Array([0x4b7a70e9, 0xb5b32944, 0xdb75092e, 0xc4192623, 0xad6ea6b0, 0x49a7df7d, 0x9cee60b8, 0x8fedb266, 0xecaa8c71, 0x699a17ff, 0x5664526c, 0xc2b19ee1, 0x193602a5, 0x75094c29, 0xa0591340, 0xe4183a3e, 0x3f54989a, 0x5b429d65, 0x6b8fe4d6, 0x99f73fd6, 0xa1d29c07, 0xefe830f5, 0x4d2d38e6, 0xf0255dc1, 0x4cdd2086, 0x8470eb26, 0x6382e9c6, 0x021ecc5e, 0x09686b3f, 0x3ebaefc9, 0x3c971814, 0x6b6a70a1, 0x687f3584, 0x52a0e286, 0xb79c5305, 0xaa500737, 0x3e07841c, 0x7fdeae5c, 0x8e7d44ec, 0x5716f2b8, 0xb03ada37, 0xf0500c0d, 0xf01c1f04, 0x0200b3ff, 0xae0cf51a, 0x3cb574b2, 0x25837a58, 0xdc0921bd, 0xd19113f9, 0x7ca92ff6, 0x94324773, 0x22f54701, 0x3ae5e581, 0x37c2dadc, 0xc8b57634, 0x9af3dda7, 0xa9446146, 0x0fd0030e, 0xecc8c73e, 0xa4751e41, 0xe238cd99, 0x3bea0e2f, 0x3280bba1, 0x183eb331, 0x4e548b38, 0x4f6db908, 0x6f420d03, 0xf60a04bf, 0x2cb81290, 0x24977c79, 0x5679b072, 0xbcaf89af, 0xde9a771f, 0xd9930810, 0xb38bae12, 0xdccf3f2e, 0x5512721f, 0x2e6b7124, 0x501adde6, 0x9f84cd87, 0x7a584718, 0x7408da17, 0xbc9f9abc, 0xe94b7d8c, 0xec7aec3a, 0xdb851dfa, 0x63094366, 0xc464c3d2, 0xef1c1847, 0x3215d908, 0xdd433b37, 0x24c2ba16, 0x12a14d43, 0x2a65c451, 0x50940002, 0x133ae4dd, 0x71dff89e, 0x10314e55, 0x81ac77d6, 0x5f11199b, 0x043556f1, 0xd7a3c76b, 0x3c11183b, 0x5924a509, 0xf28fe6ed, 0x97f1fbfa, 0x9ebabf2c, 0x1e153c6e, 0x86e34570, 0xeae96fb1, 0x860e5e0a, 0x5a3e2ab3, 0x771fe71c, 0x4e3d06fa, 0x2965dcb9, 0x99e71d0f, 0x803e89d6, 0x5266c825, 0x2e4cc978, 0x9c10b36a, 0xc6150eba, 0x94e2ea78, 0xa5fc3c53, 0x1e0a2df4, 0xf2f74ea7, 0x361d2b3d, 0x1939260f, 0x19c27960, 0x5223a708, 0xf71312b6, 0xebadfe6e, 0xeac31f66, 0xe3bc4595, 0xa67bc883, 0xb17f37d1, 0x018cff28, 0xc332ddef, 0xbe6c5aa5, 0x65582185, 0x68ab9802, 0xeecea50f, 0xdb2f953b, 0x2aef7dad, 0x5b6e2f84, 0x1521b628, 0x29076170, 0xecdd4775, 0x619f1510, 0x13cca830, 0xeb61bd96, 0x0334fe1e, 0xaa0363cf, 0xb5735c90, 0x4c70a239, 0xd59e9e0b, 0xcbaade14, 0xeecc86bc, 0x60622ca7, 0x9cab5cab, 0xb2f3846e, 0x648b1eaf, 0x19bdf0ca, 0xa02369b9, 0x655abb50, 0x40685a32, 0x3c2ab4b3, 0x319ee9d5, 0xc021b8f7, 0x9b540b19, 0x875fa099, 0x95f7997e, 0x623d7da8, 0xf837889a, 0x97e32d77, 0x11ed935f, 0x16681281, 0x0e358829, 0xc7e61fd6, 0x96dedfa1, 0x7858ba99, 0x57f584a5, 0x1b227263, 0x9b83c3ff, 0x1ac24696, 0xcdb30aeb, 0x532e3054, 0x8fd948e4, 0x6dbc3128, 0x58ebf2ef, 0x34c6ffea, 0xfe28ed61, 0xee7c3c73, 0x5d4a14d9, 0xe864b7e3, 0x42105d14, 0x203e13e0, 0x45eee2b6, 0xa3aaabea, 0xdb6c4f15, 0xfacb4fd0, 0xc742f442, 0xef6abbb5, 0x654f3b1d, 0x41cd2105, 0xd81e799e, 0x86854dc7, 0xe44b476a, 0x3d816250, 0xcf62a1f2, 0x5b8d2646, 0xfc8883a0, 0xc1c7b6a3, 0x7f1524c3, 0x69cb7492, 0x47848a0b, 0x5692b285, 0x095bbf00, 0xad19489d, 0x1462b174, 0x23820e00, 0x58428d2a, 0x0c55f5ea, 0x1dadf43e, 0x233f7061, 0x3372f092, 0x8d937e41, 0xd65fecf1, 0x6c223bdb, 0x7cde3759, 0xcbee7460, 0x4085f2a7, 0xce77326e, 0xa6078084, 0x19f8509e, 0xe8efd855, 0x61d99735, 0xa969a7aa, 0xc50c06c2, 0x5a04abfc, 0x800bcadc, 0x9e447a2e, 0xc3453484, 0xfdd56705, 0x0e1e9ec9, 0xdb73dbd3, 0x105588cd, 0x675fda79, 0xe3674340, 0xc5c43465, 0x713e38d8, 0x3d28f89e, 0xf16dff20, 0x153e21e7, 0x8fb03d4a, 0xe6e39f2b, 0xdb83adf7]);
this.sBox2 = new Uint32Array([0xe93d5a68, 0x948140f7, 0xf64c261c, 0x94692934, 0x411520f7, 0x7602d4f7, 0xbcf46b2e, 0xd4a20068, 0xd4082471, 0x3320f46a, 0x43b7d4b7, 0x500061af, 0x1e39f62e, 0x97244546, 0x14214f74, 0xbf8b8840, 0x4d95fc1d, 0x96b591af, 0x70f4ddd3, 0x66a02f45, 0xbfbc09ec, 0x03bd9785, 0x7fac6dd0, 0x31cb8504, 0x96eb27b3, 0x55fd3941, 0xda2547e6, 0xabca0a9a, 0x28507825, 0x530429f4, 0x0a2c86da, 0xe9b66dfb, 0x68dc1462, 0xd7486900, 0x680ec0a4, 0x27a18dee, 0x4f3ffea2, 0xe887ad8c, 0xb58ce006, 0x7af4d6b6, 0xaace1e7c, 0xd3375fec, 0xce78a399, 0x406b2a42, 0x20fe9e35, 0xd9f385b9, 0xee39d7ab, 0x3b124e8b, 0x1dc9faf7, 0x4b6d1856, 0x26a36631, 0xeae397b2, 0x3a6efa74, 0xdd5b4332, 0x6841e7f7, 0xca7820fb, 0xfb0af54e, 0xd8feb397, 0x454056ac, 0xba489527, 0x55533a3a, 0x20838d87, 0xfe6ba9b7, 0xd096954b, 0x55a867bc, 0xa1159a58, 0xcca92963, 0x99e1db33, 0xa62a4a56, 0x3f3125f9, 0x5ef47e1c, 0x9029317c, 0xfdf8e802, 0x04272f70, 0x80bb155c, 0x05282ce3, 0x95c11548, 0xe4c66d22, 0x48c1133f, 0xc70f86dc, 0x07f9c9ee, 0x41041f0f, 0x404779a4, 0x5d886e17, 0x325f51eb, 0xd59bc0d1, 0xf2bcc18f, 0x41113564, 0x257b7834, 0x602a9c60, 0xdff8e8a3, 0x1f636c1b, 0x0e12b4c2, 0x02e1329e, 0xaf664fd1, 0xcad18115, 0x6b2395e0, 0x333e92e1, 0x3b240b62, 0xeebeb922, 0x85b2a20e, 0xe6ba0d99, 0xde720c8c, 0x2da2f728, 0xd0127845, 0x95b794fd, 0x647d0862, 0xe7ccf5f0, 0x5449a36f, 0x877d48fa, 0xc39dfd27, 0xf33e8d1e, 0x0a476341, 0x992eff74, 0x3a6f6eab, 0xf4f8fd37, 0xa812dc60, 0xa1ebddf8, 0x991be14c, 0xdb6e6b0d, 0xc67b5510, 0x6d672c37, 0x2765d43b, 0xdcd0e804, 0xf1290dc7, 0xcc00ffa3, 0xb5390f92, 0x690fed0b, 0x667b9ffb, 0xcedb7d9c, 0xa091cf0b, 0xd9155ea3, 0xbb132f88, 0x515bad24, 0x7b9479bf, 0x763bd6eb, 0x37392eb3, 0xcc115979, 0x8026e297, 0xf42e312d, 0x6842ada7, 0xc66a2b3b, 0x12754ccc, 0x782ef11c, 0x6a124237, 0xb79251e7, 0x06a1bbe6, 0x4bfb6350, 0x1a6b1018, 0x11caedfa, 0x3d25bdd8, 0xe2e1c3c9, 0x44421659, 0x0a121386, 0xd90cec6e, 0xd5abea2a, 0x64af674e, 0xda86a85f, 0xbebfe988, 0x64e4c3fe, 0x9dbc8057, 0xf0f7c086, 0x60787bf8, 0x6003604d, 0xd1fd8346, 0xf6381fb0, 0x7745ae04, 0xd736fccc, 0x83426b33, 0xf01eab71, 0xb0804187, 0x3c005e5f, 0x77a057be, 0xbde8ae24, 0x55464299, 0xbf582e61, 0x4e58f48f, 0xf2ddfda2, 0xf474ef38, 0x8789bdc2, 0x5366f9c3, 0xc8b38e74, 0xb475f255, 0x46fcd9b9, 0x7aeb2661, 0x8b1ddf84, 0x846a0e79, 0x915f95e2, 0x466e598e, 0x20b45770, 0x8cd55591, 0xc902de4c, 0xb90bace1, 0xbb8205d0, 0x11a86248, 0x7574a99e, 0xb77f19b6, 0xe0a9dc09, 0x662d09a1, 0xc4324633, 0xe85a1f02, 0x09f0be8c, 0x4a99a025, 0x1d6efe10, 0x1ab93d1d, 0x0ba5a4df, 0xa186f20f, 0x2868f169, 0xdcb7da83, 0x573906fe, 0xa1e2ce9b, 0x4fcd7f52, 0x50115e01, 0xa70683fa, 0xa002b5c4, 0x0de6d027, 0x9af88c27, 0x773f8641, 0xc3604c06, 0x61a806b5, 0xf0177a28, 0xc0f586e0, 0x006058aa, 0x30dc7d62, 0x11e69ed7, 0x2338ea63, 0x53c2dd94, 0xc2c21634, 0xbbcbee56, 0x90bcb6de, 0xebfc7da1, 0xce591d76, 0x6f05e409, 0x4b7c0188, 0x39720a3d, 0x7c927c24, 0x86e3725f, 0x724d9db9, 0x1ac15bb4, 0xd39eb8fc, 0xed545578, 0x08fca5b5, 0xd83d7cd3, 0x4dad0fc4, 0x1e50ef5e, 0xb161e6f8, 0xa28514d9, 0x6c51133c, 0x6fd5c7e7, 0x56e14ec4, 0x362abfce, 0xddc6c837, 0xd79a3234, 0x92638212, 0x670efa8e, 0x406000e0]);
this.sBox3 = new Uint32Array([0x3a39ce37, 0xd3faf5cf, 0xabc27737, 0x5ac52d1b, 0x5cb0679e, 0x4fa33742, 0xd3822740, 0x99bc9bbe, 0xd5118e9d, 0xbf0f7315, 0xd62d1c7e, 0xc700c47b, 0xb78c1b6b, 0x21a19045, 0xb26eb1be, 0x6a366eb4, 0x5748ab2f, 0xbc946e79, 0xc6a376d2, 0x6549c2c8, 0x530ff8ee, 0x468dde7d, 0xd5730a1d, 0x4cd04dc6, 0x2939bbdb, 0xa9ba4650, 0xac9526e8, 0xbe5ee304, 0xa1fad5f0, 0x6a2d519a, 0x63ef8ce2, 0x9a86ee22, 0xc089c2b8, 0x43242ef6, 0xa51e03aa, 0x9cf2d0a4, 0x83c061ba, 0x9be96a4d, 0x8fe51550, 0xba645bd6, 0x2826a2f9, 0xa73a3ae1, 0x4ba99586, 0xef5562e9, 0xc72fefd3, 0xf752f7da, 0x3f046f69, 0x77fa0a59, 0x80e4a915, 0x87b08601, 0x9b09e6ad, 0x3b3ee593, 0xe990fd5a, 0x9e34d797, 0x2cf0b7d9, 0x022b8b51, 0x96d5ac3a, 0x017da67d, 0xd1cf3ed6, 0x7c7d2d28, 0x1f9f25cf, 0xadf2b89b, 0x5ad6b472, 0x5a88f54c, 0xe029ac71, 0xe019a5e6, 0x47b0acfd, 0xed93fa9b, 0xe8d3c48d, 0x283b57cc, 0xf8d56629, 0x79132e28, 0x785f0191, 0xed756055, 0xf7960e44, 0xe3d35e8c, 0x15056dd4, 0x88f46dba, 0x03a16125, 0x0564f0bd, 0xc3eb9e15, 0x3c9057a2, 0x97271aec, 0xa93a072a, 0x1b3f6d9b, 0x1e6321f5, 0xf59c66fb, 0x26dcf319, 0x7533d928, 0xb155fdf5, 0x03563482, 0x8aba3cbb, 0x28517711, 0xc20ad9f8, 0xabcc5167, 0xccad925f, 0x4de81751, 0x3830dc8e, 0x379d5862, 0x9320f991, 0xea7a90c2, 0xfb3e7bce, 0x5121ce64, 0x774fbe32, 0xa8b6e37e, 0xc3293d46, 0x48de5369, 0x6413e680, 0xa2ae0810, 0xdd6db224, 0x69852dfd, 0x09072166, 0xb39a460a, 0x6445c0dd, 0x586cdecf, 0x1c20c8ae, 0x5bbef7dd, 0x1b588d40, 0xccd2017f, 0x6bb4e3bb, 0xdda26a7e, 0x3a59ff45, 0x3e350a44, 0xbcb4cdd5, 0x72eacea8, 0xfa6484bb, 0x8d6612ae, 0xbf3c6f47, 0xd29be463, 0x542f5d9e, 0xaec2771b, 0xf64e6370, 0x740e0d8d, 0xe75b1357, 0xf8721671, 0xaf537d5d, 0x4040cb08, 0x4eb4e2cc, 0x34d2466a, 0x0115af84, 0xe1b00428, 0x95983a1d, 0x06b89fb4, 0xce6ea048, 0x6f3f3b82, 0x3520ab82, 0x011a1d4b, 0x277227f8, 0x611560b1, 0xe7933fdc, 0xbb3a792b, 0x344525bd, 0xa08839e1, 0x51ce794b, 0x2f32c9b7, 0xa01fbac9, 0xe01cc87e, 0xbcc7d1f6, 0xcf0111c3, 0xa1e8aac7, 0x1a908749, 0xd44fbd9a, 0xd0dadecb, 0xd50ada38, 0x0339c32a, 0xc6913667, 0x8df9317c, 0xe0b12b4f, 0xf79e59b7, 0x43f5bb3a, 0xf2d519ff, 0x27d9459c, 0xbf97222c, 0x15e6fc2a, 0x0f91fc71, 0x9b941525, 0xfae59361, 0xceb69ceb, 0xc2a86459, 0x12baa8d1, 0xb6c1075e, 0xe3056a0c, 0x10d25065, 0xcb03a442, 0xe0ec6e0e, 0x1698db3b, 0x4c98a0be, 0x3278e964, 0x9f1f9532, 0xe0d392df, 0xd3a0342b, 0x8971f21e, 0x1b0a7441, 0x4ba3348c, 0xc5be7120, 0xc37632d8, 0xdf359f8d, 0x9b992f2e, 0xe60b6f47, 0x0fe3f11d, 0xe54cda54, 0x1edad891, 0xce6279cf, 0xcd3e7e6f, 0x1618b166, 0xfd2c1d05, 0x848fd2c5, 0xf6fb2299, 0xf523f357, 0xa6327623, 0x93a83531, 0x56cccd02, 0xacf08162, 0x5a75ebb5, 0x6e163697, 0x88d273cc, 0xde966292, 0x81b949d0, 0x4c50901b, 0x71c65614, 0xe6c6c7bd, 0x327a140a, 0x45e1d006, 0xc3f27b9a, 0xc9aa53fd, 0x62a80f00, 0xbb25bfe2, 0x35bdd2f6, 0x71126905, 0xb2040222, 0xb6cbcf7c, 0xcd769c2b, 0x53113ec0, 0x1640e3d3, 0x38abbd60, 0x2547adf0, 0xba38209c, 0xf746ce76, 0x77afa1c5, 0x20756060, 0x85cbfe4e, 0x8ae88dd8, 0x7aaaf9b0, 0x4cf9aa7e, 0x1948c25c, 0x02fb8a8c, 0x01c36ae4, 0xd6ebe1f9, 0x90d4f869, 0xa65cdea0, 0x3f09252d, 0xc208e69f, 0xb74e6132, 0xce77e25b, 0x578fdfe3, 0x3ac372e6]);
this.pArray = new Uint32Array([0x243f6a88, 0x85a308d3, 0x13198a2e, 0x03707344, 0xa4093822, 0x299f31d0, 0x082efa98, 0xec4e6c89, 0x452821e6, 0x38d01377, 0xbe5466cf, 0x34e90c6c, 0xc0ac29b7, 0xc97c50dd, 0x3f84d5b5, 0xb5470917, 0x9216d5d9, 0x8979fb1b]);
this.generateSubkeys(key);
};
Blowfish.prototype = {
sBox0: null,
sBox1: null,
sBox2: null,
sBox3: null,
pArray: null,
key: null,
fixNegative: (number) => (number >>> 0),
num2block32: (num) => [num >>> 24, num << 8 >>> 24, num << 16 >>> 24, num << 24 >>> 24],
block32toNum: function(block32) {
return this.fixNegative(block32[0] << 24 | block32[1] << 16 | block32[2] << 8 | block32[3]);
},
xor: function(a, b) {
return this.fixNegative(a ^ b);
},
addMod32: function(a, b) {
return this.fixNegative((a + b) | 0);
},
split64by32: function(block64) {
return [this.block32toNum(block64.subarray(0, 4)), this.block32toNum(block64.subarray(4, 8))];
},
decryptCBC: function(data, iv) {
let blocks = Math.ceil(data.length / 8),
ivLivR = this.split64by32(iv),
ivL = ivLivR[0],
ivR = ivLivR[1],
ivLtmp,
ivRtmp;
for (let i = 0; i < blocks; i++) {
let block = data.subarray(i * 8, (i + 1) * 8);
if (block.length < 8) throw new Error('BF: ciphertext too short (must be multiple of 8 bytes)');
let xLxR = this.split64by32(block),
xL = xLxR[0],
xR = xLxR[1];
ivLtmp = xL;
ivRtmp = xR;
xLxR = this.decipher(xL, xR);
xL = xLxR[0];
xR = xLxR[1];
xL = this.xor(xL, ivL);
xR = this.xor(xR, ivR);
ivL = ivLtmp;
ivR = ivRtmp;
data.set(this.num2block32(xL), i * 8);
data.set(this.num2block32(xR), i * 8 + 4);
}
},
F: function(xL) {
let a = xL >>> 24,
b = xL << 8 >>> 24,
c = xL << 16 >>> 24,
d = xL << 24 >>> 24,
res = this.addMod32(this.sBox0[a], this.sBox1[b]);
res = this.xor(res, this.sBox2[c]);
res = this.addMod32(res, this.sBox3[d]);
return res;
},
encipher: function(xL, xR) {
let tmp;
for (let i = 0; i < 16; i++) {
xL = this.xor(xL, this.pArray[i]);
xR = this.xor(this.F(xL), xR);
tmp = xL;
xL = xR;
xR = tmp;
}
tmp = xL;
xL = xR;
xR = tmp;
xR = this.xor(xR, this.pArray[16]);
xL = this.xor(xL, this.pArray[17]);
return [xL, xR];
},
decipher: function(xL, xR) {
let tmp;
xL = this.xor(xL, this.pArray[17]);
xR = this.xor(xR, this.pArray[16]);
tmp = xL;
xL = xR;
xR = tmp;
for (let i = 15; i >= 0; i--) {
tmp = xL;
xL = xR;
xR = tmp;
xR = this.xor(this.F(xL), xR);
xL = this.xor(xL, this.pArray[i]);
}
return [xL, xR];
},
generateSubkeys: function(key) {
let data = 0,
k = 0,
i,
j;
for (i = 0; i < 18; i++) {
for (j = 4; j > 0; j--) {
data = this.fixNegative(data << 8 | key[k]);
k = (k + 1) % key.length;
}
this.pArray[i] = this.xor(this.pArray[i], data);
data = 0;
}
let block64 = [0, 0];
for (i = 0; i < 18; i += 2) {
block64 = this.encipher(block64[0], block64[1]);
this.pArray[i] = block64[0];
this.pArray[i + 1] = block64[1];
}
for (i = 0; i < 256; i += 2) {
block64 = this.encipher(block64[0], block64[1]);
this.sBox0[i] = block64[0];
this.sBox0[i + 1] = block64[1];
}
for (i = 0; i < 256; i += 2) {
block64 = this.encipher(block64[0], block64[1]);
this.sBox1[i] = block64[0];
this.sBox1[i + 1] = block64[1];
}
for (i = 0; i < 256; i += 2) {
block64 = this.encipher(block64[0], block64[1]);
this.sBox2[i] = block64[0];
this.sBox2[i + 1] = block64[1];
}
for (i = 0; i < 256; i += 2) {
block64 = this.encipher(block64[0], block64[1]);
this.sBox3[i] = block64[0];
this.sBox3[i + 1] = block64[1];
}
}
}; // End Blowfish.prototype
/* Below — message handling, downloading and other routines */
// Browser ID3 Writer v4.0.0
// Author: egoroof
// License: MIT
// https://github.com/egoroof/browser-id3-writer
// Minified version from https://egoroof.ru/browser-id3-writer/js/browser-id3-writer.4.0.0.js
// JS FLACMetadataEditor v0.0.2.1
// Author: AHOHNMYC
// License: GPL-3.0-or-later
// https://greasyfork.org/scripts/40545
/* jshint ignore: start */
const ID3Writer = (() => {
"use strict";
function e(e, t) {
if (!(e instanceof t)) throw new TypeError("Cannot call a class as a function")
}
function t(e) {
return String(e).split("").map(function(e) {
return e.charCodeAt(0)
})
}
function r(e) {
return new Uint8Array(t(e))
}
function n(e) {
let r = new Uint8Array(2 * e.length);
return new Uint16Array(r.buffer).set(t(e)), r
}
function a(e) {
return 73 === e[0] && 68 === e[1] && 51 === e[2]
}
function i(e) {
if (!e || !e.length) return null;
if (255 === e[0] && 216 === e[1] && 255 === e[2]) return "image/jpeg";
if (137 === e[0] && 80 === e[1] && 78 === e[2] && 71 === e[3]) return "image/png";
if (71 === e[0] && 73 === e[1] && 70 === e[2]) return "image/gif";
if (87 === e[8] && 69 === e[9] && 66 === e[10] && 80 === e[11]) return "image/webp";
let t = 73 === e[0] && 73 === e[1] && 42 === e[2] && 0 === e[3],
r = 77 === e[0] && 77 === e[1] && 0 === e[2] && 42 === e[3];
return t || r ? "image/tiff" : 66 === e[0] && 77 === e[1] ? "image/bmp" : 0 === e[0] && 0 === e[1] && 1 === e[2] && 0 === e[3] ? "image/x-icon" : null
}
function s(e) {
return [e >>> 24 & 255, e >>> 16 & 255, e >>> 8 & 255, 255 & e]
}
function o(e) {
return [e >>> 21 & 127, e >>> 14 & 127, e >>> 7 & 127, 127 & e]
}
function c(e) {
return (e[0] << 21) + (e[1] << 14) + (e[2] << 7) + e[3]
}
function u(e) {
return 11 + e
}
function f(e) {
return 13 + 2 * e
}
function h(e, t) {
return 16 + 2 * e + 2 + 2 + 2 * t
}
function p(e, t, r, n) {
return 11 + t + 1 + 1 + (n ? 2 + 2 * (r + 1) : r + 1) + e
}
function l(e, t) {
return 16 + 2 * e + 2 + 2 + 2 * t
}
function g(e, t) {
return 13 + 2 * e + 2 + 2 + 2 * t
}
function m(e) {
return 10 + e
}
return function() {
function t(r) {
if (e(this, t), !(r && "object" == typeof r && "byteLength" in r)) throw new Error("First argument should be an instance of ArrayBuffer or Buffer");
this.arrayBuffer = r, this.padding = 4096, this.frames = [], this.url = ""
}
return t.prototype._setIntegerFrame = function(e, t) {
let r = parseInt(t, 10);
this.frames.push({
name: e,
value: r,
size: u(r.toString().length)
})
}, t.prototype._setStringFrame = function(e, t) {
let r = t.toString();
this.frames.push({
name: e,
value: r,
size: f(r.length)
})
}, t.prototype._setPictureFrame = function(e, t, r, n) {
let a = i(new Uint8Array(t)),
s = r.toString();
if (!a) throw new Error("Unknown picture MIME type");
r || (n = !1), this.frames.push({
name: "APIC",
value: t,
pictureType: e,
mimeType: a,
useUnicodeEncoding: n,
description: s,
size: p(t.byteLength, a.length, s.length, n)
})
}, t.prototype._setLyricsFrame = function(e, t) {
let r = e.toString(),
n = t.toString();
this.frames.push({
name: "USLT",
value: n,
description: r,
size: h(r.length, n.length)
})
}, t.prototype._setCommentFrame = function(e, t) {
let r = e.toString(),
n = t.toString();
this.frames.push({
name: "COMM",
value: n,
description: r,
size: l(r.length, n.length)
})
}, t.prototype._setUserStringFrame = function(e, t) {
let r = e.toString(),
n = t.toString();
this.frames.push({
name: "TXXX",
description: r,
value: n,
size: g(r.length, n.length)
})
}, t.prototype._setUrlLinkFrame = function(e, t) {
let r = t.toString();
this.frames.push({
name: e,
value: r,
size: m(r.length)
})
}, t.prototype.setFrame = function(e, t) {
switch (e) {
case "TPE1":
case "TCOM":
case "TCON":
if (!Array.isArray(t)) throw new Error(e + " frame value should be an array of strings");
let r = "TCON" === e ? ";" : "/",
n = t.join(r);
this._setStringFrame(e, n);
break;
case "TIT2":
case "TALB":
case "TPE2":
case "TPE3":
case "TPE4":
case "TRCK":
case "TPOS":
case "TMED":
case "TPUB":
this._setStringFrame(e, t);
break;
case "TBPM":
case "TLEN":
case "TYER":
this._setIntegerFrame(e, t);
break;
case "USLT":
if (!("object" == typeof t && "description" in t && "lyrics" in t)) throw new Error("USLT frame value should be an object with keys description and lyrics");
this._setLyricsFrame(t.description, t.lyrics);
break;
case "APIC":
if (!("object" == typeof t && "type" in t && "data" in t && "description" in t)) throw new Error("APIC frame value should be an object with keys type, data and description");
if (t.type < 0 || t.type > 20) throw new Error("Incorrect APIC frame picture type");
this._setPictureFrame(t.type, t.data, t.description, !!t.useUnicodeEncoding);
break;
case "TXXX":
if (!("object" == typeof t && "description" in t && "value" in t)) throw new Error("TXXX frame value should be an object with keys description and value");
this._setUserStringFrame(t.description, t.value);
break;
case "TKEY":
if (!/^([A-G][#b]?m?|o)$/.test(t)) throw new Error(e + " frame value should be like Dbm, C#, B or o");
this._setStringFrame(e, t);
break;
case "WCOM":
case "WCOP":
case "WOAF":
case "WOAR":
case "WOAS":
case "WORS":
case "WPAY":
case "WPUB":
this._setUrlLinkFrame(e, t);
break;
case "COMM":
if (!("object" == typeof t && "description" in t && "text" in t)) throw new Error("COMM frame value should be an object with keys description and text");
this._setCommentFrame(t.description, t.text);
break;
default:
throw new Error("Unsupported frame " + e)
}
return this
}, t.prototype.removeTag = function() {
if (!(this.arrayBuffer.byteLength < 10)) {
let e = new Uint8Array(this.arrayBuffer),
t = e[3],
r = c([e[6], e[7], e[8], e[9]]) + 10;
!a(e) || t < 2 || t > 4 || (this.arrayBuffer = new Uint8Array(e.subarray(r)).buffer)
}
}, t.prototype.addTag = function() {
this.removeTag();
let e = [255, 254],
t = [101, 110, 103],
a = 10 + this.frames.reduce(function(e, t) {
return e + t.size
}, 0) + this.padding,
i = new ArrayBuffer(this.arrayBuffer.byteLength + a),
c = new Uint8Array(i),
u = 0,
f = [];
return f = [73, 68, 51, 3], c.set(f, u), u += f.length, u++, u++, f = o(a - 10), c.set(f, u), u += f.length, this.frames.forEach(function(a) {
switch (f = r(a.name), c.set(f, u), u += f.length, f = s(a.size - 10), c.set(f, u), u += f.length, u += 2, a.name) {
case "WCOM":
case "WCOP":
case "WOAF":
case "WOAR":
case "WOAS":
case "WORS":
case "WPAY":
case "WPUB":
f = r(a.value), c.set(f, u), u += f.length;
break;
case "TPE1":
case "TCOM":
case "TCON":
case "TIT2":
case "TALB":
case "TPE2":
case "TPE3":
case "TPE4":
case "TRCK":
case "TPOS":
case "TKEY":
case "TMED":
case "TPUB":
f = [1].concat(e), c.set(f, u), u += f.length, f = n(a.value), c.set(f, u), u += f.length;
break;
case "TXXX":
case "USLT":
case "COMM":
f = [1], "USLT" !== a.name && "COMM" !== a.name || (f = f.concat(t)), f = f.concat(e), c.set(f, u), u += f.length, f = n(a.description), c.set(f, u), u += f.length, f = [0, 0].concat(e), c.set(f, u), u += f.length, f = n(a.value), c.set(f, u), u += f.length;
break;
case "TBPM":
case "TLEN":
case "TYER":
u++, f = r(a.value), c.set(f, u), u += f.length;
break;
case "APIC":
f = [a.useUnicodeEncoding ? 1 : 0], c.set(f, u), u += f.length, f = r(a.mimeType), c.set(f, u), u += f.length, f = [0, a.pictureType], c.set(f, u), u += f.length, a.useUnicodeEncoding ? (f = [].concat(e), c.set(f, u), u += f.length, f = n(a.description), c.set(f, u), u += f.length, u += 2) : (f = r(a.description), c.set(f, u), u += f.length, u++), c.set(new Uint8Array(a.value), u), u += a.value.byteLength
}
}), u += this.padding, c.set(new Uint8Array(this.arrayBuffer), u), this.arrayBuffer = i, i
}, t.prototype.getBlob = function() {
return new Blob([this.arrayBuffer], {
type: "audio/mpeg"
})
}, t.prototype.getURL = function() {
return this.url || (this.url = URL.createObjectURL(this.getBlob())), this.url
}, t.prototype.revokeURL = function() {
URL.revokeObjectURL(this.url)
}, t
}()
})();
const FLACMetadataEditor = (() => {
"use strict";
const t = "0.0.2.1";
class VorbisComment extends Array {}
class VorbisCommentPacket {
_addComment(t) {
const e = t.split("=")[1];
return t = t.split("=")[0].toUpperCase(), this.hasOwnProperty(t) || (this[t] = new VorbisComment), this[t].some(t => t === e) || this[t].push(e.toString()), this
}
toStringArray() {
const t = [];
return Object.keys(this).sort().forEach(e => {
this[e].forEach(s => {
t.push(e + "=" + s)
})
}), t
}
}
class FLACMetadataBlockData {}
class FLACMetadataBlock {
constructor() {
this.blockType = "", this.blockTypeNubmer = 0, this.blockSize = 0, this.data = new FLACMetadataBlockData, this.offset = 0
}
get serializedSize() {
switch (this.blockType) {
case "STREAMINFO":
return 34;
case "PADDING":
return this.blockSize;
case "APPLICATION":
return 4 + this.data.applicationData.length;
case "SEEKTABLE":
return 18 * this.data.points.length;
case "VORBIS_COMMENT":
const t = this.data.comments.toStringArray().reduce((t, e) => t + 4 + e.toUTF8().length, 0);
return 4 + this.data.vendorString.length + 4 + t;
case "CUESHEET":
return 0;
case "PICTURE":
return 8 + this.data.MIMEType.toUTF8().length + 4 + this.data.description.toUTF8().length + 4 + 4 + 4 + 4 + 4 + this.data.data.length
}
}
}
class FLACMetadataBlocks extends Array {}
class FLACMetadata {
constructor() {
this.blocks = new FLACMetadataBlocks, this.framesOffset = 0, this.signature = ""
}
}
return class {
get scriptVersion() {
return t
}
constructor(t) {
if (!(t && "object" == typeof t && "byteLength" in t)) throw new Error("First argument should be an instance of ArrayBuffer or Buffer");
return this.arrayBuffer = t, this.metadata = new FLACMetadata, String.prototype.toUTF8 = function(t = null) {
return (new TextEncoder).encode(t || this)
}, this._parseMetadata(), this
}
_getBytesAsNumber(t, e = 0, s = t.length - e) {
return Array.from(t.subarray(e, e + s)).reduce((t, e) => t = 256 * t + e, 0)
}
_getBytesAsNumberLittleEndian(t, e = 0, s = t.length - e) {
return Array.from(t.subarray(e, e + s)).reduceRight((t, e) => t = 256 * t + e, 0)
}
_getBytesAsHexString(t, e = 0, s = t.length - e) {
return Array.from(t.subarray(e, e + s)).map(t => (t >> 4).toString(16) + (15 & t).toString(16)).join("")
}
_getBytesAsUTF8String(t, e = 0, s = t.length - e) {
return (new TextDecoder).decode(t.subarray(e, e + s))
}
_getBlockType(t) {
switch (t) {
case 0:
return "STREAMINFO";
case 1:
return "PADDING";
case 2:
return "APPLICATION";
case 3:
return "SEEKTABLE";
case 4:
return "VORBIS_COMMENT";
case 5:
return "CUESHEET";
case 6:
return "PICTURE";
case 127:
return "invalid, to avoid confusion with a frame sync code";
default:
return "reserved"
}
}
_uint32ToUint8Array(t) {
return [t >>> 24 & 255, t >>> 16 & 255, t >>> 8 & 255, 255 & t]
}
_uint24ToUint8Array(t) {
return [t >>> 16 & 255, t >>> 8 & 255, 255 & t]
}
_uint16ToUint8Array(t) {
return [t >>> 8 & 255, 255 & t]
}
_hexStringToUint8Array(t) {
return t.replace(/(\w\w)/g, "$1,").slice(0, -1).split(",").map(t => (parseInt(t[0], 16) << 4) + parseInt(t[1], 16))
}
get _vorbisComment() {
const t = this.metadata.blocks.find(t => "VORBIS_COMMENT" === t.blockType);
if (t) return t.data
}
addComment(t, e = null) {
if (t) {
if (!e) {
const s = t.split("=");
if (!s[1]) return this;
e = s[1], t = s[0]
}
t = t.toUpperCase(), this._vorbisComment.comments.hasOwnProperty(t) || (this._vorbisComment.comments[t] = new VorbisComment), this._vorbisComment.comments[t].find(t => t === e) || this._vorbisComment.comments[t].push(e.toString())
}
return this
}
removeComment(t = null, e = null) {
return t ? (t = t.toUpperCase(), e ? (e = e.toString(), this.hasOwnProperty(t) && (this._vorbisComment.comments[t] = this._vorbisComment.comments[t].filter(t => t !== e))) : delete this._vorbisComment.comments[t]) : Object.keys(this._vorbisComment.comments).forEach(t => delete this._vorbisComment.comments[t]), this
}
getComment(t) {
return this._vorbisComment.comments[t.toUpperCase()]
}
addPicture(t) {
if (!(t.data && t.data && "object" == typeof t.data && "byteLength" in t.data)) throw new Error('Field "data" should be an instance of ArrayBuffer or Buffer');
t.data = new Uint8Array(t.data);
const e = {
APICtype: 3,
MIMEType: "image/jpeg",
colorDepth: 0,
colorNumber: 0,
data: new Uint8Array([]),
description: "",
width: 0,
height: 0
},
s = new FLACMetadataBlock;
s.blockTypeNubmer = 6, s.blockType = "PICTURE";
for (let r in e) t[r] ? s.data[r] = t[r] : s.data[r] = e[r];
const r = this.metadata.blocks;
let a = r.length;
return "PADDING" === r[r.length - 1].blockType && a--, r.splice(a, 0, s), this.metadata.blocks = r, this
}
_serializeMetadataBlock(t) {
const e = new Uint8Array(t.serializedSize),
s = t.data;
let r = 0;
switch (t.blockType) {
case "STREAMINFO":
e.set(this._uint16ToUint8Array(s.minBlockSize)), r += 2, e.set(this._uint16ToUint8Array(s.maxBlockSize), r), r += 2, e.set(this._uint24ToUint8Array(s.minFrameSize), r), r += 3, e.set(this._uint24ToUint8Array(s.maxFrameSize), r), r += 3, e.set(this._uint24ToUint8Array((s.sampleRate << 4) + (s.numberOfChannels - 1 << 1) + (s.bitsPerSample - 1 >> 4)), r), e[r += 3] = ((s.bitsPerSample - 1 & 15) << 4) + (15 & Math.trunc(s.totalSamples / Math.pow(2, 32))), r += 1, e.set(this._uint32ToUint8Array(s.totalSamples), r), r += 4, e.set(this._hexStringToUint8Array(s.rawMD5), r);
break;
case "PADDING":
break;
case "APPLICATION":
e.set(s.applicationID.toUTF8()), r += 4, e.set(s.applicationData, r);
break;
case "SEEKTABLE":
s.points.forEach(t => {
e.set(this._hexStringToUint8Array(t.sampleNumber), r), e.set(this._hexStringToUint8Array(t.offset), r + 8), e.set(this._hexStringToUint8Array(t.numberOfSamples), r + 16), r += 18
});
break;
case "VORBIS_COMMENT":
e.set(this._uint32ToUint8Array(s.vendorString.toUTF8().length).reverse(), r), r += 4, e.set(s.vendorString.toUTF8(), r), r += s.vendorString.toUTF8().length;
const a = s.comments.toStringArray();
e.set(this._uint32ToUint8Array(a.length).reverse(), r), r += 4, a.forEach(t => {
e.set(this._uint32ToUint8Array(t.toUTF8().length).reverse(), r), r += 4, e.set(t.toUTF8(), r), r += t.toUTF8().length
});
break;
case "CUESHEET":
break;
case "PICTURE":
e.set(this._uint32ToUint8Array(s.APICtype)), r += 4, e.set(this._uint32ToUint8Array(s.MIMEType.toUTF8().length), r), r += 4, e.set(s.MIMEType.toUTF8(), r), r += s.MIMEType.toUTF8().length, e.set(this._uint32ToUint8Array(s.description.toUTF8().length), r), r += 4, e.set(s.description.toUTF8(), r), r += s.description.toUTF8().length, e.set(this._uint32ToUint8Array(s.width), r), r += 4, e.set(this._uint32ToUint8Array(s.height), r), r += 4, e.set(this._uint32ToUint8Array(s.colorDepth), r), r += 4, e.set(this._uint32ToUint8Array(s.colorNumber), r), r += 4, e.set(this._uint32ToUint8Array(s.data.length), r), r += 4, e.set(s.data, r)
}
return e
}
serializeMetadata() {
const t = 4 + this.metadata.blocks.reduce((t, e) => t + 4 + e.serializedSize, 0) + (this.arrayBuffer.byteLength > this.metadata.framesOffset ? this.arrayBuffer.byteLength - this.metadata.framesOffset : 0),
e = new Uint8Array(t);
e.set(this.metadata.signature.toUTF8());
let s = 4,
r = !1;
return this.metadata.blocks.forEach((t, a, i) => {
i.length - 1 === a && (r = !0), e[s] = t.blockTypeNubmer | r << 7, s += 1, e.set(this._uint24ToUint8Array(t.serializedSize), s), s += 3, e.set(this._serializeMetadataBlock(t), s), s += t.serializedSize
}), e.set(new Uint8Array(this.arrayBuffer).subarray(this.metadata.framesOffset), s), this.arrayBuffer = e.buffer, this
}
_parseMetadataBlock(t, e, s, r) {
const a = t.subarray(e, e + r);
let i = 0;
const n = new FLACMetadataBlockData;
switch (s) {
case "STREAMINFO":
n.minBlockSize = this._getBytesAsNumber(a, i, 2), i += 2, n.maxBlockSize = this._getBytesAsNumber(a, i, 2), i += 2, n.minFrameSize = this._getBytesAsNumber(a, i, 3), i += 3, n.maxFrameSize = this._getBytesAsNumber(a, i, 3), i += 3, n.sampleRate = this._getBytesAsNumber(a, i, 3) >> 4, i += 2, n.numberOfChannels = 1 + (a[i] >> 1 & 7), n.bitsPerSample = 1 + ((1 & a[i]) << 4) + (a[i + 1] >> 4), i += 1, n.totalSamples = (15 & a[i]) * Math.pow(2, 32) + this._getBytesAsNumber(a, i + 1, 4), i += 5, n.rawMD5 = this._getBytesAsHexString(a, i, 16).toUpperCase();
break;
case "PADDING":
break;
case "APPLICATION":
n.applicationID = this._getBytesAsUTF8String(a, i, 4), i += 4, n.applicationData = a.subarray(i);
break;
case "SEEKTABLE":
n.pointCount = r / 18, n.points = [];
for (let t = 0; t < n.pointCount; t++) n.points.push({
sampleNumber: this._getBytesAsHexString(a, i, 8),
offset: this._getBytesAsHexString(a, i + 8, 8),
numberOfSamples: this._getBytesAsHexString(a, i + 16, 2)
}), i += 18;
break;
case "VORBIS_COMMENT":
const t = this._getBytesAsNumberLittleEndian(a, i, 4);
i += 4, n.vendorString = this._getBytesAsUTF8String(a, i, t), i += t;
const e = this._getBytesAsNumberLittleEndian(a, i, 4);
i += 4, n.comments = new VorbisCommentPacket;
let o = 0;
for (let t = 0; t < e; t++) o = this._getBytesAsNumberLittleEndian(a, i, 4), i += 4, n.comments._addComment(this._getBytesAsUTF8String(a, i, o)), i += o;
break;
case "CUESHEET":
break;
case "PICTURE":
n.APICtype = this._getBytesAsNumber(a, i, 4), i += 4;
const h = this._getBytesAsNumber(a, i, 4);
i += 4, n.MIMEType = this._getBytesAsUTF8String(a, i, h), i += h;
const c = this._getBytesAsNumber(a, i, 4);
i += 4, n.description = this._getBytesAsUTF8String(a, i, c), i += c, n.width = this._getBytesAsNumber(a, i, 4), i += 4, n.height = this._getBytesAsNumber(a, i, 4), i += 4, n.colorDepth = this._getBytesAsNumber(a, i, 4), i += 4, n.colorNumber = this._getBytesAsNumber(a, i, 4), i += 4;
const m = this._getBytesAsNumber(a, i, 4);
i += 4, n.data = a.subarray(i, i + m)
}
return n
}
_parseMetadata() {
const t = new Uint8Array(this.arrayBuffer);
this.metadata.signature = this._getBytesAsUTF8String(t, 0, 4);
let e, s = 4,
r = !1,
a = 0;
for (; !r && s < t.length;) {
if (a++ > 42) throw new RangeError("Too much METADATA_BLOCKS. Looks like file corrupted");
(e = new FLACMetadataBlock).offset = s, r = !!(t[s] >> 7), e.blockTypeNubmer = 127 & t[s], e.blockType = this._getBlockType(e.blockTypeNubmer), s += 1, e.blockSize = this._getBytesAsNumber(t, s, 3), s += 3, e.data = this._parseMetadataBlock(t, s, e.blockType, e.blockSize), s += e.blockSize, this.metadata.blocks.push(e)
}
return this.metadata.framesOffset = s, this
}
}
})();
/* jshint ignore: end */
// Inner, in worker scope debug. Rewrites later
let DEBUG = true;
let coverSize = 1200;
let coverQuality = 80;
this.onmessage = ({
data: data
}) => {
if (data.hasOwnProperty('debug')) {
DEBUG = data.debug;
coverSize = data.coverSize;
coverQuality = data.coverQuality;
return true;
}
if (DEBUG) console.info('Worker got message:', data);
const url = data.url;
const key = data.key;
const trackData = data.trackData;
const tags = data.tags;
const azLyrics = data.azLyrics;
const albumData = data.albumData;
// config: url, responseType, onProgress, onSuccess, anyway
function niceXhr(config) {
const xhr = new XMLHttpRequest();
xhr.responseType = config.responseType;
xhr.onload = e => {
if (200 === e.target.status)
config.onSuccess(e.target.response);
else
console.warn('Error with getting data from', e.target.responseURL);
config.anyway();
};
xhr.onerror = config.anyway;
xhr.onabort = config.anyway;
xhr.onprogress = config.onProgress;
xhr.open('GET', config.url);
xhr.send();
}
postMessage([trackData, '^waiting']);
niceXhr({
url: url,
responseType: 'arraybuffer',
onProgress: e => {
const toMB = bytes => (bytes / 1024 / 1024).toFixed(2);
const loaded = toMB(e.loaded);
if (e.lengthComputable) {
const total = toMB(e.total);
const percent = Math.floor(100 * e.loaded / e.total);
postMessage([trackData, `^downloading ${loaded}/${total}^mb : ${percent}%`]);
} else {
postMessage([trackData, `^downloading ${loaded}^mb`]);
}
},
onSuccess: response => {
postMessage([trackData, '^decrypting']);
let data = new Uint8Array(response);
const bf = new Blowfish(key);
const iv = new Uint8Array([0, 1, 2, 3, 4, 5, 6, 7]);
const L = data.length;
if (DEBUG) console.info('Data length:', data.length);
for (let i = 0; i < L; i += 6144) {
if (i % (6144 * 20) === 6144 * 19) { // let it display state at every 120K
const percent = Math.floor(i * 100.0 / L);
postMessage([trackData, `^decrypting ${percent}%`]);
}
if (i + 2048 <= L) {
let D = data.subarray(i, i + 2048);
// if (i===0) console.log(D.toString(), D.length);
bf.decryptCBC(D, iv);
data.set(D, i);
}
}
function binArr2utfStr(arr) {
return Array.from(arr).map(n => String.fromCharCode(n)).join('');
}
// According https://en.wikipedia.org/wiki/List_of_file_signatures
const isMP3 = (data[0] === 0xFF && data[1] === 0xFB) || 'ID3' === binArr2utfStr(data.subarray(0, 3));
const isFLAC = 'fLaC' === binArr2utfStr(data.subarray(0, 4));
if (!(isMP3 || isFLAC)) return console.warn('Decrypted file not MP3 nor FLAC!', data);
let writer;
if (isMP3) {
writer = new ID3Writer(data);
writer.setFrame('TIT2', tags.title)
.setFrame('TPE1', tags._artistsArray)
.setFrame('TALB', tags.album);
// TODO: wait answer: https://github.com/egoroof/browser-id3-writer/issues/52
// .setFrame('TSRC', tags.isrc)
if (tags.track)
writer.setFrame('TRCK', tags.track);
if (albumData.label)
writer.setFrame('TPUB', albumData.label);
if (albumData.release_date)
writer.setFrame('TYER', albumData.release_date);
if (azLyrics.lyrics)
writer.setFrame('USLT', {
description: '',
lyrics: azLyrics.lyrics
});
// if (azLyrics.year)
// writer.setFrame('TYER', azLyrics.year);
}
if (isFLAC) {
writer = new FLACMetadataEditor(data);
writer.removeComment()
.addComment('ALBUM', tags.album)
.addComment('TITLE', tags.title)
.addComment('ISRC', tags.isrc);
tags._artistsArray.forEach(artist => writer.addComment('ARTIST', artist));
if (tags.track)
writer.addComment('TRACKNUMBER', tags.track);
if (albumData.label)
writer.addComment('ORGANIZATION', albumData.label);
if (albumData.release_date)
writer.addComment('DATE', albumData.release_date);
// At first i thought that GAIN is for ReplayGain, but my player returns other values,
// So now i don't know what is it ~
// if (tags.gain) writer.addComment('REPLAYGAIN_TRACK_GAIN', `${tags.replayGain} dB`);
if (azLyrics.lyrics)
writer.addComment('LYRICS', azLyrics.lyrics);
// if (azLyrics.year)
// writer.addComment('DATE', azLyrics.year);
}
// Downloading cover
const coverLink = `https://e-cdns-images.dzcdn.net/images/cover/${tags.cover}/${coverSize}x${coverSize}-000000-${coverQuality}-0-0.jpg`;
if (DEBUG) console.info('Trying to download', coverLink);
postMessage([trackData, '^coverDownloading']);
niceXhr({
url: coverLink,
responseType: 'arraybuffer',
onProgress: e => {
const toMB = bytes => (bytes / 1024 / 1024).toFixed(2);
const loaded = toMB(e.loaded);
if (e.lengthComputable) {
const total = toMB(e.total);
const percent = Math.floor(100 * e.loaded / e.total);
postMessage([trackData, `^downloading ${loaded}/${total}^mb : ${percent}%`]);
} else {
postMessage([trackData, `^downloading ${loaded}^mb`]);
}
},
onSuccess: imageData => {
if (isMP3) {
writer.setFrame('APIC', {
type: 3,
data: imageData,
description: ''
});
}
if (isFLAC) {
writer.addPicture({
data: imageData,
colorDepth: 24,
width: coverSize,
height: coverSize
});
}
},
anyway: () => {
if (isMP3)
writer.addTag();
if (isFLAC)
writer.serializeMetadata();
data = writer.arrayBuffer;
const blobUrl = URL.createObjectURL(new Blob([data], {
type: (isMP3 ? 'audio/mpeg' : isFLAC ? 'audio/flac' : 'audio/mpeg')
}));
if (DEBUG) console.info('Blob for "%s - %s" created: %s', tags.artist, tags.title, blobUrl);
postMessage([trackData, 'DONE', blobUrl]);
}
});
},
anyway: () => {}
});
};
}); //End var mainWk
// Set debug and other values in worker like in script
mainWk.postMessage({
debug: DEBUG,
coverSize: coverSize,
coverQuality: coverQuality,
});
// azLyrics parser
function azLyrics(artist, title, onsuccess, anyway) {
const url = formatAzLyricsUrl(artist, title);
if (!url) return anyway();
const data = {
method: 'GET',
url: url,
onload: e => {
if (200 === e.status)
onsuccess(parseAzLyricsPage(e.response));
else
console.warn('Cannot grab lyrics from', e.finalUrl);
anyway();
},
onerror: anyway,
onabort: anyway
};
// Greasemonkey with their 4.0 API became a monster ~
if (typeof GM !== 'undefined' && typeof GM.xmlHttpRequest !== 'undefined')
GM.xmlHttpRequest(data);
else
GM_xmlhttpRequest(data);
}
function parseAzLyricsPage(text) {
const azPage = new DOMParser().parseFromString(text, 'text/html');
const data = {};
const divs = Array.from(azPage.getElementsByTagName('div'));
const lyricsDiv = divs.find(div => div.innerHTML.substr(1, 4) === '<!--');
const lyrics = lyricsDiv.textContent;
data.lyrics = lyrics.replace(/^\s*/, '').replace(/\s*$/, ''); // clean spaces and new lines in begin and end
const album = azPage.querySelector('.album-panel a[data-toggle]');
if (album) {
const matches = album.textContent.match(/"([^"]+)" \((\d+)\)/);
data.album = matches[1];
data.year = +matches[2];
}
return data;
}
function formatAzLyricsUrl(artist, title) {
const partOfUrl = [artist, title].map(el => el.toLowerCase().replace(/[^\w]/g, '')).join('/').replace(/^the/, '');
if (partOfUrl === '/') return false;
return `https://www.azlyrics.com/lyrics/${partOfUrl}.html`;
}
// DOWNLOADER LOGIC: URL ENCRYPTION
function zeroPad(data) {
const aesBS = 16;
const l = data.length;
if (l % aesBS !== 0)
data += '\0'.repeat(aesBS - (l % aesBS));
return data;
}
function encryptURL(track, fmt) {
const urlsep = '\xa4';
const key = aesjs.utils.utf8.toBytes('jo6aey6haid2Teih');
const cryptor = new aesjs.ModeOfOperation.ecb(key);
let steps = [track.MD5_ORIGIN, fmt, track.SNG_ID, track.MEDIA_VERSION].join(urlsep);
steps = [hex_md5(steps), steps, ''].join(urlsep);
steps = zeroPad(steps);
// This not works because it translates `urlsep' as two symbols
// steps = aesjs.utils.utf8.toBytes(steps);
// That's why we use this:
steps = steps.split('').map(c => c.charCodeAt(0)); // str2bin
steps = cryptor.encrypt(steps);
steps = aesjs.utils.hex.fromBytes(steps);
return steps;
}
// DOWNLOADER LOGIC: GLOBAL VARIABLES AND HELPER METHODS
const fmtMp3_128 = 1,
fmtMp3_320 = 3,
fmtFLAC = 9;
const trackDB = {};
const urlDB = {};
if (!localStorage.dlNameProto)
unsafeWindow.localStorage.dlNameProto = '%Artists - %Title';
if (!localStorage.dlAzLyrics)
unsafeWindow.localStorage.dlAzLyrics = true;
function generateName(track, format = null) {
const tags = dzTags(track);
const nameProto = elNameProto.value;
let name = nameProto.replace(/%(\w+)/g, (full, keyword) => {
let replacement = '';
if (/^n+$/i.test(keyword)) {
if (!tags.track) return '';
// Replace `NNNNN' to track number padded with zeroes
let rept = keyword.length - (tags.track).toString().length;
if (rept < 0) rept = 0;
return '0'.repeat(rept) + tags.track;
} else if (tags.hasOwnProperty(keyword.toLowerCase())) {
// Finded in dzTags values
replacement = tags[keyword.toLowerCase()];
if (!replacement) return '';
if (keyword === keyword.toLowerCase())
replacement = replacement.toLowerCase();
if (keyword === keyword.toUpperCase())
replacement = replacement.toUpperCase();
return replacement;
} else {
return full;
}
});
// remove escaping slashes
name = name.replace(/\\%/g, '%');
// remove trailing and starting spaces
name = name.replace(/\s*(.*[^\s])\s*/, '$1');
if (format) {
if (format === fmtFLAC)
name += '.flac';
else
name += '.mp3';
}
return name;
}
function updateNames() {
const trackNameEls = document.querySelectorAll('.dlTrackName');
trackNameEls.forEach(nameEl => {
const nbsp = '\u00A0';
const trackId = nameEl.closest('.dlTrack').dataset.trackId;
nameEl.textContent = generateName(trackDB[trackId]).replace(/\s/g, nbsp);
});
}
// CONVERT TIME into min:sec
function formatTime(seconds) {
const minutes = Math.floor(seconds / 60);
seconds %= 60;
const zeroPad = seconds < 10 ? '0' : '';
return `${minutes}:${zeroPad}${seconds}`;
}
// FILES TAGS
function dzTags(trackInfo) {
// `new Set' is needed to unique atrists. e.g. https://deezer.com/album/6883271
const artistsArray = trackInfo.ARTISTS && GM_info.scriptHandler !== 'Greasemonkey' ? [...new Set(trackInfo.ARTISTS.map(a => a.ART_NAME))] : [trackInfo.ART_NAME];
return {
artist: trackInfo.ART_NAME,
_artistsArray: artistsArray,
artists: artistsArray.join(', '),
album: trackInfo.ALB_TITLE,
album_id: trackInfo.ALB_ID,
cover: trackInfo.ALB_PICTURE,
disk: trackInfo.DISK_NUMBER,
// by name, it's have to be ReplayGain value. But in fact, this is something other ~
// looks like https://en.wikipedia.org/wiki/Audio_normalization#Peak_normalization
// http://wiki.hydrogenaud.io/index.php?title=ReplayGain
gain: trackInfo.GAIN,
id: trackInfo.SNG_ID,
// ISO 3901:1986 International Standard Recording Code (ISRC). Technical committee / subcommittee: TC 46 / SC 9
isrc: trackInfo.ISRC,
time: formatTime(trackInfo.DURATION),
title: trackInfo.SNG_TITLE,
track: trackInfo.TRACK_NUMBER,
version: trackInfo.VERSION
};
}
const bfGK = 'g4el58wc0zvf9na1';
function bfGenKey(id) {
const hash = hex_md5(id.toString());
const key = new Uint8Array(16);
for (let i = 0; i < 16; i++)
key[i] = bfGK.charCodeAt(i) ^ hash.charCodeAt(i) ^ hash.charCodeAt(i + 16);
return key;
}
function fileSizeToMb(aSize) {
return isNaN(aSize) ? 0 : Math.round(aSize / 1024 / 1024 * 10) / 10;
}
// DOWNLOAD ENTRY POINT
function dzDownload(track, trackId, fmt) {
const tags = dzTags(track);
const trackData = {
trackId: trackId,
fmt: fmt
};
let albumData = {};
let azLyricsData = {};
function processTrack() {
mainWk.postMessage({
url: `https://e-cdns-proxy-${track.MD5_ORIGIN[0]}.dzcdn.net/mobile/1/${encryptURL(track, fmt)}`,
key: bfGenKey(track.SNG_ID),
trackData: trackData,
tags: dzTags(track),
azLyrics: azLyricsData,
albumData: albumData
});
}
function getAlbumData() {
messageProcessor({
data: [trackData, translate.gettingAlbumInfo]
});
niceXhr({
url: `https://api.deezer.com/album/${tags.album_id}`,
responseType: 'json',
onProgress: {},
onSuccess: json => {
albumData.label = json.label;
albumData.release_date = json.release_date;
},
anyway: processTrack
});
}
if (elAzLyrics.checked) {
messageProcessor({
data: [trackData, translate.lyricsDownloading]
});
azLyrics(tags.artist, tags.title,
data => {
if (DEBUG) console.info(data);
azLyricsData = data;
},
getAlbumData
);
} else {
getAlbumData();
}
}
// DOWNLOADER WORKER CALLBACK
function messageProcessor(msg) {
const fmt = +msg.data[0].fmt;
const trackId = msg.data[0].trackId;
let trackEl = document.querySelector(`.dlTrack[data-track-id="${trackId}"]`);
if (!trackEl)
console.warn('Downloaded track not found in tracklist!');
const state = msg.data[1];
switch (state) {
case 'DONE':
if (!urlDB[trackId]) urlDB[trackId] = {};
urlDB[trackId][fmt] = msg.data[2];
let anchor;
if (trackEl) {
anchor = trackEl.querySelector(`.dlLink[data-fmt="${fmt}"]`);
} else {
// create new temporary link element if container not found
anchor = document.head.appendChild(document.createElement('a'));
}
anchor.download = generateName(trackDB[trackId], fmt);
anchor.href = msg.data[2];
anchor.click();
// remove temporary link
if (!trackEl) anchor.remove();
break;
case 'ABORT':
console.error('Download abort:', trackId, fmt);
break;
case 'ERROR':
console.error('Download ERROR!', trackId, fmt);
break;
default:
const message = translateCircumstring(state);
if (trackEl) {
trackEl.querySelector('.dlTrackLinks').classList.add('hidden');
trackEl.querySelector('.dlTrackStatus').classList.remove('hidden');
trackEl.querySelector('.dlTrackStatus').textContent = message;
} else {
const tags = dzTags(trackDB[trackId]);
console.info('%s - %s: %s', tags.artist, tags.title, message);
}
return;
}
if (trackEl) {
trackEl.querySelector('.dlTrackLinks').classList.remove('hidden');
trackEl.querySelector('.dlTrackStatus').classList.add('hidden');
}
}
mainWk.onmessage = messageProcessor;
// DOWNLOADER LOGIC: HTML GENERATOR
function getFilesize(track, fmt_name) {
const sizeProp = `FILESIZE_${fmt_name}`;
let size = 0;
if (track[sizeProp] && !isNaN(track[sizeProp]))
size = +track[sizeProp];
return size;
}
function generateDlLink(track, fmt_name, fmt_id, size) {
const trackId = track.SNG_ID;
const el = document.createElement('a');
el.classList.add('dlLink');
el.dataset.fmt = fmt_id;
el.textContent = `⬇ ${fmt_name}`;
//el.title = `${fileSizeToMb(size).toLocaleString()} ${translate.mb}`;
if (urlDB[trackId] && urlDB[trackId][fmt_id]) {
el.download = generateName(trackDB[trackId], fmt_id);
el.href = urlDB[trackId][fmt_id];
}
el.onclick = function() {
if (this.href)
return true;
const trackId = this.closest('.dlTrack').dataset.trackId;
const fmt = this.dataset.fmt;
dzDownload(trackDB[trackId], trackId, fmt);
return false;
};
return el;
}
function generateDlTrack(track) {
const tags = dzTags(track);
const infosTooltips =
'^artist:\t' + tags.artist +
'\n^title:\t' + tags.title +
'\n^album:\t' + tags.album + // disk number sometimes not recognized when change track playing ????
'\n^duration:\t' + tags.time;
const trackEl = document.createElement('div');
trackEl.classList.add('dlTrack');
trackEl.dataset.trackId = tags.id;
const nameEl = document.createElement('span');
nameEl.classList.add('dlTrackName');
nameEl.title = translateCircumstring(infosTooltips);
const linksEl = document.createElement('div');
linksEl.classList.add('dlTrackLinks');
if (showMp3_128)
if (getFilesize(track, 'MP3_128') > 0) linksEl.appendChild(generateDlLink(track, 'mp3 128', fmtMp3_128, getFilesize(track, 'MP3_128')));
if (showMp3_320)
if (getFilesize(track, 'MP3_320') === 0) linksEl.appendChild(generateDlLink(track, 'mp3 320', fmtMp3_320, getFilesize(track, 'MP3_320')));
if (showFLAC)
if (getFilesize(track, 'FLAC') === 0) linksEl.appendChild(generateDlLink(track, 'flac', fmtFLAC, getFilesize(track, 'FLAC')));
const statusEl = document.createElement('div');
statusEl.classList.add('dlTrackStatus', 'hidden');
const endFloat = document.createElement('div');
endFloat.classList.add('dlEndFloat');
trackEl.appendChild(nameEl);
trackEl.appendChild(linksEl);
trackEl.appendChild(statusEl);
trackEl.appendChild(endFloat);
return trackEl;
}
function getTrackListByHand() {
const links = Array.from(document.querySelectorAll('[data-instance=track_name]'));
const albums = links.map(el => el.href.match(/\d+/)[0]);
const names = links.map(el => el.textContent);
if (DEBUG) console.info('trackDB:: length:', Object.keys(trackDB).length, 'db:', trackDB);
const result = [];
for (let i = 0; i < links.length; i++) {
Object.keys(trackDB).forEach(trackId => {
if (trackDB[trackId].ALB_ID === albums[i])
if (trackDB[trackId].SNG_TITLE === names[i] || `${trackDB[trackId].SNG_TITLE} ${trackDB[trackId].VERSION}` === names[i])
result.push(trackDB[trackId]);
});
}
if (DEBUG) console.info('filtered', result);
return result;
}
function getTrackListIndexByHand() {
const links = Array.from(document.querySelectorAll('[data-instance=track_name]'));
const names = links.map(el => el.textContent);
let currentIndex = -1;
const currentEl = document.querySelector('.player-track-link');
if (currentEl)
currentIndex = names.indexOf(currentEl.textContent);
return currentIndex;
}
// ENTRY POINTS
function refreshTracklist() {
if (DEBUG) console.info('Refreshing Tracklist');
while (divTracklist.firstElementChild)
divTracklist.firstElementChild.remove();
let list, current;
if (unsafeWindow.dzPlayer) {
if (DEBUG) console.info('Used native `dzPlayer\'');
// In fact, this one is already array, but Firefox+Greasemonkey don't believe it
list = Array.from(unsafeWindow.dzPlayer.getTrackList());
current = unsafeWindow.dzPlayer.getTrackListIndex();
} else {
if (DEBUG) console.info('Tracks are getting manually. Be careful with result');
// Do some magic
list = getTrackListByHand();
current = getTrackListIndexByHand();
}
list.forEach((track, index) => {
trackDB[track.SNG_ID] = track;
const trackDiv = generateDlTrack(track);
if (index === current)
trackDiv.classList.add('current');
divTracklist.appendChild(trackDiv);
});
updateNames();
}
// THE DOWNLOADER PANEL
const rootEl = document.createElement('div');
rootEl.classList.add('dzHidden');
rootEl.id = 'dzDownloader';
const elNameProto = document.createElement('input');
elNameProto.classList.add('dlNameProto');
elNameProto.title = `${translate.choose} ${translate.fileNaming}`;
elNameProto.value = localStorage.dlNameProto;
['change', 'keyup', 'keydown'].forEach(event => {
elNameProto.addEventListener(event, () => {
unsafeWindow.localStorage.dlNameProto = elNameProto.value;
updateNames();
});
});
const forceAzLyrics = false;
const elInsert = document.createElement('span');
elInsert.classList.add('dlInsert');
elInsert.addEventListener('mouseenter', () => elNameProto.focus());
const elInsertButton = document.createElement('button');
elInsertButton.classList.add('dlInsertButton');
elInsertButton.innerHTML = '<img src="https://i.imgur.com/6IkahnK.png" />';
elInsertButton.title = `Case of tags
Upper-case, proper-case and lower-case can be used.
Example:
%ARTIST\t= THE BEATLES
%Artist\t= The Beatles
%artist\t= the beatles
%TITLE\t= LET IT BE
%Title\t= Let It Be
%title\t= let it be
%Artists - %Title = The Beatles - Let It Be
----------------------------------------------------------
Padding
Padding refers to adding zeros before the first digit.
Example:
%N\t= 1
%NN\t= 01
%NNN\t= 001
%NNNN\t= 0001`;
const elInsertTable = document.createElement('table');
elInsertTable.classList.add('dlInsertTable');
function addInsertVariant(value) {
const row = elInsertTable.insertRow();
const title = row.insertCell();
title.textContent = value;
if ('---' === value) return;
row.addEventListener('click', () => {
const sel = {
start: elNameProto.selectionStart,
end: elNameProto.selectionEnd,
direction: elNameProto.selectionDirection,
};
const src = elNameProto.value;
elNameProto.value = src.substr(0, elNameProto.selectionStart) + '%' + value + src.substr(elNameProto.selectionEnd);
if (sel.start === src.length) {
elNameProto.selectionStart = elNameProto.selectionEnd = elNameProto.value.length;
} else {
elNameProto.selectionStart = sel.start;
elNameProto.selectionEnd = sel.end;
}
elNameProto.selectionDirection = sel.direction;
elNameProto.dispatchEvent(new Event('change'));
elNameProto.focus();
});
}
// strings
addInsertVariant('Artist');
addInsertVariant('Artists');
addInsertVariant('Album');
addInsertVariant('Title');
addInsertVariant('Version');
addInsertVariant('__________');
// numbers
addInsertVariant('disk');
addInsertVariant('time');
addInsertVariant('isrc');
addInsertVariant('NN');
addInsertVariant('__________');
// Deezer-specific
addInsertVariant('cover');
addInsertVariant('gain');
addInsertVariant('id');
addInsertVariant('album_id');
elInsert.appendChild(elInsertButton);
elInsert.appendChild(elInsertTable);
const elRefresh = document.createElement('a');
elRefresh.classList.add('dlRefresh');
elRefresh.textContent = `${translate.refresh} ${translate.tracklist.toLowerCase()}`;
elRefresh.addEventListener('click', refreshTracklist);
const elTrackListHeader = document.createElement('p');
elTrackListHeader.classList.add('dlTitle');
elTrackListHeader.textContent = `♪ ${translate.tracklist}`;
if (forceAzLyrics)
unsafeWindow.localStorage.dlAzLyrics = forceAzValue;
const elAzLyrics = document.createElement('input');
elAzLyrics.type = 'checkbox';
elAzLyrics.classList.add('dlDoAzLyrics');
elAzLyrics.id = 'dlDoAzLyrics';
const elAzLyricsLabel = document.createElement('label');
elAzLyricsLabel.setAttribute('for', 'dlDoAzLyrics');
elAzLyrics.classList.add('dlDoAzLyricsLabel');
elAzLyricsLabel.textContent = ` azLyrics`;
const elListDownloadSelect = document.createElement('select');
elListDownloadSelect.classList.add('dlListDownloadSelect');
elListDownloadSelect.innerHTML =
(!showMp3_128 ? '' : `<option value="${fmtMp3_128}">mp3 128</option>`) +
(!showMp3_320 ? '' : `<option value="${fmtMp3_320}">mp3 320</option>`) +
(!showFLAC ? '' : `<option value="${fmtFLAC}" >flac</option>`);
elListDownloadSelect.value = localStorage.dlListDownload;
elListDownloadSelect.addEventListener('change', () => {
unsafeWindow.localStorage.dlListDownload = elListDownloadSelect.value;
});
const elListDownloadButton = document.createElement('button');
elListDownloadButton.classList.add('dlListDownloadButton');
elListDownloadButton.textContent = translate.downloadList;
elListDownloadButton.addEventListener('click', () => {
const selector = `.dlLink[data-fmt="${elListDownloadSelect.value}"]`;
document.querySelectorAll(selector).forEach(anchor => anchor.click());
});
if (showAzLyrics) {
elTrackListHeader.appendChild(elAzLyrics);
elTrackListHeader.appendChild(elAzLyricsLabel);
}
if (showListDownloader) {
elTrackListHeader.appendChild(elListDownloadSelect);
elTrackListHeader.appendChild(elListDownloadButton);
}
const divTracklist = document.createElement('div');
divTracklist.classList.add('dlTrackList');
rootEl.appendChild(elNameProto);
rootEl.appendChild(elInsert);
rootEl.appendChild(elRefresh);
if (showAzLyrics || showListDownloader)
rootEl.appendChild(elTrackListHeader);
rootEl.appendChild(divTracklist);
document.body.appendChild(rootEl);
const triggerEl = document.createElement('div');
triggerEl.classList.add('dlTrigger');
triggerEl.title = translate.clickToOpen;
triggerEl.textContent = 'D➲wnloader ►';
triggerEl.addEventListener('click', () => {
if (rootEl.classList.contains('dzHidden')) {
triggerEl.textContent = 'D➲wnloader ◄';
} else {
triggerEl.textContent = 'D➲wnloader ►';
}
rootEl.classList.toggle('dzHidden');
});
document.body.appendChild(triggerEl);
function hidePannel() {
if (!rootEl.classList.contains('dzHidden'))
triggerEl.click();
}
// Hotkey handling
addEventListener('keydown', e => {
switch (e.code) {
case 'Escape':
hidePannel();
break;
case 'KeyD':
if (document.activeElement.nodeName !== 'INPUT')
triggerEl.click();
break;
default:
if (SHOW_KEYS) console.info(`Key with code "${e.code}" was pressed.`);
}
});
// Hide trigger when Deezer interface loading
// This element will be removed later
const delayStyleElement = document.head.appendChild(document.createElement('style'));
function onInterfaceLoaded() {
delayStyleElement.remove();
document.getElementById('menu_search').addEventListener('focus', hidePannel);
document.getElementById('page_panels').addEventListener('click', hidePannel);
document.getElementById('page_sidebar').addEventListener('click', hidePannel);
if (document.querySelector('.hotkeys')) {
document.head.appendChild(document.createElement('style')).textContent =
'.hotkeys {height: 225px !important}';
document.querySelector('.hotkeys .mapping').innerHTML +=
'<div class="hotkey">' +
' <div class="hotkey-icon single">D</div>' +
` <div class="hotkey-label">${translate.downloaderHotkey}</div>` +
'</div>';
}
// There we show and immediately hide playlist. It's for initializing this element, nothing more
if (!unsafeWindow.dzPlayer) {
const pannels = document.getElementById('page_panels');
const playListEl = document.querySelector('button.control-qlist');
pannels.style.dislplay = 'none';
playListEl.click();
playListEl.click();
setTimeout(() => {
pannels.removeAttribute('style');
}, 500);
}
// grab info about tracks from `PLAYER_INIT' variable
trackRecurseLookup(unsafeWindow.PLAYER_INIT);
refreshTracklist();
if (openOnStart)
triggerEl.click();
}
const waitInterfaceLoading = () => {
if (!document.querySelector('button.control-qlist')) return setTimeout(waitInterfaceLoading, 150);
onInterfaceLoaded();
};
waitInterfaceLoading();
// DATA STYLESHEET
document.head.appendChild(document.createElement('style')).textContent = `
#dzDownloader {
transform: translateX(220px);
position: fixed;
top: 0;
left: 0;
z-index: 299;
background: #e0e0e0;
padding: 5px;
height: 91%;
overflow-y: auto;
width: 35%;
font-size: 1.05em;
}
#dzDownloader.dzHidden {
transform: translateX(calc(0px - 100%));
}
.dlNameProto {
text-align: center;
width: 30em;
}
.dlInsertButton {
cursor: help;
height: 1.86em;
padding-left: 0.8em;
color: #000;
}
.dlInsert:not(:hover) .dlInsertTable {
display: none;
}
.dlInsertTable {
position: absolute;
left: calc(220px + 13em);
background-color: #c8c8c8;
border: #000 solid 1px;
}
.dlInsertTable tr:hover {
background-color: #ddd;
}
.dlInsertTable td {
cursor: pointer;
}
.dlRefresh:hover {
background: #444;
}
.dlRefresh:active {
background: #00c853;
color: #000;
}
.dlRefresh {
border: #000 solid 1px;
color: #fff;
font-size: 12px;
padding: 2px 11px;
background: #777;
margin-left: 72px;
}
.dlTitle {
cursor: default;
border-bottom: gray 1.5px solid;
color: #000;
margin-bottom: 1%;
padding-bottom: 5px;
padding-top: 5px;
}
.dlDoAzLyrics {
margin-left: 4em;
}
.dlListDownloadSelect {
text-align: center;
margin-left: 225px;
}
.dlListDownloadButton:hover {
background: #444;
}
.dlListDownloadButton:active {
background: #00c853;
color: #000;
}
.dlListDownloadButton {
border: #000 solid 1px;
color: #fff;
font-size: 12px;
padding: 2px 11px;
background: #777;
margin-left: -213px;
}
.dlTrack:nth-child(even) {
background: #fff;
}
.dlTrack.current {
background: #00c853;
color: #fff;
}
.dlTrackName {
color: #000;
display: inline-block;
max-width: 30em;
margin-left: .5em;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
padding-top: 0.1em;
cursor: help;
}
.dlTrackLinks {
float: right;
margin-top: .2em;
margin-right: .3em;
}
.dlLink {
margin-left: 4px;
}
.dlLink[download] {
text-decoration: line-through;
}
.dlLink:hover {
text-decoration: underline;
}
.dlTrackStatus {
float: right;
margin-top: .2em;
margin-right: .3em;
overflow: hidden;
}
.dlEndFloat {
clear: both;
}
.dlTrigger {
cursor: pointer;
position: fixed;
left: 0;
top: 0;
z-index: 501;
color: #23232c;
background: #e0e0e0;
padding: 3px;
width: 220px;
text-align: right;
font-size: 1.3em;
font-weight: bold;
}
/* below - deezer's native elements */
#page_sidebar {
padding-top: 20px;
}
.logo-deezer {
height: 55px
}
.index-header-logo {
height: 65px;
max-width: 100%;
width: 227px;
}
.logo-deezer-hp.logo.index-header-logo, .logo-deezer.logo {
background-position: center;
background-repeat: no-repeat;
background-size: contain;
}
.page-sidebar .brand {
margin-bottom: 0;
}
#menu_navigation {
top: 135px;
}
.page-sidebar .nav-link {
padding: 7px;
}
.conversion-banner {
display: none;
}
.ads-bottom {
display: none;
}
.ads-top {
display: none;
}
.has-ads-bottom .ads-bottom-alone {
display: none;
}
.page-player .player-track .track-seekbar .slider-counter {
color: #23232c;
}
.page-player .player-bottom {
border-top-style: solid !important;
border-style: solid !important;
border-color: #e0e0e0 !important;
border-top-width: 3px !important;
border-top-color: 777 !important;
}
.slider-track-input {
background: rgba(25,25,34,.05);
}
.abp-banner {
display: none;
}
`;
@saintgrave
Copy link

Flac download is no longer working.

@GoodSoulGermany
Copy link

FLAC download is broke, everything else still working fine. Please update and thanks a lot for this awesome scrip! 👍

@oreidoflac
Copy link

Bro, fix the flac and mp3 download, today mp3 download as stopped

@oreidoflac
Copy link

i cannot download drake CLB in mp3

@ner00
Copy link

ner00 commented Sep 18, 2021

Seems that they're tracking abuse, I was able to pull 2 FLAC tracks before getting blocked. Now it just downloads standard 128Kbps. Console returns 403 forbidden on URL and promise error:

GET https://e-cdns-proxy-c.dzcdn.net/mobile/1/3080e75e3c035827a59aabb9729fe4be818e4c0744b6e6076f325c91ddd05114fd92c3e5bab151470276ebcb6a87a407d0d05ffee2771a9e42209192bff319376f08b783bb67a51a1c4e211e38dc419a063ef54d67c219821901e1ef8d31ed56 [HTTP/1.1 403 Forbidden 0ms]

Error: Promised response from onMessage listener went out of scope

I'm not sure if this is even fixable, it may well be an abuse detection that locks the account from further HQ dls.
Apparently Deezer changed their API, not sure if the script can be fixed.

So actually, it wasn't just a minor patch by Deezer, but a big one. Deezer has officially patched old URL generator and added new server-side one (requires premium/hifi account).

@John09f9
Copy link

So actually, it wasn't just a minor patch by Deezer, but a big one. Deezer has officially patched old URL generator and added new server-side one (requires premium/hifi account).

I have premium. It would be nice if this script was updated to work with it.

@ner00
Copy link

ner00 commented Sep 18, 2021

@John09f9 Give deemix a try, here's a cool UI version of that core: https://deemix.app/gui/

@oreidoflac
Copy link

Seems that they're tracking abuse, I was able to pull 2 FLAC tracks before getting blocked. Now it just downloads standard 128Kbps. Console returns 403 forbidden on URL and promise error:

GET https://e-cdns-proxy-c.dzcdn.net/mobile/1/3080e75e3c035827a59aabb9729fe4be818e4c0744b6e6076f325c91ddd05114fd92c3e5bab151470276ebcb6a87a407d0d05ffee2771a9e42209192bff319376f08b783bb67a51a1c4e211e38dc419a063ef54d67c219821901e1ef8d31ed56 [HTTP/1.1 403 Forbidden 0ms]

Error: Promised response from onMessage listener went out of scope

I'm not sure if this is even fixable, it may well be an abuse detection that locks the account from further HQ dls.
Apparently Deezer changed their API, not sure if the script can be fixed.

So actually, it wasn't just a minor patch by Deezer, but a big one. Deezer has officially patched old URL generator and added new server-side one (requires premium/hifi account).

Can, https://free-mp3-download.net/ flac download works normally.

@ner00
Copy link

ner00 commented Sep 19, 2021

Can, https://free-mp3-download.net/ flac download works normally.

Yes, It uses premium accounts in the back-end. Consider disabling ad-blockers on that site to help with costs.

@oreidoflac
Copy link

Can, https://free-mp3-download.net/ flac download works normally.

Yes, It uses premium accounts in the back-end. Consider disabling ad-blockers on that site to help with costs.

I disable it. I think the site is incredible, I just think it's a shame that cover art comes with a low quality, here on deezrevived I can put maximum size and quality, there in the free-mp3-download.net it comes in a very low quality...

If you own the free-mp3-download.net is reading this PLEASE PUTS EVERYTHING IN MAXIMUM QUALITY.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment