Program service
The ProgramService
offers APIs to retrieve program datas to be displayed in a grid. The model returned by this service represents a light view of program datas (only the ones needed to display a grid). It provides API to quickly access program data retrieved at startup or on demand.
A ProgramGrid
, the model returned by ProgramService
, is a list of ProgramList
. A ProgramList
is a list of Section
representing a list of program data for a given period.
Basics
First let’s specify vocabulary and main features.
.
ProgramGrid
ProgramGrid
is a high level model returned by ProgramService
to retrieve program datas for a given date.
Main features are:
- inserting EPG data which are represented in chunks (
Section
) for a given channel, - retrieve a
ProgramList
for a given channel
ProgramList
ProgramList
is a high level model returned by ProgramGrid
to retrieve program datas for a given date and channel. A ProgramList
represents all programs for a channel inside a bound. Programs are given in Section
ordered by date.
Main features are:
- inserting EPG data which are represented in chunks (
Section
), - retrieve a
GridProgram
for a given date
Section
A Section
is a chunk of program datas for a channel. It is bounded with start and end time and all program datas are listed in arrays in order to retrieve them as quick as possible.
Example of section:
{
"channelId": "chanId",
"contentIds": ["c1", "c2"],
"endTime": 1000,
"ids": ["p1", "p2"],
"more": [],
"names": ["Program 1", "Program 2"],
"relativeStarts": [0, 500],
"shortDescriptions": ["Description 1", "Description 2"],
"startTime": 2000,
"tags": []
}
GridProgram
A GridProgram
is a model to display program datas in a grid view. It represents light merged datas of program and related content datas.
Note
May not be used to display a full information page.
Data loading features
Program data are cached in a light straight representation. But cache has to be filled first. ProgramList
is designed to fill data in one Section
with all programs or one Section
for each program. This allows to fully fill program datas in one shot or insert datas as it comes or as it is requested.
Init time
Depending on operator or backend, program data may be fully loaded at startup or at least with big chunks (full day at a time). In this case, data are assumed up to date when requested by the application. It means that if there is no programs for a given date and channel inside the bound, an empty program is retrieved.
.
Lazy
In this case programs cache is fully empty and will be filled on demand (depending on data requested). As backend calls are asynchronous, application will first receive an empty program that will be updated on real data reception.
.
Note
Integration team may keep in mind that the empty program retrieved first may not fully fit with real program (program longer or more than one program in the empty one duration).
Integration
As said before, data loading type and retrieval may depend on integration. To integrate them, you have to implement AbstractProgramList
and AbstractProgramService
. You may also need to implement AbstractGridProgram
if you want to add data to generic grid program details.
Fully fill data (init or first retrieval)
To fully fill ProgramGrid when needed, you’ll need to implement AbstractProgramService
and method _getProgramGrid
.
In the example below, data of current day are retrieved first (synchronous) and then data of next day are retrieved in background (asynchronous):
export default $AbstractProgramService.declare("MyProgramService", {
properties: {
adapter: {class: $Adapter}
},
methods: /** @lends MyProgramService.prototype */ {
/**
* @override
*/
_getProgramGrid: function () {
const now = Date.now();
return this.adapter.getEpgData(now) // retrieve today's data asap
.then(data => {
// retrieve next day data in background
$DateUtils.addDays(now, 1);
this.adapter.getEpgData(now)
.then(nexDayData => {
this._getFromCache("PROGRAMGRID_CACHE_KEY") // we assume it has been created yet
.then(programGrid => {
programGrid.insertSection(nexDayData.events);
});
});
return data.events; // AbstractProgramService will create program grid and insert data
});
}
}
});
Load data on demand
In lazy loading case, most of the logical is owned by implementation of AbstractProgramList
. This is due to the fact that application will first request a program that will trigger a request to backend.
First implementation of AbstractProgramService
need to provide empty data for all channels, otherwise ProgramGrid
won’t be able to retrieve programs for an unknown ProgramList
:
export default $AbstractProgramService.declare("MyProgramService", {
properties: /** @lends MyProgramService.prototype */ {
channelService: { class: $ChannelService}
},
methods: /** @lends MyProgramService.prototype */ {
/**
* @override
*/
_getProgramGrid: function () {
// Create an empty section (that wil never be in conflict with real ones) with a duration of zero for each channel
// It will ends up with a ProgramList for each channel that will be fed on demand
return this.channelService.getChannelList()
.then(channels => channels.map(channel => $ProgramList.createZeroDurationSection(channel.id, 0)));
}
}
});
Then add the lazy loading feature to AbstractProgramList
implementation:
export default $AbstractProgramList.declare("MyProgramList", {
statics: /** @lends MyProgramList */ {
/**
* Create a section with a duration of zero
*
* @param {string} channelId - id of the channel
* @param {number} startTime - start time of the section
* @return {Section}
*/
createZeroDurationSection: function (channelId, startTime) {
let section = this.prototype._createEmptySection.apply({channelId}, [startTime, startTime]);
delete section.__emptyProgramMap;
section.ids = [""];
section.relativeStarts = [0];
return section;
}
},
properties: /** @lends MyProgramList.prototype */ {
adapter: { class: $Adapter}
},
methods: /** @lends MyProgramList.prototype */ {
/**
* @override
*/
getProgramAt: function (timestamp, offset) {
// Overridden to retrieve
if (this.boundingStart != null && this.boundingEnd != null && (timestamp < this.boundingStart || timestamp > this.boundingEnd)) {
return;
}
// try to retrieve program from cache first
const cacheProgram = $AbstractProgramList.prototype.getProgramAt.apply(this, arguments);
if (cacheProgram != null) {
return cacheProgram;
}
if (offset != null && offset !== 0) {
// at least request program is not in cache
// so we have to request the whole programs from the root one to the one targeted by offset
offset = offset - Math.sign(offset);
const rootProgram = this.getProgramAt(timestamp, offset);
if(offset < 0) {
timestamp = rootProgram.start - this.emptyProgramDuration;
} else {
timestamp = rootProgram.start + rootProgram.duration;
}
return this.getProgramAt(timestamp, offset);
}
// No program in cache then we create an empty section of one program waiting for data
const emptySectionStart = Math.trunc(timestamp/1000);
const emptySection = this._createEmptySection(emptySectionStart, emptySectionStart + Math.trunc(this.emptyProgramDuration/1000));
this.insertSection(emptySection);
this.adapter.getEpgData(timestamp, {channelId: this.channelId})
.then(({events}) => {
const requestedProgram = events[0];
let sectionIndex = this._getSectionIndexAt(requestedProgram.startDate);
if(this.sections[sectionIndex] != null && this.statics.isSectionEmpty(this.sections[sectionIndex]) === true) {
const newSection = this.statics.createZeroDurationSection(this.channelId, Math.trunc(requestedProgram.startDate/1000));
newSection.endTime = Math.trunc(requestedProgram.endtDate/1000);
newSection.ids = [requestedProgram.id];
newSection.relativeStarts = [0];
// ... other program data
this.insertSection(newSection);
}
});
return $AbstractProgramList.prototype.getProgramAt.apply(this, arguments);
}
}
});
Note
The previous example is a simple integration assuming the real program will fit the empty one created. In real life, the program may start before the empty one and last longer or smaller than the empty one.