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

> Discover how the iptv playlist update command synchronizes GitHub issues with M3U files. Learn about loading data, API fetching, stream parsing, and change persistence.

- Repository: [iptv-org/iptv](https://github.com/iptv-org/iptv)
- Tags: internals
- Published: 2026-02-25

---

**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`](https://github.com/iptv-org/iptv/blob/main/scripts/commands/playlist/update.ts). The script imports several core modules to handle different aspects of the update workflow:

```typescript
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:

```typescript
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`](https://github.com/iptv-org/iptv/blob/main/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:

```typescript
  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`](https://github.com/iptv-org/iptv/blob/main/scripts/core/playlistParser.ts) converts each M3U file into a collection of `Stream` model instances defined in [`scripts/models/stream.ts`](https://github.com/iptv-org/iptv/blob/main/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`:

```typescript
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`:

```typescript
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`:

```typescript
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:

```typescript
  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.