migrate DB endpoints

This commit is contained in:
IrosTheBeggar 2021-02-16 11:55:46 -05:00
parent 03f8c18175
commit c7477aa95b
8 changed files with 795 additions and 813 deletions

View File

@ -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();
}

View File

@ -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, () => {

View File

@ -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
View 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
View 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();
}

View File

@ -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);

View File

@ -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);
}

View File

@ -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',