Compare commits
4 Commits
0bde41fd60
...
67e306625c
Author | SHA1 | Date |
---|---|---|
Jill | 67e306625c | |
Jill | c423c65273 | |
Jill | 24c097e2b1 | |
Jill | e5b48778f1 |
|
@ -18,11 +18,13 @@ it's intended use is for small groups of people to self-host, and as such there'
|
|||
|
||||
2. (optionally) copy the config.example.json to config.json in the same folder, and modify it
|
||||
|
||||
3. `npm install`
|
||||
3. `npm install` / `pnpm install`
|
||||
|
||||
4. install the `zip` linux tool into your path (there are currently no plans for windows support, however feel free to contribute)
|
||||
|
||||
5. build the project with `npm run build` / `pnpm build`
|
||||
|
||||
5. (optionally) put the service on pm2 like such: `pm2 start src/index.js --name deemix-web-frontend` (or just run it with `node src/index.js`)
|
||||
6. (optionally) put the service on pm2 like such: `pm2 start src/index.js --name deemix-web-frontend` (or just run it with `node src/index.js`)
|
||||
|
||||
### nginx addenum
|
||||
|
||||
|
|
File diff suppressed because it is too large
Load Diff
12
package.json
12
package.json
|
@ -2,9 +2,11 @@
|
|||
"name": "deemix-web-frontend",
|
||||
"version": "1.0.0",
|
||||
"description": "a dumb frontend for just getting some got damned songs",
|
||||
"main": "src/index.js",
|
||||
"main": "dist/index.js",
|
||||
"scripts": {
|
||||
"test": "echo \"Error: no test specified\" && exit 1"
|
||||
"test": "echo \"Error: no test specified\" && exit 1",
|
||||
"quickrun": "tsc && node dist/index.js",
|
||||
"build": "tsc"
|
||||
},
|
||||
"repository": {
|
||||
"type": "git",
|
||||
|
@ -30,5 +32,11 @@
|
|||
"optionalDependencies": {
|
||||
"bufferutil": "^4.0.5",
|
||||
"utf-8-validate": "^5.0.7"
|
||||
},
|
||||
"devDependencies": {
|
||||
"typescript": "^4.4.4",
|
||||
"@types/express": "^4.17.13",
|
||||
"@types/express-ws": "^3.0.1",
|
||||
"@types/ws": "^8.2.0"
|
||||
}
|
||||
}
|
||||
|
|
File diff suppressed because it is too large
Load Diff
|
@ -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;
|
325
src/index.js
325
src/index.js
|
@ -1,325 +0,0 @@
|
|||
const express = require('express');
|
||||
const deezer = require('deezer-js');
|
||||
const deemix = require('deemix');
|
||||
const path = require('path');
|
||||
const { promisify } = require('util');
|
||||
const fs = require('fs');
|
||||
const { exec } = require('child_process');
|
||||
const timeago = require('timeago.js');
|
||||
const toml = require('toml');
|
||||
const winston = require('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'
|
||||
});
|
||||
|
||||
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'));
|
||||
logger.info('loaded config');
|
||||
|
||||
let searchcache = {};
|
||||
let albumcache = {};
|
||||
let trackcache = {};
|
||||
|
||||
const port = config.server.port || 4500;
|
||||
const deleteTimer = config.timer.deleteTimer || 1000 * 60 * 25;
|
||||
|
||||
require('dotenv').config();
|
||||
|
||||
const app = new express();
|
||||
const expressWs = require('express-ws')(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;
|
||||
|
||||
const toDeleteLocation = './data/toDelete.json';
|
||||
|
||||
if (!fs.existsSync(toDeleteLocation)) fs.writeFileSync(toDeleteLocation, '[]', {encoding: 'utf8'});
|
||||
let toDelete = 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) {
|
||||
toDelete = toDelete.filter(c => 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) {
|
||||
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) {
|
||||
app.enable('trust proxy');
|
||||
logger.info('enabled express.js reverse proxy settings');
|
||||
}
|
||||
|
||||
app.use((req, res, next) => {
|
||||
logger.http(`${(config.server.proxy && req.headers['x-forwarded-for']) || req.connection.remoteAddress} ${req.method} ${req.originalUrl} `);
|
||||
next();
|
||||
});
|
||||
|
||||
app.use(express.static('public'));
|
||||
app.use('/data', express.static('data', {extensions: ['flac', 'mp3']}));
|
||||
|
||||
app.get('/api/search', async (req, res) => {
|
||||
if (!req.query.search) return res.sendStatus(400);
|
||||
let s;
|
||||
try {
|
||||
s = searchcache[req.query.search] || (await deezerInstance.api.search_album(req.query.search, {
|
||||
limit: config.limits.searchLimit || 15,
|
||||
}));
|
||||
} catch(err) {
|
||||
logger.error(err.toString());
|
||||
res.sendStatus(500);
|
||||
}
|
||||
if (!searchcache[req.query.search]) searchcache[req.query.search] = s;
|
||||
|
||||
let format = s.data.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 req.sendStatus(400);
|
||||
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.toString());
|
||||
return req.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,
|
||||
};
|
||||
})
|
||||
});
|
||||
});
|
||||
|
||||
async function deemixDownloadWrapper(dlObj, ws, coverArt, metadata) {
|
||||
let trackpaths = [];
|
||||
|
||||
const listener = {
|
||||
send(key, data) {
|
||||
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.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.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) => {
|
||||
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.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) => {
|
||||
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) => {
|
||||
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.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(() => {
|
||||
logger.info('logged into deezer');
|
||||
app.listen(port, () => {
|
||||
logger.info(`hosting on http://localhost:${port} and wss://localhost:${port}`);
|
||||
});
|
||||
});
|
|
@ -0,0 +1,44 @@
|
|||
import express from 'express';
|
||||
import * as dotenv from 'dotenv';
|
||||
import expressws from 'express-ws';
|
||||
|
||||
dotenv.config();
|
||||
|
||||
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);
|
||||
|
||||
if (config.server.proxy) {
|
||||
app.enable('trust proxy');
|
||||
logger.info('enabled express.js reverse proxy settings');
|
||||
}
|
||||
|
||||
app.use((req, res, next) => {
|
||||
logger.http(`${(config.server.proxy && req.headers['x-forwarded-for']) || req.connection.remoteAddress} ${req.method} ${req.originalUrl} `);
|
||||
next();
|
||||
});
|
||||
|
||||
app.use(express.static('public'));
|
||||
app.use('/data', express.static('data', {extensions: ['flac', 'mp3']}));
|
||||
|
||||
import get from './get';
|
||||
get.forEach((q) => {app.use(q)});
|
||||
|
||||
import ws from './ws';
|
||||
ws.forEach((q) => {app.use(q)});
|
||||
|
||||
deezerInstance.login_via_arl(process.env.DEEZER_ARL || '').then(() => {
|
||||
logger.info('logged into deezer');
|
||||
app.listen(port, () => {
|
||||
logger.info(`hosting on http://localhost:${port} and wss://localhost:${port}`);
|
||||
});
|
||||
});
|
|
@ -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,129 @@
|
|||
interface Listener {
|
||||
send(key: any, data: any): void
|
||||
}
|
||||
|
||||
interface TagOptions {
|
||||
title: boolean,
|
||||
artist: boolean,
|
||||
album: boolean,
|
||||
cover: boolean,
|
||||
trackNumber: boolean,
|
||||
trackTotal: boolean,
|
||||
discNumber: boolean,
|
||||
discTotal: boolean,
|
||||
albumArtist: boolean,
|
||||
genre: boolean,
|
||||
year: boolean,
|
||||
date: boolean,
|
||||
explicit: boolean,
|
||||
isrc: boolean,
|
||||
length: boolean,
|
||||
barcode: boolean,
|
||||
bpm: boolean,
|
||||
replayGain: boolean,
|
||||
label: boolean,
|
||||
lyrics: boolean,
|
||||
syncedLyrics: boolean,
|
||||
copyright: boolean,
|
||||
composer: boolean,
|
||||
involvedPeople: boolean,
|
||||
source: boolean,
|
||||
rating: boolean,
|
||||
savePlaylistAsCompilation: boolean,
|
||||
useNullSeparator: boolean,
|
||||
saveID3v1: boolean,
|
||||
multiArtistSeparator: string,
|
||||
singleAlbumArtist: boolean,
|
||||
coverDescriptionUTF8: boolean
|
||||
}
|
||||
|
||||
interface DeemixSettings {
|
||||
downloadLocation: string,
|
||||
tracknameTemplate: string,
|
||||
albumTracknameTemplate: string,
|
||||
playlistTracknameTemplate: string,
|
||||
createPlaylistFolder: boolean,
|
||||
playlistNameTemplate: string,
|
||||
createArtistFolder: boolean,
|
||||
artistNameTemplate: string,
|
||||
createAlbumFolder: boolean,
|
||||
albumNameTemplate: string,
|
||||
createCDFolder: boolean,
|
||||
createStructurePlaylist: boolean,
|
||||
createSingleFolder: boolean,
|
||||
padTracks: boolean,
|
||||
paddingSize: string,
|
||||
illegalCharacterReplacer: string,
|
||||
queueConcurrency: number,
|
||||
maxBitrate: keyof TrackFormats,
|
||||
fallbackBitrate: boolean,
|
||||
fallbackSearch: boolean,
|
||||
logErrors: boolean,
|
||||
logSearched: boolean,
|
||||
overwriteFile: OverwriteOption,
|
||||
createM3U8File: boolean,
|
||||
playlistFilenameTemplate: string,
|
||||
syncedLyrics: boolean,
|
||||
embeddedArtworkSize: number,
|
||||
embeddedArtworkPNG: boolean,
|
||||
localArtworkSize: number,
|
||||
localArtworkFormat: string,
|
||||
saveArtwork: boolean,
|
||||
coverImageTemplate: string,
|
||||
saveArtworkArtist: boolean,
|
||||
artistImageTemplate: string,
|
||||
jpegImageQuality: number,
|
||||
dateFormat: string,
|
||||
albumVariousArtists: boolean,
|
||||
removeAlbumVersion: boolean,
|
||||
removeDuplicateArtists: boolean,
|
||||
featuredToTitle: FeaturesOption,
|
||||
titleCasing: string,
|
||||
artistCasing: string,
|
||||
executeCommand: string,
|
||||
tags: TagOptions
|
||||
}
|
||||
|
||||
declare module 'deemix' {
|
||||
function generateDownloadObject(dz: Deezer, link: string, bitrate: TrackFormats, plugins?: any, listener?: Listener): Promise<IDownloadObject>
|
||||
|
||||
module downloader {
|
||||
class Downloader {
|
||||
dz: Deezer;
|
||||
|
||||
constructor(dz: Deezer, downloadObject: IDownloadObject, settings: DeemixSettings, listener: Listener): void
|
||||
|
||||
start(): Promise<void>
|
||||
}
|
||||
}
|
||||
|
||||
module types {
|
||||
module downloadObjects {
|
||||
class IDownloadObject {
|
||||
isCanceled: boolean;
|
||||
|
||||
constructor(obj: any): void;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
module settings {
|
||||
enum OverwriteOption {
|
||||
OVERWRITE = 'y', // Yes, overwrite the file
|
||||
DONT_OVERWRITE = 'n', // No, don't overwrite the file
|
||||
DONT_CHECK_EXT = 'e', // No, and don't check for extensions
|
||||
KEEP_BOTH = 'b', // No, and keep both files
|
||||
ONLY_TAGS = 't', // Overwrite only the tags
|
||||
}
|
||||
|
||||
// What should I do with featured artists?
|
||||
enum FeaturesOption {
|
||||
NO_CHANGE = "0", // Do nothing
|
||||
REMOVE_TITLE = "1", // Remove from track title
|
||||
REMOVE_TITLE_ALBUM = "3", // Remove from track title and album title
|
||||
MOVE_TITLE = "2" // Move to track title
|
||||
}
|
||||
|
||||
const DEFAULTS: DeemixSettings
|
||||
}
|
||||
}
|
|
@ -0,0 +1,217 @@
|
|||
type DeezerResponse<Data> = {
|
||||
data: Data,
|
||||
total: number,
|
||||
next: string,
|
||||
}
|
||||
|
||||
enum ExplicitContent {
|
||||
NOT_EXPLICIT,
|
||||
EXPLICIT,
|
||||
UNKNOWN,
|
||||
EDITED,
|
||||
PARTIALLY_EXPLICIT,
|
||||
PARTIALLY_UNKNOWN,
|
||||
NO_ADVICE_AVAILABLE,
|
||||
PARTIALLY_NO_ADVICE_AVAILABLE,
|
||||
}
|
||||
|
||||
enum SearchOrder {
|
||||
RANKING = "RANKING",
|
||||
TRACK_ASC = "TRACK_ASC",
|
||||
TRACK_DESC = "TRACK_DESC",
|
||||
ARTIST_ASC = "ARTIST_ASC",
|
||||
ARTIST_DESC = "ARTIST_DESC",
|
||||
ALBUM_ASC = "ALBUM_ASC",
|
||||
ALBUM_DESC = "ALBUM_DESC",
|
||||
RATING_ASC = "RATING_ASC",
|
||||
RATING_DESC = "RATING_DESC",
|
||||
DURATION_ASC = "DURATION_ASC",
|
||||
DURATION_DESC = "DURATION_DESC"
|
||||
}
|
||||
|
||||
interface Track {
|
||||
id: number,
|
||||
readable: boolean,
|
||||
title: string,
|
||||
title_short: string,
|
||||
title_version: string,
|
||||
isrc: string,
|
||||
link: string,
|
||||
share: string,
|
||||
duration: number,
|
||||
track_position: number,
|
||||
disk_number: number,
|
||||
rank: number,
|
||||
release_date: string,
|
||||
explicit_lyrics: boolean,
|
||||
explicit_content_lyrics: ExplicitContent,
|
||||
explicit_content_cover: ExplicitContent,
|
||||
preview: string,
|
||||
bpm: number,
|
||||
gain: number,
|
||||
available_countries: string[],
|
||||
contributors: Contributor[],
|
||||
md5_image: string,
|
||||
artist: Pick<Artist, "id" | "name" | "link" | "share" | "picture" | "picture_small" | "picture_medium" | "picture_big" | "picture_xl" | "radio" | "tracklist">,
|
||||
album: Pick<Album, "id" | "title" | "link" | "cover" | "cover_small" | "cover_medium" | "cover_big" | "cover_xl" | "md5_image" | "release_date" | "tracklist">,
|
||||
}
|
||||
|
||||
interface Artist {
|
||||
id: number,
|
||||
name: string,
|
||||
link: string,
|
||||
share: string,
|
||||
picture: string,
|
||||
picture_small: string,
|
||||
picture_medium: string,
|
||||
picture_big: string,
|
||||
picture_xl: string,
|
||||
nb_album: number,
|
||||
nb_fan: number,
|
||||
radio: boolean,
|
||||
tracklist: string,
|
||||
}
|
||||
|
||||
interface Contributor {
|
||||
id: number,
|
||||
name: string,
|
||||
link: string,
|
||||
share: string,
|
||||
picture: string,
|
||||
picture_small: string,
|
||||
picture_medium: string,
|
||||
picture_big: string,
|
||||
picture_xl: string,
|
||||
radio: boolean,
|
||||
tracklist: string,
|
||||
role: string,
|
||||
}
|
||||
|
||||
interface Album {
|
||||
id: number,
|
||||
title: string,
|
||||
upc: string,
|
||||
link: string,
|
||||
share: string,
|
||||
cover: string,
|
||||
cover_small: string,
|
||||
cover_medium: string,
|
||||
cover_big: string,
|
||||
cover_xl: string,
|
||||
md5_image: string,
|
||||
genre_id: number, // use an enum?
|
||||
genres: any,
|
||||
label: string,
|
||||
nb_tracks: number,
|
||||
duration: number,
|
||||
fans: number,
|
||||
release_date: string,
|
||||
record_type: string,
|
||||
available: boolean,
|
||||
tracklist: string,
|
||||
explicit_lyrics: boolean,
|
||||
explicit_content_lyrics: ExplicitContent,
|
||||
explicit_content_cover: ExplicitContent,
|
||||
contributors: Contributor[],
|
||||
artist: Pick<Artist, "id" | "name" | "picture" | "picture_small" | "picture_medium" | "picture_big" | "picture_xl", "tracklist">,
|
||||
tracks: Pick<DeezerResponse<Pick<Track, "id" | "readable" | "title" | "title_short" | "title_version" | "link" | "duration" | "rank" | "explicit_lyrics" | "explicit_content_lyrics" | "explicit_content_cover" | "preview" | "md5_image" | "artist">[]>, "data">,
|
||||
}
|
||||
|
||||
interface SearchOptions {
|
||||
index?: number,
|
||||
limit?: number,
|
||||
}
|
||||
|
||||
interface QueryOptions extends SearchOptions {
|
||||
strict?: boolean,
|
||||
order?: SearchOrder,
|
||||
}
|
||||
|
||||
declare module 'deezer-js' {
|
||||
enum TrackFormats {
|
||||
FLAC = 9,
|
||||
MP3_320 = 3,
|
||||
MP3_128 = 1,
|
||||
MP4_RA3 = 15,
|
||||
MP4_RA2 = 14,
|
||||
MP4_RA1 = 13,
|
||||
DEFAULT = 8,
|
||||
LOCAL = 0,
|
||||
}
|
||||
|
||||
class API {
|
||||
http_headers: any;
|
||||
cookie_jar: CookieJar;
|
||||
access_token: string | null;
|
||||
|
||||
constructor(cookie_jar: CookieJar, headers: any): void;
|
||||
|
||||
api_call(method: string, args?: any): Promise<any>;
|
||||
|
||||
get_album(album_id: string | number): Promise<Album>;
|
||||
get_album_by_UPC(upc: string): Promise<Album>;
|
||||
get_album_comments(album_id: string | number, options?: SearchOptions): Promise<any>;
|
||||
get_album_fans(album_id: string | number, options?: SearchOptions): Promise<any>;
|
||||
get_album_tracks(album_id: string | number, options?: SearchOptions): Promise<DeezerResponse<[Track]>>;
|
||||
|
||||
get_artist(artist_id: string | number): Promise<Artist>;
|
||||
get_artist_top(artist_id: string | number, options?: SearchOptions): Promise<any>;
|
||||
get_artist_albums(artist_id: string | number, options?: SearchOptions): Promise<DeezerResponse<[Album]>>;
|
||||
get_artist_comments(artist_id: string | number, options?: SearchOptions): Promise<any>;
|
||||
get_artist_fans(artist_id: string | number, options?: SearchOptions): Promise<any>;
|
||||
get_artist_related(artist_id: string | number, options?: SearchOptions): Promise<any>;
|
||||
get_artist_radio(artist_id: string | number, options?: SearchOptions): Promise<any>;
|
||||
get_artist_playlists(artist_id: string | number, options?: SearchOptions): Promise<any>;
|
||||
|
||||
get_chart(genre_id?: string | number, options?: SearchOptions): Promise<any>;
|
||||
get_chart_tracks(genre_id?: string | number, options?: SearchOptions): Promise<any>;
|
||||
get_chart_albums(genre_id?: string | number, options?: SearchOptions): Promise<any>;
|
||||
get_chart_artists(genre_id?: string | number, options?: SearchOptions): Promise<any>;
|
||||
get_chart_playlists(genre_id?: string | number, options?: SearchOptions): Promise<any>;
|
||||
get_chart_podcasts(genre_id?: string | number, options?: SearchOptions): Promise<any>;
|
||||
|
||||
get_comment(comment_id: string | number): Promise<Comment>;
|
||||
|
||||
get_editorials(options?: SearchOptions): Promise<any>;
|
||||
get_editorial(genre_id?: number): Promise<any>;
|
||||
|
||||
// for now who cares
|
||||
|
||||
search(query: string, options?: QueryOptions): Promise<DeezerResponse<[Track | Album | Artist | Playlist | Radio | User]>>;
|
||||
search_album(query: string, options?: QueryOptions): Promise<DeezerResponse<[Album]>>;
|
||||
search_artist(query: string, options?: QueryOptions): Promise<DeezerResponse<[Artist]>>;
|
||||
search_playlist(query: string, options?: QueryOptions): Promise<DeezerResponse<[Playlist]>>;
|
||||
search_radio(query: string, options?: QueryOptions): Promise<DeezerResponse<[Radio]>>;
|
||||
search_track(query: string, options?: QueryOptions): Promise<DeezerResponse<[Track]>>;
|
||||
|
||||
get_track(song_id: string | number): Promise<Track>;
|
||||
get_track_by_ISRC(isrc: string): Promise<Track>;
|
||||
|
||||
get_user(user_id: string | number): Promise<User>;
|
||||
}
|
||||
|
||||
class GW {
|
||||
|
||||
}
|
||||
|
||||
class Deezer {
|
||||
http_headers: any;
|
||||
cookie_jar: CookieJar;
|
||||
logged_in: boolean;
|
||||
current_user: any;
|
||||
childs: any[];
|
||||
selected_account: number;
|
||||
|
||||
api: API;
|
||||
gw: GW;
|
||||
|
||||
constructor(): void;
|
||||
|
||||
login(email: string, password: string, re_captcha_token: string, child: number): Promise<boolean>;
|
||||
login_via_arl(arl: string): Promise<boolean>;
|
||||
_post_login(user_data: any): void;
|
||||
change_account(child_n: number): any[2];
|
||||
get_track_url(track_token: string, format: TrackFormats): Promise<Track>;
|
||||
get_tracks_url(track_tokens: string[] | string, format: TrackFormats): Promise<Track[]>;
|
||||
}
|
||||
}
|
|
@ -0,0 +1,13 @@
|
|||
import * as WS from 'ws';
|
||||
|
||||
/* eslint-disable no-unused-vars */
|
||||
export {};
|
||||
|
||||
declare global {
|
||||
namespace Express {
|
||||
// Inject additional properties on express.Request
|
||||
interface Application {
|
||||
ws(route: string, callback: (ws: WS.WebSocket, req: Express.Request) => void): Express
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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;
|
|
@ -0,0 +1,18 @@
|
|||
{
|
||||
"include": [
|
||||
"src/**/*.ts"
|
||||
],
|
||||
"exclude": [
|
||||
"node_modules"
|
||||
],
|
||||
"compilerOptions": {
|
||||
"incremental": true,
|
||||
"module": "commonjs",
|
||||
"target": "es6",
|
||||
"strict": true,
|
||||
"forceConsistentCasingInFileNames": true,
|
||||
"esModuleInterop": true,
|
||||
"skipLibCheck": true,
|
||||
"outDir": "dist"
|
||||
}
|
||||
}
|
Loading…
Reference in New Issue