diff --git a/README.md b/README.md
index 7c295b0..051f951 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).
@@ -73,4 +73,4 @@ npm run dev-server
## 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
+ * dizqueTV's improvements are released under zlib license (c) 2020 Victor Hugo Soliz Kuncar
diff --git a/src/api.js b/src/api.js
index 9a44617..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",
@@ -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`
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;
diff --git a/src/constants.js b/src/constants.js
index 37836dd..412f36e 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"
}
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/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/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() {
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)
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)
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 8734685..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,15 +1080,30 @@ 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)
}
}
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(
@@ -1131,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) => {
@@ -1164,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 },
@@ -1177,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" },
@@ -1228,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/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/channel-config.html b/web/public/templates/channel-config.html
index 98ffe4f..d47ab93 100644
--- a/web/public/templates/channel-config.html
+++ b/web/public/templates/channel-config.html
@@ -139,9 +139,6 @@
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.
-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.
-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,33 +148,33 @@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.
-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.
-The channel's regular programming between the specified hours. Flex time will fill up the remaining hours.
+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.
+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.
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.
- -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.
- -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.
- +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.
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.
+Makes multiple copies of the schedule and plays them in sequence. Normally this isn't necessary, because dizqueTV will always play the schedule back from the beginning when it finishes. But creating replicas is a useful intermediary step sometimes before applying other transformations. Note that because very large channels can be problematic, the number of replicas will be limited to avoid creating really large channels.
+ +Like "Replicate", it will make multiple copies of the programming. In addition it will shuffle the programs, but it will make sure not to have too small a distance between two identical programs.
+ +Adds a channel redirect. During this period of time, the channel will redirect to another channel.
-Will redirect to another channel while between the selected hours.
@@ -285,7 +282,7 @@ ng-options="o as o.description for o in paddingOptions" /> -