How are channel logos resolved and embedded in M3U8 metadata for IPTV?

Channel logos in the Free-TV/IPTV project are resolved by extracting the src attribute from HTML <img> tags embedded in Markdown tables and embedded directly into the M3U8 playlist as the tvg-logo attribute without downloading or validating the images.

The Free-TV/IPTV repository generates standardized IPTV playlists from human-readable Markdown tables. Understanding how channel logos are resolved and embedded in M3U8 metadata requires examining the make_playlist.py script, which transforms these tables into structured playlist files that IPTV players can consume.

How Logo Resolution Works in IPTV Playlist Generation

The playlist generation process involves three distinct steps to handle channel logos: parsing the Markdown source, extracting the image URL, and injecting it into the M3U8 metadata.

Parsing the Markdown Source

Each channel entry originates from a Markdown file stored in the lists/ directory (e.g., usa.md, spain.md). These files contain pipe-separated tables where the Logo column holds an HTML <img> tag.

A typical table row looks like this:

| 1 | Buzzr Ⓖ | [>](https://buzzrota-ono.amagi.tv/playlist1080.m3u8) | <img height="20" src="https://upload.wikimedia.org/wikipedia/commons/thumb/d/d6/Buzzr_logo.svg/768px-Buzzr_logo.svg.png"/> | Buzzr.us

The Channel class in make_playlist.py processes these rows by splitting the line on pipe characters (|). The logo data resides in the fourth field (parts[4]), which contains the raw HTML tag.

Extracting the Logo URL

The script performs string slicing to extract the URL from the HTML tag rather than using an HTML parser. According to the source code in make_playlist.py (lines 94-106), this implementation uses find operations to locate the src attribute:


# From make_playlist.py - Channel class initialization

self.logo = parts[4].strip()

# Extract URL from <img src="..."> tag

self.logo = self.logo[self.logo.find('src="')+5:self.logo.rfind('"')]

This approach extracts the URL between the quotes of the src attribute, converting <img height="20" src="https://example.com/logo.png"/> into https://example.com/logo.png.

Embedding into M3U8 Metadata

Once extracted, the URL is embedded into the M3U8 playlist using the tvg-logo attribute. In make_playlist.py (lines 115-121), the to_m3u_line() method constructs the #EXTINF directive:


# Constructing the EXTINF line with tvg-logo attribute

f'#EXTINF:-1 tvg-name="{self.name}" tvg-logo="{self.logo}" tvg-chno="{self.number}" group-title="{self.group}",{self.name}'

The resulting output in the playlist file appears as:

#EXTINF:-1 tvg-name="Buzzr Ⓖ" tvg-logo="https://upload.wikimedia.org/wikipedia/commons/thumb/d/d6/Buzzr_logo.svg/768px-Buzzr_logo.svg.png" tvg-chno="1" group-title="USA",Buzzr Ⓖ
https://buzzrota-ono.amagi.tv/playlist1080.m3u8

Code Implementation Details

The logo resolution mechanism relies on simple string manipulation rather than external HTTP requests or image processing libraries.

Extracting Logo URLs from Markdown

Here is the extraction logic as implemented in the repository:

def extract_logo_from_markdown(md_line):
    """Extract logo URL from a Markdown table row"""
    parts = md_line.split('|')
    raw_logo = parts[4].strip()  # Get the <img> tag field

    
    # Extract src attribute using string slicing

    start = raw_logo.find('src="') + 5
    end = raw_logo.rfind('"')
    logo_url = raw_logo[start:end]
    
    return logo_url

# Example usage

md_line = '| 1 | Buzzr Ⓖ | [>](https://buzzrota-ono.amagi.tv/playlist1080.m3u8) | <img height="20" src="https://upload.wikimedia.org/wikipedia/commons/thumb/d/d6/Buzzr_logo.svg/768px-Buzzr_logo.svg.png"/> | Buzzr.us'
print(extract_logo_from_markdown(md_line))

# Output: https://upload.wikimedia.org/wikipedia/commons/thumb/d/d6/Buzzr_logo.svg/768px-Buzzr_logo.svg.png

Building M3U8 Entries with Logo Metadata

The following function demonstrates how the final M3U8 entry is constructed with the tvg-logo attribute:

def to_m3u_line(name, url, logo, chno=None, group=''):
    """Build an M3U8 entry with embedded logo metadata"""
    parts = [f'#EXTINF:-1 tvg-name="{name}" tvg-logo="{logo}"']
    
    if chno:
        parts.append(f' tvg-chno="{chno}"')
    parts.append(f' group-title="{group}",{name}')
    
    header = ''.join(parts)
    return f'{header}\n{url}'

Repository Structure and Key Files

The logo resolution workflow depends on these specific files within the Free-TV/IPTV repository:

  • make_playlist.py – The core Python script that parses Markdown tables, extracts logo URLs using string slicing, and generates the final M3U8 playlists. The critical logic resides in the Channel class (lines 94-106 for parsing, lines 115-121 for M3U8 generation).

  • lists/*.md – Country-specific Markdown files containing channel definitions. Each row includes an HTML <img> tag in the Logo column pointing to external image URLs (e.g., Imgur, Wikimedia Commons).

  • playlists/ – The output directory containing generated .m3u8 files. These files contain the processed tvg-logo attributes that IPTV players use to display channel thumbnails.

Summary

  • Logo resolution in the Free-TV/IPTV project extracts image URLs from HTML <img> tags embedded in Markdown table rows using string slicing operations.
  • The extraction occurs in make_playlist.py (lines 94-106) by parsing the src attribute from the fourth column of pipe-separated Markdown tables.
  • No image validation or downloading occurs during playlist generation; the script only copies the external URL into the M3U8 metadata.
  • The tvg-logo attribute is injected into #EXTINF directives (lines 115-121 of make_playlist.py), making logos available to any IPTV player that supports standard M3U8 attributes.
  • This approach works uniformly across all country-specific lists in the lists/ directory because they follow the same table schema.

Frequently Asked Questions

Why does the script use string slicing instead of an HTML parser to extract logos?

The script uses string slicing (find('src="') and rfind('"')) because the Markdown table format is highly predictable and consistent across all list files. This approach avoids dependencies on external HTML parsing libraries like BeautifulSoup, keeping the make_playlist.py script lightweight and self-contained. The method reliably extracts the URL from the <img src="..."> pattern present in every Logo column entry.

Are logo images downloaded or validated during playlist generation?

No. The Free-TV/IPTV playlist generator does not download, cache, or validate the image files. It performs a simple text extraction of the URL from the Markdown source and inserts it directly into the tvg-logo attribute. The actual image fetching and rendering is handled by the IPTV player software (such as VLC, Kodi, or IPTV Smarters) when it consumes the generated M3U8 file.

Can I use relative URLs or local image paths for channel logos?

The current implementation in make_playlist.py extracts whatever URL is present in the src attribute of the <img> tag. While the repository typically uses absolute URLs (HTTPS links to Imgur or Wikimedia), the string extraction logic would preserve a relative path if one were provided in the Markdown table. However, most IPTV players require absolute URLs to resolve images correctly, so absolute paths are recommended for compatibility.

Where is the tvg-logo attribute defined in the M3U8 specification?

The tvg-logo attribute follows the de facto IPTV standard used by popular players like VLC, Perfect Player, and IPTV Smarters. While M3U8 files technically follow the HTTP Live Streaming specification, the tvg- prefix attributes (including tvg-name, tvg-logo, tvg-id, and tvg-chno) are extended metadata attributes that IPTV players parse to display channel information and thumbnails alongside the stream URLs.

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 →