diff --git a/README.md b/README.md index 0747c10..d0228bf 100644 --- a/README.md +++ b/README.md @@ -1,4 +1,4 @@ -# dizqueTV 1.1.3 +# dizqueTV 1.1.4-prerelease    Create live TV channel streams from media on your Plex servers. diff --git a/index.js b/index.js index 79d90a3..36ea844 100644 --- a/index.js +++ b/index.js @@ -17,6 +17,7 @@ const constants = require('./src/constants') const ChannelDB = require("./src/dao/channel-db"); const FillerDB = require("./src/dao/filler-db"); const TVGuideService = require("./src/tv-guide-service"); +const onShutdown = require("node-graceful-shutdown").onShutdown; console.log( ` \\ @@ -209,3 +210,11 @@ function initDB(db, channelDB) { } } + +onShutdown("log" , [], async() => { + console.log("Received exit signal, attempting graceful shutdonw..."); +}); +onShutdown("xmltv-writer" , [], async() => { + await xmltv.shutdown(); +} ); + diff --git a/package.json b/package.json index 22d7a59..a5498d0 100644 --- a/package.json +++ b/package.json @@ -19,6 +19,7 @@ "angular": "^1.7.9", "angular-router-browserify": "0.0.2", "angular-vs-repeat": "2.0.13", + "random-js" : "2.1.0", "axios": "^0.19.2", "body-parser": "^1.19.0", "diskdb": "^0.1.17", @@ -26,6 +27,7 @@ "node-ssdp": "^4.0.0", "request": "^2.88.2", "uuid": "^8.0.0", + "node-graceful-shutdown" : "1.1.0", "xml-writer": "^1.7.0" }, "bin": "dist/index.js", diff --git a/src/constants.js b/src/constants.js index a0621e6..e4c0497 100644 --- a/src/constants.js +++ b/src/constants.js @@ -5,5 +5,5 @@ module.exports = { TVGUIDE_MAXIMUM_FLEX_DURATION : 6 * 60 * 60 * 1000, TOO_FREQUENT: 100, - VERSION_NAME: "1.1.3" + VERSION_NAME: "1.1.4-prerelease" } diff --git a/src/helperFuncs.js b/src/helperFuncs.js index d232f42..e1708ad 100644 --- a/src/helperFuncs.js +++ b/src/helperFuncs.js @@ -6,6 +6,9 @@ module.exports = { let channelCache = require('./channel-cache'); const SLACK = require('./constants').SLACK; +const randomJS = require("random-js"); +const Random = randomJS.Random; +const random = new Random( randomJS.MersenneTwister19937.autoSeed() ); function getCurrentProgramAndTimeElapsed(date, channel) { let channelStartTime = (new Date(channel.startTime)).getTime(); @@ -102,7 +105,7 @@ function createLineup(obj, channel, fillers, isFirst) { //it's boring and odd to tune into a channel and it's always //the start of a commercial. let more = Math.max(0, filler.duration - fillerstart - 15000 - SLACK); - fillerstart += Math.floor(more * Math.random() ); + fillerstart += random.integer(0, more); } lineup.push({ // just add the video, starting at 0, playing the entire duration type: 'commercial', @@ -152,12 +155,7 @@ function createLineup(obj, channel, fillers, isFirst) { } function weighedPick(a, total) { - if (a==total) { - return true; - } else { - let ran = Math.random(); - return ran * total < a; - } + return random.bool(a, total); } function pickRandomWithMaxDuration(channel, fillers, maxDuration) { diff --git a/src/plexTranscoder.js b/src/plexTranscoder.js index ee1a79a..11f3def 100644 --- a/src/plexTranscoder.js +++ b/src/plexTranscoder.js @@ -182,7 +182,7 @@ lang=en` try { return this.getVideoStats().videoDecision === "copy"; } catch (e) { - console.log("Error at decision:" + e); + console.log("Error at decision:", e); return false; } } @@ -199,7 +199,7 @@ lang=en` try { return this.getVideoStats().videoDecision === "copy" && this.getVideoStats().audioDecision === "copy"; } catch (e) { - console.log("Error at decision:" + e); + console.log("Error at decision:" , e); return false; } } @@ -245,7 +245,7 @@ lang=en` } }.bind(this) ) } catch (e) { - console.log("Error at decision:" + e); + console.log("Error at decision:" , e); } this.log("Current video stats:") @@ -289,7 +289,11 @@ lang=en` } - async getDecision(directPlay) { + async getDecisionUnmanaged(directPlay) { + if (this.settings.streamPath === 'direct') { + console.log("Skip get transcode decision because direct path is enabled"); + return; + } let res = await axios.get(`${this.server.uri}/video/:/transcode/universal/decision?${this.transcodingArgs}`, { headers: { Accept: 'application/json' } }) @@ -307,6 +311,14 @@ lang=en` } } + async getDecision(directPlay) { + try { + await this.getDecisionUnmanaged(directPlay); + } catch (err) { + console.error(err); + } + } + getStatusUrl() { let profileName=`Generic`; diff --git a/src/xmltv.js b/src/xmltv.js index d1596be..6d738b2 100644 --- a/src/xmltv.js +++ b/src/xmltv.js @@ -1,11 +1,29 @@ const XMLWriter = require('xml-writer') const fs = require('fs') -const helperFuncs = require('./helperFuncs') -const constants = require('./constants') -module.exports = { WriteXMLTV: WriteXMLTV } +module.exports = { WriteXMLTV: WriteXMLTV, shutdown: shutdown } -function WriteXMLTV(json, xmlSettings, throttle ) { +let isShutdown = false; +let isWorking = false; + +async function WriteXMLTV(json, xmlSettings, throttle ) { + if (isShutdown) { + return; + } + if (isWorking) { + console.log("Concurrent xmltv write attempt detected, skipping"); + return; + } + isWorking = true; + try { + await writePromise(json, xmlSettings, throttle); + } catch (err) { + console.error("Error writing xmltv", err); + } + isWorking = false; +} + +function writePromise(json, xmlSettings, throttle) { return new Promise((resolve, reject) => { let ws = fs.createWriteStream(xmlSettings.file) let xw = new XMLWriter(true, (str, enc) => ws.write(str, enc)) @@ -59,7 +77,9 @@ function _writeChannels(xw, channels) { async function _writePrograms(xw, channel, programs, throttle) { for (let i = 0; i < programs.length; i++) { - await throttle(); + if (! isShutdown) { + await throttle(); + } await _writeProgramme(channel, programs[i], xw); } } @@ -117,8 +137,25 @@ async function _writeProgramme(channel, program, xw) { function _createXMLTVDate(d) { return d.substring(0,19).replace(/[-T:]/g,"") + " +0000"; } -function _throttle() { +function wait(x) { return new Promise((resolve) => { - setTimeout(resolve, 0); + setTimeout(resolve, x); }); } + +async function shutdown() { + isShutdown = true; + console.log("Shutting down xmltv writer."); + if (isWorking) { + let s = "Wait for xmltv writer..."; + while (isWorking) { + console.log(s); + await wait(100); + s = "Still waiting for xmltv writer..."; + } + console.log("Write finished."); + } else { + console.log("xmltv writer had no pending jobs."); + } +} + diff --git a/web/directives/channel-config.js b/web/directives/channel-config.js index 3c9fca1..cd4b9e5 100644 --- a/web/directives/channel-config.js +++ b/web/directives/channel-config.js @@ -110,18 +110,23 @@ module.exports = function ($timeout, $location, dizquetv) { scope._selectedProgram = null updateChannelDuration() } - scope.dropFunction = (dropIndex, index, program) => { - if (scope.channel.programs[index].start == program.start) { - return false; + scope.dropFunction = (dropIndex, program) => { + let y = program.$index; + let z = dropIndex + scope.currentStartIndex - 1; + scope.channel.programs.splice(y, 1); + if (z >= y) { + z--; } - - setTimeout( () => { - scope.channel.programs.splice(dropIndex + index, 0, program); - updateChannelDuration() - scope.$apply(); - }, 1); - return true; + scope.channel.programs.splice(z, 0, program ); + updateChannelDuration(); + $timeout(); + return false; } + scope.setUpWatcher = function setupWatchers() { + this.$watch('vsRepeat.startIndex', function(val) { + scope.currentStartIndex = val; + }); + }; let fixFillerCollection = (f) => { return { @@ -283,6 +288,7 @@ module.exports = function ($timeout, $location, dizquetv) { newProgs.push(tmpProgs[keys[i]]) } scope.channel.programs = newProgs + updateChannelDuration(); //oops someone forgot to add this } scope.removeOffline = () => { let tmpProgs = [] @@ -1242,6 +1248,8 @@ module.exports = function ($timeout, $location, dizquetv) { scope.minBreakSize = -1; scope.maxBreakSize = -1; let breakSizeOptions = [ + { id: 10, description: "10 seconds" }, + { id: 15, description: "15 seconds" }, { id: 30, description: "30 seconds" }, { id: 45, description: "45 seconds" }, { id: 60, description: "60 seconds" }, @@ -1250,8 +1258,9 @@ module.exports = function ($timeout, $location, dizquetv) { { id: 180, description: "3 minutes" }, { id: 300, description: "5 minutes" }, { id: 450, description: "7.5 minutes" }, - { id: 600, description: "10 minutes" }, - { id: 1200, description: "20 minutes" }, + { id: 10*60, description: "10 minutes" }, + { id: 20*60, description: "20 minutes" }, + { id: 30*60, description: "30 minutes" }, ] scope.minBreakSizeOptions = [ { id: -1, description: "Min Duration" }, diff --git a/web/directives/filler-config.js b/web/directives/filler-config.js index ce95311..f5c89f5 100644 --- a/web/directives/filler-config.js +++ b/web/directives/filler-config.js @@ -14,6 +14,40 @@ module.exports = function ($timeout) { scope.visible = false; scope.error = undefined; + function refreshContentIndexes() { + for (let i = 0; i < scope.content.length; i++) { + scope.content[i].$index = i; + } + } + + scope.contentSplice = (a,b) => { + scope.content.splice(a,b) + refreshContentIndexes(); + } + + scope.dropFunction = (dropIndex, program) => { + let y = program.$index; + let z = dropIndex + scope.currentStartIndex - 1; + scope.content.splice(y, 1); + if (z >= y) { + z--; + } + scope.content.splice(z, 0, program ); + $timeout(); + return false; + } + scope.setUpWatcher = function setupWatchers() { + this.$watch('vsRepeat.startIndex', function(val) { + scope.currentStartIndex = val; + }); + }; + + scope.movedFunction = (index) => { + console.log("movedFunction(" + index + ")"); + } + + + scope.linker( (filler) => { if ( typeof(filler) === 'undefined') { scope.name = ""; @@ -26,6 +60,7 @@ module.exports = function ($timeout) { scope.id = filler.id; scope.title = "Edit Filler List"; } + refreshContentIndexes(); scope.visible = true; } ); @@ -49,7 +84,10 @@ module.exports = function ($timeout) { scope.visible = false; scope.onDone( { name: scope.name, - content: scope.content, + content: scope.content.map( (c) => { + delete c.$index + return c; + } ), id: scope.id, } ); } @@ -58,9 +96,11 @@ module.exports = function ($timeout) { } scope.sortFillers = () => { scope.content.sort( (a,b) => { return a.duration - b.duration } ); + refreshContentIndexes(); } scope.fillerRemoveAllFiller = () => { scope.content = []; + refreshContentIndexes(); } scope.fillerRemoveDuplicates = () => { function getKey(p) { @@ -77,12 +117,14 @@ module.exports = function ($timeout) { } } scope.content = newFiller; + refreshContentIndexes(); } scope.importPrograms = (selectedPrograms) => { for (let i = 0, l = selectedPrograms.length; i < l; i++) { selectedPrograms[i].commercials = [] } scope.content = scope.content.concat(selectedPrograms); + refreshContentIndexes(); scope.showPlexLibrary = false; } diff --git a/web/public/index.html b/web/public/index.html index c701ec2..a495695 100644 --- a/web/public/index.html +++ b/web/public/index.html @@ -25,7 +25,7 @@ - + diff --git a/web/public/templates/channel-config.html b/web/public/templates/channel-config.html index f6fdc75..8e38fa3 100644 --- a/web/public/templates/channel-config.html +++ b/web/public/templates/channel-config.html @@ -432,11 +432,12 @@