module.exports = function ($timeout, $location, dizquetv, resolutionOptions, getShowData, commonProgramTools) { return { restrict: 'E', templateUrl: 'templates/channel-config.html', replace: true, scope: { visible: "=visible", channels: "=channels", channel: "=channel", onDone: "=onDone" }, link: { post: function (scope, element, attrs) { scope.screenW = 1920; scope.screenh = 1080; scope.maxSize = 50000; scope.programming = { maxHeight: 30, step : 1, } try { let h = parseFloat( localStorage.getItem("channel-programming-list-height" ) ); if (isNaN(h)) { h = 30; } h = Math.min(64, Math.max(1, h)); console.log("loaded=" + h); scope.programming.maxHeight = h; } catch (e) { console.error(e); } scope.blockCount = 1; scope.showShuffleOptions = (localStorage.getItem("channel-tools") === "on"); scope.reverseTools = (localStorage.getItem("channel-tools-position") === "left"); scope.hasFlex = false; scope.showHelp = { check: false } scope._frequencyModified = false; scope._frequencyMessage = ""; scope.minProgramIndex = 0; scope.libraryLimit = 50000; scope.displayPlexLibrary = false; scope.episodeMemory = { saved : false, }; scope.fixedOnDemand = false; if (typeof scope.channel === 'undefined' || scope.channel == null) { scope.channel = {} scope.channel.programs = [] scope.channel.watermark = defaultWatermark(); scope.channel.fillerCollections = [] scope.channel.guideFlexPlaceholder = ""; scope.channel.fillerRepeatCooldown = 30 * 60 * 1000; scope.channel.fallback = []; scope.channel.guideMinimumDurationSeconds = 5 * 60; scope.isNewChannel = true scope.channel.icon = `${$location.protocol()}://${location.host}/images/dizquetv.png` scope.channel.groupTitle = "dizqueTV"; scope.channel.disableFillerOverlay = true; scope.channel.iconWidth = 120 scope.channel.iconDuration = 60 scope.channel.iconPosition = "2" scope.channel.startTime = new Date() scope.channel.startTime.setMilliseconds(0) scope.channel.startTime.setSeconds(0) scope.channel.offlinePicture = `${$location.protocol()}://${location.host}/images/generic-offline-screen.png` scope.channel.offlineSoundtrack = '' scope.channel.offlineMode = "pic"; if (scope.channel.startTime.getMinutes() < 30) scope.channel.startTime.setMinutes(0) else scope.channel.startTime.setMinutes(30) if (scope.channels.length > 0) { scope.channel.number = scope.channels[scope.channels.length - 1].number + 1 scope.channel.name = "Channel " + scope.channel.number } else { scope.channel.number = 1 scope.channel.name = "Channel 1" } scope.showRotatedNote = false; scope.channel.transcoding = { targetResolution: "", } scope.channel.onDemand = { isOnDemand : false, modulo: 1, } } else { scope.beforeEditChannelNumber = scope.channel.number if ( (typeof(scope.channel.watermark) === 'undefined') || (scope.channel.watermark.enabled !== true) ) { scope.channel.watermark = defaultWatermark(); } if ( (typeof(scope.channel.groupTitle) === 'undefined') || (scope.channel.groupTitle === '') ) { scope.channel.groupTitle = "dizqueTV"; } if (typeof(scope.channel.fillerRepeatCooldown) === 'undefined') { scope.channel.fillerRepeatCooldown = 30 * 60 * 1000; } if (typeof(scope.channel.offlinePicture)==='undefined') { scope.channel.offlinePicture = `${$location.protocol()}://${location.host}/images/generic-offline-screen.png` scope.channel.offlineSoundtrack = ''; } if (typeof(scope.channel.fillerCollections)==='undefined') { scope.channel.fillerCollections = []; } if (typeof(scope.channel.fallback)==='undefined') { scope.channel.fallback = []; scope.channel.offlineMode = "pic"; } if (typeof(scope.channel.offlineMode)==='undefined') { scope.channel.offlineMode = 'pic'; } if (typeof(scope.channel.disableFillerOverlay) === 'undefined') { scope.channel.disableFillerOverlay = true; } if ( (typeof(scope.channel.guideMinimumDurationSeconds) === 'undefined') || isNaN(scope.channel.guideMinimumDurationSeconds) ) { scope.channel.guideMinimumDurationSeconds = 5 * 60; } if (typeof(scope.channel.transcoding) ==='undefined') { scope.channel.transcoding = {}; } if ( (scope.channel.transcoding.targetResolution == null) || (typeof(scope.channel.transcoding.targetResolution) === 'undefined') || (scope.channel.transcoding.targetResolution === '') ) { scope.channel.transcoding.targetResolution = ""; } if (typeof(scope.channel.onDemand) === 'undefined') { scope.channel.onDemand = {}; } if (typeof(scope.channel.onDemand.isOnDemand) !== 'boolean') { scope.channel.onDemand.isOnDemand = false; } if (typeof(scope.channel.onDemand.modulo) !== 'number') { scope.channel.onDemand.modulo = 1; } adjustStartTimeToCurrentProgram(); updateChannelDuration(); setTimeout( () => { scope.showRotatedNote = true }, 1, 'funky'); } function defaultWatermark() { return { enabled: false, position: "bottom-right", width: 10.00, verticalMargin: 0.00, horizontalMargin: 0.00, duration: 0, } } function adjustStartTimeToCurrentProgram() { let t = Date.now(); let originalStart = scope.channel.startTime.getTime(); let n = scope.channel.programs.length; if ( (scope.channel.onDemand.isOnDemand === true) && (scope.channel.onDemand.paused === true) && ! scope.fixedOnDemand ) { //this should only happen once per channel scope.fixedOnDemand = true; originalStart = new Date().getTime(); originalStart -= scope.channel.onDemand.playedOffset; let m = scope.channel.onDemand.firstProgramModulo; let n = originalStart % scope.channel.onDemand.modulo; if (n < m) { originalStart += (m - n); } else if (n > m) { originalStart -= (n - m) - scope.channel.onDemand.modulo; } } //scope.channel.totalDuration might not have been initialized let totalDuration = 0; for (let i = 0; i < n; i++) { totalDuration += scope.channel.programs[i].duration; } if (totalDuration == 0) { return; } let m = (t - originalStart) % totalDuration; let x = 0; let runningProgram = -1; let offset = 0; for (let i = 0; i < n; i++) { let d = scope.channel.programs[i].duration; if (x + d > m) { runningProgram = i offset = m - x; break; } else { x += d; } } // move runningProgram to index 0 scope.channel.programs = scope.channel.programs.slice(runningProgram) .concat(scope.channel.programs.slice(0, runningProgram) ); scope.channel.startTime = new Date(t - offset); } let addMinuteVersionsOfFields = () => { //add the minutes versions of the cooldowns: scope.channel.fillerRepeatCooldownMinutes = scope.channel.fillerRepeatCooldown / 1000 / 60; for (let i = 0; i < scope.channel.fillerCollections.length; i++) { scope.channel.fillerCollections[i].cooldownMinutes = scope.channel.fillerCollections[i].cooldown / 1000 / 60; } } addMinuteVersionsOfFields(); let removeMinuteVersionsOfFields = (channel) => { channel.fillerRepeatCooldown = channel.fillerRepeatCooldownMinutes * 60 * 1000; delete channel.fillerRepeatCooldownMinutes; for (let i = 0; i < channel.fillerCollections.length; i++) { channel.fillerCollections[i].cooldown = channel.fillerCollections[i].cooldownMinutes * 60 * 1000; delete channel.fillerCollections[i].cooldownMinutes; } } scope.tabOptions = [ { name: "Properties", id: "basic" }, { name: "Programming", id: "programming" }, { name: "Flex", id: "flex" }, { name: "EPG", id: "epg" }, { name: "FFmpeg", id: "ffmpeg" }, { name: "On-demand", id: "ondemand" }, ]; scope.setTab = (tab) => { scope.tab = tab; } if (scope.isNewChannel) { scope.tab = "basic"; } else { scope.tab = "programming"; } scope.getTitle = () => { if (scope.isNewChannel) { return "Create Channel"; } else { let x = "?"; if ( (scope.channel.number != null) && ( typeof(scope.channel.number) !== 'undefined') && (! isNaN(scope.channel.number) ) ) { x = "" + scope.channel.number; } let y = "Unnamed"; if (typeof(scope.channel.name) !== 'undefined') { y = scope.channel.name; } return `${x} - ${y}`; } } scope._selectedRedirect = { isOffline : true, type : "redirect", duration : 60*60*1000, } scope.finshedProgramEdit = (program) => { scope.channel.programs[scope.selectedProgram] = program scope._selectedProgram = null updateChannelDuration() } scope.dropFunction = (dropIndex, program) => { let y = program.$index; let z = dropIndex + scope.currentStartIndex - 1; scope.channel.programs.splice(y, 1); if (z >= y) { z--; } scope.channel.programs.splice(z, 0, program ); updateChannelDuration(); $timeout(); return false; } scope.setUpWatcher = function setupWatchers() { this.$watch('vsRepeat.startIndex', function(val) { scope.currentStartIndex = val; }); }; scope.finishedOfflineEdit = (program) => { let editedProgram = scope.channel.programs[scope.selectedProgram]; let duration = program.durationSeconds * 1000; editedProgram.duration = duration; editedProgram.isOffline = true; scope._selectedOffline = null updateChannelDuration() } scope.finishedAddingOffline = (result) => { let duration = result.durationSeconds * 1000; let program = { duration: duration, isOffline: true } scope.channel.programs.splice(scope.minProgramIndex, 0, program); scope._selectedOffline = null scope._addingOffline = null; updateChannelDuration() } scope.$watch('channel.startTime', () => { updateChannelDuration() }) scope.sortShows = () => { scope.removeOffline(); scope.channel.programs = commonProgramTools.sortShows(scope.channel.programs); updateChannelDuration() } scope.dateForGuide = (date) => { let t = date.toLocaleTimeString(undefined, { hour: "2-digit", minute: "2-digit", second: "2-digit", }); if (t.charCodeAt(1) == 58) { t = "0" + t; } return date.toLocaleDateString(undefined,{ year: "numeric", month: "2-digit", day: "2-digit" }) + " " + t; } scope.sortByDate = () => { scope.removeOffline(); scope.channel.programs = commonProgramTools.sortByDate( scope.channel.programs ); updateChannelDuration() } scope.slideAllPrograms = (offset) => { let t0 = scope.channel.startTime.getTime(); let t1 = t0 - offset; let t = (new Date()).getTime(); let total = scope.channel.duration; while(t1 > t) { //TODO: Replace with division t1 -= total; } scope.channel.startTime = new Date(t1); adjustStartTimeToCurrentProgram(); updateChannelDuration(); } scope.removeDuplicates = () => { scope.channel.programs = commonProgramTools.removeDuplicates(scope.channel.programs); updateChannelDuration(); //oops someone forgot to add this } scope.removeOffline = () => { let tmpProgs = [] let progs = scope.channel.programs for (let i = 0, l = progs.length; i < l; i++) { if ( (progs[i].isOffline !== true) || (progs[i].type === 'redirect') ) { tmpProgs.push(progs[i]); } } scope.channel.programs = tmpProgs updateChannelDuration() } scope.wipeSpecials = () => { scope.channel.programs =commonProgramTools.removeSpecials(scope.channel.programs); updateChannelDuration() } scope.startRemoveShows = () => { let seenIds = {}; let rem = []; scope.channel.programs .map( getShowData ) .filter( data => data.hasShow ) .forEach( x => { if ( seenIds[x.showId] !== true) { seenIds[x.showId] = true; rem.push( { id: x.showId, displayName : x.showDisplayName } ); } } ); scope._removablePrograms = rem; scope._deletedProgramNames = []; } scope.removeShows = (deletedShowIds) => { const p = scope.channel.programs; let set = {}; deletedShowIds.forEach( (a) => set[a] = true ); scope.channel.programs = p.filter( (a) => { let data = getShowData(a); return ( ! data.hasShow || ! set[ data.showId ] ); } ); updateChannelDuration(); } scope.describeFallback = () => { if (scope.channel.offlineMode === 'pic') { if ( (typeof(scope.channel.offlineSoundtrack) !== 'undefined') && (scope.channel.offlineSoundtrack.length > 0) ) { return "pic+sound"; } else { return "pic"; } } else { return "clip"; } } scope.getProgramDisplayTitle = (x) => { return commonProgramTools.getProgramDisplayTitle(x); } scope.programSquareStyle = (x) => { return commonProgramTools.programSquareStyle(x); } scope.doReruns = (rerunStart, rerunBlockSize, rerunRepeats) => { let o =(new Date()).getTimezoneOffset() * 60 * 1000; let start = (o + rerunStart * 60 * 60 * 1000) % (24*60*60*1000); let blockSize = rerunBlockSize * 60*60* 1000; let repeats = rerunRepeats; let programs = []; let block = []; let currentBlockSize = 0; let currentSize = 0; let addBlock = () => { let high = currentSize + currentBlockSize; let m = high % blockSize; if (m >= 1000) { high = high - m + blockSize; } high -= currentSize; let rem = Math.max(0, high - currentBlockSize); if (rem >= 1000) { currentBlockSize += rem; let t = block.length; if ( (t > 0) && block[t-1].isOffline && (block[t-1].type !== 'redirect') ) { block[t-1].duration += rem; } else { block.push( { isOffline: true, duration: rem, } ); } } for (let i = 0; i < repeats; i++) { for (let j = 0; j < block.length; j++) { programs.push( JSON.parse( JSON.stringify(block[j]) ) ); } } currentSize += repeats * currentBlockSize; block = []; currentBlockSize = 0; }; for (let i = 0; i < scope.channel.programs.length; i++) { if (currentBlockSize + scope.channel.programs[i].duration - 500 > blockSize) { addBlock(); } block.push( scope.channel.programs[i] ); currentBlockSize += scope.channel.programs[i].duration; } if (currentBlockSize != 0) { addBlock(); } scope.channel.startTime = new Date( scope.channel.startTime.getTime() - scope.channel.startTime % (24*60*60*1000) + start ); scope.channel.programs = programs; scope.updateChannelDuration(); }; 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; b = (m + b * 60 * 60 * 1000 + o) % m; if (b < a) { b += m; } b -= a; let progs = []; let t = scope.channel.startTime.getTime(); function pos(x) { if (x % m < a) { return m + x % m - a; } else { return x % m - a; } } t -= pos(t); scope.channel.startTime = new Date(t); for (let i = 0, l = scope.channel.programs.length; i < l; i++) { let p = pos(t); if ( (p != 0) && (p + scope.channel.programs[i].duration > b) ) { if (b - 30000 > p) { let d = b- p; t += d; p = pos(t); progs.push( { duration: d, isOffline: true, } ) } //time to pad let d = m - p; progs.push( { duration: d, isOffline: true, channel: ch, type: (typeof(ch) === 'undefined') ? undefined: "redirect", } ) t += d; p = 0; } progs.push( scope.channel.programs[i] ); t += scope.channel.programs[i].duration; } if (pos(t) != 0) { if (b > pos(t)) { let d = b - pos(t) % m; t += d; progs.push( { duration: d, isOffline: true, } ) } let d = m - pos(t); progs.push( { duration: d, isOffline: true, channel: ch, type: (typeof(ch) === 'undefined') ? undefined: "redirect", } ) } scope.channel.programs = progs; updateChannelDuration(); } scope.savePositions = () => { scope.episodeMemory = { saved : false, }; let array = scope.channel.programs; for (let i = 0; i < array.length; i++) { let data = getShowData( array[i] ); if (data.hasShow) { let key = data.showId; if (typeof(scope.episodeMemory[key]) === 'undefined') { scope.episodeMemory[key] = data.order; } } } scope.episodeMemory.saved = true; } scope.recoverPositions = () => { //this is basically the code for cyclic shuffle let array = scope.channel.programs; let shows = {}; let next = {}; let counts = {}; // some precalculation, useful to stop the shuffle from being quadratic... for (let i = 0; i < array.length; i++) { let vid = array[i]; let data = getShowData(vid); if (data.hasShow) { let countKey = { id: data.showId, order: data.order, } let key = JSON.stringify(countKey); let c = ( (typeof(counts[key]) === 'undefined') ? 0 : counts[key] ); counts[key] = c + 1; let showEntry = { c: c, it: vid } if ( typeof(shows[data.showId]) === 'undefined') { shows[data.showId] = []; } shows[data.showId].push(showEntry); } } //this is O(|N| log|M|) where |N| is the total number of TV // episodes and |M| is the maximum number of episodes // in a single show. I am pretty sure this is a lower bound // on the time complexity that's possible here. Object.keys(shows).forEach(function(key,index) { shows[key].sort( (a,b) => { if (a.c == b.c) { return getShowData(a.it).order - getShowData(b.it).order; } else { return (a.c < b.c)? -1: 1; } }); next[key] = 0; if (typeof(scope.episodeMemory[key]) !== 'undefined') { for (let i = 0; i < shows[key].length; i++) { if ( getShowData(shows[key][i].it).order == scope.episodeMemory[key] ) { next[key] = i; break; } } } }); for (let i = 0; i < array.length; i++) { let data = getShowData( array[i] ); if (data.hasShow) { let key = data.showId; var sequence = shows[key]; let j = next[key]; array[i] = sequence[j].it; next[key] = (j + 1) % sequence.length; } } scope.channel.programs = array; updateChannelDuration(); } scope.cannotRecoverPositions = () => { return scope.episodeMemory.saved !== true; } scope.addBreaks = (afterMinutes, minDurationSeconds, maxDurationSeconds) => { let after = afterMinutes * 60 * 1000 + 5000; //allow some seconds of excess let minDur = minDurationSeconds; let maxDur = maxDurationSeconds; let progs = []; 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 && prog.type != 'redirect') { tired = 0; } else { if (tired + prog.duration >= after) { tired = 0; let dur = 1000 * (minDur + Math.floor( (maxDur - minDur) * Math.random() ) ); progs.push( { isOffline : true, duration: dur, }); } tired += prog.duration; } if (i < l) { progs.push(prog); } } scope.channel.programs = progs; updateChannelDuration(); } scope.padTimes = (paddingMod, allow5) => { let mod = paddingMod * 60 * 1000; if (mod == 0) { mod = 60*60*1000; } scope.removeOffline(); let progs = []; let t = scope.channel.startTime.getTime(); t = t - t % mod; scope.channel.startTime = new Date(t); function addPad(force) { let m = t % mod; let r = (mod - t % mod) % mod; if ( (force && (m != 0)) || ((m >= 15*1000) && (r >= 15*1000)) ) { if (allow5 && (m <= 5*60*1000) ) { r = 5*60*1000 - m; } // (If the difference is less than 30 seconds, it's // not worth padding it progs.push( { duration : r, isOffline : true, }); t += r; } } for (let i = 0, l = scope.channel.programs.length; i < l; i++) { let prog = scope.channel.programs[i]; progs.push(prog); t += prog.duration; addPad(i == l - 1); } scope.channel.programs = progs; updateChannelDuration(); } scope.blockShuffle = (blockCount, randomize) => { if (typeof blockCount === 'undefined' || blockCount == null) return let shows = {} let movies = [] let newProgs = [] let progs = scope.channel.programs for (let i = 0, l = progs.length; i < l; i++) { let data = getShowData(progs[i]); if (! data.hasShow) { continue; } else if (data.showId === 'movie.') { movies.push(progs[i]) } else { if (typeof shows[data.showId] === 'undefined') { shows[data.showId] = []; } shows[data.showId].push(progs[i]) } } let keys = Object.keys(shows) let index = 0 if (randomize) { index = getRandomInt(0, keys.length - 1); } while (keys.length > 0) { if (shows[keys[index]].length === 0) { keys.splice(index, 1) if (randomize) { let tmp = index index = getRandomInt(0, keys.length - 1) while (keys.length > 1 && tmp == index) index = getRandomInt(0, keys.length - 1) } else { if (index >= keys.length) index = 0 } continue } for (let i = 0, l = blockCount; i < l; i++) { if (shows[keys[index]].length > 0) newProgs.push(shows[keys[index]].shift()) } if (randomize) { let tmp = index index = getRandomInt(0, keys.length - 1) while (keys.length > 1 && tmp == index) index = getRandomInt(0, keys.length - 1) } else { index++ if (index >= keys.length) index = 0 } } scope.channel.programs = newProgs.concat(movies) updateChannelDuration() } scope.randomShuffle = () => { commonProgramTools.shuffle(scope.channel.programs); updateChannelDuration() } scope.cyclicShuffle = () => { // cyclic shuffle can be reproduced by simulating the effects // of save and recover positions. let oldSaved = scope.episodeMemory; commonProgramTools.shuffle(scope.channel.programs); scope.savePositions(); scope.recoverPositions(); scope.episodeMemory = oldSaved; } scope.equalizeShows = () => { scope.removeDuplicates(); scope.channel.programs = equalizeShows(scope.channel.programs, {} ); updateChannelDuration(); } scope.startFrequencyTweak = () => { let programs = {}; let displayName = {}; for (let i = 0; i < scope.channel.programs.length; i++) { let data = getShowData( scope.channel.programs[i] ); if ( data.hasShow ) { let c = data.showId; displayName[c] = data.showDisplayName; if ( typeof(programs[c]) === 'undefined') { programs[c] = 0; } programs[c] += scope.channel.programs[i].duration; } } let mx = 0; Object.keys(programs).forEach(function(key,index) { mx = Math.max(mx, programs[key]); }); let arr = []; Object.keys(programs).forEach( (key,index) => { let w = Math.ceil( (24.00*programs[key]) / mx ); let obj = { name : key, weight: w, specialCategory: false, displayName: displayName[key], } if (! key.startsWith("tv.")) { obj.specialCategory = true; } arr.push(obj); }); if (arr.length <= 1) { scope._frequencyMessage = "Add more TV shows to the programming before using this option."; } else { scope._frequencyMessage = ""; } scope._frequencyModified = false; scope._programFrequencies = arr; } scope.tweakFrequencies = (freqs) => { var f = {}; for (let i = 0; i < freqs.length; i++) { f[freqs[i].name] = freqs[i].weight; } scope.removeDuplicates(); scope.channel.programs = equalizeShows(scope.channel.programs, f ); updateChannelDuration(); scope.startFrequencyTweak(); scope._frequencyMessage = "TV Show weights have been applied."; } scope.wipeSchedule = () => { scope.channel.programs = []; updateChannelDuration(); } scope.makeOfflineFromChannel = (duration) => { return { durationSeconds: duration, } } scope.addOffline = () => { scope._addingOffline = scope.makeOfflineFromChannel(10*60); } function getShowCode(program) { return getShowData(program).showId; } function getRandomInt(min, max) { min = Math.ceil(min) max = Math.floor(max) return Math.floor(Math.random() * (max - min + 1)) + min } function equalizeShows(array, freqObject) { let shows = {}; let progs = []; for (let i = 0; i < array.length; i++) { if (array[i].isOffline && array[i].type !== 'redirect') { continue; } let vid = array[i]; let code = getShowCode(vid); if ( typeof(shows[code]) === 'undefined') { shows[code] = { total: 0, episodes: [] } } shows[code].total += vid.duration; shows[code].episodes.push(vid); } let maxDuration = 0; Object.keys(shows).forEach(function(key,index) { let w = 3; if ( typeof(freqObject[key]) !== 'undefined') { w = freqObject[key]; } shows[key].total = Math.ceil(shows[key].total / w ); maxDuration = Math.max( maxDuration, shows[key].total ); }); let F = 2; let good = true; Object.keys(shows).forEach(function(key,index) { let amount = Math.floor( (maxDuration*F) / shows[key].total); good = (good && (amount % F == 0) ); }); if (good) { F = 1; } Object.keys(shows).forEach(function(key,index) { let amount = Math.floor( (maxDuration*F) / shows[key].total); let episodes = shows[key].episodes; if (amount % F != 0) { } for (let i = 0; i < amount; i++) { for (let j = 0; j < episodes.length; j++) { progs.push( JSON.parse( angular.toJson(episodes[j]) ) ); } } }); return progs; } scope.replicate = (t) => { let arr = []; for (let j = 0; j < t; j++) { for (let i = 0; i < scope.channel.programs.length; i++) { arr.push( JSON.parse( angular.toJson(scope.channel.programs[i]) ) ); arr[i].$index = i; } } scope.channel.programs = arr; updateChannelDuration(); } scope.shuffleReplicate =(t) => { commonProgramTools.shuffle( scope.channel.programs ); let n = scope.channel.programs.length; let a = Math.floor(n / 2); scope.replicate(t); for (let i = 0; i < t; i++) { commonProgramTools.shuffle( scope.channel.programs, n*i, n*i + a); commonProgramTools.shuffle( scope.channel.programs, n*i + a, n*i + n); } updateChannelDuration(); } scope.updateChannelDuration = updateChannelDuration function updateChannelDuration() { scope.showRotatedNote = false; scope.channel.duration = 0 scope.hasFlex = false; for (let i = 0, l = scope.channel.programs.length; i < l; i++) { scope.channel.programs[i].start = new Date(scope.channel.startTime.valueOf() + scope.channel.duration) scope.channel.programs[i].$index = i; scope.channel.duration += scope.channel.programs[i].duration scope.channel.programs[i].stop = new Date(scope.channel.startTime.valueOf() + scope.channel.duration) if (scope.channel.programs[i].isOffline) { 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.endTime = new Date( scope.channel.startTime.valueOf() + scope.channel.duration ); } scope.error = {} 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) // validate var now = new Date() scope.error.any = true; if (typeof channel.number === "undefined" || channel.number === null || channel.number === "" ) { scope.error.number = "Select a channel number" scope.error.tab = "basic"; } else if (channelNumbers.indexOf(parseInt(channel.number, 10)) !== -1 && scope.isNewChannel) { // we need the parseInt for indexOf to work properly scope.error.number = "Channel number already in use." scope.error.tab = "basic"; } else if (!scope.isNewChannel && channel.number !== scope.beforeEditChannelNumber && channelNumbers.indexOf(parseInt(channel.number, 10)) !== -1) { scope.error.number = "Channel number already in use." scope.error.tab = "basic"; } else if ( ! checkChannelNumber(channel.number) ) { scope.error.number = "Invalid channel number."; scope.error.tab = "basic"; } else if (channel.number < 0 || channel.number > 9999) { scope.error.name = "Enter a valid number (0-9999)" scope.error.tab = "basic"; } else if (typeof channel.name === "undefined" || channel.name === null || channel.name === "") { scope.error.name = "Enter a channel name." scope.error.tab = "basic"; } else if (channel.icon !== "" && !validURL(channel.icon)) { scope.error.icon = "Please enter a valid image URL. Or leave blank." scope.error.tab = "basic"; } else if (channel.overlayIcon && !validURL(channel.icon)) { scope.error.icon = "Please enter a valid image URL. Cant overlay an invalid image." scope.error.tab = "basic"; } else if (now < channel.startTime) { scope.error.startTime = "Start time must not be set in the future." scope.error.tab = "programming"; } else if (channel.programs.length === 0) { scope.error.programs = "No programs have been selected. Select at least one program." scope.error.tab = "programming"; } else if ( channel.watermark.enabled && notValidNumber(scope.channel.watermark.width, 0.01,100)) { scope.error.watermark = "Please include a valid watermark width."; scope.error.tab = "ffmpeg"; } else if ( channel.watermark.enabled && notValidNumber(scope.channel.watermark.verticalMargin, 0.00,100)) { scope.error.watermark = "Please include a valid watermark vertical margin."; scope.error.tab = "ffmpeg"; } else if ( channel.watermark.enabled && notValidNumber(scope.channel.watermark.horizontalMargin, 0.00,100)) { scope.error.watermark = "Please include a valid watermark horizontal margin."; scope.error.tab = "ffmpeg"; } else if ( channel.watermark.enabled && (scope.channel.watermark.width + scope.channel.watermark.horizontalMargin > 100.0) ) { scope.error.watermark = "Horizontal margin + width should not exceed 100."; scope.error.tab = "ffmpeg"; } else if ( channel.watermark.enabled && notValidNumber(scope.channel.watermark.duration, 0)) { scope.error.watermark = "Please include a valid watermark duration."; scope.error.tab = "ffmpeg"; } else if ( channel.offlineMode != 'pic' && (channel.fallback.length == 0) ) { scope.error.fallback = 'Either add a fallback clip or change the fallback mode to Picture.'; scope.error.tab = "flex"; } else { scope.error.any = false; for (let i = 0; i < scope.channel.programs.length; i++) { delete scope.channel.programs[i].$index; } try { removeMinuteVersionsOfFields(channel); let s = angular.toJson(channel); addMinuteVersionsOfFields(); if (s.length > 50*1000*1000) { scope.error.any = true; scope.error.programs = "Channel is too large, can't save."; scope.error.tab = "programming"; } else { let cloned = JSON.parse(s); //clean up some stuff that's only used by the UI: cloned.fillerCollections = cloned.fillerCollections.filter( (f) => { return f.id != 'none'; } ); cloned.fillerCollections.forEach( (c) => { delete c.percentage; delete c.options; } ); await scope.onDone(cloned) s = null; } } catch(err) { addMinuteVersionsOfFields(); $timeout(); console.error(err); scope.error.any = true; scope.error.programs = "Unable to save channel." scope.error.tab = "programming"; } } $timeout(() => { scope.error = {} }, 60000) } } scope.importPrograms = (selectedPrograms) => { for (let i = 0, l = selectedPrograms.length; i < l; i++) { delete selectedPrograms[i].commercials; } scope.channel.programs = scope.channel.programs.concat(selectedPrograms) updateChannelDuration() setTimeout( () => { scope.$apply( () => { scope.minProgramIndex = Math.max(0, scope.channel.programs.length - 100); } ) }, 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) { 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)); } } scope.maxReplicas = () => { if (scope.channel.programs.length == 0) { return 1; } else { return Math.floor( scope.maxSize / (scope.channel.programs.length) ); } } scope.removeItem = (x) => { 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.setTool = (toolName) => { scope.tool = toolName; } scope.hasPrograms = () => { return scope.channel.programs.length > 0; } scope.showPlexLibrary = () => { scope.displayPlexLibrary = true; } scope.toggleTools = () => { scope.showShuffleOptions = !scope.showShuffleOptions localStorage.setItem("channel-tools", (scope.showShuffleOptions? 'on' : 'off') ); } scope.toggleToolsDirection = () => { scope.reverseTools = ! scope.reverseTools; localStorage.setItem("channel-tools-position", (scope.reverseTools? 'left' : 'right') ); } 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 }, { id: 15, description: ":00, :15, :30, :45", allow5: false }, { id: 60, description: ":00", allow5: false }, { id: 20, description: ":00, :20, :40", allow5: false }, { id: 10, description: ":00, :10, :20, ..., :50", allow5: false }, { id: 5, description: ":00, :05, :10, ..., :55", allow5: false }, { id: 60, description: ":00, :05", allow5: true }, { id: 30, description: ":00, :05, :30, :35", allow5: true }, ] 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" }, { id: 10, description: "10 minutes" }, { id: 15, description: "15 minutes" }, { id: 20, description: "20 minutes" }, { id: 25, description: "25 minutes" }, { id: 30, description: "30 minutes" }, { id: 60, description: "1 hour" }, { id: 90, description: "90 minutes" }, { id: 120, description: "2 hours" }, ] scope.breakAfter = -1; scope.minBreakSize = -1; scope.maxBreakSize = -1; let breakSizeOptions = [ { id: 10, description: "10 seconds" }, { id: 15, description: "15 seconds" }, { id: 30, description: "30 seconds" }, { id: 45, description: "45 seconds" }, { id: 60, description: "60 seconds" }, { id: 90, description: "90 seconds" }, { id: 120, description: "2 minutes" }, { id: 180, description: "3 minutes" }, { id: 300, description: "5 minutes" }, { id: 450, description: "7.5 minutes" }, { id: 10*60, description: "10 minutes" }, { id: 20*60, description: "20 minutes" }, { id: 30*60, description: "30 minutes" }, ] scope.minBreakSizeOptions = [ { id: -1, description: "Min Duration" }, ] scope.minBreakSizeOptions = scope.minBreakSizeOptions.concat(breakSizeOptions); scope.maxBreakSizeOptions = [ { id: -1, description: "Max Duration" }, ] scope.maxBreakSizeOptions = scope.maxBreakSizeOptions.concat(breakSizeOptions); scope.rerunStart = -1; scope.rerunBlockSize = -1; scope.rerunBlockSizes = [ { id: -1, description: "Block" }, { id: 4, description: "4 Hours" }, { id: 6, description: "6 Hours" }, { id: 8, description: "8 Hours" }, { id: 12, description: "12 Hours" }, ]; scope.rerunRepeats = -1; scope.rerunRepeatOptions = [ { id: -1, description: "Repeats" }, { id: 2, description: "2" }, { id: 3, description: "3" }, { id: 4, description: "4" }, { id: 6, description: "6" }, ]; scope.rerunsDisabled = () => { return scope.rerunStart == -1 || scope.rerunBlockSize == -1 || scope.rerunRepeats == -1 || (scope.channel.programs.length * scope.rerunRepeats > scope.maxSize) } scope.openFallbackLibrary = () => { scope.showFallbackPlexLibrary = true } scope.importFallback = (selectedPrograms) => { for (let i = 0, l = selectedPrograms.length; i < l && i < 1; i++) { selectedPrograms[i].commercials = [] } scope.channel.fallback = []; if (selectedPrograms.length > 0) { scope.channel.fallback = [ selectedPrograms[0] ]; } scope.showFallbackPlexLibrary = false; } scope.fillerOptions = scope.channel.fillerCollections.map( (f) => { return { id: f.id, name: `(${f.id})`, } }); scope.slide = { value: -1, options: [ {id:-1, description: "Time Amount" }, {id: 1 * 60 * 1000, description: "1 minute" }, {id: 10 * 60 * 1000, description: "10 minutes" }, {id: 15 * 60 * 1000, description: "15 minutes" }, {id: 30 * 60 * 1000, description: "30 minutes" }, {id: 60 * 60 * 1000, description: "1 hour" }, {id: 2 * 60 * 60 * 1000, description: "2 hours" }, {id: 4 * 60 * 60 * 1000, description: "4 hours" }, {id: 8 * 60 * 60 * 1000, description: "8 hours" }, {id:12 * 60 * 60 * 1000, description: "12 hours" }, {id:24 * 60 * 60 * 1000, description: "1 day" }, {id: 7 * 24 * 60 * 60 * 1000, description: "1 week" }, ] } scope.resolutionOptions = [ { id: "", description: "(Use global setting)" }, ]; resolutionOptions.get() .forEach( (a) => { scope.resolutionOptions.push(a) } ); scope.nightStartHours = [ { id: -1, description: "Start" } ]; 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); scope.nightEndHours.push(v); } scope.rerunStartHours = scope.nightStartHours; scope.paddingMod = 30; let fillerOptionsFor = (index) => { let used = {}; let added = {}; for (let i = 0; i < scope.channel.fillerCollections.length; i++) { if (scope.channel.fillerCollections[i].id != 'none' && i != index) { used[ scope.channel.fillerCollections[i].id ] = true; } } let options = []; for (let i = 0; i < scope.fillerOptions.length; i++) { if ( used[scope.fillerOptions[i].id] !== true) { added[scope.fillerOptions[i].id] = true; options.push( scope.fillerOptions[i] ); } } if (scope.channel.fillerCollections[index].id == 'none') { added['none'] = true; options.push( { id: 'none', name: 'Add a filler list...', } ); } if ( added[scope.channel.fillerCollections[index].id] !== true ) { options.push( { id: scope.channel.fillerCollections[index].id, name: `[${f.id}]`, } ); } return options; } scope.programmingHeight = () => { return scope.programming.maxHeight + "rem"; } let setProgrammingHeight = (h) => { scope.programming.step++; $timeout( () => { scope.programming.step--; }, 1000 ) scope.programming.maxHeight = h; localStorage.setItem("channel-programming-list-height", "" + h ); }; scope.programmingZoomIn = () => { let h = scope.programming.maxHeight; h = Math.min( Math.ceil(h + scope.programming.step ), 64); setProgrammingHeight(h); } scope.programmingZoomOut = () => { let h = scope.programming.maxHeight; h = Math.max( Math.floor(h - scope.programming.step ), 2 ); setProgrammingHeight(h); } scope.refreshFillerStuff = () => { if (typeof(scope.channel.fillerCollections) === 'undefined') { return; } addAddFiller(); updatePercentages(); refreshIndividualOptions(); } let updatePercentages = () => { let w = 0; for (let i = 0; i < scope.channel.fillerCollections.length; i++) { if (scope.channel.fillerCollections[i].id !== 'none') { w += scope.channel.fillerCollections[i].weight; } } for (let i = 0; i < scope.channel.fillerCollections.length; i++) { if (scope.channel.fillerCollections[i].id !== 'none') { scope.channel.fillerCollections[i].percentage = (scope.channel.fillerCollections[i].weight * 100 / w).toFixed(2) + "%"; } } }; let addAddFiller = () => { if ( (scope.channel.fillerCollections.length == 0) || (scope.channel.fillerCollections[scope.channel.fillerCollections.length-1].id !== 'none') ) { scope.channel.fillerCollections.push ( { 'id': 'none', 'weight': 300, 'cooldown': 0, } ); } } let refreshIndividualOptions = () => { for (let i = 0; i < scope.channel.fillerCollections.length; i++) { scope.channel.fillerCollections[i].options = fillerOptionsFor(i); } } let refreshFillerOptions = async() => { try { let r = await dizquetv.getAllFillersInfo(); scope.fillerOptions = r.map( (f) => { return { id: f.id, name: f.name, }; } ); scope.refreshFillerStuff(); scope.$apply(); } catch(err) { console.error("Unable to get filler info", err); } }; scope.refreshFillerStuff(); refreshFillerOptions(); function parseResolutionString(s) { var i = s.indexOf('x'); if (i == -1) { i = s.indexOf("×"); if (i == -1) { return {w:1920, h:1080} } } return { w: parseInt( s.substring(0,i) , 10 ), h: parseInt( s.substring(i+1) , 10 ), } } scope.videoRateDefault = "(Use global setting)"; scope.videoBufSizeDefault = "(Use global setting)"; let refreshScreenResolution = async () => { try { let ffmpegSettings = await dizquetv.getFfmpegSettings() if ( (ffmpegSettings.targetResolution != null) && (typeof(ffmpegSettings.targetResolution) !== 'undefined') && (typeof(ffmpegSettings.targetResolution) !== '') ) { let p = parseResolutionString( ffmpegSettings.targetResolution ); scope.resolutionOptions[0] = { id: "", description: `Use global setting (${ffmpegSettings.targetResolution})`, } ffmpegSettings.targetResolution scope.screenW = p.w; scope.screenH = p.h; scope.videoRateDefault = `global setting=${ffmpegSettings.videoBitrate}`; scope.videoBufSizeDefault = `global setting=${ffmpegSettings.videoBufSize}`; $timeout(); } } catch(err) { console.error("Could not fetch ffmpeg settings", err); } } refreshScreenResolution(); scope.showList = () => { return ! scope.showFallbackPlexLibrary; } scope.deleteFillerList =(index) => { scope.channel.fillerCollections.splice(index, 1); scope.refreshFillerStuff(); } scope.durationString = (duration) => { var date = new Date(0); date.setSeconds( Math.floor(duration / 1000) ); // specify value for SECONDS here return date.toISOString().substr(11, 8); } scope.getCurrentWH = () => { if (scope.channel.transcoding.targetResolution !== '') { return parseResolutionString( scope.channel.transcoding.targetResolution ); } return { w: scope.screenW, h: scope.screenH } } scope.getWatermarkPreviewOuter = () => { let tm = scope.getCurrentWH(); let resolutionW = tm.w; let resolutionH = tm.h; let width = 100; let height = width / ( resolutionW / resolutionH ); return { width: `${width}%`, "overflow" : "hidden", "padding-top": 0, "padding-left": 0, "padding-right": 0, "padding-bottom": `${height}%`, position: "relative", } } scope.getWatermarkPreviewRectangle = (p,q) => { let s = scope.getCurrentWH(); if ( (s.w*q) == (s.h*p) ) { //not necessary, hide it return { position: "absolute", visibility: "hidden", } } else { //assume width is equal // s.w / h2 = p / q let h2 = (s.w * q * 100) / (p * s.h); let w2 = 100; let left = undefined; let top = undefined; if (h2 > 100) { //wrong //the other way around w2 = (s.h / s.w) * p * 100 / q; left = (100 - w2) / 2; } else { top = (100 - h2) / 2; } let padding = (100 * q) / p; return { "width" : `${w2}%`, "padding-top": "0", "padding-left": "0", "padding-right": "0", "padding-bottom": `${padding}%`, "margin" : "0", "left": `${left}%`, "top" : `${top}%`, "position": "absolute", } } } scope.getWatermarkSrc = () => { let url = scope.channel.watermark.url; if ( url == null || typeof(url) == 'undefined' || url == '') { url = scope.channel.icon; } return url; } scope.getWatermarkPreviewInner = () => { let width = Math.max(Math.min(100, scope.channel.watermark.width), 0); let res = { width: `${width}%`, margin: "0", position: "absolute", } if (scope.channel.watermark.fixedSize === true) { delete res.width; } let mH = scope.channel.watermark.horizontalMargin; let mV = scope.channel.watermark.verticalMargin; if (scope.channel.watermark.position == 'top-left') { res["top"] = `${mV}%`; res["left"] = `${mH}%`; } else if (scope.channel.watermark.position == 'top-right') { res["top"] = `${mV}%`; res["right"] = `${mH}%`; } else if (scope.channel.watermark.position == 'bottom-right') { res["bottom"] = `${mV}%`; res["right"] = `${mH}%`; } else if (scope.channel.watermark.position == 'bottom-left') { res["bottom"] = `${mV}%`; res["left"] = `${mH}%`; } else { console.log("huh? " + scope.channel.watermark.position ); } return res; } function notValidNumber(x, lower, upper) { if ( (x == null) || (typeof(x) === 'undefined') || isNaN(x) ) { return true; } if ( (typeof(lower) !== 'undefined') && (x < lower) ) { return true; } if ( (typeof(upper) !== 'undefined') && (x > upper) ) { return true; } return false; } let readSlotsResult = (slotsResult) => { scope.channel.programs = slotsResult.programs; let t = (new Date()).getTime(); let t1 =new Date( (new Date( slotsResult.startTime ) ).getTime() ); let total = 0; for (let i = 0; i < slotsResult.programs.length; i++) { total += slotsResult.programs[i].duration; } while(t1 > t) { //TODO: Replace with division t1 -= total; } scope.channel.startTime = new Date(t1); adjustStartTimeToCurrentProgram(); updateChannelDuration(); }; scope.onTimeSlotsDone = (slotsResult) => { scope.channel.scheduleBackup = slotsResult.schedule; readSlotsResult(slotsResult); } scope.onRandomSlotsDone = (slotsResult) => { scope.channel.randomScheduleBackup = slotsResult.schedule; readSlotsResult(slotsResult); } scope.onTimeSlotsButtonClick = () => { let progs = commonProgramTools.removeDuplicates( scope.channel.programs ); scope.timeSlots.startDialog( progs, scope.maxSize, scope.channel.scheduleBackup ); } scope.onRandomSlotsButtonClick = () => { let progs = commonProgramTools.removeDuplicates( scope.channel.programs ); scope.randomSlots.startDialog(progs, scope.maxSize, scope.channel.randomScheduleBackup ); } scope.logoOnChange = (event) => { const formData = new FormData(); formData.append('image', event.target.files[0]); dizquetv.uploadImage(formData).then((response) => { scope.channel.icon = response.data.fileUrl; }) } scope.watermarkOnChange = (event) => { const formData = new FormData(); formData.append('image', event.target.files[0]); dizquetv.uploadImage(formData).then((response) => { scope.channel.watermark.url = response.data.fileUrl; }) } }, pre: function(scope) { scope.timeSlots = null; scope.randomSlots = null; scope.registerTimeSlots = (timeSlots) => { scope.timeSlots = timeSlots; } scope.registerRandomSlots = (randomSlots) => { scope.randomSlots = randomSlots; } }, } } } function validURL(url) { return /^(ftp|http|https):\/\/[^ "]+$/.test(url); } function checkChannelNumber(number) { if ( /^(([1-9][0-9]*)|(0))$/.test(number) ) { let x = parseInt(number); return (0 <= x && x < 10000); } else { return false; } }