· 4 years ago · May 31, 2021, 06:04 PM
1//---CONFIGURATION---//
2const showMp3_128 = true; // Show MP3 @128k download (default: true)
3const showMp3_320 = true; // Show MP3 @320k download (default: true)
4const showFLAC = true; // Show FLAC download (default: true)
5const showAzLyrics = true; // Show azLyrics checkbox (default: true)
6const showListDownloader = true; // Show bulk download option (default: true)
7const coverSize = 600; // JPEG cover size in px (default: 600)
8const coverQuality = 80; // JPEG cover quality from 0 to 100 (default: 80)
9
10//---DEBUG---//
11const SHOW_KEYS = !true; // Show pressed keys (default: !true)
12const DEBUG = !true; // Debug ON/OFF (default: !true)
13const L10nDEBUG = !true; // Enable localization override (default: !true)
14const L10nOVERRIDE = 'en'; // Override browser language with selected (default: 'en')
15
16// ==UserScript==
17// @name DeezRevived
18// @description Download music from Deezer in any chosen format. Supports HQ/HiFi even without Premium subscription.
19// @version 1.1.8.1
20// @author Deki Haker, Kawashi666 and some others developers
21// devs Jonathan Tavares (revisions, fixes, proofreading, compatibility)
22// devs K-mik@Z (fixes, proofreading, style, translation, compatibility)
23// devs AHOHNMYC (code refractoring, fixes, translation)
24// devs AleX (fixes, style, compatibility)
25// @namespace Script from original 'Deezer:Download', revisited by some developers and contributors
26// @icon https://i.imgur.com/kFReGp3.jpg
27// screenshot https://i.imgur.com/7FogWtb.jpg
28// license Beerware License https://paste.debian.net/hidden/plainh/f360edb6
29// @homepageURL https://gist.github.com/aleandroid/cbd675e1655e22b2727ef02dedfe3dfb
30// @downloadURL https://gist.github.com/aleandroid/cbd675e1655e22b2727ef02dedfe3dfb/raw/deez.revived.user.js
31// @updateURL https://gist.github.com/aleandroid/cbd675e1655e22b2727ef02dedfe3dfb/raw/deez.revived.user.js
32// @grant unsafeWindow
33// @grant GM_xmlhttpRequest
34// @grant GM.xmlHttpRequest
35// @connect azlyrics.com
36// @connect api.deezer.com
37// @require https://gist.githubusercontent.com/aleandroid/ca7b7fd58a4a05751f5608107f87a7f0/raw/PajHome.MD5.min.js
38// license BSD
39// attribution Paul Johnston http://pajhome.org.uk/crypt/md5/
40// @require https://cdnjs.cloudflare.com/ajax/libs/aes-js/3.1.2/index.min.js
41// license MIT
42// attribution ricmoo https://github.com/ricmoo/aes-js
43// @match https://www.deezer.com/*
44// @description:de Laden Sie Musik von Deezer im MP3 oder FLAC format herunter, auch ohne Abonnement!
45// @description:en Download music from Deezer in MP3 or FLAC format, even without a subscription!
46// @description:es ¡Descargue música de Deezer en formato MP3 o FLAC, incluso sin suscripción!
47// @description:fr Téléchargez de la musique de Deezer au format MP3 ou FLAC, même sans abonnement!
48// @description:it Scarica musica da Deezer in formato MP3 o FLAC, anche senza abbonamento!
49// @description:pt Baixe músicas do Deezer em formato MP3 ou FLAC, mesmo sem uma assinatura!
50// @description:ru Скачивайте музыку с Deezer в формате MP3 или FLAC, даже без подписки!
51// @noframes
52// compatible Chrome (TamperMonkey, ViolentMonkey)
53// compatible Opera (TamperMonkey)
54// compatible Firefox (TamperMonkey, GreaseMonkey, ViolentMonkey)
55// compatible Edge (TamperMonkey)
56// ==/UserScript==
57
58const _getI18nMap = { // Default lang is 'en'
59 en: {
60 downloading: 'Downloading',
61 decrypting: 'Decrypting',
62 waiting: 'Waiting',
63 choose: 'Choose',
64 fileNaming: 'file naming',
65 title: 'Title',
66 artist: 'Artist',
67 refresh: 'Refresh',
68 album: 'Album',
69 mb: 'МB',
70 duration: 'Duration',
71 coverDownloading: 'Downloading cover',
72 downloaderHotkey: 'Open "Deezer:D\➲wnloader"',
73 tracklist: 'Tracklist',
74 clickToOpen: 'Click to open\nor D key (keyboard shortcut)',
75 downloadList: 'Download tracklist',
76 lyricsDownloading: 'Downloading lyrics',
77 gettingAlbumInfo: 'Getting album info'
78 },
79 de: {
80 downloading: 'Herunterladen',
81 decrypting: 'Entschlüsselung',
82 waiting: 'Warten',
83 choose: 'Wählen',
84 fileNaming: 'Sie die Dateibenennung',
85 title: 'Titel',
86 artist: 'Künstler',
87 refresh: 'Aktualisiere die',
88 album: 'Album',
89 duration: 'Dauer',
90 coverDownloading: 'Frontabdeckung Herunterladen',
91 mb: 'МB',
92 downloaderHotkey: 'Öffnen Sie das Panel "Deezer:D\➲wnloader"',
93 tracklist: 'Liste der Tracks',
94 clickToOpen: 'Klicken Sie auf Öffnen\noder D-Taste (Tastaturkürzel)',
95 downloadList: 'Laden Sie die Liste herunter',
96 lyricsDownloading: 'Herunterladen der Liedtexte',
97 gettingAlbumInfo: 'Informationen zum Album erhalten'
98 },
99 es: {
100 downloading: 'Descargan',
101 decrypting: 'Descifrado',
102 waiting: 'Esperando la descarga',
103 choose: 'Elege',
104 fileNaming: 'el nombre del archivo',
105 title: 'Título',
106 artist: 'Artista',
107 refresh: 'actualizar la',
108 album: 'Álbum',
109 duration: 'Duración',
110 coverDownloading: 'Descargando la portada',
111 mb: 'МB',
112 downloaderHotkey: 'Abra el panel "Deezer:D\➲wnloader"',
113 tracklist: 'Lista de pistas',
114 clickToOpen: 'Haga clic para abrir\no la tecla D (atajo de teclado)',
115 downloadList: 'Descargue la lista',
116 lyricsDownloading: 'Descargando la letra',
117 gettingAlbumInfo: 'Obtener información del álbum'
118 },
119 fr: {
120 downloading: 'Téléchargement',
121 decrypting: 'Décryptage',
122 waiting: 'En attente',
123 choose: 'choisir',
124 fileNaming: 'le nom de fichier souhaité',
125 title: 'Titre',
126 artist: 'Artiste',
127 refresh: 'Rafraîchir la',
128 album: 'Album',
129 duration: 'Durée',
130 coverDownloading: 'Téléchargement de la couverture',
131 mb: 'MB',
132 downloaderHotkey: 'Ouvrir "Deezer:D\➲wnloader"',
133 tracklist: 'Liste des pistes',
134 clickToOpen: 'Cliquer pour ouvrir\nou touche D (raccourcis clavier)',
135 downloadList: 'Télécharger la liste',
136 lyricsDownloading: 'Télécharger les paroles',
137 gettingAlbumInfo: 'Obtenir des informations sur l\'album'
138 },
139 it: {
140 downloading: 'In download',
141 decrypting: 'Decrypting',
142 waiting: 'In attesa',
143 choose: 'Imposta',
144 fileNaming: 'denominazione file',
145 title: 'Titolo',
146 artist: 'Artista',
147 refresh: 'Aggiorna',
148 album: 'Album',
149 duration: 'Durata',
150 coverDownloading: 'Download copertina',
151 mb: 'МB',
152 downloaderHotkey: 'Apri "Deezer:D\➲wnloader"',
153 tracklist: 'Tracklist',
154 clickToOpen: 'Fai click per aprire\no premi il tasto D',
155 downloadList: 'Scarica tracklist',
156 lyricsDownloading: 'Download testi...',
157 gettingAlbumInfo: 'Download informazioni album...'
158 },
159 pt: {
160 downloading: 'Baixar',
161 decrypting: 'Descriptografar',
162 waiting: 'Carregando',
163 choose: 'Escolha',
164 fileNaming: 'o nome do arquivo',
165 title: 'Título',
166 artist: 'Artista',
167 refresh: 'Atualize a',
168 album: 'Álbum',
169 duration: 'Duração',
170 coverDownloading: 'Baixando capa',
171 mb: 'МB',
172 downloaderHotkey: 'Abra o painel "Deezer\:D\➲wnloader"',
173 tracklist: 'Lista de faixas',
174 clickToOpen: 'Clique para abrir\nou tecla D (atalho do teclado)',
175 downloadList: 'Baixe a lista',
176 lyricsDownloading: 'Download das letras',
177 gettingAlbumInfo: 'Obtendo informações sobre o álbum'
178 },
179 'pt-BR': {
180 downloading: 'Baixar',
181 decrypting: 'Descriptografar',
182 waiting: 'Carregando',
183 choose: 'Escolha',
184 fileNaming: 'o nome do arquivo',
185 title: 'Título',
186 artist: 'Artista',
187 refresh: 'Atualize a',
188 album: 'Álbum',
189 duration: 'Duração',
190 coverDownloading: 'Baixando capa',
191 mb: 'МB',
192 downloaderHotkey: 'Abra o painel "Deezer:D\➲wnloader"',
193 tracklist: 'Lista de faixas',
194 clickToOpen: 'Clique para abrir\nou tecla D (atalho do teclado)',
195 downloadList: 'Baixe a lista',
196 lyricsDownloading: 'Download das letras',
197 gettingAlbumInfo: 'Obtendo informações sobre o álbum'
198 },
199 ru: {
200 downloading: 'Загрузка',
201 decrypting: 'Расшифровка',
202 waiting: 'Ожидание загрузки',
203 choose: 'Выбор',
204 fileNaming: 'имени файла',
205 title: 'Название',
206 artist: 'Исполнитель',
207 refresh: 'Обновить',
208 album: 'Альбом',
209 duration: 'Длительность',
210 coverDownloading: 'Загрузка обложки',
211 mb: 'МБ',
212 downloaderHotkey: 'Открыть панель "Deezer:D\➲wnloader"',
213 tracklist: 'Список треков',
214 clickToOpen: 'Клик здесь или нажатие клавиши D\nоткроют панель',
215 downloadList: 'Загрузить список',
216 lyricsDownloading: 'Загрузка текста',
217 gettingAlbumInfo: 'Получение информации об альбоме'
218 }
219};
220
221//[ °°° SECTION GUI LANGUAGE °°° ]
222let L10n = L10nDEBUG ? L10nOVERRIDE : navigator.language;
223if (!_getI18nMap[L10n]) L10n = L10n.substring(0, 2);
224if (!_getI18nMap[L10n]) L10n = 'en';
225
226const translate = _getI18nMap[L10n]; // Now, use translate.whatYouWant ( better than translate[whatYouWant] ).
227
228/* Replaces `10 ^mb' to `10 MB' */
229function translateCircumstring(data) {
230 return data.replace(/\^(\w+)/g, (full, part) => translate[part] ? translate[part] : full);
231}
232
233// FETCH SUBSTITUTE
234if (GM_info.scriptHandler) {
235 if (!localStorage.gmFetchWarningShown) {
236 unsafeWindow.localStorage.gmFetchWarningShown = 'false';
237 alert('We cannot safely replace `fetch()\' in GreaseMonkey. If troubles starts, replace GreaseMonkey with ViolentMonkey or TamperMonkey. This message will not be shown anymore');
238 }
239} else {
240 const orig_fetch = unsafeWindow.fetch;
241 unsafeWindow.fetch = (input, init = {}) => {
242 if (DEBUG) console.info('fetch() to:', input);
243 if (input.startsWith('https://www.deezer.com/ajax/gw-light.php?method=log.listen'))
244 return;
245
246 if (input.startsWith('https://www.deezer.com/ajax/gw-light.php')) {
247 orig_fetch(input, init)
248 .then(response => response.json())
249 .then(trackRecurseLookup);
250 }
251
252 return orig_fetch(input, init);
253 };
254}
255
256function trackRecurseLookup(key) {
257 if (key !== null && typeof key === 'object') {
258 if (key.SNG_ID)
259 trackDB[key.SNG_ID] = key;
260
261 else if (key.forEach)
262 key.forEach(trackRecurseLookup);
263 else
264 Object.keys(key).forEach(subKey => trackRecurseLookup(key[subKey]));
265 }
266}
267
268// config: url, responseType, onProgress, onSuccess, anyway
269function niceXhr(config) {
270 const data = {
271 responseType: config.responseType,
272 method: 'GET',
273 url: config.url,
274 onload: e => {
275 if (200 === e.status)
276 config.onSuccess(e.response);
277 else
278 console.warn('Error with getting data from', e.finalUrl);
279 config.anyway();
280 },
281 onerror: config.anyway,
282 onabort: config.anyway,
283 onprogress: config.onProgress,
284 };
285
286 // Greasemonkey with their 4.0 API became a monster ~
287 if (typeof GM !== 'undefined' && typeof GM.xmlHttpRequest !== 'undefined')
288 GM.xmlHttpRequest(data);
289 else
290 GM_xmlhttpRequest(data);
291}
292
293// WORKER
294function createWorker(code) {
295 const blobURL = URL.createObjectURL(new Blob(
296 ['(', code.toString(), ')()'], {
297 type: 'text/javascript'
298 }
299 ));
300 const worker = new Worker(blobURL);
301 URL.revokeObjectURL(blobURL);
302 return worker;
303}
304
305const mainWk = createWorker(function() {
306 // BLOWFISH LIBRARY, adapted from https://github.com/agorlov/javascript-blowfish (MIT-licensed)
307 // Modified to work with byte arrays, and also supports encryption / decryption in-place for buffers.
308 // Now it works ONLY with byte arrays and can ONLY decrypt data ONLY in CBC mode.
309 // Cannot be @require-d, as it is part of worker code. Workers share NO data so everything must be embedded.
310 const Blowfish = function(key) {
311 this.key = key;
312 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]);
313 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]);
314 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]);
315 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]);
316 this.pArray = new Uint32Array([0x243f6a88, 0x85a308d3, 0x13198a2e, 0x03707344, 0xa4093822, 0x299f31d0, 0x082efa98, 0xec4e6c89, 0x452821e6, 0x38d01377, 0xbe5466cf, 0x34e90c6c, 0xc0ac29b7, 0xc97c50dd, 0x3f84d5b5, 0xb5470917, 0x9216d5d9, 0x8979fb1b]);
317 this.generateSubkeys(key);
318 };
319 Blowfish.prototype = {
320 sBox0: null,
321 sBox1: null,
322 sBox2: null,
323 sBox3: null,
324 pArray: null,
325 key: null,
326 fixNegative: (number) => (number >>> 0),
327 num2block32: (num) => [num >>> 24, num << 8 >>> 24, num << 16 >>> 24, num << 24 >>> 24],
328 block32toNum: function(block32) {
329 return this.fixNegative(block32[0] << 24 | block32[1] << 16 | block32[2] << 8 | block32[3]);
330 },
331 xor: function(a, b) {
332 return this.fixNegative(a ^ b);
333 },
334 addMod32: function(a, b) {
335 return this.fixNegative((a + b) | 0);
336 },
337 split64by32: function(block64) {
338 return [this.block32toNum(block64.subarray(0, 4)), this.block32toNum(block64.subarray(4, 8))];
339 },
340 decryptCBC: function(data, iv) {
341 let blocks = Math.ceil(data.length / 8),
342 ivLivR = this.split64by32(iv),
343 ivL = ivLivR[0],
344 ivR = ivLivR[1],
345 ivLtmp,
346 ivRtmp;
347 for (let i = 0; i < blocks; i++) {
348 let block = data.subarray(i * 8, (i + 1) * 8);
349 if (block.length < 8) throw new Error('BF: ciphertext too short (must be multiple of 8 bytes)');
350 let xLxR = this.split64by32(block),
351 xL = xLxR[0],
352 xR = xLxR[1];
353 ivLtmp = xL;
354 ivRtmp = xR;
355 xLxR = this.decipher(xL, xR);
356 xL = xLxR[0];
357 xR = xLxR[1];
358 xL = this.xor(xL, ivL);
359 xR = this.xor(xR, ivR);
360 ivL = ivLtmp;
361 ivR = ivRtmp;
362 data.set(this.num2block32(xL), i * 8);
363 data.set(this.num2block32(xR), i * 8 + 4);
364 }
365 },
366 F: function(xL) {
367 let a = xL >>> 24,
368 b = xL << 8 >>> 24,
369 c = xL << 16 >>> 24,
370 d = xL << 24 >>> 24,
371 res = this.addMod32(this.sBox0[a], this.sBox1[b]);
372 res = this.xor(res, this.sBox2[c]);
373 res = this.addMod32(res, this.sBox3[d]);
374 return res;
375 },
376 encipher: function(xL, xR) {
377 let tmp;
378 for (let i = 0; i < 16; i++) {
379 xL = this.xor(xL, this.pArray[i]);
380 xR = this.xor(this.F(xL), xR);
381 tmp = xL;
382 xL = xR;
383 xR = tmp;
384 }
385 tmp = xL;
386 xL = xR;
387 xR = tmp;
388 xR = this.xor(xR, this.pArray[16]);
389 xL = this.xor(xL, this.pArray[17]);
390 return [xL, xR];
391 },
392 decipher: function(xL, xR) {
393 let tmp;
394 xL = this.xor(xL, this.pArray[17]);
395 xR = this.xor(xR, this.pArray[16]);
396 tmp = xL;
397 xL = xR;
398 xR = tmp;
399 for (let i = 15; i >= 0; i--) {
400 tmp = xL;
401 xL = xR;
402 xR = tmp;
403 xR = this.xor(this.F(xL), xR);
404 xL = this.xor(xL, this.pArray[i]);
405 }
406 return [xL, xR];
407 },
408 generateSubkeys: function(key) {
409 let data = 0,
410 k = 0,
411 i,
412 j;
413 for (i = 0; i < 18; i++) {
414 for (j = 4; j > 0; j--) {
415 data = this.fixNegative(data << 8 | key[k]);
416 k = (k + 1) % key.length;
417 }
418 this.pArray[i] = this.xor(this.pArray[i], data);
419 data = 0;
420 }
421 let block64 = [0, 0];
422 for (i = 0; i < 18; i += 2) {
423 block64 = this.encipher(block64[0], block64[1]);
424 this.pArray[i] = block64[0];
425 this.pArray[i + 1] = block64[1];
426 }
427 for (i = 0; i < 256; i += 2) {
428 block64 = this.encipher(block64[0], block64[1]);
429 this.sBox0[i] = block64[0];
430 this.sBox0[i + 1] = block64[1];
431 }
432 for (i = 0; i < 256; i += 2) {
433 block64 = this.encipher(block64[0], block64[1]);
434 this.sBox1[i] = block64[0];
435 this.sBox1[i + 1] = block64[1];
436 }
437 for (i = 0; i < 256; i += 2) {
438 block64 = this.encipher(block64[0], block64[1]);
439 this.sBox2[i] = block64[0];
440 this.sBox2[i + 1] = block64[1];
441 }
442 for (i = 0; i < 256; i += 2) {
443 block64 = this.encipher(block64[0], block64[1]);
444 this.sBox3[i] = block64[0];
445 this.sBox3[i + 1] = block64[1];
446 }
447 }
448 }; // End Blowfish.prototype
449
450 /* Below — message handling, downloading and other routines */
451
452 // Browser ID3 Writer v4.0.0
453 // Author: egoroof
454 // License: MIT
455 // https://github.com/egoroof/browser-id3-writer
456 // Minified version from https://egoroof.ru/browser-id3-writer/js/browser-id3-writer.4.0.0.js
457
458 // JS FLACMetadataEditor v0.0.2.1
459 // Author: AHOHNMYC
460 // License: GPL-3.0-or-later
461 // https://greasyfork.org/scripts/40545
462
463 /* jshint ignore: start */
464 const ID3Writer = (() => {
465 "use strict";
466
467 function e(e, t) {
468 if (!(e instanceof t)) throw new TypeError("Cannot call a class as a function")
469 }
470
471 function t(e) {
472 return String(e).split("").map(function(e) {
473 return e.charCodeAt(0)
474 })
475 }
476
477 function r(e) {
478 return new Uint8Array(t(e))
479 }
480
481 function n(e) {
482 let r = new Uint8Array(2 * e.length);
483 return new Uint16Array(r.buffer).set(t(e)), r
484 }
485
486 function a(e) {
487 return 73 === e[0] && 68 === e[1] && 51 === e[2]
488 }
489
490 function i(e) {
491 if (!e || !e.length) return null;
492 if (255 === e[0] && 216 === e[1] && 255 === e[2]) return "image/jpeg";
493 if (137 === e[0] && 80 === e[1] && 78 === e[2] && 71 === e[3]) return "image/png";
494 if (71 === e[0] && 73 === e[1] && 70 === e[2]) return "image/gif";
495 if (87 === e[8] && 69 === e[9] && 66 === e[10] && 80 === e[11]) return "image/webp";
496 let t = 73 === e[0] && 73 === e[1] && 42 === e[2] && 0 === e[3],
497 r = 77 === e[0] && 77 === e[1] && 0 === e[2] && 42 === e[3];
498 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
499 }
500
501 function s(e) {
502 return [e >>> 24 & 255, e >>> 16 & 255, e >>> 8 & 255, 255 & e]
503 }
504
505 function o(e) {
506 return [e >>> 21 & 127, e >>> 14 & 127, e >>> 7 & 127, 127 & e]
507 }
508
509 function c(e) {
510 return (e[0] << 21) + (e[1] << 14) + (e[2] << 7) + e[3]
511 }
512
513 function u(e) {
514 return 11 + e
515 }
516
517 function f(e) {
518 return 13 + 2 * e
519 }
520
521 function h(e, t) {
522 return 16 + 2 * e + 2 + 2 + 2 * t
523 }
524
525 function p(e, t, r, n) {
526 return 11 + t + 1 + 1 + (n ? 2 + 2 * (r + 1) : r + 1) + e
527 }
528
529 function l(e, t) {
530 return 16 + 2 * e + 2 + 2 + 2 * t
531 }
532
533 function g(e, t) {
534 return 13 + 2 * e + 2 + 2 + 2 * t
535 }
536
537 function m(e) {
538 return 10 + e
539 }
540 return function() {
541 function t(r) {
542 if (e(this, t), !(r && "object" == typeof r && "byteLength" in r)) throw new Error("First argument should be an instance of ArrayBuffer or Buffer");
543 this.arrayBuffer = r, this.padding = 4096, this.frames = [], this.url = ""
544 }
545 return t.prototype._setIntegerFrame = function(e, t) {
546 let r = parseInt(t, 10);
547 this.frames.push({
548 name: e,
549 value: r,
550 size: u(r.toString().length)
551 })
552 }, t.prototype._setStringFrame = function(e, t) {
553 let r = t.toString();
554 this.frames.push({
555 name: e,
556 value: r,
557 size: f(r.length)
558 })
559 }, t.prototype._setPictureFrame = function(e, t, r, n) {
560 let a = i(new Uint8Array(t)),
561 s = r.toString();
562 if (!a) throw new Error("Unknown picture MIME type");
563 r || (n = !1), this.frames.push({
564 name: "APIC",
565 value: t,
566 pictureType: e,
567 mimeType: a,
568 useUnicodeEncoding: n,
569 description: s,
570 size: p(t.byteLength, a.length, s.length, n)
571 })
572 }, t.prototype._setLyricsFrame = function(e, t) {
573 let r = e.toString(),
574 n = t.toString();
575 this.frames.push({
576 name: "USLT",
577 value: n,
578 description: r,
579 size: h(r.length, n.length)
580 })
581 }, t.prototype._setCommentFrame = function(e, t) {
582 let r = e.toString(),
583 n = t.toString();
584 this.frames.push({
585 name: "COMM",
586 value: n,
587 description: r,
588 size: l(r.length, n.length)
589 })
590 }, t.prototype._setUserStringFrame = function(e, t) {
591 let r = e.toString(),
592 n = t.toString();
593 this.frames.push({
594 name: "TXXX",
595 description: r,
596 value: n,
597 size: g(r.length, n.length)
598 })
599 }, t.prototype._setUrlLinkFrame = function(e, t) {
600 let r = t.toString();
601 this.frames.push({
602 name: e,
603 value: r,
604 size: m(r.length)
605 })
606 }, t.prototype.setFrame = function(e, t) {
607 switch (e) {
608 case "TPE1":
609 case "TCOM":
610 case "TCON":
611 if (!Array.isArray(t)) throw new Error(e + " frame value should be an array of strings");
612 let r = "TCON" === e ? ";" : "/",
613 n = t.join(r);
614 this._setStringFrame(e, n);
615 break;
616 case "TIT2":
617 case "TALB":
618 case "TPE2":
619 case "TPE3":
620 case "TPE4":
621 case "TRCK":
622 case "TPOS":
623 case "TMED":
624 case "TPUB":
625 this._setStringFrame(e, t);
626 break;
627 case "TBPM":
628 case "TLEN":
629 case "TYER":
630 this._setIntegerFrame(e, t);
631 break;
632 case "USLT":
633 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");
634 this._setLyricsFrame(t.description, t.lyrics);
635 break;
636 case "APIC":
637 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");
638 if (t.type < 0 || t.type > 20) throw new Error("Incorrect APIC frame picture type");
639 this._setPictureFrame(t.type, t.data, t.description, !!t.useUnicodeEncoding);
640 break;
641 case "TXXX":
642 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");
643 this._setUserStringFrame(t.description, t.value);
644 break;
645 case "TKEY":
646 if (!/^([A-G][#b]?m?|o)$/.test(t)) throw new Error(e + " frame value should be like Dbm, C#, B or o");
647 this._setStringFrame(e, t);
648 break;
649 case "WCOM":
650 case "WCOP":
651 case "WOAF":
652 case "WOAR":
653 case "WOAS":
654 case "WORS":
655 case "WPAY":
656 case "WPUB":
657 this._setUrlLinkFrame(e, t);
658 break;
659 case "COMM":
660 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");
661 this._setCommentFrame(t.description, t.text);
662 break;
663 default:
664 throw new Error("Unsupported frame " + e)
665 }
666 return this
667 }, t.prototype.removeTag = function() {
668 if (!(this.arrayBuffer.byteLength < 10)) {
669 let e = new Uint8Array(this.arrayBuffer),
670 t = e[3],
671 r = c([e[6], e[7], e[8], e[9]]) + 10;
672 !a(e) || t < 2 || t > 4 || (this.arrayBuffer = new Uint8Array(e.subarray(r)).buffer)
673 }
674 }, t.prototype.addTag = function() {
675 this.removeTag();
676 let e = [255, 254],
677 t = [101, 110, 103],
678 a = 10 + this.frames.reduce(function(e, t) {
679 return e + t.size
680 }, 0) + this.padding,
681 i = new ArrayBuffer(this.arrayBuffer.byteLength + a),
682 c = new Uint8Array(i),
683 u = 0,
684 f = [];
685 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) {
686 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) {
687 case "WCOM":
688 case "WCOP":
689 case "WOAF":
690 case "WOAR":
691 case "WOAS":
692 case "WORS":
693 case "WPAY":
694 case "WPUB":
695 f = r(a.value), c.set(f, u), u += f.length;
696 break;
697 case "TPE1":
698 case "TCOM":
699 case "TCON":
700 case "TIT2":
701 case "TALB":
702 case "TPE2":
703 case "TPE3":
704 case "TPE4":
705 case "TRCK":
706 case "TPOS":
707 case "TKEY":
708 case "TMED":
709 case "TPUB":
710 f = [1].concat(e), c.set(f, u), u += f.length, f = n(a.value), c.set(f, u), u += f.length;
711 break;
712 case "TXXX":
713 case "USLT":
714 case "COMM":
715 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;
716 break;
717 case "TBPM":
718 case "TLEN":
719 case "TYER":
720 u++, f = r(a.value), c.set(f, u), u += f.length;
721 break;
722 case "APIC":
723 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
724 }
725 }), u += this.padding, c.set(new Uint8Array(this.arrayBuffer), u), this.arrayBuffer = i, i
726 }, t.prototype.getBlob = function() {
727 return new Blob([this.arrayBuffer], {
728 type: "audio/mpeg"
729 })
730 }, t.prototype.getURL = function() {
731 return this.url || (this.url = URL.createObjectURL(this.getBlob())), this.url
732 }, t.prototype.revokeURL = function() {
733 URL.revokeObjectURL(this.url)
734 }, t
735 }()
736 })();
737 const FLACMetadataEditor = (() => {
738 "use strict";
739 const t = "0.0.2.1";
740 class VorbisComment extends Array {}
741 class VorbisCommentPacket {
742 _addComment(t) {
743 const e = t.split("=")[1];
744 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
745 }
746 toStringArray() {
747 const t = [];
748 return Object.keys(this).sort().forEach(e => {
749 this[e].forEach(s => {
750 t.push(e + "=" + s)
751 })
752 }), t
753 }
754 }
755 class FLACMetadataBlockData {}
756 class FLACMetadataBlock {
757 constructor() {
758 this.blockType = "", this.blockTypeNubmer = 0, this.blockSize = 0, this.data = new FLACMetadataBlockData, this.offset = 0
759 }
760 get serializedSize() {
761 switch (this.blockType) {
762 case "STREAMINFO":
763 return 34;
764 case "PADDING":
765 return this.blockSize;
766 case "APPLICATION":
767 return 4 + this.data.applicationData.length;
768 case "SEEKTABLE":
769 return 18 * this.data.points.length;
770 case "VORBIS_COMMENT":
771 const t = this.data.comments.toStringArray().reduce((t, e) => t + 4 + e.toUTF8().length, 0);
772 return 4 + this.data.vendorString.length + 4 + t;
773 case "CUESHEET":
774 return 0;
775 case "PICTURE":
776 return 8 + this.data.MIMEType.toUTF8().length + 4 + this.data.description.toUTF8().length + 4 + 4 + 4 + 4 + 4 + this.data.data.length
777 }
778 }
779 }
780 class FLACMetadataBlocks extends Array {}
781 class FLACMetadata {
782 constructor() {
783 this.blocks = new FLACMetadataBlocks, this.framesOffset = 0, this.signature = ""
784 }
785 }
786 return class {
787 get scriptVersion() {
788 return t
789 }
790 constructor(t) {
791 if (!(t && "object" == typeof t && "byteLength" in t)) throw new Error("First argument should be an instance of ArrayBuffer or Buffer");
792 return this.arrayBuffer = t, this.metadata = new FLACMetadata, String.prototype.toUTF8 = function(t = null) {
793 return (new TextEncoder).encode(t || this)
794 }, this._parseMetadata(), this
795 }
796 _getBytesAsNumber(t, e = 0, s = t.length - e) {
797 return Array.from(t.subarray(e, e + s)).reduce((t, e) => t = 256 * t + e, 0)
798 }
799 _getBytesAsNumberLittleEndian(t, e = 0, s = t.length - e) {
800 return Array.from(t.subarray(e, e + s)).reduceRight((t, e) => t = 256 * t + e, 0)
801 }
802 _getBytesAsHexString(t, e = 0, s = t.length - e) {
803 return Array.from(t.subarray(e, e + s)).map(t => (t >> 4).toString(16) + (15 & t).toString(16)).join("")
804 }
805 _getBytesAsUTF8String(t, e = 0, s = t.length - e) {
806 return (new TextDecoder).decode(t.subarray(e, e + s))
807 }
808 _getBlockType(t) {
809 switch (t) {
810 case 0:
811 return "STREAMINFO";
812 case 1:
813 return "PADDING";
814 case 2:
815 return "APPLICATION";
816 case 3:
817 return "SEEKTABLE";
818 case 4:
819 return "VORBIS_COMMENT";
820 case 5:
821 return "CUESHEET";
822 case 6:
823 return "PICTURE";
824 case 127:
825 return "invalid, to avoid confusion with a frame sync code";
826 default:
827 return "reserved"
828 }
829 }
830 _uint32ToUint8Array(t) {
831 return [t >>> 24 & 255, t >>> 16 & 255, t >>> 8 & 255, 255 & t]
832 }
833 _uint24ToUint8Array(t) {
834 return [t >>> 16 & 255, t >>> 8 & 255, 255 & t]
835 }
836 _uint16ToUint8Array(t) {
837 return [t >>> 8 & 255, 255 & t]
838 }
839 _hexStringToUint8Array(t) {
840 return t.replace(/(\w\w)/g, "$1,").slice(0, -1).split(",").map(t => (parseInt(t[0], 16) << 4) + parseInt(t[1], 16))
841 }
842 get _vorbisComment() {
843 const t = this.metadata.blocks.find(t => "VORBIS_COMMENT" === t.blockType);
844 if (t) return t.data
845 }
846 addComment(t, e = null) {
847 if (t) {
848 if (!e) {
849 const s = t.split("=");
850 if (!s[1]) return this;
851 e = s[1], t = s[0]
852 }
853 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())
854 }
855 return this
856 }
857 removeComment(t = null, e = null) {
858 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
859 }
860 getComment(t) {
861 return this._vorbisComment.comments[t.toUpperCase()]
862 }
863 addPicture(t) {
864 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');
865 t.data = new Uint8Array(t.data);
866 const e = {
867 APICtype: 3,
868 MIMEType: "image/jpeg",
869 colorDepth: 0,
870 colorNumber: 0,
871 data: new Uint8Array([]),
872 description: "",
873 width: 0,
874 height: 0
875 },
876 s = new FLACMetadataBlock;
877 s.blockTypeNubmer = 6, s.blockType = "PICTURE";
878 for (let r in e) t[r] ? s.data[r] = t[r] : s.data[r] = e[r];
879 const r = this.metadata.blocks;
880 let a = r.length;
881 return "PADDING" === r[r.length - 1].blockType && a--, r.splice(a, 0, s), this.metadata.blocks = r, this
882 }
883 _serializeMetadataBlock(t) {
884 const e = new Uint8Array(t.serializedSize),
885 s = t.data;
886 let r = 0;
887 switch (t.blockType) {
888 case "STREAMINFO":
889 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);
890 break;
891 case "PADDING":
892 break;
893 case "APPLICATION":
894 e.set(s.applicationID.toUTF8()), r += 4, e.set(s.applicationData, r);
895 break;
896 case "SEEKTABLE":
897 s.points.forEach(t => {
898 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
899 });
900 break;
901 case "VORBIS_COMMENT":
902 e.set(this._uint32ToUint8Array(s.vendorString.toUTF8().length).reverse(), r), r += 4, e.set(s.vendorString.toUTF8(), r), r += s.vendorString.toUTF8().length;
903 const a = s.comments.toStringArray();
904 e.set(this._uint32ToUint8Array(a.length).reverse(), r), r += 4, a.forEach(t => {
905 e.set(this._uint32ToUint8Array(t.toUTF8().length).reverse(), r), r += 4, e.set(t.toUTF8(), r), r += t.toUTF8().length
906 });
907 break;
908 case "CUESHEET":
909 break;
910 case "PICTURE":
911 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)
912 }
913 return e
914 }
915 serializeMetadata() {
916 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),
917 e = new Uint8Array(t);
918 e.set(this.metadata.signature.toUTF8());
919 let s = 4,
920 r = !1;
921 return this.metadata.blocks.forEach((t, a, i) => {
922 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
923 }), e.set(new Uint8Array(this.arrayBuffer).subarray(this.metadata.framesOffset), s), this.arrayBuffer = e.buffer, this
924 }
925 _parseMetadataBlock(t, e, s, r) {
926 const a = t.subarray(e, e + r);
927 let i = 0;
928 const n = new FLACMetadataBlockData;
929 switch (s) {
930 case "STREAMINFO":
931 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();
932 break;
933 case "PADDING":
934 break;
935 case "APPLICATION":
936 n.applicationID = this._getBytesAsUTF8String(a, i, 4), i += 4, n.applicationData = a.subarray(i);
937 break;
938 case "SEEKTABLE":
939 n.pointCount = r / 18, n.points = [];
940 for (let t = 0; t < n.pointCount; t++) n.points.push({
941 sampleNumber: this._getBytesAsHexString(a, i, 8),
942 offset: this._getBytesAsHexString(a, i + 8, 8),
943 numberOfSamples: this._getBytesAsHexString(a, i + 16, 2)
944 }), i += 18;
945 break;
946 case "VORBIS_COMMENT":
947 const t = this._getBytesAsNumberLittleEndian(a, i, 4);
948 i += 4, n.vendorString = this._getBytesAsUTF8String(a, i, t), i += t;
949 const e = this._getBytesAsNumberLittleEndian(a, i, 4);
950 i += 4, n.comments = new VorbisCommentPacket;
951 let o = 0;
952 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;
953 break;
954 case "CUESHEET":
955 break;
956 case "PICTURE":
957 n.APICtype = this._getBytesAsNumber(a, i, 4), i += 4;
958 const h = this._getBytesAsNumber(a, i, 4);
959 i += 4, n.MIMEType = this._getBytesAsUTF8String(a, i, h), i += h;
960 const c = this._getBytesAsNumber(a, i, 4);
961 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;
962 const m = this._getBytesAsNumber(a, i, 4);
963 i += 4, n.data = a.subarray(i, i + m)
964 }
965 return n
966 }
967 _parseMetadata() {
968 const t = new Uint8Array(this.arrayBuffer);
969 this.metadata.signature = this._getBytesAsUTF8String(t, 0, 4);
970 let e, s = 4,
971 r = !1,
972 a = 0;
973 for (; !r && s < t.length;) {
974 if (a++ > 42) throw new RangeError("Too much METADATA_BLOCKS. Looks like file corrupted");
975 (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)
976 }
977 return this.metadata.framesOffset = s, this
978 }
979 }
980 })();
981 /* jshint ignore: end */
982
983 // Inner, in worker scope debug. Rewrites later
984 let DEBUG = true;
985 let coverSize = 1200;
986 let coverQuality = 80;
987
988 this.onmessage = ({
989 data: data
990 }) => {
991 if (data.hasOwnProperty('debug')) {
992 DEBUG = data.debug;
993 coverSize = data.coverSize;
994 coverQuality = data.coverQuality;
995 return true;
996 }
997
998 if (DEBUG) console.info('Worker got message:', data);
999
1000 const url = data.url;
1001 const key = data.key;
1002 const trackData = data.trackData;
1003 const tags = data.tags;
1004 const azLyrics = data.azLyrics;
1005 const albumData = data.albumData;
1006
1007 // config: url, responseType, onProgress, onSuccess, anyway
1008 function niceXhr(config) {
1009 const xhr = new XMLHttpRequest();
1010 xhr.responseType = config.responseType;
1011 xhr.onload = e => {
1012 if (200 === e.target.status)
1013 config.onSuccess(e.target.response);
1014 else
1015 console.warn('Error with getting data from', e.target.responseURL);
1016 config.anyway();
1017 };
1018 xhr.onerror = config.anyway;
1019 xhr.onabort = config.anyway;
1020 xhr.onprogress = config.onProgress;
1021 xhr.open('GET', config.url);
1022 xhr.send();
1023 }
1024
1025 postMessage([trackData, '^waiting']);
1026
1027 niceXhr({
1028 url: url,
1029 responseType: 'arraybuffer',
1030 onProgress: e => {
1031 const toMB = bytes => (bytes / 1024 / 1024).toFixed(2);
1032 const loaded = toMB(e.loaded);
1033 if (e.lengthComputable) {
1034 const total = toMB(e.total);
1035 const percent = Math.floor(100 * e.loaded / e.total);
1036 postMessage([trackData, `^downloading ${loaded}/${total}^mb : ${percent}%`]);
1037 } else {
1038 postMessage([trackData, `^downloading ${loaded}^mb`]);
1039 }
1040 },
1041 onSuccess: response => {
1042 postMessage([trackData, '^decrypting']);
1043 let data = new Uint8Array(response);
1044 const bf = new Blowfish(key);
1045 const iv = new Uint8Array([0, 1, 2, 3, 4, 5, 6, 7]);
1046 const L = data.length;
1047 if (DEBUG) console.info('Data length:', data.length);
1048 for (let i = 0; i < L; i += 6144) {
1049 if (i % (6144 * 20) === 6144 * 19) { // let it display state at every 120K
1050 const percent = Math.floor(i * 100.0 / L);
1051 postMessage([trackData, `^decrypting ${percent}%`]);
1052 }
1053 if (i + 2048 <= L) {
1054 let D = data.subarray(i, i + 2048);
1055 // if (i===0) console.log(D.toString(), D.length);
1056 bf.decryptCBC(D, iv);
1057 data.set(D, i);
1058 }
1059 }
1060
1061 function binArr2utfStr(arr) {
1062 return Array.from(arr).map(n => String.fromCharCode(n)).join('');
1063 }
1064 // According https://en.wikipedia.org/wiki/List_of_file_signatures
1065 const isMP3 = (data[0] === 0xFF && data[1] === 0xFB) || 'ID3' === binArr2utfStr(data.subarray(0, 3));
1066 const isFLAC = 'fLaC' === binArr2utfStr(data.subarray(0, 4));
1067
1068 if (!(isMP3 || isFLAC)) return console.warn('Decrypted file not MP3 nor FLAC!', data);
1069
1070 let writer;
1071 if (isMP3) {
1072 writer = new ID3Writer(data);
1073 writer.setFrame('TIT2', tags.title)
1074 .setFrame('TPE1', tags._artistsArray)
1075 .setFrame('TALB', tags.album);
1076 // TODO: wait answer: https://github.com/egoroof/browser-id3-writer/issues/52
1077 // .setFrame('TSRC', tags.isrc)
1078 if (tags.track)
1079 writer.setFrame('TRCK', tags.track);
1080
1081 if (albumData.label)
1082 writer.setFrame('TPUB', albumData.label);
1083 if (albumData.release_date)
1084 writer.setFrame('TYER', albumData.release_date);
1085
1086 if (azLyrics.lyrics)
1087 writer.setFrame('USLT', {
1088 description: '',
1089 lyrics: azLyrics.lyrics
1090 });
1091 // if (azLyrics.year)
1092 // writer.setFrame('TYER', azLyrics.year);
1093 }
1094 if (isFLAC) {
1095 writer = new FLACMetadataEditor(data);
1096 writer.removeComment()
1097 .addComment('ALBUM', tags.album)
1098 .addComment('TITLE', tags.title)
1099 .addComment('ISRC', tags.isrc);
1100 tags._artistsArray.forEach(artist => writer.addComment('ARTIST', artist));
1101 if (tags.track)
1102 writer.addComment('TRACKNUMBER', tags.track);
1103
1104 if (albumData.label)
1105 writer.addComment('ORGANIZATION', albumData.label);
1106 if (albumData.release_date)
1107 writer.addComment('DATE', albumData.release_date);
1108
1109 // At first i thought that GAIN is for ReplayGain, but my player returns other values,
1110 // So now i don't know what is it ~
1111 // if (tags.gain) writer.addComment('REPLAYGAIN_TRACK_GAIN', `${tags.replayGain} dB`);
1112 if (azLyrics.lyrics)
1113 writer.addComment('LYRICS', azLyrics.lyrics);
1114 // if (azLyrics.year)
1115 // writer.addComment('DATE', azLyrics.year);
1116 }
1117
1118 // Downloading cover
1119 const coverLink = `https://e-cdns-images.dzcdn.net/images/cover/${tags.cover}/${coverSize}x${coverSize}-000000-${coverQuality}-0-0.jpg`;
1120 if (DEBUG) console.info('Trying to download', coverLink);
1121 postMessage([trackData, '^coverDownloading']);
1122 niceXhr({
1123 url: coverLink,
1124 responseType: 'arraybuffer',
1125 onProgress: e => {
1126 const toMB = bytes => (bytes / 1024 / 1024).toFixed(2);
1127 const loaded = toMB(e.loaded);
1128 if (e.lengthComputable) {
1129 const total = toMB(e.total);
1130 const percent = Math.floor(100 * e.loaded / e.total);
1131 postMessage([trackData, `^downloading ${loaded}/${total}^mb : ${percent}%`]);
1132 } else {
1133 postMessage([trackData, `^downloading ${loaded}^mb`]);
1134 }
1135 },
1136 onSuccess: imageData => {
1137 if (isMP3) {
1138 writer.setFrame('APIC', {
1139 type: 3,
1140 data: imageData,
1141 description: ''
1142 });
1143 }
1144 if (isFLAC) {
1145 writer.addPicture({
1146 data: imageData,
1147 colorDepth: 24,
1148 width: coverSize,
1149 height: coverSize
1150 });
1151 }
1152 },
1153 anyway: () => {
1154 if (isMP3)
1155 writer.addTag();
1156 if (isFLAC)
1157 writer.serializeMetadata();
1158 data = writer.arrayBuffer;
1159
1160 const blobUrl = URL.createObjectURL(new Blob([data], {
1161 type: (isMP3 ? 'audio/mpeg' : isFLAC ? 'audio/flac' : 'audio/mpeg')
1162 }));
1163
1164 if (DEBUG) console.info('Blob for "%s - %s" created: %s', tags.artist, tags.title, blobUrl);
1165 postMessage([trackData, 'DONE', blobUrl]);
1166 }
1167 });
1168 },
1169 anyway: () => {}
1170 });
1171
1172 };
1173
1174}); //End var mainWk
1175
1176// Set debug and other values in worker like in script
1177mainWk.postMessage({
1178 debug: DEBUG,
1179 coverSize: coverSize,
1180 coverQuality: coverQuality,
1181});
1182
1183// azLyrics parser
1184function azLyrics(artist, title, onsuccess, anyway) {
1185 const url = formatAzLyricsUrl(artist, title);
1186 if (!url) return anyway();
1187
1188 const data = {
1189 method: 'GET',
1190 url: url,
1191 onload: e => {
1192 if (200 === e.status)
1193 onsuccess(parseAzLyricsPage(e.response));
1194 else
1195 console.warn('Cannot grab lyrics from', e.finalUrl);
1196 anyway();
1197 },
1198 onerror: anyway,
1199 onabort: anyway
1200 };
1201
1202 // Greasemonkey with their 4.0 API became a monster ~
1203 if (typeof GM !== 'undefined' && typeof GM.xmlHttpRequest !== 'undefined')
1204 GM.xmlHttpRequest(data);
1205 else
1206 GM_xmlhttpRequest(data);
1207}
1208
1209function parseAzLyricsPage(text) {
1210 const azPage = new DOMParser().parseFromString(text, 'text/html');
1211 const data = {};
1212 const divs = Array.from(azPage.getElementsByTagName('div'));
1213 const lyricsDiv = divs.find(div => div.innerHTML.substr(1, 4) === '<!--');
1214 const lyrics = lyricsDiv.textContent;
1215 data.lyrics = lyrics.replace(/^\s*/, '').replace(/\s*$/, ''); // clean spaces and new lines in begin and end
1216
1217 const album = azPage.querySelector('.album-panel a[data-toggle]');
1218 if (album) {
1219 const matches = album.textContent.match(/"([^"]+)" \((\d+)\)/);
1220 data.album = matches[1];
1221 data.year = +matches[2];
1222 }
1223 return data;
1224}
1225
1226function formatAzLyricsUrl(artist, title) {
1227 const partOfUrl = [artist, title].map(el => el.toLowerCase().replace(/[^\w]/g, '')).join('/').replace(/^the/, '');
1228 if (partOfUrl === '/') return false;
1229 return `https://www.azlyrics.com/lyrics/${partOfUrl}.html`;
1230}
1231
1232// DOWNLOADER LOGIC: URL ENCRYPTION
1233function zeroPad(data) {
1234 const aesBS = 16;
1235 const l = data.length;
1236 if (l % aesBS !== 0)
1237 data += '\0'.repeat(aesBS - (l % aesBS));
1238 return data;
1239}
1240
1241function encryptURL(track, fmt) {
1242 const urlsep = '\xa4';
1243 const key = aesjs.utils.utf8.toBytes('jo6aey6haid2Teih');
1244 const cryptor = new aesjs.ModeOfOperation.ecb(key);
1245
1246 let steps = [track.MD5_ORIGIN, fmt, track.SNG_ID, track.MEDIA_VERSION].join(urlsep);
1247 steps = [hex_md5(steps), steps, ''].join(urlsep);
1248 steps = zeroPad(steps);
1249 // This not works because it translates `urlsep' as two symbols
1250 // steps = aesjs.utils.utf8.toBytes(steps);
1251 // That's why we use this:
1252 steps = steps.split('').map(c => c.charCodeAt(0)); // str2bin
1253 steps = cryptor.encrypt(steps);
1254 steps = aesjs.utils.hex.fromBytes(steps);
1255 return steps;
1256}
1257
1258// DOWNLOADER LOGIC: GLOBAL VARIABLES AND HELPER METHODS
1259const fmtMp3_128 = 1,
1260 fmtMp3_320 = 3,
1261 fmtFLAC = 9;
1262const trackDB = {};
1263const urlDB = {};
1264if (!localStorage.dlNameProto)
1265 unsafeWindow.localStorage.dlNameProto = '%Artists - %Title';
1266if (!localStorage.dlAzLyrics)
1267 unsafeWindow.localStorage.dlAzLyrics = true;
1268
1269function generateName(track, format = null) {
1270 const tags = dzTags(track);
1271 const nameProto = elNameProto.value;
1272
1273 let name = nameProto.replace(/%(\w+)/g, (full, keyword) => {
1274 let replacement = '';
1275
1276 if (/^n+$/i.test(keyword)) {
1277 if (!tags.track) return '';
1278 // Replace `NNNNN' to track number padded with zeroes
1279 let rept = keyword.length - (tags.track).toString().length;
1280 if (rept < 0) rept = 0;
1281 return '0'.repeat(rept) + tags.track;
1282
1283 } else if (tags.hasOwnProperty(keyword.toLowerCase())) {
1284 // Finded in dzTags values
1285 replacement = tags[keyword.toLowerCase()];
1286 if (!replacement) return '';
1287
1288 if (keyword === keyword.toLowerCase())
1289 replacement = replacement.toLowerCase();
1290 if (keyword === keyword.toUpperCase())
1291 replacement = replacement.toUpperCase();
1292 return replacement;
1293
1294 } else {
1295 return full;
1296 }
1297 });
1298
1299 // remove escaping slashes
1300 name = name.replace(/\\%/g, '%');
1301 // remove trailing and starting spaces
1302 name = name.replace(/\s*(.*[^\s])\s*/, '$1');
1303
1304 if (format) {
1305 if (format === fmtFLAC)
1306 name += '.flac';
1307 else
1308 name += '.mp3';
1309 }
1310 return name;
1311}
1312
1313function updateNames() {
1314 const trackNameEls = document.querySelectorAll('.dlTrackName');
1315 trackNameEls.forEach(nameEl => {
1316 const nbsp = '\u00A0';
1317 const trackId = nameEl.closest('.dlTrack').dataset.trackId;
1318 nameEl.textContent = generateName(trackDB[trackId]).replace(/\s/g, nbsp);
1319 });
1320}
1321
1322// CONVERT TIME into min:sec
1323function formatTime(seconds) {
1324 const minutes = Math.floor(seconds / 60);
1325 seconds %= 60;
1326 const zeroPad = seconds < 10 ? '0' : '';
1327 return `${minutes}:${zeroPad}${seconds}`;
1328}
1329
1330// FILES TAGS
1331function dzTags(trackInfo) {
1332 // `new Set' is needed to unique atrists. e.g. https://deezer.com/album/6883271
1333 const artistsArray = trackInfo.ARTISTS && GM_info.scriptHandler !== 'Greasemonkey' ? [...new Set(trackInfo.ARTISTS.map(a => a.ART_NAME))] : [trackInfo.ART_NAME];
1334 return {
1335 artist: trackInfo.ART_NAME,
1336 _artistsArray: artistsArray,
1337 artists: artistsArray.join(', '),
1338 album: trackInfo.ALB_TITLE,
1339 album_id: trackInfo.ALB_ID,
1340 cover: trackInfo.ALB_PICTURE,
1341 disk: trackInfo.DISK_NUMBER,
1342 // by name, it's have to be ReplayGain value. But in fact, this is something other ~
1343 // looks like https://en.wikipedia.org/wiki/Audio_normalization#Peak_normalization
1344 // http://wiki.hydrogenaud.io/index.php?title=ReplayGain
1345 gain: trackInfo.GAIN,
1346 id: trackInfo.SNG_ID,
1347 // ISO 3901:1986 International Standard Recording Code (ISRC). Technical committee / subcommittee: TC 46 / SC 9
1348 isrc: trackInfo.ISRC,
1349 time: formatTime(trackInfo.DURATION),
1350 title: trackInfo.SNG_TITLE,
1351 track: trackInfo.TRACK_NUMBER,
1352 version: trackInfo.VERSION
1353 };
1354}
1355
1356const bfGK = 'g4el58wc0zvf9na1';
1357
1358function bfGenKey(id) {
1359 const hash = hex_md5(id.toString());
1360 const key = new Uint8Array(16);
1361 for (let i = 0; i < 16; i++)
1362 key[i] = bfGK.charCodeAt(i) ^ hash.charCodeAt(i) ^ hash.charCodeAt(i + 16);
1363 return key;
1364}
1365
1366
1367function fileSizeToMb(aSize) {
1368 return isNaN(aSize) ? 0 : Math.round(aSize / 1024 / 1024 * 10) / 10;
1369}
1370
1371// DOWNLOAD ENTRY POINT
1372function dzDownload(track, trackId, fmt) {
1373 const tags = dzTags(track);
1374 const trackData = {
1375 trackId: trackId,
1376 fmt: fmt
1377 };
1378 let albumData = {};
1379 let azLyricsData = {};
1380
1381 function processTrack() {
1382 mainWk.postMessage({
1383 url: `https://e-cdns-proxy-${track.MD5_ORIGIN[0]}.dzcdn.net/mobile/1/${encryptURL(track, fmt)}`,
1384 key: bfGenKey(track.SNG_ID),
1385 trackData: trackData,
1386 tags: dzTags(track),
1387 azLyrics: azLyricsData,
1388 albumData: albumData
1389 });
1390 }
1391
1392 function getAlbumData() {
1393 messageProcessor({
1394 data: [trackData, translate.gettingAlbumInfo]
1395 });
1396 niceXhr({
1397 url: `https://api.deezer.com/album/${tags.album_id}`,
1398 responseType: 'json',
1399 onProgress: {},
1400 onSuccess: json => {
1401 albumData.label = json.label;
1402 albumData.release_date = json.release_date;
1403 },
1404 anyway: processTrack
1405 });
1406 }
1407
1408 if (elAzLyrics.checked) {
1409 messageProcessor({
1410 data: [trackData, translate.lyricsDownloading]
1411 });
1412
1413 azLyrics(tags.artist, tags.title,
1414 data => {
1415 if (DEBUG) console.info(data);
1416 azLyricsData = data;
1417 },
1418 getAlbumData
1419 );
1420 } else {
1421 getAlbumData();
1422 }
1423
1424}
1425
1426// DOWNLOADER WORKER CALLBACK
1427function messageProcessor(msg) {
1428 const fmt = +msg.data[0].fmt;
1429 const trackId = msg.data[0].trackId;
1430 let trackEl = document.querySelector(`.dlTrack[data-track-id="${trackId}"]`);
1431 if (!trackEl)
1432 console.warn('Downloaded track not found in tracklist!');
1433
1434 const state = msg.data[1];
1435
1436 switch (state) {
1437 case 'DONE':
1438 if (!urlDB[trackId]) urlDB[trackId] = {};
1439 urlDB[trackId][fmt] = msg.data[2];
1440
1441 let anchor;
1442 if (trackEl) {
1443 anchor = trackEl.querySelector(`.dlLink[data-fmt="${fmt}"]`);
1444 } else {
1445 // create new temporary link element if container not found
1446 anchor = document.head.appendChild(document.createElement('a'));
1447 }
1448 anchor.download = generateName(trackDB[trackId], fmt);
1449 anchor.href = msg.data[2];
1450 anchor.click();
1451 // remove temporary link
1452 if (!trackEl) anchor.remove();
1453 break;
1454 case 'ABORT':
1455 console.error('Download abort:', trackId, fmt);
1456 break;
1457 case 'ERROR':
1458 console.error('Download ERROR!', trackId, fmt);
1459 break;
1460 default:
1461 const message = translateCircumstring(state);
1462 if (trackEl) {
1463 trackEl.querySelector('.dlTrackLinks').classList.add('hidden');
1464 trackEl.querySelector('.dlTrackStatus').classList.remove('hidden');
1465 trackEl.querySelector('.dlTrackStatus').textContent = message;
1466 } else {
1467 const tags = dzTags(trackDB[trackId]);
1468 console.info('%s - %s: %s', tags.artist, tags.title, message);
1469 }
1470 return;
1471 }
1472 if (trackEl) {
1473 trackEl.querySelector('.dlTrackLinks').classList.remove('hidden');
1474 trackEl.querySelector('.dlTrackStatus').classList.add('hidden');
1475 }
1476}
1477mainWk.onmessage = messageProcessor;
1478
1479// DOWNLOADER LOGIC: HTML GENERATOR
1480function getFilesize(track, fmt_name) {
1481 const sizeProp = `FILESIZE_${fmt_name}`;
1482 let size = 0;
1483 if (track[sizeProp] && !isNaN(track[sizeProp]))
1484 size = +track[sizeProp];
1485 return size;
1486}
1487
1488function generateDlLink(track, fmt_name, fmt_id, size) {
1489 const trackId = track.SNG_ID;
1490 const el = document.createElement('a');
1491 el.classList.add('dlLink');
1492 el.dataset.fmt = fmt_id;
1493 el.textContent = `⬇ ${fmt_name}`;
1494 //el.title = `${fileSizeToMb(size).toLocaleString()} ${translate.mb}`;
1495 if (urlDB[trackId] && urlDB[trackId][fmt_id]) {
1496 el.download = generateName(trackDB[trackId], fmt_id);
1497 el.href = urlDB[trackId][fmt_id];
1498 }
1499 el.onclick = function() {
1500 if (this.href)
1501 return true;
1502
1503 const trackId = this.closest('.dlTrack').dataset.trackId;
1504 const fmt = this.dataset.fmt;
1505 dzDownload(trackDB[trackId], trackId, fmt);
1506 return false;
1507 };
1508 return el;
1509}
1510
1511function generateDlTrack(track) {
1512 const tags = dzTags(track);
1513 const infosTooltips =
1514 '^artist:\t' + tags.artist +
1515 '\n^title:\t' + tags.title +
1516 '\n^album:\t' + tags.album + // disk number sometimes not recognized when change track playing ????
1517 '\n^duration:\t' + tags.time;
1518
1519 const trackEl = document.createElement('div');
1520 trackEl.classList.add('dlTrack');
1521 trackEl.dataset.trackId = tags.id;
1522
1523 const nameEl = document.createElement('span');
1524 nameEl.classList.add('dlTrackName');
1525 nameEl.title = translateCircumstring(infosTooltips);
1526
1527 const linksEl = document.createElement('div');
1528 linksEl.classList.add('dlTrackLinks');
1529 if (showMp3_128)
1530 if (getFilesize(track, 'MP3_128') > 0) linksEl.appendChild(generateDlLink(track, 'mp3 128', fmtMp3_128, getFilesize(track, 'MP3_128')));
1531 if (showMp3_320)
1532 if (getFilesize(track, 'MP3_320') === 0) linksEl.appendChild(generateDlLink(track, 'mp3 320', fmtMp3_320, getFilesize(track, 'MP3_320')));
1533 if (showFLAC)
1534 if (getFilesize(track, 'FLAC') === 0) linksEl.appendChild(generateDlLink(track, 'flac', fmtFLAC, getFilesize(track, 'FLAC')));
1535
1536 const statusEl = document.createElement('div');
1537 statusEl.classList.add('dlTrackStatus', 'hidden');
1538
1539 const endFloat = document.createElement('div');
1540 endFloat.classList.add('dlEndFloat');
1541
1542 trackEl.appendChild(nameEl);
1543 trackEl.appendChild(linksEl);
1544 trackEl.appendChild(statusEl);
1545 trackEl.appendChild(endFloat);
1546 return trackEl;
1547}
1548
1549function getTrackListByHand() {
1550 const links = Array.from(document.querySelectorAll('[data-instance=track_name]'));
1551 const albums = links.map(el => el.href.match(/\d+/)[0]);
1552 const names = links.map(el => el.textContent);
1553
1554 if (DEBUG) console.info('trackDB:: length:', Object.keys(trackDB).length, 'db:', trackDB);
1555
1556 const result = [];
1557 for (let i = 0; i < links.length; i++) {
1558 Object.keys(trackDB).forEach(trackId => {
1559 if (trackDB[trackId].ALB_ID === albums[i])
1560 if (trackDB[trackId].SNG_TITLE === names[i] || `${trackDB[trackId].SNG_TITLE} ${trackDB[trackId].VERSION}` === names[i])
1561 result.push(trackDB[trackId]);
1562 });
1563 }
1564
1565 if (DEBUG) console.info('filtered', result);
1566
1567 return result;
1568}
1569
1570function getTrackListIndexByHand() {
1571 const links = Array.from(document.querySelectorAll('[data-instance=track_name]'));
1572 const names = links.map(el => el.textContent);
1573 let currentIndex = -1;
1574 const currentEl = document.querySelector('.player-track-link');
1575 if (currentEl)
1576 currentIndex = names.indexOf(currentEl.textContent);
1577 return currentIndex;
1578}
1579
1580// ENTRY POINTS
1581function refreshTracklist() {
1582 if (DEBUG) console.info('Refreshing Tracklist');
1583
1584 while (divTracklist.firstElementChild)
1585 divTracklist.firstElementChild.remove();
1586
1587 let list, current;
1588 if (unsafeWindow.dzPlayer) {
1589 if (DEBUG) console.info('Used native `dzPlayer\'');
1590 // In fact, this one is already array, but Firefox+Greasemonkey don't believe it
1591 list = Array.from(unsafeWindow.dzPlayer.getTrackList());
1592 current = unsafeWindow.dzPlayer.getTrackListIndex();
1593 } else {
1594 if (DEBUG) console.info('Tracks are getting manually. Be careful with result');
1595 // Do some magic
1596 list = getTrackListByHand();
1597 current = getTrackListIndexByHand();
1598 }
1599 list.forEach((track, index) => {
1600 trackDB[track.SNG_ID] = track;
1601
1602 const trackDiv = generateDlTrack(track);
1603 if (index === current)
1604 trackDiv.classList.add('current');
1605 divTracklist.appendChild(trackDiv);
1606 });
1607
1608 updateNames();
1609}
1610
1611// THE DOWNLOADER PANEL
1612const rootEl = document.createElement('div');
1613rootEl.classList.add('dzHidden');
1614rootEl.id = 'dzDownloader';
1615
1616const elNameProto = document.createElement('input');
1617elNameProto.classList.add('dlNameProto');
1618elNameProto.title = `${translate.choose} ${translate.fileNaming}`;
1619elNameProto.value = localStorage.dlNameProto;
1620['change', 'keyup', 'keydown'].forEach(event => {
1621 elNameProto.addEventListener(event, () => {
1622 unsafeWindow.localStorage.dlNameProto = elNameProto.value;
1623 updateNames();
1624 });
1625});
1626
1627const forceAzLyrics = false;
1628
1629const elInsert = document.createElement('span');
1630elInsert.classList.add('dlInsert');
1631elInsert.addEventListener('mouseenter', () => elNameProto.focus());
1632
1633const elInsertButton = document.createElement('button');
1634elInsertButton.classList.add('dlInsertButton');
1635elInsertButton.innerHTML = '<img src="https://i.imgur.com/6IkahnK.png" />';
1636elInsertButton.title = `Case of tags
1637
1638Upper-case, proper-case and lower-case can be used.
1639
1640Example:
1641
1642%ARTIST\t= THE BEATLES
1643%Artist\t= The Beatles
1644%artist\t= the beatles
1645
1646%TITLE\t= LET IT BE
1647%Title\t= Let It Be
1648%title\t= let it be
1649
1650%Artists - %Title = The Beatles - Let It Be
1651----------------------------------------------------------
1652Padding
1653
1654Padding refers to adding zeros before the first digit.
1655
1656Example:
1657
1658%N\t= 1
1659%NN\t= 01
1660%NNN\t= 001
1661%NNNN\t= 0001`;
1662
1663const elInsertTable = document.createElement('table');
1664elInsertTable.classList.add('dlInsertTable');
1665
1666function addInsertVariant(value) {
1667 const row = elInsertTable.insertRow();
1668 const title = row.insertCell();
1669 title.textContent = value;
1670
1671 if ('---' === value) return;
1672
1673 row.addEventListener('click', () => {
1674 const sel = {
1675 start: elNameProto.selectionStart,
1676 end: elNameProto.selectionEnd,
1677 direction: elNameProto.selectionDirection,
1678 };
1679 const src = elNameProto.value;
1680 elNameProto.value = src.substr(0, elNameProto.selectionStart) + '%' + value + src.substr(elNameProto.selectionEnd);
1681 if (sel.start === src.length) {
1682 elNameProto.selectionStart = elNameProto.selectionEnd = elNameProto.value.length;
1683 } else {
1684 elNameProto.selectionStart = sel.start;
1685 elNameProto.selectionEnd = sel.end;
1686 }
1687 elNameProto.selectionDirection = sel.direction;
1688 elNameProto.dispatchEvent(new Event('change'));
1689 elNameProto.focus();
1690 });
1691}
1692// strings
1693addInsertVariant('Artist');
1694addInsertVariant('Artists');
1695addInsertVariant('Album');
1696addInsertVariant('Title');
1697addInsertVariant('Version');
1698addInsertVariant('__________');
1699// numbers
1700addInsertVariant('disk');
1701addInsertVariant('time');
1702addInsertVariant('isrc');
1703addInsertVariant('NN');
1704addInsertVariant('__________');
1705// Deezer-specific
1706addInsertVariant('cover');
1707addInsertVariant('gain');
1708addInsertVariant('id');
1709addInsertVariant('album_id');
1710
1711elInsert.appendChild(elInsertButton);
1712elInsert.appendChild(elInsertTable);
1713
1714const elRefresh = document.createElement('a');
1715elRefresh.classList.add('dlRefresh');
1716elRefresh.textContent = `${translate.refresh} ${translate.tracklist.toLowerCase()}`;
1717elRefresh.addEventListener('click', refreshTracklist);
1718
1719const elTrackListHeader = document.createElement('p');
1720elTrackListHeader.classList.add('dlTitle');
1721elTrackListHeader.textContent = `♪ ${translate.tracklist}`;
1722
1723if (forceAzLyrics)
1724 unsafeWindow.localStorage.dlAzLyrics = forceAzValue;
1725
1726const elAzLyrics = document.createElement('input');
1727elAzLyrics.type = 'checkbox';
1728elAzLyrics.classList.add('dlDoAzLyrics');
1729elAzLyrics.id = 'dlDoAzLyrics';
1730
1731const elAzLyricsLabel = document.createElement('label');
1732elAzLyricsLabel.setAttribute('for', 'dlDoAzLyrics');
1733elAzLyrics.classList.add('dlDoAzLyricsLabel');
1734elAzLyricsLabel.textContent = ` azLyrics`;
1735
1736const elListDownloadSelect = document.createElement('select');
1737elListDownloadSelect.classList.add('dlListDownloadSelect');
1738elListDownloadSelect.innerHTML =
1739 (!showMp3_128 ? '' : `<option value="${fmtMp3_128}">mp3 128</option>`) +
1740 (!showMp3_320 ? '' : `<option value="${fmtMp3_320}">mp3 320</option>`) +
1741 (!showFLAC ? '' : `<option value="${fmtFLAC}" >flac</option>`);
1742elListDownloadSelect.value = localStorage.dlListDownload;
1743elListDownloadSelect.addEventListener('change', () => {
1744 unsafeWindow.localStorage.dlListDownload = elListDownloadSelect.value;
1745});
1746
1747const elListDownloadButton = document.createElement('button');
1748elListDownloadButton.classList.add('dlListDownloadButton');
1749elListDownloadButton.textContent = translate.downloadList;
1750elListDownloadButton.addEventListener('click', () => {
1751 const selector = `.dlLink[data-fmt="${elListDownloadSelect.value}"]`;
1752 document.querySelectorAll(selector).forEach(anchor => anchor.click());
1753});
1754
1755if (showAzLyrics) {
1756 elTrackListHeader.appendChild(elAzLyrics);
1757 elTrackListHeader.appendChild(elAzLyricsLabel);
1758}
1759if (showListDownloader) {
1760 elTrackListHeader.appendChild(elListDownloadSelect);
1761 elTrackListHeader.appendChild(elListDownloadButton);
1762}
1763
1764const divTracklist = document.createElement('div');
1765divTracklist.classList.add('dlTrackList');
1766
1767rootEl.appendChild(elNameProto);
1768rootEl.appendChild(elInsert);
1769rootEl.appendChild(elRefresh);
1770if (showAzLyrics || showListDownloader)
1771 rootEl.appendChild(elTrackListHeader);
1772rootEl.appendChild(divTracklist);
1773
1774document.body.appendChild(rootEl);
1775
1776const triggerEl = document.createElement('div');
1777triggerEl.classList.add('dlTrigger');
1778triggerEl.title = translate.clickToOpen;
1779triggerEl.textContent = 'D➲wnloader ►';
1780triggerEl.addEventListener('click', () => {
1781 if (rootEl.classList.contains('dzHidden')) {
1782 triggerEl.textContent = 'D➲wnloader ◄';
1783 } else {
1784 triggerEl.textContent = 'D➲wnloader ►';
1785 }
1786 rootEl.classList.toggle('dzHidden');
1787});
1788
1789document.body.appendChild(triggerEl);
1790
1791
1792function hidePannel() {
1793 if (!rootEl.classList.contains('dzHidden'))
1794 triggerEl.click();
1795}
1796
1797// Hotkey handling
1798addEventListener('keydown', e => {
1799 switch (e.code) {
1800 case 'Escape':
1801 hidePannel();
1802 break;
1803 case 'KeyD':
1804 if (document.activeElement.nodeName !== 'INPUT')
1805 triggerEl.click();
1806 break;
1807 default:
1808 if (SHOW_KEYS) console.info(`Key with code "${e.code}" was pressed.`);
1809 }
1810});
1811
1812// Hide trigger when Deezer interface loading
1813// This element will be removed later
1814const delayStyleElement = document.head.appendChild(document.createElement('style'));
1815
1816function onInterfaceLoaded() {
1817 delayStyleElement.remove();
1818
1819 document.getElementById('menu_search').addEventListener('focus', hidePannel);
1820 document.getElementById('page_panels').addEventListener('click', hidePannel);
1821 document.getElementById('page_sidebar').addEventListener('click', hidePannel);
1822
1823 if (document.querySelector('.hotkeys')) {
1824 document.head.appendChild(document.createElement('style')).textContent =
1825 '.hotkeys {height: 225px !important}';
1826
1827 document.querySelector('.hotkeys .mapping').innerHTML +=
1828 '<div class="hotkey">' +
1829 ' <div class="hotkey-icon single">D</div>' +
1830 ` <div class="hotkey-label">${translate.downloaderHotkey}</div>` +
1831 '</div>';
1832 }
1833
1834 // There we show and immediately hide playlist. It's for initializing this element, nothing more
1835 if (!unsafeWindow.dzPlayer) {
1836 const pannels = document.getElementById('page_panels');
1837 const playListEl = document.querySelector('button.control-qlist');
1838 pannels.style.dislplay = 'none';
1839 playListEl.click();
1840 playListEl.click();
1841 setTimeout(() => {
1842 pannels.removeAttribute('style');
1843 }, 500);
1844 }
1845
1846 // grab info about tracks from `PLAYER_INIT' variable
1847 trackRecurseLookup(unsafeWindow.PLAYER_INIT);
1848
1849 refreshTracklist();
1850
1851 if (openOnStart)
1852 triggerEl.click();
1853}
1854
1855const waitInterfaceLoading = () => {
1856 if (!document.querySelector('button.control-qlist')) return setTimeout(waitInterfaceLoading, 150);
1857 onInterfaceLoaded();
1858};
1859waitInterfaceLoading();
1860
1861// DATA STYLESHEET
1862document.head.appendChild(document.createElement('style')).textContent = `
1863#dzDownloader {
1864 transform: translateX(220px);
1865 position: fixed;
1866 top: 0;
1867 left: 0;
1868 z-index: 299;
1869 background: #e0e0e0;
1870 padding: 5px;
1871 height: 91%;
1872 overflow-y: auto;
1873 width: 35%;
1874 font-size: 1.05em;
1875}
1876 #dzDownloader.dzHidden {
1877 transform: translateX(calc(0px - 100%));
1878}
1879 .dlNameProto {
1880 text-align: center;
1881 width: 30em;
1882}
1883 .dlInsertButton {
1884 cursor: help;
1885 height: 1.86em;
1886 padding-left: 0.8em;
1887 color: #000;
1888}
1889 .dlInsert:not(:hover) .dlInsertTable {
1890 display: none;
1891}
1892 .dlInsertTable {
1893 position: absolute;
1894 left: calc(220px + 13em);
1895 background-color: #c8c8c8;
1896 border: #000 solid 1px;
1897}
1898 .dlInsertTable tr:hover {
1899 background-color: #ddd;
1900}
1901 .dlInsertTable td {
1902 cursor: pointer;
1903}
1904 .dlRefresh:hover {
1905 background: #444;
1906}
1907 .dlRefresh:active {
1908 background: #00c853;
1909 color: #000;
1910}
1911 .dlRefresh {
1912 border: #000 solid 1px;
1913 color: #fff;
1914 font-size: 12px;
1915 padding: 2px 11px;
1916 background: #777;
1917 margin-left: 72px;
1918}
1919 .dlTitle {
1920 cursor: default;
1921 border-bottom: gray 1.5px solid;
1922 color: #000;
1923 margin-bottom: 1%;
1924 padding-bottom: 5px;
1925 padding-top: 5px;
1926}
1927 .dlDoAzLyrics {
1928 margin-left: 4em;
1929}
1930 .dlListDownloadSelect {
1931 text-align: center;
1932 margin-left: 225px;
1933}
1934 .dlListDownloadButton:hover {
1935 background: #444;
1936}
1937 .dlListDownloadButton:active {
1938 background: #00c853;
1939 color: #000;
1940}
1941 .dlListDownloadButton {
1942 border: #000 solid 1px;
1943 color: #fff;
1944 font-size: 12px;
1945 padding: 2px 11px;
1946 background: #777;
1947 margin-left: -213px;
1948}
1949 .dlTrack:nth-child(even) {
1950 background: #fff;
1951}
1952 .dlTrack.current {
1953 background: #00c853;
1954 color: #fff;
1955}
1956 .dlTrackName {
1957 color: #000;
1958 display: inline-block;
1959 max-width: 30em;
1960 margin-left: .5em;
1961 overflow: hidden;
1962 text-overflow: ellipsis;
1963 white-space: nowrap;
1964 padding-top: 0.1em;
1965 cursor: help;
1966}
1967 .dlTrackLinks {
1968 float: right;
1969 margin-top: .2em;
1970 margin-right: .3em;
1971}
1972 .dlLink {
1973 margin-left: 4px;
1974}
1975 .dlLink[download] {
1976 text-decoration: line-through;
1977}
1978 .dlLink:hover {
1979 text-decoration: underline;
1980}
1981 .dlTrackStatus {
1982 float: right;
1983 margin-top: .2em;
1984 margin-right: .3em;
1985 overflow: hidden;
1986}
1987 .dlEndFloat {
1988 clear: both;
1989}
1990 .dlTrigger {
1991 cursor: pointer;
1992 position: fixed;
1993 left: 0;
1994 top: 0;
1995 z-index: 501;
1996 color: #23232c;
1997 background: #e0e0e0;
1998 padding: 3px;
1999 width: 220px;
2000 text-align: right;
2001 font-size: 1.3em;
2002 font-weight: bold;
2003}
2004/* below - deezer's native elements */
2005 #page_sidebar {
2006 padding-top: 20px;
2007}
2008 .logo-deezer {
2009 height: 55px
2010}
2011 .index-header-logo {
2012 height: 65px;
2013 max-width: 100%;
2014 width: 227px;
2015}
2016 .logo-deezer-hp.logo.index-header-logo, .logo-deezer.logo {
2017 background-position: center;
2018 background-repeat: no-repeat;
2019 background-size: contain;
2020}
2021 .page-sidebar .brand {
2022 margin-bottom: 0;
2023}
2024 #menu_navigation {
2025 top: 135px;
2026}
2027 .page-sidebar .nav-link {
2028 padding: 7px;
2029}
2030 .conversion-banner {
2031 display: none;
2032}
2033 .ads-bottom {
2034 display: none;
2035}
2036 .ads-top {
2037 display: none;
2038}
2039 .has-ads-bottom .ads-bottom-alone {
2040 display: none;
2041}
2042 .page-player .player-track .track-seekbar .slider-counter {
2043 color: #23232c;
2044}
2045 .page-player .player-bottom {
2046 border-top-style: solid !important;
2047 border-style: solid !important;
2048 border-color: #e0e0e0 !important;
2049 border-top-width: 3px !important;
2050 border-top-color: 777 !important;
2051}
2052 .slider-track-input {
2053 background: rgba(25,25,34,.05);
2054}
2055 .abp-banner {
2056 display: none;
2057}
2058`;