I found a solution that is working for me to the unknown playlist format issue with the help of chat GPT.
Tvheadend does not accept quotes on the second line of a channel entry.
Like this:
pipe://streamlink 'https://pluto.tv/live-tv/51c75f7bb6f26ba1cd00002f' best --stdout
They must be:
pipe://streamlink https://pluto.tv/live-tv/51c75f7bb6f26ba1cd00002f best --stdout
also Tvheadend expects tvg-id (lower-case with a dash), not tvg-ID.
The script needed to have the playlist build lines changed to
f'#EXTINF:-1 tvg-id="pluto-{channel["_id"]}" tvg-chno="{channel["number"]}" '
f'tvg-name="{channel["name"]}" tvg-logo="{channel.get("colorLogoPNG", {}).get("path", "")}",'
f'{channel["name"]}\n'
)
m3u += (
f"pipe://streamlink https://pluto.tv/live-tv/{channel['_id']} best --stdout\n"
)
So the complete updated script with the changes made need to be this:
import requests
from datetime import datetime, timedelta, timezone
import xml.etree.ElementTree as ET
import socket
import io
# CONFIG -----------------------------------------------------------------------------
SOCKET = "/config/epggrab/xmltv.sock" # Path to your Tvheadend XMLTV UNIX socket
# ------------------------------------------------------------------------------------
now = datetime.now(timezone.utc)
# round the start time down to the nearest 30 min
start_time = now - timedelta(
minutes=now.minute % 30, seconds=now.second, microseconds=now.microsecond
)
stop_time = start_time + timedelta(hours=8)
epg_begin = start_time.strftime("%Y-%m-%dT%H:%M:%S-00:00")
epg_end = stop_time.strftime("%Y-%m-%dT%H:%M:%S-00:00")
data = requests.get(
f"https://api.pluto.tv/v2/channels?start={epg_begin}&stop={epg_end}"
).json()
m3u = "#EXTM3U\n"
root = ET.Element("tv")
programmes = []
for channel in data:
# ----- XMLTV channel element -----
chan_id = f"pluto-{channel['_id']}"
channel_element = ET.SubElement(root, "channel", id=chan_id)
ET.SubElement(channel_element, "display-name").text = channel["name"]
ET.SubElement(channel_element, "channel-number").text = str(channel["number"])
ET.SubElement(channel_element, "icon").text = channel.get("colorLogoPNG", {}).get(
"path", ""
)
# ----- M3U entry -----
logo = channel.get("colorLogoPNG", {}).get("path", "")
m3u += (
f'#EXTINF:-1 tvg-id="{chan_id}" '
f'tvg-chno="{channel["number"]}" '
f'tvg-name="{channel["name"]}" '
f'tvg-logo="{logo}",'
f'{channel["name"]}\n'
)
# NOTE: no quotes around URL
m3u += (
f"pipe://streamlink https://pluto.tv/live-tv/{channel['_id']} best --stdout\n"
)
# ----- Programme list -----
for programme in channel.get("timelines", []):
programmes.append(
{
"start": programme["start"],
"stop": programme["stop"],
"channel": chan_id,
"title": programme.get("title", "Untitled"),
"episode": programme.get("episode", {}),
}
)
# Build XMLTV EPG
for programme in programmes:
start_str = datetime.fromisoformat(programme["start"]).strftime(
"%Y%m%d%H%M%S +0000"
)
stop_str = datetime.fromisoformat(programme["stop"]).strftime("%Y%m%d%H%M%S +0000")
p = ET.SubElement(
root,
"programme",
start=start_str,
stop=stop_str,
channel=programme["channel"],
)
ep = programme["episode"]
ET.SubElement(p, "title").text = programme["title"]
ET.SubElement(p, "sub-title").text = ep.get("name", "")
ET.SubElement(p, "desc").text = ep.get("description", "")
ET.SubElement(p, "episode-num").text = f"S{ep.get('season', 0)}E{ep.get('number', 0)}"
ET.SubElement(p, "category").text = ep.get("genre", "")
ET.SubElement(p, "category").text = ep.get("subGenre", "")
ET.SubElement(p, "category").text = programme.get("category", "")
tree = ET.ElementTree(root)
ET.indent(tree, space="\t", level=0)
# Send EPG to Tvheadend’s XMLTV socket
xml_bytes = io.BytesIO()
tree.write(xml_bytes, encoding="UTF-8", xml_declaration=True)
try:
with socket.socket(socket.AF_UNIX, socket.SOCK_STREAM) as s:
s.connect(SOCKET)
xml_bytes.seek(0)
s.sendall(xml_bytes.read())
except Exception:
# You can uncomment these lines for debugging
# with open("pluto.xmltv", "wb") as f:
# tree.write(f, encoding="utf-8", xml_declaration=True)
pass
# Print the playlist to stdout — Tvheadend reads this
print(m3u, end="")
Hopefully this can help someone out