split everything into files
This commit is contained in:
parent
c423c65273
commit
67e306625c
|
@ -3,7 +3,6 @@
|
||||||
"version": "1.0.0",
|
"version": "1.0.0",
|
||||||
"description": "a dumb frontend for just getting some got damned songs",
|
"description": "a dumb frontend for just getting some got damned songs",
|
||||||
"main": "dist/index.js",
|
"main": "dist/index.js",
|
||||||
"type": "module",
|
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"test": "echo \"Error: no test specified\" && exit 1",
|
"test": "echo \"Error: no test specified\" && exit 1",
|
||||||
"quickrun": "tsc && node dist/index.js",
|
"quickrun": "tsc && node dist/index.js",
|
||||||
|
|
|
@ -0,0 +1,17 @@
|
||||||
|
import fs from 'fs';
|
||||||
|
import toml from 'toml';
|
||||||
|
|
||||||
|
import { logger } from './logger';
|
||||||
|
|
||||||
|
if (!fs.existsSync('./config.toml')) {
|
||||||
|
if (!fs.existsSync('./config.example.toml')) {
|
||||||
|
logger.error('no config.toml OR config.example.toml found!!! what the hell are you up to!!!');
|
||||||
|
process.exit(1);
|
||||||
|
}
|
||||||
|
logger.warn('copying config.example.toml to config.toml as it was not found. the default config may not be preferable!');
|
||||||
|
fs.copyFileSync('./config.example.toml', './config.toml');
|
||||||
|
}
|
||||||
|
|
||||||
|
export const config = toml.parse(fs.readFileSync('./config.toml', {encoding: 'utf8'}));
|
||||||
|
|
||||||
|
logger.info('loaded config');
|
|
@ -0,0 +1,25 @@
|
||||||
|
import deezer from 'deezer-js';
|
||||||
|
import deemix from 'deemix';
|
||||||
|
import path from 'path';
|
||||||
|
|
||||||
|
import { config } from './config';
|
||||||
|
|
||||||
|
export const deezerInstance = new deezer.Deezer();
|
||||||
|
|
||||||
|
export const format = deezer.TrackFormats[config.deemix.trackFormat || 'FLAC'];
|
||||||
|
|
||||||
|
export const deemixSettings = deemix.settings.DEFAULTS;
|
||||||
|
deemixSettings.downloadLocation = path.join(process.cwd(), 'data/');
|
||||||
|
deemixSettings.overwriteFile = deemix.settings.OverwriteOption.ONLY_TAGS;
|
||||||
|
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;
|
|
@ -0,0 +1,71 @@
|
||||||
|
import fs from 'fs';
|
||||||
|
import * as timeago from 'timeago.js';
|
||||||
|
|
||||||
|
import { logger } from './logger';
|
||||||
|
import { config } from './config';
|
||||||
|
|
||||||
|
interface QueuedFile {
|
||||||
|
file: string,
|
||||||
|
date: number
|
||||||
|
}
|
||||||
|
|
||||||
|
export const deleteTimer = config.timer.deleteTimer || 1000 * 60 * 25;
|
||||||
|
|
||||||
|
const toDeleteLocation = './data/toDelete.json';
|
||||||
|
|
||||||
|
if (!fs.existsSync(toDeleteLocation)) fs.writeFileSync(toDeleteLocation, '[]', {encoding: 'utf8'});
|
||||||
|
let toDelete: QueuedFile[] = JSON.parse(fs.readFileSync(toDeleteLocation, {encoding: 'utf8'}));
|
||||||
|
|
||||||
|
export function updateQueueFile() {
|
||||||
|
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);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export function deleteQueuedFile(file: string) {
|
||||||
|
toDelete = toDelete.filter((c: QueuedFile) => c.file !== file);
|
||||||
|
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');
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
export function queueDeletion(file: string) {
|
||||||
|
logger.info(`queued deletion of ${file} ${timeago.format(Date.now() + deleteTimer)}`);
|
||||||
|
|
||||||
|
toDelete.push({
|
||||||
|
date: Date.now() + deleteTimer,
|
||||||
|
file
|
||||||
|
});
|
||||||
|
setTimeout(() => {
|
||||||
|
logger.info(`deleting queued file ${file}`);
|
||||||
|
deleteQueuedFile(file);
|
||||||
|
}, deleteTimer);
|
||||||
|
updateQueueFile();
|
||||||
|
}
|
||||||
|
|
||||||
|
logger.info(`loaded ${toDelete.length} items in deletion queue`);
|
||||||
|
let updateQueue = false;
|
||||||
|
for (let del of toDelete) {
|
||||||
|
if (Date.now() - del.date >= 0) {
|
||||||
|
logger.warn(`deleting ${del.file} - was meant to be deleted ${timeago.format(del.date)}`);
|
||||||
|
deleteQueuedFile(del.file);
|
||||||
|
} else {
|
||||||
|
logger.info(`queueing deletion of ${del.file} ${timeago.format(del.date)}`);
|
||||||
|
setTimeout(() => {
|
||||||
|
logger.info(`deleting queued file ${del.file}`);
|
||||||
|
deleteQueuedFile(del.file);
|
||||||
|
}, del.date - Date.now());
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
if (updateQueue) {
|
||||||
|
updateQueueFile();
|
||||||
|
logger.info('updated deletion queue json');
|
||||||
|
}
|
|
@ -0,0 +1,64 @@
|
||||||
|
import * as deemix from 'deemix';
|
||||||
|
import { queueDeletion, deleteQueuedFile } from './deletionQueue';
|
||||||
|
import { deemixSettings, deezerInstance } from './deemix';
|
||||||
|
import { logger } from './logger';
|
||||||
|
import { promisify } from 'util';
|
||||||
|
import { exec } from 'child_process';
|
||||||
|
import { config } from './config';
|
||||||
|
|
||||||
|
export interface Metadata {
|
||||||
|
id: number,
|
||||||
|
title: string,
|
||||||
|
artist: string,
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function deemixDownloadWrapper(dlObj: deemix.types.downloadObjects.IDownloadObject, ws: WebSocket, coverArt: string, metadata: Metadata) {
|
||||||
|
let trackpaths: string[] = [];
|
||||||
|
|
||||||
|
const listener = {
|
||||||
|
send(key: any, data: any) {
|
||||||
|
if (data.downloaded) {
|
||||||
|
trackpaths.push(data.downloadPath);
|
||||||
|
queueDeletion(data.downloadPath);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (data.state !== 'tagging' && data.state !== 'getAlbumArt' && data.state !== 'getTags' && !dlObj.isCanceled) ws.send(JSON.stringify({key, data}));
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
listener.send('coverArt', coverArt);
|
||||||
|
listener.send('metadata', metadata);
|
||||||
|
|
||||||
|
let deemixDownloader = new deemix.downloader.Downloader(deezerInstance, dlObj, deemixSettings, listener);
|
||||||
|
try {
|
||||||
|
await deemixDownloader.start();
|
||||||
|
} catch(err) {
|
||||||
|
logger.warn((err as Error).toString());
|
||||||
|
logger.warn('(this may be deemix just being whiny)');
|
||||||
|
}
|
||||||
|
|
||||||
|
if (dlObj.isCanceled) {
|
||||||
|
logger.debug('download gracefully cancelled, cleaning up');
|
||||||
|
trackpaths.forEach((q) => {
|
||||||
|
logger.info(`removing ${q}`);
|
||||||
|
deleteQueuedFile(q);
|
||||||
|
});
|
||||||
|
} else if (trackpaths.length > 1) {
|
||||||
|
await ws.send(JSON.stringify({key: 'zipping'}));
|
||||||
|
|
||||||
|
const folderName = trackpaths[0].split('/').slice(-2)[0];
|
||||||
|
logger.debug(`zipping ${folderName}`);
|
||||||
|
try {
|
||||||
|
await promisify(exec)(`${config.server.zipBinaryLocation} ${config.server.zipArguments} "data/${folderName}.zip" "data/${folderName}"`);
|
||||||
|
} catch(err) {
|
||||||
|
logger.error((err as Error).toString());
|
||||||
|
return ws.close(1011, 'Zipping album failed');
|
||||||
|
}
|
||||||
|
|
||||||
|
await ws.send(JSON.stringify({key: 'download', data: `data/${folderName}.zip`}));
|
||||||
|
|
||||||
|
queueDeletion('./data/' + folderName + '.zip');
|
||||||
|
} else if (trackpaths.length === 1) {
|
||||||
|
await ws.send(JSON.stringify({key: 'download', data: trackpaths[0].replace(process.cwd(), '')}));
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,39 @@
|
||||||
|
import express from 'express';
|
||||||
|
|
||||||
|
import { albumcache } from '..';
|
||||||
|
import { logger } from '../logger';
|
||||||
|
import { deezerInstance } from '../deemix';
|
||||||
|
|
||||||
|
const router = express.Router();
|
||||||
|
|
||||||
|
router.get('/api/album', async (req, res) => {
|
||||||
|
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;
|
||||||
|
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) {
|
||||||
|
logger.error((err as Error).toString());
|
||||||
|
return res.status(404).send('Album not found!');
|
||||||
|
}
|
||||||
|
|
||||||
|
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,
|
||||||
|
};
|
||||||
|
})
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
export default router;
|
|
@ -0,0 +1,7 @@
|
||||||
|
import album from './album';
|
||||||
|
import search from './search';
|
||||||
|
|
||||||
|
export default [
|
||||||
|
album,
|
||||||
|
search
|
||||||
|
];
|
|
@ -0,0 +1,41 @@
|
||||||
|
import express from 'express';
|
||||||
|
|
||||||
|
import { searchcache } from '..';
|
||||||
|
import { config } from '../config';
|
||||||
|
import { logger } from '../logger';
|
||||||
|
import { deezerInstance } from '../deemix';
|
||||||
|
|
||||||
|
const router = express.Router();
|
||||||
|
|
||||||
|
router.get('/api/search', async (req, res) => {
|
||||||
|
if (!req.query.search) return res.sendStatus(400);
|
||||||
|
if (Array.isArray(req.query.search)) req.query.search = req.query.search.join('');
|
||||||
|
req.query.search = req.query.search as string;
|
||||||
|
|
||||||
|
let s: [Album];
|
||||||
|
try {
|
||||||
|
s = searchcache[req.query.search] || (await deezerInstance.api.search_album(req.query.search, {
|
||||||
|
limit: config.limits.searchLimit || 15,
|
||||||
|
})).data;
|
||||||
|
} catch(err) {
|
||||||
|
logger.error((err as Error).toString());
|
||||||
|
return res.sendStatus(500);
|
||||||
|
}
|
||||||
|
if (!searchcache[req.query.search]) searchcache[req.query.search] = s;
|
||||||
|
|
||||||
|
let format = s.map(s => {
|
||||||
|
return {
|
||||||
|
id: s.id,
|
||||||
|
title: s.title,
|
||||||
|
cover: s.md5_image,
|
||||||
|
artist: {
|
||||||
|
id: s.artist.id,
|
||||||
|
name: s.artist.name
|
||||||
|
},
|
||||||
|
};
|
||||||
|
});
|
||||||
|
|
||||||
|
res.send(format);
|
||||||
|
});
|
||||||
|
|
||||||
|
export default router;
|
332
src/index.ts
332
src/index.ts
|
@ -1,155 +1,21 @@
|
||||||
import express from 'express';
|
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 * as dotenv from 'dotenv';
|
||||||
import expressws from 'express-ws';
|
import expressws from 'express-ws';
|
||||||
|
|
||||||
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: [
|
|
||||||
new winston.transports.File({filename: 'deemix-web-fe-error.log', level: 'warn'}),
|
|
||||||
new winston.transports.File({filename: 'deemix-web-fe.log'}),
|
|
||||||
new winston.transports.Console({
|
|
||||||
format: winston.format.combine(
|
|
||||||
winston.format.timestamp(),
|
|
||||||
winston.format.colorize(),
|
|
||||||
logFormatter
|
|
||||||
)
|
|
||||||
})
|
|
||||||
]
|
|
||||||
});
|
|
||||||
|
|
||||||
if (!fs.existsSync('./config.toml')) {
|
|
||||||
if (!fs.existsSync('./config.example.toml')) {
|
|
||||||
logger.error('no config.toml OR config.example.toml found!!! what the hell are you up to!!!');
|
|
||||||
process.exit(1);
|
|
||||||
}
|
|
||||||
logger.warn('copying config.example.toml to config.toml as it was not found. the default config may not be preferable!');
|
|
||||||
fs.copyFileSync('./config.example.toml', './config.toml');
|
|
||||||
}
|
|
||||||
const config = toml.parse(fs.readFileSync('./config.toml', {encoding: 'utf8'}));
|
|
||||||
logger.info('loaded config');
|
|
||||||
|
|
||||||
let searchcache: Record<string, [Album]> = {};
|
|
||||||
let albumcache: Record<string, Album> = {};
|
|
||||||
let trackcache: Record<string, Track> = {};
|
|
||||||
|
|
||||||
const port = config.server.port || 4500;
|
|
||||||
const deleteTimer = config.timer.deleteTimer || 1000 * 60 * 25;
|
|
||||||
|
|
||||||
dotenv.config();
|
dotenv.config();
|
||||||
|
|
||||||
const app = express();
|
import { logger } from './logger';
|
||||||
|
import { config } from './config';
|
||||||
|
import { deezerInstance } from './deemix';
|
||||||
|
|
||||||
|
export const port = config.server.port || 4500;
|
||||||
|
|
||||||
|
export let searchcache: Record<string, [Album]> = {};
|
||||||
|
export let albumcache: Record<string, Album> = {};
|
||||||
|
export let trackcache: Record<string, Track> = {};
|
||||||
|
|
||||||
|
export const app = express();
|
||||||
expressws(app);
|
expressws(app);
|
||||||
const deezerInstance = new deezer.Deezer();
|
|
||||||
let deemixDownloader;
|
|
||||||
|
|
||||||
let deemixSettings = deemix.settings.DEFAULTS;
|
|
||||||
deemixSettings.downloadLocation = path.join(process.cwd(), 'data/');
|
|
||||||
deemixSettings.overwriteFile = deemix.settings.OverwriteOption.ONLY_TAGS;
|
|
||||||
|
|
||||||
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;
|
|
||||||
|
|
||||||
interface QueuedFile {
|
|
||||||
file: string,
|
|
||||||
date: number
|
|
||||||
}
|
|
||||||
|
|
||||||
const toDeleteLocation = './data/toDelete.json';
|
|
||||||
|
|
||||||
if (!fs.existsSync(toDeleteLocation)) fs.writeFileSync(toDeleteLocation, '[]', {encoding: 'utf8'});
|
|
||||||
let toDelete: QueuedFile[] = JSON.parse(fs.readFileSync(toDeleteLocation, {encoding: 'utf8'}));
|
|
||||||
|
|
||||||
function updateQueueFile() {
|
|
||||||
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);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function deleteQueuedFile(file: string) {
|
|
||||||
toDelete = toDelete.filter((c: QueuedFile) => c.file !== file);
|
|
||||||
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');
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
function queueDeletion(file: string) {
|
|
||||||
logger.info(`queued deletion of ${file} ${timeago.format(Date.now() + deleteTimer)}`);
|
|
||||||
|
|
||||||
toDelete.push({
|
|
||||||
date: Date.now() + deleteTimer,
|
|
||||||
file
|
|
||||||
});
|
|
||||||
setTimeout(() => {
|
|
||||||
logger.info(`deleting queued file ${file}`);
|
|
||||||
deleteQueuedFile(file);
|
|
||||||
}, deleteTimer);
|
|
||||||
updateQueueFile();
|
|
||||||
}
|
|
||||||
|
|
||||||
logger.info(`loaded ${toDelete.length} items in deletion queue`);
|
|
||||||
let updateQueue = false;
|
|
||||||
for (let del of toDelete) {
|
|
||||||
if (Date.now() - del.date >= 0) {
|
|
||||||
logger.warn(`deleting ${del.file} - was meant to be deleted ${timeago.format(del.date)}`);
|
|
||||||
deleteQueuedFile(del.file);
|
|
||||||
} else {
|
|
||||||
logger.info(`queueing deletion of ${del.file} ${timeago.format(del.date)}`);
|
|
||||||
setTimeout(() => {
|
|
||||||
logger.info(`deleting queued file ${del.file}`);
|
|
||||||
deleteQueuedFile(del.file);
|
|
||||||
}, del.date - Date.now());
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
if (updateQueue) {
|
|
||||||
updateQueueFile();
|
|
||||||
logger.info('updated deletion queue json');
|
|
||||||
}
|
|
||||||
|
|
||||||
if (config.server.proxy) {
|
if (config.server.proxy) {
|
||||||
app.enable('trust proxy');
|
app.enable('trust proxy');
|
||||||
|
@ -164,179 +30,11 @@ app.use((req, res, next) => {
|
||||||
app.use(express.static('public'));
|
app.use(express.static('public'));
|
||||||
app.use('/data', express.static('data', {extensions: ['flac', 'mp3']}));
|
app.use('/data', express.static('data', {extensions: ['flac', 'mp3']}));
|
||||||
|
|
||||||
app.get('/api/search', async (req, res) => {
|
import get from './get';
|
||||||
if (!req.query.search) return res.sendStatus(400);
|
get.forEach((q) => {app.use(q)});
|
||||||
if (Array.isArray(req.query.search)) req.query.search = req.query.search.join('');
|
|
||||||
req.query.search = req.query.search as string;
|
|
||||||
|
|
||||||
let s: [Album];
|
import ws from './ws';
|
||||||
try {
|
ws.forEach((q) => {app.use(q)});
|
||||||
s = searchcache[req.query.search] || (await deezerInstance.api.search_album(req.query.search, {
|
|
||||||
limit: config.limits.searchLimit || 15,
|
|
||||||
})).data;
|
|
||||||
} catch(err) {
|
|
||||||
logger.error((err as Error).toString());
|
|
||||||
return res.sendStatus(500);
|
|
||||||
}
|
|
||||||
if (!searchcache[req.query.search]) searchcache[req.query.search] = s;
|
|
||||||
|
|
||||||
let format = s.map(s => {
|
|
||||||
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) => {
|
|
||||||
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;
|
|
||||||
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) {
|
|
||||||
logger.error((err as Error).toString());
|
|
||||||
return res.status(404).send('Album not found!');
|
|
||||||
}
|
|
||||||
|
|
||||||
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,
|
|
||||||
};
|
|
||||||
})
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
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[] = [];
|
|
||||||
|
|
||||||
const listener = {
|
|
||||||
send(key: any, data: any) {
|
|
||||||
if (data.downloaded) {
|
|
||||||
trackpaths.push(data.downloadPath);
|
|
||||||
queueDeletion(data.downloadPath);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (data.state !== 'tagging' && data.state !== 'getAlbumArt' && data.state !== 'getTags' && !dlObj.isCanceled) ws.send(JSON.stringify({key, data}));
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
listener.send('coverArt', coverArt);
|
|
||||||
listener.send('metadata', metadata);
|
|
||||||
|
|
||||||
deemixDownloader = new deemix.downloader.Downloader(deezerInstance, dlObj, deemixSettings, listener);
|
|
||||||
try {
|
|
||||||
await deemixDownloader.start();
|
|
||||||
} catch(err) {
|
|
||||||
logger.warn((err as Error).toString());
|
|
||||||
logger.warn('(this may be deemix just being whiny)');
|
|
||||||
}
|
|
||||||
|
|
||||||
if (dlObj.isCanceled) {
|
|
||||||
logger.debug('download gracefully cancelled, cleaning up');
|
|
||||||
trackpaths.forEach((q) => {
|
|
||||||
logger.info(`removing ${q}`);
|
|
||||||
deleteQueuedFile(q);
|
|
||||||
});
|
|
||||||
} else if (trackpaths.length > 1) {
|
|
||||||
await ws.send(JSON.stringify({key: 'zipping'}));
|
|
||||||
|
|
||||||
const folderName = trackpaths[0].split('/').slice(-2)[0];
|
|
||||||
logger.debug(`zipping ${folderName}`);
|
|
||||||
try {
|
|
||||||
await promisify(exec)(`${config.server.zipBinaryLocation} ${config.server.zipArguments} "data/${folderName}.zip" "data/${folderName}"`);
|
|
||||||
} catch(err) {
|
|
||||||
logger.error((err as Error).toString());
|
|
||||||
return ws.close(1011, 'Zipping album failed');
|
|
||||||
}
|
|
||||||
|
|
||||||
await ws.send(JSON.stringify({key: 'download', data: `data/${folderName}.zip`}));
|
|
||||||
|
|
||||||
queueDeletion('./data/' + folderName + '.zip');
|
|
||||||
} else if (trackpaths.length === 1) {
|
|
||||||
await ws.send(JSON.stringify({key: 'download', data: trackpaths[0].replace(process.cwd(), '')}));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
app.ws('/api/album', async (ws, req: any) => {
|
|
||||||
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;
|
|
||||||
|
|
||||||
ws.on('close', (code) => {
|
|
||||||
if (isDone) return;
|
|
||||||
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) {
|
|
||||||
logger.error((err as Error).toString());
|
|
||||||
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');
|
|
||||||
|
|
||||||
ws.close(1000);
|
|
||||||
});
|
|
||||||
|
|
||||||
app.ws('/api/track', async (ws, req: any) => {
|
|
||||||
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/track/' + req.query.id, format);
|
|
||||||
let isDone = false;
|
|
||||||
|
|
||||||
ws.on('close', (code: number) => {
|
|
||||||
if (isDone) return;
|
|
||||||
dlObj.isCanceled = true;
|
|
||||||
logger.debug(`client left unexpectedly with code ${code}; cancelling download`);
|
|
||||||
});
|
|
||||||
|
|
||||||
let track;
|
|
||||||
try {
|
|
||||||
track = trackcache[req.query.id] || (await deezerInstance.api.get_track(req.query.id));
|
|
||||||
if (!trackcache[req.query.id]) trackcache[req.query.id] = track;
|
|
||||||
} catch(err) {
|
|
||||||
logger.error((err as Error).toString());
|
|
||||||
return ws.close(1012, 'Track not found');
|
|
||||||
}
|
|
||||||
|
|
||||||
await deemixDownloadWrapper(dlObj, ws, track.album.cover_medium, {id: track.id, title: track.title, artist: track.artist.name});
|
|
||||||
isDone = true;
|
|
||||||
logger.debug('download done');
|
|
||||||
|
|
||||||
ws.close(1000);
|
|
||||||
});
|
|
||||||
|
|
||||||
deezerInstance.login_via_arl(process.env.DEEZER_ARL || '').then(() => {
|
deezerInstance.login_via_arl(process.env.DEEZER_ARL || '').then(() => {
|
||||||
logger.info('logged into deezer');
|
logger.info('logged into deezer');
|
||||||
|
|
|
@ -0,0 +1,33 @@
|
||||||
|
import winston from 'winston';
|
||||||
|
|
||||||
|
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'
|
||||||
|
});
|
||||||
|
|
||||||
|
export const logger = winston.createLogger({
|
||||||
|
level: 'debug',
|
||||||
|
format: winston.format.combine(
|
||||||
|
winston.format.simple()
|
||||||
|
),
|
||||||
|
transports: [
|
||||||
|
new winston.transports.File({filename: 'deemix-web-fe-error.log', level: 'warn'}),
|
||||||
|
new winston.transports.File({filename: 'deemix-web-fe.log'}),
|
||||||
|
new winston.transports.Console({
|
||||||
|
format: winston.format.combine(
|
||||||
|
winston.format.timestamp(),
|
||||||
|
winston.format.colorize(),
|
||||||
|
logFormatter
|
||||||
|
)
|
||||||
|
})
|
||||||
|
]
|
||||||
|
});
|
|
@ -0,0 +1,48 @@
|
||||||
|
import express from 'express';
|
||||||
|
import * as deemix from 'deemix';
|
||||||
|
import { deezerInstance, format } from '../deemix';
|
||||||
|
import { logger } from '../logger';
|
||||||
|
import { albumcache } from '..';
|
||||||
|
|
||||||
|
import { deemixDownloadWrapper } from '../download';
|
||||||
|
|
||||||
|
const router = express.Router();
|
||||||
|
|
||||||
|
router.ws('/api/album', async (ws, req: any) => {
|
||||||
|
if (!req.query.id) return ws.close(1008, 'Supply a track ID in the query!');
|
||||||
|
|
||||||
|
|
||||||
|
let dlObj: deemix.types.downloadObjects.IDownloadObject;
|
||||||
|
try {
|
||||||
|
dlObj = await deemix.generateDownloadObject(deezerInstance, 'https://www.deezer.com/album/' + req.query.id, format);
|
||||||
|
} catch(err) {
|
||||||
|
logger.error((err as Error).toString());
|
||||||
|
return ws.close(1012, 'Album not found');
|
||||||
|
}
|
||||||
|
|
||||||
|
let isDone = false;
|
||||||
|
|
||||||
|
ws.on('close', (code) => {
|
||||||
|
if (isDone) return;
|
||||||
|
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) {
|
||||||
|
logger.error((err as Error).toString());
|
||||||
|
return ws.close(1012, 'Album not found');
|
||||||
|
}
|
||||||
|
|
||||||
|
// @ts-expect-error
|
||||||
|
await deemixDownloadWrapper(dlObj, ws, album.cover_medium, {id: album.id, title: album.title, artist: album.artist.name});
|
||||||
|
isDone = true;
|
||||||
|
logger.debug('download done');
|
||||||
|
|
||||||
|
ws.close(1000);
|
||||||
|
});
|
||||||
|
|
||||||
|
export default router;
|
|
@ -0,0 +1,7 @@
|
||||||
|
import album from './album';
|
||||||
|
import track from './track';
|
||||||
|
|
||||||
|
export default [
|
||||||
|
album,
|
||||||
|
track
|
||||||
|
];
|
|
@ -0,0 +1,46 @@
|
||||||
|
import express from 'express';
|
||||||
|
import * as deemix from 'deemix';
|
||||||
|
import { deezerInstance, format } from '../deemix';
|
||||||
|
import { logger } from '../logger';
|
||||||
|
import { trackcache } from '..';
|
||||||
|
|
||||||
|
import { deemixDownloadWrapper } from '../download';
|
||||||
|
|
||||||
|
const router = express.Router();
|
||||||
|
|
||||||
|
router.ws('/api/track', async (ws, req: any) => {
|
||||||
|
if (!req.query.id) return ws.close(1008, 'Supply a track ID in the query!');
|
||||||
|
|
||||||
|
let dlObj: deemix.types.downloadObjects.IDownloadObject;
|
||||||
|
try {
|
||||||
|
dlObj = await deemix.generateDownloadObject(deezerInstance, 'https://www.deezer.com/track/' + req.query.id, format);
|
||||||
|
} catch(err) {
|
||||||
|
logger.error((err as Error).toString());
|
||||||
|
return ws.close(1012, 'Track not found');
|
||||||
|
}
|
||||||
|
let isDone = false;
|
||||||
|
|
||||||
|
ws.on('close', (code: number) => {
|
||||||
|
if (isDone) return;
|
||||||
|
dlObj.isCanceled = true;
|
||||||
|
logger.debug(`client left unexpectedly with code ${code}; cancelling download`);
|
||||||
|
});
|
||||||
|
|
||||||
|
let track;
|
||||||
|
try {
|
||||||
|
track = trackcache[req.query.id] || (await deezerInstance.api.get_track(req.query.id));
|
||||||
|
if (!trackcache[req.query.id]) trackcache[req.query.id] = track;
|
||||||
|
} catch(err) {
|
||||||
|
logger.error((err as Error).toString());
|
||||||
|
return ws.close(1012, 'Track not found');
|
||||||
|
}
|
||||||
|
|
||||||
|
// @ts-expect-error
|
||||||
|
await deemixDownloadWrapper(dlObj, ws, track.album.cover_medium, {id: track.id, title: track.title, artist: track.artist.name});
|
||||||
|
isDone = true;
|
||||||
|
logger.debug('download done');
|
||||||
|
|
||||||
|
ws.close(1000);
|
||||||
|
});
|
||||||
|
|
||||||
|
export default router;
|
|
@ -6,9 +6,9 @@
|
||||||
"node_modules"
|
"node_modules"
|
||||||
],
|
],
|
||||||
"compilerOptions": {
|
"compilerOptions": {
|
||||||
"module": "esnext",
|
"incremental": true,
|
||||||
"target": "esnext",
|
"module": "commonjs",
|
||||||
"moduleResolution": "node",
|
"target": "es6",
|
||||||
"strict": true,
|
"strict": true,
|
||||||
"forceConsistentCasingInFileNames": true,
|
"forceConsistentCasingInFileNames": true,
|
||||||
"esModuleInterop": true,
|
"esModuleInterop": true,
|
||||||
|
|
Loading…
Reference in New Issue