Random Slots
This commit is contained in:
parent
3fadcc487c
commit
d6b2bd1d5e
19
src/api.js
19
src/api.js
@ -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);
|
||||
|
||||
469
src/services/random-slots-service.js
Normal file
469
src/services/random-slots-service.js
Normal 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(),
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
@ -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'))
|
||||
|
||||
@ -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;
|
||||
}
|
||||
|
||||
},
|
||||
|
||||
}
|
||||
|
||||
338
web/directives/random-slots-schedule-editor.js
Normal file
338
web/directives/random-slots-schedule-editor.js
Normal 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.",
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@ -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) {
|
||||
|
||||
@ -449,7 +449,7 @@
|
||||
<p ng-show='showHelp.check'>Slides the whole schedule. The "Fast-Forward" button will advance the stream by the specified amount of time. The "Rewind" 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'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>
|
||||
|
||||
185
web/public/templates/random-slots-schedule-editor.html
Normal file
185
web/public/templates/random-slots-schedule-editor.html
Normal 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>
|
||||
@ -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
|
||||
*/
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user