1 json per channel. Plex server editing and status. Max resolution for transcoding. 640x360 fix.

This commit is contained in:
vexorian 2020-08-22 09:45:47 -04:00
parent 55c22846bf
commit 3022dfe375
34 changed files with 1187 additions and 217 deletions

View File

@ -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)

View File

@ -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

View File

@ -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) {

View File

@ -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,
}
}

103
src/dao/channel-db.js Normal file
View File

@ -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;

142
src/dao/plex-server-db.js Normal file
View File

@ -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

View File

@ -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,

View File

@ -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 ),

View File

@ -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)

View File

@ -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;

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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'))

View File

@ -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
}
}

View File

@ -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;

View File

@ -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;

View File

@ -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)"},

View File

@ -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) ;

View File

@ -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;

View File

@ -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();
}
}
}
};
}

View File

@ -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

View File

@ -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)))

View File

@ -303,7 +303,10 @@
<div style="margin-right: 5px; font-weight:ligther" ng-show="x.isOffline">
<i>Flex</i>
</div>
<span class="flex-pull-right btn fa fa-trash" ng-click="removeItem($index); $event.stopPropagation()"></span>
<div class="flex-pull-right"></div>
<button class="btn btn-sm btn-link" ng-click="removeItem($index); $event.stopPropagation()">
<i class="text-danger fa fa-trash"></i>
</button>
</div>
</div>
<div ng-if="minProgramIndex &lt; channel.programs.length - 100" class="list-group-item flex-container" >

View File

@ -132,7 +132,7 @@
<div class="form-group">
<input id="enableNormalizeResolution" type="checkbox" ng-model="settings.normalizeResolution" ng-disabled="isTranscodingNotNeeded()" />
<label for="enableNormalizeResolution">Normalize Resolution</label>
<small class="form-text text-muted">Some clients experience issues when the video stream changes resolution. This option will make dizqueTV convert all videos to the preferred resolution selected above.
<small class="form-text text-muted">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.
</small>
</div>
</div>

View File

@ -61,14 +61,16 @@
<div class="list-group list-group-root" dnd-list="program.fallback">
<div class="list-group-item flex-container" style="cursor: default;" ng-repeat="x in program.fallback" dnd-draggable="x" dnd-moved="program.fallback.splice($index, 1)" dnd-effect-allowed="move">
<div class="program-start" >
{{durationString(x.actualDuration)}}
{{durationString(x.duration)}}
</div>
<div ng-style="programSquareStyle(x, true)" />
<div style="margin-right: 5px;">
<strong>Fallback:</strong> {{x.title}}
</div>
<div class="flex-pull-right">
<span class="btn fa fa-trash" ng-click="program.fallback.splice($index,1)"></span>
<button class="btn btn-sm btn-link" ng-click="program.fallback.splice($index,1)">
<i class="text-danger fa fa-trash" ></i>
</button>
</div>
</div>
</div>
@ -133,14 +135,16 @@
<div class="list-group list-group-root" dnd-list="program.filler">
<div class="list-group-item flex-container" style="cursor: default;" ng-repeat="x in program.filler" dnd-draggable="x" dnd-moved="program.filler.splice($index, 1)" dnd-effect-allowed="move">
<div class="program-start" >
{{durationString(x.actualDuration)}}
{{durationString(x.duration)}}
</div>
<div ng-style="programSquareStyle(x, false)" />
<div style="margin-right: 5px;">
{{x.title}}
</div>
<div class="flex-pull-right">
<span class="btn fa fa-trash" ng-click="program.filler.splice($index,1)"></span>
<button class="btn btn-sm btn-link" ng-click="program.filler.splice($index,1)">
<i class="text-danger fa fa-trash" ></i>
</button>
</div>
</div>
</div>

View File

@ -113,9 +113,9 @@
<div ng-if="selection.length === 0">Select media items from your plex library above.</div>
<li ng-if="selection.length + x &gt;= 0" class="list-group-item" ng-repeat="x in allowedIndexes" style="cursor:default;" dnd-draggable="x" dnd-moved="selection.splice(selection.length + x, 1)" dnd-effect-allowed="move">
{{ (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'))}}
<span class="pull-right">
<span class="btn fa fa-trash" ng-click="selection.splice(selection.length + x,1)"></span>
</span>
<button class="pull-right btn btn-sm btn-link" ng-click="selection.splice(selection.length + x,1)">
<span class="text-danger fa fa-trash" ></span>
</button>
</li>
</ul>
</div>

View File

@ -0,0 +1,96 @@
<div ng-show="state.visible">
<div class="modal" tabindex="-1" role="dialog" style="display: block; background-color: rgba(0, 0, 0, .5);">
<div class="modal-dialog modal-dialog-scrollable modal-xl" role="document">
<div class="modal-content">
<div>
<div class="modal-header">
<h5 class="modal-title">Plex Server</h5>
</div>
</div>
<div class="modal-body container" >
<div ng-if="state.channelReport == null" class="form-group">
<label for="serverName">Name:</label>
<input class="form-control" type="text" placeholder="{{state.server.name}}" readonly>
</div>
<div ng-if="state.channelReport == null" class="form-group">
<label for="uri">Server URI:</label>
<input type="text" class="form-control" id="uri" ng-model = "state.server.uri" ng-change="setModified()"></input>
</div>
<div ng-if="state.channelReport == null" class="form-group">
<label ng-if="!state.accessVisible" for="accessToken">User Access Token</label>
<label ng-if="state.accessVisible" for="accessToken2">User Access Token (Do not share this token with strangers)</label>
<div class="input-group">
<input ng-if="!state.accessVisible" type="password" class="form-control" id="accessToken" ng-model = "state.server.accessToken" ng-change="setModified()"></input>
<input ng-if="state.accessVisible" type="text" class="form-control" id="accessToken2" ng-model = "state.server.accessToken" ng-change="setModified()" aria-describedby="tokenHelp"></input>
<div class="input-group-append">
<button class="btn btn-secondary form-control form-control-sm" type="button" ng-click="state.accessVisible = ! state.accessVisible">
<i ng-hide='state.accessVisible' class='fa fa-eye'></i>
<i ng-show='state.accessVisible' class='fa fa-asterisk'></i>
</button>
</div>
</div>
</div>
<div ng-if="state.channelReport == null" class="form-check">
<input class="form-check-input" type="checkbox" value="" id="arGuide" ng-model="state.server.arGuide" ng-change="setModified()">
<label class="form-check-label" for="arGuide">
Send Guide Updates
</label>
</div>
<div ng-if="state.channelReport == null" class="form-check">
<input class="form-check-input" type="checkbox" value="" id="arChannels" ng-model="state.server.arChannels" ng-change="setModified()">
<label class="form-check-label" for="arChannels">
Send Channel Updates
</label>
</div>
<hr ng-if="state.channelReport == null" ng-hide="state.showDelete"></ht>
<div ng-if="state.channelReport == null" ng-hide="state.showDelete" class="form-group">
<button class="btn btn-link" ng-click="onShowDelete()">
<span class="text-danger"><i class="fa fa-trash"></i> Delete server...</span>
</button>
</div>
<div ng-if="state.channelReport == null" ng-show="state.showDelete" class="card" style="width: 100%;">
<div class="card-body">
<h5 class="card-title">
Delete Server
</h5>
<p class="card-text">If you delete a plex server, all the existing programs that reference to it will be
replaced with Flex time. Fillers that reference to the server will be removed. This operation cannot be undone.</p>
</div>
<button ng-if="state.channelReport == null" type="button" class="btn btn-sm btn-danger" ng-click="onDelete();" ><i class='fa fa-trash'></i> Delete</button>
</div>
<div ng-if="state.channelReport != null" class="card" style="width: 100%;">
<div class="card-body">
<h5 class="card-title">Server deleted</h5>
<table class='table'>
<tr ng-repeat="x in state.channelReport" ng-if="x.destroyedPrograms &gt; 0">
<td>{{x.channelNumber}}</td>
<td>{{x.channelName}}</td>
<td>{{x.destroyedPrograms}} program{{ (x.destroyedPrograms&gt;1?&quot;s&quot;:&quot;&quot;) }} removed.</td>
</tr>
</table>
</div>
</div>
</div>
<div class="modal-footer">
<div class='text-success small'>{{state.success}}</div>
<div class='text-danger small'>{{state.error}}</div>
<button type="button" class="btn btn-sm btn-link" ng-click="onFinish()">{{state.modified?"Cancel":"Close"}}</button>
<button type="button" class="btn btn-sm btn-primary" ng-click="onSave();" ng-show="state.modified" >Save</button>
</div>
</div>
</div>
</div>
</div>

View File

@ -15,31 +15,53 @@
<table class="table">
<tr>
<th>Name</th>
<th>Address</th>
<th>Refresh Guide</th>
<th>Refresh Channels</th>
<th>uri</th>
<th>UI Route</th>
<th>Backend Route</th>
<th></th>
</tr>
<tr ng-if="servers.length === 0">
<td colspan="5">
<td colspan="7">
<p class="text-center text-danger">Add a Plex Server</p>
</td>
</tr>
<tr ng-repeat="x in servers">
<tr ng-if="serversPending">
<td><div class="loader"></div> <span class='text-info'>{{ addingServer }}</span></td>
</tr>
<tr ng-repeat="x in servers" ng-hide="serversPending" >
<td>{{ x.name }}</td>
<td>{{ x.uri }}</td>
<td>{{ x.arGuide }}</td>
<td>{{ x.arChannels }}</td>
<td><span class='text-secondary text-small'>{{ x.uri }}</span></td>
<td>
<button class="btn btn-sm btn-danger" ng-click="deletePlexServer(x._id)">
<span class="fa fa-minus"></span>
<div class='loader' ng-if="x.uiStatus == 0"></div>
<div class='fa fa-check text-success' ng-if="x.uiStatus == 1">ok</div>
<div class='fa fa-warning text-danger' ng-if="x.uiStatus == -1">error</div>
</td>
<td>
<div class='loader' ng-if="x.backendStatus == 0"></div>
<div class='fa fa-check text-success' ng-if="x.backendStatus == 1">ok</div>
<div class='fa fa-warning text-danger' ng-if="x.backendStatus == -1">error</div>
</td>
<td>
<button class="btn btn-sm btn-outline-secondary" ng-click="editPlexServer(x)">
<span class="fa fa-edit"></span>
</button>
</td>
<tr ng-if="servers.length !== 0">
<tr ng-if="serverError.length &gt; 0">
<td colspan="5">
<p class="text-center text-info small">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.</p>
<p class="text-center text-danger small">{{serverError}}</p>
</td>
</tr>
<tr ng-if="isAnyUIBad()">
<td colspan="5">
<p class="text-center text-danger small">If a Plex server configuration has problems with the UI route, the channel editor won&apos;t be able to access its content.</p>
</td>
</tr>
<tr ng-if="isAnyBackendBad()">
<td colspan="5">
<p class="text-center text-danger small">If a Plex server configuration has problems with the backend route, dizqueTV won&apos;t be able to play its content.</p>
</td>
</tr>
</table>
<hr>
@ -174,3 +196,4 @@
</div>
</div>
</div>
<plex-server-edit state="_serverEditorState" on-finish="serverEditFinished"></plex-server-edit>

View File

@ -9,8 +9,8 @@
</h5>
<table class="table">
<tr>
<th width="120">Number</th>
<th width="120">Icon</th>
<th width="40">Number</th>
<th width="120" class='text-center'>Icon</th>
<th>Name</th>
<th></th>
</tr>
@ -19,16 +19,19 @@
<p class="text-center text-danger">No channels found. Click the <span class="fa fa-plus"></span> to create a channel.</p>
</td>
</tr>
<tr ng-repeat="x in channels" ng-click="selectChannel($index)" style="cursor: pointer;">
<td>{{x.number}}</td>
<tr ng-repeat="x in channels" ng-click="selectChannel($index)" style="cursor: pointer; height: 3em">
<td style='height: 3em'>
<div class="loader" ng-if="x.pending"></div>
<span ng-show="!x.pending">{{x.number}}</span>
</td>
<td style="padding: 0" class="text-center">
<img ng-if="x.icon !== ''" ng-src="{{x.icon}}" alt="{{x.name}}" style="max-height: 40px;"/>
<div ng-if="x.icon === ''" style="padding-top: 14px;"><small>{{x.name}}</small></div>
</td>
<td>{{x.name}}</td>
<td class="text-right">
<button class="btn btn-sm" ng-click="removeChannel(x); $event.stopPropagation()">
<span class="fa fa-trash"></span>
<button ng-show="!x.pending" class="btn btn-sm btn-link" ng-click="removeChannel($index, x); $event.stopPropagation()">
<span class="text-danger fa fa-trash"></span>
</button>
</td>
</tr>

View File

@ -10,15 +10,15 @@
</tr>
<tr>
<td>dizqueTV-backend</td>
<td>{{version}}</td>
<td><div class='loader' ng-if="version.length &lt;= 0"></div>{{version}}</td>
</tr>
<tr>
<td>dizqueTV-ui</td>
<td id='uiversion'>Getting dizqueTV UI version...</td>
<td id='uiversion'><div class='loader'></div></td>
</tr>
<tr>
<td>FFMPEG</td>
<td>{{ffmpegVersion}}</td>
<td><div class='loader' ng-if="version.length &lt;= 0"></div>{{ffmpegVersion}}</td>
</tr>
<!-- coming soon, ffmpeg version, nodejs version, plex version, whatever can be used to help debug things-->
</table>

View File

@ -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 })

View File

@ -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,