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 @@
| Name | -Address | -Refresh Guide | -Refresh Channels | +uri | +UI Route | +Backend 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. + |
+ |||||||||||
| Number | -Icon | +Number | +Icon | Name | |
|---|---|---|---|---|---|
| {{x.number}} | +|||||
| + + {{x.number}} + |
{{x.name}}
|
{{x.name}} | - | ||
| dizqueTV-backend | -{{version}} | +{{version}} | |||
| dizqueTV-ui | -Getting dizqueTV UI version... | +||||
| FFMPEG | -{{ffmpegVersion}} | +{{ffmpegVersion}} |