From 3022dfe375da5f65366f80e30c72d20acff74be7 Mon Sep 17 00:00:00 2001 From: vexorian Date: Sat, 22 Aug 2020 09:45:47 -0400 Subject: [PATCH] 1 json per channel. Plex server editing and status. Max resolution for transcoding. 640x360 fix. --- index.js | 28 ++- make_dist.sh | 4 +- src/api.js | 173 ++++++++++++---- src/channel-cache.js | 26 +-- src/dao/channel-db.js | 103 ++++++++++ src/dao/plex-server-db.js | 142 +++++++++++++ src/database-migration.js | 143 ++++++++++++- src/ffmpeg.js | 20 +- src/hdhr.js | 6 +- src/helperFuncs.js | 28 +-- src/plex-player.js | 7 +- src/plex.js | 9 + src/plexTranscoder.js | 8 +- src/video.js | 29 ++- web/app.js | 1 + web/controllers/channels.js | 85 ++++++-- web/controllers/version.js | 4 +- web/directives/channel-config.js | 18 +- web/directives/ffmpeg-settings.js | 2 +- web/directives/offline-config.js | 6 +- web/directives/plex-library.js | 3 +- web/directives/plex-server-edit.js | 72 +++++++ web/directives/plex-settings.js | 221 ++++++++++++++++++--- web/directives/program-config.js | 2 +- web/public/templates/channel-config.html | 5 +- web/public/templates/ffmpeg-settings.html | 2 +- web/public/templates/offline-config.html | 12 +- web/public/templates/plex-library.html | 6 +- web/public/templates/plex-server-edit.html | 96 +++++++++ web/public/templates/plex-settings.html | 47 +++-- web/public/views/channels.html | 15 +- web/public/views/version.html | 6 +- web/services/dizquetv.js | 55 ++++- web/services/plex.js | 20 +- 34 files changed, 1187 insertions(+), 217 deletions(-) create mode 100644 src/dao/channel-db.js create mode 100644 src/dao/plex-server-db.js create mode 100644 web/directives/plex-server-edit.js create mode 100644 web/public/templates/plex-server-edit.html diff --git a/index.js b/index.js index 1336335..87411a3 100644 --- a/index.js +++ b/index.js @@ -14,6 +14,7 @@ const xmltv = require('./src/xmltv') const Plex = require('./src/plex'); const channelCache = require('./src/channel-cache'); const constants = require('./src/constants') +const ChannelDB = require("./src/dao/channel-db"); console.log( ` \\ @@ -43,18 +44,25 @@ if (!fs.existsSync(process.env.DATABASE)) { fs.mkdirSync(process.env.DATABASE) } -if(!fs.existsSync(path.join(process.env.DATABASE, 'images'))) +if(!fs.existsSync(path.join(process.env.DATABASE, 'images'))) { fs.mkdirSync(path.join(process.env.DATABASE, 'images')) +} + +if(!fs.existsSync(path.join(process.env.DATABASE, 'channels'))) { + fs.mkdirSync(path.join(process.env.DATABASE, 'channels')) +} + +channelDB = new ChannelDB( path.join(process.env.DATABASE, 'channels') ); db.connect(process.env.DATABASE, ['channels', 'plex-servers', 'ffmpeg-settings', 'plex-settings', 'xmltv-settings', 'hdhr-settings', 'db-version', 'client-id']) -initDB(db) +initDB(db, channelDB) let xmltvInterval = { interval: null, lastRefresh: null, - updateXML: () => { - let channels = db['channels'].find() + updateXML: async () => { + let channels = await channelDB.getAllChannels() channels.forEach( (channel) => { // if we are going to go through the trouble of loading the whole channel db, we might // as well take that opportunity to reduce stream loading times... @@ -84,8 +92,8 @@ let xmltvInterval = { startInterval: () => { let xmltvSettings = db['xmltv-settings'].find()[0] if (xmltvSettings.refresh !== 0) { - xmltvInterval.interval = setInterval(() => { - xmltvInterval.updateXML() + xmltvInterval.interval = setInterval( async () => { + await xmltvInterval.updateXML() }, xmltvSettings.refresh * 60 * 60 * 1000) } }, @@ -99,7 +107,7 @@ let xmltvInterval = { xmltvInterval.updateXML() xmltvInterval.startInterval() -let hdhr = HDHR(db) +let hdhr = HDHR(db, channelDB) let app = express() app.use(bodyParser.json({limit: '50mb'})) app.get('/version.js', (req, res) => { @@ -122,7 +130,7 @@ app.get('/version.js', (req, res) => { app.use('/images', express.static(path.join(process.env.DATABASE, 'images'))) app.use(express.static(path.join(__dirname, 'web/public'))) app.use('/images', express.static(path.join(process.env.DATABASE, 'images'))) -app.use(api.router(db, xmltvInterval)) +app.use(api.router(db, channelDB, xmltvInterval)) app.use(video.router(db)) app.use(hdhr.router) app.listen(process.env.PORT, () => { @@ -132,8 +140,8 @@ app.listen(process.env.PORT, () => { hdhr.ssdp.start() }) -function initDB(db) { - dbMigration.initDB(db); +function initDB(db, channelDB) { + dbMigration.initDB(db, channelDB); if (!fs.existsSync(process.env.DATABASE + '/font.ttf')) { let data = fs.readFileSync(path.resolve(path.join(__dirname, 'resources/font.ttf'))) fs.writeFileSync(process.env.DATABASE + '/font.ttf', data) diff --git a/make_dist.sh b/make_dist.sh index 21767e5..c920ddd 100644 --- a/make_dist.sh +++ b/make_dist.sh @@ -6,8 +6,8 @@ MACOSX=dizquetv-macos-x64 LINUX64=${LINUXBUILD:-dizquetv-linux-x64} rm -R ./dist/* -npm run build -npm run compile +npm run build || exit 1 +npm run compile || exit 1 cp -R ./web ./dist/web cp -R ./resources ./dist/ cd dist diff --git a/src/api.js b/src/api.js index 44f6b91..4fcac57 100644 --- a/src/api.js +++ b/src/api.js @@ -5,10 +5,13 @@ const databaseMigration = require('./database-migration'); const channelCache = require('./channel-cache') const constants = require('./constants'); const FFMPEGInfo = require('./ffmpeg-info'); +const PlexServerDB = require('./dao/plex-server-db'); +const Plex = require("./plex.js"); module.exports = { router: api } -function api(db, xmltvInterval) { +function api(db, channelDB, xmltvInterval) { let router = express.Router() + let plexServerDB = new PlexServerDB(channelDB, channelCache, db); router.get('/api/version', async (req, res) => { let ffmpegSettings = db['ffmpeg-settings'].find()[0]; @@ -22,50 +25,125 @@ function api(db, xmltvInterval) { // Plex Servers router.get('/api/plex-servers', (req, res) => { let servers = db['plex-servers'].find() + servers.sort( (a,b) => { return a.index - b.index } ); res.send(servers) }) - router.delete('/api/plex-servers', (req, res) => { - db['plex-servers'].remove(req.body, false) - let servers = db['plex-servers'].find() - res.send(servers) + router.post("/api/plex-servers/status", async (req, res) => { + let servers = db['plex-servers'].find( { + name: req.body.name, + }); + if (servers.length != 1) { + return res.status(404).send("Plex server not found."); + } + let plex = new Plex(servers[0]); + let s = await Promise.race( [ + (async() => { + return await plex.checkServerStatus(); + })(), + new Promise( (resolve, reject) => { + setTimeout( () => { resolve(-1); }, 60000); + }), + ]); + res.send( { + status: s, + }); }) - router.post('/api/plex-servers', (req, res) => { - db['plex-servers'].save(req.body) - let servers = db['plex-servers'].find() - res.send(servers) + router.post("/api/plex-servers/foreignstatus", async (req, res) => { + let server = req.body; + let plex = new Plex(server); + let s = await Promise.race( [ + (async() => { + return await plex.checkServerStatus(); + })(), + new Promise( (resolve, reject) => { + setTimeout( () => { resolve(-1); }, 60000); + }), + ]); + res.send( { + status: s, + }); }) + router.delete('/api/plex-servers', async (req, res) => { + let name = req.body.name; + if (typeof(name) === 'undefined') { + return res.status(400).send("Missing name"); + } + let report = await plexServerDB.deleteServer(name); + res.send(report) + }) + router.post('/api/plex-servers', async (req, res) => { + try { + await plexServerDB.updateServer(req.body); + res.status(204).send("Plex server updated.");; + } catch (err) { + console.error("Could not add plex server.", err); + res.status(400).send("Could not add plex server."); + } + }) + router.put('/api/plex-servers', async (req, res) => { + try { + await plexServerDB.addServer(req.body); + res.status(201).send("Plex server added.");; + } catch (err) { + console.error("Could not add plex server.", err); + res.status(400).send("Could not add plex server."); + } + }) + // Channels - router.get('/api/channels', (req, res) => { - let channels = db['channels'].find() + router.get('/api/channels', async (req, res) => { + let channels = await channelDB.getAllChannels(); channels.sort((a, b) => { return a.number < b.number ? -1 : 1 }) res.send(channels) }) - router.post('/api/channels', (req, res) => { - cleanUpChannel(req.body); - db['channels'].save(req.body) - channelCache.clear(); - let channels = db['channels'].find() - channels.sort((a, b) => { return a.number < b.number ? -1 : 1 }) - res.send(channels) - updateXmltv() - + router.get('/api/channel/:number', async (req, res) => { + let number = parseInt(req.params.number, 10); + let channel = await channelCache.getChannelConfig(channelDB, number); + if (channel.length == 1) { + channel = channel[0]; + res.send( channel ); + } else { + return res.status(404).send("Channel not found"); + } }) - router.put('/api/channels', (req, res) => { - cleanUpChannel(req.body); - db['channels'].update({ _id: req.body._id }, req.body) - channelCache.clear(); - let channels = db['channels'].find() - channels.sort((a, b) => { return a.number < b.number ? -1 : 1 }) + router.get('/api/channel/description/:number', async (req, res) => { + let number = parseInt(req.params.number, 10); + let channel = await channelCache.getChannelConfig(channelDB, number); + if (channel.length == 1) { + channel = channel[0]; + res.send( { + number: channel.number, + icon: channel.icon, + name: channel.name, + }); + } else { + return res.status(404).send("Channel not found"); + } + }) + router.get('/api/channelNumbers', async (req, res) => { + let channels = await channelDB.getAllChannelNumbers(); + channels.sort( (a,b) => { return parseInt(a) - parseInt(b) } ); res.send(channels) + }) + router.post('/api/channel', async (req, res) => { + cleanUpChannel(req.body); + await channelDB.saveChannel( req.body.number, req.body ); + channelCache.clear(); + res.send( { number: req.body.number} ) updateXmltv() }) - router.delete('/api/channels', (req, res) => { - db['channels'].remove({ _id: req.body._id }, false) + router.put('/api/channel', async (req, res) => { + cleanUpChannel(req.body); + await channelDB.saveChannel( req.body.number, req.body ); channelCache.clear(); - let channels = db['channels'].find() - channels.sort((a, b) => { return a.number < b.number ? -1 : 1 }) - res.send(channels) + res.send( { number: req.body.number} ) + updateXmltv() + }) + router.delete('/api/channel', async (req, res) => { + await channelDB.deleteChannel( req.body.number ); + channelCache.clear(); + res.send( { number: req.body.number} ) updateXmltv() }) @@ -180,9 +258,9 @@ function api(db, xmltvInterval) { }) // CHANNELS.M3U Download - router.get('/api/channels.m3u', (req, res) => { + router.get('/api/channels.m3u', async (req, res) => { res.type('text') - let channels = db['channels'].find() + let channels = await channelDB.getAllChannels(); channels.sort((a, b) => { return a.number < b.number ? -1 : 1 }) var data = "#EXTM3U\n" for (var i = 0; i < channels.length; i++) { @@ -196,19 +274,36 @@ function api(db, xmltvInterval) { res.send(data) }) + // hls.m3u Download is not really working correctly right now + router.get('/api/hls.m3u', async (req, res) => { + res.type('text') + let channels = await channelDB.getAllChannels(); + channels.sort((a, b) => { return a.number < b.number ? -1 : 1 }) + var data = "#EXTM3U\n" + for (var i = 0; i < channels.length; i++) { + data += `#EXTINF:0 tvg-id="${channels[i].number}" tvg-chno="${channels[i].number}" tvg-name="${channels[i].name}" tvg-logo="${channels[i].icon}" group-title="dizqueTV",${channels[i].name}\n` + data += `${req.protocol}://${req.get('host')}/m3u8?channel=${channels[i].number}\n` + } + if (channels.length === 0) { + data += `#EXTINF:0 tvg-id="1" tvg-chno="1" tvg-name="dizqueTV" tvg-logo="https://raw.githubusercontent.com/vexorian/dizquetv/main/resources/dizquetv.png" group-title="dizqueTV",dizqueTV\n` + data += `${req.protocol}://${req.get('host')}/setup\n` + } + res.send(data) + }) + + + function updateXmltv() { xmltvInterval.updateXML() xmltvInterval.restartInterval() } function cleanUpProgram(program) { - if ( typeof(program.server) !== 'undefined') { - program.server = { - uri: program.server.uri, - accessToken: program.server.accessToken, - } - } + delete program.start + delete program.stop delete program.streams; + delete program.durationStr; + delete program.commercials; } function cleanUpChannel(channel) { diff --git a/src/channel-cache.js b/src/channel-cache.js index 4af4272..beacc58 100644 --- a/src/channel-cache.js +++ b/src/channel-cache.js @@ -4,16 +4,16 @@ let cache = {}; let programPlayTimeCache = {}; let configCache = {}; -function getChannelConfig(db, channelId) { +async function getChannelConfig(channelDB, channelId) { //with lazy-loading if ( typeof(configCache[channelId]) === 'undefined') { - let channel = db['channels'].find( { number: channelId } ) - configCache[channelId] = channel; - return channel; - } else { - return configCache[channelId]; + let channel = await channelDB.getChannel(channelId) + //console.log("channel=" + JSON.stringify(channel) ); + configCache[channelId] = [channel]; } + //console.log("channel=" + JSON.stringify(configCache[channelId]).slice(0,200) ); + return configCache[channelId]; } function saveChannelConfig(number, channel ) { @@ -27,7 +27,7 @@ function getCurrentLineupItem(channelId, t1) { let recorded = cache[channelId]; let lineupItem = JSON.parse( JSON.stringify(recorded.lineupItem) ); let diff = t1 - recorded.t0; - if ( (diff <= SLACK) && (lineupItem.actualDuration >= 2*SLACK) ) { + if ( (diff <= SLACK) && (lineupItem.duration >= 2*SLACK) ) { //closed the stream and opened it again let's not lose seconds for //no reason return lineupItem; @@ -40,7 +40,7 @@ function getCurrentLineupItem(channelId, t1) { return null; } } - if(lineupItem.start + SLACK > lineupItem.actualDuration) { + if(lineupItem.start + SLACK > lineupItem.duration) { return null; } return lineupItem; @@ -48,9 +48,9 @@ function getCurrentLineupItem(channelId, t1) { function getKey(channelId, program) { let serverKey = "!unknown!"; - if (typeof(program.server) !== 'undefined') { - if (typeof(program.server.name) !== 'undefined') { - serverKey = "plex|" + program.server.name; + if (typeof(program.serverKey) !== 'undefined') { + if (typeof(program.serverKey) !== 'undefined') { + serverKey = "plex|" + program.serverKey; } } let programKey = "!unknownProgram!"; @@ -67,7 +67,7 @@ function recordProgramPlayTime(channelId, lineupItem, t0) { if ( typeof(lineupItem.streamDuration) !== 'undefined') { remaining = lineupItem.streamDuration; } else { - remaining = lineupItem.actualDuration - lineupItem.start; + remaining = lineupItem.duration - lineupItem.start; } programPlayTimeCache[ getKey(channelId, lineupItem) ] = t0 + remaining; } @@ -103,4 +103,4 @@ module.exports = { getProgramLastPlayTime: getProgramLastPlayTime, getChannelConfig: getChannelConfig, saveChannelConfig: saveChannelConfig, -} \ No newline at end of file +} diff --git a/src/dao/channel-db.js b/src/dao/channel-db.js new file mode 100644 index 0000000..5684d05 --- /dev/null +++ b/src/dao/channel-db.js @@ -0,0 +1,103 @@ +const path = require('path'); +var fs = require('fs'); + +class ChannelDB { + + constructor(folder) { + this.folder = folder; + } + + async getChannel(number) { + let f = path.join(this.folder, `${number}.json` ); + return await new Promise( (resolve, reject) => { + fs.readFile(f, (err, data) => { + if (err) { + return reject(err); + } + try { + resolve( JSON.parse(data) ) + } catch (err) { + reject(err); + } + }) + }); + } + + async saveChannel(number, json) { + if (typeof(number) === 'undefined') { + throw Error("Mising channel number"); + } + let f = path.join(this.folder, `${number}.json` ); + return await new Promise( (resolve, reject) => { + let data = undefined; + try { + data = JSON.stringify(json); + } catch (err) { + return reject(err); + } + fs.writeFile(f, data, (err) => { + if (err) { + return reject(err); + } + resolve(); + }); + }); + } + + saveChannelSync(number, json) { + json.number = number; + let data = JSON.stringify(json); + let f = path.join(this.folder, `${number}.json` ); + fs.writeFileSync( f, data ); + } + + async deleteChannel(number) { + let f = path.join(this.folder, `${number}.json` ); + await new Promise( (resolve, reject) => { + fs.unlink(f, function (err) { + if (err) { + return reject(err); + } + resolve(); + }); + }); + } + + async getAllChannelNumbers() { + return await new Promise( (resolve, reject) => { + fs.readdir(this.folder, function(err, items) { + if (err) { + return reject(err); + } + let channelNumbers = []; + for (let i = 0; i < items.length; i++) { + let name = path.basename( items[i] ); + if (path.extname(name) === '.json') { + let numberStr = name.slice(0, -5); + if (!isNaN(numberStr)) { + channelNumbers.push( parseInt(numberStr) ); + } + } + } + resolve (channelNumbers); + }); + }); + } + + async getAllChannels() { + let numbers = await this.getAllChannelNumbers(); + return await Promise.all( numbers.map( async (c) => this.getChannel(c) ) ); + } + +} + + + + + + + + + + +module.exports = ChannelDB; \ No newline at end of file diff --git a/src/dao/plex-server-db.js b/src/dao/plex-server-db.js new file mode 100644 index 0000000..ebbf09e --- /dev/null +++ b/src/dao/plex-server-db.js @@ -0,0 +1,142 @@ + +//hmnn this is more of a "PlexServerService"... +class PlexServerDB +{ + constructor(channelDB, channelCache, db) { + this.channelDB = channelDB; + this.db = db; + this.channelCache = channelCache; + } + + async deleteServer(name) { + let channelNumbers = await this.channelDB.getAllChannelNumbers(); + let report = await Promise.all( channelNumbers.map( async (i) => { + let channel = await this.channelDB.getChannel(i); + let channelReport = { + channelNumber : channel.number, + channelName : channel.name, + destroyedPrograms: 0, + }; + this.fixupProgramArray(channel.programs, name, channelReport); + this.fixupProgramArray(channel.fillerContent, name, channelReport); + this.fixupProgramArray(channel.fallback, name, channelReport); + if (typeof(channel.fillerContent) !== 'undefined') { + channel.fillerContent = channel.fillerContent.filter( + (p) => { + return (true !== p.isOffline); + } + ); + } + if ( + (typeof(channel.fallback) !=='undefined') + && (channel.fallback.length > 0) + && (channel.fallback[0].isOffline) + ) { + channel.fallback = []; + if (channel.offlineMode != "pic") { + channel.offlineMode = "pic"; + channel.offlinePicture = `http://localhost:${process.env.PORT}/images/generic-offline-screen.png`; + } + } + this.fixupProgramArray(channel.fallback, name, channelReport); + await this.channelDB.saveChannel(i, channel); + this.db['plex-servers'].remove( { name: name } ); + return channelReport; + }) ); + this.channelCache.clear(); + return report; + } + + doesNameExist(name) { + return this.db['plex-servers'].find( { name: name} ).length > 0; + } + + async updateServer(server) { + let name = server.name; + if (typeof(name) === 'undefined') { + throw Error("Missing server name from request"); + } + let s = this.db['plex-servers'].find( { name: name} ); + if (s.length != 1) { + throw Error("Server doesn't exist."); + } + s = s[0]; + let arGuide = server.arGuide; + if (typeof(arGuide) === 'undefined') { + arGuide = true; + } + let arChannels = server.arGuide; + if (typeof(arChannels) === 'undefined') { + arChannels = false; + } + let newServer = { + name: s.name, + uri: server.uri, + accessToken: server.accessToken, + arGuide: arGuide, + arChannels: arChannels, + index: s.index, + } + + this.db['plex-servers'].update( + { _id: s._id }, + newServer + ); + + + } + + async addServer(server) { + let name = server.name; + if (typeof(name) === 'undefined') { + name = "plex"; + } + let i = 2; + let prefix = name; + let resultName = name; + while (this.doesNameExist(resultName)) { + resultName = `${prefix}${i}` ; + i += 1; + } + name = resultName; + let arGuide = server.arGuide; + if (typeof(arGuide) === 'undefined') { + arGuide = true; + } + let arChannels = server.arGuide; + if (typeof(arChannels) === 'undefined') { + arChannels = false; + } + let index = this.db['plex-servers'].find({}).length; + + let newServer = { + name: name, + uri: server.uri, + accessToken: server.accessToken, + arGuide: arGuide, + arChannels: arChannels, + index: index, + }; + this.db['plex-servers'].save(newServer); + } + + fixupProgramArray(arr, serverName, channelReport) { + if (typeof(arr) !== 'undefined') { + for(let i = 0; i < arr.length; i++) { + arr[i] = this.fixupProgram( arr[i], serverName, channelReport ); + } + } + } + fixupProgram(program, serverName, channelReport) { + if (program.serverKey === serverName) { + channelReport.destroyedPrograms += 1; + return { + isOffline: true, + duration: program.duration, + } + } + return program; + } +} + +module.exports = PlexServerDB \ No newline at end of file diff --git a/src/database-migration.js b/src/database-migration.js index e17ad4d..d6d2ced 100644 --- a/src/database-migration.js +++ b/src/database-migration.js @@ -17,7 +17,7 @@ * but with time it will be worth it, really. * ***/ - const TARGET_VERSION = 400; + const TARGET_VERSION = 500; const STEPS = [ // [v, v2, x] : if the current version is v, call x(db), and version becomes v2 @@ -25,6 +25,7 @@ const STEPS = [ [ 100, 200, (db) => commercialsRemover(db) ], [ 200, 300, (db) => appNameChange(db) ], [ 300, 400, (db) => createDeviceId(db) ], + [ 400, 500, (db,channels) => splitServersSingleChannels(db, channels) ], ] const { v4: uuidv4 } = require('uuid'); @@ -323,7 +324,10 @@ function commercialsRemover(db) { } -function initDB(db) { +function initDB(db, channelDB ) { + if (typeof(channelDB) === 'undefined') { + throw Error("???"); + } let dbVersion = db['db-version'].find()[0]; if (typeof(dbVersion) === 'undefined') { dbVersion = { 'version': 0 }; @@ -335,7 +339,7 @@ function initDB(db) { ran = true; console.log("Migrating from db version " + dbVersion.version + " to: " + STEPS[i][1] + "..."); try { - STEPS[i][2](db); + STEPS[i][2](db, channelDB); if (typeof(dbVersion._id) === 'undefined') { db['db-version'].save( {'version': STEPS[i][1] } ); } else { @@ -441,6 +445,139 @@ function repairFFmpeg0(existingConfigs) { }; } +function splitServersSingleChannels(db, channelDB ) { + console.log("Migrating channels and plex servers so that plex servers are no longer embedded in program data"); + let servers = db['plex-servers'].find(); + let serverCache = {}; + let serverNames = {}; + let newServers = []; + + let getServerKey = (uri, accessToken) => { + return uri + "|" + accessToken; + } + + let getNewName = (name) => { + if ( (typeof(name) === 'undefined') || (typeof(serverNames[name])!=='undefined') ) { + //recurse because what if some genius actually named their server plex#3 ? + name = getNewName("plex#" + (Object.keys(serverNames).length + 1)); + } + serverNames[name] = true; + return name; + } + + let saveServer = (name, uri, accessToken, arGuide, arChannels) => { + if (typeof(arGuide) === 'undefined') { + arGuide = true; + } + if (typeof(arChannels) === 'undefined') { + arChannels = false; + } + if (uri.endsWith("/")) { + uri = uri.slice(0,-1); + } + let key = getServerKey(uri, accessToken); + if (typeof(serverCache[key]) === 'undefined') { + serverCache[key] = getNewName(name); + console.log(`for key=${key} found server with name=${serverCache[key]}, uri=${uri}, accessToken=${accessToken}` ); + newServers.push({ + name: serverCache[key], + uri: uri, + accessToken: accessToken, + index: newServers.length, + arChannels : arChannels, + arGuide: arGuide, + }); + } + return serverCache[key]; + } + for (let i = 0; i < servers.length; i++) { + let server = servers[i]; + saveServer( server.name, server.uri, server.accessToken, server.arGuide, server.arChannels); + } + + let cleanupProgram = (program) => { + delete program.actualDuration; + delete program.commercials; + delete program.durationStr; + delete program.start; + delete program.stop; + } + + let fixProgram = (program) => { + //Also remove the "actualDuration" and "commercials" fields. + try { + cleanupProgram(program); + if (program.isOffline) { + return program; + } + let newProgram = JSON.parse( JSON.stringify(program) ); + let s = newProgram.server; + delete newProgram.server; + let name = saveServer( undefined, s.uri, s.accessToken, undefined, undefined); + if (typeof(name) === "undefined") { + throw Error("Unable to find server name"); + } + //console.log(newProgram.title + " : " + name); + newProgram.serverKey = name; + return newProgram; + } catch (err) { + console.error("Unable to migrate program. Replacing it with flex"); + return { + isOffline: true, + duration : program.duration, + }; + } + } + + let fixChannel = (channel) => { + console.log("Migrating channel: " + channel.name + " " + channel.number); + for (let i = 0; i < channel.programs.length; i++) { + channel.programs[i] = fixProgram( channel.programs[i] ); + } + //if (channel.programs.length > 10) { + //channel.programs = channel.programs.slice(0, 10); + //} + channel.duration = 0; + for (let i = 0; i < channel.programs.length; i++) { + channel.duration += channel.programs[i].duration; + } + if ( typeof(channel.fallback) === 'undefined') { + channel.fallback = []; + } + for (let i = 0; i < channel.fallback.length; i++) { + channel.fallback[i] = fixProgram( channel.fallback[i] ); + } + if ( typeof(channel.fillerContent) === 'undefined') { + channel.fillerContent = []; + } + for (let i = 0; i < channel.fillerContent.length; i++) { + channel.fillerContent[i] = fixProgram( channel.fillerContent[i] ); + } + return channel; + } + + let channels = db['channels'].find(); + for (let i = 0; i < channels.length; i++) { + channels[i] = fixChannel(channels[i]); + } + + console.log("Done migrating channels for this step. Saving updates to storage..."); + + //wipe out servers + for (let i = 0; i < servers.length; i++) { + db['plex-servers'].remove( { _id: servers[i]._id } ); + } + //wipe out old channels + db['channels'].remove(); + // insert all over again + db['plex-servers'].save( newServers ); + for (let i = 0; i < channels.length; i++) { + channelDB.saveChannelSync( channels[i].number, channels[i] ); + } + console.log("Done migrating channels for this step..."); + +} + module.exports = { initDB: initDB, diff --git a/src/ffmpeg.js b/src/ffmpeg.js index de332c3..73316e0 100644 --- a/src/ffmpeg.js +++ b/src/ffmpeg.js @@ -214,12 +214,18 @@ class FFMPEG extends events.EventEmitter { } // Resolution fix: Add scale filter, current stream becomes [siz] + let beforeSizeChange = currentVideo; if (this.ensureResolution && (iW != this.wantedW || iH != this.wantedH) ) { //Maybe the scaling algorithm could be configurable. bicubic seems good though videoComplex += `;${currentVideo}scale=${this.wantedW}:${this.wantedH}:flags=bicubic:force_original_aspect_ratio=decrease,pad=${this.wantedW}:${this.wantedH}:(ow-iw)/2:(oh-ih)/2[siz]` currentVideo = "[siz]"; iW = this.wantedW; iH = this.wantedH; + } else if ( isLargerResolution(iW, iH, this.wantedW, this.wantedH) ) { + videoComplex += `;${currentVideo}scale=${this.wantedW}:${this.wantedH}:flags=bicubic:force_original_aspect_ratio=decrease,pad=${this.wantedW}:${this.wantedH}:(ow-iw)/2:(oh-ih)/2[minsiz]` + currentVideo = "[minsiz]"; + iW = this.wantedW; + iH = this.wantedH; } // Channel overlay: @@ -255,6 +261,11 @@ class FFMPEG extends events.EventEmitter { var transcodeVideo = (this.opts.normalizeVideoCodec && isDifferentVideoCodec( streamStats.videoCodec, this.opts.videoEncoder) ); var transcodeAudio = (this.opts.normalizeAudioCodec && isDifferentAudioCodec( streamStats.audioCodec, this.opts.audioEncoder) ); var filterComplex = ''; + if ( (!transcodeVideo) && (currentVideo == '[minsiz]') ) { + //do not change resolution if no other transcoding will be done + // and resolution normalization is off + currentVideo = beforeSizeChange; + } if (currentVideo != '[video]') { transcodeVideo = true; //this is useful so that it adds some lines below filterComplex += videoComplex; @@ -393,10 +404,17 @@ function isDifferentAudioCodec(codec, encoder) { return true; } +function isLargerResolution(w1,h1, w2,h2) { + return (w1 > w2) || (h1 > h2); +} + function parseResolutionString(s) { var i = s.indexOf('x'); if (i == -1) { - return {w:1920, h:1080} + i = s.indexOf("×"); + if (i == -1) { + return {w:1920, h:1080} + } } return { w: parseInt( s.substring(0,i) , 10 ), diff --git a/src/hdhr.js b/src/hdhr.js index 8f3d775..52a5a3a 100644 --- a/src/hdhr.js +++ b/src/hdhr.js @@ -3,7 +3,7 @@ const SSDP = require('node-ssdp').Server module.exports = hdhr -function hdhr(db) { +function hdhr(db, channelDB) { const server = new SSDP({ location: { @@ -43,10 +43,10 @@ function hdhr(db) { } res.send(JSON.stringify(data)) }) - router.get('/lineup.json', (req, res) => { + router.get('/lineup.json', async (req, res) => { res.header("Content-Type", "application/json") var lineup = [] - var channels = db['channels'].find() + var channels = await channelDB.getAllChannels(); for (let i = 0, l = channels.length; i < l; i++) lineup.push({ GuideNumber: channels[i].number.toString(), GuideName: channels[i].name, URL: `${req.protocol}://${req.get('host')}/video?channel=${channels[i].number}` }) if (lineup.length === 0) diff --git a/src/helperFuncs.js b/src/helperFuncs.js index 7c00711..c8259f9 100644 --- a/src/helperFuncs.js +++ b/src/helperFuncs.js @@ -44,7 +44,7 @@ function createLineup(obj, channel, isFirst) { if (activeProgram.isOffline === true) { //offline case - let remaining = activeProgram.actualDuration - timeElapsed; + let remaining = activeProgram.duration - timeElapsed; //look for a random filler to play let filler = null; let special = null; @@ -66,16 +66,16 @@ function createLineup(obj, channel, isFirst) { if (filler != null) { let fillerstart = 0; if (isSpecial) { - if (filler.actualDuration > remaining) { - fillerstart = filler.actualDuration - remaining; + if (filler.duration > remaining) { + fillerstart = filler.duration - remaining; } else { ffillerstart = 0; } } else if(isFirst) { - fillerstart = Math.max(0, filler.actualDuration - remaining); + fillerstart = Math.max(0, filler.duration - remaining); //it's boring and odd to tune into a channel and it's always //the start of a commercial. - let more = Math.max(0, filler.actualDuration - fillerstart - 15000 - SLACK); + let more = Math.max(0, filler.duration - fillerstart - 15000 - SLACK); fillerstart += Math.floor(more * Math.random() ); } lineup.push({ // just add the video, starting at 0, playing the entire duration @@ -86,9 +86,9 @@ function createLineup(obj, channel, isFirst) { file: filler.file, ratingKey: filler.ratingKey, start: fillerstart, - streamDuration: Math.max(1, Math.min(filler.actualDuration - fillerstart, remaining) ), - duration: filler.actualDuration, - server: filler.server + streamDuration: Math.max(1, Math.min(filler.duration - fillerstart, remaining) ), + duration: filler.duration, + serverKey: filler.serverKey }); return lineup; } @@ -118,9 +118,9 @@ function createLineup(obj, channel, isFirst) { file: activeProgram.file, ratingKey: activeProgram.ratingKey, start: timeElapsed, - streamDuration: activeProgram.actualDuration - timeElapsed, - duration: activeProgram.actualDuration, - server: activeProgram.server + streamDuration: activeProgram.duration - timeElapsed, + duration: activeProgram.duration, + serverKey: activeProgram.serverKey } ]; } @@ -138,21 +138,21 @@ function pickRandomWithMaxDuration(channel, list, maxDuration) { for (let i = 0; i < list.length; i++) { let clip = list[i]; // a few extra milliseconds won't hurt anyone, would it? dun dun dun - if (clip.actualDuration <= maxDuration + SLACK ) { + if (clip.duration <= maxDuration + SLACK ) { let t1 = channelCache.getProgramLastPlayTime( channel.number, clip ); let timeSince = ( (t1 == 0) ? D : (t0 - t1) ); if (timeSince < channel.fillerRepeatCooldown - SLACK) { let w = channel.fillerRepeatCooldown - timeSince; - if (clip.actualDuration + w <= maxDuration + SLACK) { + if (clip.duration + w <= maxDuration + SLACK) { minimumWait = Math.min(minimumWait, w); } timeSince = 0; //30 minutes is too little, don't repeat it at all } if (timeSince >= D) { - let w = Math.pow(clip.actualDuration, 1.0 / 4.0); + let w = Math.pow(clip.duration, 1.0 / 4.0); n += w; if ( n*Math.random() < w) { pick1 = clip; diff --git a/src/plex-player.js b/src/plex-player.js index 9ceb4ea..1fb2bf3 100644 --- a/src/plex-player.js +++ b/src/plex-player.js @@ -46,10 +46,15 @@ class PlexPlayer { let ffmpegSettings = this.context.ffmpegSettings; let db = this.context.db; let channel = this.context.channel; + let server = db['plex-servers'].find( { 'name': lineupItem.serverKey } ); + if (server.length == 0) { + throw Error(`Unable to find server "${lineupItem.serverKey}" specied by program.`); + } + server = server[0]; try { let plexSettings = db['plex-settings'].find()[0]; - let plexTranscoder = new PlexTranscoder(this.clientId, plexSettings, channel, lineupItem); + let plexTranscoder = new PlexTranscoder(this.clientId, server, plexSettings, channel, lineupItem); this.plexTranscoder = plexTranscoder; let enableChannelIcon = this.context.enableChannelIcon; let ffmpeg = new FFMPEG(ffmpegSettings, channel); // Set the transcoder options diff --git a/src/plex.js b/src/plex.js index 0364b7b..4cb7672 100644 --- a/src/plex.js +++ b/src/plex.js @@ -113,6 +113,15 @@ class Plex { }) }) } + async checkServerStatus() { + try { + await this.Get('/'); + return 1; + } catch (err) { + console.error("Error getting Plex server status", err); + return -1; + } + } async GetDVRS() { var result = await this.Get('/livetv/dvrs') var dvrs = result.Dvr diff --git a/src/plexTranscoder.js b/src/plexTranscoder.js index 1fb8dd8..b0f3788 100644 --- a/src/plexTranscoder.js +++ b/src/plexTranscoder.js @@ -2,7 +2,7 @@ const { v4: uuidv4 } = require('uuid'); const axios = require('axios'); class PlexTranscoder { - constructor(clientId, settings, channel, lineupItem) { + constructor(clientId, server, settings, channel, lineupItem) { this.session = uuidv4() this.device = "channel-" + channel.number; @@ -16,16 +16,16 @@ class PlexTranscoder { this.log("Debug logging enabled") this.key = lineupItem.key - this.plexFile = `${lineupItem.server.uri}${lineupItem.plexFile}?X-Plex-Token=${lineupItem.server.accessToken}` + this.plexFile = `${server.uri}${lineupItem.plexFile}?X-Plex-Token=${server.accessToken}` if (typeof(lineupItem.file)!=='undefined') { this.file = lineupItem.file.replace(settings.pathReplace, settings.pathReplaceWith) } - this.transcodeUrlBase = `${lineupItem.server.uri}/video/:/transcode/universal/start.m3u8?` + this.transcodeUrlBase = `${server.uri}/video/:/transcode/universal/start.m3u8?` this.ratingKey = lineupItem.ratingKey this.currTimeMs = lineupItem.start this.currTimeS = this.currTimeMs / 1000 this.duration = lineupItem.duration - this.server = lineupItem.server + this.server = server this.transcodingArgs = undefined this.decisionJson = undefined diff --git a/src/video.js b/src/video.js index 5ffd2ae..67e51e9 100644 --- a/src/video.js +++ b/src/video.js @@ -42,14 +42,14 @@ function video(db) { }) }) // Continuously stream video to client. Leverage ffmpeg concat for piecing together videos - router.get('/video', (req, res) => { + router.get('/video', async (req, res) => { // Check if channel queried is valid if (typeof req.query.channel === 'undefined') { res.status(500).send("No Channel Specified") return } let number = parseInt(req.query.channel, 10); - let channel = channelCache.getChannelConfig(db, number); + let channel = await channelCache.getChannelConfig(db, number); if (channel.length === 0) { res.status(500).send("Channel doesn't exist") return @@ -118,7 +118,7 @@ function video(db) { } let m3u8 = (req.query.m3u8 === '1'); let number = parseInt(req.query.channel); - let channel = channelCache.getChannelConfig(db, number); + let channel = await channelCache.getChannelConfig(db, number); if (channel.length === 0) { res.status(404).send("Channel doesn't exist") @@ -167,7 +167,6 @@ function video(db) { //filler to play (if any) let t = 365*24*60*60*1000; prog.program = { - actualDuration: t, duration: t, isOffline : true, }; @@ -257,8 +256,9 @@ function video(db) { }); - router.get('/m3u8', (req, res) => { - res.type('text') + router.get('/m3u8', async (req, res) => { + res.type('application/vnd.apple.mpegurl') + // Check if channel queried is valid if (typeof req.query.channel === 'undefined') { @@ -267,7 +267,7 @@ function video(db) { } let channelNum = parseInt(req.query.channel, 10) - let channel = channelCache.getChannelConfig(db, channelNum ); + let channel = await channelCache.getChannelConfig(db, channelNum ); if (channel.length === 0) { res.status(500).send("Channel doesn't exist") return @@ -279,19 +279,30 @@ function video(db) { var data = "#EXTM3U\n" + data += `#EXT-X-VERSION:3 + #EXT-X-MEDIA-SEQUENCE:0 + #EXT-X-ALLOW-CACHE:YES + #EXT-X-TARGETDURATION:60 + #EXT-X-PLAYLIST-TYPE:VOD\n`; + let ffmpegSettings = db['ffmpeg-settings'].find()[0] + cur ="59.0"; + if ( ffmpegSettings.enableFFMPEGTranscoding === true) { + data += `#EXTINF:${cur},\n`; data += `${req.protocol}://${req.get('host')}/stream?channel=${channelNum}&first=0&m3u8=1\n`; } + data += `#EXTINF:${cur},\n`; data += `${req.protocol}://${req.get('host')}/stream?channel=${channelNum}&first=1&m3u8=1\n` for (var i = 0; i < maxStreamsToPlayInARow - 1; i++) { + data += `#EXTINF:${cur},\n`; data += `${req.protocol}://${req.get('host')}/stream?channel=${channelNum}&m3u8=1\n` } res.send(data) }) - router.get('/playlist', (req, res) => { + router.get('/playlist', async (req, res) => { res.type('text') // Check if channel queried is valid @@ -301,7 +312,7 @@ function video(db) { } let channelNum = parseInt(req.query.channel, 10) - let channel = channelCache.getChannelConfig(db, channelNum ); + let channel = await channelCache.getChannelConfig(db, channelNum ); if (channel.length === 0) { res.status(500).send("Channel doesn't exist") return diff --git a/web/app.js b/web/app.js index 564d820..131c284 100644 --- a/web/app.js +++ b/web/app.js @@ -17,6 +17,7 @@ app.directive('plexLibrary', require('./directives/plex-library')) app.directive('programConfig', require('./directives/program-config')) app.directive('offlineConfig', require('./directives/offline-config')) app.directive('frequencyTweak', require('./directives/frequency-tweak')) +app.directive('plexServerEdit', require('./directives/plex-server-edit')) app.directive('channelConfig', require('./directives/channel-config')) app.controller('settingsCtrl', require('./controllers/settings')) diff --git a/web/controllers/channels.js b/web/controllers/channels.js index 03dee69..7dc7421 100644 --- a/web/controllers/channels.js +++ b/web/controllers/channels.js @@ -4,40 +4,85 @@ module.exports = function ($scope, dizquetv) { $scope.selectedChannel = null $scope.selectedChannelIndex = -1 - dizquetv.getChannels().then((channels) => { - $scope.channels = channels - }) - $scope.removeChannel = (channel) => { - if (confirm("Are you sure to delete channel: " + channel.name + "?")) { - dizquetv.removeChannel(channel).then((channels) => { - $scope.channels = channels - }) + $scope.refreshChannels = async () => { + $scope.channels = [ { number: 1, pending: true} ] + let channelNumbers = await dizquetv.getChannelNumbers(); + $scope.channels = channelNumbers.map( (x) => { + return { + number: x, + pending: true, + } + }); + $scope.queryChannels(); + } + $scope.refreshChannels(); + + $scope.queryChannels = () => { + for (let i = 0; i < $scope.channels.length; i++) { + $scope.queryChannel(i, $scope.channels[i] ); } } - $scope.onChannelConfigDone = (channel) => { + + $scope.queryChannel = async (index, channel) => { + let ch = await dizquetv.getChannelDescription(channel.number); + ch.pending = false; + $scope.channels[index] = ch; + $scope.$apply(); + } + + $scope.removeChannel = async ($index, channel) => { + if (confirm("Are you sure to delete channel: " + channel.name + "?")) { + $scope.channels[$index].pending = true; + await dizquetv.removeChannel(channel); + $scope.refreshChannels(); + } + } + $scope.onChannelConfigDone = async (channel) => { + $scope.showChannelConfig = false + if ($scope.selectedChannelIndex != -1) { + $scope.channels[ $scope.selectedChannelIndex ].pending = false; + } if (typeof channel !== 'undefined') { if ($scope.selectedChannelIndex == -1) { // add new channel - dizquetv.addChannel(channel).then((channels) => { - $scope.channels = channels - }) + await dizquetv.addChannel(channel); + $scope.refreshChannels(); + + } else if ( + (typeof($scope.originalChannelNumber) !== 'undefined') + && ($scope.originalChannelNumber != channel.number) + ) { + //update + change channel number. + $scope.channels[ $scope.selectedChannelIndex ].pending = true; + await dizquetv.updateChannel(channel), + await dizquetv.removeChannel( { number: $scope.originalChannelNumber } ) + $scope.$apply(); + $scope.refreshChannels(); } else { // update existing channel - dizquetv.updateChannel(channel).then((channels) => { - $scope.channels = channels - }) + $scope.channels[ $scope.selectedChannelIndex ].pending = true; + await dizquetv.updateChannel(channel); + $scope.$apply(); + $scope.refreshChannels(); } } - $scope.showChannelConfig = false + + } - $scope.selectChannel = (index) => { - if (index === -1) { + $scope.selectChannel = async (index) => { + if ( (index === -1) || $scope.channels[index].pending ) { + $scope.originalChannelNumber = undefined; $scope.selectedChannel = null $scope.selectedChannelIndex = -1 + $scope.showChannelConfig = true } else { - let newObj = JSON.parse(angular.toJson($scope.channels[index])) + $scope.channels[index].pending = true; + let ch = await dizquetv.getChannel($scope.channels[index].number); + let newObj = ch; newObj.startTime = new Date(newObj.startTime) + $scope.originalChannelNumber = newObj.number; $scope.selectedChannel = newObj $scope.selectedChannelIndex = index + $scope.showChannelConfig = true + $scope.$apply(); } - $scope.showChannelConfig = true } } \ No newline at end of file diff --git a/web/controllers/version.js b/web/controllers/version.js index 8f0f35d..7fedd00 100644 --- a/web/controllers/version.js +++ b/web/controllers/version.js @@ -1,6 +1,6 @@ module.exports = function ($scope, dizquetv) { - $scope.version = "Getting dizqueTV version..." - $scope.ffmpegVersion = "Getting ffmpeg version..." + $scope.version = "" + $scope.ffmpegVersion = "" dizquetv.getVersion().then((version) => { $scope.version = version.dizquetv; $scope.ffmpegVersion = version.ffmpeg; diff --git a/web/directives/channel-config.js b/web/directives/channel-config.js index ca75418..d068789 100644 --- a/web/directives/channel-config.js +++ b/web/directives/channel-config.js @@ -124,7 +124,6 @@ module.exports = function ($timeout, $location) { let duration = program.durationSeconds * 1000; scope.updateChannelFromOfflineResult(program); editedProgram.duration = duration; - editedProgram.actualDuration = duration; editedProgram.isOffline = true; scope._selectedOffline = null updateChannelDuration() @@ -133,7 +132,6 @@ module.exports = function ($timeout, $location) { let duration = result.durationSeconds * 1000; let program = { duration: duration, - actualDuration: duration, isOffline: true } scope.updateChannelFromOfflineResult(result); @@ -329,7 +327,7 @@ module.exports = function ($timeout, $location) { background = "repeating-linear-gradient( " + angle + "deg, " + rgb1 + ", " + rgb1 + " " + w + "px, " + rgb2 + " " + w + "px, " + rgb2 + " " + (w*2) + "px)"; } - let ems = Math.pow( Math.min(24*60*60*1000, program.actualDuration), 0.7 ); + let ems = Math.pow( Math.min(24*60*60*1000, program.duration), 0.7 ); ems = ems / Math.pow(5*60*1000., 0.7); ems = Math.max( 0.25 , ems); let top = Math.max(0.0, (1.75 - ems) / 2.0) ; @@ -391,7 +389,6 @@ module.exports = function ($timeout, $location) { progs.push( { duration: d, - actualDuration: d, isOffline: true, } ) @@ -406,7 +403,6 @@ module.exports = function ($timeout, $location) { progs.push( { duration: d, - actualDuration: d, isOffline: true, } ) @@ -425,16 +421,15 @@ module.exports = function ($timeout, $location) { if (prog.isOffline) { tired = 0; } else { - if (tired + prog.actualDuration >= after) { + if (tired + prog.duration >= after) { tired = 0; let dur = 1000 * (minDur + Math.floor( (maxDur - minDur) * Math.random() ) ); progs.push( { isOffline : true, duration: dur, - actualDuration: dur, }); } - tired += prog.actualDuration; + tired += prog.duration; } if (i < l) { progs.push(prog); @@ -465,7 +460,6 @@ module.exports = function ($timeout, $location) { // not worth padding it progs.push( { duration : r, - actualDuration : r, isOffline : true, }); t += r; @@ -474,7 +468,7 @@ module.exports = function ($timeout, $location) { for (let i = 0, l = scope.channel.programs.length; i < l; i++) { let prog = scope.channel.programs[i]; progs.push(prog); - t += prog.actualDuration; + t += prog.duration; addPad(i == l - 1); } scope.channel.programs = progs; @@ -553,7 +547,7 @@ module.exports = function ($timeout, $location) { if ( typeof(programs[c]) === 'undefined') { programs[c] = 0; } - programs[c] += scope.channel.programs[i].actualDuration; + programs[c] += scope.channel.programs[i].duration; } } let mx = 0; @@ -659,7 +653,7 @@ module.exports = function ($timeout, $location) { episodes: [] } } - shows[code].total += vid.actualDuration; + shows[code].total += vid.duration; shows[code].episodes.push(vid); } let maxDuration = 0; diff --git a/web/directives/ffmpeg-settings.js b/web/directives/ffmpeg-settings.js index 5670c94..23dfdc4 100644 --- a/web/directives/ffmpeg-settings.js +++ b/web/directives/ffmpeg-settings.js @@ -29,7 +29,7 @@ {id:"420x420",description:"420x420 (1:1)"}, {id:"480x270",description:"480x270 (HD1080/16 16:9)"}, {id:"576x320",description:"576x320 (18:10)"}, - {id:"640×360",description:"640×360 (nHD 16:9)"}, + {id:"640x360",description:"640x360 (nHD 16:9)"}, {id:"720x480",description:"720x480 (WVGA 3:2)"}, {id:"800x600",description:"800x600 (SVGA 4:3)"}, {id:"1024x768",description:"1024x768 (WXGA 4:3)"}, diff --git a/web/directives/offline-config.js b/web/directives/offline-config.js index 7ba04eb..7b7ff73 100644 --- a/web/directives/offline-config.js +++ b/web/directives/offline-config.js @@ -34,14 +34,14 @@ module.exports = function ($timeout) { scope.program = null } scope.sortFillers = () => { - scope.program.filler.sort( (a,b) => { return a.actualDuration - b.actualDuration } ); + scope.program.filler.sort( (a,b) => { return a.duration - b.duration } ); } scope.fillerRemoveAllFiller = () => { scope.program.filler = []; } scope.fillerRemoveDuplicates = () => { function getKey(p) { - return p.server.uri + "|" + p.server.accessToken + "|" + p.plexFile; + return p.serverKey + "|" + p.plexFile; } let seen = {}; let newFiller = []; @@ -81,7 +81,7 @@ module.exports = function ($timeout) { scope.programSquareStyle = (program, dash) => { let background = "rgb(255, 255, 255)"; - let ems = Math.pow( Math.min(60*60*1000, program.actualDuration), 0.7 ); + let ems = Math.pow( Math.min(60*60*1000, program.duration), 0.7 ); ems = ems / Math.pow(1*60*1000., 0.7); ems = Math.max( 0.25 , ems); let top = Math.max(0.0, (1.75 - ems) / 2.0) ; diff --git a/web/directives/plex-library.js b/web/directives/plex-library.js index 46e00b8..149d04b 100644 --- a/web/directives/plex-library.js +++ b/web/directives/plex-library.js @@ -46,7 +46,8 @@ module.exports = function (plex, dizquetv, $timeout) { await scope.wait(0); scope.pending += 1; try { - item.streams = await plex.getStreams(scope.plexServer, item.key, scope.errors) + delete item.server; + item.serverKey = scope.plexServer.name; scope.selection.push(JSON.parse(angular.toJson(item))) } catch (err) { let msg = "Unable to add item: " + item.key + " " + item.title; diff --git a/web/directives/plex-server-edit.js b/web/directives/plex-server-edit.js new file mode 100644 index 0000000..c608d19 --- /dev/null +++ b/web/directives/plex-server-edit.js @@ -0,0 +1,72 @@ +module.exports = function (dizquetv, $timeout) { + return { + restrict: 'E', + templateUrl: 'templates/plex-server-edit.html', + replace: true, + scope: { + state: "=state", + _onFinish: "=onFinish", + }, + link: function (scope, element, attrs) { + scope.state.modified = false; + scope.setModified = () => { + scope.state.modified = true; + } + scope.onSave = async () => { + try { + await dizquetv.updatePlexServer(scope.state.server); + scope.state.modified = false; + scope.state.success = "The server was updated."; + scope.state.changesSaved = true; + scope.state.error = ""; + } catch (err) { + scope.state.error = "There was an error updating the server"; + scope.state.success = ""; + console.error(scope.state.error, err); + } + $timeout( () => { scope.$apply() } , 0 ); + } + + scope.onDelete = async () => { + try { + let channelReport = await dizquetv.removePlexServer(scope.state.server.name); + scope.state.channelReport = channelReport; + channelReport.sort( (a,b) => { + if (a.destroyedPrograms != b.destroyedPrograms) { + return (b.destroyedPrograms - a.destroyedPrograms); + } else { + return (a.channelNumber - b.channelNumber); + } + }); + scope.state.success = "The server was deleted."; + scope.state.error = ""; + scope.state.modified = false; + scope.state.changesSaved = true; + } catch (err) { + scope.state.error = "There was an error deleting the server."; + scope.state.success = ""; + } + $timeout( () => { scope.$apply() } , 0 ); + } + + scope.onShowDelete = async () => { + scope.state.showDelete = true; + scope.deleteTime = (new Date()).getTime(); + $timeout( () => { + if (scope.deleteTime + 29000 < (new Date()).getTime() ) { + scope.state.showDelete = false; + scope.$apply(); + } + }, 30000); + + } + + scope.onFinish = () => { + scope.state.visible = false; + if (scope.state.changesSaved) { + scope._onFinish(); + } + } + } + }; +} diff --git a/web/directives/plex-settings.js b/web/directives/plex-settings.js index 24785df..3b7abca 100644 --- a/web/directives/plex-settings.js +++ b/web/directives/plex-settings.js @@ -5,40 +5,195 @@ module.exports = function (plex, dizquetv, $timeout) { replace: true, scope: {}, link: function (scope, element, attrs) { - dizquetv.getPlexServers().then((servers) => { - scope.servers = servers - }) - scope.addPlexServer = function () { - scope.isProcessing = true - plex.login() - .then((result) => { - result.servers.forEach((server) => { - // add in additional settings - server.arGuide = true - server.arChannels = false // should not be enabled unless dizqueTV tuner already added to plex - dizquetv.addPlexServer(server) - }); - return dizquetv.getPlexServers() - }).then((servers) => { - scope.$apply(() => { - scope.servers = servers - scope.isProcessing = false - }) - }, (err) => { - scope.$apply(() => { - scope.isProcessing = false - scope.error = err - $timeout(() => { - scope.error = null - }, 3500) - }) - }) + scope.requestId = 0; + scope._serverToEdit = null; + scope._serverEditorState = { + visible:false, } - scope.deletePlexServer = (x) => { - dizquetv.removePlexServer(x) - .then((servers) => { - scope.servers = servers - }) + scope.serversPending = true; + scope.channelReport = null; + scope.serverError = ""; + scope.refreshServerList = async () => { + scope.serversPending = true; + let servers = await dizquetv.getPlexServers(); + scope.serversPending = false; + scope.servers = 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); + } + setTimeout( () => { scope.$apply() }, 31000 ); + scope.$apply(); + }; + scope.refreshServerList(); + + scope.editPlexServer = (server) => { + scope._serverEditorState = { + visible: true, + server: { + name: server.name, + uri: server.uri, + arGuide: server.arGuide, + arChannels: server.arChannels, + accessToken: server.accessToken, + }, + } + } + + scope.serverEditFinished = () => { + scope.refreshServerList(); + } + + scope.isAnyUIBad = () => { + let t = (new Date()).getTime(); + 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(); + for (let i = 0; i < scope.servers.length; i++) { + let s = scope.servers[i]; + if ( + (s.backendStatus == -1) + || ( (s.backendStatus == 0) && (s.backendPending + 30000 < t) ) + ) { + return true; + } + } + return false; + }; + + + 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) { + // avoid updating for a previous instance of the row + scope.servers[i].backendStatus = s.status; + } + scope.$apply(); + }; + + + scope.findGoodConnection = async (server, connections) => { + return await Promise.any(connections.map( async (connection) => { + let hypothethical = { + name: server.name, + accessToken : server.accessToken, + uri: connection.uri, + }; + + let q = await Promise.race([ + new Promise( (resolve, reject) => $timeout( () => {resolve(-1)}, 60000) ), + (async() => { + let s1 = await plex.check( hypothethical ); + let s2 = (await dizquetv.checkNewPlexServer(hypothethical)).status; + if (s1 == 1 && s2 == 1) { + return 1; + } else { + return -1; + } + })(), + ]); + if (q === 1) { + return hypothethical; + } else { + throw Error("Not proper status"); + } + }) ); + } + + scope.getLocalConnections = (connections) => { + let r = []; + for (let i = 0; i < connections.length; i++) { + if (connections[i].local === true) { + r.push( connections[i] ); + } + } + return r; + } + + scope.getRemoteConnections = (connections) => { + let r = []; + for (let i = 0; i < connections.length; i++) { + if (connections[i].local !== true) { + r.push( connections[i] ); + } + } + return r; + } + + + scope.addPlexServer = async () => { + scope.isProcessing = true; + scope.serversPending = true; + scope.serverError = ""; + let result = await plex.login(); + scope.addingServer = "Looking for servers in the Plex account, please wait..."; + await Promise.all( result.servers.map( async (server) => { + try { + let connections = scope.getLocalConnections(server.connections); + let connection = null; + try { + connection = await scope.findGoodConnection(server, connections); + } catch (err) { + connection = null; + } + if (connection == null) { + connections = scope.getRemoteConnections(server.connections); + try { + connection = await scope.findGoodConnection(server, connections); + } catch (err) { + connection = null; + } + } + if (connection == null) { + //pick a random one, really. + connections = scope.getLocalConnections(server.connections); + if (connections.length > 0) { + connection = connections[0]; + } else { + connection = server.connections[0]; + } + connection = { + name: server.name, + uri: connection.uri, + accessToken: server.accessToken, + } + } + connection.arGuide = true + connection.arChannels = false // should not be enabled unless dizqueTV tuner already added to plex + await dizquetv.addPlexServer(connection); + } catch (err) { + scope.serverError = "Could not add Plex server: There was an error."; + console.error("error adding server", err); + } + }) ); + scope.addingServer = ""; + scope.isProcessing = false; + scope.refreshServerList(); } dizquetv.getPlexSettings().then((settings) => { scope.settings = settings diff --git a/web/directives/program-config.js b/web/directives/program-config.js index 1194c8e..ce50987 100644 --- a/web/directives/program-config.js +++ b/web/directives/program-config.js @@ -37,7 +37,7 @@ module.exports = function ($timeout) { return } - prog.duration = prog.actualDuration + prog.duration = prog.duration for (let i = 0, l = prog.commercials.length; i < l; i++) prog.duration += prog.commercials[i].duration scope.onDone(JSON.parse(angular.toJson(prog))) diff --git a/web/public/templates/channel-config.html b/web/public/templates/channel-config.html index 1652b04..83cfd0c 100644 --- a/web/public/templates/channel-config.html +++ b/web/public/templates/channel-config.html @@ -303,7 +303,10 @@
Flex
- +
+
diff --git a/web/public/templates/ffmpeg-settings.html b/web/public/templates/ffmpeg-settings.html index 6e417d4..ea684ed 100644 --- a/web/public/templates/ffmpeg-settings.html +++ b/web/public/templates/ffmpeg-settings.html @@ -132,7 +132,7 @@
- Some clients experience issues when the video stream changes resolution. This option will make dizqueTV convert all videos to the preferred resolution selected above. + Some clients experience issues when the video stream changes resolution. This option will make dizqueTV convert all videos to the preferred resolution selected above. Otherwise, the preferred resolution will be used as a maximum resolution for transcoding.
diff --git a/web/public/templates/offline-config.html b/web/public/templates/offline-config.html index dce3945..57f6d3b 100644 --- a/web/public/templates/offline-config.html +++ b/web/public/templates/offline-config.html @@ -61,14 +61,16 @@
- {{durationString(x.actualDuration)}} + {{durationString(x.duration)}}
Fallback: {{x.title}}
- +
@@ -133,14 +135,16 @@
- {{durationString(x.actualDuration)}} + {{durationString(x.duration)}}
{{x.title}}
- +
diff --git a/web/public/templates/plex-library.html b/web/public/templates/plex-library.html index 5d0e7ae..b281e84 100644 --- a/web/public/templates/plex-library.html +++ b/web/public/templates/plex-library.html @@ -113,9 +113,9 @@
Select media items from your plex library above.
  • {{ (selection[selection.length + x].type !== 'episode') ? selection[selection.length + x].title : (selection[selection.length + x].showTitle + ' - S' + selection[selection.length + x].season.toString().padStart(2,'0') + 'E' + selection[selection.length + x].episode.toString().padStart(2,'0'))}} - - - +
  • diff --git a/web/public/templates/plex-server-edit.html b/web/public/templates/plex-server-edit.html new file mode 100644 index 0000000..9a0d7d7 --- /dev/null +++ b/web/public/templates/plex-server-edit.html @@ -0,0 +1,96 @@ +
    + +
    diff --git a/web/public/templates/plex-settings.html b/web/public/templates/plex-settings.html index 0345e10..d70b032 100644 --- a/web/public/templates/plex-settings.html +++ b/web/public/templates/plex-settings.html @@ -15,31 +15,53 @@ - - - + + + - - + + + + - - - + + + - + + + + + + + +
    NameAddressRefresh GuideRefresh ChannelsuriUI RouteBackend Route
    +

    Add a Plex Server

    {{ addingServer }}
    {{ x.name }}{{ x.uri }}{{ x.arGuide }}{{ x.arChannels }}{{ x.uri }} - +
    +
    ok
    +
    error
    +
    +
    -

    To further tweak server values you can edit plex-servers.json. Note that changing server address requires you to re-make your channels programming, or else they will still try to use the previous server.

    +

    {{serverError}}

    +

    If a Plex server configuration has problems with the UI route, the channel editor won't be able to access its content.

    +
    +

    If a Plex server configuration has problems with the backend route, dizqueTV won't be able to play its content.

    +

    @@ -174,3 +196,4 @@
    + \ No newline at end of file diff --git a/web/public/views/channels.html b/web/public/views/channels.html index a62ff05..f04fd3b 100644 --- a/web/public/views/channels.html +++ b/web/public/views/channels.html @@ -9,8 +9,8 @@ - - + + @@ -19,16 +19,19 @@

    No channels found. Click the to create a channel.

    - - + + diff --git a/web/public/views/version.html b/web/public/views/version.html index c0d2f0e..23ec21f 100644 --- a/web/public/views/version.html +++ b/web/public/views/version.html @@ -10,15 +10,15 @@ - + - + - +
    NumberIconNumberIcon Name
    {{x.number}}
    +
    + {{x.number}} +
    {{x.name}}
    {{x.name}}
    {{x.name}} -
    dizqueTV-backend{{version}}
    {{version}}
    dizqueTV-uiGetting dizqueTV UI version...
    FFMPEG{{ffmpegVersion}}
    {{ffmpegVersion}}
    diff --git a/web/services/dizquetv.js b/web/services/dizquetv.js index f2d80c0..4b49016 100644 --- a/web/services/dizquetv.js +++ b/web/services/dizquetv.js @@ -7,6 +7,14 @@ module.exports = function ($http) { return $http.get('/api/plex-servers').then((d) => { return d.data }) }, addPlexServer: (plexServer) => { + return $http({ + method: 'PUT', + url: '/api/plex-servers', + data: plexServer, + headers: { 'Content-Type': 'application/json; charset=utf-8' } + }).then((d) => { return d.data }) + }, + updatePlexServer: (plexServer) => { return $http({ method: 'POST', url: '/api/plex-servers', @@ -14,13 +22,32 @@ module.exports = function ($http) { headers: { 'Content-Type': 'application/json; charset=utf-8' } }).then((d) => { return d.data }) }, - removePlexServer: (serverId) => { - return $http({ + checkExistingPlexServer: async (serverName) => { + let d = await $http({ + method: 'POST', + url: '/api/plex-servers/status', + data: { name: serverName }, + headers: { 'Content-Type': 'application/json; charset=utf-8' } + }) + return d.data; + }, + checkNewPlexServer: async (server) => { + let d = await $http({ + method: 'POST', + url: '/api/plex-servers/foreignstatus', + data: server, + headers: { 'Content-Type': 'application/json; charset=utf-8' } + }) + return d.data; + }, + removePlexServer: async (serverName) => { + let d = await $http({ method: 'DELETE', url: '/api/plex-servers', - data: { _id: serverId }, + data: { name: serverName }, headers: { 'Content-Type': 'application/json; charset=utf-8' } - }).then((d) => { return d.data }) + }); + return d.data; }, getPlexSettings: () => { return $http.get('/api/plex-settings').then((d) => { return d.data }) @@ -101,10 +128,24 @@ module.exports = function ($http) { getChannels: () => { return $http.get('/api/channels').then((d) => { return d.data }) }, + + getChannel: (number) => { + return $http.get(`/api/channel/${number}`).then( (d) => { return d.data }) + }, + + getChannelDescription: (number) => { + return $http.get(`/api/channel/description/${number}`).then( (d) => { return d.data } ) + }, + + + getChannelNumbers: () => { + return $http.get('/api/channelNumbers').then( (d) => { return d.data } ) + }, + addChannel: (channel) => { return $http({ method: 'POST', - url: '/api/channels', + url: '/api/channel', data: angular.toJson(channel), headers: { 'Content-Type': 'application/json; charset=utf-8' } }).then((d) => { return d.data }) @@ -112,7 +153,7 @@ module.exports = function ($http) { updateChannel: (channel) => { return $http({ method: 'PUT', - url: '/api/channels', + url: '/api/channel', data: angular.toJson(channel), headers: { 'Content-Type': 'application/json; charset=utf-8' } }).then((d) => { return d.data }) @@ -120,7 +161,7 @@ module.exports = function ($http) { removeChannel: (channel) => { return $http({ method: 'DELETE', - url: '/api/channels', + url: '/api/channel', data: angular.toJson(channel), headers: { 'Content-Type': 'application/json; charset=utf-8' } }).then((d) => { return d.data }) diff --git a/web/services/plex.js b/web/services/plex.js index dafef50..b80fa0e 100644 --- a/web/services/plex.js +++ b/web/services/plex.js @@ -51,13 +51,6 @@ module.exports = function ($http, $window, $interval) { if (server.provides != `server`) return; - // true = local server, false = remote - const i = (server.publicAddressMatches == true) ? 0 : server.connections.length - 1 - server.uri = server.connections[i].uri - server.protocol = server.connections[i].protocol - server.address = server.connections[i].address - server.port = server.connections[i].port - res_servers.push(server); }); @@ -79,6 +72,18 @@ module.exports = function ($http, $window, $interval) { }) }) }, + + check: async(server) => { + let client = new Plex(server) + try { + const res = await client.Get('/') + return 1; + } catch (err) { + console.error(err); + return -1; + } + }, + getLibrary: async (server) => { var client = new Plex(server) const res = await client.Get('/library/sections') @@ -160,7 +165,6 @@ module.exports = function ($http, $window, $interval) { icon: `${server.uri}${res.Metadata[i].thumb}?X-Plex-Token=${server.accessToken}`, type: res.Metadata[i].type, duration: res.Metadata[i].duration, - actualDuration: res.Metadata[i].duration, durationStr: msToTime(res.Metadata[i].duration), subtitle: res.Metadata[i].subtitle, summary: res.Metadata[i].summary,