· 6 years ago · Oct 20, 2019, 06:48 PM
1/* jslint bitwise: true, expr: true, esversion: 6 *//* JSHINT PARAMS (details: http://jshint.com/docs/option) */
2/* globals unsafeWindow, GM_xmlhttpRequest, GM_info, GM, postMessage, aesjs, hex_md5, ID3Writer, FLACMetadataEditor */
3const openOnStart = !true;
4const DEBUG = !true;
5const SHOW_KEYS = !true; // Show pressed keys
6const L10nDEBUG = !true; // Enable localization override?
7const L10nOVERRIDE = 'de'; // Override your browser language with selected (handy for translation testing)
8
9const showMp3_32 = !true;
10const showMp3_64 = !true;
11const showMp3_128 = true;
12const showMp3_320 = true;
13const showFLAC = true;
14
15const showAzLyrics = true; // Show element asking you for parsing azLyrics
16const forceAzLyrics = !true; // If `true' — override parsing settings with \
17const forceAzValue = true; // <- this value
18
19const showListDownloader = true;
20
21const coverSize = 1200;
22const coverQuality = 80;
23// ==UserScript==
24// @name Deezer:Download
25// @description Download music from Deezer in any chosen format. Supports HQ even without Premium subscription. Also recommend 'Deezer Premium Enabler' to have a better experience.
26// @version 1.1.7.3
27// @author Deki Haker, Kawashi666 & some others.
28// @namespace com.deezer.downloader
29// @icon https://e-cdns-files.dzcdn.net/images/common/favicon/favicon-96x96-v00400107.png
30// @screenshot https://framapic.org/pBnowQP75uGT/6CnUCfbvFx7W.png
31// @license Beerware License; https://paste.debian.net/hidden/plainh/f360edb6/
32// @supportURL https://greasyfork.org/scripts/35724/feedback
33// @grant unsafeWindow
34// @grant GM_xmlhttpRequest
35// @grant GM.xmlHttpRequest
36// @connect azlyrics.com
37// @connect api.deezer.com
38//
39// @require https://greasyfork.org/scripts/32982-pajhome-md5/code/PajHome%20MD5.js
40// license BSD
41// attribution Paul Johnston ( http://pajhome.org.uk/crypt/md5/ )
42//
43// @require https://cdnjs.cloudflare.com/ajax/libs/aes-js/3.1.1/index.min.js
44// license MIT
45// attribution ricmoo ( https://github.com/ricmoo/aes-js )
46//
47// @match https://www.deezer.com/*
48//
49// @name:de Deezer:D➲wnloader [Revived] ... Dafür kann Musik die Limits überschreiten!
50// @description:de Laden Sie einfach das abgespielte Lied oder jedes andere Lied in der Liste in FLAC oder MP3 herunter. Unterstützt HQ auch ohne Premium-Abonnement♛
51//
52// @name:en Deezer:D➲wnloader [Revived] ... For that music can exceed the limits!
53// @description:en Easily download the song being played, or any other song in the list, in FLAC or MP3. Supports HQ even without Premium subscription♛
54//
55// @name:es Deezer:D➲wnloader [Revived] ... ¡Porque esa música puede exceder los límites!
56// @description:es Descargue fácilmente la canción que se está reproduciendo o cualquier otra canción en la lista, en FLAC o MP3. Admite HQ incluso sin suscripción Premium♛
57//
58// @name:fr Deezer:D➲wnloader [Revived] ... Pour que la musique puisse dépasser les limites!
59// @description:fr Téléchargez facilement la chanson en cours de lecture, ou n'importe quelle autre de la liste, en FLAC ou MP3. Supporte le HQ même sans abonnement Premium♛
60//
61// @name:it Deezer:D➲wnloader [Revived] ... Per quella musica puoi superare i limiti!
62// @description:it Scarica facilmente il brano in riproduzione o qualsiasi altro brano nell'elenco, in formato FLAC o MP3. Supporta HQ anche senza abbonamento Premium♛
63//
64// @name:pt Deezer:D➲wnloader [Revived] ... Para que a música possa ultrapassar os limites!
65// @description:pt Baixe facilmente a música que está sendo tocada, ou qualquer outra música da lista, em FLAC ou MP3. Suporta HQ mesmo sem ter assinatura Premium♛
66// @description:pt-BR Baixe facilmente a música que está sendo tocada, ou qualquer outra música da lista, em FLAC ou MP3. Suporta HQ mesmo sem ter assinatura Premium♛
67//
68// @name:ru Deezer:D➲wnloader [Revived] ... чтоб музыка лилась без остановки!
69// @description:ru Легко загружайте воспроизводимую песню или любую другую песню в списке, в FLAC или MP3. Поддержка HQ даже без подписки на Premium♛
70//
71// @noframes
72// @compatible chrome,+ chromium + vivaldi TamperMonkey || ViolentMonkey
73// @compatible opera, TamperMonkey || ViolentMonkey
74// @compatible firefox,+ seamonkey TamperMonkey || ViolentMonkey || GreaseMonkey + GreaseMonkey Port
75// @compatible edge, TamperMonkey
76//
77// @nocompat qupzilla, GreaseMonkey
78// @nocompat safari, (test on v5 only) TamperMonkey
79// ==/UserScript==
80
81/* DEVELOPERS:
82 * Jonathan Tavares [ Revisions, fixes, proofreading, compatibility ]
83 * K-mik@Z [ Fixes, proofreading (code), style, translation, compatibility and 2 or 3 tricks ]
84 * AHOHNMYC [ Refactoring (code), fixes, translation ]
85 *
86 * EMBEDDED (because of worker has own scope) SCRIPTS:
87 * javascript-blowfish by agorlov
88 * License: MIT
89 * https://github.com/agorlov/javascript-blowfish
90 * Browser ID3 Writer v4.0.0 by egoroof
91 * License: MIT
92 * https://github.com/egoroof/browser-id3-writer
93 * JS FLACMetadataEditor v0.0.2.1 by AHOHNMYC
94 * License: GPL-3.0-or-later
95 * https://greasyfork.org/scripts/40545
96 *
97 * ALL PERFORMED: See log < https://paste.debian.net/hidden/plainh/c846afa5/ >
98 *
99 * TOOLS: JSHint < http://jshint.com >,
100 * UglifyJS 3 < https://skalman.github.io/UglifyJS-online/ > => can minify without supress `;` by changing in options: output > `beautify` value to false.
101 *
102 * NOTES: `unsafeWindow' needed only for Firefox+GreaseMonkey
103 * uses in executing `dzPlayer' methods, writing to `localStorage' ang getting `USER.USER_ID' value
104 * `fetch()' also owerrides because it's easiest way to get track info
105 *
106 * GM_xmlhttpRequest used in azLyrics page getting
107 *
108 * code refactored to ES6 standards: consts, lets,
109 * byte arrays, ``-strings with substitution and so on
110 *
111 * MORE ABOUT METADATA: https://violentmonkey.github.io/api/metadata-block.html
112 * https://tampermonkey.net/documentation.php
113 *
114 *
115 */
116
117// < GLOBALS VARS > ( /!\ WARNING ) --------------------------------------------------------------------------------------------------
118//[ °°° SECTION LANGUAGES MAP °°°
119// new line with `\n` and escaping special character with `\` (except for downloaderHotkey and fixmeGM).
120const _getI18nMap = { // Default lng is 'en'
121 en: {
122 downloading: 'Downloading',
123 decrypting: 'Decrypting',
124 waiting: 'Waiting',
125 choose: 'Choose',
126 fileNaming: 'file naming',
127 title: 'Title',
128 artist: 'Artist',
129 refresh: 'Refresh the',
130 album: 'Album',
131 mb: 'МB',
132 duration: 'Duration',
133 coverDownloading: 'Downloading the cover',
134 downloaderHotkey: 'Open "Deezer:D\➲wnloader" panel',
135 tracklist: 'Track list',
136 clickToOpen: 'Click to open\nor D key (keyboard shortcut)',
137 downloadList: 'Download the list',
138 lyricsDownloading: 'Downloading the lyrics',
139 pleaseLogIn: 'Please, log in to use this script',
140 gettingAlbumInfo: 'Getting album info'
141 }, de: {
142 downloading: 'Herunterladen',
143 decrypting: 'Entschlüsselung',
144 waiting: 'Warten',
145 choose: 'Wählen',
146 fileNaming: 'Sie die Dateibenennung',
147 title: 'Titel',
148 artist: 'Künstler',
149 refresh: 'Aktualisiere die',
150 album: 'Album',
151 duration: 'Dauer',
152 coverDownloading: 'Frontabdeckung Herunterladen',
153 mb: 'МB',
154 downloaderHotkey: 'Öffnen Sie das Panel "Deezer:D\➲wnloader"',
155 tracklist: 'Liste der Tracks',
156 clickToOpen: 'Klicken Sie auf Öffnen\noder D-Taste (Tastaturkürzel)',
157 downloadList: 'Laden Sie die Liste herunter',
158 lyricsDownloading: 'Herunterladen der Liedtexte',
159 pleaseLogIn: 'Melden Sie sich an, um dieses Skript zu verwenden',
160 gettingAlbumInfo: 'Informationen zum Album erhalten'
161 }, es: {
162 downloading: 'Descargan',
163 decrypting: 'Descifrado',
164 waiting: 'Esperando la descarga',
165 choose: 'Elege',
166 fileNaming: 'el nombre del archivo',
167 title: 'Título',
168 artist: 'Artista',
169 refresh: 'actualizar la',
170 album: 'Álbum',
171 duration: 'Duración',
172 coverDownloading: 'Descargando la portada',
173 mb: 'МB',
174 downloaderHotkey: 'Abra el panel "Deezer:D\➲wnloader"',
175 tracklist: 'Lista de pistas',
176 clickToOpen: 'Haga clic para abrir\no la tecla D (atajo de teclado)',
177 downloadList: 'Descargue la lista',
178 lyricsDownloading: 'Descargando la letra',
179 pleaseLogIn: 'Por favor, inicie sesión para usar este script',
180 gettingAlbumInfo: 'Obtener información del álbum'
181 }, fr: {
182 downloading: 'Téléchargement',
183 decrypting: 'Décryptage',
184 waiting: 'En attente',
185 choose: 'choisir',
186 fileNaming: 'le nom de fichier souhaité',
187 title: 'Titre',
188 artist: 'Artiste',
189 refresh: 'Rafraîchir la',
190 album: 'Album',
191 duration: 'Durée',
192 coverDownloading: 'Téléchargement de la couverture',
193 mb: 'MB',
194 downloaderHotkey: 'Ouvrir "Deezer:D\➲wnloader"',
195 tracklist: 'Liste des pistes',
196 clickToOpen: 'Cliquer pour ouvrir\nou touche D (raccourcis clavier)',
197 downloadList: 'Télécharger la liste',
198 lyricsDownloading: 'Télécharger les paroles',
199 pleaseLogIn: 'Veuillez vous connecter pour utiliser ce script',
200 gettingAlbumInfo: 'Obtenir des informations sur l\'album'
201 }, it: {
202 downloading: 'Scaricamento',
203 decrypting: 'Decifrare',
204 waiting: 'In attesa',
205 choose: 'Scegli',
206 fileNaming: 'la denominazione del file',
207 title: 'Titolo',
208 artist: 'Artista',
209 refresh: 'Aggiorna la',
210 album: 'Album',
211 duration: 'Durata',
212 coverDownloading: 'Scaricamento della copertina',
213 mb: 'МB',
214 downloaderHotkey: 'Aprire il pannello "Deezer:D\➲wnloader"',
215 tracklist: 'Lista delle tracce',
216 clickToOpen: 'Fai clic per aprire\no il tasto D (scorciatoia da tastiera)',
217 downloadList: 'Scarica la lista',
218 lyricsDownloading: 'Download dei testi',
219 pleaseLogIn: 'Per favore, accedi per usare questo script',
220 gettingAlbumInfo: 'Ottenere informazioni sugli album'
221 }, pt: {
222 downloading: 'Baixar',
223 decrypting: 'Descriptografar',
224 waiting: 'Carregando',
225 choose: 'Escolha',
226 fileNaming: 'o nome do arquivo',
227 title: 'Título',
228 artist: 'Artista',
229 refresh: 'Atualize a',
230 album: 'Álbum',
231 duration: 'Duração',
232 coverDownloading: 'Baixando capa',
233 mb: 'МB',
234 downloaderHotkey: 'Abra o painel "Deezer\:D\➲wnloader"',
235 tracklist: 'Lista de faixas',
236 clickToOpen: 'Clique para abrir\nou tecla D (atalho do teclado)',
237 downloadList: 'Baixe a lista',
238 lyricsDownloading: 'Download das letras',
239 pleaseLogIn: 'Por favor, faça o login para usar este script',
240 gettingAlbumInfo: 'Obtendo informações sobre o álbum'
241 }, 'pt-BR': {
242 downloading: 'Baixar',
243 decrypting: 'Descriptografar',
244 waiting: 'Carregando',
245 choose: 'Escolha',
246 fileNaming: 'o nome do arquivo',
247 title: 'Título',
248 artist: 'Artista',
249 refresh: 'Atualize a',
250 album: 'Álbum',
251 duration: 'Duração',
252 coverDownloading: 'Baixando capa',
253 mb: 'МB',
254 downloaderHotkey: 'Abra o painel "Deezer:D\➲wnloader"',
255 tracklist: 'Lista de faixas',
256 clickToOpen: 'Clique para abrir\nou tecla D (atalho do teclado)',
257 downloadList: 'Baixe a lista',
258 lyricsDownloading: 'Download das letras',
259 pleaseLogIn: 'Por favor, faça o login para usar este script',
260 gettingAlbumInfo: 'Obtendo informações sobre o álbum'
261 }, ru: {
262 downloading: 'Загрузка',
263 decrypting: 'Расшифровка',
264 waiting: 'Ожидание загрузки',
265 choose: 'Выбор',
266 fileNaming: 'имени файла',
267 title: 'Название',
268 artist: 'Исполнитель',
269 refresh: 'Обновить',
270 album: 'Альбом',
271 duration: 'Длительность',
272 coverDownloading: 'Загрузка обложки',
273 mb: 'МБ',
274 downloaderHotkey: 'Открыть панель "Deezer:D\➲wnloader"',
275 tracklist: 'Список треков',
276 clickToOpen: 'Клик здесь или нажатие клавиши D\nоткроют панель',
277 downloadList: 'Загрузить список',
278 lyricsDownloading: 'Загрузка текста',
279 pleaseLogIn: 'Пожалуйста, залогиньтесь для нормальной работы скрипта',
280 gettingAlbumInfo: 'Получение информации об альбоме'
281 }
282};
283
284//[ °°° SECTION GUI LANGUAGE °°° ]
285let L10n = L10nDEBUG ? L10nOVERRIDE : navigator.language;
286if (!_getI18nMap[L10n]) L10n = L10n.substring(0, 2);
287if (!_getI18nMap[L10n]) L10n = 'en';
288
289const translate = _getI18nMap[L10n]; // Now, use translate.whatYouWant ( better than translate[whatYouWant] ).
290
291/* Replaces `10 ^mb' to `10 MB' */
292function translateCircumstring(data) {
293 return data.replace(/\^(\w+)/g, (full, part) => translate[part] ? translate[part] : full);
294}
295
296// FETCH SUBSTITUTE
297if (GM_info.scriptHandler === 'Greasemonkey') {
298 if (!localStorage.gmFetchWarningShown) {
299 unsafeWindow.localStorage.gmFetchWarningShown = 'true';
300 alert('We cannot safely replace `fetch()\' in GreaseMonkey. If troubles starts, replace GreaseMonkey with ViolentMonkey or TamperMonkey. This message will not be shown anymore');
301 }
302} else {
303 const orig_fetch = unsafeWindow.fetch;
304 unsafeWindow.fetch = (input, init = {}) => {
305 if (DEBUG) console.info('fetch() to:', input);
306 if (input.startsWith('https://www.deezer.com/ajax/gw-light.php?method=log.listen'))
307 return;
308
309 if (input.startsWith('https://www.deezer.com/ajax/gw-light.php')) {
310 orig_fetch(input, init)
311 .then(response=>response.json())
312 .then(trackRecurseLookup);
313 }
314
315 return orig_fetch(input, init);
316 };
317}
318
319function trackRecurseLookup(key){
320 if (key !== null && typeof key === 'object') {
321 if (key.SNG_ID)
322 trackDB[key.SNG_ID] = key;
323
324 else if (key.forEach)
325 key.forEach(trackRecurseLookup);
326 else
327 Object.keys(key).forEach(subKey=>trackRecurseLookup(key[subKey]));
328 }
329}
330
331
332// config: url, responseType, onProgress, onSuccess, anyway
333function niceXhr(config) {
334 const data = {
335 responseType: config.responseType,
336 method: 'GET',
337 url: config.url,
338 onload: e=>{
339 if (200 === e.status)
340 config.onSuccess(e.response);
341 else
342 console.warn('Error with getting data from', e.finalUrl);
343 config.anyway();
344 },
345 onerror: config.anyway,
346 onabort: config.anyway,
347 onprogress: config.onProgress,
348 };
349
350 // Greasemonkey with their 4.0 API became a monster ~
351 if (typeof GM !== 'undefined' && typeof GM.xmlHttpRequest !== 'undefined')
352 GM.xmlHttpRequest(data);
353 else
354 GM_xmlhttpRequest(data);
355}
356
357
358
359// WORKER
360function createWorker(code) {
361 const blobURL = URL.createObjectURL(new Blob(
362 ['(', code.toString(), ')()'],
363 {type: 'text/javascript'}
364 ));
365 const worker = new Worker(blobURL);
366 URL.revokeObjectURL(blobURL);
367 return worker;
368}
369
370const mainWk = createWorker(function() {
371 // BLOWFISH LIBRARY, adapted from https://github.com/agorlov/javascript-blowfish (MIT-licensed)
372 // Modified to work with byte arrays, and also supports encryption / decryption in-place for buffers.
373 // Now it works ONLY with byte arrays and can ONLY decrypt data ONLY in CBC mode.
374 // Cannot be @require-d, as it is part of worker code. Workers share NO data so everything must be embedded.
375 const Blowfish = function(key) {
376 this.key = key;
377 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]);
378 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]);
379 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]);
380 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]);
381 this.pArray= new Uint32Array([0x243f6a88, 0x85a308d3, 0x13198a2e, 0x03707344, 0xa4093822, 0x299f31d0, 0x082efa98, 0xec4e6c89, 0x452821e6, 0x38d01377, 0xbe5466cf, 0x34e90c6c, 0xc0ac29b7, 0xc97c50dd, 0x3f84d5b5, 0xb5470917, 0x9216d5d9, 0x8979fb1b]);
382 this.generateSubkeys(key);
383 };
384 Blowfish.prototype = {
385 sBox0: null,
386 sBox1: null,
387 sBox2: null,
388 sBox3: null,
389 pArray: null,
390 key: null,
391 fixNegative: (number) => (number >>> 0),
392 num2block32: (num) => [num >>> 24, num << 8 >>> 24, num << 16 >>> 24, num << 24 >>> 24],
393 block32toNum: function(block32) {
394 return this.fixNegative(block32[0] << 24 | block32[1] << 16 | block32[2] << 8 | block32[3]);
395 },
396 xor: function(a, b) {
397 return this.fixNegative(a ^ b);
398 },
399 addMod32: function(a, b) {
400 return this.fixNegative((a + b) | 0);
401 },
402 split64by32: function(block64) {
403 return [this.block32toNum(block64.subarray(0, 4)), this.block32toNum(block64.subarray(4, 8))];
404 },
405 decryptCBC: function(data, iv) {
406 let blocks = Math.ceil(data.length / 8),
407 ivLivR = this.split64by32(iv),
408 ivL = ivLivR[0],
409 ivR = ivLivR[1],
410 ivLtmp,
411 ivRtmp;
412 for (let i = 0; i < blocks; i++) {
413 let block = data.subarray(i * 8, (i + 1) * 8);
414 if (block.length < 8) throw new Error('BF: ciphertext too short (must be multiple of 8 bytes)');
415 let xLxR = this.split64by32(block),
416 xL = xLxR[0],
417 xR = xLxR[1];
418 ivLtmp = xL;
419 ivRtmp = xR;
420 xLxR = this.decipher(xL, xR);
421 xL = xLxR[0];
422 xR = xLxR[1];
423 xL = this.xor(xL, ivL);
424 xR = this.xor(xR, ivR);
425 ivL = ivLtmp;
426 ivR = ivRtmp;
427 data.set(this.num2block32(xL), i * 8);
428 data.set(this.num2block32(xR), i * 8 + 4);
429 }
430 },
431 F: function(xL) {
432 let a = xL >>> 24,
433 b = xL << 8 >>> 24,
434 c = xL << 16 >>> 24,
435 d = xL << 24 >>> 24,
436 res = this.addMod32(this.sBox0[a], this.sBox1[b]);
437 res = this.xor(res, this.sBox2[c]);
438 res = this.addMod32(res, this.sBox3[d]);
439 return res;
440 },
441 encipher: function(xL, xR) {
442 let tmp;
443 for (let i = 0; i < 16; i++) {
444 xL = this.xor(xL, this.pArray[i]);
445 xR = this.xor(this.F(xL), xR);
446 tmp = xL;
447 xL = xR;
448 xR = tmp;
449 }
450 tmp = xL;
451 xL = xR;
452 xR = tmp;
453 xR = this.xor(xR, this.pArray[16]);
454 xL = this.xor(xL, this.pArray[17]);
455 return [xL, xR];
456 },
457 decipher: function(xL, xR) {
458 let tmp;
459 xL = this.xor(xL, this.pArray[17]);
460 xR = this.xor(xR, this.pArray[16]);
461 tmp = xL;
462 xL = xR;
463 xR = tmp;
464 for (let i = 15; i >= 0; i--) {
465 tmp = xL;
466 xL = xR;
467 xR = tmp;
468 xR = this.xor(this.F(xL), xR);
469 xL = this.xor(xL, this.pArray[i]);
470 }
471 return [xL, xR];
472 },
473 generateSubkeys: function(key) {
474 let data = 0,
475 k = 0,
476 i,
477 j;
478 for (i = 0; i < 18; i++) {
479 for (j = 4; j > 0; j--) {
480 data = this.fixNegative(data << 8 | key[k]);
481 k = (k + 1) % key.length;
482 }
483 this.pArray[i] = this.xor(this.pArray[i], data);
484 data = 0;
485 }
486 let block64 = [0, 0];
487 for (i = 0; i < 18; i += 2) {
488 block64 = this.encipher(block64[0], block64[1]);
489 this.pArray[i] = block64[0];
490 this.pArray[i + 1] = block64[1];
491 }
492 for (i = 0; i < 256; i += 2) {
493 block64 = this.encipher(block64[0], block64[1]);
494 this.sBox0[i] = block64[0];
495 this.sBox0[i + 1] = block64[1];
496 }
497 for (i = 0; i < 256; i += 2) {
498 block64 = this.encipher(block64[0], block64[1]);
499 this.sBox1[i] = block64[0];
500 this.sBox1[i + 1] = block64[1];
501 }
502 for (i = 0; i < 256; i += 2) {
503 block64 = this.encipher(block64[0], block64[1]);
504 this.sBox2[i] = block64[0];
505 this.sBox2[i + 1] = block64[1];
506 }
507 for (i = 0; i < 256; i += 2) {
508 block64 = this.encipher(block64[0], block64[1]);
509 this.sBox3[i] = block64[0];
510 this.sBox3[i + 1] = block64[1];
511 }
512 }
513 };// End Blowfish.prototype
514
515
516 /* Below — message handling, downloading and other routines */
517
518 // Browser ID3 Writer v4.0.0
519 // Author: egoroof
520 // License: MIT
521 // https://github.com/egoroof/browser-id3-writer
522 // Minified version from https://egoroof.ru/browser-id3-writer/js/browser-id3-writer.4.0.0.js
523
524 // JS FLACMetadataEditor v0.0.2.1
525 // Author: AHOHNMYC
526 // License: GPL-3.0-or-later
527 // https://greasyfork.org/scripts/40545
528
529 /* jshint ignore: start */
530 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}()})();
531 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}}})();
532 /* jshint ignore: end */
533
534 // Inner, in worker scope debug. Rewrites later
535 let DEBUG = true;
536 let coverSize = 1200;
537 let coverQuality = 80;
538
539
540 this.onmessage = ({data: data}) => {
541 if (data.hasOwnProperty('debug')) {
542 DEBUG = data.debug;
543 coverSize = data.coverSize;
544 coverQuality = data.coverQuality;
545 return true;
546 }
547
548 if (DEBUG) console.info('Worker got message:', data);
549
550
551 const url = data.url;
552 const key = data.key;
553 const trackData = data.trackData;
554 const tags = data.tags;
555 const azLyrics = data.azLyrics;
556 const albumData = data.albumData;
557
558 // config: url, responseType, onProgress, onSuccess, anyway
559 function niceXhr(config) {
560 const xhr = new XMLHttpRequest();
561 xhr.responseType = config.responseType;
562 xhr.onload = e=>{
563 if (200 === e.target.status)
564 config.onSuccess(e.target.response);
565 else
566 console.warn('Error with getting data from', e.target.responseURL);
567 config.anyway();
568 };
569 xhr.onerror = config.anyway;
570 xhr.onabort = config.anyway;
571 xhr.onprogress = config.onProgress;
572 xhr.open('GET', config.url);
573 xhr.send();
574 }
575
576
577 postMessage([trackData, '^waiting']);
578
579 niceXhr({
580 url: url,
581 responseType: 'arraybuffer',
582 onProgress: e=>{
583 const toMB = bytes=>(bytes/1024/1024).toFixed(2);
584 const loaded = toMB(e.loaded);
585 if (e.lengthComputable) {
586 const total = toMB(e.total);
587 const percent = Math.floor(100 * e.loaded / e.total);
588 postMessage([trackData, `^downloading ${loaded}/${total}^mb : ${percent}%`]);
589 } else {
590 postMessage([trackData, `^downloading ${loaded}^mb`]);
591 }
592 },
593 onSuccess: response=>{
594 postMessage([trackData,'^decrypting']);
595 let data = new Uint8Array(response);
596 const bf = new Blowfish(key);
597 const iv = new Uint8Array([0, 1, 2, 3, 4, 5, 6, 7]);
598 const L = data.length;
599 if (DEBUG) console.info('Data length:', data.length);
600 for (let i = 0; i < L; i += 6144) {
601 if (i % (6144 * 20) === 6144 * 19){ // let it display state at every 120K
602 const percent = Math.floor(i * 100.0 / L);
603 postMessage([trackData, `^decrypting ${percent}%`]);
604 }
605 if (i + 2048 <= L) {
606 let D = data.subarray(i, i + 2048);
607 // if (i===0) console.log(D.toString(), D.length);
608 bf.decryptCBC(D, iv);
609 data.set(D, i);
610 }
611 }
612 function binArr2utfStr(arr) { return Array.from(arr).map(n=>String.fromCharCode(n)).join(''); }
613 // According https://en.wikipedia.org/wiki/List_of_file_signatures
614 const isMP3 = (data[0] === 0xFF && data[1] === 0xFB) || 'ID3' === binArr2utfStr(data.subarray(0,3));
615 const isFLAC = 'fLaC' === binArr2utfStr(data.subarray(0,4));
616
617 if (!(isMP3 || isFLAC)) return console.warn('Decrypted file not MP3 nor FLAC!', data);
618
619 let writer;
620 if (isMP3) {
621 writer = new ID3Writer(data);
622 writer.setFrame('TIT2', tags.title)
623 .setFrame('TPE1', tags._artistsArray)
624 .setFrame('TALB', tags.album)
625 // TODO: wait answer: https://github.com/egoroof/browser-id3-writer/issues/52
626 // .setFrame('TSRC', tags.isrc)
627 // TODO: replace WOAS with WXXX
628 // TODO: wait answer: https://github.com/egoroof/browser-id3-writer/issues/53
629 .setFrame('WOAS', `https://deezer.com/track/${tags.id}`);
630 if (tags.track)
631 writer.setFrame('TRCK', tags.track);
632
633 if (albumData.label)
634 writer.setFrame('TPUB', albumData.label);
635 if (albumData.release_date)
636 writer.setFrame('TYER', albumData.release_date);
637
638 if (azLyrics.lyrics)
639 writer.setFrame('USLT', {
640 description: '',
641 lyrics: azLyrics.lyrics
642 });
643 // if (azLyrics.year)
644 // writer.setFrame('TYER', azLyrics.year);
645 }
646 if (isFLAC) {
647 writer = new FLACMetadataEditor(data);
648 writer.removeComment()
649 .addComment('ALBUM', tags.album)
650 .addComment('TITLE', tags.title)
651 .addComment('ISRC', tags.isrc)
652 .addComment('URL', `https://deezer.com/track/${tags.id}`);
653 tags._artistsArray.forEach(artist => writer.addComment('ARTIST', artist));
654 if (tags.track)
655 writer.addComment('TRACKNUMBER', tags.track);
656
657 if (albumData.label)
658 writer.addComment('ORGANIZATION', albumData.label);
659 if (albumData.release_date)
660 writer.addComment('DATE', albumData.release_date);
661
662 // At first i thought that GAIN is for ReplayGain, but my player returns other values,
663 // So now i don't know what is it ~
664 // if (tags.gain) writer.addComment('REPLAYGAIN_TRACK_GAIN', `${tags.replayGain} dB`);
665 if (azLyrics.lyrics)
666 writer.addComment('LYRICS', azLyrics.lyrics);
667 // if (azLyrics.year)
668 // writer.addComment('DATE', azLyrics.year);
669 }
670
671 // Downloading cover
672 const coverLink = `https://e-cdns-images.dzcdn.net/images/cover/${tags.cover}/${coverSize}x${coverSize}-000000-${coverQuality}-0-0.jpg`;
673 if (DEBUG) console.info('Trying to download', coverLink );
674 postMessage([trackData,'^coverDownloading']);
675 niceXhr({
676 url: coverLink,
677 responseType: 'arraybuffer',
678 onProgress: e=>{
679 const toMB = bytes=>(bytes/1024/1024).toFixed(2);
680 const loaded = toMB(e.loaded);
681 if (e.lengthComputable) {
682 const total = toMB(e.total);
683 const percent = Math.floor(100 * e.loaded / e.total);
684 postMessage([trackData, `^downloading ${loaded}/${total}^mb : ${percent}%`]);
685 } else {
686 postMessage([trackData, `^downloading ${loaded}^mb`]);
687 }
688 },
689 onSuccess: imageData=> {
690 if (isMP3) {
691 writer.setFrame('APIC', {
692 type: 3,
693 data: imageData,
694 description: ''
695 });
696 }
697 if (isFLAC) {
698 writer.addPicture({
699 data: imageData,
700 colorDepth: 24,
701 width: coverSize,
702 height: coverSize
703 });
704 }
705 },
706 anyway: ()=> {
707 if (isMP3)
708 writer.addTag();
709 if (isFLAC)
710 writer.serializeMetadata();
711 data = writer.arrayBuffer;
712
713 const blobUrl = URL.createObjectURL(new Blob([data], {
714 type: (isMP3 ? 'audio/mpeg' : isFLAC ? 'audio/flac' : 'audio/mpeg')
715 }));
716
717 if (DEBUG) console.info('Blob for "%s - %s" created: %s', tags.artist, tags.title, blobUrl);
718 postMessage([trackData, 'DONE', blobUrl]);
719 }
720 });
721 },
722 anyway: ()=>{}
723 });
724
725 };
726
727});//End var mainWk
728
729
730// Set debug and other values in worker like in script
731mainWk.postMessage({
732 debug: DEBUG,
733 coverSize: coverSize,
734 coverQuality: coverQuality,
735});
736
737
738// azLyrics parser
739function azLyrics(artist, title, onsuccess, anyway) {
740 const url = formatAzLyricsUrl(artist, title);
741 if (!url) return anyway();
742
743 const data = {
744 method: 'GET',
745 url: url,
746 onload: e=>{
747 if (200 === e.status)
748 onsuccess(parseAzLyricsPage(e.response));
749 else
750 console.warn('Cannot grab lyrics from', e.finalUrl);
751 anyway();
752 },
753 onerror: anyway,
754 onabort: anyway
755 };
756
757 // Greasemonkey with their 4.0 API became a monster ~
758 if (typeof GM !== 'undefined' && typeof GM.xmlHttpRequest !== 'undefined')
759 GM.xmlHttpRequest(data);
760 else
761 GM_xmlhttpRequest(data);
762}
763function parseAzLyricsPage(text) {
764 const azPage = new DOMParser().parseFromString(text, 'text/html');
765 const data = {};
766 const divs = Array.from(azPage.getElementsByTagName('div'));
767 const lyricsDiv = divs.find(div=>div.innerHTML.substr(1,4) === '<!--');
768 const lyrics = lyricsDiv.textContent;
769 data.lyrics = lyrics.replace(/^\s*/,'').replace(/\s*$/,''); // clean spaces and new lines in begin and end
770
771 const album = azPage.querySelector('.album-panel a[data-toggle]');
772 if (album) {
773 const matches = album.textContent.match(/"([^"]+)" \((\d+)\)/);
774 data.album = matches[1];
775 data.year = +matches[2];
776 }
777 return data;
778}
779function formatAzLyricsUrl(artist, title) {
780 const partOfUrl = [artist, title].map(el=>el.toLowerCase().replace(/[^\w]/g,'')).join('/').replace(/^the/,'');
781 if (partOfUrl === '/') return false;
782 return `https://www.azlyrics.com/lyrics/${partOfUrl}.html`;
783}
784
785
786// DOWNLOADER LOGIC: URL ENCRYPTION
787function zeroPad(data) {
788 const aesBS = 16;
789 const l = data.length;
790 if (l % aesBS !== 0)
791 data += '\0'.repeat(aesBS - (l % aesBS));
792 return data;
793}
794
795function encryptURL(track, fmt) {
796 const urlsep = '\xa4';
797 const key = aesjs.utils.utf8.toBytes('jo6aey6haid2Teih');
798 const cryptor = new aesjs.ModeOfOperation.ecb(key);
799
800 let steps = [track.MD5_ORIGIN, fmt, track.SNG_ID, track.MEDIA_VERSION].join(urlsep);
801 steps = [hex_md5(steps), steps, ''].join(urlsep);
802 steps = zeroPad(steps);
803 // This not works because it translates `urlsep' as two symbols
804 // steps = aesjs.utils.utf8.toBytes(steps);
805 // That's why we use this:
806 steps = steps.split('').map(c => c.charCodeAt(0)); // str2bin
807 steps = cryptor.encrypt(steps);
808 steps = aesjs.utils.hex.fromBytes(steps);
809 return steps;
810}
811
812// DOWNLOADER LOGIC: GLOBAL VARIABLES AND HELPER METHODS
813const fmtMp3_32 = 11, fmtMp3_64 = 10, fmtMp3_128 = 1, fmtMp3_320 = 3, fmtFLAC = 9;
814const trackDB = {};
815const urlDB = {};
816if (!localStorage.dlNameProto)
817 unsafeWindow.localStorage.dlNameProto = '%NN. %Artists - %Title %Version';
818if (!localStorage.dlAzLyrics)
819 unsafeWindow.localStorage.dlAzLyrics = true;
820
821function generateName(track, format = null) {
822 const tags = dzTags(track);
823 const nameProto = elNameProto.value;
824
825 let name = nameProto.replace(/%(\w+)/g, (full, keyword)=> {
826 let replacement = '';
827
828 if (/^n+$/i.test(keyword)) {
829 if (!tags.track) return '';
830 // Replace `NNNNN' to track number padded with zeroes
831 let rept = keyword.length - (tags.track).toString().length;
832 if (rept < 0) rept = 0;
833 return '0'.repeat(rept) + tags.track;
834
835 } else if (tags.hasOwnProperty(keyword.toLowerCase())) {
836 // Finded in dzTags values
837 replacement = tags[keyword.toLowerCase()];
838 if (!replacement) return '';
839
840 if (keyword === keyword.toLowerCase())
841 replacement = replacement.toLowerCase();
842 if (keyword === keyword.toUpperCase())
843 replacement = replacement.toUpperCase();
844 return replacement;
845
846 } else {
847 return full;
848 }
849 });
850
851 // remove escaping slashes
852 name = name.replace(/\\%/g, '%');
853 // remove trailing and starting spaces
854 name = name.replace(/\s*(.*[^\s])\s*/, '$1');
855
856 if (format) {
857 if (format === fmtFLAC)
858 name += '.flac';
859 else
860 name += '.mp3';
861 }
862 return name;
863}
864
865function updateNames() {
866 const trackNameEls = document.querySelectorAll('.dlTrackName');
867 trackNameEls.forEach( nameEl=> {
868 const nbsp = '\u00A0';
869 const trackId = nameEl.closest('.dlTrack').dataset.trackId;
870 nameEl.textContent = generateName(trackDB[trackId]).replace(/\s/g, nbsp);
871 });
872}
873
874// CONVERT TIME into min:sec
875function formatTime(seconds) {
876 const minutes = Math.floor(seconds/60);
877 seconds %= 60;
878 const zeroPad = seconds<10 ? '0' : '';
879 return `${minutes}:${zeroPad}${seconds}`;
880}
881
882// FILES TAGS
883function dzTags(trackInfo) {
884 // `new Set' is needed to unique atrists. e.g. https://deezer.com/album/6883271
885 const artistsArray = trackInfo.ARTISTS && GM_info.scriptHandler !== 'Greasemonkey' ? [...new Set(trackInfo.ARTISTS.map(a=>a.ART_NAME))] : [trackInfo.ART_NAME];
886 return {
887 artist: trackInfo.ART_NAME,
888 _artistsArray: artistsArray,
889 artists: artistsArray.join(', '),
890 album: trackInfo.ALB_TITLE,
891 album_id: trackInfo.ALB_ID,
892 cover: trackInfo.ALB_PICTURE,
893 disk: trackInfo.DISK_NUMBER,
894 // by name, it's have to be ReplayGain value. But in fact, this is something other ~
895 // looks like https://en.wikipedia.org/wiki/Audio_normalization#Peak_normalization
896 // http://wiki.hydrogenaud.io/index.php?title=ReplayGain
897 gain: trackInfo.GAIN,
898 id: trackInfo.SNG_ID,
899 // ISO 3901:1986 International Standard Recording Code (ISRC). Technical committee / subcommittee: TC 46 / SC 9
900 isrc: trackInfo.ISRC,
901 time: formatTime( trackInfo.DURATION ),
902 title: trackInfo.SNG_TITLE,
903 track: trackInfo.TRACK_NUMBER,
904 version: trackInfo.VERSION
905 };
906}
907
908const bfGK = 'g4el58wc0zvf9na1';
909
910function bfGenKey(id) {
911 const hash = hex_md5(id.toString());
912 const key = new Uint8Array(16);
913 for (let i = 0; i < 16; i++)
914 key[i] = bfGK.charCodeAt(i) ^ hash.charCodeAt(i) ^ hash.charCodeAt(i+16);
915 return key;
916}
917
918
919function fileSizeToMb(aSize) {
920 return isNaN(aSize) ? 0 : Math.round(aSize /1024/1024 * 10)/10;
921}
922
923// DOWNLOAD ENTRY POINT
924function dzDownload(track, trackId, fmt) {
925 const tags = dzTags(track);
926 const trackData = {
927 trackId: trackId,
928 fmt: fmt
929 };
930 let albumData = {};
931 let azLyricsData = {};
932
933 function processTrack() {
934 mainWk.postMessage({
935 url: `https://e-cdns-proxy-${track.MD5_ORIGIN[0]}.dzcdn.net/mobile/1/${encryptURL(track, fmt)}`,
936 key: bfGenKey(track.SNG_ID),
937 trackData: trackData,
938 tags: dzTags(track),
939 azLyrics: azLyricsData,
940 albumData: albumData
941 });
942 }
943
944 function getAlbumData() {
945 messageProcessor({data: [trackData, translate.gettingAlbumInfo]});
946 niceXhr({
947 url: `https://api.deezer.com/album/${tags.album_id}`,
948 responseType: 'json',
949 onProgress: ()=>{},
950 onSuccess: json=> {
951 albumData.label = json.label;
952 albumData.release_date = json.release_date;
953 },
954 anyway: processTrack
955 });
956 }
957
958 if (elAzLyrics.checked) {
959 messageProcessor({data: [trackData, translate.lyricsDownloading]});
960
961 azLyrics(tags.artist, tags.title,
962 data=>{
963 if (DEBUG) console.info(data);
964 azLyricsData=data;
965 },
966 getAlbumData
967 );
968 } else {
969 getAlbumData();
970 }
971
972}
973
974// DOWNLOADER WORKER CALLBACK
975function messageProcessor(msg) {
976 const fmt = +msg.data[0].fmt;
977 const trackId = msg.data[0].trackId;
978 let trackEl = document.querySelector(`.dlTrack[data-track-id="${trackId}"]`);
979 if (!trackEl)
980 console.warn('Downloaded track not found in track list!');
981
982 const state = msg.data[1];
983
984 switch (state) {
985 case 'DONE':
986 if (!urlDB[trackId]) urlDB[trackId] = {};
987 urlDB[trackId][fmt] = msg.data[2];
988
989 let anchor;
990 if (trackEl) {
991 anchor = trackEl.querySelector(`.dlLink[data-fmt="${fmt}"]`);
992 } else {
993 // create new temporary link element if container not found
994 anchor = document.head.appendChild(document.createElement('a'));
995 }
996 anchor.download = generateName(trackDB[trackId], fmt);
997 anchor.href = msg.data[2];
998 anchor.click();
999 // remove temporary link
1000 if (!trackEl) anchor.remove();
1001 break;
1002 case 'ABORT':
1003 console.error('Download abort:', trackId, fmt);
1004 break;
1005 case 'ERROR':
1006 console.error('Download ERROR!', trackId, fmt);
1007 break;
1008 default:
1009 const message = translateCircumstring(state);
1010 if (trackEl) {
1011 trackEl.querySelector('.dlTrackLinks').classList.add('hidden');
1012 trackEl.querySelector('.dlTrackStatus').classList.remove('hidden');
1013 trackEl.querySelector('.dlTrackStatus').textContent = message;
1014 } else {
1015 const tags = dzTags(trackDB[trackId]);
1016 console.info('%s - %s: %s', tags.artist, tags.title, message);
1017 }
1018 return;
1019 }
1020 if (trackEl) {
1021 trackEl.querySelector('.dlTrackLinks').classList.remove('hidden');
1022 trackEl.querySelector('.dlTrackStatus').classList.add('hidden');
1023 }
1024}
1025mainWk.onmessage = messageProcessor;
1026
1027// DOWNLOADER LOGIC: HTML GENERATOR
1028function getFilesize(track, fmt_name) {
1029 const sizeProp = `FILESIZE_${fmt_name}`;
1030 let size = 0;
1031 if (track[sizeProp] && !isNaN(track[sizeProp]))
1032 size = +track[sizeProp];
1033 return size;
1034}
1035
1036function generateDlLink(track, fmt_name, fmt_id, size) {
1037 const trackId = track.SNG_ID;
1038 const el = document.createElement('a');
1039 el.classList.add('dlLink');
1040 el.dataset.fmt = fmt_id;
1041 el.textContent = `⬇ ${fmt_name}`;
1042 el.title = `${fileSizeToMb(size).toLocaleString()} ${translate.mb}`;
1043 if (urlDB[trackId] && urlDB[trackId][fmt_id]) {
1044 el.download = generateName(trackDB[trackId], fmt_id);
1045 el.href = urlDB[trackId][fmt_id];
1046 }
1047 el.onclick = function() {
1048 if (this.href)
1049 return true;
1050
1051 const trackId = this.closest('.dlTrack').dataset.trackId;
1052 const fmt = this.dataset.fmt;
1053 dzDownload(trackDB[trackId], trackId, fmt);
1054 return false;
1055 };
1056 return el;
1057}
1058
1059function generateDlTrack(track) {
1060 const tags = dzTags(track);
1061 const infosTooltips =
1062 '^artist:\t' + tags.artist +
1063 '\n^title:\t' + tags.title +
1064 '\n^album:\t' + tags.album + // disk number sometimes not recognized when change track playing ????
1065 '\n^duration:\t' + tags.time;
1066
1067 const trackEl = document.createElement('div');
1068 trackEl.classList.add('dlTrack');
1069 trackEl.dataset.trackId = tags.id;
1070
1071 const nameEl = document.createElement('span');
1072 nameEl.classList.add('dlTrackName');
1073 nameEl.title = translateCircumstring(infosTooltips);
1074
1075 const linksEl = document.createElement('div');
1076 linksEl.classList.add('dlTrackLinks');
1077 if (showMp3_32) if (getFilesize(track, 'MP3_32') > 0) linksEl.appendChild(generateDlLink(track, 'mp3 32', fmtMp3_32, getFilesize(track, 'MP3_32')));
1078 if (showMp3_64) if (getFilesize(track, 'MP3_64') > 0) linksEl.appendChild(generateDlLink(track, 'mp3 64', fmtMp3_64, getFilesize(track, 'MP3_64')));
1079 if (showMp3_128) if (getFilesize(track, 'MP3_128') > 0) linksEl.appendChild(generateDlLink(track, 'mp3 128', fmtMp3_128, getFilesize(track, 'MP3_128')));
1080 if (showMp3_320) if (getFilesize(track, 'MP3_320') > 0) linksEl.appendChild(generateDlLink(track, 'mp3 320', fmtMp3_320, getFilesize(track, 'MP3_320')));
1081 if (showFLAC) if (getFilesize(track, 'FLAC') > 0) linksEl.appendChild(generateDlLink(track, 'flac', fmtFLAC, getFilesize(track, 'FLAC')));
1082
1083 const statusEl = document.createElement('div');
1084 statusEl.classList.add('dlTrackStatus', 'hidden');
1085
1086 const endFloat = document.createElement('div');
1087 endFloat.classList.add('dlEndFloat');
1088
1089 trackEl.appendChild(nameEl);
1090 trackEl.appendChild(linksEl);
1091 trackEl.appendChild(statusEl);
1092 trackEl.appendChild(endFloat);
1093 return trackEl;
1094}
1095
1096function isLoggedIn() {
1097 // return typeof unsafeWindow.USER !== 'undefined' && unsafeWindow.USER.USER_ID !== 0;
1098 return parseInt(unsafeWindow.user.current()) > 0;
1099}
1100
1101function getTrackListByHand() {
1102 const links = Array.from(document.querySelectorAll('[data-instance=track_name]'));
1103 const albums = links.map(el=>el.href.match(/\d+/)[0]);
1104 const names = links.map(el=>el.textContent);
1105
1106 if (DEBUG) console.info('albums:', albums);
1107 if (DEBUG) console.info('names:', names);
1108 if (DEBUG) console.info('trackDB:: length:', Object.keys(trackDB).length, 'db:', trackDB);
1109
1110 const result = [];
1111 for (let i=0; i<links.length; i++) {
1112 Object.keys(trackDB).forEach(trackId=>{
1113 if (trackDB[trackId].ALB_ID === albums[i])
1114 if (trackDB[trackId].SNG_TITLE === names[i] || `${trackDB[trackId].SNG_TITLE} ${trackDB[trackId].VERSION}` === names[i])
1115 result.push(trackDB[trackId]);
1116 });
1117 }
1118
1119 if (DEBUG) console.info('filtered', result);
1120
1121 return result;
1122}
1123
1124function getTrackListIndexByHand() {
1125 const links = Array.from(document.querySelectorAll('[data-instance=track_name]'));
1126 const names = links.map(el => el.textContent);
1127 let currentIndex = -1;
1128 const currentEl = document.querySelector('.player-track-link');
1129 if (currentEl)
1130 currentIndex = names.indexOf(currentEl.textContent);
1131 return currentIndex;
1132}
1133
1134// ENTRY POINTS
1135function refreshTracklist() {
1136 if (DEBUG) console.info('Refreshing the Track List');
1137
1138 while (divTracklist.firstElementChild)
1139 divTracklist.firstElementChild.remove();
1140
1141 if ( !isLoggedIn() ) {
1142 const placeHolderDiv = document.createElement('div');
1143 placeHolderDiv.classList.add('dlNotLoggedWarning');
1144 placeHolderDiv.textContent = translate.pleaseLogIn;
1145
1146 divTracklist.appendChild(placeHolderDiv);
1147
1148 return;
1149 }
1150
1151 let list, current;
1152 if (unsafeWindow.dzPlayer) {
1153 if (DEBUG) console.info('Used native `dzPlayer\'');
1154 // In fact, this one is already array, but Firefox+Greasemonkey don't believe it
1155 list = Array.from(unsafeWindow.dzPlayer.getTrackList());
1156 current = unsafeWindow.dzPlayer.getTrackListIndex();
1157 } else {
1158 if (DEBUG) console.info('Tracks are getting manually. Be careful with result');
1159 // Do some magic
1160 list = getTrackListByHand();
1161 current = getTrackListIndexByHand();
1162 }
1163 list.forEach( (track, index)=> {
1164 trackDB[track.SNG_ID] = track;
1165
1166 const trackDiv = generateDlTrack(track);
1167 if (index === current)
1168 trackDiv.classList.add('current');
1169 divTracklist.appendChild(trackDiv);
1170 });
1171
1172 updateNames();
1173}
1174
1175// THE DOWNLOADER PANEL
1176const rootEl = document.createElement('div');
1177rootEl.classList.add('dzHidden');
1178rootEl.id = 'dzDownloader';
1179
1180const elNameProto = document.createElement('input');
1181elNameProto.classList.add('dlNameProto');
1182elNameProto.title = `${translate.choose} ${translate.fileNaming}`;
1183elNameProto.value = localStorage.dlNameProto;
1184['change', 'keyup', 'keydown'].forEach( event=> {
1185 elNameProto.addEventListener(event, ()=>{
1186 unsafeWindow.localStorage.dlNameProto = elNameProto.value;
1187 updateNames();
1188 });
1189});
1190
1191const elInsert = document.createElement('span');
1192elInsert.classList.add('dlInsert');
1193elInsert.addEventListener('mouseenter', ()=>elNameProto.focus());
1194
1195const elInsertButton = document.createElement('button');
1196elInsertButton.classList.add('dlInsertButton');
1197elInsertButton.textContent = '⬇ ⬇ ⬇ ⬇';
1198elInsertButton.title = `Case of tag's impact to resulting case:
1199%ARTIST\t=> 'SYSTEM OF A DOWN'
1200%Artist\t=> 'System of a Down'
1201%artist\t=> 'system of a down'
1202
1203If count of digits in track number less than 'N' count, it will be padded with zeroes:
1204If track number is 2:
1205%N\t=> 2
1206%NN\t=> 02
1207%NNN\t=> 002
1208%NNNN\t=> 0002
1209If track number is 42:
1210%N\t=> 42
1211%NN\t=> 42
1212%NNN\t=> 042
1213%NNNN\t=> 0042`;
1214
1215const elInsertTable = document.createElement('table');
1216elInsertTable.classList.add('dlInsertTable');
1217function addInsertVariant(value) {
1218 const row = elInsertTable.insertRow();
1219 const title = row.insertCell();
1220 title.textContent = value;
1221
1222 if ('---' === value)return;
1223
1224 row.addEventListener('click', ()=>{
1225 const sel = {
1226 start: elNameProto.selectionStart,
1227 end: elNameProto.selectionEnd,
1228 direction: elNameProto.selectionDirection,
1229 };
1230 const src = elNameProto.value;
1231 elNameProto.value = src.substr(0, elNameProto.selectionStart) + '%' + value + src.substr(elNameProto.selectionEnd);
1232 if (sel.start === src.length) {
1233 elNameProto.selectionStart = elNameProto.selectionEnd = elNameProto.value.length;
1234 } else {
1235 elNameProto.selectionStart = sel.start;
1236 elNameProto.selectionEnd = sel.end;
1237 }
1238 elNameProto.selectionDirection = sel.direction;
1239 elNameProto.dispatchEvent(new Event('change'));
1240 elNameProto.focus();
1241 });
1242}
1243// strings
1244addInsertVariant('Artist');
1245addInsertVariant('Artists');
1246addInsertVariant('Album');
1247addInsertVariant('Title');
1248addInsertVariant('Version');
1249addInsertVariant('---');
1250// numbers
1251addInsertVariant('disk');
1252addInsertVariant('time');
1253addInsertVariant('isrc');
1254addInsertVariant('NN');
1255addInsertVariant('---');
1256// Deezer-specific
1257addInsertVariant('cover');
1258addInsertVariant('gain');
1259addInsertVariant('id');
1260addInsertVariant('album_id');
1261
1262elInsert.appendChild(elInsertButton);
1263elInsert.appendChild(elInsertTable);
1264
1265const elRefresh = document.createElement('a');
1266elRefresh.classList.add('dlRefresh');
1267elRefresh.textContent = `⟳ ${translate.refresh} ${translate.tracklist.toLowerCase()}`;
1268elRefresh.addEventListener('click', refreshTracklist);
1269
1270const elTrackListHeader = document.createElement('p');
1271elTrackListHeader.classList.add('dlTitle');
1272elTrackListHeader.textContent = `♪ ${translate.tracklist}`;
1273
1274
1275if (forceAzLyrics)
1276 unsafeWindow.localStorage.dlAzLyrics = forceAzValue;
1277
1278const elAzLyrics = document.createElement('input');
1279elAzLyrics.type = 'checkbox';
1280elAzLyrics.classList.add('dlDoAzLyrics');
1281elAzLyrics.id = 'dlDoAzLyrics';
1282elAzLyrics.checked = !!localStorage.dlAzLyrics;
1283elAzLyrics.addEventListener('change', ()=>{
1284 unsafeWindow.localStorage.dlAzLyrics = elAzLyrics.checked;
1285});
1286
1287const elAzLyricsLabel = document.createElement('label');
1288elAzLyricsLabel.setAttribute('for', 'dlDoAzLyrics');
1289elAzLyrics.classList.add('dlDoAzLyricsLabel');
1290elAzLyricsLabel.textContent = `azLyrics`;
1291
1292
1293const elListDownloadSelect = document.createElement('select');
1294elListDownloadSelect.classList.add('dlListDownloadSelect');
1295elListDownloadSelect.innerHTML =
1296 (!showMp3_32 ? '' : `<option value="${fmtMp3_32}" >mp3 32</option>`)+
1297 (!showMp3_64 ? '' : `<option value="${fmtMp3_64}" >mp3 64</option>`)+
1298 (!showMp3_128 ? '' : `<option value="${fmtMp3_128}">mp3 128</option>`)+
1299 (!showMp3_320 ? '' : `<option value="${fmtMp3_320}">mp3 320</option>`)+
1300 (!showFLAC ? '' : `<option value="${fmtFLAC}" >flac</option>`);
1301elListDownloadSelect.value = localStorage.dlListDownload;
1302elListDownloadSelect.addEventListener('change', ()=>{
1303 unsafeWindow.localStorage.dlListDownload = elListDownloadSelect.value;
1304});
1305
1306const elListDownloadButton = document.createElement('button');
1307elListDownloadButton.classList.add('dlListDownloadButton');
1308elListDownloadButton.textContent = translate.downloadList;
1309elListDownloadButton.addEventListener('click', ()=>{
1310 const selector = `.dlLink[data-fmt="${elListDownloadSelect.value}"]`;
1311 document.querySelectorAll(selector).forEach(anchor=>anchor.click());
1312});
1313
1314if (showAzLyrics) {
1315 elTrackListHeader.appendChild(elAzLyrics);
1316 elTrackListHeader.appendChild(elAzLyricsLabel);
1317}
1318if (showListDownloader) {
1319 elTrackListHeader.appendChild(elListDownloadSelect);
1320 elTrackListHeader.appendChild(elListDownloadButton);
1321}
1322
1323
1324const divTracklist = document.createElement('div');
1325divTracklist.classList.add('dlTrackList');
1326
1327rootEl.appendChild(elNameProto);
1328rootEl.appendChild(elInsert);
1329rootEl.appendChild(elRefresh);
1330if (showAzLyrics || showListDownloader)
1331 rootEl.appendChild(elTrackListHeader);
1332rootEl.appendChild(divTracklist);
1333
1334document.body.appendChild(rootEl);
1335
1336const triggerEl = document.createElement('div');
1337triggerEl.classList.add('dlTrigger');
1338triggerEl.title = translate.clickToOpen;
1339triggerEl.textContent = 'D➲wnloader ☰';
1340triggerEl.addEventListener('click', ()=>{
1341 let panelContainer = document.querySelector('.panel-container');
1342
1343 if (rootEl.classList.contains('dzHidden')) {
1344 panelContainer.style.display = 'block';
1345 panelContainer.classList.add('opened');
1346 setTimeout(()=>{
1347 if (panelContainer.classList.contains('opened'))
1348 panelContainer.style.display = 'block';
1349 }, 150);
1350
1351 triggerEl.textContent = 'D➲wnloader ⚟';
1352 // refreshTracklist();
1353 } else {
1354 if ('true' === panelContainer.getAttribute('aria-hidden')) {
1355 panelContainer.classList.remove('opened');
1356 setTimeout(()=>{
1357 if ('true' === panelContainer.getAttribute('aria-hidden'))
1358 panelContainer.style = '';
1359 }, 150);
1360 }
1361
1362 triggerEl.textContent = 'D➲wnloader ☰';
1363 }
1364 rootEl.classList.toggle('dzHidden');
1365});
1366
1367document.body.appendChild(triggerEl);
1368
1369
1370function hidePannel() {
1371 if (!rootEl.classList.contains('dzHidden'))
1372 triggerEl.click();
1373}
1374
1375
1376// Hotkey handling
1377addEventListener('keydown', e => {
1378 switch (e.code) {
1379 case 'Escape':
1380 hidePannel();
1381 break;
1382 case 'KeyD':
1383 if (document.activeElement.nodeName !== 'INPUT')
1384 triggerEl.click();
1385 break;
1386 default:
1387 if (SHOW_KEYS) console.info(`Key with code "${e.code}" was pressed.`);
1388 }
1389});
1390
1391
1392
1393// Hide trigger when Deezer interface loading
1394// This element will be removed later
1395const delayStyleElement = document.head.appendChild(document.createElement('style'));
1396delayStyleElement.textContent = '#dzDownloader, .dlTrigger {display: none !important}';
1397
1398function onInterfaceLoaded() {
1399 delayStyleElement.remove();
1400
1401 setTimeout(() => {
1402 const closePremium = document.querySelector`#modal_push_premium > #modal-close`;
1403 if (closePremium) closePremium.click();
1404 }, 500)
1405
1406 const isBetaInterface = !document.getElementById('menu_search');
1407 if (isBetaInterface) {
1408 const dzrApp = document.getElementById('dzr-app');
1409 const panelContainer = dzrApp.appendChild(document.createElement('div'));
1410 panelContainer.classList.add('panel-container');
1411 const panelOverlay = panelContainer.appendChild(document.createElement('div'));
1412 panelOverlay.classList.add('panel-overlay')
1413
1414 document.head.appendChild(document.createElement('style')).textContent = `
1415#dzDownloader.dzHidden {
1416 transform: translateX(-100%);
1417}
1418`;
1419 } else {
1420 document.getElementById('menu_search').addEventListener('focus', hidePannel);
1421 document.getElementById('page_panels').addEventListener('click', hidePannel);
1422 document.getElementById('page_sidebar').addEventListener('click', hidePannel);
1423 }
1424
1425
1426 if (document.querySelector('.hotkeys')) {
1427 document.head.appendChild(document.createElement('style')).textContent =
1428 '.hotkeys {height: 225px !important}';
1429
1430 document.querySelector('.hotkeys .mapping').innerHTML +=
1431 '<div class="hotkey">'+
1432 ' <div class="hotkey-icon single">D</div>'+
1433 ` <div class="hotkey-label">${translate.downloaderHotkey}</div>`+
1434 '</div>';
1435 }
1436
1437 // There we show and immediately hide playlist. It's for initializing this element, nothing more
1438 if (!unsafeWindow.dzPlayer) {
1439 const pannels = document.getElementById('page_panels');
1440 const playListEl = document.querySelector('button.control-qlist');
1441 pannels.style.dislplay = 'none';
1442 playListEl.click();
1443 playListEl.click();
1444 setTimeout(() => {
1445 pannels.removeAttribute('style');
1446 }, 500);
1447 }
1448
1449 // grab info about tracks from `PLAYER_INIT' variable
1450 trackRecurseLookup(unsafeWindow.PLAYER_INIT);
1451
1452 refreshTracklist();
1453
1454 if (openOnStart)
1455 triggerEl.click();
1456}
1457
1458const waitInterfaceLoading = ()=>{
1459 if (!(document.querySelector('button.control-qlist') || document.querySelector('svg.svg-icon-equalizer'))) return setTimeout(waitInterfaceLoading, 150);
1460 onInterfaceLoaded();
1461};
1462waitInterfaceLoading();
1463
1464/*
1465if (/playlist/.test(location)) {
1466 niceXhr({
1467 url: `https://api.deezer.com/${location.pathname.match(/playlist\/\d+/)[0]}/tracks`,
1468 responseType: 'json',
1469 onProgress: ()=>{},
1470 onSuccess: json=>{
1471 if (DEBUG) console.info('trackDB:: length:', Object.keys(trackDB).length, 'db:', trackDB);
1472 console.log(json);
1473 debugger;
1474
1475 json.tracks.data.forEach(track=>niceXhr({
1476 url: track.link.replace('www','api'),
1477 responseType: 'json',
1478 onProgress: ()=>{},
1479
1480 }));
1481
1482 trackRecurseLookup();
1483 if (DEBUG) console.info('trackDB:: length:', Object.keys(trackDB).length, 'db:', trackDB);
1484 refreshTracklist();
1485 },
1486 anyway: ()=>{}
1487 });
1488}
1489*/
1490
1491// DATA STYLESHEET.
1492document.head.appendChild(document.createElement('style')).textContent = `
1493#dzDownloader {
1494 transform: translateX(220px);
1495 transition: transform .15s;
1496 position: fixed;
1497 top: 0;
1498 left: 0;
1499 z-index: 299;
1500 background: #ccc;
1501 padding: 5px;
1502 height: calc(100% - 48px);
1503 overflow-y: auto;
1504 max-width: 80%;
1505 font-size: 1.05em;
1506}
1507#dzDownloader.dzHidden {
1508 transform: translateX(calc(220px - 100%));
1509}
1510.dlNameProto {
1511 text-align: center;
1512 width: 30em;
1513}
1514.dlInsert {
1515}
1516.dlInsertButton {
1517 cursor: help;
1518 height: 1.86em;
1519 padding-left: 0.3em;
1520}
1521.dlInsert:not(:hover) .dlInsertTable {
1522 display: none;
1523}
1524.dlInsertTable {
1525 position: absolute;
1526 left: calc(220px + 13em);
1527 background-color: #c8c8c8;
1528 border: #888 solid 1px;
1529}
1530.dlInsertTable tr:hover {
1531 background-color: #ddd;
1532}
1533.dlInsertTable td {
1534 cursor: pointer;
1535}
1536.dlRefresh {
1537 font-size: .85em;
1538 margin-left: 25px;
1539}
1540.dlTitle {
1541 cursor: default;
1542 border-bottom: gray 1.5px solid;
1543 border-style: none none dashed;
1544 color: #000;
1545 font-weight: 700er;
1546 margin-bottom: 1%;
1547 margin-left: 7px;
1548 padding-bottom: 3px;
1549 padding-top: 3px;
1550}
1551.dlDoAzLyrics {
1552 margin-left: 4em;
1553}
1554.dlDoAzLyricsLabel {
1555}
1556.dlListDownloadSelect {
1557 text-align: center;
1558 margin-left: 4em;
1559}
1560.dlListDownloadButton:hover {background: #444;}
1561.dlListDownloadButton {
1562 border-radius: 12px;
1563 text-shadow: 2px 1px 2px #000000;
1564 color: #fff;
1565 font-size: 12px;
1566 box-shadow: 1px 1px 4px #000000;
1567 padding: 2px 11px;
1568 background: #777;
1569 margin-left: 14px;
1570}
1571.dlNotLoggedWarning {
1572 color: #b91616;
1573 display: grid;
1574 font-size: medium;
1575 font-weight: bold;
1576 height: 4em;
1577 width: 100%;
1578 text-align: center;
1579 align-content: center;
1580}
1581.dlTrackList {
1582}
1583.dlTrack:nth-child(even) {
1584 background: #ddd;
1585}
1586.dlTrack.current {
1587 background: #888;
1588 color: #fff;
1589}
1590.dlTrackName {
1591 display: inline-block;
1592 max-width: 30em;
1593 margin-left: .5em;
1594 overflow: hidden;
1595 text-overflow: ellipsis;
1596 white-space: nowrap;
1597 padding-top: 0.1em;
1598 cursor: help;
1599}
1600.dlTrackLinks {
1601 float: right;
1602 margin-top: .2em;
1603 margin-right: .3em;
1604}
1605.dlLink {
1606 margin-left: 4px;
1607}
1608.dlLink[download] {
1609 text-decoration: line-through;
1610}
1611.dlLink:hover {
1612 text-decoration: underline;
1613}
1614.dlTrackStatus {
1615 float: right;
1616 margin-top: .2em;
1617 margin-right: .3em;
1618 overflow: hidden;
1619}
1620.dlEndFloat {
1621 clear: both;
1622}
1623
1624.dlTrigger {
1625 cursor: pointer;
1626 position: fixed;
1627 left: 0;
1628 top: 0;
1629 z-index: 501;
1630 background: #ccc;
1631 padding: 3px;
1632 width: 220px;
1633 text-align: right;
1634 font-size: 1.1em;
1635}
1636
1637/* below - deezer's native elements */
1638
1639.page-electron-sidebar, .page-player .player-full {
1640 bottom: 48px !important;
1641}
1642.page-player .player-bottom {
1643 height: auto !important;
1644}
1645
1646#page_sidebar {
1647 padding-top: 20px;
1648 bottom: 48px;
1649}
1650.logo-deezer {height: 55px}
1651.index-header-logo {
1652 height: 65px;
1653 max-width: 100%;
1654 width: 227px;
1655}
1656
1657.logo-deezer-hp.logo.index-header-logo,
1658.logo-deezer.logo {
1659/*
1660 background-image: url(https://framapic.org/5RWAy9Zz6ekd/X7hftDFqC9MY.png);
1661*/
1662 background-position: center;
1663 background-repeat: no-repeat;
1664 background-size: contain;
1665}
1666.page-sidebar .brand {margin-bottom: 0;}
1667#menu_navigation {top: 135px;}
1668.page-sidebar .nav-link {padding: 7px;}
1669
1670/* AdBlock warning */
1671.abp-banner-container {display: none !important}
1672
1673`;