dizquetv/src/services/tv-guide-service.js
Rafael Vieira 5913b7b3e9 feat(server): add i18n for translation of api
Implementation of i18n system to API translation. This change use i18next lib, simple and
extensible internationalization-framework, with support to plugins.

- out of router: use passing i18next on index.js for any class. Look tv-guide-service.js
- on a route: use req.t(). look api.js
2021-07-06 01:00:39 -03:00

518 lines
17 KiB
JavaScript

const constants = require("../constants");
const FALLBACK_ICON = "https://raw.githubusercontent.com/vexorain/dizquetv/main/resources/dizquetv.png";
const throttle = require('./throttle');
class TVGuideService
{
/****
*
**/
constructor(xmltv, db, cacheImageService, eventService, i18next) {
this.cached = null;
this.lastUpdate = 0;
this.updateTime = 0;
this.currentUpdate = -1;
this.currentLimit = -1;
this.currentChannels = null;
this.xmltv = xmltv;
this.db = db;
this.cacheImageService = cacheImageService;
this.eventService = eventService;
this._throttle = throttle;
this.i18next = i18next;
}
async get() {
while (this.cached == null) {
await _wait(100);
}
return this.cached;
}
prepareRefresh(inputChannels, limit) {
let t = (new Date()).getTime();
this.updateTime = t;
this.updateLimit = t + limit;
let channels = inputChannels;
this.updateChannels = channels;
return t;
}
async refresh(t) {
while( this.lastUpdate < t) {
if (this.currentUpdate == -1) {
this.currentUpdate = this.updateTime;
this.currentLimit = this.updateLimit;
this.currentChannels = this.updateChannels;
let t = "" + ( (new Date()) );
eventService.push(
"xmltv",
{
"message": `Started building tv-guide at = ${t}`,
"module" : "xmltv",
"detail" : {
"time": new Date(),
},
"level" : "info"
}
);
await this.buildIt();
}
await _wait(100);
}
return await this.get();
}
async makeAccumulated(channel) {
if (typeof(channel.programs) === 'undefined') {
throw Error( JSON.stringify(channel).slice(0,200) );
}
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.index !== -1)
&& (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 == null) || (typeof(playing) === 'undefined') ) {
console.log("There is a weird issue with the TV guide generation. A placeholder program is placed to prevent further issues. Please report this.");
playing = {
index: -1,
program: {
isOffline: true,
duration: 30*60*1000,
},
start: 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) {
if (typeof(channel) === 'undefined') {
throw Error("Couldn't find 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 melded = 0;
let push = async (x) => {
await this._throttle();
if (
(programs.length > 0)
&& isProgramFlex(x.program, channel)
&& (
(x.program.duration <= constants.TVGUIDE_MAXIMUM_PADDING_LENGTH_MS)
|| isProgramFlex(programs[ programs.length - 1].program, channel)
)
) {
//meld with previous
let y = clone( programs[ programs.length - 1] );
y.program.duration += x.program.duration;
melded += x.program.duration;
if (
(melded > constants.TVGUIDE_MAXIMUM_PADDING_LENGTH_MS)
&& !isProgramFlex(programs[ programs.length - 1].program, channel)
) {
y.program.duration -= melded;
programs[ programs.length - 1] = y;
if (y.start + y.program.duration < t1) {
programs.push( {
start: y.start + y.program.duration,
program: {
isOffline : true,
duration: melded,
},
} );
}
melded = 0;
} else {
programs[ programs.length - 1] = y;
}
} else if (isProgramFlex(x.program, channel) ) {
melded = 0;
programs.push( {
start: x.start,
program: {
isOffline : true,
duration: x.program.duration,
},
} );
} else {
melded = 0;
programs.push(x);
}
};
while (x.start < t1) {
await push(x);
let t2 = x.start + x.program.duration;
x = await this.getChannelPlaying(channel, x, t2);
if (x.start < t2) {
let d = t2 - x.start;
x.start = t2;
x.program = clone(x.program);
x.program.duration -= d;
}
if (x.program.duration == 0) {
console.error("There's a program with duration 0?");
}
}
result.programs = [];
for (let i = 0; i < programs.length; i++) {
await this._throttle();
if (isProgramFlex( programs[i].program, channel) ) {
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 {
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) {
let channel = {
name: "dizqueTV",
icon: FALLBACK_ICON,
}
result[1] = {
channel : channel,
programs: [
makeEntry(
channel
, {
start: t0 - t0 % (30 * 60*1000),
program: {
duration: 24*60*60*1000,
icon: FALLBACK_ICON,
showTitle: this.i18next.t("tvGuide.no_channels"),
date: formatDateYYYYMMDD(new Date()),
summary : this.i18next.t("tvGuide.no_channels_summary")
}
} )
]
}
} else {
for (let i = 0; i < channels.length; i++) {
if(! channels[i].stealth) {
let programs = await this.getChannelPrograms(t0, t1, channels[i] );
result[ channels[i].number ] = programs;
}
}
}
return result;
}
async buildIt() {
try {
this.cached = await this.buildItManaged();
console.log("Internal TV Guide data refreshed at " + (new Date()).toLocaleString() );
await this.refreshXML();
} catch(err) {
console.error("Unable to update internal guide data", err);
await _wait(100);
console.error("Retrying TV guide...");
await this.buildIt();
} finally {
this.lastUpdate = this.currentUpdate;
this.currentUpdate = -1;
}
}
async refreshXML() {
let xmltvSettings = this.db['xmltv-settings'].find()[0];
await this.xmltv.WriteXMLTV(this.cached, xmltvSettings, async() => await this._throttle(), this.cacheImageService);
let t = "" + ( (new Date()) );
eventService.push(
"xmltv",
{
"message": this.i18next.t("tvGuide.xmltv_updated", {t}),
"module" : "xmltv",
"detail" : {
"time": new Date(),
},
"level" : "info"
}
);
}
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 getChannelStealthDuration(channel) {
if (
(typeof(channel.guideMinimumDurationSeconds) !== 'undefined')
&&
! isNaN(channel.guideMinimumDurationSeconds)
) {
return channel.guideMinimumDurationSeconds * 1000;
} else {
return constants.DEFAULT_GUIDE_STEALTH_DURATION;
}
}
function isProgramFlex(program, channel) {
return program.isOffline || program.duration <= getChannelStealthDuration(channel)
}
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, channel)) {
if ( (typeof(channel.guideFlexPlaceholder) === 'string')
&& channel.guideFlexPlaceholder !== "") {
title = channel.guideFlexPlaceholder;
} else {
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,
}
}
}
if (typeof(title)==='undefined') {
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,
}
}
function formatDateYYYYMMDD(date) {
var year = date.getFullYear().toString();
var month = (date.getMonth() + 101).toString().substring(1);
var day = (date.getDate() + 100).toString().substring(1);
return year + "-" + month + "-" + day;
}
module.exports = TVGuideService