diff --git a/index.js b/index.js index 390c6e9..fab64c3 100644 --- a/index.js +++ b/index.js @@ -17,6 +17,7 @@ const HDHR = require('./src/hdhr') const FileCacheService = require('./src/services/file-cache-service'); const CacheImageService = require('./src/services/cache-image-service'); const ChannelService = require("./src/services/channel-service"); +const FillerService = require("./src/services/filler-service"); const xmltv = require('./src/xmltv') const Plex = require('./src/plex'); @@ -104,7 +105,7 @@ initDB(db, channelDB) channelService = new ChannelService(channelDB); -fillerDB = new FillerDB( path.join(process.env.DATABASE, 'filler') , channelService ); +let fillerDB = new FillerDB( path.join(process.env.DATABASE, 'filler') ); customShowDB = new CustomShowDB( path.join(process.env.DATABASE, 'custom-shows') ); let programPlayTimeDB = new ProgramPlayTimeDB( path.join(process.env.DATABASE, 'play-cache') ); let ffmpegSettingsService = new FfmpegSettingsService(db); @@ -134,6 +135,9 @@ activeChannelService = new ActiveChannelService(onDemandService, channelService) eventService = new EventService(); +let fillerService = new FillerService(fillerDB, plexProxyService, + channelService); + i18next .use(i18nextBackend) .use(i18nextMiddleware.LanguageDetector) @@ -323,12 +327,12 @@ app.use('/favicon.svg', express.static( app.use('/custom.css', express.static(path.join(process.env.DATABASE, 'custom.css'))) // API Routers -app.use(api.router(db, channelService, fillerDB, customShowDB, xmltvInterval, guideService, m3uService, eventService, ffmpegSettingsService, plexServerDB, plexProxyService, ffmpegInfo)) +app.use(api.router(db, channelService, fillerDB, customShowDB, xmltvInterval, guideService, m3uService, eventService, ffmpegSettingsService, plexServerDB, plexProxyService, ffmpegInfo, fillerService)) app.use('/api/cache/images', cacheImageService.apiRouters()) app.use('/' + fontAwesome, express.static(path.join(process.env.DATABASE, fontAwesome))) app.use('/' + bootstrap, express.static(path.join(process.env.DATABASE, bootstrap))) -app.use(video.router( channelService, fillerDB, db, programmingService, activeChannelService, programPlayTimeDB, ffmpegInfo )) +app.use(video.router( channelService, fillerService, db, programmingService, activeChannelService, programPlayTimeDB, ffmpegInfo )) app.use(hdhr.router) app.listen(process.env.PORT, () => { console.log(`HTTP server running on port: http://*:${process.env.PORT}`) diff --git a/src/api.js b/src/api.js index 3e5cba9..bb4c72d 100644 --- a/src/api.js +++ b/src/api.js @@ -22,7 +22,7 @@ function safeString(object) { } module.exports = { router: api } -function api(db, channelService, fillerDB, customShowDB, xmltvInterval, guideService, _m3uService, eventService, ffmpegSettingsService, plexServerDB, plexProxyService, ffmpegInfo ) { +function api(db, channelService, fillerDB, customShowDB, xmltvInterval, guideService, _m3uService, eventService, ffmpegSettingsService, plexServerDB, plexProxyService, ffmpegInfo, fillerService ) { let m3uService = _m3uService; const router = express.Router() @@ -419,7 +419,7 @@ function api(db, channelService, fillerDB, customShowDB, xmltvInterval, guideSe if (typeof(id) === 'undefined') { return res.status(400).send("Missing id"); } - await fillerDB.saveFiller(id, req.body ); + await fillerService.saveFiller(id, req.body ); return res.status(204).send({}); } catch(err) { console.error(err); @@ -428,7 +428,7 @@ function api(db, channelService, fillerDB, customShowDB, xmltvInterval, guideSe }) router.put('/api/filler', async (req, res) => { try { - let uuid = await fillerDB.createFiller(req.body ); + let uuid = await fillerService.createFiller(req.body ); return res.status(201).send({id: uuid}); } catch(err) { console.error(err); @@ -441,7 +441,7 @@ function api(db, channelService, fillerDB, customShowDB, xmltvInterval, guideSe if (typeof(id) === 'undefined') { return res.status(400).send("Missing id"); } - await fillerDB.deleteFiller(id); + await fillerService.deleteFiller(id); return res.status(204).send({}); } catch(err) { console.error(err); @@ -455,7 +455,7 @@ function api(db, channelService, fillerDB, customShowDB, xmltvInterval, guideSe if (typeof(id) === 'undefined') { return res.status(400).send("Missing id"); } - let channels = await fillerDB.getFillerChannels(id); + let channels = await fillerService.getFillerChannels(id); if (channels == null) { return res.status(404).send("Filler not found"); } diff --git a/src/dao/filler-db.js b/src/dao/filler-db.js index 0718ceb..9bd3d0b 100644 --- a/src/dao/filler-db.js +++ b/src/dao/filler-db.js @@ -4,12 +4,9 @@ let fs = require('fs'); class FillerDB { - constructor(folder, channelService) { + constructor(folder) { this.folder = folder; this.cache = {}; - this.channelService = channelService; - - } async $loadFiller(id) { @@ -77,40 +74,8 @@ class FillerDB { return id; } - async getFillerChannels(id) { - let numbers = await this.channelService.getAllChannelNumbers(); - let channels = []; - await Promise.all( numbers.map( async(number) => { - let ch = await this.channelService.getChannel(number); - let name = ch.name; - let fillerCollections = ch.fillerCollections; - for (let i = 0 ; i < fillerCollections.length; i++) { - if (fillerCollections[i].id === id) { - channels.push( { - number: number, - name : name, - } ); - break; - } - } - ch = null; - - } ) ); - return channels; - } - async deleteFiller(id) { try { - let channels = await this.getFillerChannels(id); - await Promise.all( channels.map( async(channel) => { - console.log(`Updating channel ${channel.number} , remove filler: ${id}`); - let json = await channelService.getChannel(channel.number); - json.fillerCollections = json.fillerCollections.filter( (col) => { - return col.id != id; - } ); - await this.channelService.saveChannel( channel.number, json ); - } ) ); - let f = path.join(this.folder, `${id}.json` ); await new Promise( (resolve, reject) => { fs.unlink(f, function (err) { @@ -162,27 +127,6 @@ class FillerDB { } ); } - async getFillersFromChannel(channel) { - - let loadChannelFiller = async(fillerEntry) => { - let content = []; - try { - let filler = await this.getFiller(fillerEntry.id); - content = filler.content; - } catch(e) { - console.error(`Channel #${channel.number} - ${channel.name} references an unattainable filler id: ${fillerEntry.id}`); - } - return { - id: fillerEntry.id, - content: content, - weight: fillerEntry.weight, - cooldown: fillerEntry.cooldown, - } - }; - return await Promise.all( - channel.fillerCollections.map(loadChannelFiller) - ); - } } diff --git a/src/database-migration.js b/src/database-migration.js index a5e5771..3059081 100644 --- a/src/database-migration.js +++ b/src/database-migration.js @@ -20,7 +20,7 @@ const path = require('path'); var fs = require('fs'); -const TARGET_VERSION = 900; +const TARGET_VERSION = 1000; const STEPS = [ // [v, v2, x] : if the current version is v, call x(db), and version becomes v2 @@ -46,6 +46,7 @@ const STEPS = [ [ 803, 900, (db) => fixFFMpegPathSetting(db) ], [ 804, 900, (db) => fixFFMpegPathSetting(db) ], [ 805, 900, (db) => fixFFMpegPathSetting(db) ], + [ 900, 1000, () => fixFillerModes() ], ] const { v4: uuidv4 } = require('uuid'); @@ -679,6 +680,25 @@ function extractFillersFromChannels() { } +function fixFillerModes() { + console.log("Fixing filler modes..."); + let fillers = path.join(process.env.DATABASE, 'filler'); + let fillerFiles = fs.readdirSync(fillers); + + for (let i = 0; i < fillerFiles.length; i++) { + if (path.extname( fillerFiles[i] ) === '.json') { + console.log("Migrating filler : " + fillerFiles[i] +"..." ); + let fillerPath = path.join(fillers, fillerFiles[i]); + let filler = JSON.parse(fs.readFileSync(fillerPath, 'utf-8')); + if ( typeof(filler.mode) !== "string" ) { + filler.mode = "custom"; + } + fs.writeFileSync( fillerPath, JSON.stringify(filler), 'utf-8'); + } + } + console.log("Done fixing filler modes."); +} + function addFPS(db) { let ffmpegSettings = db['ffmpeg-settings'].find()[0]; let f = path.join(process.env.DATABASE, 'ffmpeg-settings.json'); diff --git a/src/services/filler-service.js b/src/services/filler-service.js new file mode 100644 index 0000000..e715fa0 --- /dev/null +++ b/src/services/filler-service.js @@ -0,0 +1,150 @@ +const events = require('events') + +const FILLER_UPDATE = 30 * 60 * 1000; //30 minutes might be too aggressive +//this will be configurable one day. + +class FillerService extends events.EventEmitter { + + constructor(fillerDB, plexProxyService, channelService) { + super(); + this.fillerDB = fillerDB; + this.plexProxyService = plexProxyService; + this.channelService = channelService; + } + + async saveFiller(id, body) { + body = await this.prework(body); + return this.fillerDB.saveFiller(id, body); + } + + async createFiller(body) { + body = await this.prework(body); + return this.fillerDB.createFiller(body); + } + + async getFillerChannels(id) { + let numbers = await this.channelService.getAllChannelNumbers(); + let channels = []; + await Promise.all( numbers.map( async(number) => { + let ch = await this.channelService.getChannel(number); + let name = ch.name; + let fillerCollections = ch.fillerCollections; + for (let i = 0 ; i < fillerCollections.length; i++) { + if (fillerCollections[i].id === id) { + channels.push( { + number: number, + name : name, + } ); + break; + } + } + ch = null; + + } ) ); + return channels; + } + + async deleteFiller(id) { + try { + let channels = await this.getFillerChannels(id); + await Promise.all( channels.map( async(channel) => { + console.log(`Updating channel ${channel.number} , remove filler: ${id}`); + let json = await channelService.getChannel(channel.number); + json.fillerCollections = json.fillerCollections.filter( (col) => { + return col.id != id; + } ); + await this.channelService.saveChannel( channel.number, json ); + } ) ); + } finally { + await this.fillerDB.deleteFiller(id); + } + } + + async prework(body) { + if (body.mode === "import") { + body.content = await this.getContents(body); + body.import.lastRefreshTime = new Date().getTime(); + } else { + delete body.import; + } + return body; + } + + async getContents(body) { + let serverKey = body.import.serverName; + let key = body.import.key; + let content = await this.plexProxyService.getKeyMediaContents(serverKey, key); + console.log(JSON.stringify(content)); + return content; + } + + async getFillersFromChannel(channel) { + + let loadChannelFiller = async(fillerEntry) => { + let content = []; + try { + let filler = await this.fillerDB.getFiller(fillerEntry.id); + await this.fillerUsageWatcher(fillerEntry.id, filler); + content = filler.content; + } catch(e) { + console.error(`Channel #${channel.number} - ${channel.name} references an unattainable filler id: ${fillerEntry.id}`, e); + } + return { + id: fillerEntry.id, + content: content, + weight: fillerEntry.weight, + cooldown: fillerEntry.cooldown, + } + }; + return await Promise.all( + channel.fillerCollections.map(loadChannelFiller) + ); + } + + async fillerUsageWatcher(id, filler) { + if (filler.mode === "import") { + //I need to upgrade nodejs version ASAP + let lastTime = 0; + if ( + (typeof(filler.import) !== "undefined") + && + !isNaN(filler.import.lastRefreshTime) + ) { + lastTime = filler.import.lastRefreshTime; + } + let t = new Date().getTime(); + if ( t - lastTime >= FILLER_UPDATE) { + //time to do an update. + if ( (typeof(filler.content) === "undefined") + || (filler.content.length == 0) + ) { + //It should probably be an sync update... + await this.refreshFiller(id); + } else { + this.refreshFiller(id); + } + } + } + } + + async refreshFiller(id) { + let t0 = new Date().getTime(); + console.log(`Refreshing filler with id=${id}`); + try { + let filler = await this.fillerDB.getFiller(id); + await this.saveFiller(id, filler); + } catch (err) { + console.log(`Unable to update filler: ${id}`, err); + } finally { + let t1 = new Date().getTime(); + console.log(`Refreshed filler with id=${id} in ${t1-t0}ms`); + } + } + + +} + + + + +module.exports = FillerService \ No newline at end of file diff --git a/src/services/plex-proxy-service.js b/src/services/plex-proxy-service.js index 8a14ba1..7ef9f21 100644 --- a/src/services/plex-proxy-service.js +++ b/src/services/plex-proxy-service.js @@ -9,22 +9,86 @@ class PlexProxyService extends events.EventEmitter { } async get(serverName64, path) { - let plexServer = await getPlexServer(this.plexServerDB, serverName64); + let plexServer = await getPlexServer64(this.plexServerDB, serverName64); + // A potential area of improvement is to reuse the client when possible let client = new Plex(plexServer); return { MediaContainer: await client.Get("/" + path) }; } + + async getKeyMediaContents(serverName, key) { + let plexServer = await getPlexServer(this.plexServerDB, serverName); + let client = new Plex(plexServer); + let obj = { MediaContainer: await client.Get(key) }; + let metadata = obj.MediaContainer.Metadata; + if ( typeof(metadata) !== "object") { + return []; + } + metadata = metadata.map( (item) => fillerMapper(serverName, item) ); + + return metadata; + } } +function fillerMapper(serverName, plexMetadata) { + let image = {}; + if ( (typeof(plexMetadata.Image) === "object") + && (typeof(plexMetadata.Image[0]) === "object") + ) { + image = plexMetadata.Image[0]; + } + + let media = {}; + if ( (typeof(plexMetadata.Media) === "object") + && (typeof(plexMetadata.Media[0]) === "object") + ) { + media = plexMetadata.Media[0]; + } -async function getPlexServer(plexServerDB, serverName64) { - let serverKey = Buffer.from(serverName64, 'base64').toString('utf-8'); + let part = {}; + if ( (typeof(media.Part) === "object") + && (typeof(media.Part[0]) === "object") + ) { + part = media.Part[0]; + } + + + return { + title : plexMetadata.title, + key : plexMetadata.key, + ratingKey: plexMetadata.ratingKey, + icon : image.url, + type : plexMetadata.type, + duration : part.duration, + durationStr : undefined, + summary : "", + date : "", + year : plexMetadata.year, + plexFile : part.key, + file : part.file, + showTitle: plexMetadata.title, + episode : 1, + season : 1, + serverKey: serverName, + commercials: [], + } +} + +async function getPlexServer(plexServerDB, serverKey) { let server = await plexServerDB.getPlexServerByName(serverKey); if (server == null) { throw Error("server not found"); } return server; - } + + +async function getPlexServer64(plexServerDB, serverName64) { + let serverKey = Buffer.from(serverName64, 'base64').toString('utf-8'); + return await getPlexServer(plexServerDB, serverKey); +} + + + module.exports = PlexProxyService \ No newline at end of file diff --git a/src/video.js b/src/video.js index acea791..512004b 100644 --- a/src/video.js +++ b/src/video.js @@ -18,7 +18,7 @@ async function shutdown() { stopPlayback = true; } -function video( channelService, fillerDB, db, programmingService, activeChannelService, programPlayTimeDB, ffmpegInfo ) { +function video( channelService, fillerService, db, programmingService, activeChannelService, programPlayTimeDB, ffmpegInfo ) { var router = express.Router() router.get('/setup', async (req, res) => { @@ -304,7 +304,7 @@ function video( channelService, fillerDB, db, programmingService, activeChannelS if ( (prog == null) || (typeof(prog) === 'undefined') || (prog.program == null) || (typeof(prog.program) == "undefined") ) { throw "No video to play, this means there's a serious unexpected bug or the channel db is corrupted." } - let fillers = await fillerDB.getFillersFromChannel(brandChannel); + let fillers = await fillerService.getFillersFromChannel(brandChannel); try { let lineup = helperFuncs.createLineup(programPlayTimeDB, prog, brandChannel, fillers, isFirst) lineupItem = lineup.shift(); diff --git a/web/directives/filler-config.js b/web/directives/filler-config.js index 56cf2e3..f775eee 100644 --- a/web/directives/filler-config.js +++ b/web/directives/filler-config.js @@ -1,4 +1,4 @@ -module.exports = function ($timeout, commonProgramTools, getShowData) { +module.exports = function ($timeout, dizquetv, commonProgramTools, getShowData) { return { restrict: 'E', templateUrl: 'templates/filler-config.html', @@ -13,6 +13,16 @@ module.exports = function ($timeout, commonProgramTools, getShowData) { scope.content = []; scope.visible = false; scope.error = undefined; + scope.modes = [ { + name: "import", + description: "Collection/Playlist from Plex", + }, { + name: "custom", + description: "Custom List of Clips", + } ]; + scope.servers = []; + scope.libraries = []; + scope.sources = []; function refreshContentIndexes() { for (let i = 0; i < scope.content.length; i++) { @@ -47,20 +57,173 @@ module.exports = function ($timeout, commonProgramTools, getShowData) { console.log("movedFunction(" + index + ")"); } + scope.serverChanged = async () => { + if (scope.server === "") { + scope.libraryKey = ""; + return; + } + scope.loadingLibraries = true; + try { + let libraries = (await dizquetv.getFromPlexProxy(scope.server, "/library/sections")).Directory; + if ( typeof(libraries) === "undefined") { + libraries = [] + } + let officialLibraries = libraries.map( (library) => { + return { + "key" : library.key, + "description" : library.title, + } + } ); + + let defaultLibrary = { + "key": "", + "description" : "Select a Library...", + } + let playlists = [ + { + "key": "$PLAYLISTS", + "description" : "Playlists", + } + ]; + let combined = officialLibraries.concat(playlists); + if (! combined.some( (library) => library.key === scope.libraryKey) ) { + scope.libraryKey = ""; + scope.libraries = [defaultLibrary].concat(combined); + } else { + scope.libraries = combined; + } + } catch (err) { + scope.libraries = [ { name: "", description: "Unable to load libraries"} ]; + scope.libraryKey = "" + throw err; + } finally { + scope.loadingLibraries = false; + $timeout( () => {}, 0); + } + } - scope.linker( (filler) => { + scope.libraryChanged = async () => { + if (scope.libraryKey == null) { + throw Error(`null libraryKey? ${scope.libraryKey} ${new Date().getTime()} `); + } + if (scope.libraryKey === "") { + scope.sourceKey = ""; + return; + } + scope.loadingCollections = true; + try { + let collections; + if (scope.libraryKey === "$PLAYLISTS") { + collections = (await dizquetv.getFromPlexProxy(scope.server, `/playlists`)).Metadata; + } else { + collections = (await dizquetv.getFromPlexProxy(scope.server, `/library/sections/${scope.libraryKey}/collections`)); + collections = collections.Metadata + } + if (typeof(collections) === "undefined") { + //when the library has no collections it returns size=0 + //and no array + collections = []; + } + let officialCollections = collections.map( (col) => { + return { + "key" : col.key, + "description" : col.title, + } + } ); + let defaultSource = { + "key": "", + "description" : "Select a Source...", + }; + if (officialCollections.length == 0) { + defaultSource = { + "key": "", + "description" : "(No collections/lists found)", + } + } + if (! officialCollections.some( (col) => col.key === scope.sourceKey ) ) { + scope.sourceKey = ""; + scope.sources = [defaultSource].concat(officialCollections); + } else { + scope.sources = officialCollections; + } + } catch (err) { + scope.sources = [ { name: "", description: "Unable to load collections"} ]; + scope.sourceKey = ""; + throw err; + } finally { + scope.loadingCollections = false; + $timeout( () => {}, 0); + } + } + + let reloadServers = async() => { + scope.loadingServers = true; + try { + let servers = await dizquetv.getPlexServers(); + scope.servers = servers.map( (s) => { + return { + "name" : s.name, + "description" : `Plex - ${s.name}`, + } + } ); + let defaultServer = { + name: "", + description: "Select a Plex server..." + }; + if (! scope.servers.some( (server) => server.name === scope.server) ) { + scope.server = ""; + scope.servers = [defaultServer].concat(scope.servers); + } + } catch (err) { + scope.server = ""; + scope.servers = [ {name:"", description:"Could not load servers"} ]; + throw err; + } finally { + scope.loadingServers = false; + $timeout( () => {}, 0); + } + + await scope.serverChanged(); + await scope.libraryChanged(); + + }; + + + + + scope.linker( async (filler) => { + if ( typeof(filler) === 'undefined') { scope.name = ""; scope.content = []; scope.id = undefined; scope.title = "Create Filler List"; + scope.mode = "import"; + scope.server = ""; + scope.libraryKey = ""; + scope.sourceKey = ""; } else { scope.name = filler.name; scope.content = filler.content; scope.id = filler.id; scope.title = "Edit Filler List"; + scope.mode = filler.mode; + scope.server = filler?.import?.serverName; + if ( typeof(scope.server) !== "string" ) { + scope.server = ""; + } + scope.libraryKey = filler?.import?.meta?.libraryKey; + if ( typeof(scope.libraryKey) !== "string" ) { + scope.libraryKey = ""; + } + scope.sourceKey = filler?.import?.key; + if ( typeof(scope.sourceKey) !== "string" ) { + scope.sourceKey = ""; + } } + await reloadServers(); + scope.source = ""; refreshContentIndexes(); scope.visible = true; } ); @@ -73,8 +236,17 @@ module.exports = function ($timeout, commonProgramTools, getShowData) { if ( (typeof(scope.name) === 'undefined') || (scope.name.length == 0) ) { scope.error = "Please enter a name"; } - if ( scope.content.length == 0) { - scope.error = "Please add at least one clip."; + if ( scope?.mode === "import" ) { + if ( (typeof(scope?.server) !== "string" ) || (scope?.server === "") ) { + scope.error = "Please select a server" + } + if ( (typeof(scope?.source) !== "string" ) && (scope?.source === "") ) { + scope.error = "Please select a source." + } + } else { + if ( scope.content.length == 0) { + scope.error = "Please add at least one clip."; + } } if (typeof(scope.error) !== 'undefined') { $timeout( () => { @@ -83,14 +255,30 @@ module.exports = function ($timeout, commonProgramTools, getShowData) { return; } scope.visible = false; - scope.onDone( { + let object = { name: scope.name, content: scope.content.map( (c) => { delete c.$index return c; } ), id: scope.id, - } ); + mode: scope.mode, + + }; + if (object.mode === "import") { + object.content = []; + //In reality dizqueTV only needs to know the server name + //and the source key, the meta object is for extra data + //that is useful for external things like this UI. + object.import = { + serverName : scope.server, + key: scope.sourceKey, + meta: { + libraryKey : scope.libraryKey, + } + } + } + scope.onDone( object ); } scope.getText = (clip) => { let show = getShowData(clip); diff --git a/web/public/templates/filler-config.html b/web/public/templates/filler-config.html index 8724e10..21b1713 100644 --- a/web/public/templates/filler-config.html +++ b/web/public/templates/filler-config.html @@ -11,8 +11,52 @@
Click the to import filler content from your Plex server(s).