From 21381766894cdaec71141d6d4dcfbf1a2aa024e9 Mon Sep 17 00:00:00 2001 From: vexorian Date: Mon, 24 Aug 2020 22:38:22 -0400 Subject: [PATCH] Channel redirects + 'Channel At night' --- index.js | 2 +- src/channel-cache.js | 8 +- src/dao/channel-db.js | 29 +++-- src/helperFuncs.js | 14 +++ src/plex-player.js | 7 +- src/program-player.js | 2 +- src/video.js | 92 ++++++++++++-- web/app.js | 1 + web/directives/channel-config.js | 138 +++++++++++++++++---- web/directives/channel-redirect.js | 85 +++++++++++++ web/public/templates/channel-config.html | 38 +++++- web/public/templates/channel-redirect.html | 35 ++++++ web/public/templates/plex-server-edit.html | 2 +- 13 files changed, 399 insertions(+), 54 deletions(-) create mode 100644 web/directives/channel-redirect.js create mode 100644 web/public/templates/channel-redirect.html diff --git a/index.js b/index.js index 87411a3..20d57bb 100644 --- a/index.js +++ b/index.js @@ -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}`) diff --git a/src/channel-cache.js b/src/channel-cache.js index beacc58..681e99a 100644 --- a/src/channel-cache.js +++ b/src/channel-cache.js @@ -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]; diff --git a/src/dao/channel-db.js b/src/dao/channel-db.js index 5684d05..ac81bf4 100644 --- a/src/dao/channel-db.js +++ b/src/dao/channel-db.js @@ -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) { diff --git a/src/helperFuncs.js b/src/helperFuncs.js index c8259f9..f5b8068 100644 --- a/src/helperFuncs.js +++ b/src/helperFuncs.js @@ -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; diff --git a/src/plex-player.js b/src/plex-player.js index 1fb2bf3..baf5c9c 100644 --- a/src/plex-player.js +++ b/src/plex-player.js @@ -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 diff --git a/src/program-player.js b/src/program-player.js index 30e1283..576cff9 100644 --- a/src/program-player.js +++ b/src/program-player.js @@ -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') { diff --git a/src/video.js b/src/video.js index 84bd32c..cf5144c 100644 --- a/src/video.js +++ b/src/video.js @@ -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 diff --git a/web/app.js b/web/app.js index 57904f9..9360f62 100644 --- a/web/app.js +++ b/web/app.js @@ -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')) diff --git a/web/directives/channel-config.js b/web/directives/channel-config.js index a8a0f30..bb0acb9 100644 --- a/web/directives/channel-config.js +++ b/web/directives/channel-config.js @@ -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); diff --git a/web/directives/channel-redirect.js b/web/directives/channel-redirect.js new file mode 100644 index 0000000..d9c9607 --- /dev/null +++ b/web/directives/channel-redirect.js @@ -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; + + }; + + } + }; +} diff --git a/web/public/templates/channel-config.html b/web/public/templates/channel-config.html index 17f50f6..829d993 100644 --- a/web/public/templates/channel-config.html +++ b/web/public/templates/channel-config.html @@ -150,6 +150,13 @@
Add Breaks

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

+
Add Redirect
+

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

+ +
"Channel at Night"
+

Will redirect to another channel while between the selected hours.

+ +
Remove Duplicates

Removes repeated videos.

@@ -207,9 +214,10 @@ +
- +
@@ -252,6 +260,30 @@
+
+
+ +
+ +
+
+
+
+ + + +
+ +
+
+ + +
+ +
@@ -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}}
- Flex + Flex + Redirect to channel: {{x.channel}}
diff --git a/web/public/templates/channel-redirect.html b/web/public/templates/channel-redirect.html new file mode 100644 index 0000000..19cb498 --- /dev/null +++ b/web/public/templates/channel-redirect.html @@ -0,0 +1,35 @@ +
+ +
\ No newline at end of file diff --git a/web/public/templates/plex-server-edit.html b/web/public/templates/plex-server-edit.html index 9a0d7d7..6859629 100644 --- a/web/public/templates/plex-server-edit.html +++ b/web/public/templates/plex-server-edit.html @@ -11,7 +11,7 @@
- +