How the IPTV Playlist Update Command Works in iptv-org/iptv

The playlist:update command automatically synchronizes approved GitHub issues with local M3U playlist files by loading issue data, fetching the IPTV-ORG API, parsing existing streams, and applying add, edit, or remove operations before persisting changes to disk.

The playlist:update command serves as the automation backbone for the iptv-org/iptv repository, transforming labeled GitHub issues into concrete modifications of the project's IPTV M3U playlists. This TypeScript utility orchestrates data from the IPTV-ORG API, local file storage, and community contributions to maintain thousands of stream entries across hundreds of country-specific playlist files.

Command Initialization and Dependencies

The command entry point resides in scripts/commands/playlist/update.ts. The script imports several core modules to handle different aspects of the update workflow:

import { IssueLoader, PlaylistParser } from '../../core'
import { Playlist, Issue, Stream } from '../../models'
import { loadData, data as apiData } from '../../api'
import { Logger, Collection } from '@freearhey/core'
import { Storage } from '@freearhey/storage-js'
import { STREAMS_DIR } from '../../constants'
import * as sdk from '@iptv-org/sdk'
import { isURI } from '../../utils'

IssueLoader handles GitHub issue retrieval, PlaylistParser reads M3U files into Stream objects, and loadData fetches the latest channel metadata from the IPTV-ORG API.

Phase 1: Loading Issues and API Data

The main function orchestrates the update pipeline through six distinct phases. First, it loads open GitHub issues and API data:

async function main() {
  const logger = new Logger({ level: -999 })
  const issueLoader = new IssueLoader()

  logger.info('loading issues...')
  const issues = await issueLoader.load()

  logger.info('loading data from api...')
  await loadData()

The IssueLoader.load() method in scripts/core/issueLoader.ts fetches issues labeled for playlist modifications. Simultaneously, loadData() populates the apiData object with current channel, feed, and country information from the IPTV-ORG API.

Phase 2: Parsing Existing Stream Files

Next, the command loads all existing M3U playlists from the streams/ directory:

  logger.info('loading streams...')
  const streamsStorage = new Storage(STREAMS_DIR)
  const parser = new PlaylistParser({ storage: streamsStorage })
  const files = await streamsStorage.list('**/*.m3u')
  const streams = await parser.parse(files)

PlaylistParser in scripts/core/playlistParser.ts converts each M3U file into a collection of Stream model instances defined in scripts/models/stream.ts. These objects track metadata such as URL, channel ID, feed ID, and removal status.

Phase 3: Processing Stream Removals

The removeStreams function handles deletion requests from issues labeled streams:remove and approved:

async function removeStreams({ streams, issues }) {
  const requests = issues.filter(
    issue => issue.labels.includes('streams:remove') && issue.labels.includes('approved')
  )

  requests.forEach((issue: Issue) => {
    const data = issue.data
    if (data.missing('stream_url')) return

    const streamUrls = data.getString('stream_url') || ''
    
    streamUrls
      .split(/\r?\n/)
      .filter(Boolean)
      .forEach(link => {
        const found: Stream = streams.first((_stream: Stream) => _stream.url === link.trim())
        if (found) {
          found.removed = true
        }
      })
  })
}

When a matching URL is found in the existing collection, the function sets stream.removed = true. The persistence layer later filters out these marked streams before writing files back to disk.

Phase 4: Editing Stream Metadata

The editStreams function processes modification requests from issues labeled streams:edit and approved:

async function editStreams({ streams, issues }) {
  const requests = issues.filter(
    issue => issue.labels.includes('streams:edit') && issue.labels.includes('approved')
  )
  requests.forEach((issue: Issue) => {
    const data = issue.data
    const stream: Stream = streams.first(
      (_stream: Stream) => _stream.url === data.getString('stream_url')
    )
    if (!stream) return

    const streamId = data.getString('stream_id') || ''
    const [channelId, feedId] = streamId.split('@')

    if (channelId) {
      stream.channel = channelId
      stream.feed = feedId
      stream.updateTvgId().updateTitle().updateFilepath()
    }

    stream.updateWithIssue(data)
  })
}

When a stream_id (format CHANNEL@FEED) is provided, the function updates the channel and feed associations, then invokes three mutating helpers on the Stream object:

  • updateTvgId() – recomputes the tvg-id attribute based on the new channel data
  • updateTitle() – rebuilds the display title using the channel name and feed information
  • updateFilepath() – updates the target playlist file based on the channel's country

The updateWithIssue(data) method applies additional metadata such as label, quality, HTTP user-agent, and referrer.

Phase 5: Adding New Streams

The addStreams function handles creation requests from issues labeled streams:add and approved:

async function addStreams({ streams, issues }) {
  const requests = issues.filter(
    issue => issue.labels.includes('streams:add') && issue.labels.includes('approved')
  )
  requests.forEach((issue: Issue) => {
    const data = issue.data
    if (data.missing('stream_id') || data.missing('stream_url')) return
    if (streams.includes((_stream: Stream) => _stream.url === data.getString('stream_url'))) return
    const streamUrl = data.getString('stream_url') || ''
    if (!isURI(streamUrl)) return

    const streamId = data.getString('stream_id') || ''
    const [channelId, feedId] = streamId.split('@')
    const channel: sdk.Models.Channel | undefined = apiData.channelsKeyById.get(channelId)
    if (!channel) return

    const stream = new Stream({
      channel: channelId,
      feed: feedId,
      title: channel.name,
      url: streamUrl,
      user_agent: data.getString('http_user_agent'),
      referrer: data.getString('http_referrer'),
      quality: data.getString('quality')
    })

    stream.label = data.getString('label') || ''
    stream.updateTitle().updateFilepath()

    streams.add(stream)
  })
}

This function performs strict validation before creating new entries:

  1. Verifies required fields (stream_id, stream_url) exist
  2. Confirms the URL does not already exist in the current collection to prevent duplicates
  3. Validates the URL format using isURI()
  4. Verifies that the stream_id references a valid channel in the IPTV-ORG API data (apiData.channelsKeyById)

Upon validation, it instantiates a new Stream object, sets the label, updates the title and file path, and adds it to the collection.

Phase 6: Persisting Updated Playlists

After processing all mutations, the command writes the updated playlists back to the repository:

  logger.info('saving...')
  const groupedStreams = streams.groupBy((stream: Stream) => stream.getFilepath())
  for (const filepath of groupedStreams.keys()) {
    let streams = new Collection(groupedStreams.get(filepath))
    streams = streams.filter((stream: Stream) => stream.removed === false)

    const playlist = new Playlist(streams, { public: false })
    await streamsStorage.save(filepath, playlist.toString())
  }

  const output = processedIssues.map(issue_number => `closes #${issue_number}`).join(', ')
  console.log(`OUTPUT=${output}`)

The persistence logic performs three critical operations:

  1. Groups streams by target file using stream.getFilepath(), which returns paths like us.m3u or uk.m3u based on channel country codes
  2. Filters out removed streams by checking stream.removed === false before creating the playlist
  3. Formats and saves using the Playlist model's toString() method to generate valid M3U content, then writes via streamsStorage.save()

The final output line (OUTPUT=closes #...) provides a machine-readable list of processed issues for CI/CD pipelines to automatically close the corresponding GitHub issues.

Summary

  • The iptv playlist update command automates playlist maintenance by synchronizing approved GitHub issues with local M3U files in the iptv-org/iptv repository.
  • The command executes in six phases: loading issues and API data, parsing existing streams, removing dead links, editing metadata, adding new streams, and persisting updated playlists.
  • Stream removal marks entries with removed = true, filtering them out during the save phase rather than deleting immediately.
  • Stream editing updates channel associations, recalculates TVG IDs, titles, and file paths using updateTvgId(), updateTitle(), and updateFilepath().
  • Stream addition validates URLs, checks for duplicates, verifies channel existence in the API, and instantiates new Stream objects with proper metadata.
  • The script outputs OUTPUT=closes #... to enable automated issue closure via CI/CD workflows.

Frequently Asked Questions

How do I run the iptv playlist update command locally?

To execute the playlist update command on your local machine, first install the project dependencies with npm install, then run npm run playlist:update. The script requires environment variables for GitHub API access (or uses test fixtures if configured) and writes updated M3U files to the streams/ directory. The command outputs log messages for each phase and prints a final OUTPUT=closes #... line indicating which issues were processed.

What labels are required for GitHub issues to be processed by the update command?

The command only processes issues that have both an action label and the approved label. Valid action labels include streams:add for new entries, streams:edit for metadata modifications, and streams:remove for deletions. Issues missing the approved label are ignored during execution, ensuring that only vetted community requests modify the playlist files. This two-label system prevents unauthorized changes to the repository.

How does the command determine which M3U file to update when adding or editing a stream?

The Stream model calculates the target file path using the updateFilepath() method, which derives the filename from the channel's country code (e.g., us.m3u for United States channels or gb.m3u for United Kingdom channels). When editing a stream's channel association, the method recalculates the path, potentially moving the entry to a different country file if the channel's country metadata changes. This ensures streams are always organized in the correct country-specific playlist.

What validation occurs before a new stream is added to the playlists?

Before creating a new stream entry, the addStreams function performs four validation checks: it verifies that required fields (stream_id and stream_url) exist in the issue data, confirms the URL is not already present in the current collection to prevent duplicates, validates the URL format using the isURI() utility, and checks that the channel_id portion of the stream_id exists in the IPTV-ORG API data (apiData.channelsKeyById). If any validation fails, the issue is skipped without modifying the playlists.

Have a question about this repo?

These articles cover the highlights, but your codebase questions are specific. Give your agent direct access to the source. Share this with your agent to get started:

Share the following with your agent to get started:
curl -s "https://instagit.com/install.md"

Works with
Claude Codex Cursor VS Code OpenClaw Any MCP Client

Maintain an open-source project? Get it listed too →