1.0.6 - Discord integration, logging, bugfixes
This commit is contained in:
@ -3,6 +3,8 @@ const axios = require('axios');
|
||||
const decryptor = require('nodeezcryptor');
|
||||
const querystring = require('querystring');
|
||||
const {Transform} = require('stream');
|
||||
const {Track} = require('./definitions');
|
||||
const logger = require('./winston');
|
||||
|
||||
class DeezerAPI {
|
||||
|
||||
@ -162,6 +164,45 @@ 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`
|
||||
};
|
||||
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`
|
||||
}
|
||||
} catch (e) {
|
||||
logger.warning('Qualiy 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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
class DeezerDecryptionStream extends Transform {
|
||||
|
@ -1,5 +1,3 @@
|
||||
const {DeezerAPI} = require('./deezer');
|
||||
|
||||
//Datatypes, constructor parameters = gw_light API call.
|
||||
class Track {
|
||||
constructor(json) {
|
||||
@ -32,13 +30,12 @@ class Track {
|
||||
}
|
||||
|
||||
//Get Deezer CDN url by streamUrl
|
||||
static getUrl(info, quality = 3) {
|
||||
static getUrlInfo(info) {
|
||||
let md5origin = info.substring(0, 32);
|
||||
if (info.charAt(32) == '1') md5origin += '.mp3';
|
||||
let mediaVersion = parseInt(info.substring(33, 34)).toString();
|
||||
let trackId = info.substring(35);
|
||||
let url = DeezerAPI.getUrl(trackId, md5origin, mediaVersion, quality);
|
||||
return url;
|
||||
return {trackId, md5origin, mediaVersion};
|
||||
}
|
||||
}
|
||||
|
||||
@ -124,6 +121,8 @@ class DeezerImage {
|
||||
}
|
||||
|
||||
url(size = 256) {
|
||||
if (!this.hash)
|
||||
return `https://e-cdns-images.dzcdn.net/images/${this.type}/${size}x${size}-000000-80-0-0.jpg`;
|
||||
return `https://e-cdns-images.dzcdn.net/images/${this.type}/${this.hash}/${size}x${size}-000000-80-0-0.jpg`;
|
||||
}
|
||||
}
|
||||
|
@ -3,11 +3,13 @@ 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 Datastore = require('nedb');
|
||||
const ID3Writer = require('browser-id3-writer');
|
||||
const Metaflac = require('metaflac-js2');
|
||||
const sanitize = require("sanitize-filename");
|
||||
const { DeezerAPI } = require('./deezer');
|
||||
|
||||
class Downloads {
|
||||
constructor(settings, qucb) {
|
||||
@ -246,9 +248,12 @@ class Download {
|
||||
this.downloaded = start;
|
||||
|
||||
//Get download info
|
||||
if (!this.url) this.url = Track.getUrl(this.track.streamUrl, this.quality);
|
||||
if (!this.url) {
|
||||
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) => {
|
||||
|
||||
let skip = false;
|
||||
//Error
|
||||
if (r.statusCode >= 400) {
|
||||
//Fallback on error
|
||||
@ -261,12 +266,31 @@ class Download {
|
||||
};
|
||||
//Error
|
||||
this.state = -1;
|
||||
console.log(`Undownloadable track ID: ${this.track.id}`);
|
||||
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) {
|
||||
//Pipe data to file
|
||||
r.pipe(fs.createWriteStream(tmp, {flags: 'a'}));
|
||||
|
||||
} else {
|
||||
logger.warn('File already exists! Skipping...');
|
||||
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;
|
||||
this._finished(tmp);
|
||||
});
|
||||
@ -276,15 +300,13 @@ class Download {
|
||||
});
|
||||
|
||||
r.on('error', (e) => {
|
||||
console.log(`Download error: ${e}`);
|
||||
logger.error(`Download error: ${e}`);
|
||||
//TODO: Download error handling
|
||||
})
|
||||
|
||||
//Save size
|
||||
this.size = parseInt(r.headers['content-length'], 10) + start;
|
||||
|
||||
//Pipe data to file
|
||||
r.pipe(fs.createWriteStream(tmp, {flags: 'a'}));
|
||||
|
||||
});
|
||||
}
|
||||
|
||||
@ -311,7 +333,7 @@ class Download {
|
||||
} catch (e) {};
|
||||
|
||||
//Decrypt
|
||||
this.path += (this.quality == 9) ? '.flac' : '.mp3';
|
||||
//this.path += (this.quality == 9) ? '.flac' : '.mp3';
|
||||
decryptor.decryptFile(decryptor.getKey(this.track.id), tmp, `${tmp}.DEC`);
|
||||
fs.promises.copyFile(`${tmp}.DEC`, this.path);
|
||||
//Delete encrypted
|
||||
|
142
app/src/integrations.js
Normal file
142
app/src/integrations.js
Normal file
@ -0,0 +1,142 @@
|
||||
const LastfmAPI = require('lastfmapi');
|
||||
const DiscordRPC = require('discord-rpc');
|
||||
const {EventEmitter} = require('events');
|
||||
const logger = require('./winston');
|
||||
|
||||
class Integrations extends EventEmitter {
|
||||
|
||||
//LastFM, Discord etc
|
||||
|
||||
constructor(settings) {
|
||||
super();
|
||||
|
||||
this.settings = settings;
|
||||
this.discordReady = false;
|
||||
this.discordRPC = null;
|
||||
|
||||
//LastFM
|
||||
//plz don't steal creds, it's just lastfm
|
||||
this.lastfm = new LastfmAPI({
|
||||
api_key: 'b6ab5ae967bcd8b10b23f68f42493829',
|
||||
secret: '861b0dff9a8a574bec747f9dab8b82bf'
|
||||
});
|
||||
this.authorizeLastFM();
|
||||
|
||||
//Discord
|
||||
if (settings.enableDiscord)
|
||||
this.connectDiscord();
|
||||
|
||||
}
|
||||
|
||||
updateSettings(settings) {
|
||||
this.settings = settings;
|
||||
}
|
||||
|
||||
//Autorize lastfm with saved credentials
|
||||
authorizeLastFM() {
|
||||
if (!this.settings.lastFM) return;
|
||||
this.lastfm.setSessionCredentials(this.settings.lastFM.name, this.settings.lastFM.key);
|
||||
}
|
||||
|
||||
//Login to lastfm by token
|
||||
async loginLastFM(token) {
|
||||
let response = await new Promise((res) => {
|
||||
this.lastfm.authenticate(token, (err, sess) => {
|
||||
if (err) res();
|
||||
res({
|
||||
name: sess.username,
|
||||
key: sess.key
|
||||
});
|
||||
});
|
||||
});
|
||||
this.settings.lastFM = response;
|
||||
this.authorizeLastFM();
|
||||
return response;
|
||||
}
|
||||
|
||||
//LastFM Scrobble
|
||||
async scrobbleLastFM(title, artist) {
|
||||
if (this.settings.lastFM)
|
||||
this.lastfm.track.scrobble({
|
||||
artist: artist,
|
||||
track: title,
|
||||
timestamp: Math.floor((new Date()).getTime() / 1000)
|
||||
});
|
||||
}
|
||||
|
||||
//Connect to discord client
|
||||
connectDiscord() {
|
||||
//Don't steal, k ty
|
||||
const CLIENTID = '759835951450292324';
|
||||
|
||||
this.discordReady = false;
|
||||
DiscordRPC.register(CLIENTID);
|
||||
this.discordRPC = new DiscordRPC.Client({transport: 'ipc'});
|
||||
this.discordRPC.on('connected', () => {
|
||||
this.discordReady = true;
|
||||
|
||||
//Allow discord "join" button
|
||||
if (this.settings.discordJoin) {
|
||||
//Always accept join requests
|
||||
this.discordRPC.subscribe('ACTIVITY_JOIN_REQUEST', (user) => {
|
||||
this.discordRPC.sendJoinInvite(user.user).catch((e) => {
|
||||
logger.warning('Unable to accept Discord invite: ' + e);
|
||||
});
|
||||
});
|
||||
//Joined
|
||||
this.discordRPC.subscribe('ACTIVITY_JOIN', async (data) => {
|
||||
let params = JSON.parse(data.secret);
|
||||
this.emit('discordJoin', params);
|
||||
});
|
||||
|
||||
}
|
||||
});
|
||||
//Connect to discord
|
||||
this.discordRPC.login({clientId: CLIENTID}).catch(() => {
|
||||
logger.info('Error connecting to Discord!');
|
||||
//Wait 5s to retry
|
||||
setTimeout(() => {
|
||||
if (!this.discordReady)
|
||||
this.connectDiscord();
|
||||
}, 5000);
|
||||
});
|
||||
}
|
||||
|
||||
//Called when playback state changed
|
||||
async updateState(data) {
|
||||
if (this.discordReady) {
|
||||
let richPresence = {
|
||||
state: data.track.artistString,
|
||||
details: data.track.title,
|
||||
largeImageKey: 'icon',
|
||||
instance: true,
|
||||
}
|
||||
//Show timestamp only if playing
|
||||
if (data.state == 2) {
|
||||
Object.assign(richPresence, {
|
||||
startTimestamp: Date.now() - data.position,
|
||||
endTimestamp: (Date.now() - data.position) + data.duration,
|
||||
});
|
||||
}
|
||||
//Enabled discord join
|
||||
if (this.settings.discordJoin) {
|
||||
Object.assign(richPresence, {
|
||||
partySize: 1,
|
||||
partyMax: 10,
|
||||
matchSecret: 'match_secret_' + data.track.id,
|
||||
joinSecret: JSON.stringify({
|
||||
pos: Math.floor(data.position),
|
||||
ts: Date.now(),
|
||||
id: data.track.id
|
||||
}),
|
||||
partyId: 'party_id_' + data.track.id
|
||||
});
|
||||
}
|
||||
//Set
|
||||
this.discordRPC.setActivity(richPresence);
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
module.exports = {Integrations};
|
@ -3,15 +3,17 @@ const path = require('path');
|
||||
const https = require('https');
|
||||
const fs = require('fs');
|
||||
const axios = require('axios').default;
|
||||
const LastfmAPI = require('lastfmapi');
|
||||
const logger = require('./winston');
|
||||
const {DeezerAPI, DeezerDecryptionStream} = require('./deezer');
|
||||
const {Settings} = require('./settings');
|
||||
const {Track, Album, Artist, Playlist, DeezerProfile, SearchResults, DeezerLibrary, DeezerPage, Lyrics} = require('./definitions');
|
||||
const {Downloads} = require('./downloads');
|
||||
const {Integrations} = require('./integrations');
|
||||
|
||||
let settings;
|
||||
let deezer;
|
||||
let downloads;
|
||||
let integrations;
|
||||
|
||||
let sockets = [];
|
||||
|
||||
@ -22,12 +24,6 @@ app.use(express.static(path.join(__dirname, '../client', 'dist')));
|
||||
//Server
|
||||
const server = require('http').createServer(app);
|
||||
const io = require('socket.io').listen(server);
|
||||
//LastFM
|
||||
//plz don't steal creds, it's just lastfm
|
||||
let lastfm = new LastfmAPI({
|
||||
api_key: 'b6ab5ae967bcd8b10b23f68f42493829',
|
||||
secret: '861b0dff9a8a574bec747f9dab8b82bf'
|
||||
});
|
||||
|
||||
//Get playback info
|
||||
app.get('/playback', async (req, res) => {
|
||||
@ -58,6 +54,7 @@ app.post('/settings', async (req, res) => {
|
||||
if (req.body) {
|
||||
Object.assign(settings, req.body);
|
||||
downloads.settings = settings;
|
||||
integrations.updateSettings(settings);
|
||||
await settings.save();
|
||||
}
|
||||
|
||||
@ -241,14 +238,15 @@ 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 qualityFallback(info, quality));
|
||||
return res.json(await deezer.qualityFallback(info, quality));
|
||||
});
|
||||
|
||||
// S T R E A M I N G
|
||||
app.get('/stream/:info', (req, res) => {
|
||||
//Parse stream info
|
||||
let quality = req.query.q ? req.query.q : 3;
|
||||
let url = Track.getUrl(req.params.info, quality);
|
||||
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);
|
||||
|
||||
//MIME type of audio
|
||||
@ -307,7 +305,7 @@ app.get('/stream/:info', (req, res) => {
|
||||
|
||||
});
|
||||
//Internet/Request error
|
||||
_request.on('error', (e) => {
|
||||
_request.on('error', () => {
|
||||
//console.log('Streaming error: ' + e);
|
||||
//HTML audio will restart automatically
|
||||
res.destroy();
|
||||
@ -367,6 +365,20 @@ app.get('/lyrics/:id', async (req, res) => {
|
||||
res.send(new Lyrics(data.results));
|
||||
});
|
||||
|
||||
//Search Suggestions
|
||||
app.get('/suggestions/:query', async (req, res) => {
|
||||
let query = req.params.query;
|
||||
try {
|
||||
let data = await deezer.callApi('search_getSuggestedQueries', {
|
||||
QUERY: query
|
||||
});
|
||||
let out = data.results.SUGGESTION.map((s) => s.QUERY);
|
||||
res.json(out);
|
||||
} catch (e) {
|
||||
res.json([]);
|
||||
}
|
||||
});
|
||||
|
||||
//Post list of tracks to download
|
||||
app.post('/downloads', async (req, res) => {
|
||||
let tracks = req.body;
|
||||
@ -410,12 +422,7 @@ app.delete('/downloads/:index', async (req, res) => {
|
||||
//Log listen to deezer & lastfm
|
||||
app.post('/log', async (req, res) => {
|
||||
//LastFM
|
||||
if (settings.lastFM)
|
||||
lastfm.track.scrobble({
|
||||
artist: req.body.artists[0].name,
|
||||
track: req.body.title,
|
||||
timestamp: Math.floor((new Date()).getTime() / 1000)
|
||||
});
|
||||
integrations.scrobbleLastFM(req.body.title, req.body.artists[0].name);
|
||||
|
||||
//Deezer
|
||||
if (settings.logListen)
|
||||
@ -436,26 +443,19 @@ app.get('/lastfm', async (req, res) => {
|
||||
//Got token
|
||||
if (req.query.token) {
|
||||
let token = req.query.token;
|
||||
await new Promise((res, rej) => {
|
||||
lastfm.authenticate(token, (err, sess) => {
|
||||
if (err) res();
|
||||
//Save to settings
|
||||
settings.lastFM = {
|
||||
name: sess.username,
|
||||
key: sess.key
|
||||
};
|
||||
settings.save();
|
||||
res();
|
||||
});
|
||||
});
|
||||
authorizeLastFM();
|
||||
//Authorize
|
||||
let authinfo = await integrations.loginLastFM(token);
|
||||
if (authinfo) {
|
||||
settings.lastFM = authinfo;
|
||||
settings.save();
|
||||
}
|
||||
//Redirect to homepage
|
||||
return res.redirect('/');
|
||||
}
|
||||
|
||||
//Get auth url
|
||||
res.json({
|
||||
url: lastfm.getAuthenticationUrl({cb: `http://${req.socket.remoteAddress}:${settings.port}/lastfm`})
|
||||
url: integrations.lastfm.getAuthenticationUrl({cb: `http://${req.socket.remoteAddress}:${settings.port}/lastfm`})
|
||||
}).end();
|
||||
});
|
||||
|
||||
@ -478,51 +478,12 @@ io.on('connection', (socket) => {
|
||||
socket.on('disconnect', () => {
|
||||
sockets.splice(sockets.indexOf(socket), 1);
|
||||
});
|
||||
//Send to integrations
|
||||
socket.on('stateChange', (data) => {
|
||||
integrations.updateState(data);
|
||||
});
|
||||
});
|
||||
|
||||
//Quality fallback
|
||||
async function qualityFallback(info, quality = 3) {
|
||||
if (quality == 1) return {
|
||||
quality: '128kbps',
|
||||
format: 'MP3',
|
||||
source: 'stream',
|
||||
url: `/stream/${info}?q=1`
|
||||
};
|
||||
try {
|
||||
let res = await axios.head(Track.getUrl(info, 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`
|
||||
}
|
||||
} catch (e) {
|
||||
//Fallback
|
||||
//9 - FLAC
|
||||
//3 - MP3 320
|
||||
//1 - MP3 128
|
||||
let q = quality;
|
||||
if (quality == 9) q = 3;
|
||||
if (quality == 3) q = 1;
|
||||
return qualityFallback(info, q);
|
||||
}
|
||||
}
|
||||
|
||||
//Autorize lastfm with saved credentials
|
||||
function authorizeLastFM() {
|
||||
if (!settings.lastFM) return;
|
||||
lastfm.setSessionCredentials(settings.lastFM.name, settings.lastFM.key);
|
||||
}
|
||||
|
||||
//ecb = Error callback
|
||||
async function createServer(electron = false, ecb) {
|
||||
//Prepare globals
|
||||
@ -559,8 +520,21 @@ async function createServer(electron = false, ecb) {
|
||||
});
|
||||
}, 350);
|
||||
|
||||
//LastFM
|
||||
authorizeLastFM();
|
||||
//Integrations (lastfm, discord)
|
||||
integrations = new Integrations(settings);
|
||||
//Discord Join = Sync tracks
|
||||
integrations.on('discordJoin', async (data) => {
|
||||
let trackData = await deezer.callApi('deezer.pageTrack', {sng_id: data.id});
|
||||
let track = new Track(trackData.results.DATA);
|
||||
let out = {
|
||||
track: track,
|
||||
position: (Date.now() - data.ts) + data.pos
|
||||
}
|
||||
//Emit to sockets
|
||||
sockets.forEach((s) => {
|
||||
s.emit('playOffset', out);
|
||||
});
|
||||
});
|
||||
|
||||
//Start server
|
||||
server.on('error', (e) => {
|
||||
|
@ -22,9 +22,12 @@ class Settings {
|
||||
this.createAlbumFolder = true;
|
||||
this.createArtistFolder = true;
|
||||
this.downloadFilename = '%0trackNumber%. %artists% - %title%';
|
||||
this.downloadDialog = true;
|
||||
|
||||
this.logListen = false;
|
||||
this.lastFM = null;
|
||||
this.enableDiscord = false;
|
||||
this.discordJoin = false;
|
||||
}
|
||||
|
||||
//Based on electorn app.getPath
|
||||
|
23
app/src/winston.js
Normal file
23
app/src/winston.js
Normal file
@ -0,0 +1,23 @@
|
||||
const winston = require('winston');
|
||||
const path = require('path');
|
||||
const {Settings} = require('./settings');
|
||||
const { Transform } = require('stream');
|
||||
|
||||
const logger = winston.createLogger({
|
||||
level: 'info',
|
||||
format: winston.format.simple(),
|
||||
transports: [
|
||||
new winston.transports.Console(),
|
||||
new winston.transports.File({filename: path.join(Settings.getDir(), "freezer-server.log")}),
|
||||
]
|
||||
});
|
||||
|
||||
//Node errors
|
||||
process.on('uncaughtException', (err) => {
|
||||
logger.error('Unhandled Exception: ' + err + "\nStack: " + err.stack);
|
||||
});
|
||||
process.on('unhandledRejection', (err) => {
|
||||
logger.error('Unhandled Rejection: ' + err + "\nStack: " + err.stack);
|
||||
})
|
||||
|
||||
module.exports = logger;
|
Reference in New Issue
Block a user