deemix-web-frontend/src/index.ts

347 lines
12 KiB
TypeScript
Raw Normal View History

2021-10-23 12:57:36 +02:00
import express from 'express';
import deezer from 'deezer-js';
import deemix from 'deemix';
import path from 'path';
import { promisify } from 'util';
import fs from 'fs';
import { exec } from 'child_process';
import timeago from 'timeago.js';
import toml from 'toml';
import winston from 'winston';
import WebSocket from 'ws';
import * as dotenv from 'dotenv';
import expressws from 'express-ws';
2021-10-21 20:21:51 +02:00
const logFormatter = winston.format.printf(({ level, message, timestamp }) => {
return `${new Date(timestamp).toLocaleDateString('en-GB', {timeZone: 'UTC'})} ${new Date(timestamp).toLocaleTimeString('en-GB', {timeZone: 'UTC'})} ${level.replace('info', 'I').replace('warn', '!').replace('error', '!!')}: ${message}`;
});
winston.addColors({
error: 'red',
debug: 'blue',
warn: 'yellow',
http: 'gray',
info: 'blue',
verbose: 'cyan',
silly: 'magenta'
});
const logger = winston.createLogger({
level: 'debug',
format: winston.format.combine(
winston.format.simple()
),
transports: [
2021-10-21 21:55:49 +02:00
new winston.transports.File({filename: 'deemix-web-fe-error.log', level: 'warn'}),
2021-10-21 20:21:51 +02:00
new winston.transports.File({filename: 'deemix-web-fe.log'}),
new winston.transports.Console({
format: winston.format.combine(
winston.format.timestamp(),
winston.format.colorize(),
logFormatter
)
})
]
});
2021-10-19 09:22:31 +02:00
2021-10-21 18:27:00 +02:00
if (!fs.existsSync('./config.toml')) {
if (!fs.existsSync('./config.example.toml')) {
2021-10-21 20:21:51 +02:00
logger.error('no config.toml OR config.example.toml found!!! what the hell are you up to!!!');
2021-10-21 18:27:00 +02:00
process.exit(1);
}
2021-10-21 20:21:51 +02:00
logger.warn('copying config.example.toml to config.toml as it was not found. the default config may not be preferable!');
2021-10-21 18:27:00 +02:00
fs.copyFileSync('./config.example.toml', './config.toml');
}
2021-10-23 12:57:36 +02:00
const config = toml.parse(fs.readFileSync('./config.toml', {encoding: 'utf8'}));
2021-10-21 20:21:51 +02:00
logger.info('loaded config');
2021-10-21 18:27:00 +02:00
2021-10-23 12:57:36 +02:00
let searchcache: Record<string, [Album]> = {};
let albumcache: Record<string, Album> = {};
let trackcache: Record<string, Track> = {};
2021-10-21 21:53:09 +02:00
2021-10-21 18:27:00 +02:00
const port = config.server.port || 4500;
const deleteTimer = config.timer.deleteTimer || 1000 * 60 * 25;
2021-10-19 09:22:31 +02:00
2021-10-23 12:57:36 +02:00
dotenv.config();
2021-10-19 09:22:31 +02:00
2021-10-23 12:57:36 +02:00
const app = express();
expressws(app);
2021-10-19 09:22:31 +02:00
const deezerInstance = new deezer.Deezer();
let deemixDownloader;
2021-10-23 12:57:36 +02:00
let deemixSettings = deemix.settings.DEFAULTS;
2021-10-19 09:22:31 +02:00
deemixSettings.downloadLocation = path.join(process.cwd(), 'data/');
2021-10-21 18:27:00 +02:00
deemixSettings.overwriteFile = deemix.settings.OverwriteOption.ONLY_TAGS;
2021-10-21 20:21:51 +02:00
const format = deezer.TrackFormats[config.deemix.trackFormat || 'FLAC'];
deemixSettings.maxBitrate = String(format);
deemixSettings.tracknameTemplate = config.deemix.trackNameTemplate || deemixSettings.tracknameTemplate;
deemixSettings.albumTracknameTemplate = config.deemix.albumTrackNameTemplate || deemixSettings.albumTracknameTemplate;
deemixSettings.albumNameTemplate = config.deemix.albumNameTemplate || deemixSettings.createM3U8File;
deemixSettings.createM3U8File = config.deemix.createM3U8File !== undefined ? config.deemix.createM3U8File : deemixSettings.createM3U8File;
deemixSettings.embeddedArtworkPNG = config.deemix.embeddedArtworkPNG !== undefined ? config.deemix.embeddedArtworkPNG : deemixSettings.embeddedArtworkPNG;
deemixSettings.embeddedArtworkSize = config.deemix.embeddedArtworkSize || deemixSettings.embeddedArtworkSize;
deemixSettings.saveArtwork = config.deemix.saveArtwork !== undefined ? config.deemix.saveArtwork : deemixSettings.saveArtwork;
deemixSettings.localArtworkSize = deemixSettings.localArtworkSize || deemixSettings.localArtworkSize;
deemixSettings.localArtworkFormat = deemixSettings.localArtworkFormat || deemixSettings.localArtworkFormat;
deemixSettings.jpegImageQuality = deemixSettings.jpegImageQuality || deemixSettings.jpegImageQuality;
deemixSettings.removeDuplicateArtists = config.deemix.removeDuplicateArtists !== undefined ? config.deemix.removeDuplicateArtists : deemixSettings.removeDuplicateArtists;
2021-10-19 09:22:31 +02:00
2021-10-23 12:57:36 +02:00
interface QueuedFile {
file: string,
date: number
}
2021-10-20 20:08:41 +02:00
const toDeleteLocation = './data/toDelete.json';
if (!fs.existsSync(toDeleteLocation)) fs.writeFileSync(toDeleteLocation, '[]', {encoding: 'utf8'});
2021-10-23 12:57:36 +02:00
let toDelete: QueuedFile[] = JSON.parse(fs.readFileSync(toDeleteLocation, {encoding: 'utf8'}));
2021-10-20 20:08:41 +02:00
function updateQueueFile() {
2021-10-22 20:05:32 +02:00
try {
fs.writeFileSync(toDeleteLocation, JSON.stringify(toDelete), {encoding: 'utf8'});
} catch(err) {
logger.error('failed to write to deletion queue json file! wrong permissions or ran out of space?', err);
}
}
2021-10-23 12:57:36 +02:00
function deleteQueuedFile(file: string) {
toDelete = toDelete.filter((c: QueuedFile) => c.file !== file);
2021-10-22 20:05:32 +02:00
updateQueueFile();
fs.unlink(file, (err) => {
if (err) {
logger.warn(`failed to delete ${file}!`);
logger.warn(err.toString());
logger.warn('if this file still exists, you will have to manually delete it');
}
});
2021-10-20 20:08:41 +02:00
}
2021-10-23 12:57:36 +02:00
function queueDeletion(file: string) {
2021-10-21 20:21:51 +02:00
logger.info(`queued deletion of ${file} ${timeago.format(Date.now() + deleteTimer)}`);
2021-10-20 20:08:41 +02:00
toDelete.push({
date: Date.now() + deleteTimer,
file
});
setTimeout(() => {
2021-10-21 20:21:51 +02:00
logger.info(`deleting queued file ${file}`);
2021-10-22 20:05:32 +02:00
deleteQueuedFile(file);
2021-10-20 20:08:41 +02:00
}, deleteTimer);
updateQueueFile();
}
2021-10-21 20:21:51 +02:00
logger.info(`loaded ${toDelete.length} items in deletion queue`);
2021-10-20 20:08:41 +02:00
let updateQueue = false;
for (let del of toDelete) {
if (Date.now() - del.date >= 0) {
2021-10-21 20:21:51 +02:00
logger.warn(`deleting ${del.file} - was meant to be deleted ${timeago.format(del.date)}`);
2021-10-22 20:05:32 +02:00
deleteQueuedFile(del.file);
2021-10-20 20:08:41 +02:00
} else {
2021-10-21 20:21:51 +02:00
logger.info(`queueing deletion of ${del.file} ${timeago.format(del.date)}`);
2021-10-20 20:08:41 +02:00
setTimeout(() => {
2021-10-21 20:21:51 +02:00
logger.info(`deleting queued file ${del.file}`);
2021-10-22 20:05:32 +02:00
deleteQueuedFile(del.file);
2021-10-20 20:08:41 +02:00
}, del.date - Date.now());
}
};
if (updateQueue) {
updateQueueFile();
2021-10-21 20:21:51 +02:00
logger.info('updated deletion queue json');
}
if (config.server.proxy) {
app.enable('trust proxy');
logger.info('enabled express.js reverse proxy settings');
2021-10-20 20:08:41 +02:00
}
2021-10-21 20:21:51 +02:00
app.use((req, res, next) => {
logger.http(`${(config.server.proxy && req.headers['x-forwarded-for']) || req.connection.remoteAddress} ${req.method} ${req.originalUrl} `);
next();
});
2021-10-19 18:00:54 +02:00
app.use(express.static('public'));
2021-10-19 09:22:31 +02:00
app.use('/data', express.static('data', {extensions: ['flac', 'mp3']}));
2021-10-21 20:21:51 +02:00
2021-10-19 09:22:31 +02:00
app.get('/api/search', async (req, res) => {
if (!req.query.search) return res.sendStatus(400);
2021-10-23 12:57:36 +02:00
if (Array.isArray(req.query.search)) req.query.search = req.query.search.join('');
req.query.search = req.query.search as string;
let s: [Album];
2021-10-22 20:05:32 +02:00
try {
s = searchcache[req.query.search] || (await deezerInstance.api.search_album(req.query.search, {
limit: config.limits.searchLimit || 15,
2021-10-23 12:57:36 +02:00
})).data;
2021-10-22 20:05:32 +02:00
} catch(err) {
2021-10-23 12:57:36 +02:00
logger.error((err as Error).toString());
return res.sendStatus(500);
2021-10-22 20:05:32 +02:00
}
2021-10-21 21:53:09 +02:00
if (!searchcache[req.query.search]) searchcache[req.query.search] = s;
2021-10-19 09:22:31 +02:00
2021-10-23 12:57:36 +02:00
let format = s.map(s => {
2021-10-19 09:22:31 +02:00
return {
id: s.id,
title: s.title,
cover: s.md5_image,
artist: {
id: s.artist.id,
name: s.artist.name
},
};
});
res.send(format);
});
app.get('/api/album', async (req, res) => {
2021-10-23 12:57:36 +02:00
if (!req.query.id) return res.sendStatus(400);
if (Array.isArray(req.query.id)) req.query.id = req.query.id.join('');
req.query.id = req.query.id as string;
let album: Album;
2021-10-20 07:26:03 +02:00
try {
2021-10-21 21:53:09 +02:00
album = albumcache[req.query.id] || (await deezerInstance.api.get_album(req.query.id));
if (!albumcache[req.query.id]) albumcache[req.query.id] = album;
2021-10-20 07:26:03 +02:00
} catch (err) {
2021-10-23 12:57:36 +02:00
logger.error((err as Error).toString());
return res.status(404).send('Album not found!');
2021-10-20 07:26:03 +02:00
}
2021-10-23 12:57:36 +02:00
2021-10-19 18:00:54 +02:00
res.send({
id: album.id,
title: album.title,
link: album.link,
tracks: album.tracks.data.map(t => {
return {
id: t.id,
title: t.title,
duration: t.duration,
link: t.link,
artist: t.artist.name,
};
})
});
2021-10-19 09:22:31 +02:00
});
2021-10-23 12:57:36 +02:00
interface Metadata {
id: number,
title: string,
artist: string,
}
async function deemixDownloadWrapper(dlObj: deemix.types.downloadObjects.IDownloadObject, ws: WebSocket, coverArt: string, metadata: Metadata) {
let trackpaths: string[] = [];
2021-10-20 19:17:28 +02:00
2021-10-19 09:22:31 +02:00
const listener = {
2021-10-23 12:57:36 +02:00
send(key: any, data: any) {
2021-10-19 09:22:31 +02:00
if (data.downloaded) {
2021-10-20 19:17:28 +02:00
trackpaths.push(data.downloadPath);
2021-10-20 20:08:41 +02:00
queueDeletion(data.downloadPath);
2021-10-19 09:22:31 +02:00
}
2021-10-22 20:05:32 +02:00
if (data.state !== 'tagging' && data.state !== 'getAlbumArt' && data.state !== 'getTags' && !dlObj.isCanceled) ws.send(JSON.stringify({key, data}));
2021-10-19 09:22:31 +02:00
}
};
2021-10-22 20:05:32 +02:00
listener.send('coverArt', coverArt);
listener.send('metadata', metadata);
deemixDownloader = new deemix.downloader.Downloader(deezerInstance, dlObj, deemixSettings, listener);
2021-10-19 21:30:55 +02:00
try {
2021-10-22 20:05:32 +02:00
await deemixDownloader.start();
2021-10-19 21:30:55 +02:00
} catch(err) {
2021-10-23 12:57:36 +02:00
logger.warn((err as Error).toString());
2021-10-22 20:05:32 +02:00
logger.warn('(this may be deemix just being whiny)');
2021-10-19 21:30:55 +02:00
}
2021-10-19 09:22:31 +02:00
2021-10-22 20:05:32 +02:00
if (dlObj.isCanceled) {
logger.debug('download gracefully cancelled, cleaning up');
trackpaths.forEach((q) => {
logger.info(`removing ${q}`);
deleteQueuedFile(q);
});
} else if (trackpaths.length > 1) {
2021-10-20 20:08:41 +02:00
await ws.send(JSON.stringify({key: 'zipping'}));
2021-10-20 19:39:18 +02:00
2021-10-20 20:08:41 +02:00
const folderName = trackpaths[0].split('/').slice(-2)[0];
logger.debug(`zipping ${folderName}`);
2021-10-20 19:17:28 +02:00
try {
2021-10-21 18:27:00 +02:00
await promisify(exec)(`${config.server.zipBinaryLocation} ${config.server.zipArguments} "data/${folderName}.zip" "data/${folderName}"`);
2021-10-20 19:17:28 +02:00
} catch(err) {
2021-10-23 12:57:36 +02:00
logger.error((err as Error).toString());
2021-10-20 20:08:41 +02:00
return ws.close(1011, 'Zipping album failed');
2021-10-20 19:17:28 +02:00
}
2021-10-21 18:27:00 +02:00
2021-10-20 20:08:41 +02:00
await ws.send(JSON.stringify({key: 'download', data: `data/${folderName}.zip`}));
2021-10-21 18:27:00 +02:00
2021-10-20 20:08:41 +02:00
queueDeletion('./data/' + folderName + '.zip');
2021-10-22 20:05:32 +02:00
} else if (trackpaths.length === 1) {
2021-10-20 20:08:41 +02:00
await ws.send(JSON.stringify({key: 'download', data: trackpaths[0].replace(process.cwd(), '')}));
}
2021-10-22 20:05:32 +02:00
}
2021-10-23 12:57:36 +02:00
app.ws('/api/album', async (ws, req: any) => {
2021-10-22 20:05:32 +02:00
if (!req.query.id) return ws.close(1008, 'Supply a track ID in the query!');
const dlObj = await deemix.generateDownloadObject(deezerInstance, 'https://www.deezer.com/album/' + req.query.id, format);
let isDone = false;
2021-10-22 20:05:32 +02:00
ws.on('close', (code) => {
if (isDone) return;
2021-10-22 20:05:32 +02:00
dlObj.isCanceled = true;
logger.debug(`client left unexpectedly with code ${code}; cancelling download`);
});
let album;
try {
album = albumcache[req.query.id] || (await deezerInstance.api.get_album(req.query.id));
if (!albumcache[req.query.id]) albumcache[req.query.id] = album;
} catch(err) {
2021-10-23 12:57:36 +02:00
logger.error((err as Error).toString());
2021-10-22 20:05:32 +02:00
return ws.close(1012, 'Album not found');
}
await deemixDownloadWrapper(dlObj, ws, album.cover_medium, {id: album.id, title: album.title, artist: album.artist.name});
isDone = true;
logger.debug('download done');
2021-10-20 19:17:28 +02:00
2021-10-20 07:26:03 +02:00
ws.close(1000);
2021-10-19 09:22:31 +02:00
});
2021-10-23 12:57:36 +02:00
app.ws('/api/track', async (ws, req: any) => {
2021-10-20 07:26:03 +02:00
if (!req.query.id) return ws.close(1008, 'Supply a track ID in the query!');
2021-10-19 21:30:55 +02:00
2021-10-22 20:05:32 +02:00
const dlObj = await deemix.generateDownloadObject(deezerInstance, 'https://www.deezer.com/track/' + req.query.id, format);
let isDone = false;
2021-10-23 12:57:36 +02:00
ws.on('close', (code: number) => {
if (isDone) return;
dlObj.isCanceled = true;
logger.debug(`client left unexpectedly with code ${code}; cancelling download`);
});
2021-10-19 21:30:55 +02:00
let track;
try {
2021-10-21 21:53:09 +02:00
track = trackcache[req.query.id] || (await deezerInstance.api.get_track(req.query.id));
if (!trackcache[req.query.id]) trackcache[req.query.id] = track;
2021-10-19 21:30:55 +02:00
} catch(err) {
2021-10-23 12:57:36 +02:00
logger.error((err as Error).toString());
2021-10-20 07:26:03 +02:00
return ws.close(1012, 'Track not found');
2021-10-19 21:30:55 +02:00
}
2021-10-22 20:05:32 +02:00
await deemixDownloadWrapper(dlObj, ws, track.album.cover_medium, {id: track.id, title: track.title, artist: track.artist.name});
isDone = true;
logger.debug('download done');
2021-10-19 21:30:55 +02:00
2021-10-20 07:26:03 +02:00
ws.close(1000);
2021-10-19 21:30:55 +02:00
});
2021-10-23 12:57:36 +02:00
deezerInstance.login_via_arl(process.env.DEEZER_ARL || '').then(() => {
2021-10-21 20:21:51 +02:00
logger.info('logged into deezer');
2021-10-19 09:22:31 +02:00
app.listen(port, () => {
2021-10-21 20:21:51 +02:00
logger.info(`hosting on http://localhost:${port} and wss://localhost:${port}`);
2021-10-19 09:22:31 +02:00
});
});