commit
42b9bf305e
47
index.js
47
index.js
@ -21,6 +21,7 @@ const ChannelDB = require("./src/dao/channel-db");
|
||||
const M3uService = require("./src/services/m3u-service");
|
||||
const FillerDB = require("./src/dao/filler-db");
|
||||
const TVGuideService = require("./src/tv-guide-service");
|
||||
const EventService = require("./src/services/event-service");
|
||||
const onShutdown = require("node-graceful-shutdown").onShutdown;
|
||||
|
||||
console.log(
|
||||
@ -76,6 +77,7 @@ db.connect(process.env.DATABASE, ['channels', 'plex-servers', 'ffmpeg-settings',
|
||||
fileCache = new FileCacheService( path.join(process.env.DATABASE, 'cache') );
|
||||
cacheImageService = new CacheImageService(db, fileCache);
|
||||
m3uService = new M3uService(channelDB, fileCache, channelCache)
|
||||
eventService = new EventService();
|
||||
|
||||
initDB(db, channelDB)
|
||||
|
||||
@ -165,6 +167,8 @@ xmltvInterval.startInterval()
|
||||
|
||||
let hdhr = HDHR(db, channelDB)
|
||||
let app = express()
|
||||
eventService.setup(app);
|
||||
|
||||
app.use(fileUpload({
|
||||
createParentPath: true
|
||||
}));
|
||||
@ -197,7 +201,7 @@ app.use('/favicon.svg', express.static(
|
||||
) );
|
||||
|
||||
// API Routers
|
||||
app.use(api.router(db, channelDB, fillerDB, xmltvInterval, guideService, m3uService ))
|
||||
app.use(api.router(db, channelDB, fillerDB, xmltvInterval, guideService, m3uService, eventService ))
|
||||
app.use('/api/cache/images', cacheImageService.apiRouters())
|
||||
|
||||
app.use(video.router( channelDB, fillerDB, db))
|
||||
@ -242,8 +246,49 @@ function initDB(db, channelDB) {
|
||||
|
||||
}
|
||||
|
||||
|
||||
function _wait(t) {
|
||||
return new Promise((resolve) => {
|
||||
setTimeout(resolve, t);
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
async function sendEventAfterTime() {
|
||||
let t = (new Date()).getTime();
|
||||
await _wait(20000);
|
||||
eventService.push(
|
||||
"lifecycle",
|
||||
{
|
||||
"message": `Server Started`,
|
||||
"detail" : {
|
||||
"time": t,
|
||||
},
|
||||
"level" : "success"
|
||||
}
|
||||
);
|
||||
|
||||
}
|
||||
sendEventAfterTime();
|
||||
|
||||
|
||||
|
||||
|
||||
onShutdown("log" , [], async() => {
|
||||
let t = (new Date()).getTime();
|
||||
eventService.push(
|
||||
"lifecycle",
|
||||
{
|
||||
"message": `Initiated Server Shutdown`,
|
||||
"detail" : {
|
||||
"time": t,
|
||||
},
|
||||
"level" : "warning"
|
||||
}
|
||||
);
|
||||
|
||||
console.log("Received exit signal, attempting graceful shutdonw...");
|
||||
await _wait(2000);
|
||||
});
|
||||
onShutdown("xmltv-writer" , [], async() => {
|
||||
await xmltv.shutdown();
|
||||
|
||||
316
src/api.js
316
src/api.js
@ -8,12 +8,24 @@ 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');
|
||||
|
||||
function safeString(object) {
|
||||
let o = object;
|
||||
for(let i = 1; i < arguments.length; i++) {
|
||||
o = o[arguments[i]];
|
||||
if (typeof(o) === 'undefined') {
|
||||
return "missing";
|
||||
}
|
||||
}
|
||||
return String(o);
|
||||
}
|
||||
|
||||
module.exports = { router: api }
|
||||
function api(db, channelDB, fillerDB, xmltvInterval, guideService, _m3uService ) {
|
||||
const m3uService = _m3uService;
|
||||
function api(db, channelDB, fillerDB, xmltvInterval, guideService, _m3uService, eventService ) {
|
||||
let m3uService = _m3uService;
|
||||
const router = express.Router()
|
||||
const plexServerDB = new PlexServerDB(channelDB, channelCache, db);
|
||||
|
||||
@ -88,34 +100,113 @@ function api(db, channelDB, fillerDB, xmltvInterval, guideService, _m3uService
|
||||
}
|
||||
})
|
||||
router.delete('/api/plex-servers', async (req, res) => {
|
||||
let name = "unknown";
|
||||
try {
|
||||
let name = req.body.name;
|
||||
name = req.body.name;
|
||||
if (typeof(name) === 'undefined') {
|
||||
return res.status(400).send("Missing name");
|
||||
}
|
||||
let report = await plexServerDB.deleteServer(name);
|
||||
res.send(report)
|
||||
eventService.push(
|
||||
"settings-update",
|
||||
{
|
||||
"message": `Plex server ${name} removed.`,
|
||||
"module" : "plex-server",
|
||||
"detail" : {
|
||||
"serverName" : name,
|
||||
"action" : "delete"
|
||||
},
|
||||
"level" : "warn"
|
||||
}
|
||||
);
|
||||
|
||||
} catch(err) {
|
||||
console.error(err);
|
||||
res.status(500).send("error");
|
||||
eventService.push(
|
||||
"settings-update",
|
||||
{
|
||||
"message": "Error deleting plex server.",
|
||||
"module" : "plex-server",
|
||||
"detail" : {
|
||||
"action": "delete",
|
||||
"serverName" : name,
|
||||
"error" : safeString(err, "message"),
|
||||
},
|
||||
"level" : "danger"
|
||||
}
|
||||
);
|
||||
}
|
||||
})
|
||||
router.post('/api/plex-servers', async (req, res) => {
|
||||
try {
|
||||
await plexServerDB.updateServer(req.body);
|
||||
res.status(204).send("Plex server updated.");;
|
||||
eventService.push(
|
||||
"settings-update",
|
||||
{
|
||||
"message": `Plex server ${req.body.name} updated.`,
|
||||
"module" : "plex-server",
|
||||
"detail" : {
|
||||
"serverName" : req.body.name,
|
||||
"action" : "update"
|
||||
},
|
||||
"level" : "info"
|
||||
}
|
||||
);
|
||||
|
||||
} catch (err) {
|
||||
console.error("Could not add plex server.", err);
|
||||
console.error("Could not update plex server.", err);
|
||||
res.status(400).send("Could not add plex server.");
|
||||
eventService.push(
|
||||
"settings-update",
|
||||
{
|
||||
"message": "Error updating plex server.",
|
||||
"module" : "plex-server",
|
||||
"detail" : {
|
||||
"action": "update",
|
||||
"serverName" : safeString(req, "body", "name"),
|
||||
"error" : safeString(err, "message"),
|
||||
},
|
||||
"level" : "danger"
|
||||
}
|
||||
);
|
||||
}
|
||||
})
|
||||
router.put('/api/plex-servers', async (req, res) => {
|
||||
try {
|
||||
await plexServerDB.addServer(req.body);
|
||||
res.status(201).send("Plex server added.");;
|
||||
eventService.push(
|
||||
"settings-update",
|
||||
{
|
||||
"message": `Plex server ${req.body.name} added.`,
|
||||
"module" : "plex-server",
|
||||
"detail" : {
|
||||
"serverName" : req.body.name,
|
||||
"action" : "add"
|
||||
},
|
||||
"level" : "info"
|
||||
}
|
||||
);
|
||||
|
||||
} catch (err) {
|
||||
console.error("Could not add plex server.", err);
|
||||
res.status(400).send("Could not add plex server.");
|
||||
eventService.push(
|
||||
"settings-update",
|
||||
{
|
||||
"message": "Error adding plex server.",
|
||||
"module" : "plex-server",
|
||||
"detail" : {
|
||||
"action": "add",
|
||||
"serverName" : safeString(req, "body", "name"),
|
||||
"error" : safeString(err, "message"),
|
||||
},
|
||||
"level" : "danger"
|
||||
}
|
||||
);
|
||||
}
|
||||
})
|
||||
|
||||
@ -340,10 +431,34 @@ function api(db, channelDB, fillerDB, xmltvInterval, guideService, _m3uService
|
||||
if (typeof(err) !== 'undefined') {
|
||||
return res.status(400).send(err);
|
||||
}
|
||||
eventService.push(
|
||||
"settings-update",
|
||||
{
|
||||
"message": "FFMPEG configuration updated.",
|
||||
"module" : "ffmpeg",
|
||||
"detail" : {
|
||||
"action" : "update"
|
||||
},
|
||||
"level" : "info"
|
||||
}
|
||||
);
|
||||
res.send(ffmpeg)
|
||||
} catch(err) {
|
||||
console.error(err);
|
||||
res.status(500).send("error");
|
||||
eventService.push(
|
||||
"settings-update",
|
||||
{
|
||||
"message": "Error updating FFMPEG configuration.",
|
||||
"module" : "ffmpeg",
|
||||
"detail" : {
|
||||
"action": "update",
|
||||
"error" : safeString(err, "message"),
|
||||
},
|
||||
"level" : "danger"
|
||||
}
|
||||
);
|
||||
|
||||
}
|
||||
})
|
||||
router.post('/api/ffmpeg-settings', (req, res) => { // RESET
|
||||
@ -352,10 +467,34 @@ function api(db, channelDB, fillerDB, xmltvInterval, guideService, _m3uService
|
||||
ffmpeg.ffmpegPath = req.body.ffmpegPath;
|
||||
db['ffmpeg-settings'].update({ _id: req.body._id }, ffmpeg)
|
||||
ffmpeg = db['ffmpeg-settings'].find()[0]
|
||||
eventService.push(
|
||||
"settings-update",
|
||||
{
|
||||
"message": "FFMPEG configuration reset.",
|
||||
"module" : "ffmpeg",
|
||||
"detail" : {
|
||||
"action" : "reset"
|
||||
},
|
||||
"level" : "warning"
|
||||
}
|
||||
);
|
||||
res.send(ffmpeg)
|
||||
} catch(err) {
|
||||
console.error(err);
|
||||
res.status(500).send("error");
|
||||
eventService.push(
|
||||
"settings-update",
|
||||
{
|
||||
"message": "Error reseting FFMPEG configuration.",
|
||||
"module" : "ffmpeg",
|
||||
"detail" : {
|
||||
"action": "reset",
|
||||
"error" : safeString(err, "message"),
|
||||
},
|
||||
"level" : "danger"
|
||||
}
|
||||
);
|
||||
|
||||
}
|
||||
|
||||
})
|
||||
@ -383,9 +522,34 @@ function api(db, channelDB, fillerDB, xmltvInterval, guideService, _m3uService
|
||||
db['plex-settings'].update({ _id: req.body._id }, req.body)
|
||||
let plex = db['plex-settings'].find()[0]
|
||||
res.send(plex)
|
||||
eventService.push(
|
||||
"settings-update",
|
||||
{
|
||||
"message": "Plex configuration updated.",
|
||||
"module" : "plex",
|
||||
"detail" : {
|
||||
"action" : "update"
|
||||
},
|
||||
"level" : "info"
|
||||
}
|
||||
);
|
||||
|
||||
} catch(err) {
|
||||
console.error(err);
|
||||
res.status(500).send("error");
|
||||
eventService.push(
|
||||
"settings-update",
|
||||
{
|
||||
"message": "Error updating Plex configuration",
|
||||
"module" : "plex",
|
||||
"detail" : {
|
||||
"action": "update",
|
||||
"error" : safeString(err, "message"),
|
||||
},
|
||||
"level" : "danger"
|
||||
}
|
||||
);
|
||||
|
||||
}
|
||||
|
||||
})
|
||||
@ -414,9 +578,35 @@ function api(db, channelDB, fillerDB, xmltvInterval, guideService, _m3uService
|
||||
})
|
||||
let plex = db['plex-settings'].find()[0]
|
||||
res.send(plex)
|
||||
eventService.push(
|
||||
"settings-update",
|
||||
{
|
||||
"message": "Plex configuration reset.",
|
||||
"module" : "plex",
|
||||
"detail" : {
|
||||
"action" : "reset"
|
||||
},
|
||||
"level" : "warning"
|
||||
}
|
||||
);
|
||||
} catch(err) {
|
||||
console.error(err);
|
||||
res.status(500).send("error");
|
||||
|
||||
eventService.push(
|
||||
"settings-update",
|
||||
{
|
||||
"message": "Error reseting Plex configuration",
|
||||
"module" : "plex",
|
||||
"detail" : {
|
||||
"action": "reset",
|
||||
"error" : safeString(err, "message"),
|
||||
},
|
||||
"level" : "danger"
|
||||
}
|
||||
);
|
||||
|
||||
|
||||
}
|
||||
|
||||
})
|
||||
@ -457,10 +647,35 @@ function api(db, channelDB, fillerDB, xmltvInterval, guideService, _m3uService
|
||||
);
|
||||
xmltv = db['xmltv-settings'].find()[0]
|
||||
res.send(xmltv)
|
||||
eventService.push(
|
||||
"settings-update",
|
||||
{
|
||||
"message": "xmltv settings updated.",
|
||||
"module" : "xmltv",
|
||||
"detail" : {
|
||||
"action" : "update"
|
||||
},
|
||||
"level" : "info"
|
||||
}
|
||||
);
|
||||
updateXmltv()
|
||||
} catch(err) {
|
||||
console.error(err);
|
||||
res.status(500).send("error");
|
||||
|
||||
eventService.push(
|
||||
"settings-update",
|
||||
{
|
||||
"message": "Error updating xmltv configuration",
|
||||
"module" : "xmltv",
|
||||
"detail" : {
|
||||
"action": "update",
|
||||
"error" : safeString(err, "message"),
|
||||
},
|
||||
"level" : "danger"
|
||||
}
|
||||
);
|
||||
|
||||
}
|
||||
|
||||
})
|
||||
@ -474,10 +689,35 @@ function api(db, channelDB, fillerDB, xmltvInterval, guideService, _m3uService
|
||||
})
|
||||
var xmltv = db['xmltv-settings'].find()[0]
|
||||
res.send(xmltv)
|
||||
eventService.push(
|
||||
"settings-update",
|
||||
{
|
||||
"message": "xmltv settings reset.",
|
||||
"module" : "xmltv",
|
||||
"detail" : {
|
||||
"action" : "reset"
|
||||
},
|
||||
"level" : "warning"
|
||||
}
|
||||
);
|
||||
|
||||
updateXmltv()
|
||||
} catch(err) {
|
||||
console.error(err);
|
||||
res.status(500).send("error");
|
||||
eventService.push(
|
||||
"settings-update",
|
||||
{
|
||||
"message": "Error reseting xmltv configuration",
|
||||
"module" : "xmltv",
|
||||
"detail" : {
|
||||
"action": "reset",
|
||||
"error" : safeString(err, "message"),
|
||||
},
|
||||
"level" : "danger"
|
||||
}
|
||||
);
|
||||
|
||||
}
|
||||
})
|
||||
|
||||
@ -536,9 +776,34 @@ function api(db, channelDB, fillerDB, xmltvInterval, guideService, _m3uService
|
||||
db['hdhr-settings'].update({ _id: req.body._id }, req.body)
|
||||
let hdhr = db['hdhr-settings'].find()[0]
|
||||
res.send(hdhr)
|
||||
eventService.push(
|
||||
"settings-update",
|
||||
{
|
||||
"message": "HDHR configuration updated.",
|
||||
"module" : "hdhr",
|
||||
"detail" : {
|
||||
"action" : "update"
|
||||
},
|
||||
"level" : "info"
|
||||
}
|
||||
);
|
||||
|
||||
} catch(err) {
|
||||
console.error(err);
|
||||
res.status(500).send("error");
|
||||
eventService.push(
|
||||
"settings-update",
|
||||
{
|
||||
"message": "Error updating HDHR configuration",
|
||||
"module" : "hdhr",
|
||||
"detail" : {
|
||||
"action": "action",
|
||||
"error" : safeString(err, "message"),
|
||||
},
|
||||
"level" : "danger"
|
||||
}
|
||||
);
|
||||
|
||||
}
|
||||
|
||||
})
|
||||
@ -551,9 +816,34 @@ function api(db, channelDB, fillerDB, xmltvInterval, guideService, _m3uService
|
||||
})
|
||||
var hdhr = db['hdhr-settings'].find()[0]
|
||||
res.send(hdhr)
|
||||
eventService.push(
|
||||
"settings-update",
|
||||
{
|
||||
"message": "HDHR configuration reset.",
|
||||
"module" : "hdhr",
|
||||
"detail" : {
|
||||
"action" : "reset"
|
||||
},
|
||||
"level" : "warning"
|
||||
}
|
||||
);
|
||||
|
||||
} catch(err) {
|
||||
console.error(err);
|
||||
res.status(500).send("error");
|
||||
eventService.push(
|
||||
"settings-update",
|
||||
{
|
||||
"message": "Error reseting HDHR configuration",
|
||||
"module" : "hdhr",
|
||||
"detail" : {
|
||||
"action": "reset",
|
||||
"error" : safeString(err, "message"),
|
||||
},
|
||||
"level" : "danger"
|
||||
}
|
||||
);
|
||||
|
||||
}
|
||||
|
||||
})
|
||||
@ -583,9 +873,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);
|
||||
|
||||
47
src/services/event-service.js
Normal file
47
src/services/event-service.js
Normal file
@ -0,0 +1,47 @@
|
||||
const EventEmitter = require("events");
|
||||
|
||||
class EventsService {
|
||||
constructor() {
|
||||
this.stream = new EventEmitter();
|
||||
let that = this;
|
||||
let fun = () => {
|
||||
that.push( "heartbeat", "{}");
|
||||
setTimeout(fun, 5000)
|
||||
};
|
||||
fun();
|
||||
|
||||
}
|
||||
|
||||
setup(app) {
|
||||
app.get("/api/events", (request, response) => {
|
||||
console.log("Open event channel.");
|
||||
response.writeHead(200, {
|
||||
"Content-Type" : "text/event-stream",
|
||||
"Cache-Control" : "no-cache",
|
||||
"connection" : "keep-alive",
|
||||
} );
|
||||
let listener = (event,data) => {
|
||||
//console.log( String(event) + " " + JSON.stringify(data) );
|
||||
response.write("event: " + String(event) + "\ndata: "
|
||||
+ JSON.stringify(data) + "\nretry: 5000\n\n" );
|
||||
};
|
||||
|
||||
this.stream.on("push", listener );
|
||||
response.on( "close", () => {
|
||||
console.log("Remove event channel.");
|
||||
this.stream.removeListener("push", listener);
|
||||
} );
|
||||
} );
|
||||
}
|
||||
|
||||
push(event, data) {
|
||||
if (typeof(data.message) !== 'undefined') {
|
||||
console.log("Push event: " + data.message );
|
||||
}
|
||||
this.stream.emit("push", event, data );
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
|
||||
module.exports = EventsService;
|
||||
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(),
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
@ -7,7 +7,7 @@ class TVGuideService
|
||||
/****
|
||||
*
|
||||
**/
|
||||
constructor(xmltv, db, cacheImageService) {
|
||||
constructor(xmltv, db, cacheImageService, eventService) {
|
||||
this.cached = null;
|
||||
this.lastUpdate = 0;
|
||||
this.updateTime = 0;
|
||||
@ -19,6 +19,7 @@ class TVGuideService
|
||||
this.xmltv = xmltv;
|
||||
this.db = db;
|
||||
this.cacheImageService = cacheImageService;
|
||||
this.eventService = eventService;
|
||||
}
|
||||
|
||||
async get() {
|
||||
@ -44,6 +45,19 @@ class TVGuideService
|
||||
this.currentUpdate = this.updateTime;
|
||||
this.currentLimit = this.updateLimit;
|
||||
this.currentChannels = this.updateChannels;
|
||||
let t = "" + ( (new Date()) );
|
||||
eventService.push(
|
||||
"xmltv",
|
||||
{
|
||||
"message": `Started building tv-guide at = ${t}`,
|
||||
"module" : "xmltv",
|
||||
"detail" : {
|
||||
"time": new Date(),
|
||||
},
|
||||
"level" : "info"
|
||||
}
|
||||
);
|
||||
|
||||
await this.buildIt();
|
||||
}
|
||||
await _wait(100);
|
||||
@ -353,6 +367,19 @@ class TVGuideService
|
||||
async refreshXML() {
|
||||
let xmltvSettings = this.db['xmltv-settings'].find()[0];
|
||||
await this.xmltv.WriteXMLTV(this.cached, xmltvSettings, async() => await this._throttle(), this.cacheImageService);
|
||||
let t = "" + ( (new Date()) );
|
||||
eventService.push(
|
||||
"xmltv",
|
||||
{
|
||||
"message": `XMLTV updated at server time = ${t}`,
|
||||
"module" : "xmltv",
|
||||
"detail" : {
|
||||
"time": new Date(),
|
||||
},
|
||||
"level" : "info"
|
||||
}
|
||||
);
|
||||
|
||||
}
|
||||
|
||||
async getStatus() {
|
||||
|
||||
@ -19,6 +19,7 @@ app.directive('plexLibrary', require('./directives/plex-library'))
|
||||
app.directive('programConfig', require('./directives/program-config'))
|
||||
app.directive('flexConfig', require('./directives/flex-config'))
|
||||
app.directive('timeSlotsTimeEditor', require('./directives/time-slots-time-editor'))
|
||||
app.directive('toastNotifications', require('./directives/toast-notifications'))
|
||||
app.directive('fillerConfig', require('./directives/filler-config'))
|
||||
app.directive('deleteFiller', require('./directives/delete-filler'))
|
||||
app.directive('frequencyTweak', require('./directives/frequency-tweak'))
|
||||
@ -27,6 +28,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) {
|
||||
|
||||
116
web/directives/toast-notifications.js
Normal file
116
web/directives/toast-notifications.js
Normal file
@ -0,0 +1,116 @@
|
||||
module.exports = function ($timeout) {
|
||||
return {
|
||||
restrict: 'E',
|
||||
templateUrl: 'templates/toast-notifications.html',
|
||||
replace: true,
|
||||
scope: {
|
||||
},
|
||||
link: function (scope, element, attrs) {
|
||||
|
||||
const FADE_IN_START = 100;
|
||||
const FADE_IN_END = 1000;
|
||||
const FADE_OUT_START = 10000;
|
||||
const TOTAL_DURATION = 11000;
|
||||
|
||||
|
||||
scope.toasts = [];
|
||||
|
||||
let eventSource = null;
|
||||
|
||||
let timerHandle = null;
|
||||
let refreshHandle = null;
|
||||
|
||||
|
||||
let setResetTimer = () => {
|
||||
if (timerHandle != null) {
|
||||
clearTimeout( timerHandle );
|
||||
}
|
||||
timerHandle = setTimeout( () => {
|
||||
scope.setup();
|
||||
} , 10000);
|
||||
};
|
||||
|
||||
let updateAfter = (wait) => {
|
||||
if (refreshHandle != null) {
|
||||
$timeout.cancel( refreshHandle );
|
||||
}
|
||||
refreshHandle = $timeout( ()=> updater(), wait );
|
||||
};
|
||||
|
||||
let updater = () => {
|
||||
let wait = 10000;
|
||||
let updatedToasts = [];
|
||||
try {
|
||||
let t = (new Date()).getTime();
|
||||
for (let i = 0; i < scope.toasts.length; i++) {
|
||||
let toast = scope.toasts[i];
|
||||
let diff = t - toast.time;
|
||||
if (diff < TOTAL_DURATION) {
|
||||
if (diff < FADE_IN_START) {
|
||||
toast.clazz = { "about-to-fade-in" : true }
|
||||
wait = Math.min( wait, FADE_IN_START - diff );
|
||||
} else if (diff < FADE_IN_END) {
|
||||
toast.clazz = { "fade-in" : true }
|
||||
wait = Math.min( wait, FADE_IN_END - diff );
|
||||
} else if (diff < FADE_OUT_START) {
|
||||
toast.clazz = {}
|
||||
wait = Math.min( wait, FADE_OUT_START - diff );
|
||||
} else {
|
||||
toast.clazz = { "fade-out" : true }
|
||||
wait = Math.min( wait, TOTAL_DURATION - diff );
|
||||
}
|
||||
toast.clazz[toast.deco] = true;
|
||||
updatedToasts.push(toast);
|
||||
}
|
||||
}
|
||||
} catch (err) {
|
||||
console.error("error", err);
|
||||
}
|
||||
scope.toasts = updatedToasts;
|
||||
updateAfter(wait);
|
||||
};
|
||||
|
||||
let addToast = (toast) => {
|
||||
toast.time = (new Date()).getTime();
|
||||
toast.clazz= { "about-to-fade-in": true };
|
||||
toast.clazz[toast.deco] = true;
|
||||
scope.toasts.push(toast);
|
||||
$timeout( () => updateAfter(0) );
|
||||
};
|
||||
|
||||
let getDeco = (data) => {
|
||||
return "bg-" + data.level;
|
||||
}
|
||||
|
||||
scope.setup = () => {
|
||||
if (eventSource != null) {
|
||||
eventSource.close();
|
||||
eventSource = null;
|
||||
}
|
||||
setResetTimer();
|
||||
|
||||
eventSource = new EventSource("api/events");
|
||||
|
||||
eventSource.addEventListener("heartbeat", () => {
|
||||
setResetTimer();
|
||||
} );
|
||||
|
||||
let normalEvent = (title) => {
|
||||
return (event) => {
|
||||
let data = JSON.parse(event.data);
|
||||
addToast ( {
|
||||
title : title,
|
||||
text : data.message,
|
||||
deco: getDeco(data)
|
||||
} )
|
||||
};
|
||||
};
|
||||
|
||||
eventSource.addEventListener('settings-update', normalEvent("Settings Update") );
|
||||
eventSource.addEventListener('xmltv', normalEvent("TV Guide") );
|
||||
eventSource.addEventListener('lifecycle', normalEvent("Server") );
|
||||
};
|
||||
scope.setup();
|
||||
}
|
||||
};
|
||||
}
|
||||
@ -43,6 +43,7 @@
|
||||
</span>
|
||||
<hr/>
|
||||
<div ng-view></div>
|
||||
<toast-notifications></toast-notifications>
|
||||
</div>
|
||||
</body>
|
||||
|
||||
|
||||
@ -357,6 +357,38 @@ div.programming-programs div.list-group-item {
|
||||
background : rgba(255,255,255, 0.1);
|
||||
}
|
||||
|
||||
.dizque-toast {
|
||||
margin-top: 0.2rem;
|
||||
padding: 0.5rem;
|
||||
background: #FFFFFF;
|
||||
border: 1px solid rgba(0,0,0,.1);
|
||||
border-radius: .25rem;
|
||||
color: #FFFFFF;
|
||||
}
|
||||
|
||||
.dizque-toast.bg-warning {
|
||||
color: black
|
||||
}
|
||||
|
||||
.about-to-fade-in {
|
||||
opacity: 0.00;
|
||||
transition: opacity 1.00s ease-in-out;
|
||||
-moz-transition: opacity 1.00s ease-in-out;
|
||||
-webkit-transition: opacity 1.00s ease-in-out;
|
||||
}
|
||||
.fade-in {
|
||||
opacity: 0.95;
|
||||
transition: opacity 1.00s ease-in-out;
|
||||
-moz-transition: opacity 1.00s ease-in-out;
|
||||
-webkit-transition: opacity 1.00s ease-in-out;
|
||||
}
|
||||
|
||||
.fade-out {
|
||||
transition: opacity 1.00s ease-in-out;
|
||||
-moz-transition: opacity 1.00s ease-in-out;
|
||||
-webkit-transition: opacity 1.00s ease-in-out;
|
||||
opacity: 0.0;
|
||||
}
|
||||
|
||||
#dizquetv-logo {
|
||||
width: 1em;
|
||||
|
||||
@ -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>
|
||||
|
||||
@ -4,7 +4,7 @@
|
||||
<button class="pull-right btn btn-sm btn-success" style="margin-left: 5px;" ng-click="updateSettings(settings)">
|
||||
Update
|
||||
</button>
|
||||
<button class="pull-right btn btn-sm btn-info" ng-click="resetSettings(settings)">
|
||||
<button class="pull-right btn btn-sm btn-warning" ng-click="resetSettings(settings)">
|
||||
Reset Options
|
||||
</button>
|
||||
</h5>
|
||||
|
||||
@ -3,7 +3,7 @@
|
||||
<button class="pull-right btn btn-sm btn-success" style="margin-left: 5px;" ng-click="updateSettings(settings)">
|
||||
Update
|
||||
</button>
|
||||
<button class="pull-right btn btn-sm btn-info" ng-click="resetSettings(settings)">
|
||||
<button class="pull-right btn btn-sm btn-warning" ng-click="resetSettings(settings)">
|
||||
Reset Options
|
||||
</button>
|
||||
</h5>
|
||||
|
||||
@ -70,7 +70,7 @@
|
||||
<button class="pull-right btn btn-sm btn-success" style="margin-left: 5px;" ng-click="updateSettings(settings)">
|
||||
Update
|
||||
</button>
|
||||
<button class="pull-right btn btn-sm btn-info" ng-click="resetSettings(settings)">
|
||||
<button class="pull-right btn btn-sm btn-warning" ng-click="resetSettings(settings)">
|
||||
Reset Options
|
||||
</button>
|
||||
</h6>
|
||||
|
||||
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>
|
||||
11
web/public/templates/toast-notifications.html
Normal file
11
web/public/templates/toast-notifications.html
Normal file
@ -0,0 +1,11 @@
|
||||
<div style='position: fixed; top: 30px; right: 30px; width:400px; z-index: 1000000;'>
|
||||
|
||||
<div
|
||||
ng-repeat="toast in toasts track by $index"
|
||||
class="dizque-toast"
|
||||
ng-class="toast.clazz"
|
||||
>
|
||||
<strong>{{ toast.title }}</strong>
|
||||
<div>{{ toast.text }}</div>
|
||||
</div>
|
||||
</div>
|
||||
@ -4,7 +4,7 @@
|
||||
<button class="pull-right btn btn-sm btn-success" style="margin-left: 5px;" ng-click="updateSettings(settings)">
|
||||
Update
|
||||
</button>
|
||||
<button class="pull-right btn btn-sm btn-info" ng-click="resetSettings(settings)">
|
||||
<button class="pull-right btn btn-sm btn-warning" ng-click="resetSettings(settings)">
|
||||
Reset Options
|
||||
</button>
|
||||
</h5>
|
||||
|
||||
@ -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