Opinionated M3U/M3U8 IPTV playlist parser with lexer → parser → AST architecture. Strongly-typed tag structs, lossless roundtrip, HLS-aware.
- Mini-language architecture — Lexer tokenizes raw text, parser builds a typed AST, serializer emits back
- All IPTV tags —
#EXTM3U,#EXTINFwithtvg-*,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 asExtendedTagNode - Pipe
|params — URL inline headers parsed:url|key=value&key2=value2 - Strongly-typed tags —
TvgId,GroupTitle,Catchup,TvgChno, etc. - Lossless roundtrip — Unmodified input returns original source verbatim
- Lenient by default — Collects warnings for malformed lines, preserves unknown attributes
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));
}// From string
final playlist = M3uParser.parse(source);
// With instance (reuses lenient setting)
final parser = M3uParser(lenient: false); // strict mode
final playlist = parser.parseString(source);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 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.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// Unmodified playlist → original source returned verbatim
final output = M3uParser.serialize(playlist);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.
Raw M3U text
↓
M3uLexer — character-by-character scanner → List<Token>
↓
M3uParserImpl — recursive-descent parser → M3uPlaylist (AST)
↓
serializer() — lossless roundtrip → M3U string
tokens.dart—TokenType,Token,TokenPosition,DirectiveKindlexer.dart— BOM-safe scanner, classifies lines, parses EXTINF attributestags.dart— 30+ strongly-typed tag structs (TvgId,GroupTitle,Catchup, etc.)ast.dart—M3uPlaylist,M3uEntry,ExtinfNode,VlcOptNode,KodiPropNode,HlsTagNode,ExtendedTagNode, etc.parser.dart— Grammar-driven parser with error recoveryserializer.dart— Returns original source if unmodified, regenerates on changeflat_playlist.dart—FlatPlaylist/FlatEntryhelpers for flattened attribute access
| 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 |
# 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