diff --git a/modules/file-explorer.js b/modules/file-explorer.js index ec065c7..ac93b33 100644 --- a/modules/file-explorer.js +++ b/modules/file-explorer.js @@ -4,10 +4,43 @@ const fe = require("path"); const archiver = require('archiver'); const winston = require('winston'); const mkdirp = require('make-dir'); +const m3uread = require('m3u8-reader'); -const masterFileTypesArray = ["mp3", "flac", "wav", "ogg", "aac", "m4a", "opus"]; +const masterFileTypesArray = ["mp3", "flac", "wav", "ogg", "aac", "m4a", "opus", "m3u"]; exports.setup = function(mstream, program) { + + function getPathInfoOrThrow(req, pathString) { + const pathInfo = program.getVPathInfo(pathString); + if (pathInfo == false) { + throw {code: 500, json: { error: "Could not find file" }}; + } + if (!req.user.vpaths.includes(pathInfo.vpath)) { + throw {code: 500, json: { error: "Access Denied" }}; + } + return pathInfo; + } + + function getPathArray(pathString) { + return pathString.split(fe.sep).filter(Boolean); + } + + function getFileType(pathString) { + return fe.extname(pathString).substr(1); + } + + function readPlaylistSongs(pathString) { + return m3uread(fs.readFileSync(pathString)) + .filter(function (item) { return typeof item === "string" }) + .map(function (item) { return item.replace(/\\/g, fe.sep) }) // m3u path separated by \ + } + + function handleError(error, res) { + if (error.code && error.json) { + res.status(error.code).json(error.json); + } + } + mstream.post('/download-directory', (req, res) => { if (!req.body.directory) { return res.status(500).json({ error: 'Missing Params' }); @@ -33,7 +66,6 @@ exports.setup = function(mstream, program) { } const archive = archiver('zip'); - archive.on('error', function (err) { winston.error(`Download Error: ${err.message}`); res.status(500).json({ error: err.message }); @@ -48,6 +80,29 @@ exports.setup = function(mstream, program) { archive.finalize(); }); + mstream.post('/fileplaylist/download', (req, res, next) => { + try { + const playlistPathInfo = getPathInfoOrThrow(req, req.body.path); + const playlistParentDir = fe.dirname(req.body.path); + const songs = readPlaylistSongs(playlistPathInfo.fullPath); + const archive = archiver('zip'); + archive.on('error', function (err) { + winston.error(`Download Error: ${err.message}`); + res.status(500).json({ error: err.message }); + }); + res.attachment(fe.basename(req.body.path) + ".zip"); + archive.pipe(res); + for (let song of songs) { + const songPath = fe.join(playlistParentDir, song); + const songPathInfo = getPathInfoOrThrow(req, songPath); + archive.file(songPathInfo.fullPath, { name: fe.basename(song) }) + } + archive.finalize(); + } catch (error) { + handleError(error, res); + } + }); + mstream.post("/upload", function (req, res) { if (program.noUpload) { return res.status(500).json({ error: 'Uploading Disabled' }); @@ -83,7 +138,33 @@ exports.setup = function(mstream, program) { return req.pipe(busboy); }); - + + mstream.post("/fileplaylist/load", function(req, res, next) { + try { + const playlistPathInfo = getPathInfoOrThrow(req, req.body.path); + const playlistParentDir = fe.dirname(req.body.path); + const songs = readPlaylistSongs(playlistPathInfo.fullPath); + res.json({ + contents: songs.map(function (song) { + return {type: getFileType(song), name: fe.basename(song), path: fe.join(playlistParentDir, song)} + }) + }) + } catch (error) { + handleError(error, res); + } + }) + + mstream.post("/fileplaylist/loadpaths", function(req, res, next) { + try { + const playlistPathInfo = getPathInfoOrThrow(req, req.body.path); + const playlistParentDir = fe.dirname(req.body.path); + const songs = readPlaylistSongs(playlistPathInfo.fullPath); + res.json(songs.map(function (song) { return fe.join(playlistParentDir, song); })); + } catch (error) { + handleError(error, res); + } + }) + // parse directories mstream.post("/dirparser", function(req, res) { const directories = []; @@ -119,7 +200,6 @@ exports.setup = function(mstream, program) { return; } - // Will only show these files. Prevents people from snooping around var fileTypesArray; if (req.body.filetypes) { fileTypesArray = req.body.filetypes; @@ -222,7 +302,7 @@ exports.setup = function(mstream, program) { } else { const extension = getFileType(file).toLowerCase(); if (fileTypesArray.indexOf(extension) > -1 && masterFileTypesArray.indexOf(extension) > -1) { - filelist.push(fe.join(pathInfo.vpath, fe.join(relativePath, file))); + filelist.push(fe.join(pathInfo.vpath, fe.join(relativePath, file))); } } }); @@ -231,8 +311,4 @@ exports.setup = function(mstream, program) { res.json(recursiveTrot(pathInfo.fullPath, [], pathInfo.relativePath)); }); - - function getFileType(filename) { - return filename.split(".").pop(); - } }; diff --git a/package.json b/package.json index a08dadc..128ab2b 100644 --- a/package.json +++ b/package.json @@ -89,6 +89,7 @@ "inquirer-select-directory": "^1.2.0", "jsonwebtoken": "^8.5.1", "lokijs": "^1.5.7", + "m3u8-reader": "^1.1.0", "make-dir": "^3.0.0", "mime-types": "^2.1.24", "music-metadata": "^4.8.2", diff --git a/public/css/master.css b/public/css/master.css index 2d2e063..33303fe 100755 --- a/public/css/master.css +++ b/public/css/master.css @@ -163,7 +163,7 @@ div#jp_container_N { box-shadow: 0 0 10px #D6D6D6; padding: 10px; } -.filez, .dirz, .back, .artistz, .albumz, .playlist_row_container, .playlist-item, #playlist_container ul li { +.filez, .dirz, .fileplaylistz, .back, .artistz, .albumz, .playlist_row_container, .playlist-item, #playlist_container ul li { font-family: Arial, Helvetica Neue, Helvetica, sans-serif; cursor: pointer; width: 100%; @@ -182,6 +182,13 @@ div#jp_container_N { overflow: visible; padding: 10px; } + +.fileplaylistz { + float: left; + position: relative; + overflow: visible; + padding: 10px; } + .playlistz{ display: block; padding: 10px; @@ -248,7 +255,7 @@ div#jp_container_N { clear: both; } -.dirz:hover, .albumz:hover, .artistz:hover, .playlistz:hover { +.dirz:hover, .albumz:hover, .artistz:hover, .playlistz:hover, .fileplaylistz:hover { background-color: rgba(230, 154, 23, 0.15); } .back:hover { @@ -560,6 +567,11 @@ h3 { margin-right: 5px; } +.fileplaylist-image{ + height: 21px; + margin-right: 5px; +} + .item-text{ vertical-align: middle; } @@ -705,7 +717,7 @@ h3 { display: none; } - .tab-bar-section { + .tab-bar-section { margin: 0; padding: 0 !important; } @@ -744,7 +756,7 @@ ul.left-nav-menu li { color: #BBB; cursor: pointer; font-weight: 800; - + font-family: "Jura", sans-serif; } @@ -836,4 +848,4 @@ ul.left-nav-menu li.selected svg { #federation-invite-selection-panel { overflow-y: scroll; max-height: 300px; -} \ No newline at end of file +} diff --git a/public/css/mstream-player.css b/public/css/mstream-player.css index 39c65b1..81c0e16 100644 --- a/public/css/mstream-player.css +++ b/public/css/mstream-player.css @@ -115,11 +115,11 @@ top: 0px; } -.songDropdown:hover, .downloadPlaylistSong:hover, .recursiveAddDir:hover { +.songDropdown:hover, .downloadPlaylistSong:hover, .recursiveAddDir:hover, .addFileplaylist:hover { background-color: #9E9E9E; } -.songDropdown, .downloadPlaylistSong, .recursiveAddDir{ +.songDropdown, .downloadPlaylistSong, .recursiveAddDir, .addFileplaylist { height: 14px; background-color: #B5B5B5; float: right; @@ -131,20 +131,20 @@ cursor: pointer; } -.downloadPlaylistSong, .recursiveAddDir { +.downloadPlaylistSong, .recursiveAddDir, .addFileplaylist { min-width: 28px; border-right: 1px solid #9E9E9E; } -.songDropdown { +.songDropdown { min-width: 38px !important; } -.popperMenu:hover, .downloadDir:hover { +.popperMenu:hover, .downloadDir:hover, .downloadFileplaylist:hover { background-color: #9E9E9E; } -.popperMenu, .downloadDir { +.popperMenu, .downloadDir, .downloadFileplaylist { min-width: 28px !important; height: 14px; background-color: #B5B5B5; @@ -159,8 +159,6 @@ cursor: pointer; } - - #pop-d { min-width: 50px; background-color: #F5F5F5; @@ -237,7 +235,7 @@ } .drag-handle img{ - width: 20px; + width: 20px; } .titlebar{ @@ -380,13 +378,13 @@ fill: rgb(255, 255, 255); } @media (max-width: 450px) { .volume-bar { - display: none;} + display: none;} .remote-button { display: none;} } @media (max-device-width: 451px) { .volume-bar { - display: none;} + display: none;} .remote-button { display: none;} } diff --git a/public/js/api2.js b/public/js/api2.js index 88951d2..cd53d0f 100644 --- a/public/js/api2.js +++ b/public/js/api2.js @@ -45,6 +45,14 @@ var MSTREAMAPI = (function () { makePOSTRequest('/dirparser', { dir: directory }, callback); } + mstreamModule.loadFileplaylist = function (path, callback) { + makePOSTRequest('/fileplaylist/load', { path }, callback); + } + + mstreamModule.loadFileplaylistPaths = function (path, callback) { + makePOSTRequest('/fileplaylist/loadpaths', { path }, callback); + } + mstreamModule.recursiveScan = function (directory, filetypes, callback) { makePOSTRequest('/files/recursive-scan', { dir: directory }, callback); } diff --git a/public/js/mstream.js b/public/js/mstream.js index c95c031..6bc7218 100755 --- a/public/js/mstream.js +++ b/public/js/mstream.js @@ -15,6 +15,14 @@ function escapeHtml (string) { }); } +function createFileplaylistHtml(dataDirectory) { + return '
' + dataDirectory + '
'; +} + +function createMusicfileHtml(fileLocation, title, titleClass) { + return '
' + title + '
'; +} + $(document).ready(function () { new ClipboardJS('.fed-copy-button'); @@ -154,11 +162,7 @@ $(document).ready(function () { }); myDropzone.removeFile(file); } else { - var directoryString = ""; - for (var i = 0; i < fileExplorerArray.length; i++) { - directoryString += fileExplorerArray[i] + "/"; - } - file.directory = directoryString + file.fullPath.substring(0, file.fullPath.indexOf(file.name)); + file.directory = getFileExplorerPath(fileExplorerArray) + file.fullPath.substring(0, file.fullPath.indexOf(file.name)); } }); @@ -254,7 +258,7 @@ $(document).ready(function () { this.pending = true; var that = this; MSTREAMAPI.login($('#login-username').val(), $('#login-password').val(), function (response, error) { - that.pending = false; + that.pending = false; if (error !== false) { // Alert the user iziToast.error({ @@ -269,7 +273,7 @@ $(document).ready(function () { if (typeof(Storage) !== "undefined") { localStorage.setItem("token", response.token); } - + // Add the token the URL calls MSTREAMAPI.updateCurrentServer($('#login-username').val(), response.token, response.vpaths) @@ -362,7 +366,6 @@ $(document).ready(function () { ////////////////////////////// Global Variables // These vars track your position within the file explorer var fileExplorerArray = []; - var fileExplorerScrollPosition = []; // Stores an array of searchable objects var currentBrowsingList = []; @@ -392,7 +395,7 @@ $(document).ready(function () { if (err.responseJSON && err.responseJSON.error) { msg = err.responseJSON.error; } - + iziToast.error({ title: msg, position: 'topCenter', @@ -418,11 +421,9 @@ $(document).ready(function () { // Reset file explorer vars fileExplorerArray = []; - fileExplorerScrollPosition = []; if (MSTREAMAPI.currentServer.vpaths && MSTREAMAPI.currentServer.vpaths.length === 1) { fileExplorerArray.push(MSTREAMAPI.currentServer.vpaths[0]); - fileExplorerScrollPosition.push(0); } //send this directory to be parsed and displayed @@ -434,17 +435,29 @@ $(document).ready(function () { // when you click on a directory, go to that directory $("#filelist").on('click', 'div.dirz', function () { - //get the id of that class - var nextDir = $(this).data("directory"); - var newArray = []; - for (var i = 0; i < fileExplorerArray.length; i++) { - newArray.push(fileExplorerArray[i]); - } - newArray.push(nextDir); - + var newArray = fileExplorerArray.concat($(this).data("directory")); senddir(false, newArray); }); + // when you click on a playlist, go to that playlist + $("#filelist").on('click', 'div.fileplaylistz', function () { + var newArray = fileExplorerArray.concat($(this).data("directory")); + var directoryString = getFileExplorerPath(newArray); + + $('.directoryName').html('/' + directoryString); + $('#filelist').html('
'); + + MSTREAMAPI.loadFileplaylist(directoryString, function (response, error) { + if (error !== false) { + boilerplateFailure(response, error); + return; + } + + fileExplorerArray = newArray; + printdir(response); + }); + }); + // when you click the back directory $(".backButton").on('click', function () { // Handle file Explorer @@ -482,10 +495,7 @@ $(document).ready(function () { // send a new directory to be parsed. function senddir(scrollPosition, newArray) { // Construct the directory string - var directoryString = ""; - for (var i = 0; i < newArray.length; i++) { - directoryString += newArray[i] + "/"; - } + var directoryString = getFileExplorerPath(newArray); $('.directoryName').html('/' + directoryString); $('#filelist').html('
'); @@ -500,15 +510,6 @@ $(document).ready(function () { // Set any directory views // hand this data off to be printed on the page printdir(response); - // Set scroll postion - if (scrollPosition === false) { - var sp = $('#filelist').scrollTop(); - fileExplorerScrollPosition.push(sp); - $('#filelist').scrollTop(0); - } else if (scrollPosition === true) { - var sp = fileExplorerScrollPosition.pop(); - $('#filelist').scrollTop(sp); - } }); } @@ -523,13 +524,15 @@ $(document).ready(function () { //parse through the json array and make an array of corresponding divs var filelist = []; $.each(currentBrowsingList, function () { + const fileLocation = this.path || response.path + this.name; if (this.type == 'directory') { filelist.push('
' + this.name + '
'); } else { - if (this.artist != null || this.title != null) { - filelist.push('
' + this.artist + ' - ' + this.title + '
'); + if (this.type == 'm3u') { + filelist.push(createFileplaylistHtml(this.name)); } else { - filelist.push('
' + this.name + '
'); + const title = this.artist != null || this.title != null ? this.artist + ' - ' + this.title : this.name; + filelist.push(createMusicfileHtml(fileLocation, title, "item-text")); } } }); @@ -555,11 +558,6 @@ $(document).ready(function () { $('#search_folders').on('change keyup', function () { var searchVal = $(this).val(); - var path = ""; // Construct the directory string - for (var i = 0; i < fileExplorerArray.length; i++) { - path += fileExplorerArray[i] + "/"; - } - var filelist = []; // This causes an error in the playlist display $.each(currentBrowsingList, function () { @@ -590,12 +588,12 @@ $(document).ready(function () { } else { filelist.push('
' + this.metadata.artist + ' - ' + this.metadata.title + '
'); } + } else if (this.type == "m3u") { + filelist.push(createFileplaylistHtml(this.name)); } else { - if (this.artist != null || this.title != null) { - filelist.push('
' + this.artist + ' - ' + this.title + '
'); - } else { - filelist.push('
' + this.name + '
'); - } + const fileLocation = this.path || getFileExplorerPath(fileExplorerArray) + this.name; + const title = this.artist != null || this.title != null ? this.artist + ' - ' + this.title : this.name; + filelist.push(createMusicfileHtml(fileLocation, title, "title")); } } } @@ -620,27 +618,36 @@ $(document).ready(function () { } }); - $("#filelist").on('click', '.recursiveAddDir', function () { - var directoryString = "/"; - for (var i = 0; i < fileExplorerArray.length; i++) { - directoryString += fileExplorerArray[i] + "/"; - } + function getFileExplorerPath(explorerArray) { + return explorerArray.join("/") + "/"; + } - directoryString += $(this).data("directory"); + function getDirectoryString(component) { + return "/" + getFileExplorerPath(fileExplorerArray) + component.data("directory"); + } + + function addAllSongs(res) { + for (var i = 0; i < res.length; i++) { + MSTREAMAPI.addSongWizard(res[i], {}, true); + } + } + + $("#filelist").on('click', '.recursiveAddDir', function () { + var directoryString = getDirectoryString($(this)); MSTREAMAPI.recursiveScan(directoryString, false, function(res, err){ - for (var i = 0; i < res.length; i++) { - MSTREAMAPI.addSongWizard(res[i], {}, true); - } + addAllSongs(res); + }); + }); + + $("#filelist").on('click', '.addFileplaylist', function () { + var playlistPath = getDirectoryString($(this)); + MSTREAMAPI.loadFileplaylistPaths(playlistPath, function(res, err){ + addAllSongs(res); }); }); $("#filelist").on('click', '.downloadDir', function () { - var directoryString = "/"; - for (var i = 0; i < fileExplorerArray.length; i++) { - directoryString += fileExplorerArray[i] + "/"; - } - - directoryString += $(this).data("directory"); + var directoryString = getDirectoryString($(this)); // Use key if necessary $("#downform").attr("action", "/download-directory?token=" + MSTREAMAPI.currentServer.token); @@ -657,6 +664,24 @@ $(document).ready(function () { $('#downform').empty(); }); + $("#filelist").on('click', '.downloadFileplaylist', function () { + var playlistPath = getDirectoryString($(this)); + + // Use key if necessary + $("#downform").attr("action", "/fileplaylist/download?token=" + MSTREAMAPI.currentServer.token); + + $('').attr({ + type: 'hidden', + name: 'path', + value: playlistPath, + }).appendTo('#downform'); + + //submit form + $('#downform').submit(); + // clear the form + $('#downform').empty(); + }); + ////////////////////////////////////// Share playlists $('#share_playlist_form').on('submit', function (e) { e.preventDefault(); @@ -840,7 +865,7 @@ $(document).ready(function () { $('#filelist').html('
Server call failed
'); return boilerplateFailure(response, error); } - + // Add the playlist name to the modal $('#playlist_name').val(name); @@ -982,7 +1007,7 @@ $(document).ready(function () { resetPanel('Recently Added', 'scrollBoxHeight1'); $('#filelist').html('
'); $('.directoryName').html('Get last      songs'); - + redoRecentlyAdded(); } @@ -1166,7 +1191,7 @@ $(document).ready(function () { }); $('#filelist').html(albums); - // update lazy load plugin + // update lazy load plugin ll.update(); }); } @@ -1238,8 +1263,8 @@ $(document).ready(function () { newHtml += '

Default Bitrate: '+MSTREAMAPI.transcodeOptions.bitrate+'

\

Default Codec: '+MSTREAMAPI.transcodeOptions.codec+'

'; - - if (MSTREAMAPI.transcodeOptions.frontendEnabled) { + + if (MSTREAMAPI.transcodeOptions.frontendEnabled) { newHtml += '

'; } else { newHtml += '

'; @@ -1267,7 +1292,7 @@ $(document).ready(function () { // Convert playlist for (let i = 0; i < MSTREAMPLAYER.playlist.length; i++) { - MSTREAMPLAYER.playlist[i].url = MSTREAMPLAYER.playlist[i].url.replace(a, b); + MSTREAMPLAYER.playlist[i].url = MSTREAMPLAYER.playlist[i].url.replace(a, b); } // re-enable checkbox @@ -1286,7 +1311,7 @@ $(document).ready(function () { var newHtml = '\

Federation allows you easily sync folders between mStream servers or the backup tool. Federation is a one-way process. When you invite someone, they can only read the federated folders. Any changes they make will not be sent to your mStream server.

\

Federation is powered by Syncthing

'; - + if (federationId) { newHtml += '\

Federation ID: '+federationId+'

\ @@ -1296,7 +1321,7 @@ $(document).ready(function () { }else { newHtml += '

Federation is Disabled

'; } - + $('#filelist').html(newHtml); }); @@ -1311,7 +1336,7 @@ $(document).ready(function () { $('#filelist').on('click', '.trigger-generate-invite-public', function() { $('.invite-federation-id').addClass('super-hide'); $('.invite-federation-url').removeClass('super-hide'); - + $('#invite-public-url').prop('disabled', false); $('#invite-federation-id').prop('disabled', true); }); @@ -1331,7 +1356,7 @@ $(document).ready(function () { $('input[name="federate-this"]:checked').each(function () { vpaths.push($(this).val()); }); - + if(vpaths.length === 0) { iziToast.error({ title: 'Nothing to Federate', @@ -1413,7 +1438,7 @@ $(document).ready(function () { title: 'No directories selected', position: 'topCenter', timeout: 3500 - }); + }); } var sendThis = { diff --git a/public/js/winamp.js b/public/js/winamp.js index e3fd1fd..acaf4d4 100644 --- a/public/js/winamp.js +++ b/public/js/winamp.js @@ -166,7 +166,7 @@ $(document).ready(function () { this.pending = true; var that = this; MSTREAMAPI.login($('#login-username').val(), $('#login-password').val(), function (response, error) { - that.pending = false; + that.pending = false; if (error !== false) { // Alert the user iziToast.error({ @@ -181,10 +181,10 @@ $(document).ready(function () { if (typeof(Storage) !== "undefined") { localStorage.setItem("token", response.token); } - + // Reset Iframe $('#webamp-iframe').attr('src', '/public/webamp/webamp.html?token=' + response.token); - + // Add the token the URL calls MSTREAMAPI.updateCurrentServer($('#login-username').val(), response.token, response.vpaths) @@ -216,7 +216,7 @@ $(document).ready(function () { // set vPath MSTREAMAPI.currentServer.vpaths = response.vpaths; - // + // $('#webamp-iframe').attr('src', '/public/webamp/webamp.html?token=' + token); // Setup the file browser @@ -265,7 +265,6 @@ $(document).ready(function () { ////////////////////////////// Global Variables // These vars track your position within the file explorer var fileExplorerArray = []; - var fileExplorerScrollPosition = []; // Stores an array of searchable ojects var currentBrowsingList = []; @@ -311,11 +310,9 @@ $(document).ready(function () { // Reset file explorer vars fileExplorerArray = []; - fileExplorerScrollPosition = []; if (MSTREAMAPI.currentServer.vpaths && MSTREAMAPI.currentServer.vpaths.length === 1) { fileExplorerArray.push(MSTREAMAPI.currentServer.vpaths[0]); - fileExplorerScrollPosition.push(0); } //send this directory to be parsed and displayed @@ -393,15 +390,6 @@ $(document).ready(function () { // Set any directory views // hand this data off to be printed on the page printdir(response); - // Set scroll postion - if (scrollPosition === false) { - var sp = $('#filelist').scrollTop(); - fileExplorerScrollPosition.push(sp); - $('#filelist').scrollTop(0); - } else if (scrollPosition === true) { - var sp = fileExplorerScrollPosition.pop(); - $('#filelist').scrollTop(sp); - } }); } @@ -641,7 +629,7 @@ $(document).ready(function () { $('#filelist').html('
Server call failed
'); return boilerplateFailure(response, error); } - + // Add the playlist name to the modal $('#playlist_name').val(name); @@ -907,7 +895,7 @@ $(document).ready(function () { }); $('#filelist').html(albums); - // update linked list plugin + // update linked list plugin ll.update(); }); }