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 thetvg-idattribute based on the new channel dataupdateTitle()– rebuilds the display title using the channel name and feed informationupdateFilepath()– 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:
- Verifies required fields (
stream_id,stream_url) exist - Confirms the URL does not already exist in the current collection to prevent duplicates
- Validates the URL format using
isURI() - Verifies that the
stream_idreferences 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:
- Groups streams by target file using
stream.getFilepath(), which returns paths likeus.m3uoruk.m3ubased on channel country codes - Filters out removed streams by checking
stream.removed === falsebefore creating the playlist - Formats and saves using the
Playlistmodel'stoString()method to generate valid M3U content, then writes viastreamsStorage.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(), andupdateFilepath(). - Stream addition validates URLs, checks for duplicates, verifies channel existence in the API, and instantiates new
Streamobjects 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:
curl -s "https://instagit.com/install.md" Maintain an open-source project? Get it listed too →