diff --git a/src/api.js b/src/api.js index ee59ad9..ef2dd40 100644 --- a/src/api.js +++ b/src/api.js @@ -27,7 +27,7 @@ module.exports = { router: api } function api(db, channelDB, fillerDB, customShowDB, xmltvInterval, guideService, _m3uService, eventService ) { let m3uService = _m3uService; const router = express.Router() - const plexServerDB = new PlexServerDB(channelDB, channelCache, db); + const plexServerDB = new PlexServerDB(channelDB, channelCache, fillerDB, customShowDB, db); router.get('/api/version', async (req, res) => { try { @@ -141,18 +141,24 @@ function api(db, channelDB, fillerDB, customShowDB, xmltvInterval, guideService }) router.post('/api/plex-servers', async (req, res) => { try { - await plexServerDB.updateServer(req.body); + let report = await plexServerDB.updateServer(req.body); + let modifiedPrograms = 0; + let destroyedPrograms = 0; + report.forEach( (r) => { + modifiedPrograms += r.modifiedPrograms; + destroyedPrograms += r.destroyedPrograms; + } ); res.status(204).send("Plex server updated.");; eventService.push( "settings-update", { - "message": `Plex server ${req.body.name} updated.`, + "message": `Plex server ${req.body.name} updated. ${modifiedPrograms} programs modified, ${destroyedPrograms} programs deleted`, "module" : "plex-server", "detail" : { "serverName" : req.body.name, "action" : "update" }, - "level" : "info" + "level" : "warning" } ); diff --git a/src/dao/custom-show-db.js b/src/dao/custom-show-db.js index f174f3b..2f581d5 100644 --- a/src/dao/custom-show-db.js +++ b/src/dao/custom-show-db.js @@ -110,8 +110,8 @@ class CustomShowDB { async getAllShowsInfo() { //returns just name and id - let fillers = await this.getAllShows(); - return fillers.map( (f) => { + let shows = await this.getAllShows(); + return shows.map( (f) => { return { 'id' : f.id, 'name': f.name, diff --git a/src/dao/filler-db.js b/src/dao/filler-db.js index b9d7f9b..fcd2d3e 100644 --- a/src/dao/filler-db.js +++ b/src/dao/filler-db.js @@ -192,8 +192,8 @@ class FillerDB { } function fixup(json) { - if (typeof(json.fillerContent) === 'undefined') { - json.fillerContent = []; + if (typeof(json.content) === 'undefined') { + json.content = []; } if (typeof(json.name) === 'undefined') { json.name = "Unnamed Filler"; diff --git a/src/dao/plex-server-db.js b/src/dao/plex-server-db.js index ebbf09e..5b349c5 100644 --- a/src/dao/plex-server-db.js +++ b/src/dao/plex-server-db.js @@ -1,14 +1,20 @@ //hmnn this is more of a "PlexServerService"... +const ICON_REGEX = /https?:\/\/.*(\/library\/metadata\/\d+\/thumb\/\d+).X-Plex-Token=.*/; + +const ICON_FIELDS = ["icon", "showIcon", "seasonIcon", "episodeIcon"]; + class PlexServerDB { - constructor(channelDB, channelCache, db) { + constructor(channelDB, channelCache, fillerDB, showDB, db) { this.channelDB = channelDB; this.db = db; this.channelCache = channelCache; + this.fillerDB = fillerDB; + this.showDB = showDB; } - async deleteServer(name) { + async fixupAllChannels(name, newServer) { let channelNumbers = await this.channelDB.getAllChannelNumbers(); let report = await Promise.all( channelNumbers.map( async (i) => { let channel = await this.channelDB.getChannel(i); @@ -16,17 +22,10 @@ class PlexServerDB channelNumber : channel.number, channelName : channel.name, destroyedPrograms: 0, + modifiedPrograms: 0, }; - this.fixupProgramArray(channel.programs, name, channelReport); - this.fixupProgramArray(channel.fillerContent, name, channelReport); - this.fixupProgramArray(channel.fallback, name, channelReport); - if (typeof(channel.fillerContent) !== 'undefined') { - channel.fillerContent = channel.fillerContent.filter( - (p) => { - return (true !== p.isOffline); - } - ); - } + this.fixupProgramArray(channel.programs, name,newServer, channelReport); + //if fallback became offline, remove it if ( (typeof(channel.fallback) !=='undefined') && (channel.fallback.length > 0) @@ -38,15 +37,87 @@ class PlexServerDB channel.offlinePicture = `http://localhost:${process.env.PORT}/images/generic-offline-screen.png`; } } - this.fixupProgramArray(channel.fallback, name, channelReport); + this.fixupProgramArray(channel.fallback, name,newServer, channelReport); await this.channelDB.saveChannel(i, channel); - this.db['plex-servers'].remove( { name: name } ); return channelReport; }) ); this.channelCache.clear(); return report; } + async fixupAllFillers(name, newServer) { + let fillers = await this.fillerDB.getAllFillers(); + let report = await Promise.all( fillers.map( async (filler) => { + let fillerReport = { + channelNumber : "--", + channelName : filler.name + " (filler)", + destroyedPrograms: 0, + modifiedPrograms: 0, + }; + this.fixupProgramArray( filler.content, name,newServer, fillerReport ); + filler.content = this.removeOffline(filler.content); + + await this.fillerDB.saveFiller( filler.id, filler ); + + return fillerReport; + } ) ); + return report; + + } + + async fixupAllShows(name, newServer) { + let shows = await this.showDB.getAllShows(); + let report = await Promise.all( shows.map( async (show) => { + let showReport = { + channelNumber : "--", + channelName : show.name + " (custom show)", + destroyedPrograms: 0, + modifiedPrograms: 0, + }; + this.fixupProgramArray( show.content, name,newServer, showReport ); + show.content = this.removeOffline(show.content); + + await this.showDB.saveShow( show.id, show ); + + return showReport; + } ) ); + return report; + + } + + + removeOffline( progs ) { + if (typeof(progs) === 'undefined') { + return progs; + } + return progs.filter( + (p) => { + return (true !== p.isOffline); + } + ); + } + + async fixupEveryProgramHolders(serverName, newServer) { + let reports = await Promise.all( [ + this.fixupAllChannels( serverName, newServer ), + this.fixupAllFillers(serverName, newServer), + this.fixupAllShows(serverName, newServer), + ] ); + let report = []; + reports.forEach( + (r) => r.forEach( (r2) => { + report.push(r2) + } ) + ); + return report; + } + + async deleteServer(name) { + let report = await this.fixupEveryProgramHolders(name, null); + this.db['plex-servers'].remove( { name: name } ); + return report; + } + doesNameExist(name) { return this.db['plex-servers'].find( { name: name} ).length > 0; } @@ -77,11 +148,15 @@ class PlexServerDB arChannels: arChannels, index: s.index, } + this.normalizeServer(newServer); + + let report = await this.fixupEveryProgramHolders(name, newServer); this.db['plex-servers'].update( { _id: s._id }, newServer ); + return report; } @@ -117,26 +192,56 @@ class PlexServerDB arChannels: arChannels, index: index, }; + this.normalizeServer(newServer); this.db['plex-servers'].save(newServer); } - fixupProgramArray(arr, serverName, channelReport) { + fixupProgramArray(arr, serverName,newServer, channelReport) { if (typeof(arr) !== 'undefined') { for(let i = 0; i < arr.length; i++) { - arr[i] = this.fixupProgram( arr[i], serverName, channelReport ); + arr[i] = this.fixupProgram( arr[i], serverName,newServer, channelReport ); } } } - fixupProgram(program, serverName, channelReport) { - if (program.serverKey === serverName) { + fixupProgram(program, serverName,newServer, channelReport) { + if ( (program.serverKey === serverName) && (newServer == null) ) { channelReport.destroyedPrograms += 1; return { isOffline: true, duration: program.duration, } + } else if (program.serverKey === serverName) { + let modified = false; + ICON_FIELDS.forEach( (field) => { + if ( + (typeof(program[field] ) === 'string') + && + program[field].includes("/library/metadata") + && + program[field].includes("X-Plex-Token") + ) { + let m = program[field].match(ICON_REGEX); + if (m.length == 2) { + let lib = m[1]; + let newUri = `${newServer.uri}${lib}?X-Plex-Token=${newServer.accessToken}` + program[field] = newUri; + modified = true; + } + } + + } ); + if (modified) { + channelReport.modifiedPrograms += 1; + } } return program; } + + normalizeServer(server) { + while (server.uri.endsWith("/")) { + server.uri = server.uri.slice(0,-1); + } + } } module.exports = PlexServerDB \ No newline at end of file diff --git a/src/plex-player.js b/src/plex-player.js index d21bcdc..9a75f84 100644 --- a/src/plex-player.js +++ b/src/plex-player.js @@ -49,7 +49,7 @@ class PlexPlayer { let channel = this.context.channel; let server = db['plex-servers'].find( { 'name': lineupItem.serverKey } ); if (server.length == 0) { - throw Error(`Unable to find server "${lineupItem.serverKey}" specied by program.`); + throw Error(`Unable to find server "${lineupItem.serverKey}" specified by program.`); } server = server[0]; if (server.uri.endsWith("/")) { diff --git a/src/services/random-slots-service.js b/src/services/random-slots-service.js index dc3ef70..2cc6064 100644 --- a/src/services/random-slots-service.js +++ b/src/services/random-slots-service.js @@ -276,11 +276,11 @@ module.exports = async( programs, schedule ) => { let s = schedule.slots; let ts = (new Date() ).getTime(); - let curr = ts - ts % DAY; + let t0 = ts; let p = []; let t = t0; - let wantedFinish = 0; + let hardLimit = t0 + schedule.maxDays * DAY; let pushFlex = (d) => { @@ -297,6 +297,15 @@ module.exports = async( programs, schedule ) => { } } + let pushProgram = (item) => { + if ( item.isOffline && (item.type !== 'redirect') ) { + pushFlex(item.duration); + } else { + p.push(item); + t += item.duration; + } + }; + let slotLastPlayed = {}; while ( (t < hardLimit) && (p.length < LIMIT) ) { @@ -338,15 +347,14 @@ module.exports = async( programs, schedule ) => { if (item.isOffline) { //flex or redirect. We can just use the whole duration - p.push(item); - t += remaining; + item.duration = remaining; + pushProgram(item); slotLastPlayed[ slotIndex ] = t; continue; } if (item.duration > remaining) { // Slide - p.push(item); - t += item.duration; + pushProgram(item); slotLastPlayed[ slotIndex ] = t; advanceSlot(slot); continue; @@ -412,8 +420,7 @@ module.exports = async( programs, schedule ) => { } // now unroll them all for (let i = 0; i < pads.length; i++) { - p.push( pads[i].item ); - t += pads[i].item.duration; + pushProgram( pads[i].item ); slotLastPlayed[ slotIndex ] = t; pushFlex( pads[i].pad ); } @@ -421,15 +428,10 @@ module.exports = async( programs, schedule ) => { while ( (t > hardLimit) || (p.length >= LIMIT) ) { t -= p.pop().duration; } - let m = t % schedule.period; - let rem = 0; - if (m > wantedFinish) { - rem = schedule.period + wantedFinish - m; - } else if (m < wantedFinish) { - rem = wantedFinish - m; - } - if (rem > constants.SLACK) { - pushFlex(rem); + let m = (t - t0) % schedule.period; + if (m != 0) { + //ensure the schedule is a multiple of period + pushFlex( schedule.period - m); } diff --git a/src/services/time-slots-service.js b/src/services/time-slots-service.js index 5fd3159..64b6115 100644 --- a/src/services/time-slots-service.js +++ b/src/services/time-slots-service.js @@ -302,6 +302,15 @@ module.exports = async( programs, schedule ) => { } } + let pushProgram = (item) => { + if ( item.isOffline && (item.type !== 'redirect') ) { + pushFlex(item.duration); + } else { + p.push(item); + t += item.duration; + } + }; + if (ts > t0) { pushFlex( ts - t0 ); } @@ -355,14 +364,13 @@ module.exports = async( programs, schedule ) => { if (item.isOffline) { //flex or redirect. We can just use the whole duration - p.push(item); - t += remaining; + item.duration = remaining; + pushProgram(item); continue; } if (item.duration > remaining) { // Slide - p.push(item); - t += item.duration; + pushProgram(item); advanceSlot(slot); continue; } @@ -373,7 +381,7 @@ module.exports = async( programs, schedule ) => { let pads = [ padded ]; while(true) { - let item2 = getNextForSlot(slot); + let item2 = getNextForSlot(slot, remaining); if (total + item2.duration > remaining) { break; } @@ -413,23 +421,17 @@ module.exports = async( programs, schedule ) => { } // now unroll them all for (let i = 0; i < pads.length; i++) { - p.push( pads[i].item ); - t += pads[i].item.duration; + pushProgram( pads[i].item ); pushFlex( pads[i].pad ); } } while ( (t > hardLimit) || (p.length >= LIMIT) ) { t -= p.pop().duration; } - let m = t % schedule.period; - let rem = 0; - if (m > wantedFinish) { - rem = schedule.period + wantedFinish - m; - } else if (m < wantedFinish) { - rem = wantedFinish - m; - } - if (rem > constants.SLACK) { - pushFlex(rem); + let m = (t - t0) % schedule.period; + if (m > 0) { + //ensure the schedule is a multiple of period + pushFlex( schedule.period - m); } diff --git a/src/video.js b/src/video.js index 6c1d483..1010476 100644 --- a/src/video.js +++ b/src/video.js @@ -123,10 +123,14 @@ function video( channelDB , fillerDB, db) { // Stream individual video to ffmpeg concat above. This is used by the server, NOT the client router.get('/stream', async (req, res) => { // Check if channel queried is valid + res.on("error", (e) => { + console.err("There was an unexpected error in stream.", e); + } ); if (typeof req.query.channel === 'undefined') { res.status(400).send("No Channel Specified") return } + let audioOnly = ("true" == req.query.audioOnly); console.log(`/stream audioOnly=${audioOnly}`); let session = parseInt(req.query.session); @@ -323,6 +327,7 @@ function video( channelDB , fillerDB, db) { res.writeHead(200, { 'Content-Type': 'video/mp2t' }); + try { playerObj = await player.play(res); } catch (err) { diff --git a/web/directives/channel-config.js b/web/directives/channel-config.js index 685bce2..9885f29 100644 --- a/web/directives/channel-config.js +++ b/web/directives/channel-config.js @@ -163,7 +163,15 @@ module.exports = function ($timeout, $location, dizquetv, resolutionOptions, get let t = Date.now(); let originalStart = scope.channel.startTime.getTime(); let n = scope.channel.programs.length; - let totalDuration = scope.channel.duration; + //scope.channel.totalDuration might not have been initialized + let totalDuration = 0; + for (let i = 0; i < n; i++) { + totalDuration += scope.channel.programs[i].duration; + } + if (totalDuration == 0) { + return; + } + let m = (t - originalStart) % totalDuration; let x = 0; let runningProgram = -1; diff --git a/web/directives/plex-library.js b/web/directives/plex-library.js index 4e21a52..ce1018a 100644 --- a/web/directives/plex-library.js +++ b/web/directives/plex-library.js @@ -219,6 +219,31 @@ module.exports = function (plex, dizquetv, $timeout, commonProgramTools) { scope.customShows = await dizquetv.getAllShowsInfo(); scope.$apply(); } + + scope.displayTitle = (show) => { + let r = ""; + if (show.type === 'episode') { + r += show.showTitle + " - "; + if ( typeof(show.season) !== 'undefined' ) { + r += "S" + show.season.toString().padStart(2,'0'); + } + if ( typeof(show.episode) !== 'undefined' ) { + r += "E" + show.episode.toString().padStart(2,'0'); + } + } + if (r != "") { + r = r + " - "; + } + r += show.title; + if ( + (show.type !== 'episode') + && + (typeof(show.year) !== 'undefined') + ) { + r += " (" + JSON.stringify(show.year) + ")"; + } + return r; + } } }; } \ No newline at end of file diff --git a/web/directives/plex-server-edit.js b/web/directives/plex-server-edit.js index c608d19..7772381 100644 --- a/web/directives/plex-server-edit.js +++ b/web/directives/plex-server-edit.js @@ -9,11 +9,13 @@ module.exports = function (dizquetv, $timeout) { }, link: function (scope, element, attrs) { scope.state.modified = false; + scope.loading = { show: false }; scope.setModified = () => { scope.state.modified = true; } scope.onSave = async () => { try { + scope.loading = { show: true }; await dizquetv.updatePlexServer(scope.state.server); scope.state.modified = false; scope.state.success = "The server was updated."; @@ -23,6 +25,8 @@ module.exports = function (dizquetv, $timeout) { scope.state.error = "There was an error updating the server"; scope.state.success = ""; console.error(scope.state.error, err); + } finally { + scope.loading = { show: false }; } $timeout( () => { scope.$apply() } , 0 ); } diff --git a/web/directives/toast-notifications.js b/web/directives/toast-notifications.js index bb6f645..cb83870 100644 --- a/web/directives/toast-notifications.js +++ b/web/directives/toast-notifications.js @@ -110,6 +110,11 @@ module.exports = function ($timeout) { eventSource.addEventListener('xmltv', normalEvent("TV Guide") ); eventSource.addEventListener('lifecycle', normalEvent("Server") ); }; + + scope.destroy = (index) => { + scope.toasts.splice(index,1); + } + scope.setup(); } }; diff --git a/web/public/templates/plex-library.html b/web/public/templates/plex-library.html index 7a08241..7c678e1 100644 --- a/web/public/templates/plex-library.html +++ b/web/public/templates/plex-library.html @@ -36,7 +36,7 @@