diff --git a/src/schemas/channel/media/playlistMediaSchema.js b/src/schemas/channel/media/playlistMediaSchema.js index 1cef135..d6b58e2 100644 --- a/src/schemas/channel/media/playlistMediaSchema.js +++ b/src/schemas/channel/media/playlistMediaSchema.js @@ -46,6 +46,7 @@ playlistMediaProperties.pre('save', async function (next){ }); //methods +//Rehydrate to a full phat media object playlistMediaProperties.methods.rehydrate = function(){ //Return item as a full phat, standard media object return new media( @@ -58,4 +59,14 @@ playlistMediaProperties.methods.rehydrate = function(){ ); } +//Dehydrate to minified flat network-friendly object +playlistMediaProperties.methods.dehydrate = function(){ + return { + title: this.title, + url: this.url, + duration: this.duration, + uuid: this.uuid.toString() + }; +} + module.exports = mediaSchema.discriminator('saved', playlistMediaProperties); \ No newline at end of file diff --git a/src/schemas/channel/media/playlistSchema.js b/src/schemas/channel/media/playlistSchema.js index e4d7d10..a2d2b9f 100644 --- a/src/schemas/channel/media/playlistSchema.js +++ b/src/schemas/channel/media/playlistSchema.js @@ -41,11 +41,7 @@ playlistSchema.methods.dehydrate = function(){ //Fill media array for(let media of this.media){ - mediaArray.push({ - title: media.title, - url: media.url, - duration: media.duration - }); + mediaArray.push(media.dehydrate()); } //return dehydrated playlist diff --git a/src/utils/media/internetArchiveUtils.js b/src/utils/media/internetArchiveUtils.js index f0750a9..77bb4a7 100644 --- a/src/utils/media/internetArchiveUtils.js +++ b/src/utils/media/internetArchiveUtils.js @@ -16,11 +16,13 @@ along with this program. If not, see .*/ //Node Imports const url = require("node:url"); +const validator = require('validator'); //Local Imports const regexUtils = require('../regexUtils'); +const media = require('../../app/channel/media/media'); -module.exports.fetchMetadata = async function(link){ +module.exports.fetchMetadata = async function(link, title){ //Parse link const parsedLink = new url.URL(link); //Split link path @@ -31,6 +33,10 @@ module.exports.fetchMetadata = async function(link){ splitPath.splice(0,3) //Join remaining link path back together to get requested file path within the given archive.org upload const requestedPath = decodeURIComponent(splitPath.join('/')); + //Create empty list to hold media objects + const mediaList = []; + //Create empty variable to hold return data object + let data; //Create metadata link from itemID const metadataLink = `https://archive.org/metadata/${itemID}`; @@ -55,23 +61,48 @@ module.exports.fetchMetadata = async function(link){ //Filter out any in-compatible files const compatibleFiles = rawMetadata.files.filter(compatibilityFilter); + //If we're requesting an empty path if(requestedPath == ''){ //Return item metadata and compatible files - return { + data = { files: compatibleFiles, metadata: rawMetadata.metadata } //Other wise }else{ //Return item metadata and matching compatible files - return { + data = { //Filter files out that don't match requested path and return remaining list files: compatibleFiles.filter(pathFilter), metadata: rawMetadata.metadata } } + //for every compatible and relevant file returned from IA + for(let file of data.files){ + //Split file path by directories + const path = file.name.split('/'); + + //pull filename from path and escape in-case someone put something nasty in there + const name = validator.escape(validator.trim(path[path.length - 1])); + + //Construct link from pulled info + const link = `https://archive.org/download/${data.metadata.identifier}/${file.name}`; + + //if we where handed a null title + if(title == null || title == ''){ + //Create new media object from file info substituting filename for title + mediaList.push(new media(name, name, link, link, 'ia', Number(file.length))); + }else{ + //Create new media object from file info + mediaList.push(new media(title, name, link, link, 'ia', Number(file.length))); + } + } + + //return media object list + return mediaList; + function compatibilityFilter(file){ //return true for all files that match for web-safe formats return file.format == "h.264 IA" || file.format == "h.264" || file.format == "Ogg Video" || file.format.match("MPEG4"); diff --git a/src/utils/media/yanker.js b/src/utils/media/yanker.js index 191a58c..5b8dde9 100644 --- a/src/utils/media/yanker.js +++ b/src/utils/media/yanker.js @@ -19,43 +19,19 @@ const validator = require('validator');//No express here, so regular validator i //local import const iaUtil = require('./internetArchiveUtils'); -const media = require('../../app/channel/media/media'); module.exports.yankMedia = async function(url, title){ + //Get pull type const pullType = await this.getMediaType(url); - if(pullType == 'ia'){ - //Create empty list to hold media objects - const mediaList = []; - //Pull metadata from IA - const mediaInfo = await iaUtil.fetchMetadata(url); - - //for every compatible and relevant file returned from IA - for(let file of mediaInfo.files){ - //Split file path by directories - const path = file.name.split('/'); - - //pull filename from path - const name = path[path.length - 1]; - - //Construct link from pulled info - const link = `https://archive.org/download/${mediaInfo.metadata.identifier}/${file.name}`; - - //if we where handed a null title - if(title == null || title == ''){ - //Create new media object from file info substituting filename for title - mediaList.push(new media(name, name, link, link, 'ia', Number(file.length))); - }else{ - //Create new media object from file info - mediaList.push(new media(title, name, link, link, 'ia', Number(file.length))); - } - } - - //return media object list - return mediaList; - }else{ - //return null to signify a bad url - return null; + //Check pull type + switch(pullType){ + case "ia": + //return media object list from IA module + return await iaUtil.fetchMetadata(url, title); + default: + //return null to signify a bad url + return null; } } @@ -69,7 +45,6 @@ module.exports.getMediaType = async function(url){ return null; } - //If we have link to a resource from archive.org if(url.match(/^https\:\/\/archive.org\//g)){ //return internet archive code diff --git a/www/css/panel/queue.css b/www/css/panel/queue.css index e6aa120..a88f22e 100644 --- a/www/css/panel/queue.css +++ b/www/css/panel/queue.css @@ -164,10 +164,7 @@ div.dragging-queue-entry{ .queue-playlist-span{ justify-content: space-between; -} - -.queue-playlist-div{ - padding: 0 0.15em; + padding: 0 0.2em; margin: 0 0.15em; } @@ -182,10 +179,27 @@ div.dragging-queue-entry{ user-select: none; } +.queue-playlist-title-span{ + text-wrap: nowrap; + display: flex; + flex-direction: row; +} + .queue-playlist-count{ font-size: 0.8em; } +.queue-playlist-media-container-div{ + resize: vertical; + overflow: scroll; + height: 5em; +} + +.queue-playlist-media-container-div p{ + margin: 0; + font-size: 0.8em; +} + #queue-create-playlist-popup-div{ display: flex; flex-direction: column; diff --git a/www/css/theme/movie-night.css b/www/css/theme/movie-night.css index 31045e3..a7534ad 100644 --- a/www/css/theme/movie-night.css +++ b/www/css/theme/movie-night.css @@ -546,7 +546,13 @@ div.archived p{ border-block: var(--accent1) solid 1px; } -.not-first-queue-playlist-div{ + +.queue-playlist-media-container-div{ + background-color: var(--bg1-alt0); + border-block: var(--accent1) solid 1px; +} + +.queue-playlist-span.not-first{ border-top: var(--bg1-alt0) solid 1px; } diff --git a/www/js/channel/panels/queuePanel.js b/www/js/channel/panels/queuePanel.js index eb6c190..9d436ce 100644 --- a/www/js/channel/panels/queuePanel.js +++ b/www/js/channel/panels/queuePanel.js @@ -1103,24 +1103,27 @@ class playlistManager{ //Set playlist div dataset playlistDiv.dataset.name = playlist.name; - //If this isn't our first rodeo - if(playlistIndex != 0){ - //make note - playlistDiv.classList.add('not-first-queue-playlist-div'); - } - //Create span to hold playlist entry line contents const playlistSpan = document.createElement('span'); //Set classes playlistSpan.classList.add('queue-playlist-span'); + //If this isn't our first rodeo + if(playlistIndex != 0){ + //make note + playlistSpan.classList.add('not-first'); + } + + //pre-render and keep this so we can use it later + const mediaContainer = renderMedia(); + //Append items to playlist entry line playlistSpan.appendChild(renderLabels()); playlistSpan.appendChild(renderControls()); //Append items to playlist div playlistDiv.appendChild(playlistSpan); - playlistDiv.appendChild(renderMedia()); + playlistDiv.appendChild(mediaContainer); //Append current playlist div to the channel playlists div this.channelPlaylistDiv.appendChild(playlistDiv); @@ -1132,6 +1135,16 @@ class playlistManager{ //Set it's class playlistLabels.classList.add('queue-playlist-labels-span'); + //create playlist title span + const playlistTitleSpan = document.createElement('span'); + //Set class + playlistTitleSpan.classList.add('queue-playlist-title-span', 'interactive'); + + //Create playlist title caret + const playlistTitleCaret = document.createElement('i'); + //Set class + playlistTitleCaret.classList.add('bi-caret-right-fill'); + //Create playlist title label const playlistTitle = document.createElement('p'); //Set it's class @@ -1139,6 +1152,10 @@ class playlistManager{ //Unescape Sanatized Enteties and safely inject as plaintext playlistTitle.innerText = utils.unescapeEntities(playlist.name); + //Construct playlist title span + playlistTitleSpan.appendChild(playlistTitleCaret); + playlistTitleSpan.appendChild(playlistTitle); + //Create playlist count label const playlistCount = document.createElement('p'); //Set it's class @@ -1147,11 +1164,33 @@ class playlistManager{ playlistCount.innerText = `Count: ${playlist.media.length}`; //Append items to playlist labels span - playlistLabels.appendChild(playlistTitle); + playlistLabels.appendChild(playlistTitleSpan); playlistLabels.appendChild(playlistCount); + //Define input listeners + playlistTitleSpan.addEventListener('click', toggleMedia.bind(this)); + //return playlistLabels return playlistLabels; + + function toggleMedia(){ + //If the div is hidden + if(mediaContainer.style.display == 'none'){ + //Light up the button + playlistTitleSpan.classList.add('positive'); + //Flip the caret + playlistTitleCaret.classList.replace('bi-caret-right-fill', 'bi-caret-down-fill'); + //Show the div + mediaContainer.style.display = ''; + }else{ + //Unlight the button + playlistTitleSpan.classList.remove('positive'); + //Flip the caret + playlistTitleCaret.classList.replace('bi-caret-down-fill', 'bi-caret-right-fill'); + //Hide the div + mediaContainer.style.display = 'none'; + } + } } function renderControls(){ @@ -1195,14 +1234,28 @@ class playlistManager{ //Create media container div const mediaContainer = document.createElement('div'); //Set classes - mediaContainer.classList.add('queue-playlist-media-div'); + mediaContainer.classList.add('queue-playlist-media-container-div'); + //Auto-hide media container + mediaContainer.style.display = 'none'; for(let media of playlist.media){ + //Create media div + const mediaDiv = document.createElement('div'); + //Set class + mediaDiv.classList.add('queue-playlist-media-div'); + //Create media title const mediaTitle = document.createElement('p'); //Set class mediaTitle.classList.add('queue-playlist-media-title'); + //Inject text content + mediaTitle.innerText = utils.unescapeEntities(media.title); + + //Append items to media div + mediaDiv.appendChild(mediaTitle); + //Append media div to media container + mediaContainer.appendChild(mediaDiv); } //return media container