From 5fd21137e586f59b0ec6cd15804dbabaa5489383 Mon Sep 17 00:00:00 2001 From: vexorian Date: Sat, 11 Sep 2021 12:05:54 -0400 Subject: [PATCH 01/19] #363 m3u sorting. --- src/services/m3u-service.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/services/m3u-service.js b/src/services/m3u-service.js index f3563b5..95f150c 100644 --- a/src/services/m3u-service.js +++ b/src/services/m3u-service.js @@ -41,7 +41,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`; From 2fed574577dfc572af1ca1279f68cdd4f46be6a3 Mon Sep 17 00:00:00 2001 From: vexorian Date: Sun, 12 Sep 2021 22:26:16 -0400 Subject: [PATCH 02/19] Reworked the filler picked algorithm. Is it better? I don't know. But it is certainly different. --- src/helperFuncs.js | 59 ++++++++++++++++++++++++++++------------------ 1 file changed, 36 insertions(+), 23 deletions(-) diff --git a/src/helperFuncs.js b/src/helperFuncs.js index 4cf2301..dbbf273 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,23 @@ 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; - } + 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)) { + console.log(`${s} ${d} ${clip.title} picked `); + 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; - } - } + console.log(`${s} ${d} ${clip.title} not picked `); } } } } - let pick = (pick1 == null) ? pick2: pick1; + let pick = pick1; let pickTitle = "null"; if (pick != null) { pickTitle = pick.title; @@ -272,6 +266,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 +329,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; } From b0220b438b066df6df0cfe4407fd9e70d3ef211a Mon Sep 17 00:00:00 2001 From: vexorian Date: Mon, 13 Sep 2021 09:46:41 -0400 Subject: [PATCH 03/19] #365 Fix Libraries with many elements in multiple collections taking too long to load. --- web/directives/plex-library.js | 13 ++++- web/services/plex.js | 92 ++++++++-------------------------- 2 files changed, 33 insertions(+), 72 deletions(-) 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/services/plex.js b/web/services/plex.js index a990a0d..fc71ad8 100644 --- a/web/services/plex.js +++ b/web/services/plex.js @@ -172,13 +172,13 @@ module.exports = function ($http, $window, $interval) { var client = new Plex(server) const key = lib.key const res = await client.Get(key) - const size = res.Metadata !== 'undefined' ? res.Metadata.length : 0; + + const size = (typeof(res.Metadata) !== 'undefined') ? res.Metadata.length : 0; var nested = [] if (typeof (lib.genres) !== 'undefined') { nested = Array.from(lib.genres) } var seenFiles = {}; - var collections = {}; let albumKeys = {}; let albums = {}; @@ -276,43 +276,6 @@ module.exports = function ($http, $window, $interval) { program.episode = 1 program.season = 1 } - if (typeof (res.Metadata[i].Collection) !== 'undefined') { - let coll = res.Metadata[i].Collection; - if (coll.length == 2) { - // the /all endpoint returns incomplete data, so we - // might have to complete the list of collections - // when there are already 2 collections there. - //console.log(res.Metadata[i]); - let complete = {} - try { - complete = await client.Get(`/library/metadata/${res.Metadata[i].ratingKey}`); - } catch (err) { - console.error("Error attempting to load collections", err); - } - if ( - (typeof(complete.Metadata) !== 'undefined') - && - (complete.Metadata.length == 1) - && - (typeof(complete.Metadata[0].Collection) !== 'undefined') - && - ( complete.Metadata[0].Collection.length > 2) - ) { - coll = complete.Metadata[0].Collection; - } - } - for (let j = 0; j < coll.length; j++) { - let tag = coll[j].tag; - if ( (typeof(tag)!== "undefined") && (tag.length > 0) ) { - let collection = collections[tag]; - if (typeof(collection) === 'undefined') { - collection = []; - collections[tag] = collection; - } - collection.push( program ); - } - } - } nested.push(program) } catch(err) { let msg = "Error when attempting to read nested data for " + key + " " + res.Metadata[i].title; @@ -320,40 +283,27 @@ module.exports = function ($http, $window, $interval) { console.error(msg , err); } } - if (includeCollections === true) { + if ( (includeCollections === true) && (res.viewGroup !== "artist" ) ) { + let k = res.librarySectionID; + + k = `/library/sections/${k}/collection`; + let collections = await client.Get(k); + let directories = collections.Directory; let nestedCollections = []; - let keys = []; - Object.keys(collections).forEach(function(key,index) { - keys.push(key); - }); - for (let k = 0; k < keys.length; k++) { - let key = keys[k]; - if ( !(collections[key].length >= 1) ) { - //it's pointless to include it. - continue; + for (let i = 0; i < directories.length; i++) { + let title; + if (res.viewGroup === "show") { + title = directories[i].title + " Collection" + } else { + title = directories[i].title; } - let collection = { - title: key, - key: "#collection", - icon : "", - type : "collection", - nested: collections[key], - } - if (res.viewGroup === 'show') { - collection.title = collection.title + " Collection"; - //nest the seasons directly because that's way too many depth levels already - let shows = collection.nested; - let collectionContents = []; - for (let i = 0; i < shows.length; i++) { - let seasons = await exported.getNested(server, shows[i], false); - for (let j = 0; j < seasons.length; j++) { - seasons[j].title = shows[i].title + " - " + seasons[j].title; - collectionContents.push(seasons[j]); - } - } - collection.nested = collectionContents; - } - nestedCollections.push( collection ); + + nestedCollections.push( { + key : directories[i].fastKey, + title : title, + type: "collection", + collectionType : res.viewGroup, + } ); } nested = nestedCollections.concat(nested); } From 388bf11e1689f9f038f175ad71c3f8bcc22e8f17 Mon Sep 17 00:00:00 2001 From: vexorian Date: Sat, 18 Sep 2021 18:20:46 -0400 Subject: [PATCH 04/19] Remove some unnecessary logs --- src/helperFuncs.js | 3 --- 1 file changed, 3 deletions(-) diff --git a/src/helperFuncs.js b/src/helperFuncs.js index dbbf273..98e7552 100644 --- a/src/helperFuncs.js +++ b/src/helperFuncs.js @@ -243,10 +243,7 @@ function pickRandomWithMaxDuration(channel, fillers, maxDuration) { let w = s + d; n += w; if (weighedPick(w,n)) { - console.log(`${s} ${d} ${clip.title} picked `); pick1 = clip; - } else { - console.log(`${s} ${d} ${clip.title} not picked `); } } } From 84abfac78b27a08fa2f29d7b5d37bcc21a98f88d Mon Sep 17 00:00:00 2001 From: vexorian Date: Sun, 19 Sep 2021 00:53:17 -0400 Subject: [PATCH 05/19] Fix #369 : A case where flex can cut abruptly, redirect-related --- src/channel-cache.js | 5 +++++ src/video.js | 21 ++++++++++++++------- 2 files changed, 19 insertions(+), 7 deletions(-) diff --git a/src/channel-cache.js b/src/channel-cache.js index 3588c96..bc2dc6b 100644 --- a/src/channel-cache.js +++ b/src/channel-cache.js @@ -140,6 +140,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 = {}; @@ -156,4 +160,5 @@ module.exports = { getChannelConfig: getChannelConfig, saveChannelConfig: saveChannelConfig, getFillerLastPlayTime: getFillerLastPlayTime, + clearPlayback: clearPlayback, } diff --git a/src/video.js b/src/video.js index 9ed736d..5f7e347 100644 --- a/src/video.js +++ b/src/video.js @@ -2,7 +2,7 @@ const express = require('express') const helperFuncs = require('./helperFuncs') const FFMPEG = require('./ffmpeg') const FFMPEG_TEXT = require('./ffmpegText') -const PlexTranscoder = require('./plexTranscoder') +const constants = require('./constants') const fs = require('fs') const ProgramPlayer = require('./program-player'); const channelCache = require('./channel-cache') @@ -121,7 +121,7 @@ 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) => { + let streamFunction = async (req, res, t0, allowSkip) => { // Check if channel queried is valid res.on("error", (e) => { console.error("There was an unexpected error in stream.", e); @@ -166,7 +166,6 @@ function video( channelDB , fillerDB, db) { // 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; @@ -242,12 +241,15 @@ function video( channelDB , fillerDB, db) { 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." @@ -360,6 +362,11 @@ function video( channelDB , fillerDB, db) { console.log("Client Closed"); stop(); }); + }; + + router.get('/stream', async (req, res) => { + let t0 = (new Date).getTime(); + return await streamFunction(req, res, t0, true); }); From 460b552d3766cbd1862baaddd5cfa29c059e919e Mon Sep 17 00:00:00 2001 From: vexorian Date: Sun, 19 Sep 2021 00:53:46 -0400 Subject: [PATCH 06/19] New Collections browser now supports Smart Collections too! --- web/services/plex.js | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/web/services/plex.js b/web/services/plex.js index fc71ad8..35575ff 100644 --- a/web/services/plex.js +++ b/web/services/plex.js @@ -286,9 +286,12 @@ module.exports = function ($http, $window, $interval) { if ( (includeCollections === true) && (res.viewGroup !== "artist" ) ) { let k = res.librarySectionID; - k = `/library/sections/${k}/collection`; + k = `/library/sections/${k}/collections`; let collections = await client.Get(k); - let directories = collections.Directory; + if ( typeof(collections.Metadata) === 'undefined') { + collections.Metadata = []; + } + let directories = collections.Metadata; let nestedCollections = []; for (let i = 0; i < directories.length; i++) { let title; @@ -299,7 +302,7 @@ module.exports = function ($http, $window, $interval) { } nestedCollections.push( { - key : directories[i].fastKey, + key : directories[i].key, title : title, type: "collection", collectionType : res.viewGroup, From a56924463e2fcca9552e5d8e72609a24a0b56b4d Mon Sep 17 00:00:00 2001 From: vexorian Date: Sun, 19 Sep 2021 13:27:28 -0400 Subject: [PATCH 07/19] #356 Improve channel number validations, server and client-side. --- src/dao/channel-db.js | 28 ++++++++++++++++++++++------ web/directives/channel-config.js | 14 +++++++++++++- 2 files changed, 35 insertions(+), 7 deletions(-) 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/web/directives/channel-config.js b/web/directives/channel-config.js index 9885f29..b9008db 100644 --- a/web/directives/channel-config.js +++ b/web/directives/channel-config.js @@ -953,7 +953,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 @@ -962,6 +962,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"; @@ -1670,3 +1673,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]+$/.test(number) ) { + let x = parseInt(number); + return (0 <= x && x < 10000); + } else { + return false; + } +} From 395bc48c01673742f04b1906775abd3643683503 Mon Sep 17 00:00:00 2001 From: vexorian Date: Sun, 19 Sep 2021 13:28:14 -0400 Subject: [PATCH 08/19] TV Guide error retries with backoff so that they don't generate a tremendous amount of logs. Detect issues with channels provided to TV guide early-on. --- src/services/tv-guide-service.js | 18 +++++++++++++++--- 1 file changed, 15 insertions(+), 3 deletions(-) diff --git a/src/services/tv-guide-service.js b/src/services/tv-guide-service.js index cd3160d..6768b2f 100644 --- a/src/services/tv-guide-service.js +++ b/src/services/tv-guide-service.js @@ -11,6 +11,7 @@ class TVGuideService constructor(xmltv, db, cacheImageService, eventService) { this.cached = null; this.lastUpdate = 0; + this.lastBackoff = 100; this.updateTime = 0; this.currentUpdate = -1; this.currentLimit = -1; @@ -34,7 +35,15 @@ class TVGuideService 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; } @@ -345,10 +354,13 @@ class TVGuideService this.cached = await this.buildItManaged(); console.log("Internal TV Guide data refreshed at " + (new Date()).toLocaleString() ); await this.refreshXML(); + this.lastBackoff = 100; } catch(err) { console.error("Unable to update internal guide data", err); - await _wait(100); - console.error("Retrying TV guide..."); + let w = Math.min(this.lastBackoff * 2, 300000); + await _wait(w); + this.lastBackoff = w; + console.error(`Retrying TV guide after ${w} milliseconds wait...`); await this.buildIt(); } finally { From dcceb19a95a7043915cb33b45ce4b9b31717f204 Mon Sep 17 00:00:00 2001 From: vexorian Date: Sun, 19 Sep 2021 14:02:09 -0400 Subject: [PATCH 09/19] #336 make 'send guide updates' false by default. --- src/dao/plex-server-db.js | 4 ++-- src/database-migration.js | 2 +- web/directives/plex-settings.js | 2 +- 3 files changed, 4 insertions(+), 4 deletions(-) diff --git a/src/dao/plex-server-db.js b/src/dao/plex-server-db.js index 02d19ed..7d83af4 100644 --- a/src/dao/plex-server-db.js +++ b/src/dao/plex-server-db.js @@ -134,7 +134,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') { @@ -176,7 +176,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/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) { From 4c5739d659f0e965761d308773dafe6852b504b9 Mon Sep 17 00:00:00 2001 From: vexorian Date: Mon, 20 Sep 2021 09:24:08 -0400 Subject: [PATCH 10/19] 1.4.5-development --- README.md | 2 +- src/constants.js | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index fba70a9..f39d0d8 100644 --- a/README.md +++ b/README.md @@ -1,4 +1,4 @@ -# dizqueTV 1.4.4-development +# dizqueTV 1.4.5-development ![Discord](https://img.shields.io/discord/711313431457693727?logo=discord&logoColor=fff&style=flat-square) ![GitHub top language](https://img.shields.io/github/languages/top/vexorian/dizquetv?logo=github&style=flat-square) ![Docker Pulls](https://img.shields.io/docker/pulls/vexorian/dizquetv?logo=docker&logoColor=fff&style=flat-square) Create live TV channel streams from media on your Plex servers. diff --git a/src/constants.js b/src/constants.js index 75ac3f0..72afbba 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.4.4-development" + VERSION_NAME: "1.4.5-development" } From 7b1e00d54701557aa268252a7db31179bb9f619d Mon Sep 17 00:00:00 2001 From: vexorian Date: Sun, 19 Sep 2021 22:39:40 -0400 Subject: [PATCH 11/19] Fix #373 nvidia docker builds. --- Dockerfile-nvidia | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Dockerfile-nvidia b/Dockerfile-nvidia index f21f8b1..2b22dc9 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" ] From b7d61cb707778fa695d9f5b691fa1f0884d61eba Mon Sep 17 00:00:00 2001 From: vexorian Date: Sun, 19 Sep 2021 22:52:56 -0400 Subject: [PATCH 12/19] #374 Fix channel numbers smaller than 10 not being allowed. --- web/directives/channel-config.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/web/directives/channel-config.js b/web/directives/channel-config.js index b9008db..e0ce3ca 100644 --- a/web/directives/channel-config.js +++ b/web/directives/channel-config.js @@ -1675,7 +1675,7 @@ function validURL(url) { } function checkChannelNumber(number) { - if ( /^[1-9][0-9]+$/.test(number) ) { + if ( /^(([1-9][0-9]*)|(0))$/.test(number) ) { let x = parseInt(number); return (0 <= x && x < 10000); } else { From 0b1dc22e6c12861130156d9acafb00b7c8cc3208 Mon Sep 17 00:00:00 2001 From: vexorian Date: Mon, 20 Sep 2021 09:23:47 -0400 Subject: [PATCH 13/19] nodejs version warning and it also appears in version page. --- index.js | 6 ++++++ src/api.js | 1 + web/controllers/version.js | 1 + web/public/views/version.html | 7 ++++++- 4 files changed, 14 insertions(+), 1 deletion(-) diff --git a/index.js b/index.js index cfe1f39..7b8b5a3 100644 --- a/index.js +++ b/index.js @@ -35,6 +35,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 4adea09..f6a1f28 100644 --- a/src/api.js +++ b/src/api.js @@ -38,6 +38,7 @@ function api(db, channelDB, fillerDB, customShowDB, xmltvInterval, guideService res.send( { "dizquetv" : constants.VERSION_NAME, "ffmpeg" : v, + "nodejs" : process.version, } ); } catch(err) { console.error(err); 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/public/views/version.html b/web/public/views/version.html index 13b531c..01d77c5 100644 --- a/web/public/views/version.html +++ b/web/public/views/version.html @@ -20,7 +20,12 @@ FFMPEG
{{ffmpegVersion}} - + + nodejs +
{{nodejs}} + + + \ No newline at end of file From 1e2336d627cdbbfa08f3162423408fd4eebcda8c Mon Sep 17 00:00:00 2001 From: vexorian Date: Sun, 19 Sep 2021 16:57:56 -0400 Subject: [PATCH 14/19] Double the slack period to prevent rewinding on-demand channels when it is a bit late. --- src/services/on-demand-service.js | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) 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; } } From ee53210f2f1989ca9d960719bf33343273b4c7a2 Mon Sep 17 00:00:00 2001 From: vexorian Date: Mon, 20 Sep 2021 13:42:17 -0400 Subject: [PATCH 15/19] Random episode order in random/time slots is preserved in consecutive runs. --- src/api.js | 1 - src/services/get-show-data.js | 3 + src/services/random-slots-service.js | 108 +------------------ src/services/show-orderers.js | 156 +++++++++++++++++++++++++++ src/services/time-slots-service.js | 103 +----------------- web/services/common-program-tools.js | 4 +- 6 files changed, 172 insertions(+), 203 deletions(-) create mode 100644 src/services/show-orderers.js diff --git a/src/api.js b/src/api.js index f778059..dfe10ef 100644 --- a/src/api.js +++ b/src/api.js @@ -1038,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/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/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/web/services/common-program-tools.js b/web/services/common-program-tools.js index b851c21..907a6e5 100644 --- a/web/services/common-program-tools.js +++ b/web/services/common-program-tools.js @@ -59,7 +59,9 @@ module.exports = function (getShowData) { let data = getShowData(progs[i]); if (data.hasShow) { let key = data.showId + "|" + data.order; - tmpProgs[key] = progs[i]; + if (typeof(tmpProgs[key]) === 'undefined') { + tmpProgs[key] = progs[i]; + } } } } From 1e72f7354387c3c58a195c9d3c0f262ff31df29b Mon Sep 17 00:00:00 2001 From: vexorian Date: Tue, 21 Sep 2021 08:41:09 -0400 Subject: [PATCH 16/19] 1.4.6-development --- README.md | 2 +- src/constants.js | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index f39d0d8..dcd7600 100644 --- a/README.md +++ b/README.md @@ -1,4 +1,4 @@ -# dizqueTV 1.4.5-development +# dizqueTV 1.4.6-development ![Discord](https://img.shields.io/discord/711313431457693727?logo=discord&logoColor=fff&style=flat-square) ![GitHub top language](https://img.shields.io/github/languages/top/vexorian/dizquetv?logo=github&style=flat-square) ![Docker Pulls](https://img.shields.io/docker/pulls/vexorian/dizquetv?logo=docker&logoColor=fff&style=flat-square) Create live TV channel streams from media on your Plex servers. diff --git a/src/constants.js b/src/constants.js index 72afbba..4b82b5b 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.4.5-development" + VERSION_NAME: "1.4.6-development" } From 0ad1b163695a47beb3da165b7be8c07eb024e447 Mon Sep 17 00:00:00 2001 From: vexorian Date: Tue, 21 Sep 2021 00:11:45 -0400 Subject: [PATCH 17/19] Fix throttler bug --- src/constants.js | 2 +- src/throttler.js | 39 ++++++++++++++++++++++++--------------- src/video.js | 1 + 3 files changed, 26 insertions(+), 16 deletions(-) diff --git a/src/constants.js b/src/constants.js index 4b82b5b..79d990d 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, VERSION_NAME: "1.4.6-development" } diff --git a/src/throttler.js b/src/throttler.js index 543cbe9..31d1661 100644 --- a/src/throttler.js +++ b/src/throttler.js @@ -15,29 +15,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 5f7e347..14590e8 100644 --- a/src/video.js +++ b/src/video.js @@ -300,6 +300,7 @@ function video( channelDB , fillerDB, db) { 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.."), From 7b174e95a719647cdd368c456194093a11ce35ba Mon Sep 17 00:00:00 2001 From: vexorian Date: Tue, 21 Sep 2021 00:12:08 -0400 Subject: [PATCH 18/19] Fix generator name in xmltv --- src/xmltv.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) 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() From f18b853575c20240ebe95db16f44656643e61f93 Mon Sep 17 00:00:00 2001 From: vexorian Date: Tue, 21 Sep 2021 08:37:34 -0400 Subject: [PATCH 19/19] Channel Tools now have a compact version with less tools. Time/Random slots now have a reroll button to quickly refresh the schedule. Time/random slots data can be cleared from a channel. --- web/directives/channel-config.js | 87 +++++++++++++- .../random-slots-schedule-editor.js | 47 +++++++- web/directives/time-slots-schedule-editor.js | 46 +++++++- web/public/templates/channel-config.html | 106 +++++++++++++----- .../random-slots-schedule-editor.html | 3 +- .../templates/time-slots-schedule-editor.html | 3 +- 6 files changed, 247 insertions(+), 45 deletions(-) diff --git a/web/directives/channel-config.js b/web/directives/channel-config.js index db7cb5e..41a0f35 100644 --- a/web/directives/channel-config.js +++ b/web/directives/channel-config.js @@ -1465,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 () => { @@ -1653,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); + } } @@ -1672,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]); 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 -
-
+
-
- - -   -
+
-
+
-
+
-
+
@@ -255,7 +261,7 @@

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.

-
+
@@ -267,7 +273,7 @@

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.

-
+
-
+
-
+
-
+
-
+
Adds Flex breaks after each TV episode or movie to ensure that the program starts at one of the allowed minute marks. For example, you can use this to ensure that all your programs start at either XX:00 times or XX:30 times. Removes any existing Flex periods before adding the new ones. This button might be disabled if the channel is already too large.

-
+
Divides the programming in blocks of 6, 8 or 12 hours then repeats each of the blocks the specified number of times. For example, you can make a channel that plays exactly the same channels in the morning and in the afternoon. This button might be disabled if the channel is already too large.

-
+
-
+
@@ -425,7 +431,7 @@

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.

@@ -493,7 +523,7 @@

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.

+ + +
diff --git a/web/public/templates/random-slots-schedule-editor.html b/web/public/templates/random-slots-schedule-editor.html index 3cb0eff..80fa6d0 100644 --- a/web/public/templates/random-slots-schedule-editor.html +++ b/web/public/templates/random-slots-schedule-editor.html @@ -177,7 +177,8 @@
diff --git a/web/public/templates/time-slots-schedule-editor.html b/web/public/templates/time-slots-schedule-editor.html index 2f28bd0..ec8a416 100644 --- a/web/public/templates/time-slots-schedule-editor.html +++ b/web/public/templates/time-slots-schedule-editor.html @@ -148,7 +148,8 @@