-
Settings
+ {{$t('Settings')}}
+
+
+
@@ -35,8 +47,8 @@
- Show download dialog
- Always show download confirm dialog before downloading.
+ {{$t("Show download dialog")}}
+ {{$t("Always show download confirm dialog before downloading.")}}
@@ -46,7 +58,7 @@
- Create folders for artists
+ {{$t("Create folders for artists")}}
@@ -55,9 +67,19 @@
- Create folders for albums
+ {{$t("Create folders for albums")}}
+
+
+
+
+
+
+ {{$t("Download lyrics")}}
+
+
+
+
+ {{$t("UI")}}
+
+
+
+
+
+
+
+
+
+
+
+ {{$t("Show autocomplete in search")}}
+
+
+
- Integrations
+ {{$t("Integrations")}}
@@ -78,8 +123,8 @@
- Log track listens to Deezer
- This allows listening history, flow and recommendations to work properly.
+ {{$t("Log track listens to Deezer")}}
+ {{$t("This allows listening history, flow and recommendations to work properly.")}}
@@ -88,8 +133,8 @@
- Login with LastFM
- Connect your LastFM account to allow scrobbling.
+ {{$t("Login with LastFM")}}
+ {{$t("Connect your LastFM account to allow scrobbling.")}}
@@ -97,32 +142,32 @@
mdi-logout
- Disconnect LastFM
+ {{$t("Disconnect LastFM")}}
-
+
- Discord Rich Presence
- Enable Discord Rich Presence, requires restart to toggle!
+ {{$t("Discord Rich Presence")}}
+ {{$t("Enable Discord Rich Presence, requires restart to toggle!")}}
-
+
- Discord Join Button
- Enable Discord join button for syncing tracks, requires restart to toggle!
+ {{$t("Discord Join Button")}}
+ {{$t("Enable Discord join button for syncing tracks, requires restart to toggle!")}}
- Other
+ {{$t("Other")}}
@@ -131,7 +176,7 @@
- Minimize to tray
+ {{$t("Minimize to tray")}}
@@ -140,22 +185,21 @@
- Close on exit
- Don't minimize to tray
+ {{$t("Close on exit")}}
+ {{$t("Don't minimize to tray")}}
mdi-logout
- Logout
+ {{$t("Logout")}}
-
+
mdi-content-save
- Save
@@ -192,7 +236,17 @@ export default {
downloadQuality: null,
devToolsCounter: 0,
snackbarText: null,
- snackbar: false
+ snackbar: false,
+ language: 'en',
+ languages: [
+ {code: 'en', name: 'English'},
+ {code: 'ar', name: 'Arabic'},
+ {code: 'de', name: 'German'},
+ ],
+ primaryColorIndex: 0,
+ primaries: ['#F44336', '#E91E63', '#9C27B0', '#673AB7', '#3F51B5', '#2196F3', '#03A9F4',
+ '#00BCD4', '#009688', '#4CAF50', '#8BC34A', '#CDDC39', '#FFEB3B', '#FFC107', '#FF9800', '#FF5722',
+ '#795548', '#607D8B', '#9E9E9E', '#333333', '#000000']
}
},
methods: {
@@ -202,6 +256,8 @@ export default {
this.$root.saveSettings();
//Artificial wait to make it seem like something happened.
setTimeout(() => {this.saving = false;}, 500);
+ this.snackbarText = this.$t("Settings saved!");
+ this.snackbar = true;
},
getQuality(v) {
let i = this.qualities.indexOf(v);
@@ -228,7 +284,7 @@ export default {
selectDownloadPath() {
//Electron check
if (!this.$root.settings.electron) {
- alert('Available only in Electron version!');
+ alert(this.$t("Available only in Electron version!"));
return;
}
const {ipcRenderer} = window.require('electron');
@@ -252,6 +308,24 @@ export default {
this.$root.settings.lastFM = null;
await this.$root.saveSettings();
window.location.reload();
+ },
+ changeColor() {
+ this.$vuetify.theme.themes.dark.primary = this.primaries[this.primaryColorIndex];
+ this.$root.settings.primaryColor = this.primaries[this.primaryColorIndex];
+ this.primaryColorIndex++;
+ if (this.primaryColorIndex == this.primaries.length)
+ this.primaryColorIndex = 0;
+ },
+ updateLanguage(l) {
+ let code = this.languages.filter(lang => lang.name == l)[0].code;
+ this.language = code;
+ this.$root.updateLanguage(code);
+ this.$root.settings.language = code;
+ }
+ },
+ computed: {
+ languageNames() {
+ return this.languages.map(l => l.name);
}
},
mounted() {
@@ -272,6 +346,18 @@ export default {
remote.getCurrentWindow().toggleDevTools();
}
}
+
+ //Shhhhhh
+ if (event.code == 'KeyC' && event.shiftKey) {
+ this.changeColor();
+ }
+
+ //SSHHSHSHHSH
+ if (event.code == 'KeyG' && event.shiftKey && event.altKey) {
+ setInterval(() => {
+ this.changeColor();
+ }, 400);
+ }
});
}
}
diff --git a/app/client/vue.config.js b/app/client/vue.config.js
index ef6e86b..dd49452 100644
--- a/app/client/vue.config.js
+++ b/app/client/vue.config.js
@@ -1,5 +1,14 @@
module.exports = {
"transpileDependencies": [
"vuetify"
- ]
-}
\ No newline at end of file
+ ],
+
+ pluginOptions: {
+ i18n: {
+ locale: 'en',
+ fallbackLocale: 'en',
+ localeDir: 'locales',
+ enableInSFC: true
+ }
+ }
+}
diff --git a/app/package-lock.json b/app/package-lock.json
index b028e12..29d95da 100644
--- a/app/package-lock.json
+++ b/app/package-lock.json
@@ -1,6 +1,6 @@
{
"name": "freezer",
- "version": "1.0.9",
+ "version": "1.0.10",
"lockfileVersion": 1,
"requires": true,
"dependencies": {
@@ -1505,8 +1505,8 @@
"integrity": "sha512-V4aYg89jEoVRxRb2fJdAg8FHvI7cEyYdVAh94HH0UIK8oJxUfkjlDQN9RbMx+bEjP7+ggMiFRprSti032Oipxw=="
},
"nodeezcryptor": {
- "version": "git+https://notabug.org/xefglm/nodeezcryptor#26d049cba14fa1f5ee32a52f23f4eda05d9feeb4",
- "from": "git+https://notabug.org/xefglm/nodeezcryptor",
+ "version": "git+https://codeberg.org/exttex/nodeezcryptor#78d99b64127256a1590d452b4804c4e38db24e97",
+ "from": "git+https://codeberg.org/exttex/nodeezcryptor",
"requires": {
"bindings": "^1.5.0",
"node-addon-api": "^2.0.0"
diff --git a/app/package.json b/app/package.json
index deefda0..06457c8 100644
--- a/app/package.json
+++ b/app/package.json
@@ -1,7 +1,7 @@
{
"name": "freezer",
"private": true,
- "version": "1.0.9",
+ "version": "1.1.0",
"description": "",
"main": "background.js",
"scripts": {
@@ -17,7 +17,7 @@
"lastfmapi": "^0.1.1",
"metaflac-js2": "^1.0.7",
"nedb": "^1.8.0",
- "nodeezcryptor": "git+https://notabug.org/xefglm/nodeezcryptor",
+ "nodeezcryptor": "git+https://codeberg.org/exttex/nodeezcryptor",
"sanitize-filename": "^1.6.3",
"socket.io": "^2.3.0",
"winston": "^3.3.3"
diff --git a/app/src/deezer.js b/app/src/deezer.js
index d7aa065..d670ea8 100644
--- a/app/src/deezer.js
+++ b/app/src/deezer.js
@@ -2,7 +2,8 @@ const crypto = require('crypto');
const axios = require('axios');
const decryptor = require('nodeezcryptor');
const querystring = require('querystring');
-const {Transform} = require('stream');
+const https = require('https');
+const {Transform, Readable} = require('stream');
const {Track} = require('./definitions');
const logger = require('./winston');
@@ -63,6 +64,11 @@ class DeezerAPI {
}
}
+ //Invalid CSRF
+ if (data.data.error && data.data.error.VALID_TOKEN_REQUIRED) {
+ await this.callApi('deezer.getUserData');
+ return await this.callApi(method, args, gatewayInput);
+ }
return data.data;
}
@@ -118,6 +124,13 @@ class DeezerAPI {
});
data = JSON.parse(data.toString('utf-8'));
+
+ //Invalid CSRF
+ if (data.error && data.error.VALID_TOKEN_REQUIRED) {
+ await this.callApi('deezer.getUserData');
+ return await this.callApi(method, args, gatewayInput);
+ }
+
return data;
}
@@ -131,6 +144,15 @@ class DeezerAPI {
return true;
}
+ async callPublicApi(path, params) {
+ let res = await axios({
+ url: `https://api.deezer.com/${encodeURIComponent(path)}/${encodeURIComponent(params)}`,
+ responseType: 'json',
+ method: 'GET'
+ });
+ return res.data;
+ }
+
//Get track URL
static getUrl(trackId, md5origin, mediaVersion, quality = 3) {
const magic = Buffer.from([0xa4]);
@@ -165,46 +187,118 @@ class DeezerAPI {
return `https://e-cdns-proxy-${md5origin.substring(0, 1)}.dzcdn.net/mobile/1/${step3}`;
}
- //Quality fallback
- async qualityFallback(info, quality = 3) {
- if (quality == 1) return {
- quality: '128kbps',
- format: 'MP3',
- source: 'stream',
- url: `/stream/${info}?q=1`
- };
+
+ async fallback(info, quality = 3) {
+ let qualityInfo = Track.getUrlInfo(info);
+
+ //User uploaded MP3s
+ if (qualityInfo.trackId.startsWith('-')) {
+ qualityInfo.quality = 3;
+ return qualityInfo;
+ }
+
+ //Quality fallback
+ let newQuality = await this.qualityFallback(qualityInfo, quality);
+ if (newQuality != null) {
+ return qualityInfo;
+ }
+ //ID Fallback
+ let trackData = await this.callApi('deezer.pageTrack', {sng_id: qualityInfo.trackId});
try {
- let tdata = Track.getUrlInfo(info);
- let res = await axios.head(DeezerAPI.getUrl(tdata.trackId, tdata.md5origin, tdata.mediaVersion, quality));
- if (quality == 3) {
- return {
- quality: '320kbps',
- format: 'MP3',
- source: 'stream',
- url: `/stream/${info}?q=3`
- }
- }
- //Bitrate will be calculated in client
- return {
- quality: res.headers['content-length'],
- format: 'FLAC',
- source: 'stream',
- url: `/stream/${info}?q=9`
+ if (trackData.results.DATA.FALLBACK.SNG_ID.toString() != qualityInfo.trackId) {
+ let newId = trackData.results.DATA.FALLBACK.SNG_ID.toString();
+ let newTrackData = await this.callApi('deezer.pageTrack', {sng_id: newId});
+ let newTrack = new Track(newTrackData.results.DATA);
+ return this.fallback(newTrack.streamUrl);
}
} catch (e) {
- logger.warn('Qualiy fallback: ' + e);
+ logger.warn('TrackID Fallback failed: ' + e);
+ }
+ //ISRC Fallback
+ try {
+ let publicTrack = this.callPublicApi('track', 'isrc:' + trackData.results.DATA.ISRC);
+ let newId = publicTrack.id.toString();
+ let newTrackData = await this.callApi('deezer.pageTrack', {sng_id: newId});
+ let newTrack = new Track(newTrackData.results.DATA);
+ return this.fallback(newTrack.streamUrl);
+ } catch (e) {
+ logger.warn('ISRC Fallback failed: ' + e);
+ }
+ return null;
+ }
+
+ //Fallback thru available qualities, -1 if none work
+ async qualityFallback(info, quality = 3) {
+ try {
+ let res = await axios.head(DeezerAPI.getUrl(info.trackId, info.md5origin, info.mediaVersion, quality));
+ if (res.status > 400) throw new Error(`Status code: ${res.status}`);
+ //Make sure it's an int
+ info.quality = parseInt(quality.toString(), 10);
+ info.size = parseInt(res.headers['content-length'], 10);
+ return info;
+ } catch (e) {
+ logger.warn('Quality fallback: ' + e);
//Fallback
//9 - FLAC
//3 - MP3 320
//1 - MP3 128
- let q = quality;
- if (quality == 9) q = 3;
- if (quality == 3) q = 1;
- return this.qualityFallback(info, q);
+ let nq = -1;
+ if (quality == 3) nq = 1;
+ if (quality == 9) nq = 3;
+ if (quality == 1) return null;
+ return this.qualityFallback(info, nq);
}
}
}
+class DeezerStream extends Readable {
+ constructor(qualityInfo, options) {
+ super(options);
+ this.qualityInfo = qualityInfo;
+ this.ended = false;
+ }
+
+
+ async open(offset, end) {
+ //Prepare decryptor
+ this.decryptor = new DeezerDecryptionStream(this.qualityInfo.trackId, {offset});
+ this.decryptor.on('end', () => {
+ this.ended = true;
+ });
+
+ //Calculate headers
+ let offsetBytes = offset - (offset % 2048);
+ end = (end == -1) ? '' : end;
+ let url = DeezerAPI.getUrl(this.qualityInfo.trackId, this.qualityInfo.md5origin, this.qualityInfo.mediaVersion, this.qualityInfo.quality);
+
+ //Open request
+ await new Promise((res) => {
+ this.request = https.get(url, {headers: {'Range': `bytes=${offsetBytes}-${end}`}}, (r) => {
+ r.pipe(this.decryptor);
+ this.size = parseInt(r.headers['content-length'], 10) + offsetBytes;
+ res();
+ });
+ });
+ }
+
+ async _read() {
+ //Decryptor ended
+ if (this.ended)
+ return this.push(null);
+
+ this.decryptor.once('readable', () => {
+ this.push(this.decryptor.read());
+ });
+ }
+
+ _destroy(err, callback) {
+ this.request.destroy();
+ this.decryptor.destroy();
+ callback();
+ }
+
+}
+
class DeezerDecryptionStream extends Transform {
constructor(trackId, options = {offset: 0}) {
@@ -258,4 +352,4 @@ class DeezerDecryptionStream extends Transform {
}
-module.exports = {DeezerAPI, DeezerDecryptionStream};
\ No newline at end of file
+module.exports = {DeezerAPI, DeezerDecryptionStream, DeezerStream};
\ No newline at end of file
diff --git a/app/src/definitions.js b/app/src/definitions.js
index a9cbff2..7b9df57 100644
--- a/app/src/definitions.js
+++ b/app/src/definitions.js
@@ -1,3 +1,4 @@
+
//Datatypes, constructor parameters = gw_light API call.
class Track {
constructor(json) {
@@ -11,8 +12,8 @@ class Track {
this.artistString = this.artists.map((a) => a.name).join(', ');
this.album = new Album(json);
- this.trackNumber = parseInt((json.TRACK_NUMBER || 0).toString(), 10);
- this.diskNumber = parseInt((json.DISK_NUMBER || 0).toString(), 10);
+ this.trackNumber = parseInt((json.TRACK_NUMBER || 1).toString(), 10);
+ this.diskNumber = parseInt((json.DISK_NUMBER || 1).toString(), 10);
this.explicit = json['EXPLICIT_LYRICS'] == 1 ? true:false;
this.lyricsId = json.LYRICS_ID;
@@ -35,7 +36,7 @@ class Track {
if (info.charAt(32) == '1') md5origin += '.mp3';
let mediaVersion = parseInt(info.substring(33, 34)).toString();
let trackId = info.substring(35);
- return {trackId, md5origin, mediaVersion};
+ return new QualityInfo(md5origin, mediaVersion, trackId);
}
}
@@ -76,6 +77,7 @@ class Artist {
this.albumCount = albumsJson.total;
this.albums = albumsJson.data.map((a) => new Album(a));
this.topTracks = topJson.data.map((t) => new Track(t));
+ this.radio = json.SMARTRADIO;
}
}
@@ -277,5 +279,27 @@ class Lyric {
}
}
+class QualityInfo {
+ constructor(md5origin, mediaVersion, trackId, quality = 1, source='stream') {
+ this.md5origin = md5origin;
+ this.mediaVersion = mediaVersion;
+ this.trackId = trackId;
+ this.quality = quality;
+ this.source = source;
+ //For FLAC bitrate calculation
+ this.size = 1;
+
+ this.url = '';
+ }
+
+ //Generate direct stream URL
+ generateUrl() {
+ let md5 = this.md5origin.replace('.mp3', '');
+ let md5mp3bit = this.md5origin.includes('.mp3') ? '1' : '0';
+ let mv = this.mediaVersion.toString().padStart(2, '0');
+ this.url = `/stream/${md5}${md5mp3bit}${mv}${this.trackId}?q=${this.quality}`;
+ }
+}
+
module.exports = {Track, Album, Artist, Playlist, User, SearchResults,
DeezerImage, DeezerProfile, DeezerLibrary, DeezerPage, Lyrics};
\ No newline at end of file
diff --git a/app/src/downloads.js b/app/src/downloads.js
index 42c73d9..f2130ff 100644
--- a/app/src/downloads.js
+++ b/app/src/downloads.js
@@ -1,158 +1,46 @@
-const {Settings} = require('./settings');
-const {Track} = require('./definitions');
-const decryptor = require('nodeezcryptor');
-const fs = require('fs');
-const path = require('path');
-const logger = require('./winston');
-const https = require('https');
+const {DeezerAPI} = require('./deezer');
const Datastore = require('nedb');
+const {Settings} = require('./settings');
+const fs = require('fs');
+const https = require('https');
+const logger = require('./winston');
+const path = require('path');
+const decryptor = require('nodeezcryptor');
+const sanitize = require('sanitize-filename');
const ID3Writer = require('browser-id3-writer');
const Metaflac = require('metaflac-js2');
-const sanitize = require("sanitize-filename");
-const { DeezerAPI } = require('./deezer');
+const { Track, Lyrics } = require('./definitions');
-class Downloads {
- constructor(settings, qucb) {
- this.downloads = [];
- this.downloading = false;
- this.download;
+let deezer;
+class DownloadManager {
+
+ constructor(settings, callback) {
this.settings = settings;
- //Queue update callback
- this.qucb = qucb;
- }
-
- //Add track to queue
- async add(track, quality = null) {
- if (this.downloads.filter((e => e.id == track.id)).length > 0) {
- //Track already in queue
- return;
- }
-
- //Sanitize quality
- let q = this.settings.downloadsQuality;
- if (quality) q = parseInt(quality.toString(), 10);
-
- //Create download
- let outpath = this.generateTrackPath(track, q);
- let d = new Download(
- track,
- outpath,
- q,
- () => {this._downloadDone();}
- );
- this.downloads.push(d);
-
- //Update callback
- if (this.qucb) this.qucb();
-
- //Save to DB
- await new Promise((res, rej) => {
- this.db.insert(d.toDB(), (e) => {
- res();
- });
- });
- }
-
- generateTrackPath(track, quality) {
- //Generate filename
- let fn = this.settings.downloadFilename;
-
- //Disable feats for single artist
- let feats = '';
- if (track.artists.length >= 2) feats = track.artists.slice(1).map((a) => a.name).join(', ');
-
- let props = {
- '%title%': track.title,
- '%artists%': track.artistString,
- '%artist%': track.artists[0].name,
- '%feats%': feats,
- '%trackNumber%': (track.trackNumber ? track.trackNumber : 1).toString(),
- '%0trackNumber%': (track.trackNumber ? track.trackNumber : 1).toString().padStart(2, '0'),
- '%album%': track.album.title
- };
- for (let k of Object.keys(props)) {
- fn = fn.replace(new RegExp(k, 'g'), sanitize(props[k]));
- }
- //Generate folders
- let p = this.settings.downloadsPath;
- if (this.settings.createArtistFolder) p = path.join(p, sanitize(track.artists[0].name));
- if (this.settings.createAlbumFolder) p = path.join(p, sanitize(track.album.title));
-
- return path.join(p, fn);
- }
-
- async start() {
- //Already downloading
- if (this.download || this.downloads.length == 0) return;
-
- this.downloading = true;
- await this._downloadDone();
- }
-
- async stop() {
- //Not downloading
- if (!this.download || !this.downloading) return;
this.downloading = false;
- await this.download.stop();
+ this.callback = callback;
- //Back to queue if undone
- if (this.download.state < 3) this.downloads.unshift(this.download);
-
- this.download = null;
+ this.queue = [];
+ this.threads = [];
- //Update callback
- if (this.qucb) this.qucb();
+ this.updateRequests = 0;
}
- //On download finished
- async _downloadDone() {
- //Save to DB
- if (this.download) {
- await new Promise((res, rej) => {
- this.db.update({_id: this.download.id}, {
- state: this.download.state,
- fallback: this.download.fallback,
- }, (e) => {
- res();
- });
- // this.db.remove({_id: this.download.id}, (e) => {
- // res();
- // });
- });
- }
-
- this.download = null;
-
- //All downloads done
- if (this.downloads.length == 0 || this.downloading == false) {
- this.downloading = false;
- if (this.qucb) this.qucb();
- return;
- }
-
- this.download = this.downloads[0];
- this.downloads = this.downloads.slice(1);
- this.download.start();
-
- //Update callback
- if (this.qucb) this.qucb();
+ //Update DeezerAPI global
+ setDeezer(d) {
+ deezer = d;
}
- //Load downloads info
async load() {
this.db = new Datastore({filename: Settings.getDownloadsDB(), autoload: true});
- //Load downloads
- await new Promise((res, rej) => {
- this.db.find({}, (err, docs) => {
- if (err) return rej();
- if (!docs) return;
- for (let d of docs) {
- if (d.state < 3 && d.state >= 0) this.downloads.push(Download.fromDB(d, () => {this._downloadDone();}));
- //TODO: Ignore for now completed
+ //Load from DB
+ await new Promise((resolve) => {
+ this.db.find({state: 0}, (err, docs) => {
+ if (!err) {
+ this.queue = docs.map(d => Download.fromDB(d));
}
- res();
+ resolve();
});
});
@@ -162,138 +50,211 @@ class Downloads {
}
}
- //Remove download
- async delete(index) {
- //Clear all
- if (index == -1) {
- this.downloads = [];
- await new Promise((res, rej) => {
- this.db.remove({state: 0}, {multi: true}, (e) => {});
- res();
- });
+ async start() {
+ this.downloading = true;
+ await this.updateQueue();
+ }
- if (this.qucb) this.qucb();
+ async stop() {
+ this.downloading = false;
+ //Stop all threads
+ let nThreads = this.threads.length;
+ for (let i=nThreads-1; i>=0; i--) {
+ await this.threads[i].stop();
+ }
+ this.updateQueue();
+ }
+
+ async add(track, quality) {
+ //Sanitize quality
+ let q = this.settings.downloadsQuality;
+ if (quality)
+ q = parseInt(quality.toString(), 10);
+ let download = new Download(track, q, 0);
+
+ //Check if in queue
+ if (this.queue.some(d => d.track.id == track.id)) {
return;
}
- //Remove single
- if (index >= this.downloads.length) return;
- await new Promise((res, rej) => {
- this.db.remove({_id: this.downloads[index].id}, {}, (e) => {});
- res();
- });
- this.downloads.splice(index, 1);
+ //Check if in DB
+ let dbDownload = await new Promise((resolve) => {
+ this.db.find({_id: download.track.id}, (err, docs) => {
+ if (err) return resolve(null);
+ if (docs.length == 0) return resolve(null);
- if (this.qucb) this.qucb();
+ //Update download as not done, will be skipped while downloading
+ this.db.update({_id: download.track.id}, {state: 0, quality: download.quality}, {}, () => {
+ resolve(Download.fromDB(docs[0]));
+ });
+ });
+ });
+
+ //Insert to DB
+ if (!dbDownload) {
+ await new Promise((resolve) => {
+ this.db.insert(download.toDB(), () => {
+ resolve();
+ });
+ });
+ }
+
+ //Queue
+ this.queue.push(download);
+ this.updateQueue();
+ }
+
+ async delete(index) {
+ //-1 = Delete all
+ if (index == -1) {
+ let ids = this.queue.map(q => q.track.id);
+ this.queue = [];
+ //Remove from DB
+ await new Promise((res) => {
+ this.db.remove({_id: {$in: ids}}, {multi: true}, () => {
+ res();
+ })
+ });
+ this.updateQueue();
+ return;
+ }
+
+ //Remove single item
+ let id = this.queue[index].track.id;
+ this.queue.splice(index, 1);
+ await new Promise((res) => {
+ this.db.remove({_id: id}, {}, () => {
+ res();
+ })
+ })
+ this.updateQueue();
+ }
+
+ //Thread safe update
+ async updateQueue() {
+ this.updateRequests++;
+ if (this._updatePromise) return;
+ this._updatePromise = this._updateQueue();
+ await this._updatePromise;
+ this._updatePromise = null;
+ this.updateRequests--;
+ if (this.updateRequests > 0) {
+ this.updateRequests--;
+ this.updateQueue();
+ }
+ }
+
+ async _updateQueue() {
+ //Finished downloads
+ if (this.threads.length > 0) {
+ for (let i=this.threads.length-1; i>=0; i--) {
+ if (this.threads[i].download.state == 3 || this.threads[i].download.state == -1) {
+ //Update DB
+ await new Promise((resolve) => {
+ this.db.update({_id: this.threads[i].download.track.id}, {state: this.threads[i].download.state}, {}, () => {
+ resolve();
+ });
+ });
+ this.threads.splice(i, 1);
+ } else {
+ //Remove if stopped
+ if (this.threads[i].stopped) {
+ this.queue.unshift(this.threads[i].download);
+ this.threads.splice(i, 1);
+ }
+ }
+ }
+ }
+ //Create new threads
+ if (this.downloading) {
+ let nThreads = this.settings.downloadThreads - this.threads.length;
+ for (let i=0; i 0) {
+ let thread = new DownloadThread(this.queue[0], () => {this.updateQueue();}, this.settings);
+ thread.start();
+ this.threads.push(thread);
+ this.queue.splice(0, 1);
+ }
+ }
+ }
+ //Stop downloading if queues empty
+ if (this.queue.length == 0 && this.threads.length == 0 && this.downloading)
+ this.downloading = false;
+
+ //Update UI
+ if (this.callback)
+ this.callback();
}
}
-class Download {
- constructor(track, path, quality, onDone) {
- this.track = track;
- this.id = track.id;
- this.path = path;
- this.quality = quality;
- this.onDone = onDone;
-
- //States:
- //0 - none/stopped
- //1 - downloading
- //2 - post-processing
- //3 - done
- //-1 - download error
- this.state = 0;
- this.fallback = false;
-
- this._request;
- //Post Processing Promise
- this._ppp;
-
- this.downloaded = 0;
- this.size = 0;
+class DownloadThread {
+ constructor (download, callback, settings) {
+ this.download = download;
+ this.callback = callback;
+ this.settings = settings;
+ this.stopped = true;
+ this.isUserUploaded = download.track.id.toString().startsWith('-');
}
- //Serialize to database json
- toDB() {
- return {
- _id: this.id,
- path: this.path,
- quality: this.quality,
- track: this.track,
- state: this.state,
- fallback: this.fallback
- }
-
- }
-
- //Create download from DB document
- static fromDB(doc, onDone) {
- let d = new Download(doc.track, doc.path, doc.quality, onDone);
- d.fallback = doc.fallback ? true : false; //Null check
- d.state = doc.state;
- return d;
+ //Callback wrapper
+ _cb() {
+ if (this.callback) this.callback();
}
async start() {
- this.state = 1;
+ this.download.state = 1;
+ this.stopped = false;
+ //Fallback
+ this.qualityInfo = await deezer.fallback(this.download.track.streamUrl, this.download.quality);
+ if (!this.qualityInfo) {
+ this.download.state = -1;
+ return;
+ }
+
+ //Get track info
+ if (!this.isUserUploaded) {
+ this.rawTrack = await deezer.callApi('deezer.pageTrack', {'sng_id': this.download.track.id});
+ this.track = new Track(this.rawTrack.results.DATA);
+ this.publicTrack = await deezer.callPublicApi('track', this.track.id);
+ this.publicAlbum = await deezer.callPublicApi('album', this.track.album.id);
+ }
+
+ //Check if exists
+ let outPath = this.generatePath(this.qualityInfo.quality);
+ try {
+ await fs.promises.access(outPath, fs.constants.R_OK);
+ //File exists
+ this.download.state = 3;
+ return this._cb();
+ } catch (_) {}
+
//Path to temp file
- let tmp = path.join(Settings.getTempDownloads(), `${this.track.id}.ENC`);
+ let tmp = path.join(Settings.getTempDownloads(), `${this.download.track.id}.ENC`);
//Get start offset
let start = 0;
try {
let stat = await fs.promises.stat(tmp);
if (stat.size) start = stat.size;
+
+ // eslint-disable-next-line no-empty
} catch (e) {}
- this.downloaded = start;
+ this.download.downloaded = start;
- //Get download info
- let streamInfo = Track.getUrlInfo(this.track.streamUrl);
- this.url = DeezerAPI.getUrl(streamInfo.trackId, streamInfo.md5origin, streamInfo.mediaVersion, this.quality);
- this._request = https.get(this.url, {headers: {'Range': `bytes=${start}-`}}, (r) => {
+ //Download
+ let url = DeezerAPI.getUrl(this.qualityInfo.trackId, this.qualityInfo.md5origin, this.qualityInfo.mediaVersion, this.qualityInfo.quality);
+ if (this.stopped) return;
+ this._request = https.get(url, {headers: {'Range': `bytes=${start}-`}}, (r) => {
+ this._response = r;
let outFile = fs.createWriteStream(tmp, {flags: 'a'});
- let skip = false;
- //Error
- if (r.statusCode >= 400) {
- //Fallback on error
- if (this.quality > 1) {
- if (this.quality == 3) this.quality = 1;
- if (this.quality == 9) this.quality = 3;
- this.url = null;
- this.fallback = true;
- return this.start();
- };
- //Error
- this.state = -1;
- logger.error(`Undownloadable track ID: ${this.track.id}`);
- return this.onDone();
- } else {
- this.path += (this.quality == 9) ? '.flac' : '.mp3';
-
- //Check if file exits
- fs.access(this.path, (err) => {
- if (err) {
-
- } else {
- logger.warn('File already exists! Skipping...');
- outFile.close();
- skip = true;
- this._request.end();
- this.state = 3;
- return this.onDone();
- }
-
- })
- }
-
+
//On download done
r.on('end', () => {
- if (skip) return;
- if (this.downloaded != this.size) return;
+ if (this.download.size != this.download.downloaded) return;
outFile.on('finish', () => {
outFile.close(() => {
- this._finished(tmp);
+ this.postPromise = this._post(tmp);
});
});
outFile.end();
@@ -301,7 +262,7 @@ class Download {
//Progress
r.on('data', (c) => {
outFile.write(c);
- this.downloaded += c.length;
+ this.download.downloaded += c.length;
});
r.on('error', (e) => {
@@ -311,53 +272,136 @@ class Download {
//Save size
this.size = parseInt(r.headers['content-length'], 10) + start;
-
+ this.download.size = this.size;
});
}
- //Stop current request
async stop() {
- this._request.destroy();
- this._request = null;
- this.state = 0;
- if (this._ppp) await this._ppp;
+ //If post processing, wait for it
+ if (this.postPromise) {
+ await this._postPromise;
+ return this._cb();
+ }
+
+ //Cancel download
+ if (this._response)
+ this._response.destroy();
+ if (this._request)
+ this._request.destroy();
+
+ // this._response = null;
+ // this._request = null;
+
+ this.stopped = true;
+ this.download.state = 0;
+ this._cb();
}
- async _finished(tmp) {
- this.state = 2;
-
- //Create post processing promise
- let resolve;
- this._ppp = new Promise((res, rej) => {
- resolve = res;
- });
-
- //Prepare output directory
- try {
- await fs.promises.mkdir(path.dirname(this.path), {recursive: true})
- } catch (e) {};
+ async _post(tmp) {
+ this.download.state = 2;
//Decrypt
- //this.path += (this.quality == 9) ? '.flac' : '.mp3';
- decryptor.decryptFile(decryptor.getKey(this.track.id), tmp, `${tmp}.DEC`);
- await fs.promises.copyFile(`${tmp}.DEC`, this.path);
- //Delete encrypted
- await fs.promises.unlink(tmp);
+ decryptor.decryptFile(decryptor.getKey(this.qualityInfo.trackId), tmp, `${tmp}.DEC`);
+ let outPath = this.generatePath(this.qualityInfo.quality);
+ await fs.promises.mkdir(path.dirname(outPath), {recursive: true});
+ await fs.promises.copyFile(`${tmp}.DEC`, outPath);
await fs.promises.unlink(`${tmp}.DEC`);
+ await fs.promises.unlink(tmp);
- //Tags
- await this.tagAudio(this.path, this.track);
+ if (!this.isUserUploaded) {
+ //Tag
+ await this.tagTrack(outPath);
- //Finish
- this.state = 3;
- resolve();
- this._ppp = null;
- this.onDone();
+ //Lyrics
+ if (this.settings.downloadLyrics) {
+ let lrcFile = outPath.split('.').slice(0, -1).join('.') + '.lrc';
+ let lrc;
+ try {
+ lrc = this.generateLRC();
+ } catch (e) {
+ logger.warn('Error getting lyrics! ' + e);
+ }
+ if (lrc) {
+ await fs.promises.writeFile(lrcFile, lrc, {encoding: 'utf-8'});
+ }
+ }
+ }
+
+
+ this.download.state = 3;
+ this._cb();
+ }
+
+ async tagTrack(path) {
+ let cover;
+ try {
+ cover = await this.downloadCover(this.track.albumArt.full);
+ } catch (e) {}
+
+ //Genre tag
+ let genres = [];
+ if (this.publicAlbum.genres && this.publicAlbum.genres.data)
+ genres = this.publicAlbum.genres.data.map(g => g.name);
+
+ if (path.toLowerCase().endsWith('.mp3')) {
+ //Load
+ const audioData = await fs.promises.readFile(path);
+ const writer = new ID3Writer(audioData);
+
+ writer.setFrame('TIT2', this.track.title);
+ writer.setFrame('TPE1', this.track.artists.map((a) => a.name));
+ if (this.publicAlbum.artist) writer.setFrame('TPE2', this.publicAlbum.artist.name);
+ writer.setFrame('TALB', this.track.album.title);
+ writer.setFrame('TRCK', this.track.trackNumber);
+ writer.setFrame('TPOS', this.track.diskNumber);
+ writer.setFrame('TCON', genres);
+ let date = new Date(this.publicTrack.release_date);
+ writer.setFrame('TYER', date.getFullYear());
+ writer.setFrame('TDAT', `${date.getMonth().toString().padStart(2, '0')}${date.getDay().toString().padStart(2, '0')}`);
+ if (this.publicTrack.bpm > 2) writer.setFrame('TBPM', this.publicTrack.bpm);
+ if (this.publicAlbum.label) writer.setFrame('TPUB', this.publicAlbum.label);
+ writer.setFrame('TSRC', this.publicTrack.isrc);
+ if (this.rawTrack.results.LYRICS) writer.setFrame('USLT', {
+ lyrics: this.rawTrack.results.LYRICS.LYRICS_TEXT,
+ language: 'eng',
+ description: 'Unsychronised lyrics'
+ });
+
+ if (cover) writer.setFrame('APIC', {type: 3, data: cover, description: 'Cover'});
+ writer.addTag();
+
+ //Write
+ await fs.promises.writeFile(path, Buffer.from(writer.arrayBuffer));
+ return;
+ }
+
+ //Tag FLAC
+ if (path.toLowerCase().endsWith('.flac')) {
+ const flac = new Metaflac(path);
+ flac.removeAllTags();
+
+ flac.setTag(`TITLE=${this.track.title}`);
+ flac.setTag(`ALBUM=${this.track.album.title}`);
+ flac.setTag(`ARTIST=${this.track.artistString}`);
+ flac.setTag(`TRACKNUMBER=${this.track.trackNumber}`);
+ flac.setTag(`DISCNUMBER=${this.track.diskNumber}`);
+ if (this.publicAlbum.artist) flac.setTag(`ALBUMARTIST=${this.publicAlbum.artist.name}`);
+ flac.setTag(`GENRE=${genres.join(", ")}`);
+ flac.setTag(`DATE=${this.publicTrack.release_date}`);
+ if (this.publicTrack.bpm > 2) flac.setTag(`BPM=${this.publicTrack.bpm}`);
+ if (this.publicAlbum.label) flac.setTag(`LABEL=${this.publicAlbum.label}`);
+ flac.setTag(`ISRC=${this.publicTrack.isrc}`);
+ if (this.publicAlbum.upc) flac.setTag(`BARCODE=${this.publicAlbum.upc}`);
+ if (this.rawTrack.results.LYRICS) flac.setTag(`LYRICS=${this.rawTrack.results.LYRICS.LYRICS_TEXT}`);
+
+ if (cover) flac.importPicture(cover);
+
+ flac.save();
+ }
}
- //Download cover to buffer
async downloadCover(url) {
- return await new Promise((res, rej) => {
+ return await new Promise((res) => {
let out = Buffer.alloc(0);
https.get(url, (r) => {
r.on('data', (d) => {
@@ -370,49 +414,105 @@ class Download {
});
}
- //Write tags to audio file
- async tagAudio(path, track) {
- let cover;
- try {
- cover = await this.downloadCover(track.albumArt.full);
- } catch (e) {}
+ generateLRC() {
+ //Check if exists
+ if (!this.rawTrack.results.LYRICS || !this.rawTrack.results.LYRICS.LYRICS_SYNC_JSON) return;
+ let lyrics = new Lyrics(this.rawTrack.results.LYRICS);
+ if (lyrics.lyrics.length == 0) return;
+ //Metadata
+ let out = `[ar:${this.track.artistString}]\r\n[al:${this.track.album.title}]\r\n[ti:${this.track.title}]\r\n`;
+ //Lyrics
+ for (let l of lyrics.lyrics) {
+ if (l.lrcTimestamp && l.text)
+ out += `${l.lrcTimestamp}${l.text}\r\n`;
+ }
+ return out;
+ }
+
+ generatePath(quality) {
+ //User uploaded mp3s
+ if (this.isUserUploaded) {
+ //Generate path
+ let p = this.settings.downloadsPath;
+ if (this.settings.createArtistFolder && this.download.track.artists[0].name.length > 0)
+ p = path.join(p, sanitize(this.download.track.artists[0].name));
+ if (this.settings.createAlbumFolder && this.download.track.album.title.length > 0)
+ p = path.join(p, sanitize(this.download.track.album.title));
+ //Filename
+ let out = path.join(p, sanitize(this.download.track.title));
+ if (!out.includes('.'))
+ out += '.mp3';
+ return out;
+ }
+
+ //Generate filename
+ let fn = this.settings.downloadFilename;
+ //Disable feats for single artist
+ let feats = '';
+ if (this.track.artists.length >= 2)
+ feats = this.track.artists.slice(1).map((a) => a.name).join(', ');
- if (path.toLowerCase().endsWith('.mp3')) {
- //Load
- const audioData = await fs.promises.readFile(path);
- const writer = new ID3Writer(audioData);
-
- writer.setFrame('TIT2', track.title);
- if (track.artists) writer.setFrame('TPE1', track.artists.map((a) => a.name));
- if (track.album) writer.setFrame('TALB', track.album.title);
- if (track.trackNumber) writer.setFrame('TRCK', track.trackNumber);
- if (cover) writer.setFrame('APIC', {
- type: 3,
- data: cover,
- description: 'Cover'
- });
- writer.addTag();
-
- //Write
- await fs.promises.writeFile(path, Buffer.from(writer.arrayBuffer));
+ //Date
+ let date = new Date(this.publicTrack.release_date);
+
+ let props = {
+ '%title%': this.track.title,
+ '%artists%': this.track.artistString,
+ '%artist%': this.track.artists[0].name,
+ '%feats%': feats,
+ '%trackNumber%': (this.track.trackNumber ? this.track.trackNumber : 1).toString(),
+ '%0trackNumber%': (this.track.trackNumber ? this.track.trackNumber : 1).toString().padStart(2, '0'),
+ '%album%': this.track.album.title,
+ '%year%': date.getFullYear().toString(),
+ };
+ for (let k of Object.keys(props)) {
+ fn = fn.replace(new RegExp(k, 'g'), sanitize(props[k]));
}
- //Tag FLAC
- if (path.toLowerCase().endsWith('.flac')) {
- const flac = new Metaflac(path);
- flac.removeAllTags();
+ //Generate folders
+ let p = this.settings.downloadsPath;
+ if (this.settings.createArtistFolder) p = path.join(p, sanitize(this.track.artists[0].name));
+ if (this.settings.createAlbumFolder) p = path.join(p, sanitize(this.track.album.title));
- flac.setTag(`TITLE=${track.title}`);
- if (track.album)flac.setTag(`ALBUM=${track.album.title}`);
- if (track.trackNumber) flac.setTag(`TRACKNUMBER=${track.trackNumber}`);
- if (track.artistString) flac.setTag(`ARTIST=${track.artistString}`);
- if (cover) flac.importPicture(cover);
-
- flac.save();
+ //Extension
+ if (quality.toString() == '9') {
+ fn += '.flac';
+ } else {
+ fn += '.mp3';
}
+ return path.join(p, fn);
}
}
+class Download {
+ constructor (track, quality, state) {
+ this.track = track;
+ this.quality = quality;
+ // 0 - none
+ // 1 - downloading
+ // 2 - postprocess
+ // 3 - done
+ // -1 - error
+ this.state = state;
-module.exports = {Downloads, Download};
\ No newline at end of file
+ //Updated from threads
+ this.downloaded = 0;
+ this.size = 1;
+ }
+
+ toDB() {
+ return {
+ _id: this.track.id,
+ track: this.track,
+ quality: this.quality,
+ state: this.state
+ }
+ }
+
+ static fromDB(json) {
+ return new Download(json.track, json.quality, json.state);
+ }
+}
+
+module.exports = {DownloadManager}
\ No newline at end of file
diff --git a/app/src/server.js b/app/src/server.js
index 3e29c60..1304b8c 100644
--- a/app/src/server.js
+++ b/app/src/server.js
@@ -4,15 +4,15 @@ const https = require('https');
const fs = require('fs');
const axios = require('axios').default;
const logger = require('./winston');
-const {DeezerAPI, DeezerDecryptionStream} = require('./deezer');
+const {DeezerAPI, DeezerStream} = require('./deezer');
const {Settings} = require('./settings');
const {Track, Album, Artist, Playlist, DeezerProfile, SearchResults, DeezerLibrary, DeezerPage, Lyrics} = require('./definitions');
-const {Downloads} = require('./downloads');
+const {DownloadManager} = require('./downloads');
const {Integrations} = require('./integrations');
let settings;
let deezer;
-let downloads;
+let downloadManager;
let integrations;
let sockets = [];
@@ -23,13 +23,16 @@ app.use(express.json({limit: '50mb'}));
app.use(express.static(path.join(__dirname, '../client', 'dist')));
//Server
const server = require('http').createServer(app);
-const io = require('socket.io').listen(server);
+const io = require('socket.io').listen(server, {
+ path: '/socket',
+});
//Get playback info
app.get('/playback', async (req, res) => {
try {
let data = await fs.promises.readFile(Settings.getPlaybackInfoPath(), 'utf-8');
return res.json(data);
+ // eslint-disable-next-line no-empty
} catch (e) {}
return res.json({});
@@ -53,7 +56,7 @@ app.get('/settings', (req, res) => {
app.post('/settings', async (req, res) => {
if (req.body) {
Object.assign(settings, req.body);
- downloads.settings = settings;
+ downloadManager.settings = settings;
integrations.updateSettings(settings);
await settings.save();
}
@@ -70,6 +73,9 @@ app.post('/authorize', async (req, res) => {
settings.arl = req.body.arl;
if (await (deezer.authorize())) {
+ //Update download manager
+ downloadManager.setDeezer(deezer);
+
res.status(200).send('OK');
return;
}
@@ -238,16 +244,22 @@ app.put('/library/:type', async (req, res) => {
app.get('/streaminfo/:info', async (req, res) => {
let info = req.params.info;
let quality = req.query.q ? req.query.q : 3;
- return res.json(await deezer.qualityFallback(info, quality));
+ let qualityInfo = await deezer.fallback(info, quality);
+
+ if (qualityInfo == null)
+ return res.sendStatus(404).end();
+
+ //Generate stream URL before sending
+ qualityInfo.generateUrl();
+ return res.json(qualityInfo);
});
// S T R E A M I N G
-app.get('/stream/:info', (req, res) => {
+app.get('/stream/:info', async (req, res) => {
//Parse stream info
let quality = req.query.q ? req.query.q : 3;
let streamInfo = Track.getUrlInfo(req.params.info);
- let url = DeezerAPI.getUrl(streamInfo.trackId, streamInfo.md5origin, streamInfo.mediaVersion, quality);
- let trackId = req.params.info.substring(35);
+ streamInfo.quality = quality;
//MIME type of audio
let mime = 'audio/mp3';
@@ -258,59 +270,38 @@ app.get('/stream/:info', (req, res) => {
if (req.headers.range) range = req.headers.range;
let rangeParts = range.replace(/bytes=/, '').split('-');
let start = parseInt(rangeParts[0], 10);
- let end = '';
+ let end = -1;
if (rangeParts.length >= 2) end = rangeParts[1];
+ if (end == '' || end == ' ') end = -1;
- //Round to 2048 for deezer
- let dStart = start - (start % 2048);
+ //Create Stream
+ let stream = new DeezerStream(streamInfo, {});
+ await stream.open(start, end);
- //Make request to Deezer CDN
- let _request = https.get(url, {headers: {'Range': `bytes=${dStart}-${end}`}}, (r) => {
- //Error from Deezer
- //TODO: Quality fallback
- if (r.statusCode < 200 || r.statusCode > 300) {
- res.status(404);
- return res.end();
- }
+ //Range header
+ if (req.headers.range) {
+ end = (end == -1) ? stream.size - 1 : end;
+ res.writeHead(206, {
+ 'Content-Range': `bytes ${start}-${end}/${stream.size}`,
+ 'Accept-Ranges': 'bytes',
+ 'Content-Length': stream.size - start,
+ 'Content-Type': mime
+ });
+
+ //Normal (non range) request
+ } else {
+ res.writeHead(200, {
+ 'Content-Length': stream.size,
+ 'Content-Type': mime
+ });
+ }
- let decryptor = new DeezerDecryptionStream(trackId, {offset: start});
-
- //Get total size
- let chunkSize = parseInt(r.headers["content-length"], 10)
- let total = chunkSize;
- if (start > 0) total += start;
-
- //Ranged request
- if (req.headers.range) {
- end = total - 1
-
- res.writeHead(206, {
- 'Content-Range': `bytes ${start}-${end}/${total}`,
- 'Accept-Ranges': 'bytes',
- 'Content-Length': chunkSize,
- 'Content-Type': mime
- });
-
- //Normal (non range) request
- } else {
- res.writeHead(200, {
- 'Content-Length': total,
- 'Content-Type': mime
- });
- }
-
- //Pipe: Deezer -> Decryptor -> Response
- decryptor.pipe(res);
- r.pipe(decryptor);
-
- });
- //Internet/Request error
- _request.on('error', () => {
- //console.log('Streaming error: ' + e);
- //HTML audio will restart automatically
+ //Should force HTML5 to retry
+ stream.on('error', () => {
res.destroy();
});
+ stream.pipe(res);
});
//Get deezer page
@@ -361,6 +352,18 @@ app.get('/smarttracklist/:id', async (req, res) => {
return res.send(tracks);
});
+//Artist smart radio
+app.get('/smartradio/:id', async (req, res) => {
+ let data = await deezer.callApi('smart.getSmartRadio', {art_id: req.params.id});
+ res.send(data.results.data.map(t => new Track(t)));
+});
+
+//Track Mix
+app.get('/trackmix/:id', async (req, res) => {
+ let data = await deezer.callApi('song.getContextualTrackMix', {sng_ids: [req.params.id]});
+ res.send(data.results.data.map(t => new Track(t)));
+});
+
//Load lyrics, ID = SONG ID
app.get('/lyrics/:id', async (req, res) => {
let data = await deezer.callApi('song.getLyrics', {
@@ -390,7 +393,7 @@ app.post('/downloads', async (req, res) => {
let tracks = req.body;
let quality = req.query.q;
for (let track of tracks) {
- downloads.add(track, quality);
+ downloadManager.add(track, quality);
}
res.status(200).send('OK');
@@ -398,30 +401,29 @@ app.post('/downloads', async (req, res) => {
//PUT to /download to start
app.put('/download', async (req, res) => {
- await downloads.start();
+ await downloadManager.start();
res.status(200).send('OK');
});
//DELETE to /download to stop/pause
app.delete('/download', async (req, res) => {
- await downloads.stop();
+ await downloadManager.stop();
res.status(200).send('OK');
})
//Get all downloads
app.get('/downloads', async (req, res) => {
res.json({
- downloading: downloads.downloading,
- downloads: downloads.downloads.map((d) => {
- return d.toDB();
- })
+ downloading: downloadManager.downloading,
+ queue: downloadManager.queue,
+ threads: downloadManager.threads.map(t => t.download)
});
});
-//Delete singel download
+//Delete single download
app.delete('/downloads/:index', async (req, res) => {
let index = parseInt(req.params.index, 10);
- await downloads.delete(index);
+ await downloadManager.delete(index);
res.status(200).end();
});
@@ -499,32 +501,27 @@ async function createServer(electron = false, ecb) {
deezer = new DeezerAPI(settings.arl, electron);
//Prepare downloads
- downloads = new Downloads(settings, () => {
+ downloadManager = new DownloadManager(settings, () => {
//Emit queue change to socket
sockets.forEach((s) => {
s.emit('downloads', {
- downloading: downloads.downloading,
- downloads: downloads.downloads
+ downloading: downloadManager.downloading,
+ queue: downloadManager.queue,
+ threads: downloadManager.threads.map(t => t.download)
});
});
});
- await downloads.load();
+ await downloadManager.load();
+ downloadManager.setDeezer(deezer);
//Emit download progress updates
setInterval(() => {
sockets.forEach((s) => {
- if (!downloads.download) {
- s.emit('download', null);
- return;
- }
- s.emit('download', {
- id: downloads.download.id,
- size: downloads.download.size,
- downloaded: downloads.download.downloaded,
- track: downloads.download.track,
- path: downloads.download.path
- });
+ if (!downloadManager.downloading && downloadManager.threads.length == 0)
+ return;
+
+ s.emit('currentlyDownloading', downloadManager.threads.map(t => t.download));
});
- }, 350);
+ }, 400);
//Integrations (lastfm, discord)
integrations = new Integrations(settings);
diff --git a/app/src/settings.js b/app/src/settings.js
index df76fc5..e2d86ec 100644
--- a/app/src/settings.js
+++ b/app/src/settings.js
@@ -28,6 +28,12 @@ class Settings {
this.lastFM = null;
this.enableDiscord = false;
this.discordJoin = false;
+
+ this.showAutocomplete = true;
+ this.downloadThreads = 4;
+ this.downloadLyrics = true;
+ this.primaryColor = '#2196F3';
+ this.language = 'en';
}
//Based on electorn app.getPath
@@ -57,7 +63,12 @@ class Settings {
}
//Get path to downloads database
static getDownloadsDB() {
- return path.join(Settings.getDir(), 'downloads.db');
+ //Delete old DB if exists
+ let oldPath = path.join(Settings.getDir(), 'downloads.db');
+ if (fs.existsSync(oldPath))
+ fs.unlink(oldPath, () => {});
+
+ return path.join(Settings.getDir(), 'downloads2.db');
}
//Get path to temporary / unfinished downlaods
static getTempDownloads() {
diff --git a/build/installerIcon.ico b/build/installerIcon.ico
new file mode 100644
index 0000000..76c2834
Binary files /dev/null and b/build/installerIcon.ico differ
diff --git a/build/uninstallerIcon.ico b/build/uninstallerIcon.ico
new file mode 100644
index 0000000..049accc
Binary files /dev/null and b/build/uninstallerIcon.ico differ
diff --git a/generate_translations.py b/generate_translations.py
new file mode 100644
index 0000000..ffcf875
--- /dev/null
+++ b/generate_translations.py
@@ -0,0 +1,17 @@
+import zipfile
+import json
+
+def generate():
+ with zipfile.ZipFile('translations.zip') as zip:
+ for file in zip.namelist():
+ if 'freezerpc.json' in file:
+ data = zip.open(file).read()
+ lang = file.split('/')[0].split('-')[0].lower()
+ if lang != 'en':
+ with open('app/client/src/locales/' + lang + '.json', 'wb') as f:
+ f.write(data)
+
+
+
+if __name__ == '__main__':
+ generate()
\ No newline at end of file
diff --git a/package.json b/package.json
index 70a133c..bc10168 100644
--- a/package.json
+++ b/package.json
@@ -1,7 +1,7 @@
{
"name": "freezer",
"private": true,
- "version": "1.0.9",
+ "version": "1.1.0",
"description": "",
"scripts": {
"pack": "electron-builder --dir",
@@ -16,6 +16,7 @@
},
"build": {
"appId": "com.exttex.freezer",
+ "productName": "Freezer",
"extraResources": [
{
"from": "app/assets/**",
@@ -29,13 +30,19 @@
],
"win": {
"target": [
- "portable"
+ "portable", "nsis"
],
"icon": "build/icon.ico",
"asarUnpack": [
"app/node_modules/nodeezcryptor/**"
]
},
+ "nsis": {
+ "oneClick": true,
+ "perMachine": false,
+ "allowElevation": false,
+ "allowToChangeInstallationDirectory": false
+ },
"linux": {
"target": [
"AppImage"