1867 lines
78 KiB
JavaScript
1867 lines
78 KiB
JavaScript
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: "",
|
||
videoFlip: 'none',
|
||
}
|
||
scope.channel.splitScreen = {
|
||
// If true, use the global ffmpeg split-screen settings instead
|
||
// of the channel specific ones
|
||
useGlobal: true,
|
||
enabled: false,
|
||
source: "",
|
||
widthPercent: 35,
|
||
loop: true,
|
||
}
|
||
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 = {
|
||
videoFlip: 'none',
|
||
};
|
||
}
|
||
if (typeof(scope.channel.transcoding.videoFlip) === 'undefined') {
|
||
scope.channel.transcoding.videoFlip = 'none';
|
||
}
|
||
if (typeof(scope.channel.splitScreen) === 'undefined') {
|
||
scope.channel.splitScreen = {
|
||
useGlobal: true,
|
||
enabled: false,
|
||
source: "",
|
||
widthPercent: 35,
|
||
loop: true,
|
||
}
|
||
}
|
||
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.channel.programs.length, 0, program);
|
||
scope._selectedOffline = null
|
||
scope._addingOffline = null;
|
||
scrollToLast();
|
||
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)
|
||
}
|
||
}
|
||
|
||
|
||
function getAllMethods(object) {
|
||
|
||
return Object.getOwnPropertyNames(object).filter(function (p) {
|
||
return typeof object[p] == 'function';
|
||
});
|
||
}
|
||
function scrollToLast() {
|
||
var programListElement = document.getElementById("channelConfigProgramList");
|
||
$timeout(() => { programListElement.scrollTo(0, 2000000); }, 0)
|
||
}
|
||
scope.importPrograms = (selectedPrograms, insertPoint) => {
|
||
for (let i = 0, l = selectedPrograms.length; i < l; i++) {
|
||
delete selectedPrograms[i].commercials;
|
||
}
|
||
|
||
var programListElement = document.getElementById("channelConfigProgramList");
|
||
if (insertPoint === "start") {
|
||
scope.channel.programs = selectedPrograms.concat(scope.channel.programs);
|
||
programListElement.scrollTo(0, 0);
|
||
} else if (insertPoint === "current") {
|
||
scope.channel.programs = [
|
||
...scope.channel.programs.slice(0, scope.currentStartIndex),
|
||
...selectedPrograms,
|
||
...scope.channel.programs.slice(scope.currentStartIndex)
|
||
];
|
||
} else {
|
||
scope.channel.programs = scope.channel.programs.concat(selectedPrograms)
|
||
|
||
scrollToLast();
|
||
}
|
||
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.channel.programs.length, 0, program);
|
||
scrollToLast();
|
||
|
||
} 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)";
|
||
|
||
scope.randomizeBlockShuffle = false;
|
||
|
||
scope.advancedTools = (localStorage.getItem("channel-programming-advanced-tools" ) === "show");
|
||
|
||
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) => {
|
||
if (slotsResult === null) {
|
||
delete scope.channel.scheduleBackup;
|
||
} else {
|
||
scope.channel.scheduleBackup = slotsResult.schedule;
|
||
readSlotsResult(slotsResult);
|
||
}
|
||
}
|
||
|
||
scope.onRandomSlotsDone = (slotsResult) => {
|
||
if (slotsResult === null) {
|
||
delete scope.channel.randomScheduleBackup;
|
||
} else {
|
||
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.rerollRandomSlots = () => {
|
||
let progs = commonProgramTools.removeDuplicates( scope.channel.programs );
|
||
scope.randomSlots.startDialog(
|
||
progs, scope.maxSize, scope.channel.randomScheduleBackup,
|
||
true
|
||
);
|
||
}
|
||
scope.hasNoRandomSlots = () => {
|
||
return (
|
||
(typeof(scope.channel.randomScheduleBackup) === 'undefined' )
|
||
||
|
||
(scope.channel.randomScheduleBackup == null)
|
||
);
|
||
}
|
||
|
||
scope.rerollTimeSlots = () => {
|
||
let progs = commonProgramTools.removeDuplicates( scope.channel.programs );
|
||
scope.timeSlots.startDialog(
|
||
progs, scope.maxSize, scope.channel.scheduleBackup,
|
||
true
|
||
);
|
||
}
|
||
scope.hasNoTimeSlots = () => {
|
||
return (
|
||
(typeof(scope.channel.scheduleBackup) === 'undefined' )
|
||
||
|
||
(scope.channel.scheduleBackup == null)
|
||
);
|
||
}
|
||
scope.toggleAdvanced = () => {
|
||
scope.advancedTools = ! scope.advancedTools;
|
||
localStorage.setItem("channel-programming-advanced-tools" , scope.advancedTools ? "show" : "hide");
|
||
}
|
||
scope.hasAdvancedTools = () => {
|
||
return scope.advancedTools;
|
||
}
|
||
|
||
scope.toolWide = () => {
|
||
if ( scope.hasAdvancedTools()) {
|
||
return {
|
||
"col-xl-6": true,
|
||
"col-md-12" : true
|
||
}
|
||
} else {
|
||
return {
|
||
"col-xl-12": true,
|
||
"col-lg-12" : true
|
||
}
|
||
}
|
||
}
|
||
|
||
scope.toolThin = () => {
|
||
if ( scope.hasAdvancedTools()) {
|
||
return {
|
||
"col-xl-3": true,
|
||
"col-lg-6" : true
|
||
}
|
||
} else {
|
||
return {
|
||
"col-xl-6": true,
|
||
"col-lg-6" : true
|
||
}
|
||
}
|
||
}
|
||
|
||
|
||
|
||
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;
|
||
})
|
||
}
|
||
|
||
scope.splitScreenOnChange = (event) => {
|
||
const formData = new FormData();
|
||
formData.append('video', event.target.files[0]);
|
||
// include channel number so server stores file under channel hierarchy and persists it
|
||
if (typeof(scope.channel) !== 'undefined' && scope.channel != null && typeof(scope.channel.number) !== 'undefined') {
|
||
formData.append('channel', scope.channel.number);
|
||
}
|
||
dizquetv.uploadVideo(formData).then((response) => {
|
||
scope.channel.splitScreen.source = response.data.fileUrl;
|
||
}).catch((err) => {
|
||
console.error('Error uploading split-screen video', err);
|
||
})
|
||
}
|
||
|
||
|
||
},
|
||
|
||
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;
|
||
}
|
||
}
|