diff --git a/Dockerfile-nvidia b/Dockerfile-nvidia index 64a9569..740d98a 100644 --- a/Dockerfile-nvidia +++ b/Dockerfile-nvidia @@ -6,7 +6,7 @@ COPY --from=vexorian/dizquetv:nexecache /var/nexe/linux-x64-12.16.2 /var/nexe/ COPY . . RUN npm run build && LINUXBUILD=dizquetv sh make_dist.sh linuxonly -FROM jrottenberg/ffmpeg:4.3-nvidia +FROM jrottenberg/ffmpeg:4.3-nvidia1804 EXPOSE 8000 WORKDIR /home/node/app ENTRYPOINT [ "./dizquetv" ] diff --git a/README.md b/README.md index 6ff58a9..6ec4761 100644 --- a/README.md +++ b/README.md @@ -68,7 +68,6 @@ npm run dev-server ## Contribute * Pull requests welcome but please read the [Code of Conduct](CODE_OF_CONDUCT.md) and the [Pull Request Template](pull_request_template.md) first. -* We use [Conventional Commits](https://www.conventionalcommits.org/), a specification for adding human and machine readable meaning to commit messages. Add files with `git add` and call `git commit` to use your command line utility and create a commit. * Tip Jar: https://buymeacoffee.com/vexorian ## License diff --git a/index.js b/index.js index fc06018..b97c8c7 100644 --- a/index.js +++ b/index.js @@ -42,6 +42,12 @@ console.log( '------------' `); +const NODE = parseInt( process.version.match(/^[^0-9]*(\d+)\..*$/)[1] ); + +if (NODE < 12) { + console.error(`WARNING: Your nodejs version ${process.version} is lower than supported. dizqueTV has been tested best on nodejs 12.16.`); +} + for (let i = 0, l = process.argv.length; i < l; i++) { if ((process.argv[i] === "-p" || process.argv[i] === "--port") && i + 1 !== l) diff --git a/src/api.js b/src/api.js index cc17398..dfe10ef 100644 --- a/src/api.js +++ b/src/api.js @@ -37,6 +37,7 @@ function api(db, channelService, fillerDB, customShowDB, xmltvInterval, guideSe res.send( { "dizquetv" : constants.VERSION_NAME, "ffmpeg" : v, + "nodejs" : process.version, } ); } catch(err) { console.error(err); @@ -1037,7 +1038,6 @@ function api(db, channelService, fillerDB, customShowDB, xmltvInterval, guideSe delete toolRes.programs; let s = JSON.stringify(toolRes); s = s.slice(0, -1); - console.log( JSON.stringify(toolRes)); res.writeHead(200, { 'Content-Type': 'application/json' diff --git a/src/channel-cache.js b/src/channel-cache.js index 5c7b624..302510a 100644 --- a/src/channel-cache.js +++ b/src/channel-cache.js @@ -167,6 +167,10 @@ function recordPlayback(channelId, t0, lineupItem) { } } +function clearPlayback(channelId) { + delete cache[channelId]; +} + function clear() { //it's not necessary to clear the playback cache and it may be undesirable configCache = {}; @@ -184,4 +188,5 @@ module.exports = { getChannelConfig: getChannelConfig, saveChannelConfig: saveChannelConfig, getFillerLastPlayTime: getFillerLastPlayTime, + clearPlayback: clearPlayback, } diff --git a/src/constants.js b/src/constants.js index b2563f6..49d5cf8 100644 --- a/src/constants.js +++ b/src/constants.js @@ -3,7 +3,7 @@ module.exports = { TVGUIDE_MAXIMUM_PADDING_LENGTH_MS: 30*60*1000, DEFAULT_GUIDE_STEALTH_DURATION: 5 * 60* 1000, TVGUIDE_MAXIMUM_FLEX_DURATION : 6 * 60 * 60 * 1000, - TOO_FREQUENT: 100, + TOO_FREQUENT: 1000, //when a channel is forcibly stopped due to an update, let's mark it as active // for a while during the transaction just in case. diff --git a/src/dao/channel-db.js b/src/dao/channel-db.js index ac81bf4..a09eeff 100644 --- a/src/dao/channel-db.js +++ b/src/dao/channel-db.js @@ -29,10 +29,8 @@ class ChannelDB { } async saveChannel(number, json) { - if (typeof(number) === 'undefined') { - throw Error("Mising channel number"); - } - let f = path.join(this.folder, `${number}.json` ); + await this.validateChannelJson(number, json); + let f = path.join(this.folder, `${json.number}.json` ); return await new Promise( (resolve, reject) => { let data = undefined; try { @@ -50,12 +48,30 @@ class ChannelDB { } saveChannelSync(number, json) { - json.number = number; + this.validateChannelJson(number, json); + let data = JSON.stringify(json); - let f = path.join(this.folder, `${number}.json` ); + let f = path.join(this.folder, `${json.number}.json` ); fs.writeFileSync( f, data ); } + validateChannelJson(number, json) { + json.number = number; + if (typeof(json.number) === 'undefined') { + throw Error("Expected a channel.number"); + } + if (typeof(json.number) === 'string') { + try { + json.number = parseInt(json.number); + } catch (err) { + console.error("Error parsing channel number.", err); + } + } + if ( isNaN(json.number)) { + throw Error("channel.number must be a integer"); + } + } + async deleteChannel(number) { let f = path.join(this.folder, `${number}.json` ); await new Promise( (resolve, reject) => { diff --git a/src/dao/plex-server-db.js b/src/dao/plex-server-db.js index ed5853f..cd35d4c 100644 --- a/src/dao/plex-server-db.js +++ b/src/dao/plex-server-db.js @@ -135,7 +135,7 @@ class PlexServerDB s = s[0]; let arGuide = server.arGuide; if (typeof(arGuide) === 'undefined') { - arGuide = true; + arGuide = false; } let arChannels = server.arChannels; if (typeof(arChannels) === 'undefined') { @@ -177,7 +177,7 @@ class PlexServerDB name = resultName; let arGuide = server.arGuide; if (typeof(arGuide) === 'undefined') { - arGuide = true; + arGuide = false; } let arChannels = server.arGuide; if (typeof(arChannels) === 'undefined') { diff --git a/src/database-migration.js b/src/database-migration.js index 9ce4a9d..3eada1f 100644 --- a/src/database-migration.js +++ b/src/database-migration.js @@ -485,7 +485,7 @@ function splitServersSingleChannels(db, channelDB ) { let saveServer = (name, uri, accessToken, arGuide, arChannels) => { if (typeof(arGuide) === 'undefined') { - arGuide = true; + arGuide = false; } if (typeof(arChannels) === 'undefined') { arChannels = false; diff --git a/src/helperFuncs.js b/src/helperFuncs.js index 4cf2301..98e7552 100644 --- a/src/helperFuncs.js +++ b/src/helperFuncs.js @@ -29,7 +29,6 @@ function getCurrentProgramAndTimeElapsed(date, channel) { if (channelStartTime > date) { let t0 = date; let t1 = channelStartTime; - console.log(t0, t1); console.log("Channel start time is above the given date. Flex time is picked till that."); return { program: { @@ -185,10 +184,11 @@ function pickRandomWithMaxDuration(channel, fillers, maxDuration) { list = list.concat(fillers[i].content); } let pick1 = null; - let pick2 = null; + let t0 = (new Date()).getTime(); let minimumWait = 1000000000; const D = 7*24*60*60*1000; + const E = 5*60*60*1000; if (typeof(channel.fillerRepeatCooldown) === 'undefined') { channel.fillerRepeatCooldown = 30*60*1000; } @@ -198,7 +198,7 @@ function pickRandomWithMaxDuration(channel, fillers, maxDuration) { list = fillers[j].content; let pickedList = false; let n = 0; - let m = 0; + for (let i = 0; i < list.length; i++) { let clip = list[i]; // a few extra milliseconds won't hurt anyone, would it? dun dun dun @@ -206,7 +206,6 @@ function pickRandomWithMaxDuration(channel, fillers, maxDuration) { let t1 = channelCache.getProgramLastPlayTime( channel.number, clip ); let timeSince = ( (t1 == 0) ? D : (t0 - t1) ); - if (timeSince < channel.fillerRepeatCooldown - SLACK) { let w = channel.fillerRepeatCooldown - timeSince; if (clip.duration + w <= maxDuration + SLACK) { @@ -223,6 +222,7 @@ function pickRandomWithMaxDuration(channel, fillers, maxDuration) { if ( weighedPick(fillers[j].weight, listM) ) { pickedList = true; fillerId = fillers[j].id; + n = 0; } else { break; } @@ -235,29 +235,20 @@ function pickRandomWithMaxDuration(channel, fillers, maxDuration) { break; } } - if (timeSince >= D) { - let p = 200, q = Math.max( maxDuration - clip.duration, 1 ); - let pq = Math.min( Math.ceil(p / q), 10 ); - let w = pq; - n += w; - if ( weighedPick(w, n) ) { - pick1 = clip; - } - } else { - let adjust = Math.floor(timeSince / (60*1000)); - if (adjust > 0) { - adjust = adjust * adjust; - //weighted - m += adjust; - if ( weighedPick(adjust, m) ) { - pick2 = clip; - } - } + if (timeSince <= 0) { + continue; + } + let s = norm_s( (timeSince >= E) ? E : timeSince ); + let d = norm_d( clip.duration); + let w = s + d; + n += w; + if (weighedPick(w,n)) { + pick1 = clip; } } } } - let pick = (pick1 == null) ? pick2: pick1; + let pick = pick1; let pickTitle = "null"; if (pick != null) { pickTitle = pick.title; @@ -272,6 +263,22 @@ function pickRandomWithMaxDuration(channel, fillers, maxDuration) { } } +function norm_d(x) { + x /= 60 * 1000; + if (x >= 3.0) { + x = 3.0 + Math.log(x); + } + let y = 10000 * ( Math.ceil(x * 1000) + 1 ); + return Math.ceil(y / 1000000) + 1; +} + +function norm_s(x) { + let y = Math.ceil(x / 600) + 1; + y = y*y; + return Math.ceil(y / 1000000) + 1; +} + + // any channel thing used here should be added to channel context function getWatermark( ffmpegSettings, channel, type) { if (! ffmpegSettings.enableFFMPEGTranscoding || ffmpegSettings.disableChannelOverlay ) { @@ -319,7 +326,10 @@ function generateChannelContext(channel) { let channelContext = {}; for (let i = 0; i < CHANNEL_CONTEXT_KEYS.length; i++) { let key = CHANNEL_CONTEXT_KEYS[i]; - channelContext[key] = JSON.parse( JSON.stringify(channel[key] ) ); + + if (typeof(channel[key]) !== 'undefined') { + channelContext[key] = JSON.parse( JSON.stringify(channel[key] ) ); + } } return channelContext; } diff --git a/src/services/get-show-data.js b/src/services/get-show-data.js index 99d44b1..dec2a33 100644 --- a/src/services/get-show-data.js +++ b/src/services/get-show-data.js @@ -12,6 +12,7 @@ module.exports = function () { showId : "custom." + program.customShowId, showDisplayName : program.customShowName, order : program.customOrder, + shuffleOrder : program.shuffleOrder, } } else if (program.isOffline && program.type === 'redirect') { return { @@ -35,6 +36,7 @@ module.exports = function () { showId : "movie.", showDisplayName : "Movies", order : movieTitleOrder[key], + shuffleOrder : program.shuffleOrder, } } else if ( (program.type === 'episode') || (program.type === 'track') ) { let s = 0; @@ -54,6 +56,7 @@ module.exports = function () { showId : prefix + program.showTitle, showDisplayName : program.showTitle, order : s * 1000000 + e, + shuffleOrder : program.shuffleOrder, } } else { return { diff --git a/src/services/m3u-service.js b/src/services/m3u-service.js index c58c536..97cbac1 100644 --- a/src/services/m3u-service.js +++ b/src/services/m3u-service.js @@ -43,7 +43,7 @@ class M3uService { channels.sort((a, b) => { - return a.number < b.number ? -1 : 1 + return parseInt(a.number) < parseInt(b.number) ? -1 : 1 }); const tvg = `{{host}}/api/xmltv.xml`; diff --git a/src/services/on-demand-service.js b/src/services/on-demand-service.js index c2ac3cd..8f9dec7 100644 --- a/src/services/on-demand-service.js +++ b/src/services/on-demand-service.js @@ -197,7 +197,9 @@ class OnDemandService } else { let o = (tm - pm); startTime = startTime - o; - if (o >= SLACK) { + //It looks like it is convenient to make the on-demand a bit more lenient SLACK-wise tha + //other parts of the schedule process. So SLACK*2 instead of just SLACK + if (o >= SLACK*2) { startTime += onDemand.modulo; } } diff --git a/src/services/random-slots-service.js b/src/services/random-slots-service.js index d7c149b..8be4b72 100644 --- a/src/services/random-slots-service.js +++ b/src/services/random-slots-service.js @@ -2,7 +2,7 @@ const constants = require("../constants"); const getShowData = require("./get-show-data")(); const random = require('../helperFuncs').random; const throttle = require('./throttle'); - +const orderers = require("./show-orderers"); const MINUTE = 60*1000; const DAY = 24*60*MINUTE; @@ -22,29 +22,6 @@ function getShow(program) { } } - -function shuffle(array, lo, hi ) { - if (typeof(lo) === 'undefined') { - lo = 0; - hi = array.length; - } - let currentIndex = hi, temporaryValue, randomIndex - while (lo !== currentIndex) { - randomIndex = random.integer(lo, currentIndex-1); - currentIndex -= 1 - temporaryValue = array[currentIndex] - array[currentIndex] = array[randomIndex] - array[randomIndex] = temporaryValue - } - return array -} - -function _wait(t) { - return new Promise((resolve) => { - setTimeout(resolve, t); - }); -} - function getProgramId(program) { let s = program.serverKey; if (typeof(s) === 'undefined') { @@ -69,78 +46,6 @@ function addProgramToShow(show, program) { } } -function getShowOrderer(show) { - if (typeof(show.orderer) === 'undefined') { - - let sortedPrograms = JSON.parse( JSON.stringify(show.programs) ); - sortedPrograms.sort((a, b) => { - let showA = getShowData(a); - let showB = getShowData(b); - return showA.order - showB.order; - }); - - let position = 0; - while ( - (position + 1 < sortedPrograms.length ) - && - ( - getShowData(show.founder).order - !== - getShowData(sortedPrograms[position]).order - ) - ) { - position++; - } - - - show.orderer = { - - current : () => { - return sortedPrograms[position]; - }, - - next: () => { - position = (position + 1) % sortedPrograms.length; - }, - - } - } - return show.orderer; -} - - -function getShowShuffler(show) { - if (typeof(show.shuffler) === 'undefined') { - if (typeof(show.programs) === 'undefined') { - throw Error(show.id + " has no programs?") - } - - let randomPrograms = JSON.parse( JSON.stringify(show.programs) ); - let n = randomPrograms.length; - shuffle( randomPrograms, 0, n); - let position = 0; - - show.shuffler = { - - current : () => { - return randomPrograms[position]; - }, - - next: () => { - position++; - if (position == n) { - let a = Math.floor(n / 2); - shuffle(randomPrograms, 0, a ); - shuffle(randomPrograms, a, n ); - position = 0; - } - }, - - } - } - return show.shuffler; -} - module.exports = async( programs, schedule ) => { if (! Array.isArray(programs) ) { return { userError: 'Expected a programs array' }; @@ -192,9 +97,6 @@ module.exports = async( programs, schedule ) => { } let flexBetween = ( schedule.flexPreference !== "end" ); - // throttle so that the stream is not affected negatively - let steps = 0; - let showsById = {}; let shows = []; @@ -216,9 +118,9 @@ module.exports = async( programs, schedule ) => { channel: show.channel, } } else if (slot.order === 'shuffle') { - return getShowShuffler(show).current(); + return orderers.getShowShuffler(show).current(); } else if (slot.order === 'next') { - return getShowOrderer(show).current(); + return orderers.getShowOrderer(show).current(); } } @@ -228,9 +130,9 @@ module.exports = async( programs, schedule ) => { } let show = shows[ showsById[slot.showId] ]; if (slot.order === 'shuffle') { - return getShowShuffler(show).next(); + return orderers.getShowShuffler(show).next(); } else if (slot.order === 'next') { - return getShowOrderer(show).next(); + return orderers.getShowOrderer(show).next(); } } diff --git a/src/services/show-orderers.js b/src/services/show-orderers.js new file mode 100644 index 0000000..06af2e8 --- /dev/null +++ b/src/services/show-orderers.js @@ -0,0 +1,156 @@ +const random = require('../helperFuncs').random; +const getShowData = require("./get-show-data")(); +const randomJS = require("random-js"); +const Random = randomJS.Random; + + + +/**** + * + * Code shared by random slots and time slots for keeping track of the order + * of episodes + * + **/ +function shuffle(array, lo, hi, randomOverride ) { + let r = randomOverride; + if (typeof(r) === 'undefined') { + r = random; + } + if (typeof(lo) === 'undefined') { + lo = 0; + hi = array.length; + } + let currentIndex = hi, temporaryValue, randomIndex + while (lo !== currentIndex) { + randomIndex = r.integer(lo, currentIndex-1); + currentIndex -= 1 + temporaryValue = array[currentIndex] + array[currentIndex] = array[randomIndex] + array[randomIndex] = temporaryValue + } + return array +} + + +function getShowOrderer(show) { + if (typeof(show.orderer) === 'undefined') { + + let sortedPrograms = JSON.parse( JSON.stringify(show.programs) ); + sortedPrograms.sort((a, b) => { + let showA = getShowData(a); + let showB = getShowData(b); + return showA.order - showB.order; + }); + + let position = 0; + while ( + (position + 1 < sortedPrograms.length ) + && + ( + getShowData(show.founder).order + !== + getShowData(sortedPrograms[position]).order + ) + ) { + position++; + } + + + show.orderer = { + + current : () => { + return sortedPrograms[position]; + }, + + next: () => { + position = (position + 1) % sortedPrograms.length; + }, + + } + } + return show.orderer; +} + + +function getShowShuffler(show) { + if (typeof(show.shuffler) === 'undefined') { + if (typeof(show.programs) === 'undefined') { + throw Error(show.id + " has no programs?") + } + + let sortedPrograms = JSON.parse( JSON.stringify(show.programs) ); + sortedPrograms.sort((a, b) => { + let showA = getShowData(a); + let showB = getShowData(b); + return showA.order - showB.order; + }); + let n = sortedPrograms.length; + + let splitPrograms = []; + let randomPrograms = []; + + for (let i = 0; i < n; i++) { + splitPrograms.push( sortedPrograms[i] ); + randomPrograms.push( {} ); + } + + + let showId = getShowData(show.programs[0]).showId; + + let position = show.founder.shuffleOrder; + if (typeof(position) === 'undefined') { + position = 0; + } + + let localRandom = null; + + let initGeneration = (generation) => { + let seed = []; + for (let i = 0 ; i < show.showId.length; i++) { + seed.push( showId.charCodeAt(i) ); + } + seed.push(generation); + + localRandom = new Random( randomJS.MersenneTwister19937.seedWithArray(seed) ) + + if (generation == 0) { + shuffle( splitPrograms, 0, n , localRandom ); + } + for (let i = 0; i < n; i++) { + randomPrograms[i] = splitPrograms[i]; + } + let a = Math.floor(n / 2); + shuffle( randomPrograms, 0, a, localRandom ); + shuffle( randomPrograms, a, n, localRandom ); + }; + initGeneration(0); + let generation = Math.floor( position / n ); + initGeneration( generation ); + + show.shuffler = { + + current : () => { + let prog = JSON.parse( + JSON.stringify(randomPrograms[position % n] ) + ); + prog.shuffleOrder = position; + return prog; + }, + + next: () => { + position++; + if (position % n == 0) { + let generation = Math.floor( position / n ); + initGeneration( generation ); + } + }, + + } + } + return show.shuffler; +} + +module.exports = { + getShowOrderer : getShowOrderer, + getShowShuffler: getShowShuffler, +} \ No newline at end of file diff --git a/src/services/time-slots-service.js b/src/services/time-slots-service.js index fe6b6a8..1fb8fe6 100644 --- a/src/services/time-slots-service.js +++ b/src/services/time-slots-service.js @@ -4,6 +4,7 @@ const constants = require("../constants"); const getShowData = require("./get-show-data")(); const random = require('../helperFuncs').random; const throttle = require('./throttle'); +const orderers = require("./show-orderers"); const MINUTE = 60*1000; const DAY = 24*60*MINUTE; @@ -22,28 +23,6 @@ function getShow(program) { } } -function shuffle(array, lo, hi ) { - if (typeof(lo) === 'undefined') { - lo = 0; - hi = array.length; - } - let currentIndex = hi, temporaryValue, randomIndex - while (lo !== currentIndex) { - randomIndex = random.integer(lo, currentIndex-1); - currentIndex -= 1 - temporaryValue = array[currentIndex] - array[currentIndex] = array[randomIndex] - array[randomIndex] = temporaryValue - } - return array -} - -function _wait(t) { - return new Promise((resolve) => { - setTimeout(resolve, t); - }); -} - function getProgramId(program) { let s = program.serverKey; if (typeof(s) === 'undefined') { @@ -68,78 +47,6 @@ function addProgramToShow(show, program) { } } -function getShowOrderer(show) { - if (typeof(show.orderer) === 'undefined') { - - let sortedPrograms = JSON.parse( JSON.stringify(show.programs) ); - sortedPrograms.sort((a, b) => { - let showA = getShowData(a); - let showB = getShowData(b); - return showA.order - showB.order; - }); - - let position = 0; - while ( - (position + 1 < sortedPrograms.length ) - && - ( - getShowData(show.founder).order - !== - getShowData(sortedPrograms[position]).order - ) - ) { - position++; - } - - - show.orderer = { - - current : () => { - return sortedPrograms[position]; - }, - - next: () => { - position = (position + 1) % sortedPrograms.length; - }, - - } - } - return show.orderer; -} - - -function getShowShuffler(show) { - if (typeof(show.shuffler) === 'undefined') { - if (typeof(show.programs) === 'undefined') { - throw Error(show.id + " has no programs?") - } - - let randomPrograms = JSON.parse( JSON.stringify(show.programs) ); - let n = randomPrograms.length; - shuffle( randomPrograms, 0, n); - let position = 0; - - show.shuffler = { - - current : () => { - return randomPrograms[position]; - }, - - next: () => { - position++; - if (position == n) { - let a = Math.floor(n / 2); - shuffle(randomPrograms, 0, a ); - shuffle(randomPrograms, a, n ); - position = 0; - } - }, - - } - } - return show.shuffler; -} - module.exports = async( programs, schedule ) => { if (! Array.isArray(programs) ) { return { userError: 'Expected a programs array' }; @@ -224,9 +131,9 @@ module.exports = async( programs, schedule ) => { channel: show.channel, } } else if (slot.order === 'shuffle') { - return getShowShuffler(show).current(); + return orderers.getShowShuffler(show).current(); } else if (slot.order === 'next') { - return getShowOrderer(show).current(); + return orderers.getShowOrderer(show).current(); } } @@ -236,9 +143,9 @@ module.exports = async( programs, schedule ) => { } let show = shows[ showsById[slot.showId] ]; if (slot.order === 'shuffle') { - return getShowShuffler(show).next(); + return orderers.getShowShuffler(show).next(); } else if (slot.order === 'next') { - return getShowOrderer(show).next(); + return orderers.getShowOrderer(show).next(); } } diff --git a/src/services/tv-guide-service.js b/src/services/tv-guide-service.js index 09e3e8f..ead286b 100644 --- a/src/services/tv-guide-service.js +++ b/src/services/tv-guide-service.js @@ -36,7 +36,15 @@ class TVGuideService extends events.EventEmitter let t = (new Date()).getTime(); this.updateTime = t; this.updateLimit = t + limit; - let channels = inputChannels; + + let channels = []; + for (let i = 0; i < inputChannels.length; i++) { + if (typeof(inputChannels[i]) !== 'undefined') { + channels.push(inputChannels[i]); + } else { + console.error(`There is an issue with one of the channels provided to TV-guide service, it will be ignored: ${i}` ); + } + } this.updateChannels = channels; return t; } diff --git a/src/throttler.js b/src/throttler.js index ae660f5..ac42a11 100644 --- a/src/throttler.js +++ b/src/throttler.js @@ -13,29 +13,38 @@ function equalItems(a, b) { function wereThereTooManyAttempts(sessionId, lineupItem) { - let obj = cache[sessionId]; + let t1 = (new Date()).getTime(); - if (typeof(obj) === 'undefined') { + + let previous = cache[sessionId]; + if (typeof(previous) === 'undefined') { previous = cache[sessionId] = { - t0: t1 - constants.TOO_FREQUENT * 5 + t0: t1 - constants.TOO_FREQUENT * 5, + lineupItem: null, }; - - } else { - clearTimeout(obj.timer); } - previous.timer = setTimeout( () => { - cache[sessionId].timer = null; - delete cache[sessionId]; - }, constants.TOO_FREQUENT*5 ); - + let result = false; - - if (previous.t0 + constants.TOO_FREQUENT >= t1) { + if (t1 - previous.t0 < constants.TOO_FREQUENT) { //certainly too frequent result = equalItems( previous.lineupItem, lineupItem ); } - cache[sessionId].t0 = t1; - cache[sessionId].lineupItem = lineupItem; + + cache[sessionId] = { + t0: t1, + lineupItem : lineupItem, + }; + + setTimeout( () => { + if ( + (typeof(cache[sessionId]) !== 'undefined') + && + (cache[sessionId].t0 === t1) + ) { + delete cache[sessionId]; + } + }, constants.TOO_FREQUENT * 5 ); + return result; } diff --git a/src/video.js b/src/video.js index 5dd1927..fb6fefa 100644 --- a/src/video.js +++ b/src/video.js @@ -2,11 +2,11 @@ const express = require('express') const helperFuncs = require('./helperFuncs') const FFMPEG = require('./ffmpeg') const FFMPEG_TEXT = require('./ffmpegText') +const constants = require('./constants') const fs = require('fs') const ProgramPlayer = require('./program-player'); const channelCache = require('./channel-cache') const wereThereTooManyAttempts = require('./throttler'); -const constants = require('./constants'); module.exports = { router: video, shutdown: shutdown } @@ -131,7 +131,7 @@ function video( channelService, fillerDB, db, programmingService, activeChannelS } ); // Stream individual video to ffmpeg concat above. This is used by the server, NOT the client - router.get('/stream', async (req, res) => { + let streamFunction = async (req, res, t0, allowSkip) => { if (stopPlayback) { res.status(503).send("Server is shutting down.") return; @@ -180,7 +180,6 @@ function video( channelService, fillerDB, db, programmingService, activeChannelS // Get video lineup (array of video urls with calculated start times and durations.) - let t0 = (new Date()).getTime(); let lineupItem = channelCache.getCurrentLineupItem( channel.number, t0); let prog = null; let brandChannel = channel; @@ -261,12 +260,15 @@ function video( channelService, fillerDB, db, programmingService, activeChannelS duration: t, isOffline : true, }; - } else if (prog.program.isOffline && prog.program.duration - prog.timeElapsed <= 10000) { + } else if ( allowSkip && (prog.program.isOffline && prog.program.duration - prog.timeElapsed <= constants.SLACK + 1) ) { //it's pointless to show the offline screen for such a short time, might as well //skip to the next program - prog.programIndex = (prog.programIndex + 1) % channel.programs.length; - prog.program = channel.programs[prog.programIndex ]; - prog.timeElapsed = 0; + let dt = prog.program.duration - prog.timeElapsed; + for (let i = 0; i < redirectChannels.length; i++) { + channelCache.clearPlayback(redirectChannels[i].number ); + } + console.log("Too litlle time before the filler ends, skip to next slot"); + return await streamFunction(req, res, t0 + dt + 1, false); } if ( (prog == null) || (typeof(prog) === 'undefined') || (prog.program == null) || (typeof(prog.program) == "undefined") ) { throw "No video to play, this means there's a serious unexpected bug or the channel db is corrupted." @@ -319,6 +321,7 @@ function video( channelService, fillerDB, db, programmingService, activeChannelS channelCache.recordPlayback(channel.number, t0, lineupItem); } if (wereThereTooManyAttempts(session, lineupItem)) { + console.error("There are too many attempts to play the same item in a short period of time, playing the error stream instead."); lineupItem = { isOffline: true, err: Error("Too many attempts, throttling.."), @@ -443,6 +446,11 @@ function video( channelService, fillerDB, db, programmingService, activeChannelS console.log("Client Closed"); stop(); }); + }; + + router.get('/stream', async (req, res) => { + let t0 = (new Date).getTime(); + return await streamFunction(req, res, t0, true); }); diff --git a/src/xmltv.js b/src/xmltv.js index 5ca97cf..4b02f5a 100644 --- a/src/xmltv.js +++ b/src/xmltv.js @@ -51,7 +51,7 @@ function writePromise(json, xmlSettings, throttle, cacheImageService) { function _writeDocStart(xw) { xw.startDocument() xw.startElement('tv') - xw.writeAttribute('generator-info-name', 'psuedotv-plex') + xw.writeAttribute('generator-info-name', 'dizquetv') } function _writeDocEnd(xw, ws) { xw.endElement() diff --git a/web/controllers/version.js b/web/controllers/version.js index 7fedd00..5b5fe83 100644 --- a/web/controllers/version.js +++ b/web/controllers/version.js @@ -4,6 +4,7 @@ module.exports = function ($scope, dizquetv) { dizquetv.getVersion().then((version) => { $scope.version = version.dizquetv; $scope.ffmpegVersion = version.ffmpeg; + $scope.nodejs = version.nodejs; }) diff --git a/web/directives/channel-config.js b/web/directives/channel-config.js index 1bdec64..41a0f35 100644 --- a/web/directives/channel-config.js +++ b/web/directives/channel-config.js @@ -989,7 +989,7 @@ module.exports = function ($timeout, $location, dizquetv, resolutionOptions, get scope.error.any = true; - if (typeof channel.number === "undefined" || channel.number === null || channel.number === "") { + if (typeof channel.number === "undefined" || channel.number === null || channel.number === "" ) { scope.error.number = "Select a channel number" scope.error.tab = "basic"; } else if (channelNumbers.indexOf(parseInt(channel.number, 10)) !== -1 && scope.isNewChannel) { // we need the parseInt for indexOf to work properly @@ -998,6 +998,9 @@ module.exports = function ($timeout, $location, dizquetv, resolutionOptions, get } else if (!scope.isNewChannel && channel.number !== scope.beforeEditChannelNumber && channelNumbers.indexOf(parseInt(channel.number, 10)) !== -1) { scope.error.number = "Channel number already in use." scope.error.tab = "basic"; + } else if ( ! checkChannelNumber(channel.number) ) { + scope.error.number = "Invalid channel number."; + scope.error.tab = "basic"; } else if (channel.number < 0 || channel.number > 9999) { scope.error.name = "Enter a valid number (0-9999)" scope.error.tab = "basic"; @@ -1462,6 +1465,10 @@ module.exports = function ($timeout, $location, dizquetv, resolutionOptions, get scope.videoRateDefault = "(Use global setting)"; scope.videoBufSizeDefault = "(Use global setting)"; + scope.randomizeBlockShuffle = false; + + scope.advancedTools = (localStorage.getItem("channel-programming-advanced-tools" ) === "show"); + let refreshScreenResolution = async () => { @@ -1650,13 +1657,21 @@ module.exports = function ($timeout, $location, dizquetv, resolutionOptions, get scope.onTimeSlotsDone = (slotsResult) => { - scope.channel.scheduleBackup = slotsResult.schedule; - readSlotsResult(slotsResult); + if (slotsResult === null) { + delete scope.channel.scheduleBackup; + } else { + scope.channel.scheduleBackup = slotsResult.schedule; + readSlotsResult(slotsResult); + } } scope.onRandomSlotsDone = (slotsResult) => { - scope.channel.randomScheduleBackup = slotsResult.schedule; - readSlotsResult(slotsResult); + if (slotsResult === null) { + delete scope.channel.randomScheduleBackup; + } else { + scope.channel.randomScheduleBackup = slotsResult.schedule; + readSlotsResult(slotsResult); + } } @@ -1669,6 +1684,73 @@ module.exports = function ($timeout, $location, dizquetv, resolutionOptions, get scope.randomSlots.startDialog(progs, scope.maxSize, scope.channel.randomScheduleBackup ); } + scope.rerollRandomSlots = () => { + let progs = commonProgramTools.removeDuplicates( scope.channel.programs ); + scope.randomSlots.startDialog( + progs, scope.maxSize, scope.channel.randomScheduleBackup, + true + ); + } + scope.hasNoRandomSlots = () => { + return ( + (typeof(scope.channel.randomScheduleBackup) === 'undefined' ) + || + (scope.channel.randomScheduleBackup == null) + ); + } + + scope.rerollTimeSlots = () => { + let progs = commonProgramTools.removeDuplicates( scope.channel.programs ); + scope.timeSlots.startDialog( + progs, scope.maxSize, scope.channel.scheduleBackup, + true + ); + } + scope.hasNoTimeSlots = () => { + return ( + (typeof(scope.channel.scheduleBackup) === 'undefined' ) + || + (scope.channel.scheduleBackup == null) + ); + } + scope.toggleAdvanced = () => { + scope.advancedTools = ! scope.advancedTools; + localStorage.setItem("channel-programming-advanced-tools" , scope.advancedTools ? "show" : "hide"); + } + scope.hasAdvancedTools = () => { + return scope.advancedTools; + } + + scope.toolWide = () => { + if ( scope.hasAdvancedTools()) { + return { + "col-xl-6": true, + "col-md-12" : true + } + } else { + return { + "col-xl-12": true, + "col-lg-12" : true + } + } + } + + scope.toolThin = () => { + if ( scope.hasAdvancedTools()) { + return { + "col-xl-3": true, + "col-lg-6" : true + } + } else { + return { + "col-xl-6": true, + "col-lg-6" : true + } + } + } + + + scope.logoOnChange = (event) => { const formData = new FormData(); formData.append('image', event.target.files[0]); @@ -1706,3 +1788,12 @@ module.exports = function ($timeout, $location, dizquetv, resolutionOptions, get function validURL(url) { return /^(ftp|http|https):\/\/[^ "]+$/.test(url); } + +function checkChannelNumber(number) { + if ( /^(([1-9][0-9]*)|(0))$/.test(number) ) { + let x = parseInt(number); + return (0 <= x && x < 10000); + } else { + return false; + } +} diff --git a/web/directives/plex-library.js b/web/directives/plex-library.js index ce1018a..23f5e25 100644 --- a/web/directives/plex-library.js +++ b/web/directives/plex-library.js @@ -123,8 +123,19 @@ module.exports = function (plex, dizquetv, $timeout, commonProgramTools) { } scope.fillNestedIfNecessary = async (x, isLibrary) => { - if ( (typeof(x.nested) === 'undefined') && (x.type !== 'collection') ) { + if (typeof(x.nested) === 'undefined') { x.nested = await plex.getNested(scope.plexServer, x, isLibrary, scope.errors); + if (x.type === "collection" && x.collectionType === "show") { + let nested = x.nested; + x.nested = []; + for (let i = 0; i < nested.length; i++) { + let subNested = await plex.getNested(scope.plexServer, nested[i], false, scope.errors); + for (let j = 0; j < subNested.length; j++) { + subNested[j].title = nested[i].title + " - " + subNested[j].title; + x.nested.push( subNested[j] ); + } + } + } } } scope.getNested = (list, isLibrary) => { diff --git a/web/directives/plex-settings.js b/web/directives/plex-settings.js index f9714b1..53a51a2 100644 --- a/web/directives/plex-settings.js +++ b/web/directives/plex-settings.js @@ -192,7 +192,7 @@ module.exports = function (plex, dizquetv, $timeout) { accessToken: server.accessToken, } } - connection.arGuide = true + connection.arGuide = false connection.arChannels = false // should not be enabled unless dizqueTV tuner already added to plex await dizquetv.addPlexServer(connection); } catch (err) { diff --git a/web/directives/random-slots-schedule-editor.js b/web/directives/random-slots-schedule-editor.js index 0f88017..24fbc98 100644 --- a/web/directives/random-slots-schedule-editor.js +++ b/web/directives/random-slots-schedule-editor.js @@ -177,8 +177,22 @@ module.exports = function ($timeout, dizquetv, getShowData) { { id: "shuffle", description: "Shuffle" }, ]; - let doIt = async() => { + let doWait = (millis) => { + return new Promise( (resolve) => { + $timeout( resolve, millis ); + } ); + } + + let doIt = async(fromInstant) => { + let t0 = new Date().getTime(); let res = await dizquetv.calculateRandomSlots(scope.programs, scope.schedule ); + let t1 = new Date().getTime(); + + let w = Math.max(0, 250 - (t1 - t0) ); + if (fromInstant && (w > 0) ) { + await doWait(w); + } + for (let i = 0; i < scope.schedule.slots.length; i++) { delete scope.schedule.slots[i].weightPercentage; } @@ -189,7 +203,7 @@ module.exports = function ($timeout, dizquetv, getShowData) { - let startDialog = (programs, limit, backup) => { + let startDialog = (programs, limit, backup, instant) => { scope.limit = limit; scope.programs = programs; @@ -213,11 +227,15 @@ module.exports = function ($timeout, dizquetv, getShowData) { id: "flex.", description: "Flex", } ); - if (typeof(backup) !== 'undefined') { + scope.hadBackup = (typeof(backup) !== 'undefined'); + if (scope.hadBackup) { loadBackup(backup); } scope.visible = true; + if (instant) { + scope.finished(false, true); + } } @@ -225,13 +243,18 @@ module.exports = function ($timeout, dizquetv, getShowData) { startDialog: startDialog, } ); - scope.finished = async (cancel) => { + scope.finished = async (cancel, fromInstant) => { scope.error = null; if (!cancel) { + if ( scope.schedule.slots.length === 0) { + scope.onDone(null); + scope.visible = false; + return; + } try { scope.loading = true; $timeout(); - scope.onDone( await doIt() ); + scope.onDone( await doIt(fromInstant) ); scope.visible = false; } catch(err) { console.error("Unable to generate channel lineup", err); @@ -267,6 +290,20 @@ module.exports = function ($timeout, dizquetv, getShowData) { return false; } + scope.hideCreateLineup = () => { + return ( + scope.disableCreateLineup() + && (scope.schedule.slots.length == 0) + && scope.hadBackup + ); + } + + scope.showResetSlots = () => { + return scope.hideCreateLineup(); + } + + + scope.canShowSlot = (slot) => { return (slot.showId != 'flex.') && !(slot.showId.startsWith('redirect.')); } diff --git a/web/directives/time-slots-schedule-editor.js b/web/directives/time-slots-schedule-editor.js index 45b2522..90375f7 100644 --- a/web/directives/time-slots-schedule-editor.js +++ b/web/directives/time-slots-schedule-editor.js @@ -203,9 +203,23 @@ module.exports = function ($timeout, dizquetv, getShowData ) { { id: "shuffle", description: "Shuffle" }, ]; - let doIt = async() => { + let doWait = (millis) => { + return new Promise( (resolve) => { + $timeout( resolve, millis ); + } ); + } + + let doIt = async(fromInstant) => { scope.schedule.timeZoneOffset = (new Date()).getTimezoneOffset(); + let t0 = new Date().getTime(); let res = await dizquetv.calculateTimeSlots(scope.programs, scope.schedule ); + let t1 = new Date().getTime(); + + let w = Math.max(0, 250 - (t1 - t0) ); + if (fromInstant && (w > 0) ) { + await doWait(w); + } + res.schedule = scope.schedule; delete res.schedule.fake; return res; @@ -214,7 +228,7 @@ module.exports = function ($timeout, dizquetv, getShowData ) { - let startDialog = (programs, limit, backup) => { + let startDialog = (programs, limit, backup, instant) => { scope.limit = limit; scope.programs = programs; @@ -238,11 +252,15 @@ module.exports = function ($timeout, dizquetv, getShowData ) { id: "flex.", description: "Flex", } ); - if (typeof(backup) !== 'undefined') { + scope.hadBackup = (typeof(backup) !== 'undefined'); + if (scope.hadBackup) { loadBackup(backup); } scope.visible = true; + if (instant) { + scope.finished(false, true); + } } @@ -250,13 +268,19 @@ module.exports = function ($timeout, dizquetv, getShowData ) { startDialog: startDialog, } ); - scope.finished = async (cancel) => { + scope.finished = async (cancel, fromInstant) => { scope.error = null; if (!cancel) { + if ( scope.schedule.slots.length === 0) { + scope.onDone(null); + scope.visible = false; + return; + } + try { scope.loading = true; $timeout(); - scope.onDone( await doIt() ); + scope.onDone( await doIt(fromInstant) ); scope.visible = false; } catch(err) { console.error("Unable to generate channel lineup", err); @@ -292,6 +316,18 @@ module.exports = function ($timeout, dizquetv, getShowData ) { return false; } + scope.hideCreateLineup = () => { + return ( + scope.disableCreateLineup() + && (scope.schedule.slots.length == 0) + && scope.hadBackup + ); + } + + scope.showResetSlots = () => { + return scope.hideCreateLineup(); + } + scope.canShowSlot = (slot) => { return (slot.showId != 'flex.') && !(slot.showId.startsWith('redirect.')); } diff --git a/web/public/templates/channel-config.html b/web/public/templates/channel-config.html index da9a908..97dcddb 100644 --- a/web/public/templates/channel-config.html +++ b/web/public/templates/channel-config.html @@ -197,23 +197,29 @@ There are no programs in the channel, use the button to add programs from your media library or use the Tools to add Flex time or a Channel Redirect -
Makes multiple copies of the schedule and plays them in sequence. Normally this isn't necessary, because dizqueTV will always play the schedule back from the beginning when it finishes. But creating replicas is a useful intermediary step sometimes before applying other transformations. Note that because very large channels can be problematic, the number of replicas will be limited to avoid creating really large channels.
Like "Replicate", it will make multiple copies of the programming. In addition it will shuffle the programs, but it will make sure not to have too small a distance between two identical programs.
Will redirect to another channel while between the selected hours.
This allows to schedul specific shows to run at specific time slots of the day or a week. It's recommended you first populate the channel with the episodes from the shows you want to play and/or other content like movies and redirects.
+This allows to schedule specific shows to run at specific time slots of the day or a week. It's recommended you first populate the channel with the episodes from the shows you want to play and/or other content like movies and redirects.
This is similar to Time Slots, but instead of time sections, you pick a probability to play each tv show and the length of the block.
+This is similar to Time Slots, but instead of time sections, you pick a probability to play each tv show and the length of the block. Once a channel has been configured with random slots, the reload button can re-evaluate them again, with the saved settings.
Removes any Flex periods from the schedule.
Wipes out the schedule so that you can start over.
Use this button to show or hide a bunch of additional tools that might be useful.
+ + +