Ce qu’est XMLTV, et ce qu’un analyseur doit y survivre
C’est un format XML des années 1990 avec deux éléments qui comptent. Tout ce qui est difficile vient du fait que n’importe qui peut en générer un et que personne ne le valide.
XMLTV est le format dans lequel presque tous les guides de télévision hors de la pile d’un diffuseur sont livrés. Il est antérieur à toute l’industrie du streaming, c’est du XML brut, et sa spécification est une DTD plutôt qu’un schéma avec un validateur que quelqu’un exécute.
Cette dernière partie est toute l’histoire. XMLTV n’est pas difficile parce que le format est complexe. Il est difficile parce que n’importe qui peut en générer un, que personne ne le vérifie, et que les fichiers qui parviennent à un lecteur ont traversé une chaîne de scrapers et de convertisseurs sans la moindre suite de tests entre eux.
Le format, en un écran
Il y a 2 éléments qui comptent.
<tv>
<channel id="bbc1.uk">
<display-name>BBC One</display-name>
<display-name lang="en">BBC One HD</display-name>
<icon src="https://example.com/bbc1.png" />
</channel>
<programme channel="bbc1.uk" start="20260831180000 +0100" stop="20260831190000 +0100">
<title lang="en">The Six O'Clock News</title>
<desc lang="en">National and international news.</desc>
<category lang="en">News</category>
</programme>
</tv>
Un channel déclare un identifiant et un ou plusieurs noms lisibles par un humain. Un programme pointe vers un identifiant de chaîne, porte une heure de début et généralement une heure de fin, et contient un titre plus toutes les métadonnées facultatives que le générateur a eu envie d’inclure.
Voilà le format. Tout le reste est facultatif, et en pratique tout le reste est présent de façon incohérente.
L’identifiant est toute la jointure
channel id est une clé de jointure, et c’est la seule clé de jointure. C’est la chose la plus importante à savoir sur XMLTV.
Votre playlist dit qu’une chaîne est tvg-id="bbc1.uk". Votre fichier de guide contient un <channel id="bbc1.uk">. Ces deux chaînes de caractères doivent correspondre, exactement, sinon la chaîne n’a pas de grille.
Elles ne dérivent de rien. Il n’y a pas de registre. Il n’y a aucune normalisation imposée par le format. bbc1.uk, BBCOne.uk, BBC1.uk et bbc-one.uk sont 4 chaînes différentes pour n’importe quel analyseur, et il est tout à fait normal que la playlist d’un fournisseur utilise une convention tandis que le fichier de guide qu’il recommande en utilise une autre.
C’est pourquoi « mon guide est vide » est en général un problème de correspondance plutôt qu’un problème de téléchargement ou d’analyse. Les deux fichiers peuvent être parfaitement formés et parfaitement inutiles ensemble.
Les éléments display-name n’aident pas la jointure. Ils sont faits pour être montrés aux gens, ils sont souvent plusieurs par chaîne en plusieurs langues, et s’en servir pour la correspondance est une heuristique plutôt qu’une règle.
Les horodatages, et la raison pour laquelle tant de guides semblent décalés d’une heure
Un horodatage XMLTV est une chaîne d’heure murale suivie d’un décalage facultatif :
20260831180000 +0100 six in the evening, one hour ahead of UTC
20260831180000 six in the evening UTC, per the spec's default
Le décalage est facultatif. Ce seul choix de conception génère plus de questions au support que tout le reste du format réuni.
Nous nous étions trompés dans une version antérieure de cet article, qui décrivait un horodatage nu comme impossible à situer. Ce n’est pas le cas. La DTD est explicite : un horodatage sans fuseau horaire doit être lu comme de l’UTC, il y a donc un défaut défini et un analyseur conforme le suit.
Le problème, c’est qu’un générateur qui omet le décalage ne publie généralement pas de l’UTC. Il publie l’heure murale locale de l’endroit où le guide a été assemblé, et compte sur le lecteur pour se trouver au même endroit. Ce n’est pas ce que dit la spécification, et c’est ce que font un très grand nombre de fichiers. Un analyseur qui suit la DTD est alors à la fois correct et décalé d’une heure ou deux.
Notre analyseur accepte de l’appelant un fuseau horaire supposé exactement pour ce cas, ce qui transforme l’écart en réglage plutôt qu’en décision silencieuse. Les deux chemins d’ingestion sur l’appareil le laissent à null, si bien qu’un horodatage nu est lu comme de l’UTC et que le fuseau horaire de la machine n’entre jamais dans le calcul.
Le symptôme que cela produit est caractéristique : une journée entière décalée d’un nombre entier d’heures, avec toutes les durées de programme correctes. Si votre grille est juste mais systématiquement décalée d’une ou deux heures, pourquoi les heures du guide sont fausses est l’article qui donne les détails.
Un fichier de guide est une entrée non fiable
Invisible pour un utilisateur. Tout pour un analyseur.
Un fichier de guide est du XML venant d’une URL arbitraire, et le XML a une longue histoire d’attaques contre les analyseurs. Le lecteur est donc configuré défensivement avant de lire le moindre octet : les définitions de type de document sont ignorées plutôt que traitées, le résolveur externe est mis à null pour qu’aucune entité ne puisse provoquer une requête réseau, et l’expansion des entités est plafonnée à zéro caractère.
Ces trois réglages ferment les problèmes classiques d’expansion d’entités et d’entités externes. Ce n’est pas un durcissement que nous pourrions relâcher pour un analyseur plus accommodant ; une URL de guide a exactement la forme d’entrée que ces attaques visent.
Il existe une règle correspondante côté serveur, et elle est dite aux utilisateurs plutôt que cachée : vos appareils téléchargent les fichiers de guide directement, et nos serveurs ne les récupèrent jamais pour vous. Un serveur qui irait chercher des URL arbitraires fournies par les utilisateurs serait une machine à forger des requêtes, donc il ne le fait pas.
Onze limites, et pourquoi chacune existe
Durcir le lecteur ne suffit pas, parce que l’entrée dangereuse en pratique n’est pas malveillante. C’est un guide avec 40 éléments de titre.
depth 32 nesting
text 64 KB any single text node
description 4 KB aggregate across repeats
title 1 KB aggregate across repeats
credits 15 per programme
categories 6 per programme
alternate names 8 per channel
channel batch 512 rows written at a time
programme batch 2,000 rows written at a time
programme duration 6 hours ceiling
retention 8 days forward, 1 day back
Le plafond sur le titre est celui dont la raison est la plus instructive. Un programme peut légalement porter un titre par langue. Un guide qui en émet 40 n’est pas malformé, il est exhaustif, et sans plafond les 40 sont concaténés dans un seul titre stocké. Le plafond sur la description est la même règle pour la même raison.
Les tailles de lot relèvent du débit plutôt que de la sécurité : un guide national, ce sont des centaines de milliers de programmes, et les écrire un par un fait la différence entre un guide qui se charge et un guide qui semble figé.
La fenêtre de rétention est celle qui surprend les gens. Les programmes en dehors sont écartés pendant l’analyse plutôt que stockés puis élagués ensuite. En avant, le défaut est de 8 jours et en arrière de 1, bornés à 14 et 7. Un fournisseur qui publie 14 jours de grille vers un client configuré pour 8 ne perd pas de données à cause d’un bug ; on lui dit qu’un lecteur n’a pas assez besoin de la grille de la quinzaine suivante pour la stocker.
Cela a une conséquence directe pour le replay : les entrées de replay sont construites à partir des lignes de programme, donc la rétention du guide est un plafond sur la profondeur à laquelle vous pouvez remonter. Ce que sont le replay et le timeshift couvre cette interaction.
Huit façons pour un téléchargement d’échouer
Les échecs sont classés en 8 types plutôt que signalés comme une seule erreur : pas d’URL, pas du XML, réseau, limite de taille, limite de décompression, limite de sécurité, annulé, et inconnu. Limite de décompression et limite de sécurité sont séparées de la panne réseau à dessein, parce qu’elles veulent dire des choses réellement différentes : l’une est une bombe de compression, l’autre un document hostile, et la dernière une mauvaise connexion.
Les trois défauts qu’un jeu de fichiers a révélés
Les vrais fichiers de guide sont pires que ce que le format autorise. Nous avons des chiffres là-dessus plutôt qu’une impression.
Un jeu de 51 fichiers de guide réels a révélé 3 défauts distincts. Chacun était capable de détruire le guide entier d’un fournisseur pour un seul octet.
Une esperluette non échappée dans un titre de programme. Le XML exige &, beaucoup de générateurs écrivent un & nu, et un analyseur strict s’arrête à cet endroit. Tout ce qui suit est perdu.
Un fichier déclarant de l’UTF-8 tout en transportant des octets Latin-1. L’en-tête dit une chose et le corps en est une autre, et le premier caractère accentué d’un titre de film français est l’endroit où tout s’arrête.
Un caractère de contrôle égaré dans une description. Pas du XML légal quel que soit l’encodage, et là encore fatal à l’endroit où il apparaît.
Les 3 sont désormais réparés plutôt que fatals, et les réparations sont comptées, si bien qu’un fichier qui a eu besoin d’être rafistolé le dit au lieu de faire semblant de fonctionner sans bruit. La raison de publier ceci plutôt que de le corriger silencieusement, c’est que cela calibre les attentes : si vous supposez que les fichiers de guide sont bien formés, les échecs n’ont aucun sens.
Avant le XML : déterminer ce qu’est le fichier, tout simplement
Une URL de guide qui se termine par .xml.gz est un indice, pas un fait. Les fournisseurs servent du gzip depuis des chemins finissant en .xml, du XML brut depuis des chemins finissant en .gz, et les deux depuis des chemins qui ne finissent par rien du tout, et un en-tête de type de contenu n’est pas plus fiable que le chemin.
Le conteneur est donc décidé en lisant les tout premiers octets et en les comparant à des nombres magiques connus :
1F 8B gzip
50 4B 03 04 zip
FD 37 7A 58 5A 00 xz
anything else treat as uncompressed
Quatre issues, et la quatrième est la plus importante : un en-tête non reconnu n’est pas une erreur, c’est un fichier supposé être du XML brut. C’est le bon défaut, parce que le cas courant pour un en-tête non reconnu est un document XMLTV brut, et que le mauvais cas échoue proprement dans l’analyseur un instant plus tard plutôt que dans le décompresseur avec quelque chose de plus déroutant.
La seule forme pour laquelle cela n’aide pas est une URL qui répond par une page d’erreur HTML. Elle n’a pas de nombre magique, elle est donc traitée comme non compressée, passée au lecteur XML, et rejetée comme n’étant pas du XML. L’échec est correct et il est à un pas de la cause, ce qui vaut la peine d’être su quand vous diagnostiquez une URL de guide qui fonctionnait avant.
Deux plafonds sur la décompression, et pourquoi l’un d’eux est un ratio
Un guide compressé est une archive non fiable, et une archive qui se déploie jusqu’à remplir un disque est une vieille attaque qui a un nom. La décompression est donc bornée deux fois plutôt qu’une.
Le plafond absolu est de 2 GiB en sortie. C’est délibérément généreux : les guides nationaux de plusieurs pays font réellement des centaines de mégaoctets une fois décompressés, et une limite qui tronquerait un guide légitime serait pire que le problème qu’elle prévient.
La deuxième borne est la plus intéressante. La sortie est aussi plafonnée à 400 fois les octets compressés réellement reçus. Une garde sur le ratio attrape le cas que le plafond absolu manque : un fichier de 40 KB qui se déploie vers des gigaoctets est arrêté à 16 MB, bien avant d’approcher 2 GiB, parce que rien dans son profil de taille ne ressemble à un vrai guide.
Le XML se compresse extrêmement bien, donc 400 pour 1 n’est pas une limite serrée pour des données honnêtes. C’est le point au-delà duquel un fichier a cessé de ressembler à une grille et commencé à ressembler à une charge utile.
À quoi ressemble vraiment la couverture des identifiants
La jointure par identifiant n’est utile que si les playlists portent des identifiants, et la couverture varie plus qu’on ne l’espérerait.
Mesuré sur notre propre corpus de playlists, 2 203 entrées au total :
playlist_01.m3u 308 entries 308 with tvg-id (100%) 283 group-title 0 tvg-chno
playlist_02.m3u8 1,895 entries 1,392 with tvg-id (73.5%) 1,895 group-title 1,355 tvg-chno
Les deux sont des playlists organisées, de type annuaire, plutôt que des exports d’un panneau de fournisseur, et cette réserve compte ici plus que d’habitude : les listes organisées sont maintenues par des gens qui se soucient des identifiants, et un export de panneau est généré par un logiciel qui, lui, ne s’en soucie pas forcément.
Deux détails de ce tableau comptent. Le second fichier a 20 entrées qui portent un attribut d’identifiant présent et vide. C’est un état différent d’un attribut absent, et un analyseur doit le traiter comme absent plutôt que comme un identifiant qui se trouve être la chaîne vide, sinon ces 20 chaînes se joignent toutes les unes aux autres.
Et la couverture des groupes est de 100 pour cent dans le second fichier alors que la couverture des identifiants est de 73.5 pour cent. Le regroupement et la correspondance avec le guide sont des préoccupations distinctes portées par des attributs distincts, ce qui explique qu’une playlist puisse arriver parfaitement organisée en catégories et n’afficher aucune grille du tout.
Pourquoi les noms d’affichage ne peuvent pas sauver une jointure ratée
L’idée suivante qui vient naturellement, quand les identifiants ne correspondent pas, c’est de faire correspondre le nom lisible à la place. C’est un problème bien plus difficile qu’il n’y paraît.
Un nom d’affichage est du texte libre, il peut y en avoir plusieurs par chaîne en plusieurs langues, et les fournisseurs les décorent. La même chaîne dans deux sources peut porter un préfixe de pays, un suffixe de qualité, un séparateur qui est une barre verticale dans une liste et un deux-points dans l’autre, et n’importe quelle quantité d’ornements unicode. Rien de tout cela n’est malformé ; c’est ainsi que le champ est utilisé.
La correspondance par nom est donc un problème de normalisation à longue traîne, et une mauvaise correspondance est pire que pas de correspondance : une chaîne qui affiche la grille d’une autre est un bug auquel un spectateur fera confiance pendant plusieurs minutes avant de s’en rendre compte. L’identifiant existe précisément pour que ces devinettes soient inutiles, et le bon correctif quand il échoue est de faire concorder les identifiants plutôt que de deviner plus fort.
C’est l’argument pratique pour préférer le XMLTV de votre propre fournisseur à celui d’un tiers. Pas parce que les données sont meilleures, mais parce que leur guide et leur playlist sortent d’un même système, si bien que les identifiants concordent par construction.
Lire les chiffres qu’une analyse renvoie
Une analyse rapporte plus qu’un succès. Elle renvoie 7 champs, et ils répondent à des questions différentes :
Channels how many channel declarations were kept
Programmes how many programme rows were kept
Dropped how many were discarded, mostly by the retention window
BytesCompressed what arrived over the network
BytesUncompressed what it expanded to
ParseMs how long it took
CompletedAtUtc when
Cette combinaison répond à des questions qu’un booléen ne peut pas traiter. Un guide avec 200 000 programmes et 40 000 écartés fait quelque chose de différent d’un guide avec 200 000 et 0. Plusieurs diagnostics découlent directement de combinaisons comme celle-là.
Des tailles compressée et décompressée presque égales signifient que le fichier n’était pas compressé, ce qui pour une URL finissant en .gz veut dire qu’on vous sert autre chose que ce que le nom promet.
Un grand nombre de programmes avec un grand nombre d’écartés est un fournisseur qui publie plus loin en avant que ce que votre fenêtre de rétention conserve. C’est normal, et c’est la chose à changer si le replay est moins profond que vous ne l’attendiez.
Une analyse terminée en 40 millisecondes sur un fichier qui devrait faire 60 MB n’a pas lu un guide. Elle a lu une page d’erreur, et le type d’échec le dira.
Un nombre de chaînes dans les milliers avec un nombre de programmes à zéro est la forme d’un guide qui porte des déclarations de chaînes et aucune grille, ce que certains fournisseurs publient entre deux exécutions de génération.
Ce qu’il faut en retenir
Si votre guide est vide, vérifiez les identifiants avant toute autre chose. C’est la jointure, et c’est là que la plupart des problèmes se produisent.
Si votre guide est présent mais décalé, vérifiez si le fichier porte des décalages.
Si votre guide est partiel, vérifiez s’il a été tronqué en transit, ce qui est un échec précis avec une signature précise : un guide qui s’arrête à mi-chemin.
Et quand vous pouvez choisir entre le XMLTV d’un fournisseur et celui d’un tiers, préférez celui du fournisseur, pour la raison donnée plus haut. Le guide est vide déroule le diagnostic de bout en bout.
Ce que cet article a mesuré23 affirmations, chacune avec les preuves derrière elles
| Réclamation | Preuve | Compté |
|---|---|---|
| Un document XMLTV est un élément tv contenant des éléments channel et des éléments programme. Un channel porte un id et un ou plusieurs noms d’affichage ; un programme porte une référence de chaîne, une heure de début, généralement une heure de fin, et un titre.The XMLTV DTD, which defines tv as containing zero or more channel elements followed by zero or more programme elements, with id REQUIRED on channel and channel plus start REQUIRED on programme. | Spécification | Sans objet |
| Un horodatage XMLTV est une chaîne de chiffres allant de l’année jusqu’à la seconde, librement inspirée d’ISO 8601, avec un fuseau horaire facultatif ajouté à la fin. Le décalage est facultatif et son absence n’est pas indéfinie : la DTD dit que l’UTC est supposé.xmltv.dtd, date field documentation, verbatim: "All dates and times in this DTD follow the same format, loosely based on ISO 8601. They can be 'YYYYMMDDhhmmss' or some initial substring ... You can also append a timezone to the end; if no explicit timezone is given, UTC is assumed." | Spécification | Sans objet |
| Le lecteur est configuré pour traiter un guide comme une entrée non fiable : les définitions de type de document sont ignorées, le résolveur externe est null, et l’expansion des entités est plafonnée à zéro caractère. | n = 1 | 31 août 2026 |
| Onze limites structurelles sont imposées pendant l’analyse : profondeur 32, texte 64 KB, description 4 KB, titre 1 KB, 15 crédits, 6 catégories, 8 noms alternatifs, des lots de 512 chaînes et 2 000 programmes, et un plafond de durée de programme de 6 heures. | n = 11 | 31 août 2026 |
| Le plafond sur le titre existe parce qu’un programme peut légalement porter un titre par langue, et qu’un guide qui en émet quarante les concaténerait sinon tous les quarante dans un seul titre stocké. | n = 1 | 31 août 2026 |
| Les programmes hors d’une fenêtre de rétention sont écartés pendant l’analyse plutôt que stockés puis élagués ensuite. La fenêtre est par défaut de 8 jours en avant et 1 jour en arrière, bornée à 14 et 7. | n = 1 | 31 août 2026 |
| L’analyse rapporte combien de programmes elle a écartés à côté de combien elle en a gardés, ainsi que les nombres d’octets compressés et décompressés, si bien qu’un guide arrivé à moitié se distingue d’un guide pertinent à moitié. | n = 1 | 31 août 2026 |
| L’ingestion de guide distingue huit types d’échec plutôt que de signaler une seule erreur, dont une limite de décompression et une limite de sécurité comme cas séparés d’une simple panne réseau. | n = 8 | 31 août 2026 |
| Un jeu de 51 vrais fichiers de guide a révélé trois défauts qui, chacun, faisaient perdre le guide entier d’un fournisseur pour un seul octet : une esperluette non échappée, de l’UTF-8 déclaré sur des octets Latin-1, et un caractère de contrôle égaré. Les trois sont désormais réparés et les réparations sont comptées. | n = 51 | 16 août 2026 |
| Le conteneur est détecté à partir des premiers octets du fichier plutôt que de son extension ou de son type de contenu déclaré. Quatre formes sont reconnues par leur nombre magique et tout le reste est traité comme non compressé. | n = 4 | 31 août 2026 |
| La décompression est bornée deux fois : un plafond absolu de 2 GiB en sortie, et un plafond de taux de compression de 400 pour 1 mesuré contre la taille compressée réellement reçue. | n = 1 | 31 août 2026 |
| Dans notre propre corpus de playlists, la couverture des identifiants de guide était de 100 pour cent dans un fichier et de 73.5 pour cent dans l’autre, avec 20 entrées portant un attribut présent mais vide. | n = 2203 | 31 août 2026 |
| Une chaîne doit porter au moins un nom d’affichage, et peut en porter plusieurs. L’élément est déclaré comme un-ou-plusieurs plutôt que facultatif, ce qui explique pourquoi la correspondance par nom voit plusieurs candidats par chaîne.xmltv.dtd, verbatim: "<!ELEMENT channel (display-name+, icon*, url*) >" with "<!ATTLIST channel id CDATA #REQUIRED >". | Spécification | Sans objet |
| Le modèle de contenu d’un programme compte plus de vingt éléments enfants facultatifs, dont seul le titre est requis, ce qui explique pourquoi la richesse des guides varie tant d’un fournisseur à l’autre.xmltv.dtd programme element declaration, which requires title+ and then permits sub-title, desc, credits, date, category, keyword, language, orig-language, length, icon, url, country, episode-num, video, audio, previously-shown, premiere, last-chance, new, subtitles, rating, star-rating, review and image. | Spécification | Sans objet |
| L’heure de fin d’un programme est facultative alors que son début et sa chaîne sont requis, si bien qu’un guide peut légalement omettre l’information dont un lecteur a besoin pour dimensionner une ligne de grille.xmltv.dtd, verbatim: "start CDATA #REQUIRED, stop CDATA #IMPLIED ... channel CDATA #REQUIRED", with pdc-start, vps-start, showview and videoplus also implied and clumpidx defaulting to "0/1". | Spécification | Sans objet |
| Le document ordonne ses enfants : toutes les déclarations de chaînes précèdent tous les programmes, ce qui permet à un analyseur en flux de construire sa table des chaînes avant d’avoir à résoudre des références.xmltv.dtd, verbatim: "<!ELEMENT tv (channel*, programme*)>". | Spécification | Sans objet |
| L’analyse se fait en flux plutôt qu’en chargeant le fichier d’un bloc, en écrivant les lignes par lots de 512 chaînes et 2 000 programmes, ce qui évite qu’un guide national doive tenir en mémoire. | n = 2 | 1 sept. 2026 |
| Un programme de plus de 6 heures est traité comme hors limites, ce qui borne les dégâts d’une heure de fin absente ou malformée. | n = 1 | 1 sept. 2026 |
| Les crédits sont plafonnés à 15 par programme et les catégories à 6, qui sont les champs qu’un générateur généreux gonfle le plus. | n = 2 | 1 sept. 2026 |
| Les noms de chaîne alternatifs sont plafonnés à 8, parce qu’une chaîne peut légalement porter un nom d’affichage par langue et qu’un analyseur doit borner ce qu’il conserve. | n = 1 | 1 sept. 2026 |
| Côté serveur, le guide est conservé dans un magasin de séries temporelles plutôt que dans une table relationnelle ordinaire, ce qui est un problème différent du cache local du client. | n = 1 | 1 sept. 2026 |
| Une correction manuelle de correspondance est l’une des treize familles d’état synchronisées, stockée au niveau du profil contre une clé de chaîne indépendante de la source, si bien qu’une correction couvre tous les fournisseurs qui diffusent cette chaîne. | n = 1 | 1 sept. 2026 |
| Les fichiers de guide sont récupérés par les propres appareils de l’utilisateur. Aucune URL fournie par l’utilisateur n’est récupérée par nos serveurs, et l’écran des réglages le dit à l’utilisateur. | n = 1 | 31 août 2026 |