m3u plus gegen m3u: Welche Attribute eine erweiterte Playlist hinzufügt und wie man sie parst
Es gibt keine Spezifikation für M3U+. Es gibt eine Konvention, ein Korpus und einen Parser, der beidem standhalten muss.
#EXTINF:-1 tvg-id="" tvg-logo="" group-title="News;General" tvg-chno="1",Example One
https://provider.example.com/stream/one.m3u8
Zwei Zeilen. Diese Zeile ist zusammengesetzt, nicht abgeschrieben: Die beiden Playlists, die wir für die Parser-Arbeit vorhalten, enthalten die Namen echter Sendeanstalten, deshalb ist hier nichts wörtlich aus ihnen übernommen. Jede Form darin ist eine, die diese Dateien tatsächlich enthalten, und keiner der Werte ist charakteristisch. In der Datei mit 1.895 Einträgen beanspruchen 107 Zeilen die Sendernummer 1, 20 tragen ein leeres tvg-id, und 42 tragen ein leeres tvg-logo. In der Datei mit 308 Einträgen tragen 25 Zeilen ein leeres group-title, 19 tragen ein leeres tvg-logo, und 10 packen zwei Gruppennamen mit einem Semikolon in ein einziges Attribut.
Fast alles auf der ersten Zeile, was nach strukturierten Daten aussieht, fehlt, ist leer oder wird mit hundert nicht zusammengehörenden Sendern geteilt. Das eine Feld, das zuverlässig da und dabei fast eindeutig ist, ist die URL auf der zweiten Zeile: 308 verschiedene URLs auf 308 Einträge in der kleineren Datei, 1.884 auf 1.895 in der größeren.
Der Player enthält keine Sender und kann auch keine liefern. Was darin auftaucht, kommt aus der Datei, auf die Sie ihn richten, geparst auf Ihrem eigenen Gerät. Wie sich dieses Parsen gegenüber unvollkommener Eingabe verhält, macht damit den größten Teil dessen aus, was am Ende in Ihrer Senderliste steht.
Es gibt keine Spezifikation, es gibt eine Konvention
RFC 8216 definiert #EXTINF für HLS als eine Dauer, ein Komma und einen optionalen, für Menschen lesbaren Titel, der bis zum Zeilenende läuft. Zu tvg-id, tvg-chno, group-title, tvg-logo, catchup-source oder irgendeinem der anderen Token, die echte Dateien zwischen die Dauer und das Komma packen, sagt er nichts.
Diese Token stammen aus einer späteren Konvention für Live-TV-Playlists, für die es keinen vereinbarten Namen gibt, und beide Namen, zu denen die Leute greifen, sind falsch. Extended M3U ist die Konvention aus der Winamp-Zeit, die #EXTM3U und #EXTINF überhaupt erst eingeführt hat; RFC 8216 nennt sie in Abschnitt 4.3.1.1 beim Namen, und sie enthält nichts mit tvg-. M3U+ ist überhaupt kein Format: m3u_plus ist ein Ausgabemodus der Anbieter-Server hinter einem Anbieter-Login, der Abfragewert, der einen Export von nackten URLs auf Zeilen umschaltet, die tvg-id, tvg-logo und group-title tragen. Das eine ist eine Playlist-Konvention aus den 1990er-Jahren, das andere ein Abfrageparameter, und keines von beiden ist eine Spezifikation. Definiert wird die Konvention von dem, was die Erzeuger erzeugen.
Diese Unterscheidung zieht sich durch alles Weitere. Was die Konvention erlaubt und was ein einzelner Parser annimmt, sind zwei verschiedene Listen, und wer sie vermischt, landet bei einem Parser, der die Gewohnheiten einer Quelle festschreibt, als wären sie eine Regel. Also: Die Grammatik, die unser Parser umsetzt, ist diese und keine größere.
line := "#EXTINF:" WS* duration WS* attributes "," displayName
duration := "-"? DIGIT+
Der Präfixvergleich ist ordinal, #EXTINF: unterscheidet also bis hinunter zum Doppelpunkt Groß- und Kleinschreibung, und der Parser hat die Präfixlänge von acht Zeichen fest verdrahtet, bevor er den Rest an einen Dauer-Scanner übergibt. Dieser Scanner nimmt ein optionales einzelnes Minus, verlangt dann mindestens eine Ziffer und liest dann die Ziffernfolge auf. Er merkt sich, wo die Ziffern zu Ende waren, und wirft den Wert weg. Die Dauer wird nirgends in der Pipeline gelesen. Alle 2.203 #EXTINF-Zeilen in unseren beiden Beispiel-Playlists tragen -1, was in dieser Konvention „live, keine bekannte Länge“ heißt, und keinem Codepfad wäre es wichtig, wenn dort 7200 stünde.
Drei Folgen ergeben sich direkt aus diesem Scanner. +1 wird abgewiesen, weil ein Plus weder ein Minus noch eine Ziffer ist. .5 wird abgewiesen, weil vor dem Punkt eine Ziffer stehen muss. Und eine gebrochene Dauer wie #EXTINF:120.5,Example parst aus Versehen korrekt: Der Scanner hält am Punkt an, das übrig gebliebene .5 landet im Attributbereich, und keine Attributsuche trifft es je. RFC 8216 erlaubt diese gebrochene Form, und unser Parser kommt damit zurecht, aber niemand hat sie umgesetzt, und das sagt man besser deutlich, statt es unter „wir unterstützen Dezimalstellen“ abzulegen.
Eine Asymmetrie in dieser Grammatik sollte man zugeben, wenn auch nicht in der Richtung, in der sie zuerst aussieht. Beim Tag-Präfix wird auf Groß- und Kleinschreibung geachtet, und genau das verlangt RFC 8216 in Abschnitt 4.1 von jedem Tag, das mit #EXT beginnt. Bei Attributnamen wird nicht darauf geachtet, und das verlangt niemand. #extinf: in Kleinschreibung wird deshalb als unbekannte Direktive übersprungen, während TVG-CHNO = "18" auf derselben Zeile sauber parst, und die Hälfte, die vom RFC abweicht, ist die großzügige. Die Inkonsistenz kam mit einer Portierung aus einem älteren serverseitigen Parser, und die Quelldatei sagt das oben immer noch. Keine der beiden Beispiel-Playlists enthält ein Tag in Kleinschreibung, einen Attributnamen in Großschreibung oder ein Leerzeichen um ein Gleichheitszeichen, in unserem Korpus wurde also nie eine der beiden Hälften durchlaufen.
Das Komma, das kein Trennzeichen ist
Trennen Sie eine #EXTINF-Zeile an ihrem ersten Komma, und Sie haben 30 der 308 Einträge in einer unserer beiden Dateien zerstört.
Der Grund ist, dass Attributwerte Zeichenketten in Anführungszeichen sind und Zeichenketten in Anführungszeichen Kommas enthalten. Jede dieser 30 Zeilen trägt ein user-agent-Attribut mit der UA-Zeichenkette eines Browsers, und alle 30 dieser Zeichenketten enthalten das Fragment KHTML, like Gecko. Achtundzwanzig davon setzen es in Klammern, und bei zweien sind die Klammern irgendwo weiter oben entfernt worden, was am Komma nichts ändert. Hier ist der tatsächliche Vergleich, über die echte Datei laufen gelassen, mit entfernten Hostnamen der Quelle und ersetztem Sendernamen:
naive first-comma split -> like Gecko) Chrome/145.0.0.0 Safari/537.36 Edg/145.0.0.0",Example News (720p)
quote-aware split -> Example News (720p)
Neun Komma sieben Prozent dieser Datei bekommen einen Anzeigenamen, der aus der Browser-Versionszeichenkette von irgendwem besteht. Der Katalog wird trotzdem gebaut. Nichts meldet einen Fehler. Die Senderliste ist auf eine Weise falsch, die wie ein Darstellungsfehler aussieht und keiner ist.
Die Abhilfe ist ein von Hand geschriebener Scanner, kein regulärer Ausdruck: den Attributbereich Zeichen für Zeichen durchlaufen, bei jedem doppelten Anführungszeichen ein inQuotes-Flag umlegen und das erste Komma zurückgeben, das gesehen wird, während dieses Flag falsch ist. Alles links davon ist der Attributbereich, alles rechts davon der Anzeigename, beides getrimmt. Es sind neun Zeilen Code, und es sind die wertvollsten neun Zeilen im ganzen Parser.
Dieser Scanner entscheidet auch, was mit einer fehlerhaften Zeile geschieht, und die Entscheidung ist streng. Enthält der Attributbereich eine ungerade Anzahl doppelter Anführungszeichen, etwa group-title="Movies,Example Four, wo das schließende Anführungszeichen fehlt, kehrt das Flag nie auf falsch zurück, es wird nie ein Komma außerhalb der Anführungszeichen gefunden, und der ganze Eintrag wird mit einer Diagnose abgewiesen. Es gibt keinen Rückfall auf das erste Komma und keinen Versuch zu raten, wo das Anführungszeichen hätte stehen sollen. Wir haben die Abweisung der Reparatur vorgezogen, weil eine reparierte Zeile still falsch ist, während eine abgewiesene Zeile laut fehlt, und eine laut fehlende Zeile ist die, die ein Leser melden kann.
Fünf Attribute werden gelesen, und eine Datei trägt 1.895 Zeilen eines sechsten, das wir ignorieren
Die vollständige Menge der Attribute, nach denen unser Parser sucht, umfasst fünf Schreibweisen:
group-title, bis zum ersten Semikolon behalten, leer wird zu nichtstvg-logo, getrimmtlogo, nur herangezogen, wenntvg-logoganz fehlttvg-id, beim Parsen roh übernommen und später auf Vertrauenswürdigkeit geprüfttvg-chno, an einen Zahlenparser gegeben, der nichts zurückgeben darf
Das ist die ganze Liste. Attributnamen werden ohne Rücksicht auf Groß- und Kleinschreibung verglichen, mit optionalem Leerraum auf beiden Seiten des Gleichheitszeichens, TVG-CHNO = "18" und tvg-chno="18" sind für uns also dasselbe. Werte müssen in doppelten Anführungszeichen stehen; einen Weg für Werte ohne Anführungszeichen gibt es nicht, und einfache Anführungszeichen werden nirgends erkannt.
Jetzt die beobachtete Seite, und die ist interessanter. In der Datei mit 1.895 Einträgen steht tvg-name auf allen 1.895 Zeilen, und wir ignorieren jede einzelne davon. tvg-country steht auf 1.642 Zeilen, und die ignorieren wir auch. Der Anzeigename kommt immer aus dem Text hinter dem Komma, nie aus tvg-name, selbst wenn eine Quelle beides auf jeder Zeile liefert, die sie veröffentlicht. Das ist eine Entwurfsentscheidung und kein Versehen: Der Titel hinter dem Komma ist das Feld, das die Grammatik selbst garantiert, tvg-name ist optional, und eine maßgebliche Namensquelle ist besser als eine Rangfolge, die sich von Datei zu Datei anders verhält.
Die kleinere Datei zeigt die Direktivenschicht, die zwischen den beiden Zeilen eines Eintrags sitzt. Sie trägt 30 user-agent-Attribute, 2 referrer-Attribute (von diesem Erzeuger mit zwei r geschrieben, was nicht der Schreibweise des HTTP-Headers entspricht) und 32 eigene Direktivzeilen #EXTVLCOPT, von denen 30 den User Agent wiederholen, den die #EXTINF-Zeile schon genannt hat. Wir lesen nichts davon.
Andere Player lesen Attribute, die wir nicht lesen, und keine zwei ihrer Listen sind dieselbe Liste. Das Add-on IPTV Simple für Kodi dokumentiert catchup, catchup-source, catchup-days, tvg-shift, radio und eine Direktive #EXTGRP. SS-IPTV, ein Player für Smart-Fernseher, dokumentiert aspect-ratio, was Kodi nicht tut. tvg-language kam von iptv-org, das es inzwischen zusammen mit tvg-country fallen gelassen hat. Keines dieser Token taucht irgendwo in unserem Beispiel auf, und wir lesen keines davon, woraus sich eine ehrliche Einschränkung des Produkts ergibt, die man aussprechen sollte: Unsere Catch-up-Funktion gibt es, aber sie baut auf Anbieter-Logins auf, eine M3U-Quelle kann uns also kein Catch-up anbieten, ganz gleich, welche Attribute sie schreibt.
Sieben Kopfzeilen, drei Sender
Hier ist ein Fragment, gebaut, um jeden Weg auf einmal durchzuspielen. Jeder Host darin ist eine reservierte Beispiel-Domain, und jeder Name ist erfunden. Die meisten Formen, die es durchspielt, kommen in unserem Korpus vor, BS4K101 eingeschlossen, ein echter Wert aus der größeren Datei. Fünf kommen darin nirgends vor: die fehlerhafte Dauer, das unpaarige Anführungszeichen, das nackte logo, der Attributname in Großschreibung mit Leerzeichen um sein Gleichheitszeichen und der Name, der mit ## beginnt. Ein von Hand gebautes Fragment ist die einzige Möglichkeit, diese fünf Wege zu erreichen, und das sollte man wissen, bevor man den Durchgang weiter unten als Beleg über echte Dateien liest.
#EXTM3U x-tvg-url="https://provider.example.com/guide.xml"
#EXTINF:-1 tvg-id="one.example" TVG-CHNO = "22.1" group-title="News;General" tvg-logo="",Example One
https://provider.example.com/stream/one.m3u8
#EXTINF:-1 tvg-id="two.example" user-agent="Mozilla/5.0 (KHTML, like Gecko)",Example Two
#EXTVLCOPT:http-user-agent=Mozilla/5.0 (KHTML, like Gecko)
https://provider.example.com/stream/two.m3u8
#EXTINF:-1 tvg-chno="BS4K101" group-title="Local",## ===== LOCAL =====
https://provider.example.com/stream/divider
#EXTINF:x tvg-chno="7" group-title="Movies",Example Three
https://provider.example.com/stream/three.m3u8
#EXTINF:-1 group-title="Movies,Example Four
https://provider.example.com/stream/four.m3u8
#EXTINF:-1 tvg-chno="9",Example Five
rtmp://provider.example.com/stream/five
#EXTINF:-1 logo="https://provider.example.com/logos/six.png",Example Six
https://provider.example.com/stream/six.m3u8
Sieben Kopfzeilen gehen hinein. Vier Zeilen kommen aus dem Parser heraus, drei davon erreichen den Katalog, und fünf Diagnosemeldungen entstehen.
Zeile 1 wird übersprungen. Unser Parser sucht überhaupt nie nach #EXTM3U; die Zeile fällt zusammen mit allem anderen, was er nicht kennt, durch das allgemeine Überspringen für „beginnt mit einer Raute“, und das Attribut x-tvg-url darauf wird nicht gelesen. RFC 8216 verlangt diese Kopfzeile als erste Zeile einer Playlist. Wir verlangen sie nicht, und eine Datei ohne Kopfzeile, die direkt mit #EXTINF beginnt, parst sauber, weil die Formprüfung, die vor dem Parsen auf dem Gerät läuft, beide Tags als Erstes in der Datei akzeptiert, nach einem optionalen Byte Order Mark und führendem Leerraum. Sonst darf nichts davorstehen: Eine Datei, die mit einer Kommentarzeile oder einem #EXTVLCOPT beginnt, wird abgewiesen, bevor der Parser sie überhaupt sieht.
Example One parst mit dem Attributnamen in der falschen Schreibung und Leerzeichen um das Gleichheitszeichen. Seine Gruppe wird News, weil nur der Abschnitt vor dem ersten Semikolon behalten wird. Seine Nummer wird 22.1 als Dezimalzahl, nicht 22 als ganze Zahl, und das ist weiter unten wichtig. Sein Logo wird eine leere Zeichenkette, wo man gar nichts erwarten würde, und auch das ist weiter unten wichtig.
Example Two ist der Fall mit dem Komma in Anführungszeichen, und die #EXTVLCOPT-Zeile danach ist der Grund, warum das allgemeine Überspringen von Rauten die offene Kopfzeile nicht löschen darf. Direktiven sitzen mit gutem Recht zwischen einem #EXTINF und seiner URL. Ein Parser, der bei jeder unbekannten #-Zeile den Zustand zurücksetzt, würde diesen Sender und jeden Sender wie ihn verwerfen, und das sind in der kleineren Beispieldatei 30 von 308 Einträgen.
Die Trennzeile parst sauber und ist, soweit es die Grammatik angeht, ein Sender. Ihr tvg-chno-Wert BS4K101 wird vom Zahlenparser abgewiesen und wird zu nichts, was die Nummer kostet, aber nicht die Zeile. Was sie entfernt, ist ihr Name, der mit ## beginnt, und das geschieht später, bei der Namensnormalisierung, nicht beim Parsen. Die beiden Stufen getrennt zu halten heißt, dass der Parser keine Meinung darüber hat, was ein Sendername bedeutet, und dorthin gehört diese Grenze.
Example Three scheitert an der Dauer: x ist weder ein Minus noch eine Ziffer, die Kopfzeile wird also mit einer Diagnose abgewiesen, und alle fünf offenen Felder werden gelöscht. Seine URL kommt danach ohne offene Kopfzeile an und erzeugt eine zweite Diagnose. Eine fehlerhafte Kopfzeile kostet zwei Zeilen und erzeugt zwei Log-Einträge, was richtig ist und was man wissen sollte, wenn man die Diagnosen eines echten Imports liest.
Example Four ist das unpaarige Anführungszeichen und verhält sich genauso: zwei Diagnosen, nichts ausgegeben.
Example Five hat eine vollkommen gültige Kopfzeile und eine rtmp-URL. Das ist ein absoluter URI, also parst er auch als einer, aber das Schema ist weder http noch https, und wir weisen sie ab. Es ist die gefährlichste Zeile im Fragment, denn wenn der Weg für die Abweisung den offenen Zustand nicht löschen würde, würde Example Six still die Sendernummer 9 erben. Dieser Weg ist nicht hypothetisch. Die größere Beispieldatei beendet 6 ihrer 1.895 Einträge mit einer rtmp- oder rtsp-URL, fünf und eine, und diese 6 Zeilen sind die einzigen Zeilen im ganzen Korpus mit 2.203 Einträgen, die unser Parser abweist.
Example Six trägt ein nacktes logo-Attribut ohne tvg-logo und ganz ohne group-title, der Logo-Rückfall greift also, und der Sender landet in einer Standardgruppe, die nach der Quelle benannt ist.
Jeder Weg, der eine Zeile überspringt, muss die Kopfzeile löschen
Ein M3U-Eintrag besteht aus zwei Zeilen, die sich veränderlichen Zustand teilen, und darin liegt die ganze Schwierigkeit des Formats. Zwischen dem Lesen einer Kopfzeile und dem Lesen einer URL hält der Parser fünf offene Felder: Name, Gruppe, Logo, ID, Nummer. Sieben verschiedene Wege laufen durch die Schleife, und die Korrektheit des Parsers ist ganz und gar eine Frage dessen, was jeder einzelne mit diesen fünf macht.
Leerzeilen werden übersprungen, ohne zu löschen, und das ist richtig, weil eine Leerzeile zwischen einer Kopfzeile und ihrer URL den Eintrag nicht zerstören darf. Unbekannte #-Direktiven werden übersprungen, ohne zu löschen, aus dem Grund, den das Fragment oben zeigt. Eine fehlerhafte Kopfzeile löscht alle fünf. Eine abgewiesene URL löscht alle fünf. Eine erfolgreiche Ausgabe löscht alle fünf.
Der siebte Weg ist der, den die meisten Parser für Zeilenpaare falsch machen, und das Interessante daran ist, dass er gar nicht über ein Zurücksetzen behandelt wird. Wenn ein zweites #EXTINF ankommt, bevor irgendeine URL kam, weist der Parser alle fünf Felder bedingungslos zu, ohne if und ohne Null-Coalescing-Zuweisung. Eine Attributsuche, die nichts findet, gibt nichts zurück, und der Zahlenparser gibt nichts zurück, wenn er nichts bekommt, eine Kopfzeile ohne tvg-chno schreibt also Leere über die Nummer der vorigen Kopfzeile, statt sie stehen zu lassen. Es gibt keinen Weg, auf dem Feld N von Eintrag K in Eintrag K+1 überlebt. Das ist eine stärkere Eigenschaft als „wir denken ans Zurücksetzen“, und es lohnt sich, einen Parser bewusst so zu bauen, denn die Variante mit dem Zurücksetzen bleibt nur so lange korrekt, wie jede künftige Änderung daran denkt, sie zu erweitern.
Ein Weg verdient eine Anmerkung, weil er wie ein Fehler aussieht und keiner ist. Der Weg „URL kam ohne offene Kopfzeile an“ gibt seine Diagnose aus und löscht nichts. Das ist von der Konstruktion her sicher und nicht durch Zufall, und die Invariante lässt sich durch Lesen der Zuweisungen prüfen: Die anderen vier Felder werden ausschließlich auf dem Weg über die Kopfzeile gesetzt, und dieser Weg setzt den Namen im selben Durchgang immer auf einen nicht leeren Wert und fällt auf die wörtliche Zeichenkette Unnamed channel zurück, wenn der Titel leer ist. Ein Name auf null bedeutet also, dass die anderen vier schon auf null stehen, und das fehlende Zurücksetzen ist Arbeit, die nie erreicht wird.
Jeder dieser Wege braucht seinen eigenen Regressionstest, und die neun, die wir haben, decken die Wege ab, auf denen etwas durchsickert: Einer hält fest, dass ein nummerierter Eintrag seine Nummer nicht an den nächsten weiterreicht, ein anderer, dass eine fehlerhafte Kopfzeile das auch nicht tut. Überhaupt keinen Test hat der Scanner für das Komma in Anführungszeichen, jene neun Zeilen, die dieser Artikel die wertvollsten neun Zeilen im Parser genannt hat, dazu die Abweisung unpaariger Anführungszeichen, der Fall #EXTVLCOPT zwischen den Zeilen und der Rückfall auf ein nacktes logo. An dieser Lücke wirft nichts eine Exception. Der Katalog wird gebaut. Hundert Sender hinter einer einzigen schlechten Zeile tragen die falsche Gruppe.
Die zwei Felder, die wie Bezeichner aussehen
tvg-chno sind Anzeigedaten, und die Messung, die das entscheidet, kostet eine Zeile Shell-Arbeit. In der Datei mit 1.895 Einträgen tragen 1.355 Einträge das Attribut. Diese 1.355 Nummern decken 263 verschiedene Werte ab. 107 Einträge beanspruchen die Nummer 1. Die Doppelung ist kein Artefakt davon, dass Gruppen zusammengelegt wurden, denn sie übersteht das Gruppieren: Eine Gruppe in dieser Datei enthält 7 Sender, die alle die Nummer 1 tragen, und 33 der 85 Gruppen, die überhaupt eine Nummer tragen, haben mindestens eine Doppelung in sich. Diese Datei hat insgesamt 92 Gruppen, 7 davon tragen also überhaupt keine Nummern. Die andere Datei, 308 Einträge, hat nirgends ein tvg-chno. Nichts darf diesen Wert als Schlüssel verwenden, nichts darf annehmen, dass er eindeutig ist, und jede Stelle, die danach sortiert, braucht ein deterministisches Kriterium für den Gleichstand.
Ihn zu parsen ist eine kleine Übung darin, nicht klug sein zu wollen. Der Wert wird als Gleitkommazahl unter der invarianten Kultur gelesen, nicht als ganze Zahl, und nie unter der Spracheinstellung des Geräts. 56 der 1.355 werden rundheraus abgewiesen. Kein einziger der 56 ist numerisch:
- 49 sind Codes von Übertragungssystemen: 7 laufen von
D011bisD081, 19 gehören zu einer Familie vonBS101bisBS4K101, 22 zu einer Reihe vonCS055bisCS363, dazu ein einzelner Code mit dem Präfix eines Kabelnetzbetreibers - 4 sind ein wörtliches
#, offenbar ein Platzhalter, den der Erzeuger nie gefüllt hat - 3 sind Freitext mit einer darin vergrabenen Zahl, zwei lauten
CH 106undCH 108, eines ist ein Dienstname auf Japanisch, der auf527endet
Der Parser trägt außerdem eine Obergrenze von 99999, oberhalb derer ein Wert als Müll behandelt wird. Sie hat auf diesem Korpus nie gegriffen. Die größte Zahl in der Datei ist 4999, und das ist eine nützliche Erinnerung daran, dass eine Absicherung, die für eine Vermutung geschrieben wurde, kein Beleg dafür ist, dass die Vermutung stimmte.
Das Parsen als Dezimalzahl ist keine Stilfrage. 25 Einträge in dieser Datei verwenden Unterkanal-Nummerierung nach ATSC-Art, 24 verschiedene Werte von 22.1 bis 36.2, und ein Parsen als ganze Zahl verschmilzt 22.1, 22.2 und 22.3 zu drei Sendern, die alle die Nummer 22 tragen. Die invariante Kultur ist ebenfalls keine Stilfrage: Die Spracheinstellung des Geräts zu beachten hieße, dass dieselbe Datei auf einem Rechner, der auf ein Komma als Dezimaltrennzeichen eingestellt ist, andere Nummern ergibt, und das ist eine Fehlerklasse, die sich immer nur auf dem Rechner von jemand anderem nachstellen lässt.
tvg-id ist schlimmer, weil seine Fehlschläge nicht leer sind. In der Datei mit 1.895 Einträgen fehlt das Attribut in 483 Zeilen, ist in 20 weiteren leer und steht in weiteren 24 zwar da, scheitert aber an unserer Formregel, also enden 527 Zeilen, 27.8 Prozent der Datei, ohne brauchbare ID. Unangenehm sind diese 24. Siebzehn davon sehen genau wie echte IDs aus und werden aus einem einzigen Grund abgewiesen, nämlich weil sie kürzer als sechs Zeichen sind. Die anderen 7 tragen keinen Punkt. Eine Mindestlänge ist eine grobe Regel, sie kostet uns diese 17, und wir würden die Schwelle noch einmal ansehen, wenn die Zahl je stiege.
Dass es diese Regel überhaupt gibt, liegt an einem anderen Korpus, das wir für die Parser-Arbeit vorhalten und das aus Exporten von Anbieter-Servern besteht statt aus den beiden kuratierten Dateien von oben. In zwei seiner drei Quellen war der häufigste tvg-id-Wert die wörtliche Zeichenkette TS: 446 Zeilen in der einen, 274 in der anderen. TS ist das Containerformat Transport Stream, das aus einem Feld für das Ausgabeformat in die ID-Spalte gesickert ist. Diese 720 Sender hatten nichts miteinander zu tun, ein britischer Unterhaltungssender und ein arabischsprachiger Radiosender standen unter einer einzigen Kennung nebeneinander. Weil der Senderschlüssel aus der ID abgeleitet wird, fielen alle 720 zu einem einzigen logischen Sender zusammen. Die Folge ist ein Favorit, der auf Sendern auftaucht, die niemand markiert hat, eine Kindersicherung, die 720 Dinge auf einmal sperrt, und ein einziger Multiview-Platz, der für jeden einzelnen davon steht.
Die Reparatur war eine Vertrauensregel, die es nur an einer einzigen Stelle gibt, angewendet dort, wo die Daten hereinkommen, damit sich weiter hinten niemand ans Nachprüfen erinnern muss. Eine ID ist nur brauchbar, wenn sie 6 bis 128 Zeichen lang ist, einen Punkt enthält und nicht TS lautet, und was übrig bleibt, wird unter der invarianten Kultur kleingeschrieben, weil Quellen sich bei der Groß- und Kleinschreibung derselben Sender-ID tatsächlich uneinig sind. Alles andere wird zu nichts normalisiert, und „keine ID“ ist ein Zustand, den ohnehin jede Stelle behandelt, weil die meisten Sender wirklich keine haben. Die Regel ist notwendig und nicht hinreichend, und das wissen wir: Eine ID, die die Formprüfung besteht, hing am Ende an 74 nicht zusammengehörenden Sendern, deshalb läuft überall dort, wo Quellen zusammengeführt werden, obendrauf noch eine Kardinalitätsprüfung beim Bauen. Keine der beiden kuratierten Dateien in unserem Beispiel enthält auch nur eine einzige TS-ID, und das ist der ehrliche Vorbehalt zu alldem. Ein sauberes Korpus wird Ihnen diesen Fehler nie zeigen. Wir haben von ihm aus einem Fehlerbericht erfahren, nicht aus einem Test.
Weil in den meisten Dateien keine brauchbare ID steht, muss eine abgeleitet werden, und beim Ableiten wird der Kompromiss unangenehm. Die Stream-URL ist das einzige Feld, das innerhalb einer Quelle immer da und fast eindeutig ist, die synthetische ID ist deshalb ein SHA-1 dieser URL, die ersten vier Bytes little-endian gelesen und zu einer positiven 32-Bit-Ganzzahl gefaltet. Das lässt rund 2^31 Schlüsselraum übrig, und nach der Geburtstagsschranke trägt eine Playlist mit 50.000 Einträgen eine Wahrscheinlichkeit von etwa 44 Prozent für mindestens eine interne Kollision. Wir haben diese Zahl mit der Rechnung vor Augen akzeptiert, mit der Begründung, dass eine Kollision zwei Sender innerhalb einer Quelle verschmilzt, und genau das passiert ohnehin, wenn eine Quelle denselben Sender zweimal führt. Unser eigenes Korpus tut das bei 9 URLs, die 20 der 1.895 Zeilen in der größeren Datei betreffen, und in der Datei mit 308 Einträgen kein einziges Mal, es ist also ein echtes Ereignis, aber kein alltägliches, und diese Messung schwächt das Argument mehr, als sie es stützt. Was die Frage tatsächlich entscheidet, ist, dass eine breitere Grundlage mit tvg-id darin oder ein Wechsel zu einem breiteren Hash nicht auslieferbar ist: Die abgeleitete ID wird in gespeicherte Daten geschrieben, die andere Geräte wieder lesen, eine Änderung der Ableitung macht also alles davon auf einen Schlag ungültig. Die Kollisionsrate ist der Preis für ein eingefrorenes Datenformat.
Leer ist nicht dasselbe wie fehlend, außer in einem Feld
Drei Attribute, drei verschiedene Regeln für die leere Zeichenkette, und aufgefallen ist uns das erst beim Messen.
group-title="" wird zu nichts, weil der Gruppen-Normalisierer einen leeren Wert als nicht vorhanden behandelt, und 25 Zeilen in der kleineren Datei nehmen diesen Weg. tvg-id="" wird zu nichts, weil die Vertrauensregel es abweist. tvg-logo="" wird nicht zu nichts: Es kommt als leere Zeichenkette an und wandert bis ins Symbolfeld der Katalogzeile. 19 Zeilen in der einen Beispieldatei und 42 in der anderen tragen ein leeres Logo-Attribut, 61 Zeilen in einem Korpus von 2.203 Zeilen halten also eine leere Zeichenkette, wo jedes andere Feld nichts halten würde. Das Logo ist die Ausnahme, und zwar aus keinem anderen Grund als der Reihenfolge, in der diese drei Fälle geschrieben wurden.
Was das kostet, hängt an einem Weg, den wir nicht gelesen haben. Wenn der Bildlader eine leere Zeichenkette und null gleich behandelt, ist das ein Schönheitsfehler und nicht mehr. Wenn eine leere Zeichenkette eine Anfrage startet, die scheitert, leisten 61 Zeilen bei jedem Zeichnen unnötige Arbeit, und aus dem Schönheitsfehler wird ein kleiner Fehler. Jemand sollte diesen Weg lesen, bevor diese Seite eines von beidem behauptet.
Was wir nicht gemessen haben
Für einige Grenzen dieses Parsers gibt es in unserem Beispiel keinen Zeugen, und das zu sagen ist nützlicher, als so zu tun, als wären sie getestet. Einen Weg für Werte ohne Anführungszeichen gibt es nicht: tvg-chno=18 ohne Anführungszeichen ist für uns unsichtbar. Null der 2.203 #EXTINF-Zeilen, die wir vorhalten, verwenden einen Wert ohne Anführungszeichen, es hat uns also nie getroffen, und wir wissen nicht, wie häufig diese Form da draußen ist. Der Rückfall auf ein nacktes logo hat dasselbe Problem umgekehrt, denn null unserer Zeilen verwenden ihn, ein Rückfall, den wir ausliefern, hat also keinen Test gegen echte Eingabe.
Diese Seite zu schreiben hat einen Fehler gefunden, den wir ausgeliefert hatten, und das ist das Nützlichste, was sie getan hat. Den Attribut-Abgleich klar genug darzulegen, um ihn zu veröffentlichen, machte offensichtlich, dass das Muster keine linke Grenze hatte, das Muster für logo traf also innerhalb jedes Attributnamens, der auf logo endet. Dazu gehört provider-logo, ein echtes Attribut, das das Add-on IPTV Simple für Kodi dokumentiert. Eine Playlist mit provider-logo und ohne tvg-logo nahm das falsche Bild, und die Abkürzung hat uns nicht gerettet, weil sie nur greift, wenn tvg-logo da ist. Dasselbe Loch ließ tvg-id innerhalb von x-tvg-id treffen und group-title und tvg-chno innerhalb von allem, was darauf endet.
Es hat uns überrascht, dass die naheliegende Abhilfe falsch ist. Eine Wortgrenze funktioniert hier nicht: Jeder Attributname in dieser Konvention ist mit Bindestrichen zusammengesetzt, und - ist kein Wortzeichen, zwischen dem Bindestrich und dem Namen liegt also ohnehin schon eine Grenze, und \btvg-logo trifft weiterhin innerhalb von x-tvg-logo. Die Bedingung, die man tatsächlich will, lautet: Der Name steht am Anfang des Attributbereichs oder folgt auf Leerraum. In .NET ist das ein Lookbehind (?<=^|\s), und das ist in weniger Engines portabel, als man erwarten würde: Python weigert sich, es zu übersetzen, weil die meisten Engines ein Lookbehind fester Breite verlangen und dieses hier alterniert. Alle vier Attribute tragen diese Grenze jetzt, und die sechs Fälle von oben sind per Test festgenagelt.
Unser Player führt das alles auf Ihrem Gerät gegen die Playlist aus, die Sie angeben, und jede Abweisung erzeugt eine Diagnosemeldung statt einer Exception, denn eine Datei mit 40 schlechten Zeilen von 3.000 muss die anderen 2.960 importieren und Ihnen sagen, was verloren gegangen ist. Unsere Apps für Windows und Xbox erledigen diese Arbeit lokal, und Sie richten eine davon auf eine Quelle, die Sie schon haben.
Wenn Sie Ihren eigenen Parser bauen: Die Messungen oben nützen Ihnen mehr als die Dokumentation des Formats, vor allem, weil es nicht viel Dokumentation des Formats gibt. Zählen Sie Ihre Einträge. Zählen Sie, wie oft jedes Attribut vorkommt, zählen Sie, wie viele verschiedene Werte es hat, und vergleichen Sie diese beiden Zahlen, bevor Sie dieses Feld für irgendetwas als Schlüssel verwenden. Das ist ein Nachmittag mit einem Korpus, das Sie schon haben, und es ist der Unterschied zwischen einem Parser, der auf der Datei funktioniert, mit der Sie getestet haben, und einem, der auf der Datei funktioniert, die man Ihnen geschickt hat.
Was dieser Artikel gemessen hat26 Behauptungen, jeweils mit den Beweisen dahinter
| Anspruch | Beweise | Gezählt |
|---|---|---|
| RFC 8216 definiert #EXTINF als eine Dauer, ein Komma und einen optionalen Titel, der bis zum Zeilenende läuft.RFC 8216 section 4.3.2.1 | Spezifikation | Nicht zutreffend |
| RFC 8216 erlaubt bei #EXTINF eine Dauer mit Nachkommastellen.RFC 8216 section 4.3.2.1 | Spezifikation | Nicht zutreffend |
| RFC 8216 verlangt #EXTM3U als erste Zeile einer Playlist.RFC 8216 section 4.3.1.1 | Spezifikation | Nicht zutreffend |
| RFC 8216 sagt, dass Tags mit #EXT beginnen und Groß- und Kleinschreibung unterscheiden, ein ordinaler Präfixvergleich ist also das konforme Verhalten.RFC 8216 section 4.1 | Spezifikation | Nicht zutreffend |
| Extended M3U ist die Konvention aus der Winamp-Zeit, die #EXTM3U und #EXTINF eingeführt hat, und RFC 8216 nennt sie beim Namen. Sie enthält keines der Token mit dem Präfix tvg-.RFC 8216 section 4.3.1.1, which reads: the EXTM3U tag indicates that the file is an Extended M3U [M3U] Playlist file | Spezifikation | Nicht zutreffend |
| M3U+ ist kein Formatname. m3u_plus ist ein Ausgabemodus der Anbieter-Server hinter einem Anbieter-Login, der Abfragewert, der einen Export von nackten URLs auf Zeilen umschaltet, die tvg-id, tvg-logo und group-title tragen.Provider login API, playlist export parameter type=m3u_plus | Spezifikation | Nicht zutreffend |
| Das Add-on IPTV Simple für Kodi dokumentiert catchup, catchup-source, catchup-days, tvg-shift, radio, provider-logo und eine Direktive #EXTGRP.kodi-pvr/pvr.iptvsimple README, playlist attribute reference | Spezifikation | Nicht zutreffend |
| aspect-ratio ist von SS-IPTV dokumentiert, nicht von Kodi.SS-IPTV playlist attribute documentation | Spezifikation | Nicht zutreffend |
| tvg-language kam von iptv-org, das es inzwischen zusammen mit tvg-country fallen gelassen hat.iptv-org/iptv repository, playlist field history | Spezifikation | Nicht zutreffend |
| Eine Playlist mit 50.000 Einträgen über einem Schlüsselraum von rund 2^31 trägt eine Wahrscheinlichkeit von etwa 44 Prozent für mindestens eine interne Kollision.Birthday bound, 1 - exp(-n(n-1)/2N) with N = 2^31 and n = 50,000, which evaluates to 0.4412 | Spezifikation | Nicht zutreffend |
| Jede #EXTINF-Zeile im Korpus trägt eine Dauer von -1. | n = 2203 | 22.08.2026 |
| Am ersten Komma zu trennen zerstört 30 der 308 Einträge in der kleineren Datei; alle 30 tragen einen user-agent-Wert, der das Fragment KHTML, like Gecko enthält. | n = 308 | 22.08.2026 |
| 1.355 von 1.895 Einträgen tragen tvg-chno, verteilt auf 263 verschiedene Werte, wobei 107 Zeilen die Nummer 1 beanspruchen und eine Gruppe 7 Sender enthält, die alle die Nummer 1 tragen. | n = 1895 | 22.08.2026 |
| Die größere Datei hat 92 Gruppen; 85 davon tragen mindestens eine Nummer, und 33 dieser 85 enthalten eine doppelte Nummer. | n = 1895 | 22.08.2026 |
| 56 der 1.355 tvg-chno-Werte werden abgewiesen, und kein einziger davon ist numerisch: 49 Codes von Übertragungssystemen, 4 wörtliche Platzhalter #, 3 Freitextwerte. Die Obergrenze von 99999 im Parser hat nie gegriffen; die größte Zahl in der Datei ist 4999. | n = 1355 | 22.08.2026 |
| 25 Zeilen verwenden Unterkanal-Nummerierung nach ATSC-Art, verteilt auf 24 verschiedene Werte von 22.1 bis 36.2. | n = 1895 | 22.08.2026 |
| In der größeren Datei fehlt tvg-id in 483 Zeilen, ist in 20 weiteren leer und steht in weiteren 24 zwar da, scheitert aber an der Formregel, also enden 527 Zeilen (27.8 Prozent) ohne brauchbare ID. | n = 1895 | 22.08.2026 |
| 17 dieser 24 Formfehler werden allein deshalb abgewiesen, weil sie kürzer als sechs Zeichen sind, und sehen wie echte IDs aus. Die anderen 7 enthalten keinen Punkt. | n = 24 | 22.08.2026 |
| In einem eigenen Korpus aus Exporten von Anbieter-Servern war TS in zwei von drei Quellen die häufigste tvg-id, 446 Zeilen und 274 Zeilen, und diese 720 nicht zusammengehörenden Sender fielen zu einem einzigen Senderschlüssel zusammen. Eine ID, die die Formprüfung bestand, hing an 74 nicht zusammengehörenden Sendern. | n = 720 | Früheres Korpus |
| Doppelte Stream-URLs gibt es wirklich, und sie sind selten: 9 doppelte URL-Werte, die 20 der 1.895 Zeilen in der größeren Datei betreffen, und 0 von 308 in der kleineren. | n = 2203 | 22.08.2026 |
| 6 Zeilen in der größeren Datei zeigen auf eine rtmp- oder rtsp-URL (5 und 1), und sie sind die einzigen Zeilen im ganzen Korpus, die der Parser abweist. | n = 2203 | 22.08.2026 |
| 61 Zeilen tragen ein leeres tvg-logo (19 in der kleineren Datei, 42 in der größeren), und die leere Zeichenkette überlebt bis in die Katalogzeile, während ein leeres group-title (25 Zeilen) und ein leeres tvg-id beide zu nichts werden. | n = 2203 | 22.08.2026 |
| tvg-name steht auf allen 1.895 Zeilen der größeren Datei und tvg-country auf 1.642; die kleinere Datei trägt 30 user-agent-Attribute, 2 referrer-Attribute und 32 Direktivzeilen #EXTVLCOPT. | n = 2203 | 22.08.2026 |
| Das Korpus enthält null Attributwerte ohne Anführungszeichen, null Tags #extinf in Kleinschreibung, null Attributnamen in Großschreibung, null Leerzeichen um ein Gleichheitszeichen, null Zeilen mit unpaarigen Anführungszeichen und null nackte logo-Attribute. | n = 2203 | 22.08.2026 |
| Das von Hand gebaute Fragment in diesem Artikel ergibt 4 geparste Zeilen, 3 Katalogzeilen und 5 Diagnosen. | n = 7 | 22.08.2026 |
| Der Parser hat 9 Unit-Tests, darunter zwei, die festhalten, dass keine Felder zwischen Einträgen durchsickern. Der Scanner für das Komma in Anführungszeichen, die Abweisung unpaariger Anführungszeichen, der Fall #EXTVLCOPT zwischen den Zeilen und der Rückfall auf ein nacktes logo haben keinen. | n = 9 | 22.08.2026 |