diff --git a/.github/workflows/README.md b/.github/workflows/README.md new file mode 100644 index 0000000..bfd8cc9 --- /dev/null +++ b/.github/workflows/README.md @@ -0,0 +1,13 @@ +# The workflow + + edge main + ^ \ ^ + | \------2--\ | + 1 \ 3 + | \ | + development <-4-- patch + +1. Releasing a new 'edge' version. +2. Moving an 'edge' version to stable. +3. Releasing a stable version +4. Aligning bug fixes with the new features. \ No newline at end of file diff --git a/.github/workflows/development-binaries.yaml b/.github/workflows/development-binaries.yaml index 4a2705b..21b1be9 100644 --- a/.github/workflows/development-binaries.yaml +++ b/.github/workflows/development-binaries.yaml @@ -3,7 +3,7 @@ name: Development Binaries on: push: branches: - - dev/1.5.x + - development jobs: binaries: diff --git a/.github/workflows/development-tag.yaml b/.github/workflows/development-tag.yaml index a3dc0ee..198d9e6 100644 --- a/.github/workflows/development-tag.yaml +++ b/.github/workflows/development-tag.yaml @@ -3,7 +3,7 @@ name: Development Tag on: push: branches: - - dev/1.5.x + - development jobs: docker: diff --git a/Dockerfile b/Dockerfile index 991ecd6..d24b18c 100644 --- a/Dockerfile +++ b/Dockerfile @@ -10,5 +10,6 @@ FROM akashisn/ffmpeg:4.4.5 EXPOSE 8000 WORKDIR /home/node/app ENTRYPOINT [ "./dizquetv" ] +ENV DIZQUETV_FFMPEG_PATH=/usr/bin/ffmpeg COPY --from=0 /home/node/app/dist/dizquetv /home/node/app/ RUN ln -s /usr/local/bin/ffmpeg /usr/bin/ffmpeg diff --git a/Dockerfile-nvidia b/Dockerfile-nvidia index 52fc831..ad468b3 100644 --- a/Dockerfile-nvidia +++ b/Dockerfile-nvidia @@ -10,5 +10,6 @@ FROM jrottenberg/ffmpeg:4.4.5-nvidia2204 EXPOSE 8000 WORKDIR /home/node/app ENTRYPOINT [ "./dizquetv" ] +ENV DIZQUETV_FFMPEG_PATH=/usr/bin/ffmpeg COPY --from=0 /home/node/app/dist/dizquetv /home/node/app/ RUN ln -s /usr/local/bin/ffmpeg /usr/bin/ffmpeg diff --git a/README.md b/README.md index 4ce4696..0a38998 100644 --- a/README.md +++ b/README.md @@ -1,4 +1,4 @@ -# dizqueTV 1.6.0 +# dizqueTV 1.7.0    Create live TV channel streams from media on your Plex servers. diff --git a/index.js b/index.js index 5805ef7..7e92b24 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'); @@ -32,6 +33,9 @@ const ProgrammingService = require("./src/services/programming-service"); const ActiveChannelService = require('./src/services/active-channel-service') const ProgramPlayTimeDB = require('./src/dao/program-play-time-db') const FfmpegSettingsService = require('./src/services/ffmpeg-settings-service') +const PlexProxyService = require('./src/services/plex-proxy-service') +const PlexServerDB = require('./src/dao/plex-server-db'); +const FFMPEGInfo = require('./src/ffmpeg-info'); const onShutdown = require("node-graceful-shutdown").onShutdown; @@ -51,16 +55,12 @@ if (NODE < 12) { console.error(`WARNING: Your nodejs version ${process.version} is lower than supported. dizqueTV has been tested best on nodejs 12.16.`); } -unlockPath = false; for (let i = 0, l = process.argv.length; i < l; i++) { if ((process.argv[i] === "-p" || process.argv[i] === "--port") && i + 1 !== l) process.env.PORT = process.argv[i + 1] if ((process.argv[i] === "-d" || process.argv[i] === "--database") && i + 1 !== l) process.env.DATABASE = process.argv[i + 1] - if (process.argv[i] === "--unlock") { - unlockPath = true; - } } process.env.DATABASE = process.env.DATABASE || path.join(".", ".dizquetv") @@ -92,6 +92,8 @@ if(!fs.existsSync(path.join(process.env.DATABASE, 'cache','images'))) { fs.mkdirSync(path.join(process.env.DATABASE, 'cache','images')) } +const ffmpegInfo = new FFMPEGInfo(process.env, process.env.DATABASE); +ffmpegInfo.initialize(); channelDB = new ChannelDB( path.join(process.env.DATABASE, 'channels') ); @@ -103,10 +105,13 @@ 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, unlockPath); +let ffmpegSettingsService = new FfmpegSettingsService(db); +let plexServerDB = new PlexServerDB(channelService, fillerDB, customShowDB, db); +let plexProxyService = new PlexProxyService(plexServerDB); + async function initializeProgramPlayTimeDB() { try { @@ -130,6 +135,9 @@ activeChannelService = new ActiveChannelService(onDemandService, channelService) eventService = new EventService(); +let fillerService = new FillerService(fillerDB, plexProxyService, + channelService); + i18next .use(i18nextBackend) .use(i18nextMiddleware.LanguageDetector) @@ -251,6 +259,38 @@ channelService.on("channel-update", (data) => { let hdhr = HDHR(db, channelDB) let app = express() + +const responseInterceptor = ( + req, + res, + next +) => { + + let t0 = new Date().getTime(); + + const originalSend = res.send; + let responseSent = false; + console.log(`${req.method} ${req.url} ...`); + if (req.method === "GET" && req.url.includes("images/uploads") ) { + res.setHeader("Cache-Control", "public, max-age=86400"); + } + + res.send = function (body) { + + if (!responseSent) { + let t1 = new Date().getTime(); + let dt = t1 - t0; + console.log(`${req.method} ${req.url} ${res.statusCode} in ${dt}ms`); + responseSent = true; + } + + return originalSend.call(this, body); + }; + + next(); +}; +app.use(responseInterceptor); + eventService.setup(app); app.use( @@ -290,12 +330,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)) +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 )) +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/pull_request_template.md b/pull_request_template.md index b45a9ec..3190e7b 100644 --- a/pull_request_template.md +++ b/pull_request_template.md @@ -7,8 +7,8 @@ * [ ] I have read the code of conduct. * [ ] I am submitting to the correct base branch ### Changes that modify the db structure diff --git a/src/api.js b/src/api.js index 7d21f4d..bb4c72d 100644 --- a/src/api.js +++ b/src/api.js @@ -4,8 +4,6 @@ const path = require('path') const fs = require('fs') const constants = require('./constants'); const JSONStream = require('JSONStream'); -const FFMPEGInfo = require('./ffmpeg-info'); -const PlexServerDB = require('./dao/plex-server-db'); const Plex = require("./plex.js"); const timeSlotsService = require('./services/time-slots-service'); @@ -24,15 +22,13 @@ function safeString(object) { } module.exports = { router: api } -function api(db, channelService, fillerDB, customShowDB, xmltvInterval, guideService, _m3uService, eventService, ffmpegSettingsService ) { +function api(db, channelService, fillerDB, customShowDB, xmltvInterval, guideService, _m3uService, eventService, ffmpegSettingsService, plexServerDB, plexProxyService, ffmpegInfo, fillerService ) { let m3uService = _m3uService; const router = express.Router() - const plexServerDB = new PlexServerDB(channelService, fillerDB, customShowDB, db); router.get('/api/version', async (req, res) => { try { - let ffmpegSettings = db['ffmpeg-settings'].find()[0]; - let v = await (new FFMPEGInfo(ffmpegSettings)).getVersion(); + let v = await ffmpegInfo.getVersion(); res.send( { "dizquetv" : constants.VERSION_NAME, "ffmpeg" : v, @@ -216,7 +212,15 @@ function api(db, channelService, fillerDB, customShowDB, xmltvInterval, guideSe ); } }) - + router.get('/api/plex-server/:serverName64/:path(*)', async (req, res) => { + try { + let result = await plexProxyService.get(req.params.serverName64, req.params.path); + res.status(200).send(result); + } catch (err) { + console.error("Could not use plex proxy.", err); + res.status(404).send("Could not call plex server."); + } + }); // Channels router.get('/api/channels', async (req, res) => { @@ -415,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); @@ -424,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); @@ -437,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); @@ -451,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"); } @@ -609,6 +613,18 @@ function api(db, channelService, fillerDB, customShowDB, xmltvInterval, guideSe }) + router.get('/api/ffmpeg-info', async (req, res) => { + try { + let ffmpeg = await ffmpegInfo.getPath(); + let obj = { ffmpegPath: ffmpeg } + res.send(obj) + } catch(err) { + console.error(err); + res.status(500).send("error"); + } + }) + + // PLEX SETTINGS router.get('/api/plex-settings', (req, res) => { try { diff --git a/src/constants.js b/src/constants.js index 9346a71..33e061c 100644 --- a/src/constants.js +++ b/src/constants.js @@ -35,5 +35,5 @@ module.exports = { // staying active, it checks every 5 seconds PLAYED_MONITOR_CHECK_FREQUENCY: 5*1000, - VERSION_NAME: "1.6.0" + VERSION_NAME: "1.7.0" } 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/dao/plex-server-db.js b/src/dao/plex-server-db.js index cd35d4c..a3a7000 100644 --- a/src/dao/plex-server-db.js +++ b/src/dao/plex-server-db.js @@ -15,6 +15,17 @@ class PlexServerDB this.showDB = showDB; } + async getPlexServerByName(name) { + let servers = this.db['plex-servers'].find() + let server = servers.sort( (a,b) => { return a.index - b.index } ) + .filter( (server) => name === server.name ) + [0]; + if (typeof(server) === "undefined") { + return null; + } + return server; + } + async fixupAllChannels(name, newServer) { let channelNumbers = await this.channelService.getAllChannelNumbers(); let report = await Promise.all( channelNumbers.map( async (i) => { diff --git a/src/database-migration.js b/src/database-migration.js index cef34a7..3059081 100644 --- a/src/database-migration.js +++ b/src/database-migration.js @@ -20,8 +20,7 @@ const path = require('path'); var fs = require('fs'); -const TARGET_VERSION = 805; -const DAY_MS = 1000 * 60 * 60 * 24; +const TARGET_VERSION = 1000; const STEPS = [ // [v, v2, x] : if the current version is v, call x(db), and version becomes v2 @@ -44,8 +43,10 @@ const STEPS = [ [ 800, 801, (db) => addImageCache(db) ], [ 801, 802, () => addGroupTitle() ], [ 802, 803, () => fixNonIntegerDurations() ], - [ 803, 805, (db) => addFFMpegLock(db) ], - [ 804, 805, (db) => addFFMpegLock(db) ], + [ 803, 900, (db) => fixFFMpegPathSetting(db) ], + [ 804, 900, (db) => fixFFMpegPathSetting(db) ], + [ 805, 900, (db) => fixFFMpegPathSetting(db) ], + [ 900, 1000, () => fixFillerModes() ], ] const { v4: uuidv4 } = require('uuid'); @@ -75,7 +76,7 @@ function appNameChange(db) { function basicDB(db) { //this one should either try recovering the db from a very old version - //or buildl a completely empty db at version 0 + //or build a completely empty db at version 0 let ffmpegSettings = db['ffmpeg-settings'].find() let plexSettings = db['plex-settings'].find() @@ -386,8 +387,6 @@ function ffmpeg() { return { //How default ffmpeg settings should look configVersion: 5, - ffmpegPath: "/usr/bin/ffmpeg", - ffmpegPathLockDate: new Date().getTime() + DAY_MS, threads: 4, concatMuxDelay: "0", logFfmpeg: false, @@ -681,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'); @@ -769,19 +787,23 @@ function addScalingAlgorithm(db) { fs.writeFileSync( f, JSON.stringify( [ffmpegSettings] ) ); } -function addFFMpegLock(db) { +function fixFFMpegPathSetting(db) { let ffmpegSettings = db['ffmpeg-settings'].find()[0]; let f = path.join(process.env.DATABASE, 'ffmpeg-settings.json'); - if ( typeof(ffmpegSettings.ffmpegPathLockDate) === 'undefined' || ffmpegSettings.ffmpegPathLockDate == null ) { + let f2 = path.join(process.env.DATABASE, 'ffmpeg-path.json'); + delete ffmpegSettings.ffmpegPathLockDate; + let fpath = ffmpegSettings.ffmpegPath; + delete ffmpegSettings.ffmpegPath; - console.log("Adding ffmpeg lock. For your security it will not be possible to modify the ffmpeg path using the UI anymore unless you launch dizquetv by following special instructions.."); - // We are migrating an existing db that had a ffmpeg path. Make sure - // it's already locked. - ffmpegSettings.ffmpegPathLockDate = new Date().getTime() - 2 * DAY_MS; + if (typeof(fpath) === "string" ) { + console.log(`Found existing setting ffmpegPath=${fpath}, creating setting file (This file will get ignored if you are already setting an environment variable (the docker images do that)).`); + let pathJson = { ffmpegPath : fpath }; + fs.writeFileSync( f2, JSON.stringify( pathJson ) ); } fs.writeFileSync( f, JSON.stringify( [ffmpegSettings] ) ); } + function moveBackup(path) { if (fs.existsSync(`${process.env.DATABASE}${path}`) ) { let i = 0; diff --git a/src/ffmpeg-info.js b/src/ffmpeg-info.js index e7dd7f0..cceddcf 100644 --- a/src/ffmpeg-info.js +++ b/src/ffmpeg-info.js @@ -1,13 +1,98 @@ const exec = require('child_process').exec; +const fs = require('fs'); +const path = require('path'); class FFMPEGInfo { - constructor(opts) { - this.ffmpegPath = opts.ffmpegPath + + constructor(env, dbPath) { + this.initialized = false; + this.env = env; + this.dbPath = dbPath; + this.ffmpegPath = null; + this.origin = "Not found"; } - async getVersion() { + + async initialize() { + let selectedPath = null; + if (typeof(this.env.DIZQUETV_FFMPEG_PATH) === "string") { + selectedPath = this.env.DIZQUETV_FFMPEG_PATH; + this.origin = "env.DIZQUETV_FFMPEG_PATH"; + } else { + selectedPath = await this.getPathFromFile(this.dbPath, 'ffmpeg-path.json'); + this.origin = "ffmpeg-path.json"; + } + if (selectedPath == null) { + //windows Path environment var + let paths = this.env.Path; + if (typeof(paths) === "string") { + let maybe = paths.split(";").filter( + (str) => str.contains("ffmpeg" ) + )[0]; + if (typeof(maybe) === "string") { + selectedPath = path.join(maybe, "ffmpeg.exe"); + this.origin = "Widnows Env. Path"; + } + } + } + if (selectedPath == null) { + //Default install path for ffmpeg in n*x OSes. + // if someone has built ffmpeg manually or wants an alternate + // path, they are most likely capable of configuring it manually. + selectedPath = "/usr/bin/ffmpeg"; + this.origin = "Default"; + } + + if (selectedPath != null) { + let version = await this.checkVersion(selectedPath); + if (version == null) { + selectedPath = null; + } else { + console.log(`FFmpeg found: ${selectedPath} from: ${this.origin}. version: ${version}`); + this.ffmpegPath = selectedPath; + } + } + this.initialized = true; + + } + + async getPath() { + if (! this.initialized) { + await this.initialize(); + } + return this.ffmpegPath; + } + + async getPathFromFile(folder, fileName) { + let f = path.join(folder, fileName); + try { + let json = await new Promise( (resolve, reject) => { + fs.readFile(f, (err, data) => { + if (err) { + return reject(err); + } + try { + resolve( JSON.parse(data) ) + } catch (err) { + reject(err); + } + }) + }); + let ffmpeg = json["ffmpegPath"]; + if (typeof(ffmpeg) === "string") { + return ffmpeg; + } else { + return null; + } + } catch (err) { + console.error(err); + return null; + } + } + + async checkVersion(ffmpegPath) { try { let s = await new Promise( (resolve, reject) => { - exec( `"${this.ffmpegPath}" -version`, function(error, stdout, stderr){ + exec( `"${ffmpegPath}" -version`, function(error, stdout, stderr){ if (error !== null) { reject(error); } else { @@ -23,7 +108,20 @@ class FFMPEGInfo { return m[1]; } catch (err) { console.error("Error getting ffmpeg version", err); + return null; + } + } + + + async getVersion() { + if (! this.initialized) { + await this.initialize(); + } + let version = await this.checkVersion(this.ffmpegPath); + if (version == null) { return "Error"; + } else { + return version; } } } diff --git a/src/ffmpeg.js b/src/ffmpeg.js index c503279..7c5c3df 100644 --- a/src/ffmpeg.js +++ b/src/ffmpeg.js @@ -7,6 +7,7 @@ const REALLY_RIDICULOUSLY_HIGH_FPS_FOR_DIZQUETVS_USECASE = 120; class FFMPEG extends events.EventEmitter { constructor(opts, channel) { super() + this.ffmpegPath = opts.ffmpegPath; this.opts = opts; this.errorPicturePath = `http://localhost:${process.env.PORT}/images/generic-error-screen.png`; this.ffmpegName = "unnamed ffmpeg"; @@ -22,7 +23,6 @@ class FFMPEG extends events.EventEmitter { this.opts.maxFPS = REALLY_RIDICULOUSLY_HIGH_FPS_FOR_DIZQUETVS_USECASE; } this.channel = channel - this.ffmpegPath = opts.ffmpegPath let resString = opts.targetResolution; if ( @@ -322,7 +322,7 @@ class FFMPEG extends events.EventEmitter { if (watermark.animated === true) { ffmpegArgs.push('-ignore_loop', '0'); } - ffmpegArgs.push(`-i`, `${watermark.url}` ); + ffmpegArgs.push(`-i`, `async:cache:${watermark.url}` ); overlayFile = inputFiles++; this.ensureResolution = true; } @@ -601,7 +601,7 @@ class FFMPEG extends events.EventEmitter { return; } if (! this.sentData) { - this.emit('error', { code: code, cmd: `${this.opts.ffmpegPath} ${ffmpegArgs.join(' ')}` }) + this.emit('error', { code: code, cmd: `${this.ffmpegPath} ${ffmpegArgs.join(' ')}` }) } console.log( `${this.ffmpegName} exited with code 255.` ); this.emit('close', code) diff --git a/src/services/channel-service.js b/src/services/channel-service.js index 831675e..92b67bc 100644 --- a/src/services/channel-service.js +++ b/src/services/channel-service.js @@ -69,9 +69,6 @@ function cleanUpProgram(program) { program.endPosition = parseInt(program.endPosition, 10); } - if (program.start && program.stop) { - program.duration = new Date(program.stop) - new Date(program.start); - } delete program.streams; delete program.durationStr; delete program.commercials; diff --git a/src/services/ffmpeg-settings-service.js b/src/services/ffmpeg-settings-service.js index 6e4b149..0308488 100644 --- a/src/services/ffmpeg-settings-service.js +++ b/src/services/ffmpeg-settings-service.js @@ -4,11 +4,8 @@ const path = require('path'); const fs = require('fs'); class FfmpegSettingsService { - constructor(db, unlock) { + constructor(db) { this.db = db; - if (unlock) { - this.unlock(); - } } get() { @@ -21,13 +18,6 @@ class FfmpegSettingsService { return ffmpeg; } - unlock() { - let ffmpeg = this.getCurrentState(); - console.log("ffmpeg path UI unlocked for another day..."); - ffmpeg.ffmpegPathLockDate = new Date().getTime() + DAY_MS; - this.db['ffmpeg-settings'].update({ _id: ffmpeg._id }, ffmpeg) - } - update(attempt) { let ffmpeg = this.getCurrentState(); @@ -62,7 +52,6 @@ class FfmpegSettingsService { } reset() { - // Even if reseting, it's impossible to unlock the ffmpeg path let ffmpeg = databaseMigration.defaultFFMPEG() ; this.update(ffmpeg); return this.get(); 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 new file mode 100644 index 0000000..7ef9f21 --- /dev/null +++ b/src/services/plex-proxy-service.js @@ -0,0 +1,94 @@ +const Plex = require('../plex.js') +const events = require('events') + +class PlexProxyService extends events.EventEmitter { + + constructor(plexServerDB) { + super(); + this.plexServerDB = plexServerDB; + } + + async get(serverName64, path) { + 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]; + } + + + 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 f113ab2..512004b 100644 --- a/src/video.js +++ b/src/video.js @@ -18,17 +18,18 @@ async function shutdown() { stopPlayback = true; } -function video( channelService, fillerDB, db, programmingService, activeChannelService, programPlayTimeDB ) { +function video( channelService, fillerService, db, programmingService, activeChannelService, programPlayTimeDB, ffmpegInfo ) { var router = express.Router() - router.get('/setup', (req, res) => { + router.get('/setup', async (req, res) => { let ffmpegSettings = db['ffmpeg-settings'].find()[0] // Check if ffmpeg path is valid - if (!fs.existsSync(ffmpegSettings.ffmpegPath)) { - res.status(500).send("FFMPEG path is invalid. The file (executable) doesn't exist.") - console.error("The FFMPEG Path is invalid. Please check your configuration.") + let ffmpegPath = await ffmpegInfo.getPath(); + if (ffmpegPath == null) { + res.status(500).send("Missing FFmpeg.") return } + ffmpegSettings.ffmpegPath = ffmpegPath; console.log(`\r\nStream starting. Channel: 1 (dizqueTV)`) @@ -72,14 +73,14 @@ function video( channelService, fillerDB, db, programmingService, activeChannelS return } - let ffmpegSettings = db['ffmpeg-settings'].find()[0] - // Check if ffmpeg path is valid - if (!fs.existsSync(ffmpegSettings.ffmpegPath)) { - res.status(500).send("FFMPEG path is invalid. The file (executable) doesn't exist.") - console.error("The FFMPEG Path is invalid. Please check your configuration.") + let ffmpegSettings = db['ffmpeg-settings'].find()[0] + let ffmpegPath = await ffmpegInfo.getPath(); + if (ffmpegPath == null) { + res.status(500).send("Missing FFmpeg.") return } + ffmpegSettings.ffmpegPath = ffmpegPath; if (step == 0) { res.writeHead(200, { @@ -174,14 +175,14 @@ function video( channelService, fillerDB, db, programmingService, activeChannelS let isBetween = ( (typeof req.query.between !== 'undefined') && (req.query.between=='1') ); - let ffmpegSettings = db['ffmpeg-settings'].find()[0] - // Check if ffmpeg path is valid - if (!fs.existsSync(ffmpegSettings.ffmpegPath)) { - res.status(500).send("FFMPEG path is invalid. The file (executable) doesn't exist.") - console.error("The FFMPEG Path is invalid. Please check your configuration.") + let ffmpegSettings = db['ffmpeg-settings'].find()[0] + let ffmpegPath = await ffmpegInfo.getPath(); + if (ffmpegPath == null) { + res.status(500).send("Missing FFmpeg.") return } + ffmpegSettings.ffmpegPath = ffmpegPath; if (ffmpegSettings.disablePreludes === true) { //disable the preludes @@ -303,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/ffmpeg-settings.js b/web/directives/ffmpeg-settings.js index e474097..df409b9 100644 --- a/web/directives/ffmpeg-settings.js +++ b/web/directives/ffmpeg-settings.js @@ -6,7 +6,14 @@ module.exports = function (dizquetv, resolutionOptions) { scope: { }, link: function (scope, element, attrs) { - //add validations to ffmpeg settings, speciall commas in codec name + + scope.ffmpegPathLoading = true; + scope.ffmpegPath = "" + dizquetv.getFFMpegPath().then( (fpath) => { + scope.ffmpegPath = fpath.ffmpegPath; + scope.ffmpegPathLoading = false; + }); + //add validations to ffmpeg settings, special commas in codec name dizquetv.getFfmpegSettings().then((settings) => { scope.settings = settings }) 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/directives/plex-library.js b/web/directives/plex-library.js index 153b2e2..0406d89 100644 --- a/web/directives/plex-library.js +++ b/web/directives/plex-library.js @@ -31,13 +31,7 @@ module.exports = function (plex, dizquetv, $timeout, commonProgramTools) { }); } scope.selectOrigin = function (origin) { - if ( origin.type === 'plex' ) { - scope.plexServer = origin.server; - updateLibrary(scope.plexServer); - } else { - scope.plexServer = undefined; - updateCustomShows(); - } + updateLibrary(origin); } scope._onFinish = (s, insertPoint) => { if (s.length > scope.limit) { @@ -99,20 +93,31 @@ module.exports = function (plex, dizquetv, $timeout, commonProgramTools) { "type" : "plex", "name" : `Plex - ${s.name}`, "server": s, + "loaded" : false, } } ); - scope.currentOrigin = scope.origins[0]; - scope.plexServer = scope.currentOrigin.server; scope.origins.push( { "type": "dizquetv", "name" : "dizqueTV - Custom Shows", + "loaded" : false, } ); - updateLibrary(scope.plexServer) + updateLibrary(scope.origins[0]) }) - let updateLibrary = async(server) => { + let updateLibrary = async(origin) => { + scope.currentOrigin = origin; + origin.loaded = false; + if ( origin.type !== 'plex' ) { + scope.plexServer = undefined; + await updateCustomShows(); + origin.loaded = true; + return; + } + let server = scope.currentOrigin.server; let lib = await plex.getLibrary(server); let play = await plex.getPlaylists(server); + scope.currentOrigin.loaded = true; + scope.plexServer = server; play.forEach( p => { p.type = "playlist"; diff --git a/web/directives/plex-settings.js b/web/directives/plex-settings.js index 53a51a2..83994fd 100644 --- a/web/directives/plex-settings.js +++ b/web/directives/plex-settings.js @@ -20,12 +20,9 @@ module.exports = function (plex, dizquetv, $timeout) { scope.servers = servers; if(servers) { for (let i = 0; i < scope.servers.length; i++) { - scope.servers[i].uiStatus = 0; scope.servers[i].backendStatus = 0; let t = (new Date()).getTime(); - scope.servers[i].uiPending = t; scope.servers[i].backendPending = t; - scope.refreshUIStatus(t, i); scope.refreshBackendStatus(t, i); } } @@ -51,22 +48,6 @@ module.exports = function (plex, dizquetv, $timeout) { scope.refreshServerList(); } - scope.isAnyUIBad = () => { - let t = (new Date()).getTime(); - if(scope.servers) { - for (let i = 0; i < scope.servers.length; i++) { - let s = scope.servers[i]; - if ( - (s.uiStatus == -1) - || ( (s.uiStatus == 0) && (s.uiPending + 30000 < t) ) - ) { - return true; - } - } - } - return false; - }; - scope.isAnyBackendBad = () => { let t = (new Date()).getTime(); if(scope.servers) { @@ -84,15 +65,6 @@ module.exports = function (plex, dizquetv, $timeout) { }; - scope.refreshUIStatus = async (t, i) => { - let s = await plex.check(scope.servers[i]); - if (scope.servers[i].uiPending == t) { - // avoid updating for a previous instance of the row - scope.servers[i].uiStatus = s; - } - scope.$apply(); - }; - scope.refreshBackendStatus = async (t, i) => { let s = await dizquetv.checkExistingPlexServer(scope.servers[i].name); if (scope.servers[i].backendPending == t) { diff --git a/web/public/locales/en/main.json b/web/public/locales/en/main.json index 3bf946d..a067645 100644 --- a/web/public/locales/en/main.json +++ b/web/public/locales/en/main.json @@ -19,12 +19,10 @@ "minutes_to_sign_plex": "You have 2 minutes to sign into your Plex Account.", "name": "Name", "uri": "URI", - "ui_route": "UI Route", - "backend_route": "Backend Route", + "routeStatus": "Route Status", "ok": "ok", "error": "error", - "ui_bad": "If a Plex server configuration has problems with the UI route, the channel editor won't be able to access its content.", - "backend_bad": "If a Plex server configuration has problems with the backend route, dizqueTV won't be able to play its content.", + "backend_bad": "A route problem means the dizqueTV server can't establish a connection with the configured Plex server.", "plex_transcoder_settings": "Plex Transcoder Settings", "update": "Update", "reset_options": "Reset Options", diff --git a/web/public/templates/ffmpeg-settings.html b/web/public/templates/ffmpeg-settings.html index bcaa3f7..4f0f9bf 100644 --- a/web/public/templates/ffmpeg-settings.html +++ b/web/public/templates/ffmpeg-settings.html @@ -15,40 +15,20 @@
Click the to import filler content from your Plex server(s).