Use Case: Query Electronic Program Guide (EPG)

February 14, 2026 · View on GitHub

This walkthrough demonstrates how to query and display the EPG for a multi-channel TV guide.

Scenario

Build a web-based EPG grid showing what's currently airing and upcoming programs across multiple channels.

Requirements

  • Display current programs for all channels
  • Show upcoming programs (next 4-6 hours)
  • Support time-based navigation (scroll forward/backward)
  • Filter by genre/content type
  • Search programs by title
  • Handle missing EPG data gracefully

Implementation Steps

Step 1: Get Current Programs

Request - All channels, currently airing:

GET /api/epg/events/grid?mode=now&limit=100
Authorization: Basic dXNlcjpwYXNz

Response:

{
  "entries": [
    {
      "eventId": 123456,
      "channelUuid": "ch-bbc-one",
      "channelName": "BBC One HD",
      "channelNumber": 101,
      "start": 1704067200,
      "stop": 1704070800,
      "title": "News at Six",
      "subtitle": "",
      "description": "Latest news and weather",
      "duration": 3600,
      "hd": true
    },
    {
      "eventId": 123457,
      "channelUuid": "ch-bbc-two",
      "channelName": "BBC Two HD",
      "channelNumber": 102,
      "start": 1704067200,
      "stop": 1704069000,
      "title": "University Challenge",
      "subtitle": "Episode 12",
      "description": "Quiz show",
      "duration": 1800
    }
  ],
  "total": 85
}

Step 2: Get Programs for Time Window

Request - 6-hour window:

GET /api/epg/events/grid?channel=ch-bbc-one&sort=start&dir=ASC&limit=20
Authorization: Basic dXNlcjpwYXNz

With filter for time range:

{
  "filter": [
    { "field": "start", "type": "numeric", "value": 1704067200, "comparison": "gte" },
    { "field": "stop", "type": "numeric", "value": 1704088800, "comparison": "lte" }
  ]
}

Step 3: Search Programs

Search by title:

GET /api/epg/events/grid?filter=documentary&fulltext=1&limit=50
Authorization: Basic dXNlcjpwYXNz

Filter by genre/content type:

GET /api/epg/events/grid?contentType=16&limit=50
Authorization: Basic dXNlcjpwYXNz

Complete Example Code

JavaScript - EPG Grid Component

class EPGGrid {
  constructor(apiUrl, username, password) {
    this.apiUrl = apiUrl;
    this.auth = btoa(`${username}:${password}`);
    this.channels = [];
    this.currentTime = Math.floor(Date.now() / 1000);
    this.timeWindow = 6 * 3600; // 6 hours
  }

  async initialize() {
    // Load channels first
    this.channels = await this.fetchChannels();

    // Load EPG for each channel
    await this.loadEPG();
  }

  async fetchChannels() {
    const response = await fetch(`${this.apiUrl}/api/channel/grid?sort=number&dir=ASC&limit=0`, {
      headers: { Authorization: `Basic ${this.auth}` },
    });

    const data = await response.json();
    return data.entries.filter((ch) => ch.enabled);
  }

  async loadEPG() {
    const startTime = this.currentTime;
    const endTime = this.currentTime + this.timeWindow;

    const filter = JSON.stringify([
      { field: 'start', type: 'numeric', value: startTime, comparison: 'gte' },
      { field: 'stop', type: 'numeric', value: endTime, comparison: 'lte' },
    ]);

    const response = await fetch(`${this.apiUrl}/api/epg/events/grid?filter=${encodeURIComponent(filter)}&limit=0`, {
      headers: { Authorization: `Basic ${this.auth}` },
    });

    const data = await response.json();

    // Group events by channel
    this.epgData = this.groupByChannel(data.entries);
  }

  groupByChannel(events) {
    const grouped = {};

    for (const event of events) {
      if (!grouped[event.channelUuid]) {
        grouped[event.channelUuid] = [];
      }
      grouped[event.channelUuid].push(event);
    }

    // Sort events by start time within each channel
    for (const channelUuid in grouped) {
      grouped[channelUuid].sort((a, b) => a.start - b.start);
    }

    return grouped;
  }

  render(containerId) {
    const container = document.getElementById(containerId);
    container.innerHTML = '';

    // Create time header
    const timeHeader = this.createTimeHeader();
    container.appendChild(timeHeader);

    // Create channel rows
    for (const channel of this.channels) {
      const row = this.createChannelRow(channel);
      container.appendChild(row);
    }

    // Add scrolling and interaction handlers
    this.setupInteraction(container);
  }

  createTimeHeader() {
    const header = document.createElement('div');
    header.className = 'epg-time-header';

    const startTime = this.currentTime;
    const hours = 6;

    for (let i = 0; i < hours; i++) {
      const hourTime = startTime + i * 3600;
      const hourEl = document.createElement('div');
      hourEl.className = 'time-marker';
      hourEl.textContent = new Date(hourTime * 1000).toLocaleTimeString([], {
        hour: '2-digit',
        minute: '2-digit',
      });
      header.appendChild(hourEl);
    }

    return header;
  }

  createChannelRow(channel) {
    const row = document.createElement('div');
    row.className = 'epg-channel-row';

    // Channel info
    const channelInfo = document.createElement('div');
    channelInfo.className = 'channel-info';
    channelInfo.innerHTML = `
      <span class="channel-number">${channel.number}</span>
      <span class="channel-name">${channel.name}</span>
    `;
    row.appendChild(channelInfo);

    // Program blocks
    const programsContainer = document.createElement('div');
    programsContainer.className = 'programs-container';

    const events = this.epgData[channel.uuid] || [];

    for (const event of events) {
      const programBlock = this.createProgramBlock(event);
      programsContainer.appendChild(programBlock);
    }

    // Handle empty EPG
    if (events.length === 0) {
      const placeholder = document.createElement('div');
      placeholder.className = 'no-epg-data';
      placeholder.textContent = 'No program information';
      programsContainer.appendChild(placeholder);
    }

    row.appendChild(programsContainer);

    return row;
  }

  createProgramBlock(event) {
    const block = document.createElement('div');
    block.className = 'program-block';
    block.dataset.eventId = event.eventId;

    // Calculate width based on duration
    const duration = event.stop - event.start;
    const width = (duration / 3600) * 100; // 100px per hour
    block.style.width = `${width}px`;

    // Calculate position based on start time
    const offset = event.start - this.currentTime;
    const left = (offset / 3600) * 100;
    block.style.left = `${left}px`;

    // Content
    const now = Math.floor(Date.now() / 1000);
    const isLive = event.start <= now && event.stop > now;

    block.innerHTML = `
      ${isLive ? '<span class="live-indicator">LIVE</span>' : ''}
      <div class="program-time">
        ${this.formatTime(event.start)} - ${this.formatTime(event.stop)}
      </div>
      <div class="program-title">${event.title}</div>
      ${event.subtitle ? `<div class="program-subtitle">${event.subtitle}</div>` : ''}
    `;

    if (isLive) {
      block.classList.add('live');
    }

    // Add click handler
    block.addEventListener('click', () => this.onProgramClick(event));

    return block;
  }

  formatTime(timestamp) {
    return new Date(timestamp * 1000).toLocaleTimeString([], {
      hour: '2-digit',
      minute: '2-digit',
    });
  }

  setupInteraction(container) {
    // Current time indicator
    this.updateCurrentTimeIndicator(container);
    setInterval(() => this.updateCurrentTimeIndicator(container), 60000);

    // Scroll to current time
    this.scrollToNow(container);
  }

  updateCurrentTimeIndicator(container) {
    let indicator = container.querySelector('.current-time-indicator');

    if (!indicator) {
      indicator = document.createElement('div');
      indicator.className = 'current-time-indicator';
      container.appendChild(indicator);
    }

    const now = Math.floor(Date.now() / 1000);
    const offset = ((now - this.currentTime) / 3600) * 100;
    indicator.style.left = `${offset}px`;
  }

  scrollToNow(container) {
    const now = Math.floor(Date.now() / 1000);
    const offset = ((now - this.currentTime) / 3600) * 100;
    container.scrollLeft = Math.max(0, offset - 200); // Center-ish
  }

  onProgramClick(event) {
    console.log('Clicked program:', event);

    // Show program details modal
    this.showProgramDetails(event.eventId);
  }

  async showProgramDetails(eventId) {
    // Load full event details
    const response = await fetch(`${this.apiUrl}/api/epg/events/load`, {
      method: 'POST',
      headers: {
        Authorization: `Basic ${this.auth}`,
        'Content-Type': 'application/json',
      },
      body: JSON.stringify({ eventId }),
    });

    const data = await response.json();
    const event = data.entries[0];

    // Display in modal (implement your modal UI)
    this.displayModal(event);
  }

  displayModal(event) {
    const modal = document.createElement('div');
    modal.className = 'epg-modal';
    modal.innerHTML = `
      <div class="modal-content">
        <h2>${event.title}</h2>
        ${event.subtitle ? `<h3>${event.subtitle}</h3>` : ''}
        <p class="program-time">
          ${this.formatTime(event.start)} - ${this.formatTime(event.stop)}
          on ${event.channelName}
        </p>
        <p class="program-description">${event.description || 'No description available'}</p>
        <div class="modal-actions">
          <button onclick="recordingManager.scheduleRecording(${event.eventId})">
            ⏺ Record
          </button>
          <button onclick="this.closest('.epg-modal').remove()">Close</button>
        </div>
      </div>
    `;

    document.body.appendChild(modal);

    // Close on background click
    modal.addEventListener('click', (e) => {
      if (e.target === modal) {
        modal.remove();
      }
    });
  }

  // Navigation methods
  async navigateForward() {
    this.currentTime += this.timeWindow;
    await this.loadEPG();
    this.render('epg-grid');
  }

  async navigateBackward() {
    this.currentTime -= this.timeWindow;
    await this.loadEPG();
    this.render('epg-grid');
  }

  async jumpToNow() {
    this.currentTime = Math.floor(Date.now() / 1000);
    await this.loadEPG();
    this.render('epg-grid');
  }
}

// Usage
const epg = new EPGGrid('http://localhost:9981', 'api-user', 'password');

await epg.initialize();
epg.render('epg-grid');

// Navigation buttons
document.getElementById('btn-backward').addEventListener('click', () => epg.navigateBackward());
document.getElementById('btn-forward').addEventListener('click', () => epg.navigateForward());
document.getElementById('btn-now').addEventListener('click', () => epg.jumpToNow());

Python - EPG Data Export

import requests
import json
from datetime import datetime, timedelta

class EPGExporter:
    def __init__(self, api_url: str, username: str, password: str):
        self.api_url = api_url
        self.auth = (username, password)

    def get_daily_schedule(self, channel_uuid: str, date: datetime):
        """Get full day schedule for a channel"""
        start_time = int(date.replace(hour=0, minute=0, second=0).timestamp())
        end_time = int(date.replace(hour=23, minute=59, second=59).timestamp())

        filter_obj = [
            {"field": "start", "type": "numeric", "value": start_time, "comparison": "gte"},
            {"field": "stop", "type": "numeric", "value": end_time, "comparison": "lte"}
        ]

        response = requests.get(
            f'{self.api_url}/api/epg/events/grid',
            auth=self.auth,
            params={
                'channel': channel_uuid,
                'filter': json.dumps(filter_obj),
                'sort': 'start',
                'dir': 'ASC',
                'limit': 0
            }
        )

        response.raise_for_status()
        return response.json()['entries']

    def export_week_schedule(self, channel_uuid: str, output_file: str):
        """Export week schedule to JSON"""
        today = datetime.now().replace(hour=0, minute=0, second=0, microsecond=0)

        week_schedule = {}

        for day_offset in range(7):
            date = today + timedelta(days=day_offset)
            day_key = date.strftime('%Y-%m-%d')

            print(f"Fetching schedule for {day_key}...")
            events = self.get_daily_schedule(channel_uuid, date)

            week_schedule[day_key] = [
                {
                    'start': datetime.fromtimestamp(e['start']).isoformat(),
                    'stop': datetime.fromtimestamp(e['stop']).isoformat(),
                    'title': e['title'],
                    'subtitle': e.get('subtitle'),
                    'description': e.get('description')
                }
                for e in events
            ]

        with open(output_file, 'w', encoding='utf-8') as f:
            json.dump(week_schedule, f, indent=2, ensure_ascii=False)

        print(f"Exported to {output_file}")

    def find_prime_time_movies(self):
        """Find movies airing in prime time (20:00-23:00) this week"""
        movies = []

        for day_offset in range(7):
            date = datetime.now() + timedelta(days=day_offset)
            start_time = int(date.replace(hour=20, minute=0, second=0).timestamp())
            end_time = int(date.replace(hour=23, minute=0, second=0).timestamp())

            filter_obj = [
                {"field": "contentType", "type": "numeric", "value": 16, "comparison": "eq"},
                {"field": "start", "type": "numeric", "value": start_time, "comparison": "gte"},
                {"field": "start", "type": "numeric", "value": end_time, "comparison": "lt"}
            ]

            response = requests.get(
                f'{self.api_url}/api/epg/events/grid',
                auth=self.auth,
                params={
                    'filter': json.dumps(filter_obj),
                    'limit': 0
                }
            )

            response.raise_for_status()
            movies.extend(response.json()['entries'])

        return movies

# Usage
exporter = EPGExporter(
    'http://localhost:9981',
    'api-user',
    'password'
)

# Export week schedule
exporter.export_week_schedule('channel-uuid-bbc-one', 'bbc_one_week.json')

# Find prime time movies
movies = exporter.find_prime_time_movies()
print(f"Found {len(movies)} movies in prime time this week")

Advanced Features

Genre Filter

async function filterByGenre(genreCode) {
  const response = await fetch(`${apiUrl}/api/epg/events/grid?contentType=${genreCode}&limit=100`, {
    headers: { Authorization: `Basic ${auth}` },
  });

  return await response.json();
}

// Common genres
const GENRES = {
  MOVIE: 16,
  NEWS: 32,
  SHOW: 48,
  SPORTS: 64,
  CHILDREN: 80,
  MUSIC: 96,
};

// Get all sports programs
const sports = await filterByGenre(GENRES.SPORTS);

Search with Highlighting

async function searchPrograms(query) {
  const response = await fetch(
    `${apiUrl}/api/epg/events/grid?filter=${encodeURIComponent(query)}&fulltext=1&limit=50`,
    {
      headers: { Authorization: `Basic ${auth}` },
    },
  );

  const data = await response.json();

  // Add highlights
  return data.entries.map((event) => ({
    ...event,
    titleHighlighted: highlightText(event.title, query),
    descriptionHighlighted: highlightText(event.description, query),
  }));
}

function highlightText(text, query) {
  if (!text) return '';

  const regex = new RegExp(`(${query})`, 'gi');
  return text.replace(regex, '<mark>\$1</mark>');
}

CSS Styling

.epg-grid {
  display: flex;
  flex-direction: column;
  overflow-x: auto;
  overflow-y: auto;
  height: 600px;
  position: relative;
}

.epg-channel-row {
  display: flex;
  border-bottom: 1px solid #ddd;
  min-height: 60px;
}

.channel-info {
  width: 150px;
  padding: 10px;
  background: #f5f5f5;
  border-right: 1px solid #ddd;
  position: sticky;
  left: 0;
  z-index: 2;
}

.programs-container {
  position: relative;
  flex: 1;
  min-width: 600px;
}

.program-block {
  position: absolute;
  height: 50px;
  background: #e0e0e0;
  border: 1px solid #ccc;
  padding: 5px;
  cursor: pointer;
  overflow: hidden;
  transition: background 0.2s;
}

.program-block:hover {
  background: #d0d0d0;
  z-index: 10;
}

.program-block.live {
  background: #4caf50;
  color: white;
}

.current-time-indicator {
  position: absolute;
  width: 2px;
  height: 100%;
  background: red;
  z-index: 100;
  pointer-events: none;
}