mirror of
https://github.com/IrosTheBeggar/mStream.git
synced 2025-10-27 07:31:02 +00:00
migrate DB endpoints
This commit is contained in:
parent
03f8c18175
commit
c7477aa95b
@ -1,782 +0,0 @@
|
||||
const fe = require('path');
|
||||
const loki = require('lokijs');
|
||||
const winston = require('winston');
|
||||
const vpath = require('../../src/util/vpath');
|
||||
// const taskQueue = require('../../src/db/task-queue');
|
||||
|
||||
const userDataDbName = 'user-data.loki-v1.db';
|
||||
const filesDbName = 'files.loki-v2.db';
|
||||
|
||||
exports.getFileDbName = () => {
|
||||
return filesDbName;
|
||||
}
|
||||
|
||||
// Loki Collections
|
||||
var filesDB;
|
||||
var userDataDb;
|
||||
|
||||
var fileCollection;
|
||||
var playlistCollection;
|
||||
var userMetadataCollection;
|
||||
|
||||
// Default Functions for joining
|
||||
const mapFunDefault = function(left, right) {
|
||||
return {
|
||||
artist: left.artist,
|
||||
album: left.album,
|
||||
hash: left.hash,
|
||||
track: left.track,
|
||||
title: left.title,
|
||||
year: left.year,
|
||||
aaFile: left.aaFile,
|
||||
filepath: left.filepath,
|
||||
rating: right.rating,
|
||||
"replaygain-track-db": left.replaygainTrackDb,
|
||||
vpath: left.vpath
|
||||
};
|
||||
};
|
||||
|
||||
const rightFunDefault = function(rightData) {
|
||||
return rightData.hash + '-' + rightData.user;
|
||||
};
|
||||
|
||||
function loadDB() {
|
||||
filesDB.loadDatabase({}, err => {
|
||||
if (err) {
|
||||
winston.error(`Files DB Load Error : ${err}`);
|
||||
return;
|
||||
}
|
||||
|
||||
// Get files collection
|
||||
fileCollection = filesDB.getCollection('files');
|
||||
});
|
||||
|
||||
userDataDb.loadDatabase({}, err => {
|
||||
if (err) {
|
||||
winston.error(`Playlists DB Load Error : ${err}`);
|
||||
return;
|
||||
}
|
||||
|
||||
// Initialize playlists collection
|
||||
playlistCollection = userDataDb.getCollection('playlists');
|
||||
if (!playlistCollection) {
|
||||
// first time run so add and configure collection with some arbitrary options
|
||||
playlistCollection = userDataDb.addCollection("playlists");
|
||||
}
|
||||
|
||||
// Initialize user metadata collection (for song ratings, playback stats, etc)
|
||||
userMetadataCollection = userDataDb.getCollection('user-metadata');
|
||||
if (!userMetadataCollection) {
|
||||
userMetadataCollection = userDataDb.addCollection("user-metadata");
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
exports.loadDB = function () {
|
||||
loadDB();
|
||||
}
|
||||
|
||||
exports.getNumberOfFiles = function (vpaths) {
|
||||
if (!fileCollection) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
let total = 0;
|
||||
for (let vpath of vpaths) {
|
||||
total += fileCollection.count({ 'vpath': vpath })
|
||||
}
|
||||
|
||||
return total;
|
||||
}
|
||||
|
||||
// TPDP: fix this for server reboot
|
||||
exports.setup = function (mstream, program) {
|
||||
filesDB = new loki(fe.join(program.storage.dbDirectory, filesDbName));
|
||||
userDataDb = new loki(fe.join(program.storage.dbDirectory, userDataDbName));
|
||||
|
||||
// Used to determine the user has a working login token
|
||||
mstream.get('/ping', (req, res) => {
|
||||
let transcode = false;
|
||||
if (program.transcode && program.transcode.enabled) {
|
||||
transcode = {
|
||||
defaultCodec: program.transcode.defaultCodec,
|
||||
defaultBitrate: program.transcode.defaultBitrate,
|
||||
}
|
||||
}
|
||||
|
||||
res.json({
|
||||
vpaths: req.user.vpaths,
|
||||
playlists: getPlaylists(req.user.username),
|
||||
federationId: null,
|
||||
transcode
|
||||
});
|
||||
});
|
||||
|
||||
// Metadata lookup
|
||||
mstream.post('/db/metadata', (req, res) => {
|
||||
// TODO: Validate access to shared filepaths
|
||||
const pathInfo = vpath.getVPathInfo(req.body.filepath, req.user);
|
||||
if (!pathInfo) { return res.status(500).json({ error: 'Could not find file' }); }
|
||||
|
||||
if (!fileCollection) {
|
||||
res.json({ "filepath": req.body.filepath, "metadata": {} });
|
||||
return;
|
||||
}
|
||||
|
||||
const leftFun = function(leftData) {
|
||||
return leftData.hash + '-' + req.user.username;
|
||||
};
|
||||
|
||||
const result = fileCollection.chain().find({ '$and': [{'filepath': pathInfo.relativePath}, {'vpath': pathInfo.vpath}] }, true)
|
||||
.eqJoin(userMetadataCollection.chain(), leftFun, rightFunDefault, mapFunDefault).data();
|
||||
|
||||
if (!result || !result[0]) {
|
||||
res.json({ "filepath": req.body.filepath, "metadata": {} });
|
||||
return;
|
||||
}
|
||||
|
||||
res.json({
|
||||
"filepath": req.body.filepath,
|
||||
"metadata": {
|
||||
"artist": result[0].artist ? result[0].artist : null,
|
||||
"hash": result[0].hash ? result[0].hash : null,
|
||||
"album": result[0].album ? result[0].album : null,
|
||||
"track": result[0].track ? result[0].track : null,
|
||||
"title": result[0].title ? result[0].title : null,
|
||||
"year": result[0].year ? result[0].year : null,
|
||||
"album-art": result[0].aaFile ? result[0].aaFile : null,
|
||||
"rating": result[0].rating ? result[0].rating : null,
|
||||
"replaygain-track-db": result[0]['replaygain-track-db'] ? result[0]['replaygain-track-db'] : null
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
mstream.post('/playlist/add-song', (req, res) => {
|
||||
if(!req.body.song || !req.body.playlist) {
|
||||
return res.status(500).json({ error: 'Missing Params' });
|
||||
}
|
||||
|
||||
if(!playlistCollection) {
|
||||
return res.status(500).json({ error: 'Playlist DB Not Initiated' });
|
||||
}
|
||||
|
||||
playlistCollection.insert({
|
||||
name: req.body.playlist,
|
||||
filepath: req.body.song,
|
||||
user: req.user.username
|
||||
});
|
||||
|
||||
res.json({ success: true });
|
||||
userDataDb.saveDatabase(err => {
|
||||
if (err) {
|
||||
winston.error(`DB Save Error : ${err}`);
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
mstream.post('/playlist/remove-song', (req, res) => {
|
||||
if (!req.body.lokiid){
|
||||
return res.status(500).json({ error: 'Missing Params' });
|
||||
}
|
||||
|
||||
if (!playlistCollection){
|
||||
return res.status(500).json({ error: 'Playlist DB Not Initiated' });
|
||||
}
|
||||
|
||||
playlistCollection.findAndRemove({ '$loki': req.body.lokiid });
|
||||
res.json({ success: true });
|
||||
userDataDb.saveDatabase(err => {
|
||||
if (err) {
|
||||
winston.error(`BB Save Error : ${err}`)
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
// Save playlists
|
||||
mstream.post('/playlist/save', (req, res) => {
|
||||
if (!playlistCollection){
|
||||
return res.status(500).json({ error: 'Playlist DB Not Initiated' });
|
||||
}
|
||||
|
||||
const title = req.body.title;
|
||||
const songs = req.body.songs;
|
||||
|
||||
// Delete existing playlist
|
||||
playlistCollection.findAndRemove({
|
||||
'$and': [{
|
||||
'user': { '$eq': req.user.username }
|
||||
}, {
|
||||
'name': { '$eq': title }
|
||||
}]
|
||||
});
|
||||
|
||||
|
||||
while (songs.length > 0) {
|
||||
const song = songs.shift();
|
||||
playlistCollection.insert({
|
||||
name: title,
|
||||
filepath: song,
|
||||
user: req.user.username
|
||||
});
|
||||
}
|
||||
|
||||
res.json({ success: true });
|
||||
userDataDb.saveDatabase(err => {
|
||||
if (err) {
|
||||
winston.error(`DB Save Error : ${err}`);
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
// Get all playlists
|
||||
mstream.get('/playlist/getall', (req, res) => {
|
||||
res.json(getPlaylists(req.user.username));
|
||||
});
|
||||
|
||||
function getPlaylists(username) {
|
||||
const playlists = [];
|
||||
|
||||
const results = playlistCollection.find({ 'user': { '$eq': username } });
|
||||
const store = {};
|
||||
for (let row of results) {
|
||||
if (!store[row.name]) {
|
||||
playlists.push({ name: row.name });
|
||||
store[row.name] = true;
|
||||
}
|
||||
}
|
||||
return playlists;
|
||||
}
|
||||
|
||||
// Load a playlist
|
||||
mstream.post('/playlist/load', (req, res) => {
|
||||
if (!playlistCollection){
|
||||
return res.status(500).json({ error: 'Playlist DB Not Initiated' });
|
||||
}
|
||||
|
||||
const playlist = String(req.body.playlistname);
|
||||
const returnThis = [];
|
||||
|
||||
const results = playlistCollection.find({
|
||||
'$and': [{
|
||||
'user': { '$eq': req.user.username }
|
||||
}, {
|
||||
'name': { '$eq': playlist }
|
||||
}]
|
||||
});
|
||||
|
||||
for (let row of results) {
|
||||
// Look up metadata
|
||||
const pathInfo = vpath.getVPathInfo(row.filepath, req.user);
|
||||
if (!pathInfo) { return res.status(500).json({ error: 'Could not find file' }); }
|
||||
|
||||
let metadata = {};
|
||||
|
||||
if (fileCollection) {
|
||||
const leftFun = function(leftData) {
|
||||
return leftData.hash + '-' + req.user.username;
|
||||
};
|
||||
|
||||
const result = fileCollection.chain().find({ '$and': [{'filepath': pathInfo.relativePath}, { 'vpath': pathInfo.vpath }] }, true)
|
||||
.eqJoin(userMetadataCollection.chain(), leftFun, rightFunDefault, mapFunDefault).data();
|
||||
|
||||
if (result && result[0]) {
|
||||
metadata = {
|
||||
"artist": result[0].artist ? result[0].artist : null,
|
||||
"hash": result[0].hash ? result[0].hash : null,
|
||||
"album": result[0].album ? result[0].album : null,
|
||||
"track": result[0].track ? result[0].track : null,
|
||||
"title": result[0].title ? result[0].title : null,
|
||||
"year": result[0].year ? result[0].year : null,
|
||||
"album-art": result[0].aaFile ? result[0].aaFile : null,
|
||||
"rating": result[0].rating ? result[0].rating : null,
|
||||
"replaygain-track-db": result[0]['replaygain-track-db'] ? result[0]['replaygain-track-db'] : null
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
returnThis.push({ lokiId: row['$loki'], filepath: row.filepath, metadata: metadata });
|
||||
}
|
||||
|
||||
res.json(returnThis);
|
||||
});
|
||||
|
||||
// Delete playlist
|
||||
mstream.post('/playlist/delete', (req, res) => {
|
||||
if (!playlistCollection){
|
||||
return res.status(500).json({ error: 'Playlist DB Not Initiated' });
|
||||
}
|
||||
|
||||
// Delete existing playlist
|
||||
playlistCollection.findAndRemove({
|
||||
'$and': [
|
||||
{ 'user': { '$eq': req.user.username }},
|
||||
{ 'name': { '$eq': req.body.playlistname }}
|
||||
]
|
||||
});
|
||||
|
||||
res.json({ success: true });
|
||||
userDataDb.saveDatabase(err => {
|
||||
if (err) {
|
||||
winston.error(`DB Save Error : ${err}`);
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
mstream.get('/db/artists', (req, res) => {
|
||||
const artists = { "artists": [] };
|
||||
if (!fileCollection) { res.json(artists); }
|
||||
|
||||
let orClause;
|
||||
if (req.user.vpaths.length === 1) {
|
||||
orClause = { 'vpath': { '$eq': req.user.vpaths[0] } }
|
||||
} else {
|
||||
orClause = { '$or': [] }
|
||||
for (let vpath of req.user.vpaths) {
|
||||
orClause['$or'].push({ 'vpath': { '$eq': vpath } })
|
||||
}
|
||||
}
|
||||
|
||||
const results = fileCollection.find(orClause);
|
||||
const store = {};
|
||||
for (let row of results) {
|
||||
if (!store[row.artist] && !(row.artist === undefined || row.artist === null)) {
|
||||
store[row.artist] = true;
|
||||
}
|
||||
}
|
||||
|
||||
artists.artists = Object.keys(store).sort((a, b) => {
|
||||
return a.localeCompare(b);
|
||||
});
|
||||
|
||||
res.json(artists);
|
||||
});
|
||||
|
||||
mstream.post('/db/artists-albums', (req, res) => {
|
||||
const albums = { "albums": [] };
|
||||
if (fileCollection) {
|
||||
let orClause;
|
||||
if (req.user.vpaths.length === 1) {
|
||||
orClause = { 'vpath': { '$eq': req.user.vpaths[0] } }
|
||||
} else {
|
||||
orClause = { '$or': [] }
|
||||
for (let vpath of req.user.vpaths) {
|
||||
orClause['$or'].push({ 'vpath': { '$eq': vpath } })
|
||||
}
|
||||
}
|
||||
|
||||
const results = fileCollection.chain().find({
|
||||
'$and': [
|
||||
orClause,
|
||||
{'artist': { '$eq': String(req.body.artist) }}
|
||||
]
|
||||
}).simplesort('year', true).data();
|
||||
|
||||
const store = {};
|
||||
for (let row of results) {
|
||||
if (!store[row.album]) {
|
||||
albums.albums.push({
|
||||
name: row.album,
|
||||
album_art_file: row.aaFile ? row.aaFile : null
|
||||
});
|
||||
store[row.album] = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
res.json(albums);
|
||||
});
|
||||
|
||||
mstream.get('/db/albums', (req, res) => {
|
||||
const albums = { "albums": [] };
|
||||
if (!fileCollection) { return res.json(albums); }
|
||||
|
||||
let orClause;
|
||||
if (req.user.vpaths.length === 1) {
|
||||
orClause = { 'vpath': { '$eq': req.user.vpaths[0] } }
|
||||
} else {
|
||||
orClause = { '$or': [] }
|
||||
for (let vpath of req.user.vpaths) {
|
||||
orClause['$or'].push({ 'vpath': { '$eq': vpath } })
|
||||
}
|
||||
}
|
||||
|
||||
const results = fileCollection.find(orClause);
|
||||
const store = {};
|
||||
for (let row of results) {
|
||||
if (!store[row.album] && !(row.album === undefined || row.album === null)) {
|
||||
albums.albums.push({ name: row.album, album_art_file: row.aaFile });
|
||||
store[row.album] = true;
|
||||
}
|
||||
}
|
||||
|
||||
albums.albums.sort((a, b) => {
|
||||
return a.name.localeCompare(b.name);
|
||||
});
|
||||
|
||||
res.json(albums);
|
||||
});
|
||||
|
||||
mstream.post('/db/album-songs', (req, res) => {
|
||||
// TODO: Add scanning attributes to all DB functions
|
||||
// This gives a signal to the UI
|
||||
// const songs = { songs: [], scanning: taskQueue.isScanning() };
|
||||
const songs = [];
|
||||
if (fileCollection) {
|
||||
let orClause;
|
||||
if (req.user.vpaths.length === 1) {
|
||||
orClause = { 'vpath': { '$eq': req.user.vpaths[0] } }
|
||||
} else {
|
||||
orClause = { '$or': [] }
|
||||
for (let vpath of req.user.vpaths) {
|
||||
orClause['$or'].push({ 'vpath': { '$eq': vpath } })
|
||||
}
|
||||
}
|
||||
|
||||
let artistClause;
|
||||
if (req.body.artist) {
|
||||
artistClause = {'artist': { '$eq': String(req.body.artist) }}
|
||||
}
|
||||
|
||||
const leftFun = function(leftData) {
|
||||
return leftData.hash + '-' + req.user.username;
|
||||
};
|
||||
|
||||
const album = req.body.album ? String(req.body.album) : null;
|
||||
const results = fileCollection.chain().find({
|
||||
'$and': [
|
||||
orClause,
|
||||
{'album': { '$eq': album }},
|
||||
artistClause
|
||||
]
|
||||
}).compoundsort(['disk','track','filepath']).eqJoin(userMetadataCollection.chain(), leftFun, rightFunDefault, mapFunDefault).data();
|
||||
|
||||
for (let row of results) {
|
||||
songs.push({
|
||||
"filepath": fe.join(row.vpath, row.filepath).replace(/\\/g, '/'),
|
||||
"metadata": {
|
||||
"artist": row.artist ? row.artist : null,
|
||||
"hash": row.hash ? row.hash : null,
|
||||
"album": row.album ? row.album : null,
|
||||
"track": row.track ? row.track : null,
|
||||
"title": row.title ? row.title : null,
|
||||
"year": row.year ? row.year : null,
|
||||
"album-art": row.aaFile ? row.aaFile : null,
|
||||
"filename": fe.basename(row.filepath),
|
||||
"rating": row.rating ? row.rating : null,
|
||||
"replaygain-track-db": row['replaygain-track-db'] ? row['replaygain-track-db'] : null
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
res.json(songs);
|
||||
});
|
||||
|
||||
mstream.post('/db/rate-song', (req, res) => {
|
||||
if (!req.body.filepath || !req.body.rating || !Number.isInteger(req.body.rating) || req.body.rating < 0 || req.body.rating > 10) {
|
||||
return res.status(500).json({ error: 'Bad input data' });
|
||||
}
|
||||
|
||||
const pathInfo = vpath.getVPathInfo(req.body.filepath);
|
||||
if (!pathInfo) { return res.status(500).json({ error: 'Could not find file' }); }
|
||||
|
||||
if (!userMetadataCollection || !fileCollection) {
|
||||
res.status(500).json({ error: 'No DB' });
|
||||
return;
|
||||
}
|
||||
|
||||
const result = fileCollection.findOne({ '$and':[{ 'filepath': pathInfo.relativePath}, { 'vpath': pathInfo.vpath }] });
|
||||
if (!result) {
|
||||
res.status(500).json({ error: 'File not found in DB' });
|
||||
return;
|
||||
}
|
||||
|
||||
const result2 = userMetadataCollection.findOne({ '$and':[{ 'hash': result.hash}, { 'user': req.user.username }] });
|
||||
if (!result2) {
|
||||
userMetadataCollection.insert({
|
||||
user: req.user.username,
|
||||
hash: result.hash,
|
||||
rating: req.body.rating
|
||||
});
|
||||
} else {
|
||||
result2.rating = req.body.rating;
|
||||
userMetadataCollection.update(result2);
|
||||
}
|
||||
|
||||
res.json({});
|
||||
|
||||
userDataDb.saveDatabase(err => {
|
||||
if (err) {
|
||||
winston.error(`DB Save Error : ${err}`);
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
mstream.post('/db/random-songs', (req, res) => {
|
||||
if (!fileCollection) {
|
||||
res.status(500).json({ error: 'No files in DB' });
|
||||
return;
|
||||
};
|
||||
|
||||
// Ignore list
|
||||
let ignoreList = [];
|
||||
if (req.body.ignoreList && Array.isArray(req.body.ignoreList)) {
|
||||
ignoreList = req.body.ignoreList;
|
||||
}
|
||||
|
||||
let ignorePercentage = .5;
|
||||
if (req.body.ignorePercentage && typeof req.body.ignorePercentage === 'number' && req.body.ignorePercentage < 1 && !req.body.ignorePercentage < 0) {
|
||||
ignorePercentage = req.body.ignorePercentage;
|
||||
}
|
||||
|
||||
// // Preference for recently played or not played recently
|
||||
|
||||
let orClause = { '$or': [] };
|
||||
for (let vpath of req.user.vpaths) {
|
||||
if (req.body.ignoreVPaths && typeof req.body.ignoreVPaths === 'object' && req.body.ignoreVPaths[vpath] === true) {
|
||||
continue;
|
||||
}
|
||||
orClause['$or'].push({ 'vpath': { '$eq': vpath } });
|
||||
}
|
||||
|
||||
let minRating = Number(req.body.minRating);
|
||||
// Add Rating clause
|
||||
if (minRating && typeof minRating === 'number' && minRating <= 10 && !minRating < 1) {
|
||||
orClause = {'$and': [
|
||||
orClause,
|
||||
{ 'rating': { '$gte': req.body.minRating } }
|
||||
]};
|
||||
}
|
||||
|
||||
const leftFun = function(leftData) {
|
||||
return leftData.hash + '-' + req.user.username;
|
||||
};
|
||||
|
||||
const results = fileCollection.chain().eqJoin(userMetadataCollection.chain(), leftFun, rightFunDefault, mapFunDefault).find(orClause).data();
|
||||
|
||||
const count = results.length;
|
||||
if (count === 0) {
|
||||
res.status(444).json({ error: 'No songs that match criteria' });
|
||||
return;
|
||||
}
|
||||
|
||||
while (ignoreList.length > count * ignorePercentage) {
|
||||
ignoreList.shift();
|
||||
}
|
||||
|
||||
const returnThis = { songs: [], ignoreList: [] };
|
||||
|
||||
let randomNumber = Math.floor(Math.random() * count);
|
||||
while (ignoreList.indexOf(randomNumber) > -1) {
|
||||
randomNumber = Math.floor(Math.random() * count);
|
||||
}
|
||||
|
||||
const randomSong = results[randomNumber];
|
||||
|
||||
returnThis.songs.push({
|
||||
"filepath": fe.join(randomSong.vpath, randomSong.filepath).replace(/\\/g, '/'),
|
||||
"metadata": {
|
||||
"artist": randomSong.artist ? randomSong.artist : null,
|
||||
"hash": randomSong.hash ? randomSong.hash : null,
|
||||
"album": randomSong.album ? randomSong.album : null,
|
||||
"track": randomSong.track ? randomSong.track : null,
|
||||
"title": randomSong.title ? randomSong.title : null,
|
||||
"year": randomSong.year ? randomSong.year : null,
|
||||
"album-art": randomSong.aaFile ? randomSong.aaFile : null,
|
||||
"rating": randomSong.rating ? randomSong.rating : null,
|
||||
"replaygain-track-db": randomSong['replaygain-track-db'] ? randomSong['replaygain-track-db'] : null
|
||||
}
|
||||
});
|
||||
|
||||
ignoreList.push(randomNumber);
|
||||
returnThis.ignoreList = ignoreList;
|
||||
res.json(returnThis);
|
||||
});
|
||||
|
||||
mstream.post('/db/search', (req, res) => {
|
||||
if (!req.body.search) {
|
||||
return res.status(500).json({ error: 'Bad input data' });
|
||||
}
|
||||
// Get user inputs
|
||||
const artists = req.body.noArtists === true ? [] : searchByX(req, 'artist');
|
||||
const albums = req.body.noAlbums === true ? [] : searchByX(req, 'album');
|
||||
const files = req.body.noFiles === true ? [] : searchByX(req, 'filepath');
|
||||
const title = req.body.noTitles === true ? [] : searchByX(req, 'title', 'filepath');
|
||||
|
||||
res.json({artists, albums, files, title });
|
||||
});
|
||||
|
||||
function searchByX(req, searchCol, resCol) {
|
||||
if (!resCol) {
|
||||
resCol = searchCol;
|
||||
}
|
||||
|
||||
const returnThis = [];
|
||||
if (!fileCollection) { return returnThis; }
|
||||
|
||||
let orClause;
|
||||
if (req.user.vpaths.length === 1) {
|
||||
orClause = { 'vpath': { '$eq': req.user.vpaths[0] } }
|
||||
} else {
|
||||
orClause = { '$or': [] }
|
||||
for (let vpath of req.user.vpaths) {
|
||||
orClause['$or'].push({ 'vpath': { '$eq': vpath } })
|
||||
}
|
||||
}
|
||||
|
||||
const findThis = {
|
||||
'$and': [
|
||||
orClause,
|
||||
{[searchCol]: {'$regex': [String(req.body.search), 'i']}}
|
||||
]
|
||||
};
|
||||
const results = fileCollection.find(findThis);
|
||||
|
||||
const store = {};
|
||||
for (let row of results) {
|
||||
if (!store[row[resCol]]) {
|
||||
let name = row[resCol];
|
||||
let filepath = false;
|
||||
|
||||
if (searchCol === 'filepath') {
|
||||
name = fe.join(row.vpath, row[resCol]).replace(/\\/g, '/');
|
||||
filepath = fe.join(row.vpath, row[resCol]).replace(/\\/g, '/');
|
||||
} else if (searchCol === 'title') {
|
||||
name = `${row.artist} - ${row.title}`;
|
||||
filepath = fe.join(row.vpath, row[resCol]).replace(/\\/g, '/');
|
||||
}
|
||||
|
||||
returnThis.push({
|
||||
name: name,
|
||||
album_art_file: row.aaFile ? row.aaFile : null,
|
||||
filepath
|
||||
});
|
||||
store[row[resCol]] = true;
|
||||
}
|
||||
}
|
||||
|
||||
return returnThis;
|
||||
}
|
||||
|
||||
mstream.get('/db/get-rated', (req, res) => {
|
||||
const songs = [];
|
||||
if (!fileCollection) {
|
||||
res.json(songs);
|
||||
return;
|
||||
}
|
||||
|
||||
let orClause;
|
||||
if (req.user.vpaths.length === 1) {
|
||||
orClause = { 'vpath': { '$eq': req.user.vpaths[0] } }
|
||||
} else {
|
||||
orClause = { '$or': [] }
|
||||
for (let vpath of req.user.vpaths) {
|
||||
orClause['$or'].push({ 'vpath': { '$eq': vpath } })
|
||||
}
|
||||
}
|
||||
|
||||
var mapFun = function(left, right) {
|
||||
return {
|
||||
artist: right.artist,
|
||||
album: right.album,
|
||||
hash: right.hash,
|
||||
track: right.track,
|
||||
title: right.title,
|
||||
year: right.year,
|
||||
aaFile: right.aaFile,
|
||||
filepath: right.filepath,
|
||||
rating: left.rating,
|
||||
"replaygain-track-db": right.replaygainTrackDb,
|
||||
vpath: right.vpath
|
||||
};
|
||||
};
|
||||
|
||||
var leftFun = function(leftData) {
|
||||
return leftData.hash + '-' + leftData.user;
|
||||
};
|
||||
|
||||
var rightFun = function(rightData) {
|
||||
return rightData.hash + '-' + req.user.username;
|
||||
};
|
||||
|
||||
const results = userMetadataCollection.chain().eqJoin(fileCollection.chain(), leftFun, rightFun, mapFun).find({
|
||||
'$and': [
|
||||
orClause,
|
||||
{ 'rating': { '$gt': 0 } }
|
||||
]
|
||||
}).simplesort('rating', true).data();
|
||||
|
||||
for (let row of results) {
|
||||
songs.push({
|
||||
"filepath": fe.join(row.vpath, row.filepath).replace(/\\/g, '/'),
|
||||
"metadata": {
|
||||
"artist": row.artist ? row.artist : null,
|
||||
"hash": row.hash ? row.hash : null,
|
||||
"album": row.album ? row.album : null,
|
||||
"track": row.track ? row.track : null,
|
||||
"title": row.title ? row.title : null,
|
||||
"year": row.year ? row.year : null,
|
||||
"album-art": row.aaFile ? row.aaFile : null,
|
||||
"filename": fe.basename(row.filepath),
|
||||
"rating": row.rating ? row.rating : null,
|
||||
"replaygain-track-db": row['replaygain-track-db'] ? row['replaygain-track-db'] : null
|
||||
}
|
||||
});
|
||||
}
|
||||
res.json(songs);
|
||||
});
|
||||
|
||||
mstream.post('/db/recent/added', (req, res) => {
|
||||
let limit = parseInt(req.body.limit);
|
||||
if (!limit || typeof limit !== 'number' || limit < 0) {
|
||||
limit = 100;
|
||||
}
|
||||
|
||||
const songs = [];
|
||||
if (!fileCollection) {
|
||||
res.json(songs);
|
||||
return;
|
||||
}
|
||||
|
||||
let orClause;
|
||||
if (req.user.vpaths.length === 1) {
|
||||
orClause = { 'vpath': { '$eq': req.user.vpaths[0] } }
|
||||
} else {
|
||||
orClause = { '$or': [] }
|
||||
for (let vpath of req.user.vpaths) {
|
||||
orClause['$or'].push({ 'vpath': { '$eq': vpath } })
|
||||
}
|
||||
}
|
||||
|
||||
const leftFun = function(leftData) {
|
||||
return leftData.hash + '-' + req.user.username;
|
||||
};
|
||||
|
||||
const results = fileCollection.chain().find({
|
||||
'$and': [
|
||||
orClause,
|
||||
{ 'ts': { '$gt': 0 } }
|
||||
]
|
||||
}).simplesort('ts', true).limit(limit).eqJoin(userMetadataCollection.chain(), leftFun, rightFunDefault, mapFunDefault).data();
|
||||
|
||||
for (let row of results) {
|
||||
songs.push({
|
||||
"filepath": fe.join(row.vpath, row.filepath).replace(/\\/g, '/'),
|
||||
"metadata": {
|
||||
"artist": row.artist ? row.artist : null,
|
||||
"hash": row.hash ? row.hash : null,
|
||||
"album": row.album ? row.album : null,
|
||||
"track": row.track ? row.track : null,
|
||||
"title": row.title ? row.title : null,
|
||||
"year": row.year ? row.year : null,
|
||||
"album-art": row.aaFile ? row.aaFile : null,
|
||||
"filename": fe.basename(row.filepath),
|
||||
"rating": row.rating ? row.rating : null,
|
||||
"replaygain-track": row.replaygainTrack ? row.replaygainTrack : null
|
||||
}
|
||||
});
|
||||
}
|
||||
res.json(songs);
|
||||
});
|
||||
|
||||
// Load DB on boot
|
||||
loadDB();
|
||||
}
|
||||
15
mstream.js
15
mstream.js
@ -4,6 +4,7 @@ const fs = require('fs');
|
||||
const bodyParser = require('body-parser');
|
||||
|
||||
const dbApi = require('./src/api/db');
|
||||
const playlistApi = require('./src/api/playlist');
|
||||
const authApi = require('./src/api/auth');
|
||||
const fileExplorerApi = require('./src/api/file-explorer');
|
||||
const downloadApi = require('./src/api/download');
|
||||
@ -15,6 +16,7 @@ const config = require('./src/state/config');
|
||||
const logger = require('./src/logger');
|
||||
const scrobbler = require('./modules/scrobbler');
|
||||
const transode = require('./src/api/transcode');
|
||||
const dbManager = require('./src/db/manager');
|
||||
|
||||
let mstream;
|
||||
let server;
|
||||
@ -59,6 +61,9 @@ exports.serveIt = async configFile => {
|
||||
next();
|
||||
});
|
||||
|
||||
// Setup DB
|
||||
dbManager.initLoki();
|
||||
|
||||
// Give access to public folder
|
||||
mstream.use('/', express.static(config.program.webAppDirectory));
|
||||
|
||||
@ -71,14 +76,18 @@ exports.serveIt = async configFile => {
|
||||
|
||||
adminApi.setup(mstream);
|
||||
dbApi.setup(mstream);
|
||||
playlistApi.setup(mstream);
|
||||
downloadApi.setup(mstream);
|
||||
fileExplorerApi.setup(mstream);
|
||||
require('./modules/db-read/database-public-loki.js').setup(mstream, config.program);
|
||||
transode.setup(mstream);
|
||||
scrobbler.setup(mstream, config.program);
|
||||
remoteApi.setupAfterAuth(mstream, server);
|
||||
sharedApi.setupAfterSecurity(mstream);
|
||||
|
||||
// Versioned APIs
|
||||
mstream.get('/api/', (req, res) => res.json({ "version": "0.1.0", "supportedVersions": ["1"] }));
|
||||
mstream.get('/api/v1', (req, res) => res.json({ "version": "0.1.0" }));
|
||||
|
||||
// album art folder
|
||||
mstream.use('/album-art', express.static(config.program.storage.albumArtDirectory));
|
||||
|
||||
@ -87,10 +96,6 @@ exports.serveIt = async configFile => {
|
||||
mstream.use('/media/' + key + '/', express.static(config.program.folders[key].root));
|
||||
});
|
||||
|
||||
// Versioned APIs
|
||||
mstream.get('/api/', (req, res) => res.json({ "version": "0.1.0", "supportedVersions": ["1"] }));
|
||||
mstream.get('/api/v1', (req, res) => res.json({ "version": "0.1.0" }));
|
||||
|
||||
// Start the server!
|
||||
server.on('request', mstream);
|
||||
server.listen(config.program.port, config.program.address, () => {
|
||||
|
||||
525
src/api/db.js
525
src/api/db.js
@ -1,16 +1,535 @@
|
||||
const dbQueue = require('../db/task-queue');
|
||||
const mstreamReadPublicDB = require('../../modules/db-read/database-public-loki');
|
||||
const winston = require('winston');
|
||||
const Joi = require('joi');
|
||||
const path = require('path');
|
||||
const vpath = require('../util/vpath');
|
||||
const dbQueue = require('../db/task-queue');
|
||||
const db = require('../db/manager');
|
||||
|
||||
getNumberOfFiles = (vpaths) => {
|
||||
if (!db.getFileCollection()) { return 0; }
|
||||
|
||||
let total = 0;
|
||||
for (const vpath of vpaths) {
|
||||
total += db.getFileCollection().count({ 'vpath': vpath })
|
||||
}
|
||||
|
||||
return total;
|
||||
}
|
||||
|
||||
const mapFunDefault = (left, right) => {
|
||||
return {
|
||||
artist: left.artist,
|
||||
album: left.album,
|
||||
hash: left.hash,
|
||||
track: left.track,
|
||||
title: left.title,
|
||||
year: left.year,
|
||||
aaFile: left.aaFile,
|
||||
filepath: left.filepath,
|
||||
rating: right.rating,
|
||||
"replaygain-track-db": left.replaygainTrackDb,
|
||||
vpath: left.vpath
|
||||
};
|
||||
};
|
||||
|
||||
const rightFunDefault = (rightData) => {
|
||||
return rightData.hash + '-' + rightData.user;
|
||||
};
|
||||
|
||||
function renderMetadataObj(row) {
|
||||
return {
|
||||
"filepath": path.join(row.vpath, row.filepath).replace(/\\/g, '/'),
|
||||
"metadata": {
|
||||
"artist": row.artist ? row.artist : null,
|
||||
"hash": row.hash ? row.hash : null,
|
||||
"album": row.album ? row.album : null,
|
||||
"track": row.track ? row.track : null,
|
||||
"title": row.title ? row.title : null,
|
||||
"year": row.year ? row.year : null,
|
||||
"album-art": row.aaFile ? row.aaFile : null,
|
||||
"rating": row.rating ? row.rating : null,
|
||||
"replaygain-track": row.replaygainTrack ? row.replaygainTrack : null
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
function renderOrClause(vpaths) {
|
||||
if (vpaths.length === 1) {
|
||||
return { 'vpath': { '$eq': vpaths[0] } };
|
||||
}
|
||||
|
||||
const returnThis = { '$or': [] }
|
||||
for (let vpath of vpaths) {
|
||||
returnThis['$or'].push({ 'vpath': { '$eq': vpath } })
|
||||
}
|
||||
|
||||
return returnThis;
|
||||
}
|
||||
|
||||
exports.setup = (mstream) => {
|
||||
mstream.get('/api/v1/db/status', (req, res) => {
|
||||
try {
|
||||
res.json({
|
||||
totalFileCount: mstreamReadPublicDB.getNumberOfFiles(req.user.vpaths),
|
||||
totalFileCount: getNumberOfFiles(req.user.vpaths),
|
||||
locked: dbQueue.isScanning()
|
||||
});
|
||||
}catch(err) {
|
||||
winston.error('Db Error', { stack: err });
|
||||
res.status(500).json({});
|
||||
}
|
||||
});
|
||||
|
||||
mstream.post('/api/v1/db/metadata', (req, res) => {
|
||||
try {
|
||||
const pathInfo = vpath.getVPathInfo(req.body.filepath, req.user);
|
||||
if (!pathInfo) { throw 'File Not Found' }
|
||||
if (!db.getFileCollection()) { return res.json({ "filepath": req.body.filepath, "metadata": {} }); }
|
||||
|
||||
const leftFun = (leftData) => {
|
||||
return leftData.hash + '-' + req.user.username;
|
||||
};
|
||||
|
||||
const result = db.getFileCollection().chain().find({ '$and': [{'filepath': pathInfo.relativePath}, {'vpath': pathInfo.vpath}] }, true)
|
||||
.eqJoin(db.getUserMetadataCollection().chain(), leftFun, rightFunDefault, mapFunDefault).data();
|
||||
|
||||
if (!result || !result[0]) {
|
||||
return res.json({ "filepath": req.body.filepath, "metadata": {} });
|
||||
}
|
||||
|
||||
res.json(renderMetadataObj(result[0]));
|
||||
} catch (err) {
|
||||
winston.error('Db Error', { stack: err });
|
||||
res.status(500).json({ error: typeof err === 'string' ? err : 'Unknown Error' });
|
||||
}
|
||||
});
|
||||
|
||||
mstream.get('/api/v1/db/artists', (req, res) => {
|
||||
try {
|
||||
const artists = { "artists": [] };
|
||||
if (!db.getFileCollection()) { res.json(artists); }
|
||||
|
||||
const results = db.getFileCollection().find(renderOrClause(req.user.vpaths));
|
||||
const store = {};
|
||||
for (let row of results) {
|
||||
if (!store[row.artist] && !(row.artist === undefined || row.artist === null)) {
|
||||
store[row.artist] = true;
|
||||
}
|
||||
}
|
||||
|
||||
artists.artists = Object.keys(store).sort((a, b) => {
|
||||
return a.localeCompare(b);
|
||||
});
|
||||
|
||||
res.json(artists);
|
||||
} catch (err) {
|
||||
winston.error('Db Error', { stack: err });
|
||||
res.status(500).json({ error: typeof err === 'string' ? err : 'Unknown Error' });
|
||||
}
|
||||
});
|
||||
|
||||
mstream.post('/api/v1/db/artists-albums', (req, res) => {
|
||||
try {
|
||||
const albums = { "albums": [] };
|
||||
if (!db.getFileCollection()) { return res.json(albums); }
|
||||
|
||||
const results = db.getFileCollection().chain().find({
|
||||
'$and': [
|
||||
renderOrClause(req.user.vpaths),
|
||||
{'artist': { '$eq': String(req.body.artist) }}
|
||||
]
|
||||
}).simplesort('year', true).data();
|
||||
|
||||
const store = {};
|
||||
for (let row of results) {
|
||||
if (!store[row.album]) {
|
||||
albums.albums.push({
|
||||
name: row.album,
|
||||
album_art_file: row.aaFile ? row.aaFile : null
|
||||
});
|
||||
store[row.album] = true;
|
||||
}
|
||||
}
|
||||
|
||||
res.json(albums);
|
||||
} catch (err) {
|
||||
winston.error('Db Error', { stack: err });
|
||||
res.status(500).json({ error: typeof err === 'string' ? err : 'Unknown Error' });
|
||||
}
|
||||
});
|
||||
|
||||
mstream.get('/api/v1/db/albums', (req, res) => {
|
||||
try {
|
||||
const albums = { "albums": [] };
|
||||
if (!db.getFileCollection()) { return res.json(albums); }
|
||||
|
||||
const results = db.getFileCollection().find(renderOrClause(req.user.vpaths));
|
||||
const store = {};
|
||||
for (let row of results) {
|
||||
if (!store[row.album] && !(row.album === undefined || row.album === null)) {
|
||||
albums.albums.push({ name: row.album, album_art_file: row.aaFile });
|
||||
store[row.album] = true;
|
||||
}
|
||||
}
|
||||
|
||||
albums.albums.sort((a, b) => {
|
||||
return a.name.localeCompare(b.name);
|
||||
});
|
||||
|
||||
res.json(albums);
|
||||
} catch (err) {
|
||||
winston.error('Db Error', { stack: err });
|
||||
res.status(500).json({ error: typeof err === 'string' ? err : 'Unknown Error' });
|
||||
}
|
||||
});
|
||||
|
||||
mstream.post('/api/v1/db/album-songs', (req, res) => {
|
||||
try {
|
||||
if (!db.getFileCollection()) { throw 'DB Not Working'; }
|
||||
|
||||
let artistClause;
|
||||
if (req.body.artist) {
|
||||
artistClause = {'artist': { '$eq': req.body.artist }};
|
||||
}
|
||||
|
||||
const leftFun = (leftData) => {
|
||||
return leftData.hash + '-' + req.user.username;
|
||||
};
|
||||
|
||||
const album = req.body.album ? String(req.body.album) : null;
|
||||
const results = db.getFileCollection().chain().find({
|
||||
'$and': [
|
||||
renderOrClause(req.user.vpaths),
|
||||
{'album': { '$eq': album }},
|
||||
artistClause
|
||||
]
|
||||
}).compoundsort(['disk','track','filepath']).eqJoin(db.getUserMetadataCollection().chain(), leftFun, rightFunDefault, mapFunDefault).data();
|
||||
|
||||
const songs = [];
|
||||
for (const row of results) {
|
||||
songs.push(renderMetadataObj(row));
|
||||
}
|
||||
res.json(songs);
|
||||
} catch (err) {
|
||||
winston.error('Db Error', { stack: err });
|
||||
res.status(500).json({ error: typeof err === 'string' ? err : 'Unknown Error' });
|
||||
}
|
||||
});
|
||||
|
||||
mstream.post('/api/v1/db/search', async (req, res) => {
|
||||
try {
|
||||
const schema = Joi.object({
|
||||
search: Joi.string().required(),
|
||||
noArtists: Joi.boolean().optional(),
|
||||
noAlbums: Joi.boolean().optional(),
|
||||
noTitles: Joi.boolean().optional(),
|
||||
noFiles: Joi.boolean().optional(),
|
||||
});
|
||||
await schema.validateAsync(req.body);
|
||||
}catch (err) {
|
||||
return res.status(500).json({ error: 'Validation Error' });
|
||||
}
|
||||
|
||||
try {
|
||||
// Get user inputs
|
||||
const artists = req.body.noArtists === true ? [] : searchByX(req, 'artist');
|
||||
const albums = req.body.noAlbums === true ? [] : searchByX(req, 'album');
|
||||
const files = req.body.noFiles === true ? [] : searchByX(req, 'filepath');
|
||||
const title = req.body.noTitles === true ? [] : searchByX(req, 'title', 'filepath');
|
||||
res.json({artists, albums, files, title });
|
||||
} catch (err) {
|
||||
winston.error('Db Error', { stack: err });
|
||||
res.status(500).json({ error: typeof err === 'string' ? err : 'Unknown Error' });
|
||||
}
|
||||
});
|
||||
|
||||
function searchByX(req, searchCol, resCol) {
|
||||
if (!resCol) {
|
||||
resCol = searchCol;
|
||||
}
|
||||
|
||||
const returnThis = [];
|
||||
if (!db.getFileCollection()) { return returnThis; }
|
||||
|
||||
const findThis = {
|
||||
'$and': [
|
||||
renderOrClause(req.user.vpaths),
|
||||
{[searchCol]: {'$regex': [String(req.body.search), 'i']}}
|
||||
]
|
||||
};
|
||||
const results = db.getFileCollection().find(findThis);
|
||||
|
||||
const store = {};
|
||||
for (let row of results) {
|
||||
if (!store[row[resCol]]) {
|
||||
let name = row[resCol];
|
||||
let filepath = false;
|
||||
|
||||
if (searchCol === 'filepath') {
|
||||
name = path.join(row.vpath, row[resCol]).replace(/\\/g, '/');
|
||||
filepath = path.join(row.vpath, row[resCol]).replace(/\\/g, '/');
|
||||
} else if (searchCol === 'title') {
|
||||
name = `${row.artist} - ${row.title}`;
|
||||
filepath = path.join(row.vpath, row[resCol]).replace(/\\/g, '/');
|
||||
}
|
||||
|
||||
returnThis.push({
|
||||
name: name,
|
||||
album_art_file: row.aaFile ? row.aaFile : null,
|
||||
filepath
|
||||
});
|
||||
store[row[resCol]] = true;
|
||||
}
|
||||
}
|
||||
|
||||
return returnThis;
|
||||
}
|
||||
|
||||
mstream.post('/api/v1/db/metadata', (req, res) => {
|
||||
try {
|
||||
const pathInfo = vpath.getVPathInfo(req.body.filepath, req.user);
|
||||
if (!pathInfo) { throw 'Could not find file'; }
|
||||
|
||||
if (!db.getFileCollection()) { throw 'DB Not Working'; }
|
||||
|
||||
const leftFun = (leftData) => {
|
||||
return leftData.hash + '-' + req.user.username;
|
||||
};
|
||||
|
||||
const result = db.getFileCollection().chain().find({ '$and': [{'filepath': pathInfo.relativePath}, {'vpath': pathInfo.vpath}] }, true)
|
||||
.eqJoin(db.getUserMetadataCollection().chain(), leftFun, rightFunDefault, mapFunDefault).data();
|
||||
|
||||
if (!result || !result[0]) { throw 'No Metadata Found'; }
|
||||
|
||||
res.json(renderMetadataObj(result[0]));
|
||||
} catch (err) {
|
||||
winston.error('Db Error', { stack: err });
|
||||
res.status(500).json({ error: typeof err === 'string' ? err : 'Unknown Error' });
|
||||
}
|
||||
});
|
||||
|
||||
mstream.get('/api/v1/db/rated', (req, res) => {
|
||||
try {
|
||||
if (!db.getFileCollection()) { throw 'DB Not Ready'; }
|
||||
|
||||
const mapFun = (left, right) => {
|
||||
return {
|
||||
artist: right.artist,
|
||||
album: right.album,
|
||||
hash: right.hash,
|
||||
track: right.track,
|
||||
title: right.title,
|
||||
year: right.year,
|
||||
aaFile: right.aaFile,
|
||||
filepath: right.filepath,
|
||||
rating: left.rating,
|
||||
"replaygain-track-db": right.replaygainTrackDb,
|
||||
vpath: right.vpath
|
||||
};
|
||||
};
|
||||
|
||||
const leftFun = (leftData) => {
|
||||
return leftData.hash + '-' + leftData.user;
|
||||
};
|
||||
|
||||
const rightFun = (rightData) => {
|
||||
return rightData.hash + '-' + req.user.username;
|
||||
};
|
||||
|
||||
const results = db.getUserMetadataCollection().chain().eqJoin(db.getFileCollection().chain(), leftFun, rightFun, mapFun).find({
|
||||
'$and': [
|
||||
renderOrClause(req.user.vpaths),
|
||||
{ 'rating': { '$gt': 0 } }
|
||||
]
|
||||
}).simplesort('rating', true).data();
|
||||
|
||||
const songs = [];
|
||||
for (const row of results) {
|
||||
songs.push(renderMetadataObj(row));
|
||||
}
|
||||
res.json(songs);
|
||||
} catch (err) {
|
||||
winston.error('Db Error', { stack: err });
|
||||
res.status(500).json({ error: typeof err === 'string' ? err : 'Unknown Error' });
|
||||
}
|
||||
});
|
||||
|
||||
mstream.post('/api/v1/db/rate-song', async (req, res) => {
|
||||
try {
|
||||
const schema = Joi.object({
|
||||
filepath: Joi.string().required(),
|
||||
rating: Joi.number().integer().min(0).max(10)
|
||||
});
|
||||
await schema.validateAsync(req.body);
|
||||
}catch (err) {
|
||||
return res.status(500).json({ error: 'Validation Error' });
|
||||
}
|
||||
|
||||
try{
|
||||
const pathInfo = vpath.getVPathInfo(req.body.filepath);
|
||||
if (!pathInfo) { return res.status(500).json({ error: 'Could not find file' }); }
|
||||
if (!db.getUserMetadataCollection() || !db.getFileDbName()) { throw 'No DB' }
|
||||
|
||||
const result = db.getFileCollection().findOne({ '$and':[{ 'filepath': pathInfo.relativePath}, { 'vpath': pathInfo.vpath }] });
|
||||
if (!result) { throw 'File Not Found' }
|
||||
|
||||
const result2 = db.getUserMetadataCollection().findOne({ '$and':[{ 'hash': result.hash}, { 'user': req.user.username }] });
|
||||
if (!result2) {
|
||||
db.getUserMetadataCollection().insert({
|
||||
user: req.user.username,
|
||||
hash: result.hash,
|
||||
rating: req.body.rating
|
||||
});
|
||||
} else {
|
||||
result2.rating = req.body.rating;
|
||||
db.getUserMetadataCollection().update(result2);
|
||||
}
|
||||
|
||||
res.json({});
|
||||
db.saveUserDB();
|
||||
}catch (err) {
|
||||
winston.error('Db Error', { stack: err });
|
||||
res.status(500).json({ error: typeof err === 'string' ? err : 'Unknown Error' });
|
||||
}
|
||||
});
|
||||
|
||||
mstream.post('/api/v1/db/recent/added', async (req, res) => {
|
||||
try {
|
||||
const schema = Joi.object({ limit: Joi.number().integer().min(1).required() });
|
||||
await schema.validateAsync(req.body);
|
||||
}catch (err) {
|
||||
return res.status(500).json({ error: 'Validation Error' });
|
||||
}
|
||||
|
||||
try {
|
||||
if (!db.getFileCollection()) { throw 'DB Not Ready'; }
|
||||
|
||||
const leftFun = (leftData) => {
|
||||
return leftData.hash + '-' + req.user.username;
|
||||
};
|
||||
|
||||
const results = db.getFileCollection().chain().find({
|
||||
'$and': [
|
||||
renderOrClause(req.user.vpaths),
|
||||
{ 'ts': { '$gt': 0 } }
|
||||
]
|
||||
}).simplesort('ts', true).limit(req.body.limit).eqJoin(db.getUserMetadataCollection().chain(), leftFun, rightFunDefault, mapFunDefault).data();
|
||||
|
||||
const songs = [];
|
||||
for (const row of results) {
|
||||
songs.push(renderMetadataObj(row));
|
||||
}
|
||||
|
||||
res.json(songs);
|
||||
}catch (err) {
|
||||
winston.error('Db Error', { stack: err });
|
||||
res.status(500).json({ error: typeof err === 'string' ? err : 'Unknown Error' });
|
||||
}
|
||||
});
|
||||
|
||||
mstream.post('/api/v1/db/random-songs', (req, res) => {
|
||||
try {
|
||||
if (!db.getFileDbName()) { throw 'No DB'; };
|
||||
|
||||
// Ignore list
|
||||
let ignoreList = [];
|
||||
if (req.body.ignoreList && Array.isArray(req.body.ignoreList)) {
|
||||
ignoreList = req.body.ignoreList;
|
||||
}
|
||||
|
||||
let ignorePercentage = .5;
|
||||
if (req.body.ignorePercentage && typeof req.body.ignorePercentage === 'number' && req.body.ignorePercentage < 1 && !req.body.ignorePercentage < 0) {
|
||||
ignorePercentage = req.body.ignorePercentage;
|
||||
}
|
||||
|
||||
let orClause = { '$or': [] };
|
||||
for (let vpath of req.user.vpaths) {
|
||||
if (req.body.ignoreVPaths && typeof req.body.ignoreVPaths === 'object' && req.body.ignoreVPaths[vpath] === true) {
|
||||
continue;
|
||||
}
|
||||
orClause['$or'].push({ 'vpath': { '$eq': vpath } });
|
||||
}
|
||||
|
||||
const leftFun = (leftData) => {
|
||||
return leftData.hash + '-' + req.user.username;
|
||||
};
|
||||
|
||||
const results = db.getFileCollection().chain().eqJoin(db.getUserMetadataCollection().chain(), leftFun, rightFunDefault, mapFunDefault).find(orClause).data();
|
||||
|
||||
const count = results.length;
|
||||
if (count === 0) { throw 'No songs that match criteria'; }
|
||||
while (ignoreList.length > count * ignorePercentage) {
|
||||
ignoreList.shift();
|
||||
}
|
||||
|
||||
const returnThis = { songs: [], ignoreList: [] };
|
||||
let randomNumber = Math.floor(Math.random() * count);
|
||||
while (ignoreList.indexOf(randomNumber) > -1) {
|
||||
randomNumber = Math.floor(Math.random() * count);
|
||||
}
|
||||
|
||||
const randomSong = results[randomNumber];
|
||||
returnThis.songs.push(renderMetadataObj(randomSong));
|
||||
ignoreList.push(randomNumber);
|
||||
returnThis.ignoreList = ignoreList;
|
||||
|
||||
res.json(returnThis);
|
||||
}catch (err) {
|
||||
winston.error('Db Error', { stack: err });
|
||||
res.status(500).json({ error: typeof err === 'string' ? err : 'Unknown Error' });
|
||||
}
|
||||
});
|
||||
|
||||
mstream.post('/api/v1/playlist/load', (req, res) => {
|
||||
try {
|
||||
if (!db.getPlaylistCollection()){ throw 'No DB'; }
|
||||
if (!db.getFileDbName()){ throw 'No DB'; }
|
||||
|
||||
const playlist = String(req.body.playlistname);
|
||||
const returnThis = [];
|
||||
|
||||
const results = db.getPlaylistCollection().find({
|
||||
'$and': [{
|
||||
'user': { '$eq': req.user.username }
|
||||
}, {
|
||||
'name': { '$eq': playlist }
|
||||
}]
|
||||
});
|
||||
|
||||
for (const row of results) {
|
||||
// Look up metadata
|
||||
const pathInfo = vpath.getVPathInfo(row.filepath, req.user);
|
||||
if (!pathInfo) { continue; }
|
||||
|
||||
|
||||
const leftFun = (leftData) => {
|
||||
return leftData.hash + '-' + req.user.username;
|
||||
};
|
||||
|
||||
const result = db.getFileCollection().chain().find({ '$and': [{'filepath': pathInfo.relativePath}, { 'vpath': pathInfo.vpath }] }, true)
|
||||
.eqJoin(db.getUserMetadataCollection().chain(), leftFun, rightFunDefault, mapFunDefault).data();
|
||||
|
||||
let metadata = {};
|
||||
if (result && result[0]) {
|
||||
metadata = {
|
||||
"artist": result[0].artist ? result[0].artist : null,
|
||||
"hash": result[0].hash ? result[0].hash : null,
|
||||
"album": result[0].album ? result[0].album : null,
|
||||
"track": result[0].track ? result[0].track : null,
|
||||
"title": result[0].title ? result[0].title : null,
|
||||
"year": result[0].year ? result[0].year : null,
|
||||
"album-art": result[0].aaFile ? result[0].aaFile : null,
|
||||
"rating": result[0].rating ? result[0].rating : null,
|
||||
"replaygain-track-db": result[0]['replaygain-track-db'] ? result[0]['replaygain-track-db'] : null
|
||||
};
|
||||
}
|
||||
|
||||
returnThis.push({ lokiId: row['$loki'], filepath: row.filepath, metadata: metadata });
|
||||
}
|
||||
|
||||
res.json(returnThis);
|
||||
}catch (err) {
|
||||
winston.error('Db Error', { stack: err });
|
||||
res.status(500).json({ error: typeof err === 'string' ? err : 'Unknown Error' });
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
155
src/api/playlist.js
Normal file
155
src/api/playlist.js
Normal file
@ -0,0 +1,155 @@
|
||||
const winston = require('winston');
|
||||
const Joi = require('joi');
|
||||
const config = require('../state/config');
|
||||
const db = require('../db/manager');
|
||||
|
||||
exports.setup = (mstream) => {
|
||||
// TODO: This is a legacy endpoint that should be improved
|
||||
mstream.get('/api/v1/ping', (req, res) => {
|
||||
let transcode = false;
|
||||
if (config.program.transcode && config.program.transcode.enabled) {
|
||||
transcode = {
|
||||
defaultCodec: config.program.transcode.defaultCodec,
|
||||
defaultBitrate: config.program.transcode.defaultBitrate,
|
||||
}
|
||||
}
|
||||
|
||||
res.json({
|
||||
vpaths: req.user.vpaths,
|
||||
playlists: getPlaylists(req.user.username),
|
||||
federationId: null,
|
||||
transcode
|
||||
});
|
||||
});
|
||||
|
||||
mstream.post('/api/v1/playlist/delete', async (req, res) => {
|
||||
try {
|
||||
const schema = Joi.object({ playlistname: Joi.string().required() });
|
||||
await schema.validateAsync(req.body);
|
||||
}catch (err) {
|
||||
return res.status(500).json({ error: 'Validation Error' });
|
||||
}
|
||||
|
||||
try {
|
||||
if (!db.getPlaylistCollection()) { throw 'DB Error'; }
|
||||
|
||||
db.getPlaylistCollection().findAndRemove({
|
||||
'$and': [
|
||||
{ 'user': { '$eq': req.user.username }},
|
||||
{ 'name': { '$eq': req.body.playlistname }}
|
||||
]
|
||||
});
|
||||
|
||||
res.json({});
|
||||
} catch (err) {
|
||||
winston.error('Db Error', { stack: err });
|
||||
res.status(500).json({ error: typeof err === 'string' ? err : 'Unknown Error' });
|
||||
}
|
||||
|
||||
userDataDb.saveDatabase(err => {
|
||||
if (err) { winston.error('Playlist Save Error', { stack: err }); }
|
||||
});
|
||||
});
|
||||
|
||||
mstream.post('/api/v1/playlist/add-song', async (req, res) => {
|
||||
try {
|
||||
const schema = Joi.object({
|
||||
song: Joi.string().required(),
|
||||
playlist: Joi.string().required()
|
||||
});
|
||||
await schema.validateAsync(req.body);
|
||||
}catch (err) {
|
||||
return res.status(500).json({ error: 'Validation Error' });
|
||||
}
|
||||
|
||||
try {
|
||||
if (!db.getPlaylistCollection()) { throw 'No DB'; }
|
||||
db.getPlaylistCollection().insert({
|
||||
name: req.body.playlist,
|
||||
filepath: req.body.song,
|
||||
user: req.user.username
|
||||
});
|
||||
res.json({ });
|
||||
|
||||
db.saveUserDB();
|
||||
}catch (err) {
|
||||
winston.error('Db Error', { stack: err });
|
||||
res.status(500).json({ error: typeof err === 'string' ? err : 'Unknown Error' });
|
||||
}
|
||||
});
|
||||
|
||||
mstream.post('/api/v1/playlist/remove-song', async (req, res) => {
|
||||
try {
|
||||
const schema = Joi.object({ lokiid: Joi.number().integer().required() });
|
||||
await schema.validateAsync(req.body);
|
||||
}catch (err) {
|
||||
return res.status(500).json({ error: 'Validation Error' });
|
||||
}
|
||||
|
||||
try {
|
||||
if (!db.getPlaylistCollection()) { throw 'No DB'; }
|
||||
db.getPlaylistCollection().findAndRemove({ '$loki': req.body.lokiid });
|
||||
res.json({});
|
||||
db.saveUserDB();
|
||||
}catch (err) {
|
||||
winston.error('Db Error', { stack: err });
|
||||
res.status(500).json({ error: typeof err === 'string' ? err : 'Unknown Error' });
|
||||
}
|
||||
});
|
||||
|
||||
mstream.post('/api/v1/playlist/save', async (req, res) => {
|
||||
try {
|
||||
const schema = Joi.object({
|
||||
title: Joi.string().required(),
|
||||
songs: Joi.array().items(Joi.string())
|
||||
});
|
||||
await schema.validateAsync(req.body);
|
||||
|
||||
}catch (err) {
|
||||
return res.status(500).json({ error: 'Validation Error' });
|
||||
}
|
||||
|
||||
try {
|
||||
// Delete existing playlist
|
||||
db.getPlaylistCollection().findAndRemove({
|
||||
'$and': [{
|
||||
'user': { '$eq': req.user.username }
|
||||
}, {
|
||||
'name': { '$eq': req.body.title }
|
||||
}]
|
||||
});
|
||||
|
||||
for (const song of req.body.songs) {
|
||||
db.getPlaylistCollection().insert({
|
||||
name: req.body.title,
|
||||
filepath: song,
|
||||
user: req.user.username
|
||||
});
|
||||
}
|
||||
|
||||
res.json({});
|
||||
db.saveUserDB();
|
||||
}catch (err) {
|
||||
winston.error('Db Error', { stack: err });
|
||||
res.status(500).json({ error: typeof err === 'string' ? err : 'Unknown Error' });
|
||||
}
|
||||
});
|
||||
|
||||
mstream.get('/api/v1/playlist/getall', (req, res) => {
|
||||
res.json(getPlaylists(req.user.username));
|
||||
});
|
||||
|
||||
function getPlaylists(username) {
|
||||
const playlists = [];
|
||||
|
||||
const results = db.getPlaylistCollection().find({ 'user': { '$eq': username } });
|
||||
const store = {};
|
||||
for (let row of results) {
|
||||
if (!store[row.name]) {
|
||||
playlists.push({ name: row.name });
|
||||
store[row.name] = true;
|
||||
}
|
||||
}
|
||||
return playlists;
|
||||
}
|
||||
}
|
||||
84
src/db/manager.js
Normal file
84
src/db/manager.js
Normal file
@ -0,0 +1,84 @@
|
||||
const path = require('path');
|
||||
const loki = require('lokijs');
|
||||
const winston = require('winston');
|
||||
const config = require('../state/config');
|
||||
|
||||
const userDataDbName = 'user-data.loki-v1.db';
|
||||
const filesDbName = 'files.loki-v2.db';
|
||||
|
||||
// Loki Collections
|
||||
let filesDB;
|
||||
let userDataDb;
|
||||
|
||||
let fileCollection;
|
||||
let playlistCollection;
|
||||
let userMetadataCollection;
|
||||
|
||||
exports.saveUserDB = () => {
|
||||
userDataDb.saveDatabase(err => {
|
||||
if (err) { winston.error('User DB Save Error', { stack: err }); }
|
||||
});
|
||||
}
|
||||
|
||||
exports.saveFilesDB = () => {
|
||||
filesDB.saveDatabase(err => {
|
||||
if (err) { winston.error('Files DB Save Error', { stack: err }); }
|
||||
});
|
||||
}
|
||||
|
||||
exports.getFileDbName = () => {
|
||||
return filesDbName;
|
||||
}
|
||||
|
||||
exports.getFileCollection = () => {
|
||||
return fileCollection;
|
||||
}
|
||||
|
||||
exports.getPlaylistCollection = () => {
|
||||
return playlistCollection;
|
||||
}
|
||||
|
||||
exports.getUserMetadataCollection = () => {
|
||||
return userMetadataCollection;
|
||||
}
|
||||
|
||||
function loadDB() {
|
||||
filesDB.loadDatabase({}, err => {
|
||||
if (err) {
|
||||
winston.error('Files DB Load Error', { stack: err });
|
||||
return;
|
||||
}
|
||||
|
||||
// Get files collection
|
||||
fileCollection = filesDB.getCollection('files');
|
||||
});
|
||||
|
||||
userDataDb.loadDatabase({}, err => {
|
||||
if (err) {
|
||||
winston.error('Playlists DB Load Error', { stack: err });
|
||||
return;
|
||||
}
|
||||
|
||||
// Initialize playlists collection
|
||||
playlistCollection = userDataDb.getCollection('playlists');
|
||||
if (!playlistCollection) {
|
||||
playlistCollection = userDataDb.addCollection("playlists");
|
||||
}
|
||||
|
||||
// Initialize user metadata collection (for song ratings, playback stats, etc)
|
||||
userMetadataCollection = userDataDb.getCollection('user-metadata');
|
||||
if (!userMetadataCollection) {
|
||||
userMetadataCollection = userDataDb.addCollection("user-metadata");
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
exports.loadDB = () => {
|
||||
loadDB();
|
||||
}
|
||||
|
||||
exports.initLoki = () => {
|
||||
filesDB = new loki(path.join(config.program.storage.dbDirectory, filesDbName));
|
||||
userDataDb = new loki(path.join(config.program.storage.dbDirectory, userDataDbName));
|
||||
loadDB();
|
||||
}
|
||||
@ -3,7 +3,7 @@ const path = require('path');
|
||||
const winston = require('winston');
|
||||
const nanoid = require('nanoid');
|
||||
const config = require('../state/config');
|
||||
const mstreamReadPublicDB = require('../../modules/db-read/database-public-loki');
|
||||
const db = require('../db/manager');
|
||||
|
||||
const taskQueue = [];
|
||||
const runningTasks = new Set();
|
||||
@ -44,7 +44,7 @@ function runScan(vpath) {
|
||||
const jsonLoad = {
|
||||
directory: config.program.folders[vpath].root,
|
||||
vpath: vpath,
|
||||
dbPath: path.join(config.program.storage.dbDirectory, mstreamReadPublicDB.getFileDbName()),
|
||||
dbPath: path.join(config.program.storage.dbDirectory, db.getFileDbName()),
|
||||
albumArtDirectory: config.program.storage.albumArtDirectory,
|
||||
skipImg: config.program.scanOptions.skipImg,
|
||||
saveInterval: config.program.scanOptions.saveInterval,
|
||||
@ -64,7 +64,7 @@ function runScan(vpath) {
|
||||
// TODO: Ideally, if there are no changes to the DB we should not be reloading it. Ideally...
|
||||
if (parsedMsg.loadDB === true) {
|
||||
parseFlag = true;
|
||||
mstreamReadPublicDB.loadDB();
|
||||
db.loadDB();
|
||||
}
|
||||
} catch (error) {
|
||||
winston.info(`File scan message: ${data}`);
|
||||
@ -77,7 +77,7 @@ function runScan(vpath) {
|
||||
|
||||
forkedScan.on('close', (code) => {
|
||||
if(parseFlag === false) {
|
||||
mstreamReadPublicDB.loadDB();
|
||||
db.loadDB();
|
||||
}
|
||||
runningTasks.delete(forkedScan);
|
||||
vpathLimiter.delete(vpath);
|
||||
|
||||
@ -46,7 +46,7 @@ var MSTREAMAPI = (function () {
|
||||
}
|
||||
|
||||
mstreamModule.loadFileplaylist = function (path, callback) {
|
||||
makePOSTRequest('/fileplaylist/load', { path }, callback);
|
||||
makePOSTRequest('/api/v1/file-explorer/m3u', { path }, callback);
|
||||
}
|
||||
|
||||
mstreamModule.recursiveScan = function (directory, callback) {
|
||||
@ -54,47 +54,47 @@ var MSTREAMAPI = (function () {
|
||||
}
|
||||
|
||||
mstreamModule.savePlaylist = function (title, songs, callback) {
|
||||
makePOSTRequest('/playlist/save', { title: title, songs: songs }, callback);
|
||||
makePOSTRequest('/api/v1/playlist/save', { title: title, songs: songs }, callback);
|
||||
}
|
||||
|
||||
mstreamModule.deletePlaylist = function (playlistname, callback) {
|
||||
makePOSTRequest('/playlist/delete', { playlistname: playlistname }, callback);
|
||||
makePOSTRequest('/api/v1/playlist/delete', { playlistname: playlistname }, callback);
|
||||
}
|
||||
|
||||
mstreamModule.removePlaylistSong = function (lokiId, callback) {
|
||||
makePOSTRequest('/playlist/remove-song', { lokiid: lokiId }, callback);
|
||||
makePOSTRequest('/api/v1/playlist/remove-song', { lokiid: lokiId }, callback);
|
||||
}
|
||||
|
||||
mstreamModule.loadPlaylist = function (playlistname, callback) {
|
||||
makePOSTRequest('/playlist/load', { playlistname: playlistname }, callback);
|
||||
makePOSTRequest('/api/v1/playlist/load', { playlistname: playlistname }, callback);
|
||||
}
|
||||
|
||||
mstreamModule.getAllPlaylists = function (callback) {
|
||||
makeGETRequest('/playlist/getall', false, callback);
|
||||
makeGETRequest('/api/v1/playlist/getall', false, callback);
|
||||
}
|
||||
|
||||
mstreamModule.addToPlaylist = function (playlist, song, callback) {
|
||||
makePOSTRequest('/playlist/add-song', { playlist: playlist, song: song }, callback);
|
||||
makePOSTRequest('/api/v1/playlist/add-song', { playlist: playlist, song: song }, callback);
|
||||
}
|
||||
|
||||
mstreamModule.search = function (postObject, callback) {
|
||||
makePOSTRequest('/db/search', postObject, callback);
|
||||
makePOSTRequest('/api/v1/db/search', postObject, callback);
|
||||
}
|
||||
|
||||
mstreamModule.artists = function (callback) {
|
||||
makeGETRequest('/db/artists', false, callback);
|
||||
makeGETRequest('/api/v1/db/artists', false, callback);
|
||||
}
|
||||
|
||||
mstreamModule.albums = function (callback) {
|
||||
makeGETRequest('/db/albums', false, callback);
|
||||
makeGETRequest('/api/v1/db/albums', false, callback);
|
||||
}
|
||||
|
||||
mstreamModule.artistAlbums = function (artist, callback) {
|
||||
makePOSTRequest("/db/artists-albums", { artist: artist }, callback);
|
||||
makePOSTRequest("/api/v1/db/artists-albums", { artist: artist }, callback);
|
||||
}
|
||||
|
||||
mstreamModule.albumSongs = function (album, artist, callback) {
|
||||
makePOSTRequest("/db/album-songs", { album: album, artist: artist }, callback);
|
||||
makePOSTRequest("/api/v1/db/album-songs", { album: album, artist: artist }, callback);
|
||||
}
|
||||
|
||||
mstreamModule.dbStatus = function (callback) {
|
||||
@ -106,23 +106,23 @@ var MSTREAMAPI = (function () {
|
||||
}
|
||||
|
||||
mstreamModule.rateSong = function (filepath, rating, callback) {
|
||||
makePOSTRequest("/db/rate-song", { filepath: filepath, rating: rating }, callback);
|
||||
makePOSTRequest("/api/v1/db/rate-song", { filepath: filepath, rating: rating }, callback);
|
||||
}
|
||||
|
||||
mstreamModule.getRated = function (callback) {
|
||||
makeGETRequest("/db/get-rated", false, callback);
|
||||
makeGETRequest("/api/v1/db/rated", false, callback);
|
||||
}
|
||||
|
||||
mstreamModule.getRecentlyAdded = function (limit, callback) {
|
||||
makePOSTRequest("/db/recent/added", { limit: limit }, callback);
|
||||
makePOSTRequest("/api/v1/db/recent/added", { limit: limit }, callback);
|
||||
}
|
||||
|
||||
mstreamModule.lookupMetadata = function (filepath, callback) {
|
||||
makePOSTRequest("/db/metadata", { filepath: filepath }, callback);
|
||||
makePOSTRequest("/api/v1/db/metadata", { filepath: filepath }, callback);
|
||||
}
|
||||
|
||||
mstreamModule.getRandomSong = function (postObject, callback) {
|
||||
makePOSTRequest("/db/random-songs", postObject, callback);
|
||||
makePOSTRequest("/api/v1/db/random-songs", postObject, callback);
|
||||
}
|
||||
|
||||
mstreamModule.generateFederationInvite = function (postObject, callback) {
|
||||
@ -153,7 +153,7 @@ var MSTREAMAPI = (function () {
|
||||
}
|
||||
|
||||
mstreamModule.ping = function (callback) {
|
||||
makeGETRequest("/ping", false, callback);
|
||||
makeGETRequest("/api/v1/ping", false, callback);
|
||||
}
|
||||
|
||||
|
||||
|
||||
@ -835,10 +835,11 @@ $(document).ready(function () {
|
||||
}
|
||||
|
||||
MSTREAMAPI.savePlaylist(title, songs, function (response, error) {
|
||||
$('#save_playlist').prop("disabled", false);
|
||||
|
||||
if (error !== false) {
|
||||
return boilerplateFailure(response, error);
|
||||
}
|
||||
$('#save_playlist').prop("disabled", false);
|
||||
$('#savePlaylist').iziModal('close');
|
||||
iziToast.success({
|
||||
title: 'Playlist Saved',
|
||||
|
||||
Loading…
Reference in New Issue
Block a user