From 621f261a59840ec973f1cd41d76597db7703b6d7 Mon Sep 17 00:00:00 2001 From: vexorian Date: Thu, 17 Sep 2020 23:51:33 -0400 Subject: [PATCH 01/18] 1.0.1 --- README.md | 2 +- src/constants.js | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index be9e28c..88d9556 100644 --- a/README.md +++ b/README.md @@ -1,4 +1,4 @@ -# dizqueTV 1.0.1-prerelease +# dizqueTV 1.0.1 ![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..aeb54e7 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.1" } From 51c978ce37094a1e200b9dbe6207e8b440b77e94 Mon Sep 17 00:00:00 2001 From: vexorian Date: Mon, 21 Sep 2020 21:17:46 -0400 Subject: [PATCH 02/18] 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 03/18] #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 3e1d207e1bf7a99d863ebd1533e63e30245f06dc Mon Sep 17 00:00:00 2001 From: vexorian Date: Mon, 21 Sep 2020 22:42:09 -0400 Subject: [PATCH 04/18] Prepare 1.1.1 development --- README.md | 2 +- src/constants.js | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index 9afff67..1006a4a 100644 --- a/README.md +++ b/README.md @@ -1,4 +1,4 @@ -# dizqueTV 1.1.0-prerelease +# dizqueTV 1.1.1-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 a0c44da..fd2fcd3 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.1.0-prerelease" + VERSION_NAME: "1.1.1-prerelease" } From a9341f12c82e0cd66b5e88f5644fd742c7e83a06 Mon Sep 17 00:00:00 2001 From: vexorian Date: Mon, 21 Sep 2020 23:15:45 -0400 Subject: [PATCH 05/18] Fix up fillers before saving them to the file. --- src/dao/filler-db.js | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/src/dao/filler-db.js b/src/dao/filler-db.js index c2f254c..b9d7f9b 100644 --- a/src/dao/filler-db.js +++ b/src/dao/filler-db.js @@ -53,6 +53,7 @@ class FillerDB { let data = undefined; try { //id is determined by the file name, not the contents + fixup(json); delete json.id; data = JSON.stringify(json); } catch (err) { @@ -72,6 +73,7 @@ class FillerDB { async createFiller(json) { let id = uuidv4(); + fixup(json); await this.saveFiller(id, json); return id; } @@ -189,4 +191,13 @@ class FillerDB { } +function fixup(json) { + if (typeof(json.fillerContent) === 'undefined') { + json.fillerContent = []; + } + if (typeof(json.name) === 'undefined') { + json.name = "Unnamed Filler"; + } +} + module.exports = FillerDB; \ No newline at end of file From e9fe6001e199a3832ab248c511eeee5d43ccefee Mon Sep 17 00:00:00 2001 From: vexorian Date: Tue, 22 Sep 2020 12:17:08 -0400 Subject: [PATCH 06/18] 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 07/18] 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 08/18] 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 665e71e24ee5e93d9c9c90545addb53fdc235ff6 Mon Sep 17 00:00:00 2001 From: Dan Ferguson <65367335+DEFENDORe@users.noreply.github.com> Date: Thu, 24 Sep 2020 17:02:41 -0400 Subject: [PATCH 09/18] Create LICENSE --- LICENSE | 21 +++++++++++++++++++++ 1 file changed, 21 insertions(+) create mode 100644 LICENSE diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..ec75841 --- /dev/null +++ b/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2020 Dan Ferguson + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. From 0df7622a325381e755d09ff1e85caac665faff60 Mon Sep 17 00:00:00 2001 From: vexorian Date: Thu, 24 Sep 2020 18:49:41 -0400 Subject: [PATCH 10/18] dizqueTV license --- LICENSE | 32 +++++++++++++++----------------- README.md | 5 +++++ 2 files changed, 20 insertions(+), 17 deletions(-) diff --git a/LICENSE b/LICENSE index ec75841..90a253c 100644 --- a/LICENSE +++ b/LICENSE @@ -1,21 +1,19 @@ -MIT License +zlib License -Copyright (c) 2020 Dan Ferguson +Copyright (c) 2020 Dan Ferguson, Victor Hugo Soliz Kuncar -Permission is hereby granted, free of charge, to any person obtaining a copy -of this software and associated documentation files (the "Software"), to deal -in the Software without restriction, including without limitation the rights -to use, copy, modify, merge, publish, distribute, sublicense, and/or sell -copies of the Software, and to permit persons to whom the Software is -furnished to do so, subject to the following conditions: +This software is provided 'as-is', without any express or implied +warranty. In no event will the authors be held liable for any damages +arising from the use of this software. -The above copyright notice and this permission notice shall be included in all -copies or substantial portions of the Software. +Permission is granted to anyone to use this software for any purpose, +including commercial applications, and to alter it and redistribute it +freely, subject to the following restrictions: -THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE -SOFTWARE. +1. The origin of this software must not be misrepresented; you must not + claim that you wrote the original software. If you use this software + in a product, an acknowledgment in the product documentation would be + appreciated but is not required. +2. Altered source versions must be plainly marked as such, and must not be + misrepresented as being the original software. +3. This notice may not be removed or altered from any source distribution. diff --git a/README.md b/README.md index 88d9556..a86bd46 100644 --- a/README.md +++ b/README.md @@ -69,3 +69,8 @@ npm run dev-server * Pull requests welcome but please read the [Code of Conduct](CODE_OF_CONDUCT.md) and the [Pull Request Template](pull_request_template.md) first. * Tip Jar: https://buymeacoffee.com/vexorian + +## License + + * Original pseudotv-Plex code was released under [MIT license (c) 2020 Dan Ferguson](https://github.com/DEFENDORe/pseudotv/blob/665e71e24ee5e93d9c9c90545addb53fdc235ff6/LICENSE) + * dizqueTV's improvements are released under zlib license (c) 2020 Victor Hugo Soliz Kuncar \ No newline at end of file From a0693c934a9cb73f10c9c8cffda270accb0bad8c Mon Sep 17 00:00:00 2001 From: vexorian Date: Sat, 26 Sep 2020 07:46:47 -0400 Subject: [PATCH 11/18] 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 2557b78c6b6283c98f793b5ca86b1004b20a75a8 Mon Sep 17 00:00:00 2001 From: vexorian Date: Sat, 26 Sep 2020 08:05:52 -0400 Subject: [PATCH 12/18] Add limit to plex-library, it is now mandatory after the 1.0.x merge. --- web/public/templates/filler-config.html | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/web/public/templates/filler-config.html b/web/public/templates/filler-config.html index 9079b31..6e8d2e1 100644 --- a/web/public/templates/filler-config.html +++ b/web/public/templates/filler-config.html @@ -88,5 +88,5 @@ - + \ 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 13/18] 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 14/18] #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 15/18] 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 16/18] 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 17/18] 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 18/18] 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).