· 6 years ago · Jan 30, 2020, 08:14 PM
1// ==UserScript==
2// @name Deezer Drop
3// @version 0.7.1
4// @description Simple tracks and playlists downloader from Deezer
5// @updateURL http://93.179.68.67/drop/drop.meta.js
6// @downloadURL http://93.179.68.67/drop/drop.user.js
7// @include http://www.deezer.com/*
8// @include https://www.deezer.com/*
9// @require https://cdn.jsdelivr.net/npm/aes-js@3.1.2/index.min.js
10// @require https://cdn.jsdelivr.net/npm/egoroof-blowfish@2.1.0/dist/blowfish.min.js
11// @require https://cdn.jsdelivr.net/npm/browser-id3-writer@4.1.0/dist/browser-id3-writer.min.js
12// @require https://cdn.jsdelivr.net/npm/spark-md5@3.0.0/spark-md5.min.js
13// @require https://cdn.jsdelivr.net/npm/file-saver@2.0.0/dist/FileSaver.min.js
14// @author LittleCrabby
15// @connect deezer.com
16// @connect dzcdn.net
17// @grant GM_addStyle
18// @grant GM_setValue
19// @grant GM_getValue
20// @grant GM_xmlhttpRequest
21// @licence MIT
22// @copyright 2018, LittleCrabby (https://openuserjs.org/users/LittleCrabby)
23// ==/UserScript==
24
25(() => {
26 'use strict';
27
28 //Check if we are on Deezer App page. We don't want script to work on Deezer homepage for example.
29 if (!document.getElementById('dzr-app')) {
30 return;
31 }
32
33 const keys = {
34 aesKey: "jo6aey6haid2Teih",
35 bfIv: "\x00\x01\x02\x03\x04\x05\x06\x07",
36 bfSecret: "g4el58wc0zvf9na1"
37 };
38
39 const urls = {
40 apiUrl: "https://www.deezer.com/ajax/gw-light.php?method={0}&input=3&api_version=1.0&api_token={1}",
41 trackUrl: "https://e-cdns-proxy-{0}.dzcdn.net/mobile/1/{1}",
42 coverUrl: "https://e-cdns-images.dzcdn.net/images/cover/{0}/{1}x{2}.jpg"
43 };
44
45 const regexes = {
46 playlist: /https:\/\/www\.deezer\.com\/\w*\/playlist\/(\d*)$/,
47 album: /https:\/\/www\.deezer\.com\/\w*\/album\/(\d*)$/
48 };
49
50 class UI {
51 constructor(history, queue, urls, regexes) {
52 this.history = history;
53 this.queue = queue;
54 this.urls = urls;
55 this.regexes = regexes;
56
57 this.dropModal = document.createElement("div");
58 this.queueContainer = document.createElement("div");
59 this.historyContainer = document.createElement("div");
60 this.dropBtn = document.createElement("span");
61
62 this._addDropModal();
63
64 this.history.items.forEach(h => {
65 this.addHistoryRow(h);
66 });
67 }
68
69 _switchTab(target) {
70 const containers = this.dropModal.querySelectorAll(".drop-container");
71 const tabs = target.parentElement.childNodes;
72 const tab = target;
73 if (!tab.classList.contains("active")) {
74 containers.forEach(x => x.classList.remove("active"));
75 tabs.forEach(x => x.classList.remove("active"));
76 tab.classList.add("active");
77 this[target.dataset.container].classList.add("active");
78 }
79 }
80
81 _addDropModal() {
82 this.dropModal.id = "drop";
83 this.dropModal.classList.add("drop");
84
85 const emptyQueue = document.createElement("span");
86 const emptyHistory = document.createElement("span");
87 emptyQueue.innerText = "Click drop button to add tracks to queue";
88 emptyQueue.classList.add("drop", "drop-empty");
89 emptyHistory.innerText = "Previously downloaded tracks will be here";
90 emptyHistory.classList.add("drop", "drop-empty");
91
92 const dropHeader = document.createElement("div");
93 const queueTab = document.createElement("span");
94 const historyTab = document.createElement("span");
95
96 queueTab.innerText = "Queue";
97 queueTab.classList.add("drop", "drop-tab", "active");
98 queueTab.id = "queue-tab";
99 queueTab.dataset.container = "queueContainer";
100 historyTab.innerText = "History";
101 historyTab.classList.add("drop", "drop-tab");
102 historyTab.id = "history-tab";
103 historyTab.dataset.container = "historyContainer";
104 dropHeader.classList.add("drop", "drop-header");
105 dropHeader.appendChild(queueTab);
106 dropHeader.appendChild(historyTab);
107
108 const clearQueue = document.createElement("span");
109 const clearHistory = document.createElement("span");
110
111 clearQueue.innerText = "? clear queue";
112 clearQueue.classList.add("drop", "drop-clear");
113 clearQueue.id = "clear-queue";
114 clearHistory.innerText = "? clear history";
115 clearHistory.classList.add("drop", "drop-clear");
116 clearHistory.id = "clear-history";
117
118 this.queueContainer.classList.add("drop", "drop-container", "active");
119 this.queueContainer.id = "queue-container";
120 this.queueContainer.appendChild(emptyQueue);
121 this.queueContainer.appendChild(clearQueue);
122 this.historyContainer.classList.add("drop", "drop-container");
123 this.historyContainer.id = "history-container";
124 this.historyContainer.appendChild(emptyHistory);
125 this.historyContainer.appendChild(clearHistory);
126
127 this.dropBtn.classList.add("drop");
128 this.dropBtn.id = "drop-btn";
129 this.dropBtn.dataset.after = 0;
130 this.dropBtn.dataset.before = 0;
131 this.dropBtn.classList.add("after-hidden", "before-hidden");
132
133 this.dropModal.appendChild(dropHeader);
134 this.dropModal.appendChild(this.queueContainer);
135 this.dropModal.appendChild(this.historyContainer);
136 this.dropModal.appendChild(this.dropBtn);
137 document.body.appendChild(this.dropModal);
138
139 this.dropBtn.addEventListener("click", () => {
140 if (this.dropModal.classList.contains("expanded")) {
141 this.dropModal.classList.remove("expanded");
142 } else {
143 this.dropModal.classList.add("expanded");
144 }
145
146 if (this.historyContainer.classList.contains("active")) {
147 this.resetHistoryBadge();
148 }
149 });
150
151 queueTab.addEventListener("click", e => this._switchTab(e.target));
152 historyTab.addEventListener("click", e => {
153 this.resetHistoryBadge();
154 this._switchTab(e.target);
155 });
156 clearHistory.addEventListener("click", () => {
157 this.historyContainer.querySelectorAll('.drop-row').forEach(r => r.remove());
158 this.resetHistoryBadge();
159 this.history.clear();
160 });
161 clearQueue.addEventListener("click", () => {
162 this.queueContainer.querySelectorAll('.drop-row').forEach(r => r.remove());
163 this.resetQueueBadge();
164 this.queue.clear();
165 });
166 }
167
168 addToolbarButton(item) {
169 const ar = this.regexes.album.exec(location.href);
170 const pr = this.regexes.playlist.exec(location.href);
171 if (item && (pr || ar)) {
172 if (!item.getElementsByClassName("drop-list-button").length) {
173 const toolbarItem = document.createElement("div");
174 const listBtn = document.createElement("button");
175 const imgSpan = document.createElement("span");
176 const txtSpan = document.createElement("span");
177 const text = document.createTextNode("Drop");
178 imgSpan.classList.add("drop", "drop-icon");
179 txtSpan.classList.add("drop", "drop-button-span");
180 txtSpan.appendChild(text);
181 listBtn.classList.add("drop", "drop-list-button");
182 listBtn.appendChild(imgSpan);
183 listBtn.appendChild(txtSpan);
184 toolbarItem.classList.add("drop", "toolbar-item");
185 toolbarItem.appendChild(listBtn);
186 item.insertBefore(toolbarItem, item.childNodes[2]);
187 if (pr) {
188 listBtn.addEventListener("click", () => this.queue.addPlaylist(pr[1]));
189 }
190 if (ar) {
191 listBtn.addEventListener("click", () => this.queue.addAlbum(ar[1]));
192 }
193 }
194 }
195 }
196
197 addTrackButton(item) {
198 let cell = item.getElementsByClassName("cell-love").item(0);
199 if (!cell.getElementsByClassName("drop-button").length) {
200 const downloadBtn = document.createElement("button");
201 const downloadImg = document.createElement("span");
202 downloadImg.classList.add("drop", "drop-icon");
203 downloadBtn.classList.add("drop", "datagrid-action", "drop-button");
204 downloadBtn.setAttribute('aria-label', "Download");
205 downloadBtn.appendChild(downloadImg);
206 downloadBtn.addEventListener("click", () => this.queue.add(item.dataset.key));
207 cell.appendChild(downloadBtn);
208 }
209 }
210
211 addQueueRow(trackInfo) {
212 const row = document.createElement("div");
213 row.classList.add("drop", "drop-row");
214 row.dataset.key = trackInfo.SNG_ID;
215
216 const picContainer = document.createElement("div");
217 const trackImg = document.createElement("img");
218
219 trackImg.src = this.urls.coverUrl.format(trackInfo.ALB_PICTURE, 60, 60);
220 trackImg.classList.add("drop");
221 picContainer.appendChild(trackImg);
222 picContainer.classList.add("drop", "drop-queue-pic");
223 row.appendChild(picContainer);
224
225 const trackTitle = document.createElement("p");
226 const trackSize = document.createElement("p");
227 const trackStatus = document.createElement("p");
228 const infoContainer = document.createElement("div");
229
230 trackTitle.classList.add("drop", "drop-queue-track-title");
231 trackTitle.innerText = trackInfo.ART_NAME + " - " + trackInfo.SNG_TITLE;
232 trackSize.classList.add("drop", "drop-queue-track-size");
233 trackSize.innerText = "" + (trackInfo.FILESIZE_MP3_320 / 1048576).toFixed(2) + " MB";
234 trackStatus.classList.add("drop", "drop-queue-track-status");
235 trackStatus.innerText = "In queue";
236 infoContainer.appendChild(trackTitle);
237 infoContainer.appendChild(trackSize);
238 infoContainer.appendChild(trackStatus);
239 infoContainer.classList.add("drop", "drop-queue-info");
240 row.appendChild(infoContainer);
241
242 const actionContainer = document.createElement("div");
243 const cancelAction = document.createElement("a");
244 cancelAction.classList.add("drop", "drop-queue-track-cancel");
245 cancelAction.innerText = "?";
246 actionContainer.appendChild(cancelAction);
247 actionContainer.classList.add("drop", "drop-queue-actions");
248 row.appendChild(actionContainer);
249
250 this.queueContainer.insertBefore(row, this.queueContainer.querySelector('.drop-empty'));
251
252 cancelAction.addEventListener("click", () => {
253 this.deleteRow(trackInfo.SNG_ID)
254 this.queue.cancel(trackInfo.SNG_ID);
255 });
256 }
257
258 addHistoryRow(trackInfo) {
259 const row = document.createElement("div");
260 row.classList.add("drop", "drop-row");
261 row.dataset.key = trackInfo.SNG_ID;
262
263 const picContainer = document.createElement("div");
264 const trackImg = document.createElement("img");
265
266 trackImg.src = this.urls.coverUrl.format(trackInfo.ALB_PICTURE, 60, 60);
267 trackImg.classList.add("drop");
268 picContainer.appendChild(trackImg);
269 picContainer.classList.add("drop", "drop-queue-pic");
270 row.appendChild(picContainer);
271
272 const trackTitle = document.createElement("p");
273 const trackSize = document.createElement("p");
274 const trackStatus = document.createElement("p");
275 const infoContainer = document.createElement("div");
276
277 trackTitle.classList.add("drop", "drop-queue-track-title");
278 trackTitle.innerText = trackInfo.ART_NAME + " - " + trackInfo.SNG_TITLE;
279 trackSize.classList.add("drop", "drop-queue-track-size");
280 trackSize.innerText = "" + (trackInfo.FILESIZE_MP3_320 / 1048576).toFixed(2) + " MB";
281 trackStatus.classList.add("drop", "drop-queue-track-status");
282 trackStatus.innerText = "Downloaded " + trackInfo.timestamp;
283 infoContainer.appendChild(trackTitle);
284 infoContainer.appendChild(trackSize);
285 infoContainer.appendChild(trackStatus);
286 infoContainer.classList.add("drop", "drop-queue-info");
287 row.appendChild(infoContainer);
288
289 const actionContainer = document.createElement("div");
290 const restartAction = document.createElement("a");
291 restartAction.classList.add("drop", "drop-queue-track-restart");
292 restartAction.innerText = "⟲";
293 actionContainer.appendChild(restartAction);
294 actionContainer.classList.add("drop", "drop-queue-actions");
295 row.appendChild(actionContainer);
296
297 this.historyContainer.insertBefore(row, this.historyContainer.firstChild);
298
299 restartAction.addEventListener("click", () => {
300 this.queue.add(trackInfo.SNG_ID);
301 });
302
303 if (this.history.length > 50) {
304 this.deleteHistoryRow();
305 }
306 }
307
308 updateRow(key, status) {
309 const row = this.queueContainer.querySelector(`.drop-row[data-key='${key}']`);
310 if (row) {
311 row.querySelector(".drop-queue-track-status").innerText = status;
312 }
313 }
314
315 deleteRow(key) {
316 const row = this.queueContainer.querySelector(`.drop-row[data-key='${key}']`);
317 if (row) {
318 row.remove();
319 }
320
321 this.decQueueBadge();
322 }
323
324 deleteHistoryRow() {
325 const row = this.historyContainer.querySelector(`.drop-row:last-child`);
326 if (row) {
327 row.remove();
328 }
329 }
330
331 incQueueBadge() {
332 let cnt = parseInt(this.dropBtn.dataset.before);
333 this.dropBtn.dataset.before = ++cnt;
334 this.showQueueBadge();
335 }
336
337 incHistoryBadge() {
338 let cnt = parseInt(this.dropBtn.dataset.after);
339 this.dropBtn.dataset.after = ++cnt;
340 this.showHistoryBadge();
341 }
342
343 decQueueBadge() {
344 let cnt = parseInt(this.dropBtn.dataset.before);
345 this.dropBtn.dataset.before = --cnt;
346 if (cnt == 0) {
347 this.dropBtn.classList.add("before-hidden");
348 }
349 }
350
351 resetQueueBadge() {
352 this.dropBtn.dataset.before = 0;
353 this.hideQueueBadge();
354 }
355
356 resetHistoryBadge() {
357 this.dropBtn.dataset.after = 0;
358 this.hideHistoryBadge();
359 }
360
361 hideHistoryBadge() {
362 this.dropBtn.classList.add("after-hidden");
363 }
364
365 showHistoryBadge() {
366 this.dropBtn.classList.remove("after-hidden");
367 }
368
369 hideQueueBadge() {
370 this.dropBtn.classList.add("before-hidden");
371 }
372
373 showQueueBadge() {
374 this.dropBtn.classList.remove("before-hidden");
375 }
376 }
377
378 class Queue {
379 constructor(deezer) {
380 this.deezer = deezer;
381 this.items = [];
382
383 // init arrays of listeners callbacks
384 this.addListeners = [];
385 this.startListeners = [];
386 this.finishListeners = [];
387 }
388
389 set onItemAdd(listener) {
390 this.addListeners.push(listener);
391 }
392
393 set onStartDownload(listener) {
394 this.startListeners.push(listener);
395 }
396
397 set onFinishDownload(listener) {
398 this.finishListeners.push(listener);
399 }
400
401 async add(item) {
402 if (Array.isArray(item)) {
403 // if argument is array, push each item to queue
404 item.forEach(i => this.push(i));
405 } else {
406 this.push(item);
407 }
408 }
409
410 async push(key) {
411 const trackInfo = await this.deezer.getTrackInfo(key);
412 this.addListeners.forEach(f => f(trackInfo));
413
414 // each item will contain info about track and XHR object
415 this.items.push({ti: trackInfo, xhr: new XMLHttpRequest()});
416
417 if (this.items.length === 1) {
418 await this.startDownload();
419 }
420 }
421
422 async startDownload() {
423 if (!this.items.length) {
424 // return if no items left in queue
425 return;
426 }
427
428 const qItem = this.items[0];
429 this.startListeners.forEach(f => f(qItem.ti));
430
431 // track downloading and decrypting operations
432 const encryptedBuffer = await this.deezer.downloadTrack(qItem.ti, qItem.xhr);
433 const decryptedBuffer = await this.deezer.decryptTrack(qItem.ti, encryptedBuffer);
434 const coverBuffer = await this.deezer.downloadCover(qItem.ti);
435 // add mp3 tags to file and generate blob
436 const blob = this.deezer.addTags(decryptedBuffer, coverBuffer, qItem.ti);
437
438 // save mp3 file
439 saveAs(blob, `${qItem.ti.ART_NAME} - ${qItem.ti.SNG_TITLE}.mp3`);
440
441 // save timestamp to display in history tab
442 qItem.ti.timestamp = new Date().toLocaleDateString() + " " + new Date().toLocaleTimeString();
443 this.finishListeners.forEach(f => f(qItem.ti));
444
445 // shift one item from queue and try again
446 this.items.shift();
447 await this.startDownload();
448 }
449
450 async addPlaylist(key) {
451 const playlist = await this.deezer.getPlaylist(key);
452
453 this.add(playlist.SONGS.data.map(x => x.SNG_ID));
454 }
455
456 async addAlbum(key) {
457 const album = await this.deezer.getAlbum(key);
458
459 this.add(album.SONGS.data.map(x => x.SNG_ID));
460 }
461
462 cancel(key) {
463 const i = this.items.findIndex(item => item.ti.SNG_ID == key);
464
465 // if key found in queue, remove and abort downloading, start download next track
466 if (i !== -1) {
467 this.items[i].xhr.abort();
468 this.items.splice(i, 1);
469 this.startDownload();
470 }
471 }
472
473 clear() {
474 this.items.forEach(i => i.xhr.abort());
475 this.items = [];
476 }
477 }
478
479 class History {
480 constructor() {
481 this.history = GM_getValue('history', []);
482 }
483
484 get length() {
485 return this.history.length;
486 }
487
488 get items() {
489 return this.history;
490 }
491
492 push(item) {
493 this.history.push(item);
494 if (this.history.length > 50) {
495 this.history.shift();
496 }
497 GM_setValue('history', this.history);
498 }
499
500 clear() {
501 GM_setValue('history', []);
502 }
503
504 }
505
506 class Deezer {
507 constructor(crypt, urls) {
508 this.crypt = crypt;
509 this.apiUrl = urls.apiUrl;
510 this.apiTrackUrl = urls.apiTrackUrl;
511 this.apiPlaylistUrl = urls.apiPlaylistUrl;
512 this.apiAlbumUrl = urls.apiAlbumUrl;
513 this.trackUrl = urls.trackUrl;
514 this.coverUrl = urls.coverUrl;
515
516 this.progressListeners = [];
517 this.apiToken = "";
518 }
519
520 set onDownloadProgress(listener) {
521 this.progressListeners.push(listener);
522 }
523
524 get apiKey() {
525 return new Promise((resolve, reject) => {
526 if (!this.apiToken) {
527 // if apiToken is empty, fetch it from deezer API and save
528 fetch(this.apiUrl.format("deezer.getUserData", ""), {
529 method: "POST"
530 })
531 .then(response => response.json())
532 .then(data => {
533 this.apiToken = data.results.checkForm;
534 resolve(this.apiToken)
535 });
536 } else {
537 resolve(this.apiToken);
538 }
539 });
540 }
541
542 async getTrackInfo(id) {
543 const response = await fetch(this.apiUrl.format("song.getData", await this.apiKey, id), {
544 method: "POST",
545 body: JSON.stringify({sng_id: id})
546 });
547
548 const data = await response.json();
549
550 return data.results;
551 }
552
553 async getAlbum(id) {
554 const response = await fetch(this.apiUrl.format("deezer.pageAlbum", await this.apiKey, id), {
555 method: "POST",
556 body: JSON.stringify({
557 alb_id: id,
558 lang: "en"
559 })
560 });
561
562 const data = await response.json();
563
564 return data.results;
565 }
566
567 async getPlaylist(id) {
568 const response = await fetch(this.apiUrl.format("deezer.pagePlaylist", await this.apiKey, id), {
569 method: "POST",
570 body: JSON.stringify({
571 playlist_id: id,
572 lang: "en"
573 })
574 });
575
576 const data = await response.json();
577
578 return data.results;
579 }
580
581 async downloadCover(trackInfo) {
582 const response = await fetch(this.coverUrl.format(trackInfo.ALB_PICTURE, 500, 500));
583
584 return response.arrayBuffer();
585 }
586
587 getTrackUrl(trackInfos) {
588 // select file format according to track info
589 const bitRate = trackInfos.FILESIZE_MP3_320 ? 3 : trackInfos.FILESIZE_MP3_256 ? 5 : 1;
590 // prepare string to be hashed
591 const toHash = [trackInfos.MD5_ORIGIN, bitRate, trackInfos.SNG_ID, trackInfos.MEDIA_VERSION].join('¤');
592 // encrypt using md5 and aes algorithms
593 const hash = this.crypt.aes(this.crypt.md5(toHash) + '¤' + toHash + '¤');
594
595 return this.trackUrl.format(trackInfos.MD5_ORIGIN[0], hash);
596 }
597
598 downloadTrack(trackInfo, r) {
599 return new Promise((resolve, reject) => {
600 r.onload = e => resolve(r.response);
601 r.onprogress = xhr => this.progressListeners.forEach(f => f(xhr, trackInfo))
602 r.open("GET", this.getTrackUrl(trackInfo));
603 r.responseType = "arraybuffer";
604 r.send();
605 });
606 }
607
608 decryptTrack(trackInfo, buffer) {
609 return new Promise((resolve, reject) => {
610 const bfKey = this.crypt.getBfKey(trackInfo.SNG_ID);
611 const data = new Uint8Array(buffer);
612
613 // work with buffer as with 2048 bytes blocks
614 for (let i = 0, j = 2048, n = 0; j < data.length; i += 2048, j += 2048, n++) {
615 if (n % 3 > 0 || data.length - j < 2048) {
616 // skip and don't decrypt blocks except 3rd and
617 // skip block that has less than 2048 bytes
618 continue;
619 }
620 // decrypt selected block and save it back to Uint8Array
621 data.set(this.crypt.bfDecrypt(data.slice(i, j), bfKey), i);
622 }
623 resolve(data.buffer);
624 });
625 }
626
627 addTags(songBuffer, coverBuffer, trackInfo) {
628 const writer = new ID3Writer(songBuffer);
629
630 let TPE1;
631
632 // set correct TPE1 tag
633 trackInfo.SNG_CONTRIBUTORS.featuring ? TPE1 = trackInfo.SNG_CONTRIBUTORS.featuring :
634 trackInfo.SNG_CONTRIBUTORS.mainartist ? TPE1 = trackInfo.SNG_CONTRIBUTORS.mainartist :
635 TPE1 = [trackInfo.ART_NAME];
636
637 // write tags and cover to mp3 file
638 writer.setFrame('TIT2', trackInfo.SNG_TITLE)
639 .setFrame('TPE1', TPE1)
640 .setFrame('TPE2', trackInfo.ART_NAME)
641 .setFrame('TALB', trackInfo.ALB_TITLE)
642 .setFrame('TYER', parseInt(trackInfo.PHYSICAL_RELEASE_DATE))
643 .setFrame('TRCK', trackInfo.TRACK_NUMBER)
644 .setFrame('TPOS', trackInfo.DISK_NUMBER)
645 .setFrame('APIC', { type: 3, data: coverBuffer, description: 'Cover' });
646 writer.addTag();
647
648 return writer.getBlob();
649 }
650 }
651
652 class Crypt {
653 constructor(keys) {
654 this.aesKey = keys.aesKey;
655 this.bfIv = keys.bfIv;
656 this.bfSecret = keys.bfSecret;
657 }
658
659 md5(value) {
660 return SparkMD5.hashBinary(value);
661 }
662
663 bfDecrypt(value, bfKey) {
664 const bf = new Blowfish(bfKey, Blowfish.MODE.CBC, Blowfish.PADDING.NULL);
665 bf.setIv(this.bfIv);
666 return bf.decode(value, Blowfish.TYPE.UINT8_ARRAY);
667 }
668
669 aes(value) {
670 while (value.length % 16 > 0) {
671 value += ' ';
672 }
673 const aesEcb = new aesjs.ModeOfOperation.ecb(this.strToBytes(this.aesKey));
674 const encryptedBytes = aesEcb.encrypt(this.strToBytes(value));
675 return aesjs.utils.hex.fromBytes(encryptedBytes);
676 }
677
678 getBfKey(songId) {
679 let key = "";
680 const idMd5 = this.md5(songId);
681 for (let i = 0; i < 16; i++) {
682 key += String.fromCharCode(idMd5.charCodeAt(i) ^ idMd5.charCodeAt(i + 16) ^ this.bfSecret.charCodeAt(i));
683 }
684 return key;
685 }
686
687 strToBytes(str) {
688 let bytes = [];
689 for (let i = 0; i < str.length; i++) {
690 bytes.push(str.charCodeAt(i));
691 }
692 return bytes;
693 }
694 }
695
696 const deezer = new Deezer(new Crypt(keys), urls);
697 const history = new History();
698 const queue = new Queue(deezer);
699 const ui = new UI(history, queue, urls, regexes);
700
701 queue.onItemAdd = (trackInfo) => {
702 ui.addQueueRow(trackInfo);
703 ui.incQueueBadge();
704 };
705
706 queue.onStartDownload = (trackInfo) => {
707 ui.updateRow(trackInfo.SNG_ID, "Downloading track...");
708 };
709
710 queue.onFinishDownload = (trackInfo) => {
711 history.push(trackInfo);
712 ui.addHistoryRow(trackInfo);
713 ui.deleteRow(trackInfo.SNG_ID);
714 ui.incHistoryBadge();
715 };
716
717 deezer.onDownloadProgress = (xhr, trackInfo) => {
718 const p = (xhr.loaded / (xhr.total / 100)).toFixed(2);
719 if (p < 99.9) {
720 ui.updateRow(trackInfo.SNG_ID, `Downloaded ${p}%`);
721 } else {
722 ui.updateRow(trackInfo.SNG_ID, `Processing track...`);
723 }
724 };
725
726 // Create an observer that will add drop buttons to each dynamically created song row and toolbar
727 const observer = new MutationObserver(mutationsList => {
728 for(let mutation of mutationsList) {
729 if (mutation.type == 'childList') {
730 let node = mutation.addedNodes[0];
731 if (node instanceof HTMLElement && typeof node.classList !== "undefined") {
732 if (node.classList.contains("song")) {
733 ui.addTrackButton(node);
734 } else {
735 const songRows = node.getElementsByClassName("song");
736 const toolbar = node.getElementsByClassName("toolbar-wrapper").item(0);
737
738 ui.addToolbarButton(toolbar);
739
740 for (let i = 0; i < songRows.length; i++) {
741 ui.addTrackButton(songRows[i]);
742 }
743 }
744 }
745 }
746 }
747 });
748
749 observer.observe(document.body, { childList: true, subtree: true });
750
751 // Add listener to collapse Drop menu when clicking outside of it
752 document.body.addEventListener("click", e => {
753 if (!e.target.classList.contains("drop")) {
754 ui.dropModal.classList.remove("expanded");
755 }
756 });
757
758 String.prototype.format = function() {
759 let k, a = this;
760 for (k in arguments) {
761 a = a.replace("{" + k + "}", arguments[k])
762 }
763 return a
764 }
765
766 GM_addStyle(`
767.datagrid-cell.datagrid-cell-action.cell-love {
768 width: 56px;
769 padding-left: 5px;
770}
771
772div.datagrid-cell.cell-title {
773 padding-left: 8px;
774}
775
776.drop-icon {
777 display: block;
778 width: 16px;
779 height:16px;
780 background-image: url(data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAACAAAAAgCAYAAABzenr0AAAABHNCSVQICAgIfAhkiAAAAAlwSFlzAAAA3QAAAN0BcFOiBwAAABl0RVh0U29mdHdhcmUAd3d3Lmlua3NjYXBlLm9yZ5vuPBoAAAOGSURBVFiFvZY/bxpJGId/Mzs7ywqJVLFk5MpAEYkq+GxrU9A7rqC6wmn9GSgi+9xcsQVWihRbWE6EdEJyvgOlN3drNzQ0RqI8pQEJOwy7814R1rqz+Q+5n7TFaqT3eWZ239HLiAjLhDEmpZR/AIBS6lciUksVIqKFHwAGY+xLMplUyWRSMca+ADCWqrWkwEchRNhoNKjRaJAQIgTw8X8RAHAMQNfrdYpTr9cJgAZw/FMFALxhjA1d143oSVzXjRhjQwBvfooAgC0hxLdSqaSewuOUSiUlhPgGYGutAgASpmneZjIZ1ev1JvGp1+tRJpNRpmneAkisU+BDIpEIm83mRHicZrNJiUQiBPBhLQIAXjPGolqtNhMep1arEWMsAvB6JQEA3DTNoFwuD+amj1IulwemaQYA+CoCx1LKsNPpLMqnTqdDUsrhrNacBn8phOidnp4uDI9zdnZGQogegJcLC5im+TmdTg/v7++XFnh4eKCtrS1lmubnhQQA7D697ZbN1dVVfEvuzi3AOf+0s7Oz8I83Kfv7+wPO+ae5BAC8MAzj+8XFxbr4dHl5SYZhfAfwYh6BYyll1O121ybQ7XZJShmN6wg+ZkRwisWiTqVSS80X45JKpVAsFjUA5+naMwHLsvL5fF6sjT5KPp8XlmXlZwporXPZbHbdfGSzWWitczMFGGNqMBg8vqfTaZyfn68sMBgMwBh7NjeOOwE/CILHSbVQKODm5mZlgSAISGvtzxQIw/Cr7/uPpoVCAdfX19BaryTg+74Kw/Drs4UxbfiWc677/T4REbVaLbJtm1zXXboN+/0+cc41gLfPeGMENjjnoed5jwWq1SpZlkVBECwl4Hkecc5DABszBUYS723bHrbbbSIi0lrTwcEBWZZFrutSFD2bSSem3W6TbdtDAO/HsiYICCnlreM4KoZpralarZJt25TL5ejo6Iiq1epUeBRF5DiOklLeAhBzC4wkXhmGoSqVyn923Gq16OTkhA4PD2lzc3MqvFKpkGEYCsCriZxJCyOJd4ZhKMdxVPw55km73SbHcdQI/m4qY9pifBJSylvbtoee51HcHePS7/fJ8zyybXs4OvaJO48fNoJMDWNMAKhwzk8A8O3tbbW3tycLhQIDflwyvu+ru7s7CUBrrX8D8DsRhTNrzyPwL5ENAL8A2BFC7HLO9/CD6I8umb8A/ElEf89b8x96fwU2MHVN0AAAAABJRU5ErkJggg==);
781 background-size: 102% 100%;
782}
783
784.drop-button {
785 width: 28px;
786 padding: 0 4px;
787}
788
789.drop-button .drop-icon {
790 margin-left: 2px;
791}
792
793.drop-list-button {
794 color: #23232D;
795 background-color: #F8F8F9;
796 border: 1px solid #D1D1D7;
797 cursor: pointer;
798 height: 32px;
799 display: inline-flex;
800 transition: background-color 150ms cubic-bezier(0.4, 0, 0.2, 1) 0ms,border-color 150ms cubic-bezier(0.4, 0, 0.2, 1) 0ms,color 150ms cubic-bezier(0.4, 0, 0.2, 1) 0ms;
801 align-items: center;
802 font-family: Open Sans;
803 font-weight: 600;
804 border-radius: 3px;
805 justify-content: center;
806}
807
808.drop-list-button:hover {
809 background-color: #FFFFFF;
810}
811
812.drop-list-button .drop-icon {
813 margin-right: 6px;
814}
815
816.toolbar-item button {
817 padding: 0 10px;
818}
819
820#drop {
821 position: fixed;
822 padding: 2px;
823 right: 20px;
824 top: 68px;
825 width:36px;
826 height:36px;
827 background-color: #fff;
828 border-radius:5px;
829 box-shadow: 0 2px 10px 0 rgba(25,25,34,.24);
830 z-index: 999;
831}
832
833#drop.expanded {
834 width:400px;
835 height:72%;
836 display:flex;
837 flex-direction:column;
838}
839
840#drop-btn {
841 position: absolute;
842 top: 5px;
843 right: 5px;
844 width:26px;
845 height:26px;
846 background-image: url(data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAACAAAAAgCAYAAABzenr0AAAABHNCSVQICAgIfAhkiAAAAAlwSFlzAAAA3QAAAN0BcFOiBwAAABl0RVh0U29mdHdhcmUAd3d3Lmlua3NjYXBlLm9yZ5vuPBoAAAOdSURBVFiFvZdPaBxVHMc/vzd/ktY0KdUtNNnFVhE2JNtom8Y2wV6EmpDaVgy0KUWDWFNYPejNILSB2EBLoGBT/zSCWNCLRUIOORm8qE2zQeofPJcNEepl8RLZnZ2fh9mSqLuZnc3iF+Yw8958v5/5vTeP90RVqUUi4mK5XwJQzA+rar4mI1WNfAEWyG3c7Xnc7XmQ24BVk1eNADcwtsfIrDIyqxjbA278LwDAKOAzNKNc/CO4hmYU8IHRqH4m4rj3IXKdY5eUjpPrDR0n4dglReS6iPRF8awaQETiGGuW5IByJP3f946kDckBxVizIhKvK4CINGKcOVoSzZyadip2PDXt0JJoxjhzItJYNwDgCsakGL7l4DZV7uU2wfAtB2NSwJW6AIjIAUTSnLhmEUuGO8aScOKahUhaRA5sCUBEDMa5SXLQIzUUHv5QqSFIDnoY56aIbJoRVoHzCF30T7jVp5fUP+Eish84XxOAiMQw9lWee9uiuS1yPs1tcPQdG2NfFZFYZACMNcUjj22j763o4Q/V+yY07W7EWFORAESkB794jhcmbOyq/qbyshug/30Hv3hORHqqBsCYNG1PF/6x2tWq9uOQ6C5gTLoqABFpATlN92vRJ14lHXzVBTkdeIcAAGcQ49B+vG75JAdBjAOcqQagl719Pg076gfQsAP29vlAbziA7XYSS9r1Sy8plrSx3c5wAN9/il376p7Prn2BdyiASJ7ihu3dVCfc+XjrAMV84B0KgC6yem99p9raBb//tHWA1XsKuhgOUPTusrK8TrqnC1YyoP7WAFaW8xS9u+EAkCF336WwFtylXoY/V+GHD2sPL6xB7r4LZKoBWAJ8fv4quHv0SXj+PVi4XPtQBF5+yXtzAFV9gPrjzI955LLBw8NvwBNH4dMB+H462nDksjA/5qH+uKo++HezlDsZiYiNcZaIP9PByJyDGEDhzifwzQQ0t0K8G/bsh8OjlcPVh89eLLDy46/4hUOq6oVWAEBVPfzCWbIZWJgsfbEEYRe+DebFXzn47oPNwxcmIZsBv3C2XHjFCmyoxCuImSHRDS995LAzUTlwo3JZ+PpCgWwG1H9dVT+vmBF2OBWRdozzBcbqZOCyTWoInG3lOxfWggk3P+bhF38pfflvm/pXczoWERt4FzEXAcPOx/PED7q0dgkQLDIry/nSr+aj/jgwWanskQE2gOwGDgHdWHYPyLNBiy6WFpkMsFRutlfS3yH6DlSLMq+6AAAAAElFTkSuQmCC);
847 background-size: 26px;
848 cursor: pointer;
849}
850
851#drop-btn:hover {
852 opacity: 0.8;
853}
854
855#drop-btn::before {
856 content: attr(data-before);
857 background-color: rgb(255, 251, 151);
858 width: 16px;
859 height: 16px;
860 display: block;
861 position: absolute;
862 text-align: center;
863 top: -10px;
864 left: -10px;
865 border-radius: 5px;
866 box-shadow: 0 2px 2px 0 rgba(25,25,34,.24);
867}
868
869#drop-btn.before-hidden::before {
870 display:none;
871}
872
873#drop-btn::after {
874 content: attr(data-after);
875 background-color: rgb(151, 255, 192);
876 width: 16px;
877 height: 16px;
878 display: block;
879 position: absolute;
880 text-align: center;
881 top: -10px;
882 right: -10px;
883 border-radius: 5px;
884 box-shadow: 0 2px 2px 0 rgba(25,25,34,.24);
885}
886
887#drop-btn.after-hidden::after {
888 display:none;
889}
890
891.drop-container {
892 display: none;
893 height: 100%;
894 overflow: auto;
895 margin: 7px 7px;
896}
897
898#drop.expanded .drop-container.active {
899 display: block;
900}
901
902#drop.expanded .drop-header {
903 display: block;
904}
905
906.drop-header {
907 display: none;
908 font-size: 16px;
909 margin: 6px 8px;
910 width: 350px;
911}
912
913.drop-tab {
914 padding: 8px 20px;
915 cursor: pointer;
916}
917
918.drop-tab:hover {
919 color: black;
920 border-bottom: 2px solid gray;
921}
922
923.drop-tab.active {
924 color: black;
925 border-bottom: 2px solid #007feb;
926}
927
928.toolbar-wrapper .c0113 {
929 padding: 0 8px;
930}
931
932.toolbar-wrapper .c0117 {
933 margin-right: 6px;
934}
935
936.drop-row {
937 display: flex;
938 margin: 5px 0;
939}
940
941.drop-queue-pic img {
942 width: 60px;
943 height: 60px;
944}
945
946.drop-queue-info {
947 flex: 1;
948 display: flex;
949 justify-content: space-around;
950 border-bottom: 1px solid lightgray;
951 flex-direction: column;
952 padding-left: 5px;
953}
954
955.drop-queue-actions {
956 width: 30px;
957 text-align: center;
958 display: flex;
959 justify-content: space-around;
960 flex-direction: column;
961 border-bottom: 1px solid lightgray;
962}
963
964.drop-empty {
965 display: block;
966 padding: 20px;
967 font-size: 14px;
968}
969
970.drop-row ~ .drop-empty {
971 display: none;
972}
973
974.drop-clear {
975 padding: 5px;
976 display: none;
977 text-align: center;
978 font-size: 14px;
979 cursor: pointer;
980}
981
982.drop-row ~ .drop-clear {
983 display: block;
984}
985
986.drop-clear:hover {
987 color: black;
988}
989
990#page_topbar > div.popper-wrapper.topbar-entrypoints > div {
991 display: none;
992}
993 `);
994})();