diff --git a/modules/db-read/database-public-loki.js b/modules/db-read/database-public-loki.js deleted file mode 100644 index 156f15c..0000000 --- a/modules/db-read/database-public-loki.js +++ /dev/null @@ -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(); -} diff --git a/mstream.js b/mstream.js index ed84932..10a5f76 100755 --- a/mstream.js +++ b/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, () => { diff --git a/src/api/db.js b/src/api/db.js index b328108..9ae4988 100644 --- a/src/api/db.js +++ b/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' }); + } + }); } diff --git a/src/api/playlist.js b/src/api/playlist.js new file mode 100644 index 0000000..03759a1 --- /dev/null +++ b/src/api/playlist.js @@ -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; + } +} \ No newline at end of file diff --git a/src/db/manager.js b/src/db/manager.js new file mode 100644 index 0000000..bb7867d --- /dev/null +++ b/src/db/manager.js @@ -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(); +} \ No newline at end of file diff --git a/src/db/task-queue.js b/src/db/task-queue.js index d886afe..bb56abe 100644 --- a/src/db/task-queue.js +++ b/src/db/task-queue.js @@ -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); diff --git a/webapp/assets/js/api2.js b/webapp/assets/js/api2.js index 4654cd9..9f37e0e 100644 --- a/webapp/assets/js/api2.js +++ b/webapp/assets/js/api2.js @@ -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); } diff --git a/webapp/assets/js/mstream.js b/webapp/assets/js/mstream.js index 93732b5..4646fd3 100644 --- a/webapp/assets/js/mstream.js +++ b/webapp/assets/js/mstream.js @@ -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',