m3u plus frente a m3u: los atributos que añade una lista de reproducción extendida, y cómo analizarlos
No hay especificación de M3U+. Hay una convención, hay un corpus, y hay un analizador que tiene que sobrevivir a las dos cosas.
#EXTINF:-1 tvg-id="" tvg-logo="" group-title="News;General" tvg-chno="1",Example One
https://provider.example.com/stream/one.m3u8
Dos líneas. Esa fila está montada, no copiada: las dos listas de reproducción que guardamos para trabajo de análisis nombran emisoras reales, así que aquí no hay nada sacado de ellas literalmente. Todas las formas que aparecen en ella sí están en esos archivos, y ninguno de los valores es distintivo. En el archivo de 1895 entradas, 107 filas reclaman el canal número 1, 20 llevan un tvg-id vacío y 42 llevan un tvg-logo vacío. En el archivo de 308 entradas, 25 filas llevan un group-title en blanco, 19 llevan un tvg-logo vacío y 10 meten dos nombres de grupo en un solo atributo con un punto y coma.
Casi todo lo que en la primera línea parece dato estructurado está ausente, en blanco o compartido con cien canales sin relación entre sí. El único campo que está presente de forma fiable y además es casi distinto en cada fila es la URL de la segunda línea: 308 URLs distintas en 308 entradas del archivo pequeño, 1884 en 1895 del grande.
El reproductor no contiene ningún canal y no puede aportar ninguno. Lo que aparece en él sale del archivo al que lo apuntas, analizado en tu propio dispositivo, y eso significa que el comportamiento de ese análisis frente a una entrada imperfecta es casi todo lo que acaba siendo tu lista de canales.
No hay especificación, hay una convención
RFC 8216 define #EXTINF para HLS como una duración, una coma y un título opcional legible por personas que llega hasta el final de la línea. No dice nada de tvg-id, tvg-chno, group-title, tvg-logo, catchup-source ni de ninguno de los demás tokens que los archivos reales meten entre la duración y la coma.
Esos tokens vienen de una convención posterior de listas de TV en directo que no tiene un nombre acordado, y los dos nombres a los que la gente echa mano son erróneos. Extended M3U es la convención de la época de Winamp que introdujo #EXTM3U y #EXTINF en sí mismos; RFC 8216 la cita por su nombre en la sección 4.3.1.1, y no contiene ningún tvg- de nada. M3U+ no es un formato en absoluto: m3u_plus es un modo de salida de los servidores que hay detrás de un acceso del proveedor, el valor de consulta que hace que una exportación pase de URLs peladas a filas que llevan tvg-id, tvg-logo y group-title. Una de esas dos cosas es una convención de listas de reproducción de los años noventa, la otra es un parámetro de consulta, y ninguna es una especificación. La convención la define lo que emitan los emisores.
Esa distinción atraviesa todo lo que viene abajo. Lo que la convención permite y lo que un analizador acepta son dos listas distintas, y mezclarlas es como un analizador acaba codificando las costumbres de una fuente como si fueran una regla. Así que: la gramática que implementa nuestro analizador es esta, y nada más.
line := "#EXTINF:" WS* duration WS* attributes "," displayName
duration := "-"? DIGIT+
La coincidencia de prefijo es ordinal, así que #EXTINF: distingue mayúsculas y minúsculas hasta los dos puntos, y el analizador tiene el prefijo fijado a ocho caracteres antes de pasarle el resto a un escáner de duración. Ese escáner admite un único signo menos opcional, luego exige al menos un dígito y luego consume la serie de dígitos. Anota dónde terminaron los dígitos y tira el valor. La duración no se lee en ningún punto de la tubería. Las 2203 líneas #EXTINF de nuestras dos listas de reproducción de muestra llevan -1, que es la forma que tiene la convención de decir «en directo, sin duración conocida», y a ningún camino de código le importaría que dijeran 7200.
De ese escáner salen directamente tres consecuencias. +1 se rechaza, porque un más no es ni un menos ni un dígito. .5 se rechaza, porque antes del punto tiene que ir un dígito. Y una duración fraccionaria como #EXTINF:120.5,Example se analiza bien por accidente: el escáner se detiene en el punto, el .5 sobrante cae dentro de la región de atributos y ninguna búsqueda de atributo llega a casar con él. RFC 8216 permite esa forma fraccionaria y nuestro analizador la acepta, pero nadie la implementó, y eso conviene decirlo claro en vez de archivarlo bajo «admitimos decimales».
Hay una asimetría dentro de esa gramática que conviene reconocer, aunque no en la dirección que parece al principio. El prefijo de la etiqueta se compara distinguiendo mayúsculas y minúsculas, que es lo que RFC 8216 exige en la sección 4.1 para toda etiqueta que empiece por #EXT. Los nombres de atributo se comparan sin distinguirlas, cosa que no exige nadie. Así que #extinf: en minúsculas se omite como directiva no reconocida mientras que TVG-CHNO = "18" en esa misma línea se analiza sin problema, y la mitad que se desvía del RFC es la permisiva. La incoherencia llegó al portar un analizador más antiguo del lado del servidor, y el archivo fuente lo sigue diciendo arriba del todo. Ninguna de las dos listas de reproducción de muestra contiene una etiqueta en minúsculas, un nombre de atributo en mayúsculas ni un espacio alrededor de un signo igual, así que nada de nuestro corpus ha ejercitado nunca ninguna de las dos mitades.
La coma que no es un delimitador
Parte una línea #EXTINF por su primera coma y habrás roto 30 de las 308 entradas de uno de nuestros dos archivos.
El motivo es que los valores de atributo son cadenas entrecomilladas y las cadenas entrecomilladas contienen comas. Cada una de esas 30 filas lleva un atributo user-agent con una cadena de UA de navegador, y las 30 cadenas contienen el fragmento KHTML, like Gecko. Veintiocho de ellas lo envuelven entre paréntesis y dos han perdido los paréntesis en algún punto anterior, lo que no cambia nada respecto a la coma. Esta es la comparación real, hecha sobre el archivo de verdad, con los nombres de host de la fuente quitados y el nombre del propio canal sustituido:
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)
El nueve punto siete por ciento de ese archivo acaba con un nombre visible hecho con la cadena de versión del navegador de alguien. El catálogo se construye igual. No falla nada. La lista de canales está mal de una forma que parece un fallo de renderizado y no lo es.
El arreglo es un escáner escrito a mano, no una expresión regular: recorrer la región de atributos carácter a carácter, voltear una bandera inQuotes en cada comilla doble y devolver la primera coma que se vea mientras esa bandera esté a falso. Todo lo que queda a la izquierda es la región de atributos, todo lo que queda a la derecha es el nombre visible, y a los dos se les recortan los espacios. Son nueve líneas de código y son las nueve líneas de mayor valor del analizador.
Ese escáner decide además qué le pasa a una fila mal formada, y la decisión es estricta. Si la región de atributos tiene un número impar de comillas dobles, por ejemplo group-title="Movies,Example Four donde se perdió la comilla de cierre, la bandera no vuelve nunca a falso, no se encuentra nunca una coma fuera de comillas y se rechaza la entrada entera con un diagnóstico. No hay respaldo que caiga a la primera coma ni intento de adivinar dónde debería haber estado la comilla. Elegimos el rechazo antes que la reparación porque una fila reparada está mal en silencio mientras que una fila rechazada falta a gritos, y una fila que falta a gritos es la que un lector puede reportar.
Se leen cinco atributos, y un archivo trae 1895 filas de un sexto que ignoramos
El conjunto completo de atributos que busca nuestro analizador son cinco grafías:
group-title, conservado hasta el primer punto y coma, y en blanco se reduce a nadatvg-logo, con los espacios recortadoslogo, consultado solo cuandotvg-logofalta por completotvg-id, tomado en bruto al analizar y sometido a la regla de confianza más tardetvg-chno, entregado a un analizador de números al que se le permite no devolver nada
Esa es la lista entera. Los nombres de atributo se comparan sin distinguir mayúsculas y minúsculas, con espacios opcionales a los dos lados del signo igual, así que TVG-CHNO = "18" y tvg-chno="18" son lo mismo para nosotros. Los valores tienen que ir entre comillas dobles; no hay camino para un valor sin comillas, y las comillas simples no se reconocen en ninguna parte.
Ahora el lado observado, que es más interesante. En el archivo de 1895 entradas, tvg-name aparece en las 1895 filas y las ignoramos todas. tvg-country aparece en 1642 filas y también las ignoramos. El nombre visible sale siempre del texto que va después de la coma, nunca de tvg-name, incluso cuando una fuente da los dos en todas las filas que publica. Eso es una decisión de diseño y no un descuido: el título posterior a la coma es el campo que la propia gramática garantiza, tvg-name es opcional, y tener un único origen con autoridad para el nombre es mejor que un orden de preferencia que se comporta distinto en cada archivo.
El archivo pequeño enseña la capa de directivas que se sienta entre las dos líneas de una entrada. Lleva 30 atributos user-agent, 2 atributos referrer (escrito con dos erres por ese emisor, que no es como se escribe la cabecera HTTP) y 32 líneas de directiva #EXTVLCOPT separadas, 30 de ellas repitiendo el user agent que la línea #EXTINF ya declaraba. No leemos ninguna de ellas.
Otros reproductores leen atributos que nosotros no, y no hay dos listas suyas que sean la misma lista. El complemento IPTV Simple para Kodi documenta catchup, catchup-source, catchup-days, tvg-shift, radio y una directiva #EXTGRP. SS-IPTV, un reproductor para televisores inteligentes, documenta aspect-ratio, que Kodi no. tvg-language venía de iptv-org, que desde entonces lo ha retirado junto con tvg-country. Ninguno de esos tokens aparece en ninguna parte de nuestra muestra y no leemos ninguno, lo que produce una limitación honesta del producto que conviene decir: el Replay lo tenemos, pero está construido sobre el acceso del proveedor, así que una fuente M3U no nos puede exponer Replay por muchos atributos que escriba.
Siete cabeceras, tres canales
Este es un fragmento construido para ejercitar todos los caminos a la vez. Todos los hosts que lleva son un dominio de ejemplo reservado y todos los nombres son inventados. La mayoría de las formas que ejercita sí se dan en nuestro corpus, BS4K101 incluido, que es un valor real sacado del archivo grande. Cinco no se dan en ninguna parte de él: la duración mal formada, la comilla desparejada, el logo a secas, el nombre de atributo en mayúsculas con espacios alrededor de su signo igual y el nombre que empieza por ##. Un fragmento hecho a mano es la única forma de llegar a esos cinco caminos, y conviene saberlo antes de leer el recorrido de abajo como evidencia sobre archivos reales.
#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
Entran siete cabeceras. Del analizador salen cuatro filas, tres de ellas llegan al catálogo, y se producen cinco cadenas de diagnóstico.
La línea 1 se omite. Nuestro analizador no busca #EXTM3U en ningún momento; cae por la omisión genérica de «empieza por almohadilla» junto con todo lo demás que no reconoce, y el atributo x-tvg-url que lleva no se lee. RFC 8216 exige esa cabecera como primera línea de una lista de reproducción. Nosotros no la exigimos, y un archivo sin cabecera que abre directamente con #EXTINF se analiza sin problema, porque la comprobación de forma que corre en el dispositivo antes de analizar acepta cualquiera de las dos etiquetas como primera cosa del archivo, después de una marca de orden de bytes opcional y de los espacios iniciales. No puede precederla nada más: un archivo que abre con una línea de comentario o con un #EXTVLCOPT se rechaza antes de que el analizador llegue a verlo.
Example One se analiza con el nombre de atributo en el caso equivocado y con espacios alrededor del signo igual. Su grupo pasa a ser News, porque solo se conserva el segmento anterior al primer punto y coma. Su número pasa a ser 22.1 como decimal, no 22 como entero, lo cual importa más abajo. Su logo pasa a ser una cadena vacía donde esperarías que no hubiera nada, lo cual también importa más abajo.
Example Two es el caso de la coma entrecomillada, y la línea #EXTVLCOPT que va detrás es la razón por la que la omisión genérica por almohadilla no puede limpiar la cabecera pendiente. Hay directivas que legítimamente se sientan entre un #EXTINF y su URL. Un analizador que reiniciara el estado en cada línea # no reconocida descartaría este canal y todos los canales como él, que en el archivo de muestra pequeño son 30 de 308 entradas.
La fila separadora se analiza limpiamente y es un canal en lo que respecta a la gramática. Su valor tvg-chno, BS4K101, lo rechaza el analizador de números y se queda en nada, lo que cuesta el número pero no la fila. Lo que la elimina es que su nombre empieza por ##, y eso pasa después, durante la normalización de nombres, no al analizar. Mantener las dos etapas separadas significa que el analizador no tiene ninguna opinión sobre lo que significa el nombre de un canal, que es el sitio correcto para esa frontera.
Example Three falla en la duración: x no es ni un menos ni un dígito, así que la cabecera se rechaza con un diagnóstico y se limpian los cinco campos pendientes. Su URL llega entonces sin cabecera pendiente y produce un segundo diagnóstico. Una sola cabecera mal formada cuesta dos líneas y produce dos entradas de log, lo cual es correcto y conviene saberlo cuando leas los diagnósticos de una importación real.
Example Four es la comilla desparejada, y se comporta igual: dos diagnósticos, nada emitido.
Example Five tiene una cabecera perfectamente válida y una URL rtmp. Eso es un URI absoluto, así que se analiza como tal, pero el esquema no es ni http ni https y lo rechazamos. Es la fila más peligrosa del fragmento, porque si el camino de rechazo no limpiara el estado pendiente, Example Six heredaría en silencio el canal número 9. El camino no es hipotético. El archivo de muestra grande termina 6 de sus 1895 entradas en una URL rtmp o rtsp, cinco y una, y esas 6 filas son las únicas de todo el corpus de 2203 entradas que nuestro analizador rechaza.
Example Six lleva un atributo logo a secas, sin tvg-logo y sin group-title de ningún tipo, así que salta el respaldo del logo y el canal acaba en un grupo por defecto que lleva el nombre de la fuente.
Todo camino de omisión tiene que limpiar la cabecera
Una entrada M3U son dos líneas que comparten estado mutable, y esa es toda la dificultad del formato. El analizador guarda cinco campos pendientes entre leer una cabecera y leer una URL: nombre, grupo, logo, id y número. Por el bucle pasan siete caminos distintos, y la corrección del analizador es enteramente una cuestión de qué le hace cada uno a esos cinco.
Las líneas en blanco se omiten sin limpiar, y está bien, porque una línea en blanco entre una cabecera y su URL no debería destruir la entrada. Las directivas # no reconocidas se omiten sin limpiar, por la razón que demuestra el fragmento de arriba. Una cabecera mal formada limpia los cinco. Una URL rechazada limpia los cinco. Una emisión con éxito limpia los cinco.
El séptimo camino es el que la mayoría de los analizadores de pares de líneas hacen mal, y lo interesante es que no se resuelve con un reinicio en absoluto. Cuando llega un segundo #EXTINF antes de ninguna URL, el analizador asigna los cinco campos sin condiciones, sin ningún if y sin ninguna asignación con fusión de nulos. Una búsqueda de atributo que no encuentra nada devuelve nada, y el analizador de números, cuando no se le da nada, devuelve nada, así que una cabecera que no lleva tvg-chno escribe vacío encima del número de la cabecera anterior en vez de dejarlo donde estaba. No hay ningún camino por el que el campo N de la entrada K sobreviva hasta la entrada K+1. Esa es una propiedad más fuerte que «nos acordamos de reiniciar», y merece la pena estructurar un analizador así a propósito, porque la versión con reinicio sigue siendo correcta solo mientras cada edición futura se acuerde de ampliarla.
Un camino merece una nota porque parece un fallo y no lo es. El camino de «llegó una URL sin cabecera pendiente» emite su diagnóstico y no limpia nada. Eso es seguro por construcción y no por suerte, y el invariante se puede comprobar leyendo las asignaciones: el único sitio donde se fijan los otros cuatro campos es el camino de la cabecera, y ese camino fija siempre el nombre a un valor no vacío en la misma pasada, cayendo a la cadena literal Unnamed channel cuando el título está en blanco. Así que un nombre nulo implica que los otros cuatro ya son nulos, y el reinicio que falta es trabajo inalcanzable.
Cada uno de estos caminos necesita su propio test de regresión, y los nueve que tenemos sí cubren los caminos de fuga: uno comprueba que una entrada numerada no filtra su número a la siguiente, y otro comprueba que una cabecera mal formada tampoco. Lo que no tiene ningún test es el escáner de comas entrecomilladas, las nueve líneas que este artículo llamó las nueve líneas de mayor valor del analizador, junto con el rechazo por comillas desparejadas, el caso del #EXTVLCOPT entre las dos líneas y el respaldo del logo a secas. Nada de esa laguna lanza una excepción. El catálogo se construye. Cien canales aguas abajo de una sola fila mala llevan el grupo equivocado.
Los dos campos que parecen identificadores
tvg-chno es dato de presentación, y la medición que lo zanja se hace con una línea de shell. En el archivo de 1895 entradas, 1355 entradas llevan el atributo. Esos 1355 números cubren 263 valores distintos. 107 entradas reclaman el número 1. La duplicación no es un artefacto de haber fusionado grupos, porque sobrevive a la agrupación: un grupo de ese archivo tiene 7 canales todos numerados como 1, y 33 de los 85 grupos que llevan algún número tienen al menos un duplicado dentro. Ese archivo tiene 92 grupos en total, así que 7 de ellos no llevan ningún número. El otro archivo, 308 entradas, no tiene tvg-chno en ninguna parte. Nada puede indexarse por este valor, nada puede suponer que es único, y todo consumidor que ordene por él necesita un desempate determinista.
Analizarlo es un pequeño ejercicio de negarse a ser listo. El valor se lee como número en coma flotante bajo la cultura invariante, no como entero, y nunca bajo la configuración regional del dispositivo. 56 de los 1355 se rechazan de plano. Ninguno de los 56 es numérico:
- 49 son códigos de sistema de difusión: 7 que van de
D011aD081, 19 de una familiaBS101aBS4K101, 22 de una serieCS055aCS363, y un único código con el prefijo de un operador de cable - 4 son un
#literal, aparentemente un marcador de posición que el emisor nunca rellenó - 3 son texto libre con un número enterrado dentro, dos que dicen
CH 106yCH 108, y uno que es un nombre de servicio en japonés terminado en527
El analizador lleva además un tope de 99999, por encima del cual un valor se trata como basura. Nunca ha saltado en este corpus. El número más grande del archivo es 4999, que es un recordatorio útil de que una guarda escrita para una hipótesis no es prueba de que la hipótesis fuera real.
El análisis decimal no es una elección de estilo. 25 entradas de ese archivo usan numeración de subcanal al estilo ATSC, 24 valores distintos que van de 22.1 a 36.2, y un análisis como entero funde 22.1, 22.2 y 22.3 en tres canales todos numerados como 22. La cultura invariante tampoco es una elección de estilo: respetar la configuración regional del dispositivo haría que el mismo archivo produjera números distintos en una máquina configurada con la coma como separador decimal, que es una clase de fallo que solo se reproduce en el ordenador de otra persona.
tvg-id es peor, porque sus fallos no vienen en blanco. En el archivo de 1895 entradas el atributo falta en 483 filas, está vacío en otras 20 y está presente pero incumple nuestra regla de forma en otras 24, así que 527 filas, el 27.8 por ciento del archivo, acaban sin ningún id utilizable. Esas 24 son las incómodas. Diecisiete de ellas parecen exactamente ids reales y se rechazan por un solo motivo, tener menos de seis caracteres. Las otras 7 no llevan punto. Una longitud mínima es una regla tosca, nos cuesta esas 17, y revisaríamos el umbral si la cuenta llegara a crecer.
El motivo de que esa regla exista es un corpus distinto, uno que guardamos para trabajo de análisis, hecho de exportaciones de proveedores y no de los dos archivos curados de arriba. En dos de sus tres fuentes el valor tvg-id más común con diferencia era la cadena literal TS: 446 filas en una, 274 en la otra. TS es el formato contenedor de transport stream, escapándose de un campo de formato de salida a la columna del id. Esos 720 canales no tenían ninguna relación entre sí: un canal de entretenimiento británico y un canal de radio en árabe, uno al lado del otro bajo un mismo identificador. Como la clave de canal se deriva del id, los 720 colapsaron en un único canal lógico. La consecuencia es un favorito que aparece en canales que nadie marcó, un bloqueo parental que bloquea 720 cosas a la vez, y un único hueco de Multivista que representa a todos ellos.
La reparación fue una regla de confianza con un solo hogar, aplicada en la frontera de ingesta para que nada aguas abajo tenga que acordarse de volver a comprobarlo. Un id solo es utilizable si tiene de 6 a 128 caracteres, contiene un punto y no es TS, y lo que sobrevive se pasa a minúsculas bajo la cultura invariante, porque las fuentes discrepan de verdad sobre las mayúsculas del id de un mismo canal. Cualquier otra cosa se normaliza a nada, y «sin id» es un estado que todo consumidor ya sabe manejar, porque la mayoría de los canales genuinamente no tienen ninguno. La regla es necesaria y no suficiente, y lo sabemos: un id que pasaba la comprobación de forma resultó estar enganchado a 74 canales sin relación entre sí, así que allí donde se agregan fuentes corre encima una comprobación de cardinalidad en tiempo de compilación. Ninguno de los dos archivos curados de nuestra muestra contiene un solo id TS, que es la advertencia honesta sobre todo esto. Un corpus limpio nunca te va a enseñar este fallo. Nos enteramos por un informe de error, no por un test.
Sin ningún id utilizable en la mayoría de los archivos, hay que derivar uno, y la derivación es donde el compromiso se pone incómodo. La URL del flujo es el único campo que está siempre y que es casi único dentro de una misma fuente, así que el id sintético es un SHA-1 de esa URL, con los primeros cuatro bytes leídos en little-endian y plegados en un entero de 32 bits positivo. Eso deja aproximadamente 2^31 de espacio de claves y, por la cota del cumpleaños, una lista de reproducción de 50000 entradas lleva alrededor de un 44 por ciento de probabilidad de al menos una colisión interna. Aceptamos ese número con la aritmética delante, con el argumento de que una colisión funde dos canales dentro de una misma fuente, que es lo que ya pasa cuando una fuente lista el mismo canal dos veces. Nuestro propio corpus lo hace en 9 URLs que cubren 20 de las 1895 filas del archivo grande, y ni una sola vez en el de 308 entradas, así que es un suceso real pero no rutinario, y esa medición debilita el argumento más de lo que lo apoya. Lo que zanja de verdad la cuestión es que ampliar la base para incluir tvg-id, o pasar a un hash más ancho, no se puede enviar: el id derivado se escribe en datos guardados que otros dispositivos vuelven a leer, así que cambiar la derivación los invalida todos de golpe. La tasa de colisión es el precio de un formato de intercambio congelado.
En blanco no es ausente, salvo en un campo
Tres atributos, tres políticas distintas para la cadena vacía, y no nos dimos cuenta hasta medir.
group-title="" se reduce a nada, porque el normalizador de grupos trata un blanco como ausente, y 25 filas del archivo pequeño toman ese camino. tvg-id="" se reduce a nada, porque la regla de confianza lo rechaza. tvg-logo="" no se reduce: llega como cadena vacía y viaja hasta el campo de icono de la fila de catálogo. 19 filas de un archivo de muestra y 42 del otro llevan un atributo de logo vacío, así que 61 filas de un corpus de 2203 filas guardan una cadena vacía donde cualquier otro campo no guardaría nada. El logo es la excepción sin más razón que el orden en que se escribieron las tres reducciones.
Lo que eso cuesta depende de un camino que no hemos leído. Si el cargador de imágenes trata igual una cadena vacía y un nulo, esto es una verruga y nada más. Si una cadena vacía inicia una petición que falla, 61 filas están haciendo trabajo innecesario en cada renderizado, y la verruga es un pequeño fallo. Alguien debería leer ese camino antes de que esta página afirme una cosa o la otra.
Lo que no hemos medido
Algunos límites de este analizador no tienen ningún caso testigo en nuestra muestra, y decirlo es más útil que fingir que están probados. No hay camino para un valor sin comillas: tvg-chno=18 sin comillas es invisible para nosotros. Cero de las 2203 líneas #EXTINF que tenemos usan un valor sin comillas, así que nunca nos ha mordido, y no sabemos lo común que es esa forma ahí fuera. El respaldo del logo a secas tiene el mismo problema al revés, ya que cero de nuestras filas lo usan, así que un respaldo que enviamos no tiene ningún test contra entrada real.
Escribir esta página encontró un fallo que ya habíamos enviado, que es lo más útil que hizo. Exponer el comparador de atributos con la claridad suficiente para publicarlo dejó a la vista que el patrón no tenía frontera por la izquierda, así que el patrón de logo casaba dentro de cualquier nombre de atributo terminado en logo. Eso incluye provider-logo, un atributo real que documenta el complemento IPTV Simple para Kodi. Una lista de reproducción que llevara provider-logo y ningún tvg-logo cogía la imagen equivocada, y el cortocircuito no nos salvó, porque solo salta cuando tvg-logo está presente. El mismo agujero dejaba que tvg-id casara dentro de x-tvg-id, y group-title y tvg-chno dentro de cualquier cosa terminada en esos.
Nos sorprendió que el arreglo obvio esté mal. Un límite de palabra no funciona aquí: en esta convención todos los nombres de atributo van unidos por guiones, y - no es un carácter de palabra, así que ya hay un límite entre el guion y el nombre y \btvg-logo sigue casando dentro de x-tvg-logo. La condición que quieres de verdad es que el nombre empiece la región de atributos o venga detrás de un espacio en blanco. En .NET eso es un lookbehind de (?<=^|\s), que es portable a menos motores de los que esperarías: Python se niega a compilarlo, porque la mayoría de los motores exigen un lookbehind de ancho fijo y este alterna. Los cuatro atributos llevan ahora esa frontera, y los seis casos de arriba están fijados por tests.
Nuestro reproductor ejecuta todo esto en tu dispositivo, contra la lista de reproducción que tú proporcionas, y cada rechazo produce una cadena de diagnóstico en vez de una excepción, porque un archivo con 40 filas malas de 3000 tiene que importar las otras 2960 y decirte qué se ha quedado por el camino. Las apps de Windows y Xbox hacen ese trabajo en local, y tú apuntas una a una fuente que ya tienes.
Si estás construyendo tu propio analizador, las mediciones de arriba te servirán mejor que la documentación del formato, sobre todo porque no hay mucha documentación del formato. Cuenta tus entradas. Cuenta cuántas veces aparece cada atributo, cuenta cuántos valores distintos tiene, y compara esos dos números antes de indexar nada por ese campo. Eso es una tarde con un corpus que ya tienes, y es la diferencia entre un analizador que funciona con el archivo con el que probaste y uno que funciona con el archivo que te mandaron.
Lo que midió este artículo26 afirmaciones, cada una con la evidencia detrás de ellas
| Reclamar | evidencia | contado |
|---|---|---|
| RFC 8216 define #EXTINF como una duración, una coma y un título opcional que llega hasta el final de la línea.RFC 8216 section 4.3.2.1 | Especificación | No aplicable |
| RFC 8216 permite una duración #EXTINF fraccionaria.RFC 8216 section 4.3.2.1 | Especificación | No aplicable |
| RFC 8216 exige #EXTM3U como primera línea de una lista de reproducción.RFC 8216 section 4.3.1.1 | Especificación | No aplicable |
| RFC 8216 dice que las etiquetas empiezan por #EXT y distinguen mayúsculas de minúsculas, así que una coincidencia de prefijo ordinal es el comportamiento conforme.RFC 8216 section 4.1 | Especificación | No aplicable |
| Extended M3U es la convención de la época de Winamp que introdujo #EXTM3U y #EXTINF, y RFC 8216 la cita por su nombre. No contiene ninguno de los tokens tvg-.RFC 8216 section 4.3.1.1, which reads: the EXTM3U tag indicates that the file is an Extended M3U [M3U] Playlist file | Especificación | No aplicable |
| M3U+ no es el nombre de un formato. m3u_plus es un modo de salida de los servidores que hay detrás de un acceso del proveedor, el valor de consulta que hace que una exportación pase de URLs peladas a filas que llevan tvg-id, tvg-logo y group-title.Provider login API, playlist export parameter type=m3u_plus | Especificación | No aplicable |
| El complemento IPTV Simple para Kodi documenta catchup, catchup-source, catchup-days, tvg-shift, radio, provider-logo y una directiva #EXTGRP.kodi-pvr/pvr.iptvsimple README, playlist attribute reference | Especificación | No aplicable |
| aspect-ratio lo documenta SS-IPTV, no Kodi.SS-IPTV playlist attribute documentation | Especificación | No aplicable |
| tvg-language venía de iptv-org, que desde entonces lo ha retirado junto con tvg-country.iptv-org/iptv repository, playlist field history | Especificación | No aplicable |
| Una lista de reproducción de 50000 entradas sobre un espacio de claves de ~2^31 lleva alrededor de un 44 por ciento de probabilidad de al menos una colisión interna.Birthday bound, 1 - exp(-n(n-1)/2N) with N = 2^31 and n = 50,000, which evaluates to 0.4412 | Especificación | No aplicable |
| Todas las líneas #EXTINF del corpus llevan una duración de -1. | n = 2203 | 22 ago 2026 |
| Partir por la primera coma rompe 30 de las 308 entradas del archivo pequeño; las 30 llevan un valor user-agent que contiene el fragmento KHTML, like Gecko. | n = 308 | 22 ago 2026 |
| 1355 de 1895 entradas llevan tvg-chno, repartidas en 263 valores distintos, con 107 filas que reclaman el número 1 y un grupo que tiene 7 canales todos numerados como 1. | n = 1895 | 22 ago 2026 |
| El archivo grande tiene 92 grupos; 85 de ellos llevan al menos un número, y 33 de esos 85 contienen un número duplicado. | n = 1895 | 22 ago 2026 |
| 56 de los 1355 valores tvg-chno se rechazan, y ninguno de ellos es numérico: 49 códigos de sistema de difusión, 4 marcadores de posición con un # literal, 3 valores de texto libre. El tope de 99999 del analizador no ha saltado nunca; el número más grande del archivo es 4999. | n = 1355 | 22 ago 2026 |
| 25 filas usan numeración de subcanal al estilo ATSC, repartidas en 24 valores distintos que van de 22.1 a 36.2. | n = 1895 | 22 ago 2026 |
| En el archivo grande tvg-id falta en 483 filas, está vacío en otras 20 y está presente pero incumple la regla de forma en otras 24, así que 527 filas (27.8 por ciento) acaban sin ningún id utilizable. | n = 1895 | 22 ago 2026 |
| 17 de esos 24 fallos de forma se rechazan solo por tener menos de seis caracteres, y parecen ids reales. Los otros 7 no contienen ningún punto. | n = 24 | 22 ago 2026 |
| En un corpus aparte de exportaciones de proveedores, TS era el tvg-id más común en dos de tres fuentes, 446 filas y 274 filas, y esos 720 canales sin relación entre sí colapsaron en una sola clave de canal. Un id que pasaba la comprobación de forma estaba enganchado a 74 canales sin relación entre sí. | n = 720 | Corpus anterior |
| Las URLs de flujo duplicadas son reales y raras: 9 valores de URL duplicados que cubren 20 de las 1895 filas del archivo grande, y 0 de 308 en el pequeño. | n = 2203 | 22 ago 2026 |
| 6 filas del archivo grande apuntan a una URL rtmp o rtsp (5 y 1), y son las únicas filas de todo el corpus que el analizador rechaza. | n = 2203 | 22 ago 2026 |
| 61 filas llevan un tvg-logo vacío (19 en el archivo pequeño, 42 en el grande) y la cadena vacía sobrevive hasta la fila de catálogo, mientras que un group-title vacío (25 filas) y un tvg-id vacío se reducen los dos a nada. | n = 2203 | 22 ago 2026 |
| tvg-name aparece en las 1895 filas del archivo grande y tvg-country en 1642; el archivo pequeño lleva 30 atributos user-agent, 2 atributos referrer y 32 líneas de directiva #EXTVLCOPT. | n = 2203 | 22 ago 2026 |
| El corpus no contiene ningún valor de atributo sin comillas, ninguna etiqueta #extinf en minúsculas, ningún nombre de atributo en mayúsculas, ningún espacio alrededor de un signo igual, ninguna fila con comillas desparejadas y ningún atributo logo a secas. | n = 2203 | 22 ago 2026 |
| El fragmento hecho a mano de este artículo da 4 filas analizadas, 3 filas de catálogo y 5 diagnósticos. | n = 7 | 22 ago 2026 |
| El analizador tiene 9 tests unitarios, entre ellos dos que comprueban que los campos no se filtran de una entrada a otra. El escáner de comas entrecomilladas, el rechazo por comillas desparejadas, el caso del #EXTVLCOPT entre las dos líneas y el respaldo del logo a secas no tienen ninguno. | n = 9 | 22 ago 2026 |