- Channel redirects will now render in the TV Guide.
- Programs shorter than 5 minutes will not appear in the TV Guide and will be treated the same way Flex time is treated normally. - The Web UI now has its own TV Guide viewer so that you don't have to rely on clients to preview your channels' line ups. - API: New endpoint to get what shows are being scheduled for a given time frame. - Fix bug where some bad luck could cause two different XMLTVs to get written at once, generating a corrupt xmltv file.
This commit is contained in:
parent
a24ce52f41
commit
9bd3e9063b
75
index.js
75
index.js
@ -15,6 +15,7 @@ const Plex = require('./src/plex');
|
||||
const channelCache = require('./src/channel-cache');
|
||||
const constants = require('./src/constants')
|
||||
const ChannelDB = require("./src/dao/channel-db");
|
||||
const TVGuideService = require("./src/tv-guide-service");
|
||||
|
||||
console.log(
|
||||
` \\
|
||||
@ -58,42 +59,64 @@ db.connect(process.env.DATABASE, ['channels', 'plex-servers', 'ffmpeg-settings',
|
||||
|
||||
initDB(db, channelDB)
|
||||
|
||||
const guideService = new TVGuideService(xmltv);
|
||||
|
||||
|
||||
|
||||
let xmltvInterval = {
|
||||
interval: null,
|
||||
lastRefresh: null,
|
||||
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...
|
||||
channelCache.saveChannelConfig( channel.number, channel );
|
||||
});
|
||||
channels.sort((a, b) => { return a.number < b.number ? -1 : 1 })
|
||||
let xmltvSettings = db['xmltv-settings'].find()[0]
|
||||
xmltv.WriteXMLTV(channels, xmltvSettings).then(async () => { // Update XML
|
||||
try {
|
||||
let channelNumbers = await channelDB.getAllChannelNumbers();
|
||||
await Promise.all( channelNumbers.map( async (x) => {
|
||||
return await channelCache.getChannelConfig(channelDB, x);
|
||||
}) );
|
||||
await guideService.refresh( await channelDB.getAllChannels(), 12*60*60*1000 );
|
||||
xmltvInterval.lastRefresh = new Date()
|
||||
console.log('XMLTV Updated at ', xmltvInterval.lastRefresh.toLocaleString())
|
||||
let plexServers = db['plex-servers'].find()
|
||||
for (let i = 0, l = plexServers.length; i < l; i++) { // Foreach plex server
|
||||
var plex = new Plex(plexServers[i])
|
||||
await plex.GetDVRS().then(async (dvrs) => { // Refresh guide and channel mappings
|
||||
if (plexServers[i].arGuide)
|
||||
plex.RefreshGuide(dvrs).then(() => { }, (err) => { console.error(err, i) })
|
||||
if (plexServers[i].arChannels && channels.length !== 0)
|
||||
plex.RefreshChannels(channels, dvrs).then(() => { }, (err) => { console.error(err, i) })
|
||||
}).catch( (err) => {
|
||||
console.log("Couldn't tell Plex to refresh channels for some reason.");
|
||||
});
|
||||
console.log('XMLTV Updated at ', xmltvInterval.lastRefresh.toLocaleString());
|
||||
} catch (err) {
|
||||
console.error("Unable to update TV guide?", err);
|
||||
}
|
||||
|
||||
let plexServers = db['plex-servers'].find()
|
||||
for (let i = 0, l = plexServers.length; i < l; i++) { // Foreach plex server
|
||||
let plex = new Plex(plexServers[i])
|
||||
let dvrs;
|
||||
if ( !plexServers[i].arGuide && !plexServers[i].arChannels) {
|
||||
continue;
|
||||
}
|
||||
}, (err) => {
|
||||
console.error("Failed to write the xmltv.xml file. Something went wrong. Check your output directory via the web UI and verify file permissions?", err)
|
||||
})
|
||||
try {
|
||||
dvrs = await plex.GetDVRS() // Refresh guide and channel mappings
|
||||
} catch(err) {
|
||||
console.error(`Couldn't get DVRS list from ${plexServers[i].name}. This error will prevent 'refresh guide' or 'refresh channels' from working for this Plex server. But it is NOT related to playback issues.` , err );
|
||||
continue;
|
||||
}
|
||||
if (plexServers[i].arGuide) {
|
||||
try {
|
||||
await plex.RefreshGuide(dvrs);
|
||||
} catch(err) {
|
||||
console.error(`Couldn't tell Plex ${plexServers[i].name} to refresh guide for some reason. This error will prevent 'refresh guide' from working for this Plex server. But it is NOT related to playback issues.` , err);
|
||||
}
|
||||
}
|
||||
if (plexServers[i].arChannels && channels.length !== 0) {
|
||||
try {
|
||||
await plex.RefreshChannels(channels, dvrs);
|
||||
} catch(err) {
|
||||
console.error(`Couldn't tell Plex ${plexServers[i].name} to refresh channels for some reason. This error will prevent 'refresh channels' from working for this Plex server. But it is NOT related to playback issues.` , err);
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
startInterval: () => {
|
||||
let xmltvSettings = db['xmltv-settings'].find()[0]
|
||||
if (xmltvSettings.refresh !== 0) {
|
||||
xmltvInterval.interval = setInterval( async () => {
|
||||
await xmltvInterval.updateXML()
|
||||
try {
|
||||
await xmltvInterval.updateXML()
|
||||
} catch(err) {
|
||||
console.error("update XMLTV error", err);
|
||||
}
|
||||
}, xmltvSettings.refresh * 60 * 60 * 1000)
|
||||
}
|
||||
},
|
||||
@ -130,7 +153,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, channelDB, xmltvInterval))
|
||||
app.use(api.router(db, channelDB, xmltvInterval, guideService ))
|
||||
app.use(video.router( channelDB, db))
|
||||
app.use(hdhr.router)
|
||||
app.listen(process.env.PORT, () => {
|
||||
|
||||
42
src/api.js
42
src/api.js
@ -9,7 +9,7 @@ const PlexServerDB = require('./dao/plex-server-db');
|
||||
const Plex = require("./plex.js");
|
||||
|
||||
module.exports = { router: api }
|
||||
function api(db, channelDB, xmltvInterval) {
|
||||
function api(db, channelDB, xmltvInterval, guideService ) {
|
||||
let router = express.Router()
|
||||
let plexServerDB = new PlexServerDB(channelDB, channelCache, db);
|
||||
|
||||
@ -351,9 +351,46 @@ function api(db, channelDB, xmltvInterval) {
|
||||
console.error(err);
|
||||
res.status(500).send("error");
|
||||
}
|
||||
|
||||
})
|
||||
|
||||
router.get('/api/guide/status', async (req, res) => {
|
||||
try {
|
||||
let s = await guideService.getStatus();
|
||||
res.send(s);
|
||||
} catch(err) {
|
||||
console.error(err);
|
||||
res.status(500).send("error");
|
||||
}
|
||||
});
|
||||
|
||||
router.get('/api/guide/debug', async (req, res) => {
|
||||
try {
|
||||
let s = await guideService.get();
|
||||
res.send(s);
|
||||
} catch(err) {
|
||||
console.error(err);
|
||||
res.status(500).send("error");
|
||||
}
|
||||
});
|
||||
|
||||
|
||||
router.get('/api/guide/channels/:number', async (req, res) => {
|
||||
try {
|
||||
let dateFrom = new Date(req.query.dateFrom);
|
||||
let dateTo = new Date(req.query.dateTo);
|
||||
let lineup = await guideService.getChannelLineup( req.params.number , dateFrom, dateTo );
|
||||
if (lineup == null) {
|
||||
console.log(`GET /api/guide/channels/${req.params.number} : 404 Not Found`);
|
||||
res.status(404).send("Channel not found in TV guide");
|
||||
} else {
|
||||
res.send( lineup );
|
||||
}
|
||||
} catch (err) {
|
||||
console.error(err);
|
||||
res.status(500).send("error");
|
||||
}
|
||||
});
|
||||
|
||||
|
||||
//HDHR SETTINGS
|
||||
router.get('/api/hdhr-settings', (req, res) => {
|
||||
@ -430,7 +467,6 @@ function api(db, channelDB, xmltvInterval) {
|
||||
res.status(500).send("error");
|
||||
}
|
||||
|
||||
|
||||
})
|
||||
|
||||
// hls.m3u Download is not really working correctly right now
|
||||
|
||||
@ -1,6 +1,8 @@
|
||||
module.exports = {
|
||||
SLACK: 9999,
|
||||
TVGUIDE_MAXIMUM_PADDING_LENGTH_MS: 30*60*1000,
|
||||
STEALTH_DURATION: 5 * 60* 1000,
|
||||
TVGUIDE_MAXIMUM_FLEX_DURATION : 6 * 60 * 60 * 1000,
|
||||
|
||||
VERSION_NAME: "0.0.66-prerelease"
|
||||
}
|
||||
|
||||
74
src/plex.js
74
src/plex.js
@ -48,8 +48,23 @@ class Plex {
|
||||
})
|
||||
})
|
||||
}
|
||||
Get(path, optionalHeaders = {}) {
|
||||
var req = {
|
||||
|
||||
doRequest(req) {
|
||||
return new Promise( (resolve, reject) => {
|
||||
request( req, (err, res) => {
|
||||
if (err) {
|
||||
reject(err);
|
||||
} else if ((res.statusCode < 200) || (res.statusCode >= 300) ) {
|
||||
reject( Error(`Request returned status code ${res.statusCode}`) );
|
||||
} else {
|
||||
resolve(res);
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
async Get(path, optionalHeaders = {}) {
|
||||
let req = {
|
||||
method: 'get',
|
||||
url: `${this.URL}${path}`,
|
||||
headers: this._headers,
|
||||
@ -57,19 +72,14 @@ class Plex {
|
||||
}
|
||||
Object.assign(req, optionalHeaders)
|
||||
req.headers['X-Plex-Token'] = this._accessToken
|
||||
return new Promise((resolve, reject) => {
|
||||
if (this._accessToken === '')
|
||||
reject("No Plex token provided. Please use the SignIn method or provide a X-Plex-Token in the Plex constructor.")
|
||||
else
|
||||
request(req, (err, res) => {
|
||||
if (err || res.statusCode !== 200)
|
||||
reject(`Plex 'Get' request failed. URL: ${this.URL}${path}`)
|
||||
else
|
||||
resolve(JSON.parse(res.body).MediaContainer)
|
||||
})
|
||||
})
|
||||
if (this._accessToken === '') {
|
||||
throw Error("No Plex token provided. Please use the SignIn method or provide a X-Plex-Token in the Plex constructor.");
|
||||
} else {
|
||||
let res = await this.doRequest(req);
|
||||
return JSON.parse(res.body).MediaContainer;
|
||||
}
|
||||
}
|
||||
Put(path, query = {}, optionalHeaders = {}) {
|
||||
async Put(path, query = {}, optionalHeaders = {}) {
|
||||
var req = {
|
||||
method: 'put',
|
||||
url: `${this.URL}${path}`,
|
||||
@ -79,7 +89,7 @@ class Plex {
|
||||
}
|
||||
Object.assign(req, optionalHeaders)
|
||||
req.headers['X-Plex-Token'] = this._accessToken
|
||||
return new Promise((resolve, reject) => {
|
||||
await new Promise((resolve, reject) => {
|
||||
if (this._accessToken === '')
|
||||
reject("No Plex token provided. Please use the SignIn method or provide a X-Plex-Token in the Plex constructor.")
|
||||
else
|
||||
@ -123,30 +133,42 @@ class Plex {
|
||||
}
|
||||
}
|
||||
async GetDVRS() {
|
||||
var result = await this.Get('/livetv/dvrs')
|
||||
var dvrs = result.Dvr
|
||||
dvrs = typeof dvrs === 'undefined' ? [] : dvrs
|
||||
return dvrs
|
||||
try {
|
||||
var result = await this.Get('/livetv/dvrs')
|
||||
var dvrs = result.Dvr
|
||||
dvrs = typeof dvrs === 'undefined' ? [] : dvrs
|
||||
return dvrs
|
||||
} catch (err) {
|
||||
throw Error( "GET /livetv/drs failed: " + err.message);
|
||||
}
|
||||
}
|
||||
async RefreshGuide(_dvrs) {
|
||||
var dvrs = typeof _dvrs !== 'undefined' ? _dvrs : await this.GetDVRS()
|
||||
for (var i = 0; i < dvrs.length; i++)
|
||||
this.Post(`/livetv/dvrs/${dvrs[i].key}/reloadGuide`).then(() => { }, (err) => { console.log(err) })
|
||||
try {
|
||||
var dvrs = typeof _dvrs !== 'undefined' ? _dvrs : await this.GetDVRS()
|
||||
for (var i = 0; i < dvrs.length; i++) {
|
||||
await this.Post(`/livetv/dvrs/${dvrs[i].key}/reloadGuide`);
|
||||
}
|
||||
} catch (err) {
|
||||
throw Error("Zort", err);
|
||||
}
|
||||
}
|
||||
async RefreshChannels(channels, _dvrs) {
|
||||
var dvrs = typeof _dvrs !== 'undefined' ? _dvrs : await this.GetDVRS()
|
||||
var _channels = []
|
||||
let qs = {}
|
||||
for (var i = 0; i < channels.length; i++)
|
||||
for (var i = 0; i < channels.length; i++) {
|
||||
_channels.push(channels[i].number)
|
||||
}
|
||||
qs.channelsEnabled = _channels.join(',')
|
||||
for (var i = 0; i < _channels.length; i++) {
|
||||
qs[`channelMapping[${_channels[i]}]`] = _channels[i]
|
||||
qs[`channelMappingByKey[${_channels[i]}]`] = _channels[i]
|
||||
}
|
||||
for (var i = 0; i < dvrs.length; i++)
|
||||
for (var y = 0; y < dvrs[i].Device.length; y++)
|
||||
this.Put(`/media/grabbers/devices/${dvrs[i].Device[y].key}/channelmap`, qs).then(() => { }, (err) => { console.log(err) })
|
||||
for (var i = 0; i < dvrs.length; i++) {
|
||||
for (var y = 0; y < dvrs[i].Device.length; y++) {
|
||||
await this.Put(`/media/grabbers/devices/${dvrs[i].Device[y].key}/channelmap`, qs);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
441
src/tv-guide-service.js
Normal file
441
src/tv-guide-service.js
Normal file
@ -0,0 +1,441 @@
|
||||
|
||||
const constants = require("./constants");
|
||||
const FALLBACK_ICON = "https://raw.githubusercontent.com/vexorain/dizquetv/main/resources/dizquetv.png";
|
||||
|
||||
class TVGuideService
|
||||
{
|
||||
/****
|
||||
*
|
||||
**/
|
||||
constructor(xmltv) {
|
||||
this.cached = null;
|
||||
this.lastUpdate = 0;
|
||||
this.updateTime = 0;
|
||||
this.currentUpdate = -1;
|
||||
this.currentLimit = -1;
|
||||
this.currentChannels = null;
|
||||
this.throttleX = 0;
|
||||
this.doThrottle = false;
|
||||
this.xmltv = xmltv;
|
||||
}
|
||||
|
||||
async get() {
|
||||
while (this.cached == null) {
|
||||
await _wait(100);
|
||||
}
|
||||
this.doThrottle = true;
|
||||
return this.cached;
|
||||
}
|
||||
|
||||
async refresh(channels, limit) {
|
||||
let t = (new Date()).getTime();
|
||||
this.updateTime = t;
|
||||
this.updateLimit = t + limit;
|
||||
this.updateChannels = channels;
|
||||
while( this.lastUpdate < t) {
|
||||
if (this.currentUpdate == -1) {
|
||||
this.currentUpdate = this.updateTime;
|
||||
this.currentLimit = this.updateLimit;
|
||||
this.currentChannels = this.updateChannels;
|
||||
await this.buildIt();
|
||||
}
|
||||
await _wait(100);
|
||||
}
|
||||
return await this.get();
|
||||
}
|
||||
|
||||
async makeAccumulated(channel) {
|
||||
let n = channel.programs.length;
|
||||
let arr = new Array( channel.programs.length + 1);
|
||||
arr[0] = 0;
|
||||
for (let i = 0; i < n; i++) {
|
||||
arr[i+1] = arr[i] + channel.programs[i].duration;
|
||||
await this._throttle();
|
||||
}
|
||||
return arr;
|
||||
}
|
||||
|
||||
async getCurrentPlayingIndex(channel, t) {
|
||||
let s = (new Date(channel.startTime)).getTime();
|
||||
if (t < s) {
|
||||
//it's flex time
|
||||
return {
|
||||
index : -1,
|
||||
start : t,
|
||||
program : {
|
||||
isOffline : true,
|
||||
duration : s - t,
|
||||
}
|
||||
}
|
||||
} else {
|
||||
let accumulate = this.accumulateTable[ channel.number ];
|
||||
if (typeof(accumulate) === 'undefined') {
|
||||
throw Error(channel.number + " wasn't preprocesed correctly???!?");
|
||||
}
|
||||
let hi = channel.programs.length;
|
||||
let lo = 0;
|
||||
let d = (t - s) % (accumulate[channel.programs.length]);
|
||||
let epoch = t - d;
|
||||
while (lo + 1 < hi) {
|
||||
let ha = Math.floor( (lo + hi) / 2 );
|
||||
if (accumulate[ha] > d) {
|
||||
hi = ha;
|
||||
} else {
|
||||
lo = ha;
|
||||
}
|
||||
}
|
||||
|
||||
if (epoch + accumulate[lo+1] <= t) {
|
||||
throw Error("General algorithm error, completely unexpected");
|
||||
}
|
||||
await this._throttle();
|
||||
return {
|
||||
index: lo,
|
||||
start: epoch + accumulate[lo],
|
||||
program: channel.programs[lo],
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
async getChannelPlaying(channel, previousKnown, t, depth) {
|
||||
if (typeof(depth) === 'undefined') {
|
||||
depth = [];
|
||||
}
|
||||
let playing = {};
|
||||
if (
|
||||
(typeof(previousKnown) !== 'undefined')
|
||||
&& (previousKnown.program.duration == channel.programs[previousKnown.index].duration )
|
||||
&& (previousKnown.start + previousKnown.program.duration == t)
|
||||
) {
|
||||
//turns out we know the index.
|
||||
let index = (previousKnown.index + 1) % channel.programs.length;
|
||||
playing = {
|
||||
index : index,
|
||||
program: channel.programs[index],
|
||||
start : t,
|
||||
}
|
||||
} else {
|
||||
playing = await this.getCurrentPlayingIndex(channel, t);
|
||||
}
|
||||
if ( playing.program.isOffline && playing.program.type === 'redirect') {
|
||||
let ch2 = playing.program.channel;
|
||||
|
||||
if (depth.indexOf(ch2) != -1) {
|
||||
console.error("Redirrect loop found! Involved channels = " + JSON.stringify(depth) );
|
||||
} else {
|
||||
depth.push( channel.number );
|
||||
let channel2 = this.channelsByNumber[ch2];
|
||||
if (typeof(channel2) === undefined) {
|
||||
console.error("Redirrect to an unknown channel found! Involved channels = " + JSON.stringify(depth) );
|
||||
} else {
|
||||
let otherPlaying = await this.getChannelPlaying( channel2, undefined, t, depth );
|
||||
let start = Math.max(playing.start, otherPlaying.start);
|
||||
let duration = Math.min(
|
||||
(playing.start + playing.program.duration) - start,
|
||||
(otherPlaying.start + otherPlaying.program.duration) - start
|
||||
);
|
||||
let program2 = clone( otherPlaying.program );
|
||||
program2.duration = duration;
|
||||
playing = {
|
||||
index: playing.index,
|
||||
start : start,
|
||||
program: program2,
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
return playing;
|
||||
}
|
||||
|
||||
async getChannelPrograms(t0, t1, channel) {
|
||||
let result = {
|
||||
channel: makeChannelEntry(channel),
|
||||
};
|
||||
let programs = [];
|
||||
let x = await this.getChannelPlaying(channel, undefined, t0);
|
||||
if (x.program.duration == 0) throw Error("A " + channel.name + " " + JSON.stringify(x) );
|
||||
|
||||
|
||||
let push = async (x) => {
|
||||
await this._throttle();
|
||||
if (
|
||||
(programs.length > 0)
|
||||
&& isProgramFlex(x.program)
|
||||
&& (
|
||||
(x.program.duration <= constants.TVGUIDE_MAXIMUM_PADDING_LENGTH_MS)
|
||||
|| isProgramFlex(programs[ programs.length - 1].program)
|
||||
)
|
||||
) {
|
||||
//meld with previous
|
||||
let y = clone( programs[ programs.length - 1] );
|
||||
y.program.duration += x.program.duration;
|
||||
programs[ programs.length - 1] = y;
|
||||
} else if (isProgramFlex(x.program) ) {
|
||||
if (programs.length > 0) {
|
||||
let y = programs[ programs.length - 1];
|
||||
let a = y.start;
|
||||
let b = a + y.program.duration;
|
||||
let a2 = x.start;
|
||||
if (b > a2) {
|
||||
throw Error( [ "darn0", b, a2, JSON.stringify(y) , JSON.stringify(x) ] );
|
||||
}
|
||||
}
|
||||
|
||||
programs.push( {
|
||||
start: x.start,
|
||||
program: {
|
||||
isOffline : true,
|
||||
duration: x.program.duration,
|
||||
},
|
||||
} );
|
||||
} else {
|
||||
if (programs.length > 0) {
|
||||
let y = programs[ programs.length - 1];
|
||||
let a = y.start;
|
||||
let b = a + y.program.duration;
|
||||
let a2 = x.start;
|
||||
if (b > a2) {
|
||||
throw Error( [ "darn", b, a2, JSON.stringify(y) , JSON.stringify(x) ] );
|
||||
}
|
||||
}
|
||||
programs.push(x);
|
||||
}
|
||||
};
|
||||
while (x.start < t1) {
|
||||
await push(x);
|
||||
x = await this.getChannelPlaying(channel, x, x.start + x.program.duration);
|
||||
if (x.program.duration == 0) throw Error("D");
|
||||
}
|
||||
result.programs = [];
|
||||
for (let i = 0; i < programs.length; i++) {
|
||||
await this._throttle();
|
||||
if (isProgramFlex( programs[i].program) ) {
|
||||
let start = programs[i].start;
|
||||
let duration = programs[i].program.duration;
|
||||
if (start <= t0) {
|
||||
const M = 5*60*1000;
|
||||
let newStart = t0 - t0%M;
|
||||
if (start < newStart) {
|
||||
duration -= (newStart - start);
|
||||
start = newStart;
|
||||
}
|
||||
}
|
||||
while( start < t1 && duration > 0) {
|
||||
let d = Math.min(duration, constants.TVGUIDE_MAXIMUM_FLEX_DURATION);
|
||||
if (duration - constants.TVGUIDE_MAXIMUM_FLEX_DURATION <= constants.TVGUIDE_MAXIMUM_PADDING_LENGTH_MS) {
|
||||
d = duration;
|
||||
}
|
||||
let x = {
|
||||
start: start,
|
||||
program: {
|
||||
isOffline: true,
|
||||
duration: d,
|
||||
}
|
||||
}
|
||||
duration -= d;
|
||||
start += d;
|
||||
result.programs.push( makeEntry(channel,x) );
|
||||
}
|
||||
} else {
|
||||
if (i > 0) {
|
||||
let y = programs[ i - 1];
|
||||
let x = programs[i];
|
||||
let a = y.start;
|
||||
let b = a + y.program.duration;
|
||||
let a2 = x.start;
|
||||
if (b > a2) {
|
||||
console.error( "darn2", b, a2 );
|
||||
}
|
||||
|
||||
}
|
||||
result.programs.push( makeEntry(channel, programs[i] ) );
|
||||
}
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
async buildItManaged() {
|
||||
let t0 = this.currentUpdate;
|
||||
let t1 = this.currentLimit;
|
||||
let channels = this.currentChannels;
|
||||
let accumulateTable = {};
|
||||
this.channelsByNumber = {};
|
||||
for (let i = 0; i < channels.length; i++) {
|
||||
this.channelsByNumber[ channels[i].number ] = channels[i];
|
||||
accumulateTable[ channels[i].number ] = await this.makeAccumulated(channels[i]);
|
||||
}
|
||||
this.accumulateTable = accumulateTable;
|
||||
let result = {};
|
||||
if (channels.length == 0) {
|
||||
result[1] = {
|
||||
channel : {
|
||||
name: "dizqueTV",
|
||||
icon: FALLBACK_ICON,
|
||||
},
|
||||
programs: [
|
||||
makeEntry( {
|
||||
start: t0 - t0 % (30 * 60*1000),
|
||||
program: {
|
||||
icon: FALLBACK_ICON,
|
||||
title: "No channels configured",
|
||||
date: (new Date()).format('YYYY-MM-DD'),
|
||||
summary : "Use the dizqueTV web UI to configure channels."
|
||||
}
|
||||
} )
|
||||
]
|
||||
}
|
||||
} else {
|
||||
for (let i = 0; i < channels.length; i++) {
|
||||
let programs = await this.getChannelPrograms(t0, t1, channels[i] );
|
||||
result[ channels[i].number ] = programs;
|
||||
}
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
async buildIt() {
|
||||
try {
|
||||
console.log("<buildit>");
|
||||
this.cached = await this.buildItManaged();
|
||||
console.log("</buildit>");
|
||||
console.log("Internal TV Guide data refreshed at " + (new Date()).toLocaleString() );
|
||||
await this.refreshXML();
|
||||
} catch(err) {
|
||||
if (this.cached == null) {
|
||||
throw err;
|
||||
} else {
|
||||
console.error("Unable to update internal guide data", err);
|
||||
}
|
||||
} finally {
|
||||
this.lastUpdate = this.currentUpdate;
|
||||
this.currentUpdate = -1;
|
||||
}
|
||||
}
|
||||
|
||||
async _throttle() {
|
||||
//this.doThrottle = true;
|
||||
if ( this.doThrottle && (this.throttleX++)%10 == 0) {
|
||||
await _wait(0);
|
||||
}
|
||||
}
|
||||
|
||||
async refreshXML() {
|
||||
let xmltvSettings = {
|
||||
file : "./.dizquetv/xmltv.xml",
|
||||
}
|
||||
await this.xmltv.WriteXMLTV(this.cached, xmltvSettings, async() => await this._throttle() );
|
||||
}
|
||||
|
||||
async getStatus() {
|
||||
await this.get();
|
||||
let channels = [];
|
||||
|
||||
Object.keys( this.cached )
|
||||
.forEach( (k,index) => channels.push(k) );
|
||||
|
||||
return {
|
||||
lastUpdate : new Date(this.lastUpdate).toISOString(),
|
||||
channelNumbers: channels,
|
||||
}
|
||||
}
|
||||
|
||||
async getChannelLineup(channelNumber, dateFrom, dateTo) {
|
||||
await this.get();
|
||||
let t0 = dateFrom.toISOString();
|
||||
let t1 = dateTo.toISOString();
|
||||
let channel = this.cached[channelNumber];
|
||||
if (typeof(channel) === undefined) {
|
||||
return null;
|
||||
}
|
||||
let programs = channel.programs;
|
||||
let result = {
|
||||
icon: channel.channel.icon,
|
||||
name: channel.channel.name,
|
||||
number: channel.channel.number,
|
||||
programs: [],
|
||||
};
|
||||
for (let i = 0; i < programs.length; i++) {
|
||||
let program = programs[i];
|
||||
let a;
|
||||
if (program.start > t0) {
|
||||
a = program.start;
|
||||
} else {
|
||||
a = t0;
|
||||
}
|
||||
let b;
|
||||
if (program.stop < t1) {
|
||||
b = program.stop;
|
||||
} else {
|
||||
b = t1;
|
||||
}
|
||||
|
||||
if (a < b) {
|
||||
result.programs.push( program );
|
||||
}
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
|
||||
function _wait(t) {
|
||||
return new Promise((resolve) => {
|
||||
setTimeout(resolve, t);
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
function isProgramFlex(program) {
|
||||
return program.isOffline || program.duration <= constants.STEALTH_DURATION
|
||||
}
|
||||
|
||||
function clone(o) {
|
||||
return JSON.parse( JSON.stringify(o) );
|
||||
}
|
||||
|
||||
function makeChannelEntry(channel) {
|
||||
return {
|
||||
name: channel.name,
|
||||
icon: channel.icon,
|
||||
number: channel.number,
|
||||
}
|
||||
}
|
||||
|
||||
function makeEntry(channel, x) {
|
||||
let title = undefined;
|
||||
let icon = undefined;
|
||||
let sub = undefined;
|
||||
if (isProgramFlex(x.program)) {
|
||||
title = channel.name;
|
||||
icon = channel.icon;
|
||||
} else {
|
||||
title = x.program.showTitle;
|
||||
if (typeof(x.program.icon) !== 'undefined') {
|
||||
icon = x.program.icon;
|
||||
}
|
||||
if (x.program.type === 'episode') {
|
||||
sub = {
|
||||
season: x.program.season,
|
||||
episode: x.program.episode,
|
||||
title: x.program.title,
|
||||
}
|
||||
}
|
||||
}
|
||||
//what data is needed here?
|
||||
return {
|
||||
start: (new Date(x.start)).toISOString(),
|
||||
stop: (new Date(x.start + x.program.duration)).toISOString(),
|
||||
summary: x.program.summary,
|
||||
date: x.program.date,
|
||||
rating: x.program.rating,
|
||||
icon: icon,
|
||||
title: title,
|
||||
sub: sub,
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = TVGuideService
|
||||
113
src/xmltv.js
113
src/xmltv.js
@ -5,35 +5,23 @@ const constants = require('./constants')
|
||||
|
||||
module.exports = { WriteXMLTV: WriteXMLTV }
|
||||
|
||||
function WriteXMLTV(channels, xmlSettings) {
|
||||
function WriteXMLTV(json, xmlSettings, throttle ) {
|
||||
return new Promise((resolve, reject) => {
|
||||
let date = new Date()
|
||||
let ws = fs.createWriteStream(xmlSettings.file)
|
||||
let xw = new XMLWriter(true, (str, enc) => ws.write(str, enc))
|
||||
ws.on('close', () => { resolve() })
|
||||
ws.on('error', (err) => { reject(err) })
|
||||
_writeDocStart(xw)
|
||||
async function middle() {
|
||||
if (channels.length === 0) { // Write Dummy dizqueTV Channel if no channel exists
|
||||
_writeChannels(xw, [{ number: 1, name: "dizqueTV", icon: "https://raw.githubusercontent.com/vexorain/dizquetv/main/resources/dizquetv.png" }])
|
||||
let program = {
|
||||
program: {
|
||||
type: 'movie',
|
||||
title: 'No Channels Configured',
|
||||
summary: 'Configure your channels using the dizqueTV Web UI.'
|
||||
},
|
||||
channel: '1',
|
||||
start: date,
|
||||
stop: new Date(date.valueOf() + xmlSettings.cache * 60 * 60 * 1000)
|
||||
}
|
||||
await _writeProgramme(null, xw, program, null)
|
||||
} else {
|
||||
_writeChannels(xw, channels)
|
||||
for (let i = 0; i < channels.length; i++) {
|
||||
await _writePrograms(xw, channels[i], date, xmlSettings.cache)
|
||||
async function middle() {
|
||||
let channelNumbers = [];
|
||||
Object.keys(json).forEach( (key, index) => channelNumbers.push(key) );
|
||||
let channels = channelNumbers.map( (number) => json[number].channel );
|
||||
_writeChannels( xw, channels );
|
||||
for (let i = 0; i < channelNumbers.length; i++) {
|
||||
let number = channelNumbers[i];
|
||||
await _writePrograms(xw, json[number].channel, json[number].programs, throttle);
|
||||
}
|
||||
}
|
||||
}
|
||||
middle().then( () => {
|
||||
_writeDocEnd(xw, ws)
|
||||
}).catch( (err) => {
|
||||
@ -69,104 +57,65 @@ function _writeChannels(xw, channels) {
|
||||
}
|
||||
}
|
||||
|
||||
async function _writePrograms(xw, channel, date, cache) {
|
||||
let item = helperFuncs.getCurrentProgramAndTimeElapsed(date.getTime(), channel)
|
||||
let prog = item.program;
|
||||
let cutoff = new Date( date.valueOf() + (cache * 60 * 60 * 1000) )
|
||||
let temp = new Date(date.valueOf() - item.timeElapsed)
|
||||
if (channel.programs.length === 0) {
|
||||
return
|
||||
}
|
||||
let i = item.programIndex;
|
||||
for (; temp < cutoff;) {
|
||||
await _throttle(); //let's not block for this process
|
||||
let program = {
|
||||
program: prog,
|
||||
channel: channel.number,
|
||||
start: new Date(temp.valueOf()),
|
||||
stop: new Date(temp.valueOf() + prog.duration)
|
||||
}
|
||||
let ni = (i + 1) % channel.programs.length;
|
||||
if (
|
||||
( (typeof(program.program.isOffline) === 'undefined') || !(program.program.isOffline) )
|
||||
&&
|
||||
(channel.programs[ni].isOffline)
|
||||
&&
|
||||
(channel.programs[ni].duration < constants.TVGUIDE_MAXIMUM_PADDING_LENGTH_MS )
|
||||
) {
|
||||
program.stop = new Date(temp.valueOf() + prog.duration + channel.programs[ni].duration)
|
||||
i = (i + 2) % channel.programs.length;
|
||||
} else {
|
||||
i = ni;
|
||||
}
|
||||
_writeProgramme(channel, xw, program, cutoff)
|
||||
temp = program.stop;
|
||||
prog = channel.programs[i];
|
||||
async function _writePrograms(xw, channel, programs, throttle) {
|
||||
for (let i = 0; i < programs.length; i++) {
|
||||
await throttle();
|
||||
await _writeProgramme(channel, programs[i], xw);
|
||||
}
|
||||
}
|
||||
|
||||
async function _writeProgramme(channel, xw, program, cutoff) {
|
||||
async function _writeProgramme(channel, program, xw) {
|
||||
// Programme
|
||||
xw.startElement('programme')
|
||||
xw.writeAttribute('start', _createXMLTVDate(program.start))
|
||||
xw.writeAttribute('stop', _createXMLTVDate(program.stop))
|
||||
xw.writeAttribute('channel', program.channel)
|
||||
xw.writeAttribute('stop', _createXMLTVDate(program.stop ))
|
||||
xw.writeAttribute('channel', channel.number)
|
||||
// Title
|
||||
xw.startElement('title')
|
||||
xw.writeAttribute('lang', 'en')
|
||||
xw.text(program.title);
|
||||
xw.endElement();
|
||||
xw.writeRaw('\n <previously-shown/>')
|
||||
|
||||
if (program.program.isOffline) {
|
||||
xw.text(channel.name)
|
||||
xw.endElement()
|
||||
} else if (program.program.type === 'episode') {
|
||||
xw.text(program.program.showTitle)
|
||||
xw.endElement()
|
||||
xw.writeRaw('\n <previously-shown/>')
|
||||
// Sub-Title
|
||||
//sub-title
|
||||
if ( typeof(program.sub) !== 'undefined') {
|
||||
xw.startElement('sub-title')
|
||||
xw.writeAttribute('lang', 'en')
|
||||
xw.text(program.program.title)
|
||||
xw.text(program.sub.title)
|
||||
xw.endElement()
|
||||
// Episode-Number
|
||||
|
||||
xw.startElement('episode-num')
|
||||
xw.writeAttribute('system', 'xmltv_ns')
|
||||
xw.text((program.program.season - 1) + ' . ' + (program.program.episode - 1) + ' . 0/1')
|
||||
xw.endElement()
|
||||
} else {
|
||||
xw.text(program.program.title)
|
||||
xw.text((program.sub.season - 1) + ' . ' + (program.sub.episode - 1) + ' . 0/1')
|
||||
xw.endElement()
|
||||
}
|
||||
// Icon
|
||||
if (typeof program.program.icon !== 'undefined') {
|
||||
if (typeof program.icon !== 'undefined') {
|
||||
xw.startElement('icon')
|
||||
xw.writeAttribute('src', program.program.icon)
|
||||
xw.writeAttribute('src', program.icon)
|
||||
xw.endElement()
|
||||
}
|
||||
// Desc
|
||||
xw.startElement('desc')
|
||||
xw.writeAttribute('lang', 'en')
|
||||
if (typeof(program.program.summary) !== 'undefined') {
|
||||
xw.text(program.program.summary)
|
||||
if ( (typeof(program.summary) !== 'undefined') && (program.summary.length > 0) ) {
|
||||
xw.text(program.summary)
|
||||
} else {
|
||||
xw.text(channel.name)
|
||||
}
|
||||
xw.endElement()
|
||||
// Rating
|
||||
if (typeof program.program.rating !== 'undefined') {
|
||||
if (typeof program.rating !== 'undefined') {
|
||||
xw.startElement('rating')
|
||||
xw.writeAttribute('system', 'MPAA')
|
||||
xw.writeElement('value', program.program.rating)
|
||||
xw.writeElement('value', program.rating)
|
||||
xw.endElement()
|
||||
}
|
||||
// End of Programme
|
||||
xw.endElement()
|
||||
}
|
||||
function _createXMLTVDate(d) {
|
||||
try {
|
||||
return d.toISOString().substring(0,19).replace(/[-T:]/g,"") + " +0000";
|
||||
} catch(e) {
|
||||
return (new Date()).toISOString().substring(0,19).replace(/[-T:]/g,"") + " +0000";
|
||||
}
|
||||
return d.substring(0,19).replace(/[-T:]/g,"") + " +0000";
|
||||
}
|
||||
function _throttle() {
|
||||
return new Promise((resolve) => {
|
||||
|
||||
@ -25,6 +25,7 @@ app.directive('channelConfig', require('./directives/channel-config'))
|
||||
app.controller('settingsCtrl', require('./controllers/settings'))
|
||||
app.controller('channelsCtrl', require('./controllers/channels'))
|
||||
app.controller('versionCtrl', require('./controllers/version'))
|
||||
app.controller('guideCtrl', require('./controllers/guide'))
|
||||
|
||||
app.config(function ($routeProvider) {
|
||||
$routeProvider
|
||||
@ -36,6 +37,10 @@ app.config(function ($routeProvider) {
|
||||
templateUrl: "views/channels.html",
|
||||
controller: 'channelsCtrl'
|
||||
})
|
||||
.when("/guide", {
|
||||
templateUrl: "views/guide.html",
|
||||
controller: 'guideCtrl'
|
||||
})
|
||||
.when("/version", {
|
||||
templateUrl: "views/version.html",
|
||||
controller: 'versionCtrl'
|
||||
|
||||
342
web/controllers/guide.js
Normal file
342
web/controllers/guide.js
Normal file
@ -0,0 +1,342 @@
|
||||
const MINUTE = 60 * 1000;
|
||||
|
||||
module.exports = function ($scope, $timeout, dizquetv) {
|
||||
|
||||
$scope.offset = 0;
|
||||
$scope.M = 60 * MINUTE;
|
||||
$scope.zoomLevel = 3
|
||||
$scope.T = 190 * MINUTE;
|
||||
$scope.before = 15 * MINUTE;
|
||||
$scope.enableNext = false;
|
||||
$scope.enableBack = false;
|
||||
$scope.showNow = false;
|
||||
$scope.nowPosition = 0;
|
||||
|
||||
const intl = new Intl.DateTimeFormat('default',
|
||||
{
|
||||
hour12: true,
|
||||
hour: 'numeric',
|
||||
minute: 'numeric'
|
||||
});
|
||||
|
||||
let hourMinute = (d) => {
|
||||
return intl.format(d);
|
||||
};
|
||||
|
||||
$scope.updateBasics = () => {
|
||||
$scope.channelNumberWidth = 5;
|
||||
$scope.channelIconWidth = 8;
|
||||
$scope.channelWidth = $scope.channelNumberWidth + $scope.channelIconWidth;
|
||||
//we want 1 minute = 1 colspan
|
||||
$scope.colspanPercent = (100 - $scope.channelWidth) / ($scope.T / MINUTE);
|
||||
$scope.channelColspan = Math.floor($scope.channelWidth / $scope.colspanPercent);
|
||||
$scope.channelNumberColspan = Math.floor($scope.channelNumberWidth / $scope.colspanPercent);
|
||||
$scope.channelIconColspan = $scope.channelColspan - $scope.channelNumberColspan;
|
||||
$scope.totalSpan = Math.floor($scope.T / MINUTE);
|
||||
$scope.colspanPercent = (100 - $scope.channelWidth) / ($scope.T / MINUTE);
|
||||
$scope.channelColspan = Math.floor($scope.channelWidth / $scope.colspanPercent);
|
||||
$scope.channelNumberColspan = Math.floor($scope.channelNumberWidth / $scope.colspanPercent);
|
||||
$scope.channelIconColspan = $scope.channelColspan - $scope.channelNumberColspan;
|
||||
|
||||
}
|
||||
$scope.updateBasics();
|
||||
|
||||
$scope.channelNumberWidth = 5;
|
||||
$scope.channelIconWidth = 8;
|
||||
$scope.channelWidth = $scope.channelNumberWidth + $scope.channelIconWidth;
|
||||
//we want 1 minute = 1 colspan
|
||||
|
||||
|
||||
|
||||
$scope.applyLater = () => {
|
||||
$timeout( () => $scope.$apply(), 0 );
|
||||
};
|
||||
|
||||
$scope.channelNumbers = [];
|
||||
$scope.channels = {};
|
||||
$scope.lastUpdate = -1;
|
||||
|
||||
$scope.updateJustNow = () => {
|
||||
$scope.t1 = (new Date()).getTime();
|
||||
if ($scope.t0 <= $scope.t1 && $scope.t1 < $scope.t0 + $scope.T) {
|
||||
let n = ($scope.t1 - $scope.t0) / MINUTE;
|
||||
$scope.nowPosition = ($scope.channelColspan + n) * $scope.colspanPercent
|
||||
if ($scope.nowPosition >= 50 && $scope.offset >= 0) {
|
||||
$scope.offset = 0;
|
||||
$scope.adjustZoom();
|
||||
}
|
||||
$scope.showNow = true;
|
||||
} else {
|
||||
$scope.showNow = false;
|
||||
}
|
||||
}
|
||||
|
||||
$scope.nowTimer = () => {
|
||||
$scope.updateJustNow();
|
||||
$timeout( () => $scope.nowTimer() , 10000);
|
||||
}
|
||||
$timeout( () => $scope.nowTimer() , 10000);
|
||||
|
||||
|
||||
$scope.refreshManaged = async (skipStatus) => {
|
||||
$scope.t1 = (new Date()).getTime();
|
||||
$scope.t1 = ($scope.t1 - $scope.t1 % MINUTE );
|
||||
$scope.t0 = $scope.t1 - $scope.before + $scope.offset;
|
||||
$scope.title = "TV Guide";
|
||||
$scope.times = [];
|
||||
|
||||
$scope.updateJustNow();
|
||||
let pending = 0;
|
||||
let addDuration = (d) => {
|
||||
let m = (pending + d) % MINUTE;
|
||||
let r = (pending + d) - m;
|
||||
pending = m;
|
||||
return Math.floor( r / MINUTE );
|
||||
}
|
||||
let deleteIfZero = () => {
|
||||
if ( $scope.times.length > 0 && $scope.times[$scope.times.length - 1].duration < 1) {
|
||||
$scope.times = $scope.times.slice(0, $scope.times.length - 1);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
let rem = $scope.T;
|
||||
let t = $scope.t0;
|
||||
if (t % $scope.M != 0) {
|
||||
let dif = $scope.M - t % $scope.M;
|
||||
$scope.times.push( {
|
||||
duration : addDuration(dif),
|
||||
} );
|
||||
deleteIfZero();
|
||||
t += dif;
|
||||
rem -= dif;
|
||||
}
|
||||
while (rem > 0) {
|
||||
let d = Math.min(rem, $scope.M );
|
||||
$scope.times.push( {
|
||||
duration : addDuration(d),
|
||||
label: hourMinute( new Date(t) ),
|
||||
} );
|
||||
t += d;
|
||||
rem -= d;
|
||||
}
|
||||
|
||||
if (skipStatus !== true) {
|
||||
$scope.channelNumbers = [0];
|
||||
$scope.channels = {} ;
|
||||
$scope.channels[0] = {
|
||||
loading: true,
|
||||
}
|
||||
$scope.applyLater();
|
||||
console.log("getting status...");
|
||||
let status = await dizquetv.getGuideStatus();
|
||||
$scope.lastUpdate = new Date(status.lastUpdate).getTime();
|
||||
console.log("got status: " + JSON.stringify(status) );
|
||||
$scope.channelNumbers = status.channelNumbers;
|
||||
$scope.channels = {} ;
|
||||
}
|
||||
|
||||
for (let i = 0; i < $scope.channelNumbers.length; i++) {
|
||||
if ( typeof($scope.channels[$scope.channelNumbers[i]]) === 'undefined') {
|
||||
$scope.channels[$scope.channelNumbers[i]] = {};
|
||||
}
|
||||
$scope.channels[$scope.channelNumbers[i]].loading = true;
|
||||
}
|
||||
$scope.applyLater();
|
||||
$scope.enableBack = false;
|
||||
$scope.enableNext = false;
|
||||
await Promise.all($scope.channelNumbers.map( $scope.loadChannel) );
|
||||
};
|
||||
|
||||
$scope.adjustZoom = async() => {
|
||||
switch ($scope.zoomLevel) {
|
||||
case 1:
|
||||
$scope.T = 50 * MINUTE;
|
||||
$scope.M = 10 * MINUTE;
|
||||
$scope.before = 5 * MINUTE;
|
||||
break;
|
||||
case 2:
|
||||
$scope.T = 100 * MINUTE;
|
||||
$scope.M = 15 * MINUTE;
|
||||
$scope.before = 10 * MINUTE;
|
||||
break;
|
||||
case 3:
|
||||
$scope.T = 190 * MINUTE;
|
||||
$scope.M = 30 * MINUTE;
|
||||
$scope.before = 15 * MINUTE;
|
||||
break;
|
||||
case 4:
|
||||
$scope.T = 270 * MINUTE;
|
||||
$scope.M = 60 * MINUTE;
|
||||
$scope.before = 15 * MINUTE;
|
||||
break;
|
||||
case 5:
|
||||
$scope.T = 380 * MINUTE;
|
||||
$scope.M = 90 * MINUTE;
|
||||
$scope.before = 15 * MINUTE;
|
||||
break;
|
||||
}
|
||||
|
||||
$scope.updateBasics();
|
||||
await $scope.refresh(true);
|
||||
}
|
||||
|
||||
$scope.zoomOut = async() => {
|
||||
$scope.zoomLevel = Math.min( 5, $scope.zoomLevel + 1 );
|
||||
await $scope.adjustZoom();
|
||||
}
|
||||
$scope.zoomIn = async() => {
|
||||
$scope.zoomLevel = Math.max( 1, $scope.zoomLevel - 1 );
|
||||
await $scope.adjustZoom();
|
||||
}
|
||||
$scope.zoomOutEnabled = () => {
|
||||
return $scope.zoomLevel < 5;
|
||||
}
|
||||
$scope.zoomInEnabled = () => {
|
||||
return $scope.zoomLevel > 1;
|
||||
}
|
||||
|
||||
$scope.next = async() => {
|
||||
$scope.offset += $scope.M * 7 / 8
|
||||
await $scope.adjustZoom();
|
||||
}
|
||||
$scope.back = async() => {
|
||||
$scope.offset -= $scope.M * 7 / 8
|
||||
await $scope.adjustZoom();
|
||||
}
|
||||
$scope.backEnabled = () => {
|
||||
return $scope.enableBack;
|
||||
}
|
||||
$scope.nextEnabled = () => {
|
||||
return $scope.enableNext;
|
||||
}
|
||||
|
||||
$scope.loadChannel = async (number) => {
|
||||
console.log(`number=${number}` );
|
||||
let d0 = new Date($scope.t0);
|
||||
let d1 = new Date($scope.t0 + $scope.T);
|
||||
let lineup = await dizquetv.getChannelLineup(number, d0, d1);
|
||||
let ch = {
|
||||
icon : lineup.icon,
|
||||
number : lineup.number,
|
||||
name: lineup.name,
|
||||
altTitle: `${lineup.number} - ${lineup.name}`,
|
||||
programs: [],
|
||||
};
|
||||
|
||||
let pending = 0;
|
||||
let totalAdded = 0;
|
||||
let addDuration = (d) => {
|
||||
totalAdded += d;
|
||||
let m = (pending + d) % MINUTE;
|
||||
let r = (pending + d) - m;
|
||||
pending = m;
|
||||
return Math.floor( r / MINUTE );
|
||||
}
|
||||
|
||||
let deleteIfZero = () => {
|
||||
if ( ch.programs.length > 0 && ch.programs[ ch.programs.length - 1].duration < 1) {
|
||||
ch.programs = ch.programs.slice(0, ch.programs.length - 1);
|
||||
}
|
||||
}
|
||||
|
||||
for (let i = 0; i < lineup.programs.length; i++) {
|
||||
let program = lineup.programs[i];
|
||||
let ad = new Date(program.start);
|
||||
let bd = new Date(program.stop);
|
||||
let a = ad.getTime();
|
||||
let b = bd.getTime();
|
||||
let hasStart = true;
|
||||
let hasStop = true;
|
||||
if (a < $scope.t0) {
|
||||
//cut-off
|
||||
a = $scope.t0;
|
||||
hasStart = false;
|
||||
$scope.enableBack = true;
|
||||
} else if ( (a > $scope.t0) && (i == 0) ) {
|
||||
ch.programs.push( {
|
||||
duration: addDuration( (a - $scope.t0) ),
|
||||
showTitle: "",
|
||||
start: false,
|
||||
end: true,
|
||||
} );
|
||||
deleteIfZero();
|
||||
}
|
||||
if (b > $scope.t0 + $scope.T) {
|
||||
b = $scope.t0 + $scope.T;
|
||||
hasStop = false;
|
||||
$scope.enableNext = true;
|
||||
}
|
||||
let subTitle = undefined;
|
||||
let altTitle = hourMinute(ad) + "-" + hourMinute(bd);
|
||||
if (typeof(program.title) !== 'undefined') {
|
||||
altTitle = altTitle + " · " + program.title;
|
||||
}
|
||||
|
||||
if (typeof(program.sub) !== 'undefined') {
|
||||
ps = "" + program.sub.season;
|
||||
if (ps.length < 2) {
|
||||
ps = "0" + ps;
|
||||
}
|
||||
pe = "" + program.sub.episode;
|
||||
if (pe.length < 2) {
|
||||
pe = "0" + pe;
|
||||
}
|
||||
subTitle = `S${ps} · E${pe}`;
|
||||
altTitle = altTitle + " " + subTitle;
|
||||
} else if ( typeof(program.date) === 'undefined' ) {
|
||||
subTitle = '.';
|
||||
} else {
|
||||
subTitle = program.date.slice(0,4);
|
||||
}
|
||||
ch.programs.push( {
|
||||
duration: addDuration(b - a),
|
||||
altTitle: altTitle,
|
||||
showTitle: program.title,
|
||||
subTitle: subTitle,
|
||||
start: hasStart,
|
||||
end: hasStop,
|
||||
} );
|
||||
deleteIfZero();
|
||||
}
|
||||
if (totalAdded < $scope.T) {
|
||||
ch.programs.push( {
|
||||
duration: addDuration( $scope.T - totalAdded ),
|
||||
showTitle: "",
|
||||
start: false,
|
||||
end: true,
|
||||
} );
|
||||
deleteIfZero();
|
||||
}
|
||||
$scope.channels[number] = ch;
|
||||
$scope.applyLater();
|
||||
$timeout( () => $scope.checkUpdates(), 60000 );
|
||||
|
||||
}
|
||||
|
||||
|
||||
$scope.refresh = async (skipStatus) => {
|
||||
try {
|
||||
await $scope.refreshManaged(skipStatus);
|
||||
} catch (err) {
|
||||
console.error("Refresh failed?", err);
|
||||
}
|
||||
}
|
||||
|
||||
$scope.refresh();
|
||||
|
||||
$scope.checkUpdates = async () => {
|
||||
try {
|
||||
let status = await dizquetv.getGuideStatus();
|
||||
let t = new Date(status.lastUpdate).getTime();
|
||||
if ( t > $scope.lastUpdate) {
|
||||
$scope.refreshManaged();
|
||||
} else {
|
||||
$timeout( () => $scope.checkUpdates(), 60000 );
|
||||
}
|
||||
} catch(err) {
|
||||
console.error(err);
|
||||
}
|
||||
};
|
||||
|
||||
}
|
||||
@ -19,7 +19,7 @@
|
||||
</a>
|
||||
</small>
|
||||
</h1>
|
||||
<a href="#!/channels">Channels</a> - <a href="#!/settings">Settings</a> - <a href="#!/version">Version</a>
|
||||
<a href="#!/channels">Channels</a> - <a href="#!/guide">Guide</a> - <a href="#!/settings">Settings</a> - <a href="#!/version">Version</a>
|
||||
<span class="pull-right">
|
||||
<span style="margin-right: 15px;">
|
||||
<a href="/api/xmltv.xml">XMLTV <span class="fa fa-file-code-o"></span></a>
|
||||
|
||||
@ -105,6 +105,126 @@
|
||||
-webkit-animation: spin 2s linear infinite; /* Safari */
|
||||
animation: spin 2s linear infinite;
|
||||
}
|
||||
table.tvguide {
|
||||
table-layout: fixed;
|
||||
}
|
||||
|
||||
.tvguide th.hour {
|
||||
padding-left: 0;
|
||||
overflow: hidden;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.tvguide .program {
|
||||
padding-left: 0.2em;
|
||||
/*border-top: 1px solid black;
|
||||
border-bottom: 1px solid black ;*/
|
||||
overflow: hidden;
|
||||
}
|
||||
.tvguide .program-with-start {
|
||||
}
|
||||
.tvguide .program-with-end {
|
||||
/*border-right: 1px solid black;*/
|
||||
}
|
||||
.tvguide .program .show-title {
|
||||
white-space: nowrap;
|
||||
font-weight: 400;
|
||||
}
|
||||
.tvguide .program .sub-title {
|
||||
white-space: nowrap;
|
||||
font-weight: 300;
|
||||
}
|
||||
|
||||
.tvguide th button {
|
||||
max-width: 20%;
|
||||
}
|
||||
|
||||
|
||||
.tvguide th {
|
||||
position: sticky;
|
||||
top: 0;
|
||||
bottom: 0;
|
||||
background: white;
|
||||
border-bottom: 1px solid black;
|
||||
}
|
||||
|
||||
.tvguide th.guidenav {
|
||||
padding-left: 5px;
|
||||
padding-right: 0;
|
||||
}
|
||||
|
||||
|
||||
.tvguide td, .tvguide th {
|
||||
color: #F0F0f0;
|
||||
border-top: 0;
|
||||
height: 3.5em;
|
||||
padding-top: 0;
|
||||
padding-bottom: 0;
|
||||
vertical-align: middle;
|
||||
overflow: hidden;
|
||||
}
|
||||
.tvguide th {
|
||||
height: 1.8em;
|
||||
}
|
||||
|
||||
.tvguide td.channel-number, .tvguide td.channelLoading {
|
||||
vertical-align: middle;
|
||||
padding:0;
|
||||
}
|
||||
|
||||
.tvguide td.channel-number div {
|
||||
text-align:center;
|
||||
width:100%;
|
||||
padding:0;
|
||||
}
|
||||
|
||||
.tvguide td.channel-icon {
|
||||
align-items: center;
|
||||
padding-top: 0;
|
||||
padding-bottom: 0;
|
||||
padding-left: 0;
|
||||
padding-right: 0.2em;
|
||||
text-align:center;
|
||||
vertical-align: middle;
|
||||
}
|
||||
|
||||
.tvguide td.channel-icon img {
|
||||
max-height: 95%;
|
||||
max-width:99%
|
||||
}
|
||||
|
||||
|
||||
.tvguide th.even {
|
||||
background: #423cd4ff;
|
||||
}
|
||||
|
||||
.tvguide th.odd {
|
||||
background: #262198ff;
|
||||
}
|
||||
|
||||
.tvguide tr.odd td.even {
|
||||
background: #212121;
|
||||
}
|
||||
|
||||
.tvguide tr.odd td.odd {
|
||||
background: #515151;;
|
||||
}
|
||||
|
||||
.tvguide tr.even td.odd {
|
||||
background: #313131
|
||||
}
|
||||
|
||||
.tvguide tr.even td.even {
|
||||
background: #414141;
|
||||
}
|
||||
|
||||
.tv-guide-now {
|
||||
width:0.2em;
|
||||
height: 100%;
|
||||
position: absolute;
|
||||
background: #FFFF0040;
|
||||
top:0;
|
||||
}
|
||||
|
||||
/* Safari */
|
||||
@-webkit-keyframes spin {
|
||||
@ -115,4 +235,5 @@
|
||||
@keyframes spin {
|
||||
0% { transform: rotate(0deg); }
|
||||
100% { transform: rotate(360deg); }
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
81
web/public/views/guide.html
Normal file
81
web/public/views/guide.html
Normal file
@ -0,0 +1,81 @@
|
||||
<div>
|
||||
|
||||
<h5>
|
||||
{{title}}
|
||||
</h5>
|
||||
<div style='padding:0; position:relative'>
|
||||
<table class="table tvguide" style="{'column-width': colspanPercent + '%' }">
|
||||
<tr>
|
||||
<th colspan="{{channelColspan}}" class='guidenav even' >
|
||||
<button class="btn btn-sm btn-primary" ng-click="zoomIn()" ng-disabled='!zoomInEnabled()'>
|
||||
<span class="fa fa-search-plus"></span>
|
||||
</button>
|
||||
<button class="btn btn-sm btn-primary" ng-click="zoomOut()" ng-disabled='!zoomOutEnabled()'>
|
||||
<span class="fa fa-search-minus"></span>
|
||||
</button>
|
||||
<button class="btn btn-sm btn-primary" ng-click="back()" ng-disabled='!backEnabled()'>
|
||||
<span class="fa fa-arrow-left"></span>
|
||||
</button>
|
||||
<button class="btn btn-sm btn-primary" ng-click="next()" ng-disabled='!nextEnabled()'>
|
||||
<span class="fa fa-arrow-right"></span>
|
||||
</button>
|
||||
|
||||
</th>
|
||||
<th class="hour" ng-Class="{'even' : ($index % 2==1), 'odd' : ($index % 2==0) }" ng-repeat="time in times track by $index" colspan="{{time.duration}}">
|
||||
{{time.label}}
|
||||
</th>
|
||||
</tr>
|
||||
|
||||
<tr ng-repeat="channelNumber in channelNumbers track by $index" ng-Class="{'even' : ($index % 2==0), 'odd' : ($index % 2==1) }" >
|
||||
<td title='{{channels[channelNumber].altTitle}}' class='even channel-number' colspan="{{channelNumberColspan}}" >
|
||||
<div>{{channels[channelNumber].number}}</div>
|
||||
</td>
|
||||
<td title='{{channels[channelNumber].altTitle}}' class='even channel-icon' colspan="{{channelIconColspan}}" >
|
||||
<img src='{{channels[channelNumber].icon}}' alt='{{channels[channelNumber].name}}'></img>
|
||||
</td>
|
||||
|
||||
<td class='odd program' colspan="{{totalSpan}}" ng-if="channels[channelNumber].loading">
|
||||
<div class='loader'></div>
|
||||
</td>
|
||||
|
||||
<td ng-repeat="program in channels[channelNumber].programs track by $index" colspan="{{program.duration}}"
|
||||
title="{{program.altTitle}}"
|
||||
ng-Class="{'program' : true, 'program-with-end' : program.end, 'program-with-start' : program.start, 'even' : ($index % 2==1), 'odd' : ($index % 2==0) }"
|
||||
ng-if="! channels[channelNumber].loading"
|
||||
>
|
||||
<div class='show-title'>
|
||||
{{program.showTitle}}
|
||||
</div>
|
||||
<div class='sub-title'>
|
||||
{{program.subTitle}}
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
|
||||
<tr>
|
||||
<th colspan="{{channelColspan}}" class='guidenav even' >
|
||||
<button class="btn btn-sm btn-primary" ng-click="zoomIn()" ng-disabled='!zoomInEnabled()'>
|
||||
<span class="fa fa-search-plus"></span>
|
||||
</button>
|
||||
<button class="btn btn-sm btn-primary" ng-click="zoomOut()" ng-disabled='!zoomOutEnabled()'>
|
||||
<span class="fa fa-search-minus"></span>
|
||||
</button>
|
||||
<button class="btn btn-sm btn-primary" ng-click="back()" ng-disabled='!backEnabled()'>
|
||||
<span class="fa fa-arrow-left"></span>
|
||||
</button>
|
||||
<button class="btn btn-sm btn-primary" ng-click="next()" ng-disabled='!nextEnabled()'>
|
||||
<span class="fa fa-arrow-right"></span>
|
||||
</button>
|
||||
|
||||
</th>
|
||||
|
||||
<th class="hour" ng-Class="{'even' : ($index % 2==1), 'odd' : ($index % 2==0) }" ng-repeat="time in times track by $index" colspan="{{time.duration}}">
|
||||
{{time.label}}
|
||||
</th>
|
||||
</tr>
|
||||
|
||||
</table>
|
||||
<div class="tv-guide-now" ng-style="{'left': nowPosition + '%'}" ng-show='showNow' ></div>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
@ -165,6 +165,32 @@ module.exports = function ($http) {
|
||||
data: angular.toJson(channel),
|
||||
headers: { 'Content-Type': 'application/json; charset=utf-8' }
|
||||
}).then((d) => { return d.data })
|
||||
}
|
||||
},
|
||||
|
||||
|
||||
/*======================================================================
|
||||
* TV Guide endpoints
|
||||
*/
|
||||
getGuideStatus: async () => {
|
||||
let d = await $http( {
|
||||
method: 'GET',
|
||||
url : '/api/guide/status',
|
||||
headers: { 'Content-Type': 'application/json; charset=utf-8' },
|
||||
} );
|
||||
return d.data;
|
||||
},
|
||||
|
||||
getChannelLineup: async (channelNumber, dateFrom, dateTo) => {
|
||||
let a = dateFrom.toISOString();
|
||||
let b = dateTo.toISOString();
|
||||
let d = await $http( {
|
||||
method: 'GET',
|
||||
url : `/api/guide/channels/${channelNumber}?dateFrom=${a}&dateTo=${b}`,
|
||||
headers: { 'Content-Type': 'application/json; charset=utf-8' },
|
||||
} );
|
||||
return d.data;
|
||||
},
|
||||
|
||||
|
||||
}
|
||||
}
|
||||
Loading…
x
Reference in New Issue
Block a user