diff --git a/index.js b/index.js index 442f8ec..6eaf637 100644 --- a/index.js +++ b/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(); diff --git a/src/api.js b/src/api.js index 9c41ef6..1e417b8 100644 --- a/src/api.js +++ b/src/api.js @@ -12,9 +12,20 @@ const Plex = require("./plex.js"); 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); @@ -89,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" + } + ); } }) @@ -341,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 @@ -353,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" + } + ); + } }) @@ -384,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" + } + ); + } }) @@ -415,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" + } + ); + + } }) @@ -458,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" + } + ); + } }) @@ -475,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" + } + ); + } }) @@ -537,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" + } + ); + } }) @@ -552,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" + } + ); + } }) diff --git a/src/services/event-service.js b/src/services/event-service.js new file mode 100644 index 0000000..1ac9893 --- /dev/null +++ b/src/services/event-service.js @@ -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; \ No newline at end of file diff --git a/src/tv-guide-service.js b/src/tv-guide-service.js index 5936dcd..d64da30 100644 --- a/src/tv-guide-service.js +++ b/src/tv-guide-service.js @@ -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() { diff --git a/web/app.js b/web/app.js index b1d9887..1b15e15 100644 --- a/web/app.js +++ b/web/app.js @@ -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')) diff --git a/web/directives/toast-notifications.js b/web/directives/toast-notifications.js new file mode 100644 index 0000000..bb6f645 --- /dev/null +++ b/web/directives/toast-notifications.js @@ -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(); + } + }; +} diff --git a/web/public/index.html b/web/public/index.html index 3b20003..360b80c 100644 --- a/web/public/index.html +++ b/web/public/index.html @@ -43,6 +43,7 @@
+ diff --git a/web/public/style.css b/web/public/style.css index 0102e80..92643a7 100644 --- a/web/public/style.css +++ b/web/public/style.css @@ -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; diff --git a/web/public/templates/ffmpeg-settings.html b/web/public/templates/ffmpeg-settings.html index 09fb68f..8ebd3ff 100644 --- a/web/public/templates/ffmpeg-settings.html +++ b/web/public/templates/ffmpeg-settings.html @@ -4,7 +4,7 @@ - diff --git a/web/public/templates/hdhr-settings.html b/web/public/templates/hdhr-settings.html index 0b6cd34..c00b6d3 100644 --- a/web/public/templates/hdhr-settings.html +++ b/web/public/templates/hdhr-settings.html @@ -3,7 +3,7 @@ - diff --git a/web/public/templates/plex-settings.html b/web/public/templates/plex-settings.html index 1324bf1..b5dfb39 100644 --- a/web/public/templates/plex-settings.html +++ b/web/public/templates/plex-settings.html @@ -70,7 +70,7 @@ - diff --git a/web/public/templates/toast-notifications.html b/web/public/templates/toast-notifications.html new file mode 100644 index 0000000..825a520 --- /dev/null +++ b/web/public/templates/toast-notifications.html @@ -0,0 +1,11 @@ +
+ +
+ {{ toast.title }} +
{{ toast.text }}
+
+
diff --git a/web/public/templates/xmltv-settings.html b/web/public/templates/xmltv-settings.html index 20c65a0..cfd3b97 100644 --- a/web/public/templates/xmltv-settings.html +++ b/web/public/templates/xmltv-settings.html @@ -4,7 +4,7 @@ -