From 51c978ce37094a1e200b9dbe6207e8b440b77e94 Mon Sep 17 00:00:00 2001 From: vexorian Date: Mon, 21 Sep 2020 21:17:46 -0400 Subject: [PATCH 01/12] Prepare 1.0.2 development --- README.md | 2 +- src/constants.js | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index be9e28c..3d65291 100644 --- a/README.md +++ b/README.md @@ -1,4 +1,4 @@ -# dizqueTV 1.0.1-prerelease +# dizqueTV 1.0.2-prerelease ![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 3c885d2..733ea56 100644 --- a/src/constants.js +++ b/src/constants.js @@ -4,5 +4,5 @@ module.exports = { STEALTH_DURATION: 5 * 60* 1000, TVGUIDE_MAXIMUM_FLEX_DURATION : 6 * 60 * 60 * 1000, - VERSION_NAME: "1.0.1-prerelease" + VERSION_NAME: "1.0.2-prerelease" } From a2bdb5c1ea01257b6c26ab1ff4b3fbd865891dd6 Mon Sep 17 00:00:00 2001 From: vexorian Date: Mon, 21 Sep 2020 21:17:01 -0400 Subject: [PATCH 02/12] #126 fix tool help --- web/public/templates/channel-config.html | 19 ++++++++----------- 1 file changed, 8 insertions(+), 11 deletions(-) diff --git a/web/public/templates/channel-config.html b/web/public/templates/channel-config.html index 98ffe4f..4ae8914 100644 --- a/web/public/templates/channel-config.html +++ b/web/public/templates/channel-config.html @@ -139,9 +139,6 @@
Sort Release Dates

Sorts everything by its release date. This will only work correctly if the release dates in Plex are correct. In case any item does not have a release date specified, it will be moved to the bottom.

-
Sort Release Dates
-

Sorts everything by its release date. This will only work correctly if the release dates in Plex are correct. In case any item does not have a release date specified, it will be moved to the bottom.

-
Balance Shows

Attempts to make the total amount of time each TV show appears in the programming as balanced as possible. This works by adding multiple copies of TV shows that have too little total time and by possibly removing duplicated episodes from TV shows that have too much total time. Note that in many situations it would be impossible to achieve perfect balance because channel duration is not infinite. Movies/Clips are treated as a single TV show. Note that this will most likely result in a larger channel and that having large channels makes some UI operations slower.

@@ -151,18 +148,22 @@
Add Flex

Adds a "Flex" Time Slot. Can be configured to play a fallback screen and/or random "filler" content (e.g "commercials", trailers, prerolls, countdowns, music videos, channel bumpers, etc.). Short Flex periods are hidden from the TV guide and are displayed as extensions to the previous program. Long Flex periods appear as the channel name in the TV guide. Normally this is not the best way to add Flex time, and you'd be better off using the Pad Times, Restrict Hours or Add Breaks features. This one is for adding specific, single instances of flex time.

-
Pad Times
-

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.

-
Restrict Hours

The channel's regular programming between the specified hours. Flex time will fill up the remaining hours.

+
Pad Times
+

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.

+
Add Breaks

Adds Flex breaks between programs, attempting to avoid groups of consecutive programs that exceed the specified number of minutes.

Reruns

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.

+
Save|Recover Episode Positions
+

The "Save" button saves the current episodes that are next to be played for each tv show. Then whenever you click the "Recover Episode Popsitions" button, episodes will be rearranged cyclically and they will start with the saved positions. So you can maintain episode sequences even after modifying the channel. If there are any new TV shows, they will start at their current positions. Movies and specials won't change positions. +

+
Replicate

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, there's a limit of 50000 programs to the size of the resulting channel when using this tool.

@@ -170,14 +171,10 @@

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.

-
Save|Recover Episode Positions
-

The "Save" button saves the current episodes that are next to be played for each tv show. Then whenever you click the "Recover Episode Popsitions" button, episodes will be rearranged cyclically and they will start with the saved positions. So you can maintain episode sequences even after modifying the channel. If there are any new TV shows, they will start at their current positions. Movies and specials won't change positions. -

-
Add Redirect

Adds a channel redirect. During this period of time, the channel will redirect to another channel.

-
"Channel at Night"
+
"Channel at Night"

Will redirect to another channel while between the selected hours.

From e9fe6001e199a3832ab248c511eeee5d43ccefee Mon Sep 17 00:00:00 2001 From: vexorian Date: Tue, 22 Sep 2020 12:17:08 -0400 Subject: [PATCH 03/12] Fix #132 Save Program not working at all. --- web/directives/channel-config.js | 5 +++-- web/directives/program-config.js | 10 ---------- web/public/templates/program-config.html | 2 +- 3 files changed, 4 insertions(+), 13 deletions(-) diff --git a/web/directives/channel-config.js b/web/directives/channel-config.js index 8734685..8a2f0ec 100644 --- a/web/directives/channel-config.js +++ b/web/directives/channel-config.js @@ -1080,8 +1080,9 @@ module.exports = function ($timeout, $location, dizquetv) { } scope.importPrograms = (selectedPrograms) => { - for (let i = 0, l = selectedPrograms.length; i < l; i++) - selectedPrograms[i].commercials = [] + for (let i = 0, l = selectedPrograms.length; i < l; i++) { + delete selectedPrograms[i].commercials; + } scope.channel.programs = scope.channel.programs.concat(selectedPrograms) updateChannelDuration() setTimeout( diff --git a/web/directives/program-config.js b/web/directives/program-config.js index ce50987..f274eb5 100644 --- a/web/directives/program-config.js +++ b/web/directives/program-config.js @@ -9,13 +9,6 @@ module.exports = function ($timeout) { onDone: "=onDone" }, link: function (scope, element, attrs) { - scope.selectedCommercials = (items) => { - scope.program.commercials = scope.program.commercials.concat(items) - for (let i = 0, l = scope.program.commercials.length; i < l; i++) { - if (typeof scope.program.commercials[i].commercialPosition === 'undefined') - scope.program.commercials[i].commercialPosition = 0 - } - } scope.finished = (prog) => { if (prog.title === "") scope.error = { title: 'You must set a program title.' } @@ -37,9 +30,6 @@ module.exports = function ($timeout) { return } - prog.duration = prog.duration - for (let i = 0, l = prog.commercials.length; i < l; i++) - prog.duration += prog.commercials[i].duration scope.onDone(JSON.parse(angular.toJson(prog))) scope.program = null } diff --git a/web/public/templates/program-config.html b/web/public/templates/program-config.html index 3577ea7..ce48dab 100644 --- a/web/public/templates/program-config.html +++ b/web/public/templates/program-config.html @@ -84,5 +84,5 @@ - + \ No newline at end of file From 43bf85db2053d333baf59100e014702956ab71ae Mon Sep 17 00:00:00 2001 From: vexorian Date: Wed, 23 Sep 2020 18:38:56 -0400 Subject: [PATCH 04/12] Fix #69 (nice?) Hopefully for good this time. It was already very difficult to create an infinite cache loop. Now even if the worst happens, it might repeat the last few seconds of a video once but nothing more. --- src/channel-cache.js | 15 +++++++++++++-- 1 file changed, 13 insertions(+), 2 deletions(-) diff --git a/src/channel-cache.js b/src/channel-cache.js index 681e99a..a0459f2 100644 --- a/src/channel-cache.js +++ b/src/channel-cache.js @@ -31,10 +31,21 @@ function getCurrentLineupItem(channelId, t1) { let recorded = cache[channelId]; let lineupItem = JSON.parse( JSON.stringify(recorded.lineupItem) ); let diff = t1 - recorded.t0; - if ( (diff <= SLACK) && (lineupItem.duration >= 2*SLACK) ) { + let rem = lineupItem.duration - lineupItem.start; + if (typeof(lineupItem.streamDuration) !== 'undefined') { + rem = Math.min(rem, lineupItem.streamDuration); + } + if ( (diff <= SLACK) && (diff + SLACK < rem) ) { //closed the stream and opened it again let's not lose seconds for //no reason - return lineupItem; + let originalT0 = recorded.lineupItem.originalT0; + if (typeof(originalT0) === 'undefined') { + originalT0 = recorded.t0; + } + if (t1 - originalT0 <= SLACK) { + lineupItem.originalT0 = originalT0; + return lineupItem; + } } lineupItem.start += diff; From 8a6fb782ad8b2b691ed3b703f73a7e9849004467 Mon Sep 17 00:00:00 2001 From: vexorian Date: Wed, 23 Sep 2020 19:10:34 -0400 Subject: [PATCH 05/12] Attempts to make error screen more likely to appear instead of stream just dying --- src/constants.js | 1 + src/plex-player.js | 26 +++++++++++++++++++++++--- src/throttler.js | 45 +++++++++++++++++++++++++++++++++++++++++++++ src/video.js | 30 +++++++++++++++++++++++------- 4 files changed, 92 insertions(+), 10 deletions(-) create mode 100644 src/throttler.js diff --git a/src/constants.js b/src/constants.js index 733ea56..19547c9 100644 --- a/src/constants.js +++ b/src/constants.js @@ -3,6 +3,7 @@ module.exports = { TVGUIDE_MAXIMUM_PADDING_LENGTH_MS: 30*60*1000, STEALTH_DURATION: 5 * 60* 1000, TVGUIDE_MAXIMUM_FLEX_DURATION : 6 * 60 * 60 * 1000, + TOO_FREQUENT: 100, VERSION_NAME: "1.0.2-prerelease" } diff --git a/src/plex-player.js b/src/plex-player.js index baf5c9c..73ba96f 100644 --- a/src/plex-player.js +++ b/src/plex-player.js @@ -82,18 +82,38 @@ class PlexPlayer { let emitter = new EventEmitter(); //setTimeout( () => { let ff = await ffmpeg.spawnStream(stream.streamUrl, stream.streamStats, streamStart, streamDuration, enableChannelIcon, lineupItem.type); // Spawn the ffmpeg process - ff.pipe(outStream); + ff.pipe(outStream, {'end':false} ); //}, 100); plexTranscoder.startUpdatingPlex(); - + ffmpeg.on('end', () => { emitter.emit('end'); }); ffmpeg.on('close', () => { emitter.emit('close'); }); - ffmpeg.on('error', (err) => { + ffmpeg.on('error', async (err) => { + console.log("Replacing failed stream with error streram"); + ff.unpipe(outStream); + ffmpeg.removeAllListeners('data'); + ffmpeg.removeAllListeners('end'); + ffmpeg.removeAllListeners('error'); + ffmpeg.removeAllListeners('close'); + ffmpeg = new FFMPEG(ffmpegSettings, channel); // Set the transcoder options + ffmpeg.on('close', () => { + emitter.emit('close'); + }); + ffmpeg.on('end', () => { + emitter.emit('end'); + }); + ffmpeg.on('error', (err) => { + emitter.emit('error', err ); + }); + + ff = await ffmpeg.spawnError('oops', 'oops', Math.min(streamStats.duration, 60000) ); + ff.pipe(outStream); + emitter.emit('error', err); }); return emitter; diff --git a/src/throttler.js b/src/throttler.js new file mode 100644 index 0000000..543cbe9 --- /dev/null +++ b/src/throttler.js @@ -0,0 +1,45 @@ +let constants = require('./constants'); + +let cache = {} + + +function equalItems(a, b) { + if ( (typeof(a) === 'undefined') || a.isOffline || b.isOffline ) { + return false; + } + console.log("no idea how to compare this: " + JSON.stringify(a) ); + console.log(" with this: " + JSON.stringify(b) ); + return true; + +} + + +function wereThereTooManyAttempts(sessionId, lineupItem) { + let obj = cache[sessionId]; + let t1 = (new Date()).getTime(); + if (typeof(obj) === 'undefined') { + previous = cache[sessionId] = { + t0: t1 - constants.TOO_FREQUENT * 5 + }; + + } 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) { + //certainly too frequent + result = equalItems( previous.lineupItem, lineupItem ); + } + cache[sessionId].t0 = t1; + cache[sessionId].lineupItem = lineupItem; + return result; + +} + +module.exports = wereThereTooManyAttempts; \ No newline at end of file diff --git a/src/video.js b/src/video.js index 0468820..f41eb39 100644 --- a/src/video.js +++ b/src/video.js @@ -6,9 +6,12 @@ const PlexTranscoder = require('./plexTranscoder') const fs = require('fs') const ProgramPlayer = require('./program-player'); const channelCache = require('./channel-cache') +const wereThereTooManyAttempts = require('./throttler'); module.exports = { router: video } +let StreamCount = 0; + function video( channelDB , db) { var router = express.Router() @@ -107,7 +110,7 @@ function video( channelDB , db) { let channelNum = parseInt(req.query.channel, 10) let ff = await ffmpeg.spawnConcat(`http://localhost:${process.env.PORT}/playlist?channel=${channelNum}`); - ff.pipe(res); + ff.pipe(res ); }) // Stream individual video to ffmpeg concat above. This is used by the server, NOT the client router.get('/stream', async (req, res) => { @@ -116,6 +119,7 @@ function video( channelDB , db) { res.status(400).send("No Channel Specified") return } + let session = parseInt(req.query.session); let m3u8 = (req.query.m3u8 === '1'); let number = parseInt(req.query.channel); let channel = await channelCache.getChannelConfig(channelDB, number); @@ -274,6 +278,14 @@ function video( channelDB , db) { if (! isLoading) { channelCache.recordPlayback(channel.number, t0, lineupItem); } + if (wereThereTooManyAttempts(session, lineupItem)) { + lineupItem = { + isOffline: true, + err: Error("Too many attempts, throttling.."), + duration : 60000, + }; + } + let playerContext = { lineupItem : lineupItem, @@ -329,6 +341,8 @@ function video( channelDB , db) { router.get('/m3u8', async (req, res) => { + let sessionId = StreamCount++; + //res.type('application/vnd.apple.mpegurl') res.type("application/x-mpegURL"); @@ -363,13 +377,13 @@ function video( channelDB , db) { if ( ffmpegSettings.enableFFMPEGTranscoding === true) { //data += `#EXTINF:${cur},\n`; - data += `${req.protocol}://${req.get('host')}/stream?channel=${channelNum}&first=0&m3u8=1\n`; + data += `${req.protocol}://${req.get('host')}/stream?channel=${channelNum}&first=0&m3u8=1&session=${sessionId}\n`; } //data += `#EXTINF:${cur},\n`; - data += `${req.protocol}://${req.get('host')}/stream?channel=${channelNum}&first=1&m3u8=1\n` + data += `${req.protocol}://${req.get('host')}/stream?channel=${channelNum}&first=1&m3u8=1&session=${sessionId}\n` for (var i = 0; i < maxStreamsToPlayInARow - 1; i++) { //data += `#EXTINF:${cur},\n`; - data += `${req.protocol}://${req.get('host')}/stream?channel=${channelNum}&m3u8=1\n` + data += `${req.protocol}://${req.get('host')}/stream?channel=${channelNum}&m3u8=1&session=${sessionId}\n` } res.send(data) @@ -398,6 +412,8 @@ function video( channelDB , db) { let ffmpegSettings = db['ffmpeg-settings'].find()[0] + let sessionId = StreamCount++; + if ( (ffmpegSettings.enableFFMPEGTranscoding === true) && (ffmpegSettings.normalizeVideoCodec === true) @@ -405,11 +421,11 @@ function video( channelDB , db) { && (ffmpegSettings.normalizeResolution === true) && (ffmpegSettings.normalizeAudio === true) ) { - data += `file 'http://localhost:${process.env.PORT}/stream?channel=${channelNum}&first=0'\n`; + data += `file 'http://localhost:${process.env.PORT}/stream?channel=${channelNum}&first=0&session=${sessionId}'\n`; } - data += `file 'http://localhost:${process.env.PORT}/stream?channel=${channelNum}&first=1'\n` + data += `file 'http://localhost:${process.env.PORT}/stream?channel=${channelNum}&first=1&session=${sessionId}'\n` for (var i = 0; i < maxStreamsToPlayInARow - 1; i++) { - data += `file 'http://localhost:${process.env.PORT}/stream?channel=${channelNum}'\n` + data += `file 'http://localhost:${process.env.PORT}/stream?channel=${channelNum}&session=${sessionId}'\n` } res.send(data) From a0693c934a9cb73f10c9c8cffda270accb0bad8c Mon Sep 17 00:00:00 2001 From: vexorian Date: Sat, 26 Sep 2020 07:46:47 -0400 Subject: [PATCH 06/12] Fix #123. Deal with channel limits. Tools and library will be limited to a maximum channel size --- web/controllers/channels.js | 6 ++- web/directives/channel-config.js | 53 ++++++++++++++++++++---- web/directives/plex-library.js | 2 +- web/public/templates/channel-config.html | 16 +++---- web/public/templates/offline-config.html | 2 +- 5 files changed, 60 insertions(+), 19 deletions(-) diff --git a/web/controllers/channels.js b/web/controllers/channels.js index cde72ef..c9fa27f 100644 --- a/web/controllers/channels.js +++ b/web/controllers/channels.js @@ -39,13 +39,13 @@ module.exports = function ($scope, dizquetv) { } } $scope.onChannelConfigDone = async (channel) => { - $scope.showChannelConfig = false if ($scope.selectedChannelIndex != -1) { $scope.channels[ $scope.selectedChannelIndex ].pending = false; } if (typeof channel !== 'undefined') { if ($scope.selectedChannelIndex == -1) { // add new channel await dizquetv.addChannel(channel); + $scope.showChannelConfig = false $scope.refreshChannels(); } else if ( @@ -56,14 +56,18 @@ module.exports = function ($scope, dizquetv) { $scope.channels[ $scope.selectedChannelIndex ].pending = true; await dizquetv.updateChannel(channel), await dizquetv.removeChannel( { number: $scope.originalChannelNumber } ) + $scope.showChannelConfig = false $scope.$apply(); $scope.refreshChannels(); } else { // update existing channel $scope.channels[ $scope.selectedChannelIndex ].pending = true; await dizquetv.updateChannel(channel); + $scope.showChannelConfig = false $scope.$apply(); $scope.refreshChannels(); } + } else { + $scope.showChannelConfig = false } diff --git a/web/directives/channel-config.js b/web/directives/channel-config.js index 8a2f0ec..16c2793 100644 --- a/web/directives/channel-config.js +++ b/web/directives/channel-config.js @@ -10,11 +10,14 @@ module.exports = function ($timeout, $location, dizquetv) { onDone: "=onDone" }, link: function (scope, element, attrs) { + scope.maxSize = 50000; + scope.hasFlex = false; scope.showHelp = false; scope._frequencyModified = false; scope._frequencyMessage = ""; scope.minProgramIndex = 0; + scope.libraryLimit = 50000; scope.episodeMemory = { saved : false, }; @@ -95,6 +98,7 @@ module.exports = function ($timeout, $location, dizquetv) { updateChannelDuration(); setTimeout( () => { scope.showRotatedNote = true }, 1, 'funky'); } + scope._selectedRedirect = { isOffline : true, type : "redirect", @@ -1038,12 +1042,15 @@ module.exports = function ($timeout, $location, dizquetv) { scope.hasFlex = true; } } + scope.maxSize = Math.max(scope.maxSize, scope.channel.programs.length); + scope.libraryLimit = Math.max(0, scope.maxSize - scope.channel.programs.length ); } scope.error = {} - scope._onDone = (channel) => { - if (typeof channel === 'undefined') - scope.onDone() - else { + scope._onDone = async (channel) => { + if (typeof channel === 'undefined') { + await scope.onDone() + $timeout(); + } else { channelNumbers = [] for (let i = 0, l = scope.channels.length; i < l; i++) channelNumbers.push(scope.channels[i].number) @@ -1056,8 +1063,8 @@ module.exports = function ($timeout, $location, dizquetv) { scope.error.number = "Channel number already in use." else if (!scope.isNewChannel && channel.number !== scope.beforeEditChannelNumber && channelNumbers.indexOf(parseInt(channel.number, 10)) !== -1) scope.error.number = "Channel number already in use." - else if (channel.number <= 0 || channel.number >= 2000) - scope.error.name = "Enter a valid number (1-2000)" + else if (channel.number < 0 || channel.number > 9999) + scope.error.name = "Enter a valid number (0-9999)" else if (typeof channel.name === "undefined" || channel.name === null || channel.name === "") scope.error.name = "Enter a channel name." else if (channel.icon !== "" && !validURL(channel.icon)) @@ -1073,7 +1080,21 @@ module.exports = function ($timeout, $location, dizquetv) { for (let i = 0; i < scope.channel.programs.length; i++) { delete scope.channel.programs[i].$index; } - scope.onDone(JSON.parse(angular.toJson(channel))) + try { + let s = angular.toJson(channel); + if (s.length > 50*1000*1000) { + scope.error.any = true; + scope.error.programs = "Channel is too large, can't save."; + } else { + await scope.onDone(JSON.parse(s)) + s = null; + } + } catch(err) { + $timeout(); + console.error(err); + scope.error.any = true; + scope.error.programs = "Unable to save channel." + } } $timeout(() => { scope.error = {} }, 60000) } @@ -1132,7 +1153,7 @@ module.exports = function ($timeout, $location, dizquetv) { if (scope.channel.programs.length == 0) { return 1; } else { - return Math.floor( 50000 / scope.channel.programs.length ); + return Math.floor( scope.maxSize / (scope.channel.programs.length) ); } } scope.removeItem = (x) => { @@ -1165,6 +1186,9 @@ module.exports = function ($timeout, $location, dizquetv) { }; scope.loadChannels(); + scope.disablePadding = () => { + return (scope.paddingOption.id==-1) || (2*scope.channel.programs.length > scope.maxSize); + } scope.paddingOptions = [ { id: -1, description: "Allowed start times", allow5: false }, { id: 30, description: ":00, :30", allow5: false }, @@ -1178,6 +1202,14 @@ module.exports = function ($timeout, $location, dizquetv) { ] scope.paddingOption = scope.paddingOptions[0]; + + scope.breaksDisabled = () => { + return scope.breakAfter==-1 + || scope.minBreakSize==-1 || scope.maxBreakSize==-1 + || (scope.minBreakSize > scope.maxBreakSize) + || (2*scope.channel.programs.length > scope.maxSize); + } + scope.breakAfterOptions = [ { id: -1, description: "After" }, { id: 5, description: "5 minutes" }, @@ -1229,6 +1261,11 @@ module.exports = function ($timeout, $location, dizquetv) { { id: 3, description: "3" }, { id: 4, description: "4" }, ]; + scope.rerunsDisabled = () => { + return scope.rerunStart == -1 || scope.rerunBlockSize == -1 || scope.rerunRepeats == -1 + || (scope.channel.programs.length * scope.rerunRepeats > scope.maxSize) + + } scope.nightStartHours = [ { id: -1, description: "Start" } ]; diff --git a/web/directives/plex-library.js b/web/directives/plex-library.js index 149d04b..b2f6576 100644 --- a/web/directives/plex-library.js +++ b/web/directives/plex-library.js @@ -7,7 +7,7 @@ module.exports = function (plex, dizquetv, $timeout) { onFinish: "=onFinish", height: "=height", visible: "=visible", - limit: "@limit", + limit: "=limit", }, link: function (scope, element, attrs) { scope.errors=[]; diff --git a/web/public/templates/channel-config.html b/web/public/templates/channel-config.html index 4ae8914..d47ab93 100644 --- a/web/public/templates/channel-config.html +++ b/web/public/templates/channel-config.html @@ -152,20 +152,20 @@

The channel's regular programming between the specified hours. Flex time will fill up the remaining hours.

Pad Times
-

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.

+

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.

Add Breaks
-

Adds Flex breaks between programs, attempting to avoid groups of consecutive programs that exceed the specified number of minutes.

+

Adds Flex breaks between programs, attempting to avoid groups of consecutive programs that exceed the specified number of minutes. This button might be disabled if the channel is already too large.

Reruns
-

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.

+

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.

Save|Recover Episode Positions

The "Save" button saves the current episodes that are next to be played for each tv show. Then whenever you click the "Recover Episode Popsitions" button, episodes will be rearranged cyclically and they will start with the saved positions. So you can maintain episode sequences even after modifying the channel. If there are any new TV shows, they will start at their current positions. Movies and specials won't change positions.

Replicate
-

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, there's a limit of 50000 programs to the size of the resulting channel when using this tool.

+

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.

Replicate & Shuffle

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.

@@ -282,7 +282,7 @@ ng-options="o as o.description for o in paddingOptions" /> - @@ -298,7 +298,7 @@ - @@ -489,6 +489,6 @@ - + diff --git a/web/public/templates/offline-config.html b/web/public/templates/offline-config.html index 2c29eb2..0661cf5 100644 --- a/web/public/templates/offline-config.html +++ b/web/public/templates/offline-config.html @@ -165,6 +165,6 @@ - + \ No newline at end of file From 18bd87dcafff60ddc6b33bffd271f965976c68f1 Mon Sep 17 00:00:00 2001 From: vexorian Date: Sat, 26 Sep 2020 12:08:48 -0400 Subject: [PATCH 07/12] Fix anamorphic video (again), when audio is transcoded by plex but video is direct played. #127 --- src/plexTranscoder.js | 22 ++++++++++++++++------ 1 file changed, 16 insertions(+), 6 deletions(-) diff --git a/src/plexTranscoder.js b/src/plexTranscoder.js index 47caba5..ee1a79a 100644 --- a/src/plexTranscoder.js +++ b/src/plexTranscoder.js @@ -16,6 +16,7 @@ class PlexTranscoder { this.log("Debug logging enabled") this.key = lineupItem.key + this.metadataPath = `${server.uri}${lineupItem.key}?X-Plex-Token=${server.accessToken}` this.plexFile = `${server.uri}${lineupItem.plexFile}?X-Plex-Token=${server.accessToken}` if (typeof(lineupItem.file)!=='undefined') { this.file = lineupItem.file.replace(settings.pathReplace, settings.pathReplaceWith) @@ -87,6 +88,8 @@ class PlexTranscoder { this.log("Decision: Direct stream. Audio is being transcoded") stream.separateVideoStream = (this.settings.streamPath === 'direct') ? this.file : this.plexFile; stream.streamUrl = `${this.transcodeUrlBase}${this.transcodingArgs}` + this.directInfo = await this.getDirectInfo(); + this.videoIsDirect = true; } stream.streamStats = this.getVideoStats(); @@ -207,10 +210,14 @@ lang=en` let streams = this.decisionJson.MediaContainer.Metadata[0].Media[0].Part[0].Stream ret.duration = parseFloat( this.decisionJson.MediaContainer.Metadata[0].Media[0].Part[0].duration ); - streams.forEach(function (stream) { + streams.forEach(function (_stream, $index) { // Video + let stream = _stream; if (stream["streamType"] == "1") { - ret.anamorphic = (stream.anamorphic === "1"); + if ( this.videoIsDirect === true && typeof(this.directInfo) !== 'undefined') { + stream = this.directInfo.MediaContainer.Metadata[0].Media[0].Part[0].Stream[$index]; + } + ret.anamorphic = ( (stream.anamorphic === "1") || (stream.anamorphic === true) ); if (ret.anamorphic) { let parsed = parsePixelAspectRatio(stream.pixelAspectRatio); if (isNaN(parsed.p) || isNaN(parsed.q) ) { @@ -236,7 +243,7 @@ lang=en` ret.audioCodec = stream["codec"]; ret.audioDecision = (typeof stream.decision === 'undefined') ? 'copy' : stream.decision; } - }) + }.bind(this) ) } catch (e) { console.log("Error at decision:" + e); } @@ -277,11 +284,15 @@ lang=en` return index } + async getDirectInfo() { + return (await axios.get(this.metadataPath) ).data; + + } + async getDecision(directPlay) { - await axios.get(`${this.server.uri}/video/:/transcode/universal/decision?${this.transcodingArgs}`, { + let res = await axios.get(`${this.server.uri}/video/:/transcode/universal/decision?${this.transcodingArgs}`, { headers: { Accept: 'application/json' } }) - .then((res) => { this.decisionJson = res.data; this.log("Recieved transcode decision:") @@ -294,7 +305,6 @@ lang=en` console.log(`IMPORTANT: Recieved transcode decision code ${transcodeDecisionCode}! Expected code 1001.`) console.log(`Error message: '${res.data.MediaContainer.transcodeDecisionText}'`) } - }) } getStatusUrl() { From c114cab269e431374ae0b79d445bec41849b99b4 Mon Sep 17 00:00:00 2001 From: vexorian Date: Sat, 26 Sep 2020 12:53:32 -0400 Subject: [PATCH 08/12] #110 expose url-tvg and x-tvg-url fields in m3u --- src/api.js | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/api.js b/src/api.js index 9a44617..752b505 100644 --- a/src/api.js +++ b/src/api.js @@ -453,7 +453,8 @@ function api(db, channelDB, xmltvInterval, guideService ) { res.type('text') let channels = await channelDB.getAllChannels(); channels.sort((a, b) => { return a.number < b.number ? -1 : 1 }) - var data = "#EXTM3U\n" + let tvg = `${req.protocol}://${req.get('host')}/api/xmltv.xml` + var data = `#EXTM3U url-tvg="${tvg}" x-tvg-url="${tvg}"\n`; for (var i = 0; i < channels.length; i++) { if (channels[i].stealth!==true) { data += `#EXTINF:0 tvg-id="${channels[i].number}" tvg-chno="${channels[i].number}" tvg-name="${channels[i].name}" tvg-logo="${channels[i].icon}" group-title="dizqueTV",${channels[i].name}\n` From c2a8bdc4c97c31dcc5cbae63d877aeab630584df Mon Sep 17 00:00:00 2001 From: vexorian Date: Sat, 26 Sep 2020 13:40:47 -0400 Subject: [PATCH 09/12] Fix xmltv writer crashing when a rating in the channel json is null for some reason (usually because of the python library) --- src/xmltv.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/xmltv.js b/src/xmltv.js index 054bf81..d1596be 100644 --- a/src/xmltv.js +++ b/src/xmltv.js @@ -105,7 +105,7 @@ async function _writeProgramme(channel, program, xw) { } xw.endElement() // Rating - if (typeof program.rating !== 'undefined') { + if ( (program.rating != null) && (typeof program.rating !== 'undefined') ) { xw.startElement('rating') xw.writeAttribute('system', 'MPAA') xw.writeElement('value', program.rating) From 10e231adb10055ffb090d8c6393f47c6db33237a Mon Sep 17 00:00:00 2001 From: vexorian Date: Sat, 26 Sep 2020 13:41:03 -0400 Subject: [PATCH 10/12] Add explanations to EPG settings. --- web/public/templates/xmltv-settings.html | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/web/public/templates/xmltv-settings.html b/web/public/templates/xmltv-settings.html index f5cb495..1707974 100644 --- a/web/public/templates/xmltv-settings.html +++ b/web/public/templates/xmltv-settings.html @@ -15,11 +15,13 @@
- + + How many hours of programming to include in the xmltv file.
- + + How often should the xmltv file be updated.
From c5a3a0de89e0f16a3c999b12f3174829a3a98ca9 Mon Sep 17 00:00:00 2001 From: vexorian Date: Sat, 26 Sep 2020 15:01:42 -0400 Subject: [PATCH 11/12] Tweak default bitrates a bit. Clarify that they are in Kbps in Plex settings --- src/api.js | 4 ++-- src/database-migration.js | 6 +++--- web/public/templates/plex-settings.html | 8 +++++--- 3 files changed, 10 insertions(+), 8 deletions(-) diff --git a/src/api.js b/src/api.js index 752b505..ee40c54 100644 --- a/src/api.js +++ b/src/api.js @@ -268,8 +268,8 @@ function api(db, channelDB, xmltvInterval, guideService ) { db['plex-settings'].update({ _id: req.body._id }, { streamPath: 'plex', debugLogging: true, - directStreamBitrate: '40000', - transcodeBitrate: '3000', + directStreamBitrate: '20000', + transcodeBitrate: '2000', mediaBufferSize: 1000, transcodeMediaBufferSize: 20000, maxPlayableResolution: "1920x1080", diff --git a/src/database-migration.js b/src/database-migration.js index 3da674c..e1a5912 100644 --- a/src/database-migration.js +++ b/src/database-migration.js @@ -78,8 +78,8 @@ function basicDB(db) { db['plex-settings'].save({ streamPath: 'plex', debugLogging: true, - directStreamBitrate: '40000', - transcodeBitrate: '3000', + directStreamBitrate: '20000', + transcodeBitrate: '2000', mediaBufferSize: 1000, transcodeMediaBufferSize: 20000, maxPlayableResolution: "1920x1080", @@ -379,7 +379,7 @@ function ffmpeg() { videoEncoder: "mpeg2video", audioEncoder: "ac3", targetResolution: "1920x1080", - videoBitrate: 10000, + videoBitrate: 2000, videoBufSize: 2000, audioBitrate: 192, audioBufSize: 50, diff --git a/web/public/templates/plex-settings.html b/web/public/templates/plex-settings.html index a28803d..1324bf1 100644 --- a/web/public/templates/plex-settings.html +++ b/web/public/templates/plex-settings.html @@ -146,12 +146,14 @@
Miscellaneous Options
- +
- - + + + Plex will decide to transcode or direct play based on these settings and if Plex transcodes, it will try to match the transcode bitrate. +
From 435e151258d3e2f0befd66c30560ce3c69ab7e30 Mon Sep 17 00:00:00 2001 From: vexorian Date: Sat, 26 Sep 2020 15:02:01 -0400 Subject: [PATCH 12/12] Phonetic pronuntiation to avoid confusion --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 94d22b4..f7335f4 100644 --- a/README.md +++ b/README.md @@ -3,7 +3,7 @@ Create live TV channel streams from media on your Plex servers. -dizqueTV is a fork of the project previously-known as [pseudotv-plex](https://gitlab.com/DEFENDORe/pseudotv-plex) or [pseudotv](https://github.com/DEFENDORe/pseudotv). New repository because of lack of activity from the main repository and the name change is because projects with the old name already existed and were created long before this approach and it was causing confusion. You can migrate from pseudoTV 0.0.51 to dizqueTV by renaming the .pseudotv folder to .dizquetv and running the new executable (or doing a similar trick with the volumes used by the docker containers). +**dizqueTV** ( *dis·keˈtiːˈvi* ) is a fork of the project previously-known as [pseudotv-plex](https://gitlab.com/DEFENDORe/pseudotv-plex) or [pseudotv](https://github.com/DEFENDORe/pseudotv). New repository because of lack of activity from the main repository and the name change is because projects with the old name already existed and were created long before this approach and it was causing confusion. You can migrate from pseudoTV 0.0.51 to dizqueTV by renaming the .pseudotv folder to .dizquetv and running the new executable (or doing a similar trick with the volumes used by the docker containers).