Skip to content

Latest commit

 

History

4 Commits

Folders and files

NameName
Last commit message
Last commit date
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

real_m3u_parser

Opinionated M3U/M3U8 IPTV playlist parser with lexer → parser → AST architecture. Strongly-typed tag structs, lossless roundtrip, HLS-aware.

Features

  • Mini-language architecture — Lexer tokenizes raw text, parser builds a typed AST, serializer emits back
  • All IPTV tags#EXTM3U, #EXTINF with tvg-*, group-title, catchup-*, radio, etc.
  • Player directives#EXTVLCOPT, #KODIPROP, #EXTGRP, #PLAYLIST, #WEBPROP
  • HLS tags#EXT-X-* parsed with comma-separated attributes (HlsTagNode)
  • Extended tags — Other #EXT-* tags preserved as ExtendedTagNode
  • Pipe | params — URL inline headers parsed: url|key=value&key2=value2
  • Strongly-typed tagsTvgId, GroupTitle, Catchup, TvgChno, etc.
  • Lossless roundtrip — Unmodified input returns original source verbatim
  • Lenient by default — Collects warnings for malformed lines, preserves unknown attributes

Usage

import 'package:real_m3u_parser/real_m3u_parser.dart';

void main() {
  final m3u = '''
#EXTM3U url-tvg="https://epg.example.com/guide.xml"

#EXTINF:-1 tvg-id="bbc.one.uk" tvg-name="BBC One" tvg-logo="https://example.com/logo.png" group-title="UK|Entertainment",BBC One HD
#EXTVLCOPT:http-user-agent=Mozilla/5.0
http://stream.example.com/bbc1.ts

#EXTINF:-1 tvg-id="sky.sports" tvg-name="Sky Sports" group-title="Sports",Sky Sports Main Event
http://stream.example.com/sky.m3u8

#EXTINF:5400 group-title="Movies",Inception (2010)
https://vod.example.com/inception.mp4
''';

  final playlist = M3uParser.parse(m3u);

  // Header-level attributes
  for (final attr in playlist.header.attributes) {
    print('${attr.key} = ${attr.value}');
  }

  // Iterate entries
  for (final entry in playlist.body.whereType<M3uEntry>()) {
    print(entry.extinf.title);
    print('  tvg-id:  ${entry.extinf.tvgId?.id}');
    print('  group:   ${entry.extinf.groupTitle?.title}');
    print('  URL:     ${entry.url.url}');

    for (final vlc in entry.directives.whereType<VlcOptNode>()) {
      print('  VLC:    ${vlc.optionKey}=${vlc.optionValue}');
    }
  }

  // Serialize back (lossless roundtrip if unmodified)
  print(M3uParser.serialize(playlist));
}

API

Parsing

// From string
final playlist = M3uParser.parse(source);

// With instance (reuses lenient setting)
final parser = M3uParser(lenient: false); // strict mode
final playlist = parser.parseString(source);

Accessing data

playlist.header.attributes           // List<M3uAttribute> — raw key/value pairs
playlist.body                        // List<M3uNode> — entries, comments, HLS tags, etc.
playlist.errors                      // List<ParseError> — warnings/errors from lenient mode

// Filter entries
final entries = playlist.body.whereType<M3uEntry>();

// Typed attribute getters on ExtinfNode
entry.extinf.tvgId                   // TvgId?
entry.extinf.tvgName                 // TvgName?
entry.extinf.tvgLogo                 // TvgLogo?
entry.extinf.tvgChno                 // TvgChno? (also reads tvg-chnum)
entry.extinf.tvgShift                // TvgShift?
entry.extinf.tvgCountry              // TvgCountry?
entry.extinf.tvgLanguage             // TvgLanguage?
entry.extinf.tvgUrl                  // TvgUrl?
entry.extinf.groupTitle              // GroupTitle? (hierarchy: List<String> from `|` or `;`)
entry.extinf.radioTag                // Radio?
entry.extinf.catchup                 // Catchup? (mode, days, hours, source)
entry.extinf.timeshift               // Timeshift?

// Directives
entry.directives                     // List<DirectiveNode>
// VlcOptNode — #EXTVLCOPT:key=value
// KodiPropNode — #KODIPROP:key=value
// ExtgrpNode — #EXTGRP:group
// PlaylistNode — #PLAYLIST:name
// WebpropNode — #WEBPROP:key=value

// URL
entry.url.url                        // String — the stream URL
entry.url.pipeParams                 // Map<String, String> — from |key=value&...

HLS and Extended Tags

// HLS tags: #EXT-X-VERSION:4, #EXT-X-KEY:METHOD=AES-128,URI="..."
final hls = playlist.body.whereType<HlsTagNode>();
for (final tag in hls) {
  print(tag.tagName);        // e.g. "#EXT-X-KEY"
  print(tag.attributes);     // {"METHOD": "AES-128", "URI": "..."}
}

// Extended tags: #EXTALBUM:..., #EXTART:...
final ext = playlist.body.whereType<ExtendedTagNode>();

FlatPlaylist (flattened access)

FlatPlaylist.from(playlist) merges all attributes, directives, and pipe params into flat objects:

final flat = FlatPlaylist.from(parsedPlaylist);

flat.name                         // from #PLAYLIST: or name="..." on #EXTM3U
flat.urlTvg                       // global url-tvg
flat.userAgent                    // global user-agent
flat.entries                      // List<FlatEntry>

for (final e in flat.entries) {
  e.title                         // channel display name
  e.uri                           // stream URL
  e.duration                      // -1 for live, seconds for VOD
  e.httpHeaders                   // Merged from all 4 sources:
                                  //   1. EXTINF http-* attrs (lowest)
                                  //   2. EXTVLCOPT directives
                                  //   3. KODIPROP stream_headers
                                  //   4. Pipe |params from URL (highest)
                                  // Names normalized: User-Agent, Referer, Origin...
  e.tvgId, e.tvgName, e.tvgLogo  // typed IPTV attributes
  e.tvgChno, e.tvgShift
  e.groupTitle, e.groupHierarchy  // "UK|News" → ["UK", "News"]
  e.isRadio, e.timeshift
  e.catchup                       // CatchupInfo { mode, days?, hours?, source? }
  e.streamType, e.provider
  e.isAdult, e.recording
  e.drm                           // DrmInfo? — scheme, licenseUri, manifestType, keyId
  e.drm?.scheme                   // DrmScheme.widevine / .clearkey / .playready
  e.drm?.licenseUri               // license server URL (or key for clearkey)
  e.drm?.manifestType             // "mpd", "hls", etc.
  e.drm?.keyId                    // clearkey key ID (parsed from key_id:key format)
  e.extra                         // Map of unknown/unrecognized attributes
}

// DRM detected from KODIPROP directives:
// #KODIPROP:inputstream=inputstream.adaptive
// #KODIPROP:inputstream.adaptive.license_type=com.widevine.alpha
// #KODIPROP:inputstream.adaptive.license_key=https://license.example.com/wv

Serialization

// Unmodified playlist → original source returned verbatim
final output = M3uParser.serialize(playlist);

Error Handling

for (final error in playlist.errors) {
  print('${error.severity} at line ${error.line}:${error.column}: ${error.message}');
}

In strict mode (M3uParser(lenient: false)), ParseException is thrown on first error.

Architecture

Raw M3U text
    ↓
M3uLexer — character-by-character scanner → List<Token>
    ↓
M3uParserImpl — recursive-descent parser → M3uPlaylist (AST)
    ↓
serializer() — lossless roundtrip → M3U string
  • tokens.dartTokenType, Token, TokenPosition, DirectiveKind
  • lexer.dart — BOM-safe scanner, classifies lines, parses EXTINF attributes
  • tags.dart — 30+ strongly-typed tag structs (TvgId, GroupTitle, Catchup, etc.)
  • ast.dartM3uPlaylist, M3uEntry, ExtinfNode, VlcOptNode, KodiPropNode, HlsTagNode, ExtendedTagNode, etc.
  • parser.dart — Grammar-driven parser with error recovery
  • serializer.dart — Returns original source if unmodified, regenerates on change
  • flat_playlist.dartFlatPlaylist/FlatEntry helpers for flattened attribute access

Supported Tags

Tag AST Node
#EXTM3U M3uHeader
#EXTINF: ExtinfNode (duration, attributes, title)
#EXTVLCOPT: VlcOptNode
#KODIPROP: KodiPropNode
#EXTGRP: ExtgrpNode
#PLAYLIST: PlaylistNode
#WEBPROP: WebpropNode
#EXT-X-* HlsTagNode (comma-separated attrs)
Other #EXT-* ExtendedTagNode
url|key=val UrlNode.pipeParams

Testing

# Unit tests
dart test test/real_m3u_parser_test.dart

# Integration test (fetches real playlist from iptv-org)
dart test test/integration_test.dart

# Full suite
dart test

About

Opinionated Dart M3U/M3U8 Parser (and reserializer)

Resources

Stars

0 stars

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages