1 json per channel. Plex server editing and status. Max resolution for transcoding. 640x360 fix.
This commit is contained in:
parent
55c22846bf
commit
3022dfe375
28
index.js
28
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)
|
||||
|
||||
@ -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
|
||||
|
||||
173
src/api.js
173
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) {
|
||||
|
||||
@ -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
103
src/dao/channel-db.js
Normal 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
142
src/dao/plex-server-db.js
Normal 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
|
||||
@ -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,
|
||||
|
||||
@ -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 ),
|
||||
|
||||
@ -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)
|
||||
|
||||
@ -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;
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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
|
||||
|
||||
29
src/video.js
29
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
|
||||
|
||||
@ -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'))
|
||||
|
||||
@ -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
|
||||
}
|
||||
}
|
||||
@ -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;
|
||||
|
||||
@ -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;
|
||||
|
||||
@ -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)"},
|
||||
|
||||
@ -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) ;
|
||||
|
||||
@ -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;
|
||||
|
||||
72
web/directives/plex-server-edit.js
Normal file
72
web/directives/plex-server-edit.js
Normal 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();
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
}
|
||||
@ -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
|
||||
|
||||
@ -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)))
|
||||
|
||||
@ -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 < channel.programs.length - 100" class="list-group-item flex-container" >
|
||||
|
||||
@ -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>
|
||||
|
||||
@ -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>
|
||||
|
||||
@ -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 >= 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>
|
||||
|
||||
96
web/public/templates/plex-server-edit.html
Normal file
96
web/public/templates/plex-server-edit.html
Normal 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 > 0">
|
||||
<td>{{x.channelNumber}}</td>
|
||||
<td>{{x.channelName}}</td>
|
||||
<td>{{x.destroyedPrograms}} program{{ (x.destroyedPrograms>1?"s":"") }} 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>
|
||||
@ -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 > 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'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'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>
|
||||
@ -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>
|
||||
|
||||
@ -10,15 +10,15 @@
|
||||
</tr>
|
||||
<tr>
|
||||
<td>dizqueTV-backend</td>
|
||||
<td>{{version}}</td>
|
||||
<td><div class='loader' ng-if="version.length <= 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 <= 0"></div>{{ffmpegVersion}}</td>
|
||||
</tr>
|
||||
<!-- coming soon, ffmpeg version, nodejs version, plex version, whatever can be used to help debug things-->
|
||||
</table>
|
||||
|
||||
@ -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 })
|
||||
|
||||
@ -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,
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user