Advertisement
AHOHNMYC

Deezer: Download

Mar 25th, 2018
274
0
Never
Not a member of Pastebin yet? Sign Up, it unlocks many cool features!
  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 */
  3. const openOnStart   = !true;
  4. const DEBUG         = !true;
  5. const SHOW_KEYS     = !true;    // Show pressed keys
  6. const L10nDEBUG     = !true;    // Enable localization override?
  7. const L10nOVERRIDE  = 'de';     // Override your browser language with selected (handy for translation testing)
  8.  
  9. const showMp3_32    = !true;
  10. const showMp3_64    = !true;
  11. const showMp3_128   = true;
  12. const showMp3_320   = true;
  13. const showFLAC      = true;
  14.  
  15. const showAzLyrics  = true;     // Show element asking you for parsing azLyrics
  16. const forceAzLyrics = !true;    // If `true' — override parsing settings with \
  17. const forceAzValue  = true;     // <- this value
  18.  
  19. const showListDownloader = true;
  20.  
  21. const coverSize     = 1200;
  22. const 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).
  120. const _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 °°° ]
  285. let L10n = L10nDEBUG ? L10nOVERRIDE : navigator.language;
  286. if (!_getI18nMap[L10n]) L10n = L10n.substring(0, 2);
  287. if (!_getI18nMap[L10n]) L10n = 'en';
  288.  
  289. const translate = _getI18nMap[L10n];    // Now, use translate.whatYouWant ( better than translate[whatYouWant] ).
  290.  
  291. /* Replaces `10 ^mb' to `10 MB' */
  292. function translateCircumstring(data) {
  293.     return data.replace(/\^(\w+)/g, (full, part) => translate[part] ? translate[part] : full);
  294. }
  295.  
  296. // FETCH SUBSTITUTE
  297. if (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.  
  319. function 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
  333. function 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
  360. function 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.  
  370. const 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
  731. mainWk.postMessage({
  732.     debug: DEBUG,
  733.     coverSize: coverSize,
  734.     coverQuality: coverQuality,
  735. });
  736.  
  737.  
  738. // azLyrics parser
  739. function 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. }
  763. function 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. }
  779. function 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
  787. function 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.  
  795. function 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
  813. const fmtMp3_32 = 11, fmtMp3_64 = 10, fmtMp3_128 = 1, fmtMp3_320 = 3, fmtFLAC = 9;
  814. const trackDB = {};
  815. const urlDB = {};
  816. if (!localStorage.dlNameProto)
  817.     unsafeWindow.localStorage.dlNameProto = '%NN. %Artists - %Title %Version';
  818. if (!localStorage.dlAzLyrics)
  819.     unsafeWindow.localStorage.dlAzLyrics = true;
  820.  
  821. function 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.  
  865. function 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
  875. function 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
  883. function 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.  
  908. const bfGK = 'g4el58wc0zvf9na1';
  909.  
  910. function 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.  
  919. function fileSizeToMb(aSize) {
  920.     return isNaN(aSize) ? 0 : Math.round(aSize /1024/1024 * 10)/10;
  921. }
  922.  
  923. // DOWNLOAD ENTRY POINT
  924. function 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
  975. function 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. }
  1025. mainWk.onmessage = messageProcessor;
  1026.  
  1027. // DOWNLOADER LOGIC: HTML GENERATOR
  1028. function 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.  
  1036. function 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.  
  1059. function 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.  
  1096. function isLoggedIn() {
  1097.     // return typeof unsafeWindow.USER !== 'undefined' && unsafeWindow.USER.USER_ID !== 0;
  1098.     return parseInt(unsafeWindow.user.current()) > 0;
  1099. }
  1100.  
  1101. function 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.  
  1124. function 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
  1135. function 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
  1176. const rootEl = document.createElement('div');
  1177. rootEl.classList.add('dzHidden');
  1178. rootEl.id = 'dzDownloader';
  1179.  
  1180. const elNameProto = document.createElement('input');
  1181. elNameProto.classList.add('dlNameProto');
  1182. elNameProto.title = `${translate.choose} ${translate.fileNaming}`;
  1183. elNameProto.value = localStorage.dlNameProto;
  1184. ['change', 'keyup', 'keydown'].forEach( event=> {
  1185.     elNameProto.addEventListener(event, ()=>{
  1186.         unsafeWindow.localStorage.dlNameProto = elNameProto.value;
  1187.         updateNames();
  1188.     });
  1189. });
  1190.  
  1191. const elInsert = document.createElement('span');
  1192. elInsert.classList.add('dlInsert');
  1193. elInsert.addEventListener('mouseenter', ()=>elNameProto.focus());
  1194.  
  1195. const elInsertButton = document.createElement('button');
  1196. elInsertButton.classList.add('dlInsertButton');
  1197. elInsertButton.textContent = '⬇ ⬇ ⬇ ⬇';
  1198. elInsertButton.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.  
  1203. If count of digits in track number less than 'N' count, it will be padded with zeroes:
  1204. If track number is 2:
  1205. %N\t=> 2
  1206. %NN\t=> 02
  1207. %NNN\t=> 002
  1208. %NNNN\t=> 0002
  1209. If track number is 42:
  1210. %N\t=> 42
  1211. %NN\t=> 42
  1212. %NNN\t=> 042
  1213. %NNNN\t=> 0042`;
  1214.  
  1215. const elInsertTable = document.createElement('table');
  1216. elInsertTable.classList.add('dlInsertTable');
  1217. function 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
  1244. addInsertVariant('Artist');
  1245. addInsertVariant('Artists');
  1246. addInsertVariant('Album');
  1247. addInsertVariant('Title');
  1248. addInsertVariant('Version');
  1249. addInsertVariant('---');
  1250. // numbers
  1251. addInsertVariant('disk');
  1252. addInsertVariant('time');
  1253. addInsertVariant('isrc');
  1254. addInsertVariant('NN');
  1255. addInsertVariant('---');
  1256. // Deezer-specific
  1257. addInsertVariant('cover');
  1258. addInsertVariant('gain');
  1259. addInsertVariant('id');
  1260. addInsertVariant('album_id');
  1261.  
  1262. elInsert.appendChild(elInsertButton);
  1263. elInsert.appendChild(elInsertTable);
  1264.  
  1265. const elRefresh = document.createElement('a');
  1266. elRefresh.classList.add('dlRefresh');
  1267. elRefresh.textContent = `⟳ ${translate.refresh} ${translate.tracklist.toLowerCase()}`;
  1268. elRefresh.addEventListener('click', refreshTracklist);
  1269.  
  1270. const elTrackListHeader = document.createElement('p');
  1271. elTrackListHeader.classList.add('dlTitle');
  1272. elTrackListHeader.textContent = `♪ ${translate.tracklist}`;
  1273.  
  1274.  
  1275. if (forceAzLyrics)
  1276.    unsafeWindow.localStorage.dlAzLyrics = forceAzValue;
  1277.  
  1278. const elAzLyrics = document.createElement('input');
  1279. elAzLyrics.type = 'checkbox';
  1280. elAzLyrics.classList.add('dlDoAzLyrics');
  1281. elAzLyrics.id = 'dlDoAzLyrics';
  1282. elAzLyrics.checked = !!localStorage.dlAzLyrics;
  1283. elAzLyrics.addEventListener('change', ()=>{
  1284.    unsafeWindow.localStorage.dlAzLyrics = elAzLyrics.checked;
  1285. });
  1286.  
  1287. const elAzLyricsLabel = document.createElement('label');
  1288. elAzLyricsLabel.setAttribute('for', 'dlDoAzLyrics');
  1289. elAzLyrics.classList.add('dlDoAzLyricsLabel');
  1290. elAzLyricsLabel.textContent = `azLyrics`;
  1291.  
  1292.  
  1293. const elListDownloadSelect = document.createElement('select');
  1294. elListDownloadSelect.classList.add('dlListDownloadSelect');
  1295. elListDownloadSelect.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>`);
  1301. elListDownloadSelect.value = localStorage.dlListDownload;
  1302. elListDownloadSelect.addEventListener('change', ()=>{
  1303.    unsafeWindow.localStorage.dlListDownload = elListDownloadSelect.value;
  1304. });
  1305.  
  1306. const elListDownloadButton = document.createElement('button');
  1307. elListDownloadButton.classList.add('dlListDownloadButton');
  1308. elListDownloadButton.textContent = translate.downloadList;
  1309. elListDownloadButton.addEventListener('click', ()=>{
  1310.    const selector = `.dlLink[data-fmt="${elListDownloadSelect.value}"]`;
  1311.    document.querySelectorAll(selector).forEach(anchor=>anchor.click());
  1312. });
  1313.  
  1314. if (showAzLyrics) {
  1315.    elTrackListHeader.appendChild(elAzLyrics);
  1316.    elTrackListHeader.appendChild(elAzLyricsLabel);
  1317. }
  1318. if (showListDownloader) {
  1319.    elTrackListHeader.appendChild(elListDownloadSelect);
  1320.    elTrackListHeader.appendChild(elListDownloadButton);
  1321. }
  1322.  
  1323.  
  1324. const divTracklist = document.createElement('div');
  1325. divTracklist.classList.add('dlTrackList');
  1326.  
  1327. rootEl.appendChild(elNameProto);
  1328. rootEl.appendChild(elInsert);
  1329. rootEl.appendChild(elRefresh);
  1330. if (showAzLyrics || showListDownloader)
  1331.    rootEl.appendChild(elTrackListHeader);
  1332. rootEl.appendChild(divTracklist);
  1333.  
  1334. document.body.appendChild(rootEl);
  1335.  
  1336. const triggerEl = document.createElement('div');
  1337. triggerEl.classList.add('dlTrigger');
  1338. triggerEl.title = translate.clickToOpen;
  1339. triggerEl.textContent = 'D➲wnloader ☰';
  1340. triggerEl.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.  
  1367. document.body.appendChild(triggerEl);
  1368.  
  1369.  
  1370. function hidePannel() {
  1371.    if (!rootEl.classList.contains('dzHidden'))
  1372.        triggerEl.click();
  1373. }
  1374.  
  1375.  
  1376. // Hotkey handling
  1377. addEventListener('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
  1395. const delayStyleElement = document.head.appendChild(document.createElement('style'));
  1396. delayStyleElement.textContent = '#dzDownloader, .dlTrigger {display: none !important}';
  1397.  
  1398. function 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.  
  1458. const waitInterfaceLoading = ()=>{
  1459.     if (!(document.querySelector('button.control-qlist') || document.querySelector('svg.svg-icon-equalizer'))) return setTimeout(waitInterfaceLoading, 150);
  1460.     onInterfaceLoaded();
  1461. };
  1462. waitInterfaceLoading();
  1463.  
  1464. /*
  1465. if (/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.
  1492. document.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. `;
Advertisement
Add Comment
Please, Sign In to add comment
Advertisement