Random Slots

This commit is contained in:
vexorian 2021-02-19 16:20:46 -04:00
parent 3fadcc487c
commit d6b2bd1d5e
9 changed files with 1063 additions and 8 deletions

View File

@ -8,8 +8,9 @@ const constants = require('./constants');
const FFMPEGInfo = require('./ffmpeg-info');
const PlexServerDB = require('./dao/plex-server-db');
const Plex = require("./plex.js");
const FillerDB = require('./dao/filler-db');
const timeSlotsService = require('./services/time-slots-service');
const randomSlotsService = require('./services/random-slots-service');
module.exports = { router: api }
function api(db, channelDB, fillerDB, xmltvInterval, guideService, _m3uService ) {
@ -583,9 +584,23 @@ function api(db, channelDB, fillerDB, xmltvInterval, guideService, _m3uService
//tool services
router.post('/api/channel-tools/time-slots', async (req, res) => {
try {
await m3uService.clearCache();
let toolRes = await timeSlotsService(req.body.programs, req.body.schedule);
if ( typeof(toolRes.userError) !=='undefined') {
console.error("time slots error: " + toolRes.userError);
return res.status(400).send(toolRes.userError);
}
res.status(200).send(toolRes);
} catch(err) {
console.error(err);
res.status(500).send("Internal error");
}
});
router.post('/api/channel-tools/random-slots', async (req, res) => {
try {
let toolRes = await randomSlotsService(req.body.programs, req.body.schedule);
if ( typeof(toolRes.userError) !=='undefined') {
console.error("random slots error: " + toolRes.userError);
return res.status(400).send(toolRes.userError);
}
res.status(200).send(toolRes);

View File

@ -0,0 +1,469 @@
const constants = require("../constants");
const random = require('../helperFuncs').random;
const MINUTE = 60*1000;
const DAY = 24*60*MINUTE;
const LIMIT = 40000;
//This is a quadruplicate code, but maybe it doesn't have to be?
function getShow(program) {
//used for equalize and frequency tweak
if (program.isOffline) {
if (program.type == 'redirect') {
return {
description : `Redirect to channel ${program.channel}`,
id: "redirect." + program.channel,
channel: program.channel,
}
} else {
return null;
}
} else if ( (program.type == 'episode') && ( typeof(program.showTitle) !== 'undefined' ) ) {
return {
description: program.showTitle,
id: "tv." + program.showTitle,
}
} else {
return {
description: "Movies",
id: "movie.",
}
}
}
function shuffle(array, lo, hi ) {
if (typeof(lo) === 'undefined') {
lo = 0;
hi = array.length;
}
let currentIndex = hi, temporaryValue, randomIndex
while (lo !== currentIndex) {
randomIndex = random.integer(lo, currentIndex-1);
currentIndex -= 1
temporaryValue = array[currentIndex]
array[currentIndex] = array[randomIndex]
array[randomIndex] = temporaryValue
}
return array
}
function _wait(t) {
return new Promise((resolve) => {
setTimeout(resolve, t);
});
}
function getProgramId(program) {
let s = program.serverKey;
if (typeof(s) === 'undefined') {
s = 'unknown';
}
let p = program.key;
if (typeof(p) === 'undefined') {
p = 'unknown';
}
return s + "|" + p;
}
function addProgramToShow(show, program) {
if ( (show.id == 'flex.') || show.id.startsWith("redirect.") ) {
//nothing to do
return;
}
let id = getProgramId(program)
if(show.programs[id] !== true) {
show.programs.push(program);
show.programs[id] = true
}
}
function getShowOrderer(show) {
if (typeof(show.orderer) === 'undefined') {
let sortedPrograms = JSON.parse( JSON.stringify(show.programs) );
sortedPrograms.sort((a, b) => {
if (a.season === b.season) {
if (a.episode > b.episode) {
return 1
} else {
return -1
}
} else if (a.season > b.season) {
return 1;
} else if (b.season > a.season) {
return -1;
} else {
return 0
}
});
let position = 0;
while (
(position + 1 < sortedPrograms.length )
&&
(
show.founder.season !== sortedPrograms[position].season
||
show.founder.episode !== sortedPrograms[position].episode
)
) {
position++;
}
show.orderer = {
current : () => {
return sortedPrograms[position];
},
next: () => {
position = (position + 1) % sortedPrograms.length;
},
}
}
return show.orderer;
}
function getShowShuffler(show) {
if (typeof(show.shuffler) === 'undefined') {
if (typeof(show.programs) === 'undefined') {
throw Error(show.id + " has no programs?")
}
let randomPrograms = JSON.parse( JSON.stringify(show.programs) );
let n = randomPrograms.length;
shuffle( randomPrograms, 0, n);
let position = 0;
show.shuffler = {
current : () => {
return randomPrograms[position];
},
next: () => {
position++;
if (position == n) {
let a = Math.floor(n / 2);
shuffle(randomPrograms, 0, a );
shuffle(randomPrograms, a, n );
position = 0;
}
},
}
}
return show.shuffler;
}
module.exports = async( programs, schedule ) => {
if (! Array.isArray(programs) ) {
return { userError: 'Expected a programs array' };
}
if (typeof(schedule) === 'undefined') {
return { userError: 'Expected a schedule' };
}
//verify that the schedule is in the correct format
if (! Array.isArray(schedule.slots) ) {
return { userError: 'Expected a "slots" array in schedule' };
}
if (typeof(schedule).period === 'undefined') {
schedule.period = DAY;
}
for (let i = 0; i < schedule.slots.length; i++) {
if (typeof(schedule.slots[i].duration) === 'undefined') {
return { userError: "Each slot should have a duration" };
}
if (typeof(schedule.slots[i].showId) === 'undefined') {
return { userError: "Each slot should have a showId" };
}
if (
(schedule.slots[i].duration <= 0)
|| (Math.floor(schedule.slots[i].duration) != schedule.slots[i].duration)
) {
return { userError: "Slot duration should be a integer number of milliseconds greater than 0" };
}
if ( isNaN(schedule.slots[i].cooldown) ) {
schedule.slots[i].cooldown = 0;
}
if ( isNaN(schedule.slots[i].weight) ) {
schedule.slots[i].weight = 1;
}
}
if (typeof(schedule.pad) === 'undefined') {
return { userError: "Expected schedule.pad" };
}
if (typeof(schedule.maxDays) == 'undefined') {
return { userError: "schedule.maxDays must be defined." };
}
if (typeof(schedule.flexPreference) === 'undefined') {
schedule.flexPreference = "distribute";
}
if (typeof(schedule.padStyle) === 'undefined') {
schedule.padStyle = "slot";
}
if (schedule.padStyle !== "slot" && schedule.padStyle !== "episode") {
return { userError: `Invalid schedule.padStyle value: "${schedule.padStyle}"` };
}
let flexBetween = ( schedule.flexPreference !== "end" );
// throttle so that the stream is not affected negatively
let steps = 0;
let throttle = async() => {
if (steps++ == 10) {
steps = 0;
await _wait(1);
}
}
let showsById = {};
let shows = [];
function getNextForSlot(slot, remaining) {
//remaining doesn't restrict what next show is picked. It is only used
//for shows with flexible length (flex and redirects)
if (slot.showId === "flex.") {
return {
isOffline: true,
duration: remaining,
}
}
let show = shows[ showsById[slot.showId] ];
if (slot.showId.startsWith("redirect.")) {
return {
isOffline: true,
type: "redirect",
duration: remaining,
channel: show.channel,
}
} else if (slot.order === 'shuffle') {
return getShowShuffler(show).current();
} else if (slot.order === 'next') {
return getShowOrderer(show).current();
}
}
function advanceSlot(slot) {
if ( (slot.showId === "flex.") || (slot.showId.startsWith("redirect") ) ) {
return;
}
let show = shows[ showsById[slot.showId] ];
if (slot.order === 'shuffle') {
return getShowShuffler(show).next();
} else if (slot.order === 'next') {
return getShowOrderer(show).next();
}
}
function makePadded(item) {
let padOption = schedule.pad;
if (schedule.padStyle === "slot") {
padOption = 1;
}
let x = item.duration;
let m = x % padOption;
let f = 0;
if ( (m > constants.SLACK) && (padOption - m > constants.SLACK) ) {
f = padOption - m;
}
return {
item: item,
pad: f,
totalDuration: item.duration + f,
}
}
// load the programs
for (let i = 0; i < programs.length; i++) {
let p = programs[i];
let show = getShow(p);
if (show != null) {
if (typeof(showsById[show.id] ) === 'undefined') {
showsById[show.id] = shows.length;
shows.push( show );
show.founder = p;
show.programs = [];
} else {
show = shows[ showsById[show.id] ];
}
addProgramToShow( show, p );
}
}
let s = schedule.slots;
let ts = (new Date() ).getTime();
let curr = ts - ts % DAY;
let t0 = ts;
let p = [];
let t = t0;
let wantedFinish = 0;
let hardLimit = t0 + schedule.maxDays * DAY;
let pushFlex = (d) => {
if (d > 0) {
t += d;
if ( (p.length > 0) && p[p.length-1].isOffline && (p[p.length-1].type != 'redirect') ) {
p[p.length-1].duration += d;
} else {
p.push( {
duration: d,
isOffline : true,
} );
}
}
}
let slotLastPlayed = {};
while ( (t < hardLimit) && (p.length < LIMIT) ) {
await throttle();
//ensure t is padded
let m = t % schedule.pad;
if ( (t % schedule.pad > constants.SLACK) && (schedule.pad - m > constants.SLACK) ) {
pushFlex( schedule.pad - m );
continue;
}
let slot = null;
let slotIndex = null;
let remaining = null;
let n = 0;
let minNextTime = t + 24*DAY;
for (let i = 0; i < s.length; i++) {
if ( typeof( slotLastPlayed[i] ) !== undefined ) {
let lastt = slotLastPlayed[i];
minNextTime = Math.min( minNextTime, lastt + s[i].cooldown );
if (t - lastt < s[i].cooldown - constants.SLACK ) {
continue;
}
}
n += s[i].weight;
if ( random.bool(s[i].weight,n) ) {
slot = s[i];
slotIndex = i;
remaining = s[i].duration;
}
}
if (slot == null) {
//Nothing to play, likely due to cooldown
pushFlex( minNextTime - t);
continue;
}
let item = getNextForSlot(slot, remaining);
if (item.isOffline) {
//flex or redirect. We can just use the whole duration
p.push(item);
t += remaining;
slotLastPlayed[ slotIndex ] = t;
continue;
}
if (item.duration > remaining) {
// Slide
p.push(item);
t += item.duration;
slotLastPlayed[ slotIndex ] = t;
advanceSlot(slot);
continue;
}
let padded = makePadded(item);
let total = padded.totalDuration;
advanceSlot(slot);
let pads = [ padded ];
while(true) {
let item2 = getNextForSlot(slot);
if (total + item2.duration > remaining) {
break;
}
let padded2 = makePadded(item2);
pads.push(padded2);
advanceSlot(slot);
total += padded2.totalDuration;
}
let temt = t + total;
let rem = 0;
if (
(temt % schedule.pad >= constants.SLACK)
&& (temt % schedule.pad < schedule.pad - constants.SLACK)
) {
rem = schedule.pad - temt % schedule.pad;
}
if (flexBetween && (schedule.padStyle === 'episode') ) {
let div = Math.floor(rem / schedule.pad );
let mod = rem % schedule.pad;
// add mod to the latest item
pads[ pads.length - 1].pad += mod;
pads[ pads.length - 1].totalDuration += mod;
let sortedPads = pads.map( (p, $index) => {
return {
pad: p.pad,
index : $index,
}
});
sortedPads.sort( (a,b) => { return a.pad - b.pad; } );
for (let i = 0; i < pads.length; i++) {
let q = Math.floor( div / pads.length );
if (i < div % pads.length) {
q++;
}
let j = sortedPads[i].index;
pads[j].pad += q * schedule.pad;
}
} else if (flexBetween) {
//just distribute it equitatively
let div = rem / pads.length;
for (let i = 0; i < pads.length; i++) {
pads[i].pad += div;
}
} else {
//also add div to the latest item
pads[ pads.length - 1].pad += rem;
pads[ pads.length - 1].totalDuration += rem;
}
// now unroll them all
for (let i = 0; i < pads.length; i++) {
p.push( pads[i].item );
t += pads[i].item.duration;
slotLastPlayed[ slotIndex ] = t;
pushFlex( pads[i].pad );
}
}
while ( (t > hardLimit) || (p.length >= LIMIT) ) {
t -= p.pop().duration;
}
let m = t % schedule.period;
let rem = 0;
if (m > wantedFinish) {
rem = schedule.period + wantedFinish - m;
} else if (m < wantedFinish) {
rem = wantedFinish - m;
}
if (rem > constants.SLACK) {
pushFlex(rem);
}
return {
programs: p,
startTime: (new Date(t0)).toISOString(),
}
}

View File

@ -27,6 +27,7 @@ app.directive('channelRedirect', require('./directives/channel-redirect'))
app.directive('plexServerEdit', require('./directives/plex-server-edit'))
app.directive('channelConfig', require('./directives/channel-config'))
app.directive('timeSlotsScheduleEditor', require('./directives/time-slots-schedule-editor'))
app.directive('randomSlotsScheduleEditor', require('./directives/random-slots-schedule-editor'))
app.controller('settingsCtrl', require('./controllers/settings'))
app.controller('channelsCtrl', require('./controllers/channels'))

View File

@ -1846,8 +1846,7 @@ module.exports = function ($timeout, $location, dizquetv, resolutionOptions) {
return false;
}
scope.onTimeSlotsDone = (slotsResult) => {
let readSlotsResult = (slotsResult) => {
scope.channel.programs = slotsResult.programs;
let t = (new Date()).getTime();
@ -1857,7 +1856,6 @@ module.exports = function ($timeout, $location, dizquetv, resolutionOptions) {
total += slotsResult.programs[i].duration;
}
scope.channel.scheduleBackup = slotsResult.schedule;
while(t1 > t) {
//TODO: Replace with division
@ -1866,10 +1864,27 @@ module.exports = function ($timeout, $location, dizquetv, resolutionOptions) {
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 = () => {
scope.timeSlots.startDialog(scope.channel.programs, scope.maxSize, scope.channel.scheduleBackup );
}
scope.onRandomSlotsButtonClick = () => {
scope.randomSlots.startDialog(scope.channel.programs, scope.maxSize, scope.channel.randomScheduleBackup );
}
scope.logoOnChange = (event) => {
const formData = new FormData();
@ -1892,9 +1907,14 @@ module.exports = function ($timeout, $location, dizquetv, resolutionOptions) {
pre: function(scope) {
scope.timeSlots = null;
scope.randomSlots = null;
scope.registerTimeSlots = (timeSlots) => {
scope.timeSlots = timeSlots;
}
scope.registerRandomSlots = (randomSlots) => {
scope.randomSlots = randomSlots;
}
},
}

View File

@ -0,0 +1,338 @@
module.exports = function ($timeout, dizquetv) {
const MINUTE = 60*1000;
const HOUR = 60*MINUTE;
const DAY = 24*HOUR;
const WEEK = 7 * DAY;
return {
restrict: 'E',
templateUrl: 'templates/random-slots-schedule-editor.html',
replace: true,
scope: {
linker: "=linker",
onDone: "=onDone"
},
link: function (scope, element, attrs) {
scope.limit = 50000;
scope.visible = false;
scope.badTimes = false;
scope._editedTime = null;
let showsById;
let shows;
function reset() {
showsById = {};
shows = [];
scope.schedule = {
maxDays: 365,
flexPreference : "distribute",
padStyle: "slot",
randomDistribution: "uniform",
slots : [],
pad: 1,
}
}
reset();
function loadBackup(backup) {
scope.schedule = JSON.parse( JSON.stringify(backup) );
if (typeof(scope.schedule.pad) == 'undefined') {
scope.schedule.pad = 1;
}
let slots = scope.schedule.slots;
for (let i = 0; i < slots.length; i++) {
let found = false;
for (let j = 0; j < scope.showOptions.length; j++) {
if (slots[i].showId == scope.showOptions[j].id) {
found = true;
}
}
if (! found) {
slots[i].showId = "flex.";
slots[i].order = "shuffle";
}
}
if (typeof(scope.schedule.flexPreference) === 'undefined') {
scope.schedule.flexPreference = "distribute";
}
if (typeof(scope.schedule.padStyle) === 'undefined') {
scope.schedule.padStyle = "slot";
}
if (typeof(scope.schedule.randomDistribution) === 'undefined') {
scope.schedule.randomDistribution = "uniform";
}
scope.refreshSlots();
}
getTitle = (index) => {
let showId = scope.schedule.slots[index].showId;
for (let i = 0; i < scope.showOptions.length; i++) {
if (scope.showOptions[i].id == showId) {
return scope.showOptions[i].description;
}
}
return "Unknown";
}
scope.isWeekly = () => {
return (scope.schedule.period === WEEK);
};
scope.addSlot = () => {
scope.schedule.slots.push(
{
duration: 30 * MINUTE,
showId: "flex.",
order: "next",
cooldown : 0,
}
);
}
scope.timeColumnClass = () => {
return { "col-md-1": true};
}
scope.programColumnClass = () => {
return { "col-md-6": true};
};
scope.durationOptions = [
{ id: 5 * MINUTE , description: "5 Minutes" },
{ id: 10 * MINUTE , description: "10 Minutes" },
{ id: 15 * MINUTE , description: "15 Minutes" },
{ id: 20 * MINUTE , description: "20 Minutes" },
{ id: 25 * MINUTE , description: "25 Minutes" },
{ id: 30 * MINUTE , description: "30 Minutes" },
{ id: 45 * MINUTE , description: "45 Minutes" },
{ id: 1 * HOUR , description: "1 Hour" },
{ id: 90 * MINUTE , description: "90 Minutes" },
{ id: 100 * MINUTE , description: "100 Minutes" },
{ id: 2 * HOUR , description: "2 Hours" },
{ id: 3 * HOUR , description: "3 Hours" },
{ id: 4 * HOUR , description: "4 Hours" },
{ id: 5 * HOUR , description: "5 Hours" },
{ id: 6 * HOUR , description: "6 Hours" },
{ id: 8 * HOUR , description: "8 Hours" },
{ id: 10* HOUR , description: "10 Hours" },
{ id: 12* HOUR , description: "12 Hours" },
{ id: 1 * DAY , description: "1 Day" },
];
scope.cooldownOptions = [
{ id: 0 , description: "No cooldown" },
{ id: 1 * MINUTE , description: "1 Minute" },
{ id: 5 * MINUTE , description: "5 Minutes" },
{ id: 10 * MINUTE , description: "10 Minutes" },
{ id: 15 * MINUTE , description: "15 Minutes" },
{ id: 20 * MINUTE , description: "20 Minutes" },
{ id: 25 * MINUTE , description: "25 Minutes" },
{ id: 30 * MINUTE , description: "30 Minutes" },
{ id: 45 * MINUTE , description: "45 Minutes" },
{ id: 1 * HOUR , description: "1 Hour" },
{ id: 90 * MINUTE , description: "90 Minutes" },
{ id: 100 * MINUTE , description: "100 Minutes" },
{ id: 2 * HOUR , description: "2 Hours" },
{ id: 3 * HOUR , description: "3 Hours" },
{ id: 4 * HOUR , description: "4 Hours" },
{ id: 5 * HOUR , description: "5 Hours" },
{ id: 6 * HOUR , description: "6 Hours" },
{ id: 8 * HOUR , description: "8 Hours" },
{ id: 10* HOUR , description: "10 Hours" },
{ id: 12* HOUR , description: "12 Hours" },
{ id: 1 * DAY , description: "1 Day" },
{ id: 1 * DAY , description: "2 Days" },
{ id: 3 * DAY + 12 * HOUR , description: "3.5 Days" },
{ id: 7 * DAY , description: "1 Week" },
];
scope.flexOptions = [
{ id: "distribute", description: "Between videos" },
{ id: "end", description: "End of the slot" },
]
scope.distributionOptions = [
{ id: "uniform", description: "Uniform" },
{ id: "weighted", description: "Weighted" },
]
scope.padOptions = [
{id: 1, description: "Do not pad" },
{id: 1*MINUTE, description: "0:00, 0:01, 0:02, ..., 0:59" },
{id: 5*MINUTE, description: "0:00, 0:05, 0:10, ..., 0:55" },
{id: 10*60*1000, description: "0:00, 0:10, 0:20, ..., 0:50" },
{id: 15*60*1000, description: "0:00, 0:15, 0:30, ..., 0:45" },
{id: 30*60*1000, description: "0:00, 0:30" },
{id: 1*60*60*1000, description: "0:00" },
];
scope.padStyleOptions = [
{id: "episode" , description: "Pad Episodes" },
{id: "slot" , description: "Pad Slots" },
];
scope.showOptions = [];
scope.orderOptions = [
{ id: "next", description: "Play Next" },
{ id: "shuffle", description: "Shuffle" },
];
let doIt = async() => {
let res = await dizquetv.calculateRandomSlots(scope.programs, scope.schedule );
for (let i = 0; i < scope.schedule.slots.length; i++) {
delete scope.schedule.slots[i].weightPercentage;
}
res.schedule = scope.schedule;
return res;
}
let startDialog = (programs, limit, backup) => {
scope.limit = limit;
scope.programs = programs;
reset();
programs.forEach( (p) => {
let show = getShow(p);
if (show != null) {
if (typeof(showsById[show.id]) === 'undefined') {
showsById[show.id] = shows.length;
shows.push( show );
} else {
show = shows[ showsById[show.id] ];
}
}
} );
scope.showOptions = shows.map( (show) => { return show } );
scope.showOptions.push( {
id: "flex.",
description: "Flex",
} );
if (typeof(backup) !== 'undefined') {
loadBackup(backup);
}
scope.visible = true;
}
scope.linker( {
startDialog: startDialog,
} );
scope.finished = async (cancel) => {
scope.error = null;
if (!cancel) {
try {
scope.loading = true;
$timeout();
scope.onDone( await doIt() );
scope.visible = false;
} catch(err) {
console.error("Unable to generate channel lineup", err);
scope.error = "There was an error processing the schedule";
return;
} finally {
scope.loading = false;
$timeout();
}
} else {
scope.visible = false;
}
}
scope.deleteSlot = (index) => {
scope.schedule.slots.splice(index, 1);
}
scope.hasTimeError = (slot) => {
return typeof(slot.timeError) !== 'undefined';
}
scope.disableCreateLineup = () => {
if (scope.badTimes) {
return true;
}
if (typeof(scope.schedule.maxDays) === 'undefined') {
return true;
}
if (scope.schedule.slots.length == 0) {
return true;
}
return false;
}
scope.canShowSlot = (slot) => {
return (slot.showId != 'flex.') && !(slot.showId.startsWith('redirect.'));
}
scope.refreshSlots = () => {
let sum = 0;
for (let i = 0; i < scope.schedule.slots.length; i++) {
sum += scope.schedule.slots[i].weight;
}
for (let i = 0; i < scope.schedule.slots.length; i++) {
if (scope.schedule.slots[i].showId == 'movie.') {
scope.schedule.slots[i].order = "shuffle";
}
if ( isNaN(scope.schedule.slots[i].cooldown) ) {
scope.schedule.slots[i].cooldown = 0;
}
scope.schedule.slots[i].weightPercentage
= (100 * scope.schedule.slots[i].weight / sum).toFixed(2) + "%";
}
$timeout();
}
scope.randomDistributionChanged = () => {
if (scope.schedule.randomDistribution === 'uniform') {
for (let i = 0; i < scope.schedule.slots.length; i++) {
scope.schedule.slots[i].weight = 1;
}
} else {
for (let i = 0; i < scope.schedule.slots.length; i++) {
scope.schedule.slots[i].weight = 300;
}
}
scope.refreshSlots();
}
}
};
}
//This is a duplicate code, but maybe it doesn't have to be?
function getShow(program) {
//used for equalize and frequency tweak
if (program.isOffline) {
if (program.type == 'redirect') {
return {
description : `Redirect to channel ${program.channel}`,
id: "redirect." + program.channel,
channel: program.channel,
}
} else {
return null;
}
} else if ( (program.type == 'episode') && ( typeof(program.showTitle) !== 'undefined' ) ) {
return {
description: program.showTitle,
id: "tv." + program.showTitle,
}
} else {
return {
description: "Movies",
id: "movie.",
}
}
}

View File

@ -68,7 +68,7 @@ module.exports = function ($timeout, dizquetv) {
}
}
getTitle = (index) => {
let getTitle = (index) => {
let showId = scope.schedule.slots[index].showId;
for (let i = 0; i < scope.showOptions.length; i++) {
if (scope.showOptions[i].id == showId) {

View File

@ -449,7 +449,7 @@
<p ng-show='showHelp.check'>Slides the whole schedule. The &quot;Fast-Forward&quot; button will advance the stream by the specified amount of time. The &quot;Rewind&quot; button does the opposite.</p>
</div>
<div class="col-xl-6 col-lg-12" style="padding: 5px;" ng-show="hasPrograms()">
<div class="col-xl-3 col-lg-6" style="padding: 5px;" ng-show="hasPrograms()">
<div class="input-group">
<button class='btn btn-sm btn-warning form-control' ng-click="onTimeSlotsButtonClick()"
title="Time Slots..."
@ -457,7 +457,20 @@
<i class='fas fa-blender'></i> Time Slots...
</button>
</div>
<p ng-show='showHelp.check'>A more advanced dialog wip description.</p>
<p ng-show='showHelp.check'>This allows to schedul specific shows to run at specific time slots of the day or a week. It&apos;s recommended you first populate the channel with the episodes from the shows you want to play and/or other content like movies and redirects.</p>
</div>
<div class="col-xl-3 col-lg-6" style="padding: 5px;" ng-show="hasPrograms()">
<div class="input-group">
<button class='btn btn-sm btn-warning form-control' ng-click="onRandomSlotsButtonClick()"
title="Random Slots..."
>
<i class='fas fa-flask'></i> Random Slots...
</button>
</div>
<p ng-show='showHelp.check'>This is similar to Time Slots, but instead of time sections, you pick a probability to play each tv show and the length of the block.</p>
</div>
@ -849,4 +862,5 @@
<plex-library height="300" limit=1 visible="showFallbackPlexLibrary" on-finish="importFallback"></plex-library>
<channel-redirect visible="_displayRedirect" on-done="finishRedirect" form-title="_redirectTitle" program="_selectedRedirect" ></channel-redirect>
<time-slots-schedule-editor linker="registerTimeSlots" on-done="onTimeSlotsDone"></time-slots-schedule-editor>
<random-slots-schedule-editor linker="registerRandomSlots" on-done="onRandomSlotsDone"></random-slots-schedule-editor>
</div>

View File

@ -0,0 +1,185 @@
<div ng-show="visible">
<div class="modal" tabindex="-1" role="dialog" style="display: block; background-color: rgba(0, 0, 0, .5);">
<div class="modal-dialog modal-dialog-scrollable modal-xl" role="document">
<div class="modal-content">
<div>
<div class="modal-header">
<h5 class="modal-title">Random Slots</h5>
</div>
</div>
<div class="modal-body" ng-show='loading' >
<p><span class='loader'></span> Generating lineup, please wait...</p>
</div>
<div class="modal-body" ng-show='! loading' >
<div class="form-row" ng-repeat = "slot in schedule.slots" track-by = "$index">
<div class='form-group col-md-2' >
<label ng-if="$index==0" for="showTime{{$index}}">Duration</label>
<select
id="showDuration{{$index}}" class="custom-select form-control"
ng-model="slot.duration" ng-options="o.id as o.description for o in durationOptions"
ng-change="refreshSlots()"
>
</select>
<small class='form-text text-danger'>{{slot.timeError}}</small>
</div>
<div class='form-group col-md-5' >
<label ng-if="$index==0" for="showId{{$index}}">Program</label>
<select
id="showId{{$index}}" class="custom-select form-control"
ng-model="slot.showId" ng-options="o.id as o.description for o in showOptions"
ng-change="refreshSlots()"
>
</select>
</div>
<div class='form-group col-md-2'>
<label ng-if="$index==0" for="showCooldown{{$index}}" >Cooldown</label>
<select
id="showCooldown{{$index}}" class="custom-select form-control"
ng-model="slot.cooldown" ng-options="o.id as o.description for o in cooldownOptions"
ng-change="refreshSlots()"
>
</select>
</div>
<div class='form-group col-md-2'>
<label ng-if="$index==0" for="showOrder{{$index}}" ng-show="canShowSlot(slot)" >Order</label>
<select
id="showOrder{{$index}}" class="custom-select form-control"
ng-model="slot.order" ng-options="o.id as o.description for o in orderOptions"
ng-change="refreshSlots()"
ng-show="canShowSlot(slot)"
ng-disabled="slot.showId == 'movie.'"
>
</select>
</div>
<div class='form-group col-md-1'>
<label ng-if="$index==0" for="delete{{$index}}">-</label>
<button id='delete{{$index}}' class='btn btn-link form-control' ng-click='deleteSlot($index)' >
<i class='text-danger fa fa-trash-alt'></i>
</button>
</div>
<div class='form-group col-md-2'>
</div>
<div class='form-group col-md-7' ng-if="schedule.randomDistribution == 'weighted'" >
<label ng-if="$index==0" for="weightRange{{$index}}">Weight</label>
<input class='form-control-range custom-range' id="weightRange{{$index}}" type='range' ng-model='slot.weight' min=1 max=600
data-toggle="tooltip" data-placement="bottom" title="Slots with more weight will be picked more frequently."
ng-change="refreshSlots()"
>
</input>
</div>
<div class='form-group col-md-2' ng-if="schedule.randomDistribution == 'weighted'" >
<label ng-if="$index==0" for="weightp{{$index}}">%</label>
<input class='form-control flex-filler-percent' id="weightp{{$index}}" type='text' ng-model='slot.weightPercentage'
data-toggle="tooltip" data-placement="bottom" title="This is the overall probability this slot might be picked, assuming all lists are available." readonly
>
</input>
</div>
</div>
<div class="form-row">
<div class='form-group col-md-2'>
<label ng-if="schedule.slots.length==0" for="fakeTime">Duration</label>
<button
type="button"
class="btn btn-outline-secondary form-control"
ng-click="addSlot()"
>
Add Slot
</button>
</div>
</div>
<hr>
<div class='form-group'>
<label for="pad">Pad times</label>
<select
id="pad" class="custom-select form-control"
ng-model="schedule.pad" ng-options="o.id as o.description for o in padOptions"
aria-describedby="padHelp"
>
</select>
<small id='padHelp' class='form-text text-muted'>
Ensures programs have a nice-looking start time, it will add Flex time to fill the gaps.
</small>
</div>
<div class='form-group' ng-show='schedule.pad != 1'>
<label for="padStyle">What to pad?</label>
<select
id="padStyle" class="custom-select form-control"
ng-model="schedule.padStyle" ng-options="o.id as o.description for o in padStyleOptions"
aria-describedby="padStyleHelp"
>
</select>
<small id='padStyleHelp' class='form-text text-muted'>
When using the pad times option, you might prefer to only ensure the start times of the slot and not the individual episodes.
</small>
</div>
<div class='form-group'>
<label for="flexPreference">What to do with flex?</label>
<select
id="flexPreference" class="custom-select form-control"
ng-model="schedule.flexPreference" ng-options="o.id as o.description for o in flexOptions"
aria-describedby="flexPreferenceHelp"
>
</select>
<small id='flexPreferenceHelp' class='form-text text-muted'>
Usually slots need to add flex time to ensure that the next slot starts at the correct time. When there are multiple videos in the slot, you might prefer to distribute the flex time between the videos or to place most of the flex time at the end of the slot.
</small>
</div>
<div class='form-group'>
<label for="randomDistribution">Random Distribution</label>
<select
id="randomDistribution" class="custom-select form-control"
ng-model="schedule.randomDistribution" ng-options="o.id as o.description for o in distributionOptions"
aria-describedby="randomDistributiondHelp"
ng-change="randomDistributionChanged()"
>
</select>
<small id='randomDistributionHelp' class='form-text text-muted'>
Uniform means that all slots have an equal chancel to be picked. Weighted makes the configuration of the slots more complicated but allows to tweak the weight for each slot so you can make some slots more likely to be picked than others.
</small>
</div>
<div class='form-group'>
<label for="lateness">Maximum days to precalculate</label>
<input
id="maxDays" class="form-control"
type='number'
ng-model="schedule.maxDays"
min = 1
max = 3652
aria-describedby="maxDaysHelp"
required
>
</input>
<small id="maxDaysHelp" class='form-text text-muted'>
Maximum number of days to precalculate the schedule. Note that the length of the schedule is also bounded by the maximum number of programs allowed in a channel.
</small>
</div>
</div>
<div class="modal-footer" ng-show='!loading'>
<div class='text-danger small'>{{error}}</div>
<button type="button" class="btn btn-sm btn-link" ng-click="finished(true)">Cancel</button>
<button ng-disabled='disableCreateLineup()' type="button" class="btn btn-sm btn-primary" ng-click="finished(false);">Create Lineup</button>
</div>
</div>
</div>
</div>
</div>

View File

@ -266,6 +266,19 @@ module.exports = function ($http, $q) {
return d.data;
},
calculateRandomSlots: async( programs, schedule) => {
let d = await $http( {
method: "POST",
url : "/api/channel-tools/random-slots",
data: {
programs: programs,
schedule: schedule,
},
headers: { 'Content-Type': 'application/json; charset=utf-8' },
} );
return d.data;
},
/*======================================================================
* Settings
*/