Channel redirects + 'Channel At night'

This commit is contained in:
vexorian 2020-08-24 22:38:22 -04:00
parent c4a0b7af96
commit 2138176689
13 changed files with 399 additions and 54 deletions

View File

@ -131,7 +131,7 @@ app.use('/images', express.static(path.join(process.env.DATABASE, 'images')))
app.use(express.static(path.join(__dirname, 'web/public')))
app.use('/images', express.static(path.join(process.env.DATABASE, 'images')))
app.use(api.router(db, channelDB, xmltvInterval))
app.use(video.router(db))
app.use(video.router( channelDB, db))
app.use(hdhr.router)
app.listen(process.env.PORT, () => {
console.log(`HTTP server running on port: http://*:${process.env.PORT}`)

View File

@ -9,8 +9,12 @@ async function getChannelConfig(channelDB, channelId) {
if ( typeof(configCache[channelId]) === 'undefined') {
let channel = await channelDB.getChannel(channelId)
//console.log("channel=" + JSON.stringify(channel) );
configCache[channelId] = [channel];
if (channel == null) {
configCache[channelId] = [];
} else {
//console.log("channel=" + JSON.stringify(channel) );
configCache[channelId] = [channel];
}
}
//console.log("channel=" + JSON.stringify(configCache[channelId]).slice(0,200) );
return configCache[channelId];

View File

@ -9,18 +9,23 @@ class ChannelDB {
async getChannel(number) {
let f = path.join(this.folder, `${number}.json` );
return await new Promise( (resolve, reject) => {
fs.readFile(f, (err, data) => {
if (err) {
return reject(err);
}
try {
resolve( JSON.parse(data) )
} catch (err) {
reject(err);
}
})
});
try {
return await new Promise( (resolve, reject) => {
fs.readFile(f, (err, data) => {
if (err) {
return reject(err);
}
try {
resolve( JSON.parse(data) )
} catch (err) {
reject(err);
}
})
});
} catch (err) {
console.error(err);
return null;
}
}
async saveChannel(number, json) {

View File

@ -42,6 +42,20 @@ function createLineup(obj, channel, isFirst) {
let lineup = []
if ( typeof(activeProgram.err) !== 'undefined') {
let remaining = activeProgram.duration - timeElapsed;
lineup.push( {
type: 'offline',
title: 'Error',
err: activeProgram.err,
streamDuration: remaining,
duration: remaining,
start: 0
})
return lineup;
}
if (activeProgram.isOffline === true) {
//offline case
let remaining = activeProgram.duration - timeElapsed;

View File

@ -9,6 +9,7 @@ const PlexTranscoder = require('./plexTranscoder')
const EventEmitter = require('events');
const helperFuncs = require('./helperFuncs')
const FFMPEG = require('./ffmpeg')
const constants = require('./constants');
let USED_CLIENTS = {};
@ -60,8 +61,10 @@ class PlexPlayer {
let ffmpeg = new FFMPEG(ffmpegSettings, channel); // Set the transcoder options
this.ffmpeg = ffmpeg;
let streamDuration;
if (typeof(streamDuration)!=='undefined') {
streamDuration = lineupItem.streamDuration / 1000;
if (typeof(lineupItem.streamDuration)!=='undefined') {
if (lineupItem.start + lineupItem.streamDuration + constants.SLACK < lineupItem.duration) {
streamDuration = lineupItem.streamDuration / 1000;
}
}
let deinterlace = ffmpegSettings.enableFFMPEGTranscoding; //for now it will always deinterlace when transcoding is enabled but this is sub-optimal

View File

@ -34,7 +34,7 @@ class ProgramPlayer {
// people might want the codec normalization to stay because of player support
context.ffmpegSettings.normalizeResolution = false;
}
if (program.err instanceof Error) {
if ( typeof(program.err) !== 'undefined') {
console.log("About to play error stream");
this.delegate = new OfflinePlayer(true, context);
} else if (program.type === 'loading') {

View File

@ -9,7 +9,7 @@ const channelCache = require('./channel-cache')
module.exports = { router: video }
function video(db) {
function video( channelDB , db) {
var router = express.Router()
router.get('/setup', (req, res) => {
@ -49,7 +49,7 @@ function video(db) {
return
}
let number = parseInt(req.query.channel, 10);
let channel = await channelCache.getChannelConfig(db, number);
let channel = await channelCache.getChannelConfig(channelDB, number);
if (channel.length === 0) {
res.status(500).send("Channel doesn't exist")
return
@ -118,7 +118,7 @@ function video(db) {
}
let m3u8 = (req.query.m3u8 === '1');
let number = parseInt(req.query.channel);
let channel = await channelCache.getChannelConfig(db, number);
let channel = await channelCache.getChannelConfig(channelDB, number);
if (channel.length === 0) {
res.status(404).send("Channel doesn't exist")
@ -150,6 +150,11 @@ function video(db) {
// Get video lineup (array of video urls with calculated start times and durations.)
let t0 = (new Date()).getTime();
let lineupItem = channelCache.getCurrentLineupItem( channel.number, t0);
let prog = null;
let brandChannel = channel;
let redirectChannels = [];
let upperBounds = [];
if (isLoading) {
lineupItem = {
type: 'loading',
@ -158,8 +163,57 @@ function video(db) {
start: 0,
};
} else if (lineupItem == null) {
let prog = helperFuncs.getCurrentProgramAndTimeElapsed(t0, channel)
prog = helperFuncs.getCurrentProgramAndTimeElapsed(t0, channel);
while (true) {
redirectChannels.push( brandChannel );
upperBounds.push( prog.program.duration - prog.timeElapsed );
if ( !(prog.program.isOffline) || (prog.program.type != 'redirect') ) {
break;
}
channelCache.recordPlayback( brandChannel.number, t0, {
/*type: 'offline',*/
title: 'Error',
err: Error("Recursive channel redirect found"),
duration : 60000,
start: 0,
});
let newChannelNumber= prog.program.channel;
let newChannel = await channelCache.getChannelConfig(channelDB, newChannelNumber);
if (newChannel.length == 0) {
let err = Error("Invalid redirect to a channel that doesn't exist");
console.error("Invalid redirect to channel that doesn't exist.", err);
prog = {
program: {
isOffline: true,
err: err,
duration : 60000,
},
timeElapsed: 0,
}
continue;
}
newChannel = newChannel[0];
brandChannel = newChannel;
lineupItem = channelCache.getCurrentLineupItem( newChannel.number, t0);
if (lineupItem != null) {
lineupItem = JSON.parse( JSON.stringify(lineupItem)) ;
break;
} else {
prog = helperFuncs.getCurrentProgramAndTimeElapsed(t0, newChannel);
}
}
}
if (lineupItem == null) {
if (prog == null) {
res.status(500).send("server error");
throw Error("Shouldn't prog be non-null?");
}
if (prog.program.isOffline && channel.programs.length == 1) {
//there's only one program and it's offline. So really, the channel is
//permanently offline, it doesn't matter what duration was set
@ -180,15 +234,33 @@ function video(db) {
if ( (prog == null) || (typeof(prog) === 'undefined') || (prog.program == null) || (typeof(prog.program) == "undefined") ) {
throw "No video to play, this means there's a serious unexpected bug or the channel db is corrupted."
}
let lineup = helperFuncs.createLineup(prog, channel, isFirst)
let lineup = helperFuncs.createLineup(prog, brandChannel, isFirst)
lineupItem = lineup.shift()
}
if ( !isLoading && (lineupItem != null) ) {
let upperBound = 1000000000;
//adjust upper bounds and record playbacks
for (let i = redirectChannels.length-1; i >= 0; i--) {
lineupItem = JSON.parse( JSON.stringify(lineupItem ));
let u = upperBounds[i];
if (typeof(u) !== 'undefined') {
let u2 = upperBound;
if ( typeof(lineupItem.streamDuration) !== 'undefined') {
u2 = Math.min(u2, lineupItem.streamDuration);
}
lineupItem.streamDuration = Math.min(u2, u);
upperBound = lineupItem.streamDuration;
}
channelCache.recordPlayback( redirectChannels[i].number, t0, lineupItem );
}
}
console.log("=========================================================");
console.log("! Start playback");
console.log(`! Channel: ${channel.name} (${channel.number})`);
if (typeof(lineupItem) === 'undefined') {
if (typeof(lineupItem.title) === 'undefined') {
lineupItem.title = 'Unknown';
}
console.log(`! Title: ${lineupItem.title}`);
@ -206,7 +278,7 @@ function video(db) {
let playerContext = {
lineupItem : lineupItem,
ffmpegSettings : ffmpegSettings,
channel: channel,
channel: brandChannel,
db: db,
m3u8: m3u8,
}
@ -267,7 +339,7 @@ function video(db) {
}
let channelNum = parseInt(req.query.channel, 10)
let channel = await channelCache.getChannelConfig(db, channelNum );
let channel = await channelCache.getChannelConfig(channelDB, channelNum );
if (channel.length === 0) {
res.status(500).send("Channel doesn't exist")
return
@ -312,7 +384,7 @@ function video(db) {
}
let channelNum = parseInt(req.query.channel, 10)
let channel = await channelCache.getChannelConfig(db, channelNum );
let channel = await channelCache.getChannelConfig(channelDB, channelNum );
if (channel.length === 0) {
res.status(500).send("Channel doesn't exist")
return

View File

@ -18,6 +18,7 @@ app.directive('programConfig', require('./directives/program-config'))
app.directive('offlineConfig', require('./directives/offline-config'))
app.directive('frequencyTweak', require('./directives/frequency-tweak'))
app.directive('removeShows', require('./directives/remove-shows'))
app.directive('channelRedirect', require('./directives/channel-redirect'))
app.directive('plexServerEdit', require('./directives/plex-server-edit'))
app.directive('channelConfig', require('./directives/channel-config'))

View File

@ -1,9 +1,10 @@
module.exports = function ($timeout, $location) {
module.exports = function ($timeout, $location, dizquetv) {
return {
restrict: 'E',
templateUrl: 'templates/channel-config.html',
replace: true,
scope: {
visible: "=visible",
channels: "=channels",
channel: "=channel",
onDone: "=onDone"
@ -93,6 +94,11 @@ module.exports = function ($timeout, $location) {
updateChannelDuration();
setTimeout( () => { scope.showRotatedNote = true }, 1, 'funky');
}
scope._selectedRedirect = {
isOffline : true,
type : "redirect",
duration : 60*60*1000,
}
scope.finshedProgramEdit = (program) => {
scope.channel.programs[scope.selectedProgram] = program
@ -151,7 +157,7 @@ module.exports = function ($timeout, $location) {
let newProgs = []
let progs = scope.channel.programs
for (let i = 0, l = progs.length; i < l; i++) {
if (progs[i].type === 'movie') {
if ( progs[i].isOffline || (progs[i].type === 'movie') ) {
movies.push(progs[i])
} else {
if (typeof shows[progs[i].showTitle] === 'undefined')
@ -241,7 +247,9 @@ module.exports = function ($timeout, $location) {
let tmpProgs = {}
let progs = scope.channel.programs
for (let i = 0, l = progs.length; i < l; i++) {
if (progs[i].type === 'movie') {
if ( progs[i].type ==='redirect' ) {
tmpProgs['_redirect ' + progs[i].channel + ' _ '+ progs[i].duration ] = progs[i];
} else if (progs[i].type === 'movie') {
tmpProgs[progs[i].title + progs[i].durationStr] = progs[i]
} else {
tmpProgs[progs[i].showTitle + '-' + progs[i].season + '-' + progs[i].episode] = progs[i]
@ -258,7 +266,7 @@ module.exports = function ($timeout, $location) {
let tmpProgs = []
let progs = scope.channel.programs
for (let i = 0, l = progs.length; i < l; i++) {
if (progs[i].isOffline !== true) {
if ( (progs[i].isOffline !== true) || (progs[i].type === 'redirect') ) {
tmpProgs.push(progs[i]);
}
}
@ -278,9 +286,17 @@ module.exports = function ($timeout, $location) {
updateChannelDuration()
}
scope.getShowTitle = (program) => {
if (program.isOffline && program.type == 'redirect') {
return `Redirect to channel ${program.channel}`;
} else {
return program.showTitle;
}
}
scope.startRemoveShows = () => {
scope._removablePrograms = scope.channel.programs
.map(program => program.showTitle)
.map(scope.getShowTitle)
.reduce((dedupedArr, showTitle) => {
if (!dedupedArr.includes(showTitle)) {
dedupedArr.push(showTitle)
@ -292,7 +308,9 @@ module.exports = function ($timeout, $location) {
}
scope.removeShows = (deletedShowNames) => {
const p = scope.channel.programs;
scope.channel.programs = p.filter(program => deletedShowNames.indexOf(program.showTitle) === -1);
let set = {};
deletedShowNames.forEach( (a) => set[a] = true );
scope.channel.programs = p.filter( (a) => (set[scope.getShowTitle(a)]!==true) );
}
scope.describeFallback = () => {
@ -312,31 +330,45 @@ module.exports = function ($timeout, $location) {
scope.programSquareStyle = (program) => {
let background ="";
if (program.isOffline) {
if ( (program.isOffline) && (program.type !== 'redirect') ) {
background = "rgb(255, 255, 255)";
} else {
let r = 0, g = 0, b = 0, r2=0, g2=0,b2=0;
let i = 0;
let angle = 45;
let w = 3;
if (program.type === 'episode') {
if (program.type === 'redirect') {
angle = 0;
w = 4 + (program.channel % 10);
let c = (program.channel * 100019);
//r = 255, g = 0, b = 0;
//r2 = 0, g2 = 0, b2 = 255;
r = ( (c & 3) * 77 );
g = ( ( (c >> 1) & 3) * 77 );
b = ( ( (c >> 2) & 3) * 77 );
r2 = ( ( (c >> 5) & 3) * 37 );
g2 = ( ( (c >> 3) & 3) * 37 );
b2 = ( ( (c >> 4) & 3) * 37 );
} else if (program.type === 'episode') {
let h = Math.abs(scope.getHashCode(program.showTitle, false));
let h2 = Math.abs(scope.getHashCode(program.showTitle, true));
r = h % 256;
g = (h / 256) % 256;
b = (h / (256*256) ) % 256;
i = h % 360;
r2 = (h2 / (256*256) ) % 256;
g2 = (h2 / (256*256) ) % 256;
b2 = (h2 / (256*256) ) % 256;
angle = -90 + h % 180
angle = (360 - 90 + h % 180) % 360;
if ( angle >= 350 || angle < 10 ) {
angle += 53;
}
} else {
r = 10, g = 10, b = 10;
r2 = 245, g2 = 245, b2 = 245;
angle = 45;
w = 6;
}
let rgb1 = "rgb("+ r + "," + g + "," + b +")";
let rgb2 = "rgb("+ r2 + "," + g2 + "," + b2 +")"
background = "repeating-linear-gradient( " + angle + "deg, " + rgb1 + ", " + rgb1 + " " + w + "px, " + rgb2 + " " + w + "px, " + rgb2 + " " + (w*2) + "px)";
@ -376,7 +408,7 @@ module.exports = function ($timeout, $location) {
return hash;
}
scope.nightChannel = (a, b) => {
scope.nightChannel = (a, b, ch) => {
let o =(new Date()).getTimezoneOffset() * 60 * 1000;
let m = 24*60*60*1000;
a = (m + a * 60 * 60 * 1000 + o) % m;
@ -405,6 +437,8 @@ module.exports = function ($timeout, $location) {
{
duration: d,
isOffline: true,
channel: ch,
type: (typeof(ch) === 'undefined') ? undefined: "redirect",
}
)
t += d;
@ -419,6 +453,8 @@ module.exports = function ($timeout, $location) {
{
duration: d,
isOffline: true,
type: (typeof(ch) === 'undefined') ? undefined: "redirect",
channel: ch,
}
)
}
@ -433,7 +469,7 @@ module.exports = function ($timeout, $location) {
let tired = 0;
for (let i = 0, l = scope.channel.programs.length; i <= l; i++) {
let prog = scope.channel.programs[i % l];
if (prog.isOffline) {
if (prog.isOffline && prog.type != 'redirect') {
tired = 0;
} else {
if (tired + prog.duration >= after) {
@ -557,7 +593,7 @@ module.exports = function ($timeout, $location) {
scope.startFrequencyTweak = () => {
let programs = {};
for (let i = 0; i < scope.channel.programs.length; i++) {
if (! scope.channel.programs[i].isOffline) {
if ( !scope.channel.programs[i].isOffline || (scope.channel.programs[i].type === 'redirect') ) {
let c = getShowCode(scope.channel.programs[i]);
if ( typeof(programs[c]) === 'undefined') {
programs[c] = 0;
@ -629,7 +665,9 @@ module.exports = function ($timeout, $location) {
function getShowCode(program) {
//used for equalize and frequency tweak
let showName = "_internal.Unknown";
if ( (program.type == 'episode') && ( typeof(program.showTitle) !== 'undefined' ) ) {
if ( program.isOffline && (program.type == 'redirect') ) {
showName = `Redirect to channel ${program.channel}`;
} else if ( (program.type == 'episode') && ( typeof(program.showTitle) !== 'undefined' ) ) {
showName = program.showTitle;
} else {
showName = "_internal.Movies";
@ -657,11 +695,11 @@ module.exports = function ($timeout, $location) {
let shows = {};
let progs = [];
for (let i = 0; i < array.length; i++) {
if (array[i].isOffline) {
if (array[i].isOffline && array[i].type !== 'redirect') {
continue;
}
vid = array[i];
let code = getShowCode(array[i]);
let vid = array[i];
let code = getShowCode(vid);
if ( typeof(shows[code]) === 'undefined') {
shows[code] = {
total: 0,
@ -708,7 +746,7 @@ module.exports = function ($timeout, $location) {
let counts = {};
// some precalculation, useful to stop the shuffle from being quadratic...
for (let i = 0; i < array.length; i++) {
var vid = array[i];
let vid = array[i];
if (vid.type === 'episode' && vid.season != 0) {
let countKey = {
title: vid.showTitle,
@ -752,10 +790,10 @@ module.exports = function ($timeout, $location) {
});
shuffle(array);
for (let i = 0; i < array.length; i++) {
if (array[i].type !== 'movie' && array[i].season != 0) {
if (array[i].type === 'episode' && array[i].season != 0) {
let title = array[i].showTitle;
var sequence = shows[title];
var j = next[title];
let j = next[title];
array[i] = sequence[j].it;
next[title] = (j + 1) % sequence.length;
@ -827,12 +865,37 @@ module.exports = function ($timeout, $location) {
}, 0
);
}
scope.finishRedirect = (program) => {
if (scope.selectedProgram == -1) {
scope.channel.programs.splice(scope.minProgramIndex, 0, program);
} else {
scope.channel.programs[ scope.selectedProgram ] = program;
}
updateChannelDuration();
}
scope.addRedirect = () => {
scope.selectedProgram = -1;
scope._displayRedirect = true;
scope._redirectTitle = "Add Redirect";
scope._selectedRedirect = {
isOffline : true,
type : "redirect",
duration : 60*60*1000,
}
};
scope.selectProgram = (index) => {
scope.selectedProgram = index;
let program = scope.channel.programs[index];
if(program.isOffline) {
scope._selectedOffline = scope.makeOfflineFromChannel( Math.round( (program.duration + 500) / 1000 ) );
if (program.type === 'redirect') {
scope._displayRedirect = true;
scope._redirectTitle = "Edit Redirect";
scope._selectedRedirect = JSON.parse(angular.toJson(program));
} else {
scope._selectedOffline = scope.makeOfflineFromChannel( Math.round( (program.duration + 500) / 1000 ) );
}
} else {
scope._selectedProgram = JSON.parse(angular.toJson(program));
}
@ -841,6 +904,32 @@ module.exports = function ($timeout, $location) {
scope.channel.programs.splice(x, 1)
updateChannelDuration()
}
scope.knownChannels = [
{ id: -1, description: "# Channel #"},
]
scope.loadChannels = async () => {
let channelNumbers = await dizquetv.getChannelNumbers();
try {
await Promise.all( channelNumbers.map( async(x) => {
let desc = await dizquetv.getChannelDescription(x);
if (desc.number != scope.channel.number) {
scope.knownChannels.push( {
id: desc.number,
description: `${desc.number} - ${desc.name}`,
});
}
}) );
} catch (err) {
console.error(err);
}
scope.knownChannels.sort( (a,b) => a.id - b.id);
scope.channelsDownloaded = true;
$timeout( () => scope.$apply(), 0);
};
scope.loadChannels();
scope.paddingOptions = [
{ id: -1, description: "Allowed start times", allow5: false },
{ id: 30, description: ":00, :30", allow5: false },
@ -894,6 +983,9 @@ module.exports = function ($timeout, $location) {
scope.nightEndHours = [ { id: -1, description: "End" } ];
scope.nightStart = -1;
scope.nightEnd = -1;
scope.atNightChannelNumber = -1;
scope.atNightStart = -1;
scope.atNightEnd = -1;
for (let i=0; i < 24; i++) {
let v = { id: i, description: ( (i<10) ? "0" : "") + i + ":00" };
scope.nightStartHours.push(v);

View File

@ -0,0 +1,85 @@
module.exports = function ($timeout, dizquetv) {
return {
restrict: 'E',
templateUrl: 'templates/channel-redirect.html',
replace: true,
scope: {
formTitle: "=formTitle",
visible: "=visible",
program: "=program",
_onDone: "=onDone"
},
link: function (scope, element, attrs) {
scope.error = "";
scope.options = [];
scope.loading = true;
scope.$watch('program', () => {
if (typeof(scope.program) === 'undefined') {
return;
}
if ( isNaN(scope.program.duration) ) {
scope.program.duration = 15000;
}
scope.durationSeconds = Math.ceil( scope.program.duration / 1000.0 );;
})
scope.refreshChannels = async() => {
let channelNumbers = await dizquetv.getChannelNumbers();
try {
await Promise.all( channelNumbers.map( async(x) => {
let desc = await dizquetv.getChannelDescription(x);
let option = {
id: x,
description: `${x} - ${desc.name}`,
};
let i = 0;
while (i < scope.options.length) {
if (scope.options[i].id == x) {
scope.options[i] = option;
break;
}
i++;
}
if (i == scope.options.length) {
scope.options.push(option);
}
scope.$apply();
}) );
} catch (err) {
console.error(err);
}
scope.options.sort( (a,b) => a.id - b.id );
scope.loading = false;
$timeout( () => scope.$apply(), 0);
};
scope.refreshChannels();
scope.onCancel = () => {
scope.visible = false;
}
scope.onDone = () => {
scope.error = "";
if (typeof(scope.program.channel) === 'undefined') {
scope.error = "Please select a channel.";
}
if ( isNaN(scope.program.channel) ) {
scope.error = "Channel must be a number.";
}
if ( isNaN(scope.durationSeconds) ) {
scope.error = "Duration must be a number.";
}
if ( scope.error != "" ) {
$timeout( () => scope.error = "", 60000);
return;
}
scope.program.duration = scope.durationSeconds * 1000;
scope._onDone( scope.program );
scope.visible = false;
};
}
};
}

View File

@ -150,6 +150,13 @@
<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>
<h6>Add Redirect</h6>
<p>Adds a channel redirect. During this period of time, the channel will redirect to another channel.</p>
<h6>&quot;Channel at Night&quot;<h6>
<p>Will redirect to another channel while between the selected hours.</p>
<h6>Remove Duplicates</h6>
<p>Removes repeated videos.</p>
@ -207,9 +214,10 @@
</div>
</div>
<div class="row">
<div class="input-group col-md-6" style="padding: 5px;">
<button class="btn btn-sm btn-warning form-control form-control-sm" type="button" ng-click="addOffline()">Add Flex</button>
<button class="btn btn-sm btn-warning form-control form-control-sm" type="button" ng-click="addOffline()">Add Flex...</button>
</div>
<div class="col-md-6" style="padding: 5px;">
@ -252,6 +260,30 @@
</div>
</div>
<div class="row">
<div class="input-group col-md-6" style="padding: 5px;">
<button class="btn btn-sm btn-warning form-control form-control-sm" type="button" ng-click="addRedirect()">Add Redirect...</button>
</div>
<div class="col-md-6" style="padding: 5px;">
<div class="input-group">
<div class="input-group-prepend">
<div class='loader' ng-hide='channelsDownloaded'></div>
<select ng-show='channelsDownloaded' style='width:5em;' ng-model="atNightChannelNumber"
ng-options="o.id as o.description for o in knownChannels" ></select>
<select ng-model="atNightStart"
ng-options="o.id as o.description for o in nightStartHours" ></select>
<select ng-model="atNightEnd"
ng-options="o.id as o.description for o in nightEndHours" ></select>
</div>
<button class="btn btn-sm btn-warning form-control form-control-sm" type="button" ng-click="nightChannel(atNightEnd, atNightStart, atNightChannelNumber)" ng-disabled="atNightChannelNumber==-1 || atNightStart==-1 || atNightEnd==-1">&quot;Channel at Night&quot;</button>
</div>
</div>
</div>
<div class="row">
<div class="input-group col" style="padding: 5px;">
<button class="btn btn-sm btn-danger form-control form-control-sm" type="button" ng-click="removeDuplicates()">Remove Duplicates</button>
@ -307,7 +339,8 @@
{{ x.type === 'episode' ? x.showTitle + ' - S' + x.season.toString().padStart(2, '0') + 'E' + x.episode.toString().padStart(2, '0') : x.title}}
</div>
<div style="margin-right: 5px; font-weight:ligther" ng-show="x.isOffline">
<i>Flex</i>
<i ng-if="x.type !== 'redirect' " >Flex</i>
<span ng-if="x.type === 'redirect' " ><i>Redirect to channel:</i> <b>{{x.channel}}</b></span>
</div>
<div class="flex-pull-right"></div>
<button class="btn btn-sm btn-link" ng-click="removeItem($index); $event.stopPropagation()">
@ -356,4 +389,5 @@
<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>
<channel-redirect visible="_displayRedirect" on-done="finishRedirect" form-title="_redirectTitle" program="_selectedRedirect" ></channel-redirect>
</div>

View File

@ -0,0 +1,35 @@
<div ng-show="visible">
<div class="modal" tabindex="-1" role="dialog" style="display: block; background-color: rgba(0, 0, 0, .5);">
<div class="modal-dialog modal-dialog-scrollable modal-xl" role="document">
<div class="modal-content">
<div>
<div class="modal-header">
<h5 class="modal-title">{{ formTitle }}</h5>
</div>
</div>
<div class="modal-body container">
<div class="form-group">
<label for="duration">Duration (seconds):</label>
<input id="duration" class="form-control" ng-model="durationSeconds" type="text" placeholder="{{state.server.name}}"></input>
</div>
<div ng-if="state.channelReport == null" class="form-group">
<label for="channel">Redirect to channel:</label>
<select id="channel" class="form-control" ng-model="program.channel"
ng-options="o.id as o.description for o in options" ></select>
<div class="loader" ng-if="loading"></div>
</div>
</div>
<div class="modal-footer">
<span class="text-danger">{{ error }}</span>
<button type="button" class="btn btn-sm btn-link" ng-click="onCancel()" >Cancel</button>
<button type="button" class="btn btn-sm btn-primary" ng-click="onDone()" >Save</button>
</div>
</div>
</div>
</div>
</div>

View File

@ -11,7 +11,7 @@
<div ng-if="state.channelReport == null" class="form-group">
<label for="serverName">Name:</label>
<input class="form-control" type="text" placeholder="{{state.server.name}}" readonly>
<input class="form-control" type="text" placeholder="{{state.server.name}}" readonly></input>
</div>
<div ng-if="state.channelReport == null" class="form-group">