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 definition.

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. On init.

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. Lazy.

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.