Files
castarr/tests/demo/make-demo-data.py
be-nj 4cb7a6efdb
All checks were successful
Build TV app / build (push) Successful in 43s
Act on the screen-by-screen review: 17 findings
Defects. "Kopplung zurücksetzen" now asks first, naming how many phones
would have to scan again — one stray OK used to wipe a working pairing, and
the settings put the focus right on it: entering the screen landed on the
gear, one press down jumped into the right column onto that very action, and
every further press down did nothing. Focus now starts on the first row of
the left column. 1. FC Köln was labelled "EFC". An unreachable source used
to look like an evening without programmes — cached list, empty EPG, club
menus counting down to zero — so both repositories now record when they last
came through, and the top bar says "Server nicht erreichbar · Stand 14:46".
The field labels on "Erweitert" sat on the outline with the border running
through them; they sit above their field now.

Polish. The app opens on the channel list — on the channel last watched,
which survives a restart — instead of the settings gear. Rows without EPG
fill their empty half with the channel's group. The club chip says "ab
15:06" like the row next to it, the club menu carries the club's name
instead of "FCSP", and a fixture title is shortened to the fixture, so
"Pokal: Nordstadt - FC St. P…" became "Nordstadt - FC St. Pauli".
The settings are three columns and no longer scroll (CONTEXT.md's ten-foot
rule).

Features. Favourites work for a plain M3U source too, stored on the device
and mirrored to the phone; they used to exist only with a Dispatcharr login.
The channel keys jump the list by initial letter, left and right jump a
division in the club picker, and right on a channel opens its day plan —
the EPG beyond now/next, which was parsed all along and never shown. When a
club match starts elsewhere the player offers to switch. The remote asked
for a code "unten auf dem TV-Bildschirm" where none was; the welcome screen
shows it now and the wording points at where it is.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-26 15:54:00 +02:00

116 lines
5.0 KiB
Python
Executable File

#!/usr/bin/env python3
"""Neutral demo line-up for screenshots and manual testing.
Invented channels and programmes only — no real provider data ever ends up
in a screenshot. One club match is placed a few minutes ahead so the club
menu shows its "kick-off imminent" state.
"""
import argparse
import datetime
import os
p = argparse.ArgumentParser()
p.add_argument("--out", default=os.path.dirname(os.path.abspath(__file__)))
p.add_argument("--kickoff-in", type=int, default=18,
help="minutes until the club match starts")
p.add_argument("--now", default=None,
help="base time as YYYY-mm-ddTHH:MM:SS; defaults to this "
"machine's clock. An emulator restored from a snapshot "
"runs on the clock it was saved with, so pass its time "
"here or every programme lands in the wrong hour")
args = p.parse_args()
if args.now:
now = datetime.datetime.strptime(args.now, "%Y-%m-%dT%H:%M:%S").astimezone()
else:
now = datetime.datetime.now().astimezone()
def fmt(dt):
# Local offset, so the emulator's clock agrees with ours (CEST vs CET).
return dt.strftime("%Y%m%d%H%M%S %z")
CHANNELS = [
("Unterhaltung", "Blau TV HD", "blau"),
("Unterhaltung", "Nordlicht HD", "nordlicht"),
("Unterhaltung", "Kanal Sieben", "sieben"),
("Filme", "Kinohaus HD", "kinohaus"),
("Filme", "Filmwerk Classic", "filmwerk"),
("Filme", "Nachtkino HD", "nachtkino"),
("Serien", "Serienbox HD", "serienbox"),
("Serien", "Staffel Eins", "staffel"),
("Sport", "Arena Sport 1 HD", "arena1"),
("Sport", "Arena Sport 2 HD", "arena2"),
("Sport", "Liga Live UHD", "ligalive"),
("Sport", "Stadionkanal HD", "stadion"),
("Nachrichten", "Tagesblick HD", "tagesblick"),
("Nachrichten", "Weltbericht", "weltbericht"),
("Doku & Wissen", "Terra Doku HD", "terra"),
("Doku & Wissen", "Wissenswelt", "wissen"),
("Kinder", "Krümelkiste", "kruemel"),
("Musik", "Tonspur HD", "tonspur"),
("Regional", "Hafenkanal HD", "hafen"),
("International", "Globus TV", "globus"),
]
PROGRAMMES = {
"blau": [("Morgenmagazin", -40, 80), ("Kochduell", 40, 60)],
"nordlicht": [("Küstenrevier", -20, 70), ("Nordlicht Talk", 50, 45)],
"sieben": [("Quiz um Sieben", -10, 60), ("Sieben Reportage", 50, 45)],
"kinohaus": [("Der weite Weg", -55, 125), ("Sturmhöhe", 70, 110)],
"filmwerk": [("Schwarzweiß", -30, 95), ("Die Erbschaft", 65, 100)],
"nachtkino": [("Mitternachtszug", -15, 105), ("Letzte Runde", 90, 95)],
"serienbox": [("Praxis Ostwind (3/12)", -25, 45), ("Praxis Ostwind (4/12)", 20, 45)],
"staffel": [("Hafenwache (7/10)", -35, 50), ("Hafenwache (8/10)", 15, 50)],
"arena1": [("Handball: Pokalrunde", -45, 105), ("Sport am Abend", 60, 60)],
"arena2": [("Radsport: Etappe 4", -50, 130), ("Motorsport kompakt", 80, 55)],
"ligalive": [("Sport aktuell", -25, 43), ("__CLUB__", None, 120)],
"stadion": [("Stadionmagazin", -20, 60), ("FC St. Pauli: Der Rückblick", 40, 45)],
"tagesblick": [("Tagesblick am Mittag", -12, 30), ("Wetter und Verkehr", 18, 15)],
"weltbericht": [("Weltbericht Spezial", -35, 75), ("Auslandsjournal", 40, 45)],
"terra": [("Wale der Arktis", -40, 90), ("Vulkane Islands", 50, 45)],
"wissen": [("Wie Brücken halten", -20, 45), ("Chemie des Alltags", 25, 45)],
"kruemel": [("Bärenbande", -15, 25), ("Malstunde", 10, 25)],
"tonspur": [("Charts der Woche", -60, 120), ("Akustik-Session", 60, 60)],
"hafen": [("Hafenrundfahrt", -30, 60), ("Regional um sechs", 30, 30)],
"globus": [("Globus Reportage", -45, 90), ("Fernweh", 45, 60)],
}
CLUB_TITLE = "Pokal: Nordstadt - FC St. Pauli, 1. Runde"
os.makedirs(args.out, exist_ok=True)
m3u = os.path.join(args.out, "playlist.m3u")
xml = os.path.join(args.out, "epg.xml")
with open(m3u, "w", encoding="utf-8") as f:
f.write("#EXTM3U\n")
for group, name, cid in CHANNELS:
f.write(f'#EXTINF:-1 tvg-id="{cid}" group-title="{group}",{name}\n')
f.write(f"http://10.0.2.2:8099/stream/{cid}.ts\n")
def escape(text):
return text.replace("&", "&amp;").replace("<", "&lt;").replace(">", "&gt;")
with open(xml, "w", encoding="utf-8") as f:
f.write('<?xml version="1.0" encoding="UTF-8"?>\n<tv>\n')
for _, name, cid in CHANNELS:
f.write(f' <channel id="{cid}"><display-name>{escape(name)}</display-name></channel>\n')
for cid, items in PROGRAMMES.items():
for title, offset, length in items:
if title == "__CLUB__":
title, offset = CLUB_TITLE, args.kickoff_in
start = now + datetime.timedelta(minutes=offset)
stop = start + datetime.timedelta(minutes=length)
f.write(
f' <programme start="{fmt(start)}" stop="{fmt(stop)}" channel="{cid}">'
f"<title>{escape(title)}</title></programme>\n"
)
f.write("</tv>\n")
print(f"{len(CHANNELS)} Sender, {sum(len(v) for v in PROGRAMMES.values())} Sendungen")
print(f"Anstoß in {args.kickoff_in} Min: {CLUB_TITLE}")
print(f"-> {m3u}\n-> {xml}")