Merge branch 'dev/1.0.x' into main
This commit is contained in:
commit
c1269e48e6
@ -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).
|
||||
|
||||
<img src="./resources/dizquetv.png" width="200">
|
||||
|
||||
@ -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
|
||||
* dizqueTV's improvements are released under zlib license (c) 2020 Victor Hugo Soliz Kuncar
|
||||
|
||||
@ -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`
|
||||
|
||||
@ -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;
|
||||
|
||||
@ -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"
|
||||
}
|
||||
|
||||
@ -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,
|
||||
|
||||
@ -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;
|
||||
|
||||
@ -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() {
|
||||
|
||||
45
src/throttler.js
Normal file
45
src/throttler.js
Normal file
@ -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;
|
||||
30
src/video.js
30
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)
|
||||
|
||||
@ -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)
|
||||
|
||||
@ -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
|
||||
}
|
||||
|
||||
|
||||
|
||||
@ -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" } ];
|
||||
|
||||
@ -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=[];
|
||||
|
||||
@ -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
|
||||
}
|
||||
|
||||
@ -139,9 +139,6 @@
|
||||
<h6>Sort Release Dates</h6>
|
||||
<p>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.</p>
|
||||
|
||||
<h6>Sort Release Dates</h6>
|
||||
<p>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.</p>
|
||||
|
||||
<h6>Balance Shows</h6>
|
||||
<p>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.</p>
|
||||
|
||||
@ -151,33 +148,33 @@
|
||||
<h6>Add Flex</h6>
|
||||
<p>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.</p>
|
||||
|
||||
<h6>Pad Times</h6>
|
||||
<p>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.</p>
|
||||
|
||||
<h6>Restrict Hours</h6>
|
||||
<p>The channel's regular programming between the specified hours. Flex time will fill up the remaining hours.</p>
|
||||
|
||||
<h6>Pad Times</h6>
|
||||
<p>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.</p>
|
||||
|
||||
<h6>Add Breaks</h6>
|
||||
<p>Adds Flex breaks between programs, attempting to avoid groups of consecutive programs that exceed the specified number of minutes.</p>
|
||||
<p>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.</p>
|
||||
|
||||
<h6>Reruns</h6>
|
||||
<p>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. </p>
|
||||
|
||||
<h6>Replicate</h6>
|
||||
<p>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.</p>
|
||||
|
||||
<h6>Replicate & Shuffle</h6>
|
||||
<p>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.</p>
|
||||
|
||||
<p>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.</p>
|
||||
|
||||
<h6>Save|Recover Episode Positions</h6>
|
||||
<p>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.
|
||||
</p>
|
||||
|
||||
<h6>Replicate</h6>
|
||||
<p>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.</p>
|
||||
|
||||
<h6>Replicate & Shuffle</h6>
|
||||
<p>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.</p>
|
||||
|
||||
|
||||
<h6>Add Redirect</h6>
|
||||
<p>Adds a channel redirect. During this period of time, the channel will redirect to another channel.</p>
|
||||
|
||||
<h6>"Channel at Night"<h6>
|
||||
<h6>"Channel at Night"</h6>
|
||||
<p>Will redirect to another channel while between the selected hours.</p>
|
||||
|
||||
|
||||
@ -285,7 +282,7 @@
|
||||
ng-options="o as o.description for o in paddingOptions" />
|
||||
|
||||
</div>
|
||||
<button ng-disabled="paddingOption.id==-1" class="btn btn-sm btn-warning form-control form-control-sm" type="button" ng-click="padTimes(paddingOption.id, paddingOption.allow5)">
|
||||
<button ng-disabled="disablePadding()" class="btn btn-sm btn-warning form-control form-control-sm" type="button" ng-click="padTimes(paddingOption.id, paddingOption.allow5)">
|
||||
<i class='far fa-clock'></i> Pad Times
|
||||
</button>
|
||||
</div>
|
||||
@ -301,7 +298,7 @@
|
||||
<select style="width:5em" ng-model="maxBreakSize"
|
||||
ng-options="o.id as o.description for o in maxBreakSizeOptions" />
|
||||
</div>
|
||||
<button class="btn btn-sm btn-warning form-control form-control-sm" type="button" ng-click="addBreaks(breakAfter, minBreakSize, maxBreakSize)" ng-disabled="breakAfter==-1 || minBreakSize==-1 || maxBreakSize==-1 || (minBreakSize > maxBreakSize)">
|
||||
<button class="btn btn-sm btn-warning form-control form-control-sm" type="button" ng-click="addBreaks(breakAfter, minBreakSize, maxBreakSize)" ng-disabled="breaksDisabled()">
|
||||
<i class='fa fa-coffee'></i> Add Breaks
|
||||
</button>
|
||||
</div>
|
||||
@ -321,7 +318,7 @@
|
||||
ng-options="o.id as o.description for o in rerunRepeatOptions">
|
||||
</select>
|
||||
</div>
|
||||
<button class="btn btn-sm btn-warning form-control form-control-sm" type="button" ng-click="doReruns(rerunStart, rerunBlockSize, rerunRepeats)" ng-disabled="rerunStart == -1 || rerunBlockSize == -1 || rerunRepeats == -1" >
|
||||
<button class="btn btn-sm btn-warning form-control form-control-sm" type="button" ng-click="doReruns(rerunStart, rerunBlockSize, rerunRepeats)" ng-disabled="rerunsDisabled()" >
|
||||
<i class='far fa-clone'></i> Reruns
|
||||
</button>
|
||||
</div>
|
||||
@ -492,6 +489,6 @@
|
||||
<frequency-tweak programs="_programFrequencies" message="_frequencyMessage" modified="_frequencyModified" on-done="tweakFrequencies"></frequency-tweak>
|
||||
<remove-shows program-titles="_removablePrograms" on-done="removeShows" deleted="_deletedProgramNames"></remove-shows>
|
||||
<offline-config offline-title="Add Flex Time" program="_addingOffline" on-done="finishedAddingOffline"></offline-config>
|
||||
<plex-library height="300" visible="displayPlexLibrary" on-finish="importPrograms"></plex-library>
|
||||
<plex-library limit="libraryLimit" height="300" visible="displayPlexLibrary" on-finish="importPrograms"></plex-library>
|
||||
<channel-redirect visible="_displayRedirect" on-done="finishRedirect" form-title="_redirectTitle" program="_selectedRedirect" ></channel-redirect>
|
||||
</div>
|
||||
|
||||
@ -165,6 +165,6 @@
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<plex-library height="300" visible="showPlexLibrary" on-finish="importPrograms"></plex-library>
|
||||
<plex-library limit=1000000000 height="300" visible="showPlexLibrary" on-finish="importPrograms"></plex-library>
|
||||
<plex-library height="300" limit=1 visible="showFallbackPlexLibrary" on-finish="importFallback"></plex-library>
|
||||
</div>
|
||||
@ -146,12 +146,14 @@
|
||||
<div class="col-sm-6">
|
||||
<h6 style="font-weight: bold">Miscellaneous Options</h6>
|
||||
<div class="form-group">
|
||||
<label>Max Direct Stream Bitrate</label>
|
||||
<label>Max Direct Stream Bitrate (Kbps)</label>
|
||||
<input type="text" class="form-control form-control-sm" ng-model="settings.directStreamBitrate" />
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label>Max Transcode Bitrate</label>
|
||||
<input type="text" class="form-control form-control-sm" ng-model="settings.transcodeBitrate" />
|
||||
<label>Max Transcode Bitrate (Kbps)</label>
|
||||
<input type="text" class="form-control form-control-sm" ng-model="settings.transcodeBitrate" aria-described-by="transcodebrhelp" />
|
||||
<small id="transcodebrhelp" class='text-muted form-text'>Plex will decide to transcode or direct play based on these settings and if Plex transcodes, it will try to match the transcode bitrate.</small>
|
||||
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label>Direct Stream Media Buffer Size</label>
|
||||
|
||||
@ -84,5 +84,5 @@
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<plex-library height="300" visible="showPlexLibrary" on-finish="selectedCommercials"></plex-library>
|
||||
|
||||
</div>
|
||||
@ -15,11 +15,13 @@
|
||||
<div class="row">
|
||||
<div class="col-sm-6">
|
||||
<label>EPG Cache (hours)</label>
|
||||
<input type="number" class="form-control form-control-sm" ng-model="settings.cache"/>
|
||||
<input type="number" class="form-control form-control-sm" ng-model="settings.cache" aria-describedby="cachehelp"/>
|
||||
<small id="cachehelp" class="form-text text-muted">How many hours of programming to include in the xmltv file.</small>
|
||||
</div>
|
||||
<div class="col-sm-6">
|
||||
<label>Refresh Timer (hours)</label>
|
||||
<input type="number" class="form-control form-control-sm" ng-model="settings.refresh"/>
|
||||
<input type="number" class="form-control form-control-sm" ng-model="settings.refresh" aria-describedby="timerhelp"/>
|
||||
<small id="timerhelp" class="form-text text-muted">How often should the xmltv file be updated.</small>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user