medias 0.6.1

TV media player on bare DRM/KMS: Vulkan UI plane over libmpv video, for local files, UPnP/DLNA servers and YouTube, controlled by HDMI-CEC remote, evdev or terminal
#!/usr/bin/env python3
"""Write dist/index.html: apt instructions and a download table per version.

Lists dist/medias-<version>-linux-<arch>.tar.gz and the .deb files in dist/ and
dist/debian/pool. Template: packaging/index.html.in (string.Template).
"""

import argparse
import hashlib
import html
import os
import re
import string
import time
from pathlib import Path

from dist import ROOT, cargo_meta

TEMPLATE = ROOT / "packaging" / "index.html.in"
ARCH_LABEL = {"x86_64": "x86_64", "amd64": "x86_64", "aarch64": "aarch64 (Pi 4/5)", "arm64": "aarch64 (Pi 4/5)"}
KIND_LABEL = {"deb": "Debian package", "tar.gz": "tar.gz"}
NAME = re.compile(r"medias[-_](\d+(?:\.\d+)*)(?:-\d+)?[-_](?:linux-)?(x86_64|aarch64|amd64|arm64)\.(tar\.gz|deb)$")


def version_key(version):
    return tuple(int(part) for part in version.split("."))


def collect(out):
    """{version: [(arch label, kind, href, size, sha256)]}; dist/ wins over the pool."""
    found = {}
    pool = sorted((out / "debian" / "pool").rglob("*.deb")) if (out / "debian" / "pool").is_dir() else []
    for path in sorted(out.glob("medias*")) + pool:
        match = NAME.match(path.name)
        if not match:
            continue
        version, arch, kind = match.groups()
        entries = found.setdefault(version, {})
        key = (ARCH_LABEL[arch], kind)
        if key not in entries:
            entries[key] = (path.relative_to(out).as_posix(), path.stat().st_size, sha256(path))
    return {
        version: [(arch, kind, *entry) for (arch, kind), entry in sorted(entries.items(), key=lambda e: (e[0][0], e[0][1] != "deb"))]
        for version, entries in found.items()
    }


def sha256(path):
    side = Path(f"{path}.sha256")
    if side.exists():
        return side.read_text().split()[0]
    return hashlib.sha256(path.read_bytes()).hexdigest()


def table(rows, indent="    "):
    lines = ["<table>", "  <thead><tr><th>Architecture</th><th>File</th><th>Size</th><th>SHA-256</th></tr></thead>", "  <tbody>"]
    for arch, kind, href, size, digest in rows:
        name = html.escape(href.rsplit("/", 1)[-1])
        lines.append(
            f"    <tr><td>{html.escape(arch)}</td>"
            f'<td><a href="{html.escape(href, quote=True)}">{name}</a> <span class="note">{KIND_LABEL[kind]}</span></td>'
            f'<td class="num">{size / 1048576:.1f} MiB</td><td class="hash">{digest}</td></tr>'
        )
    lines += ["  </tbody>", "</table>"]
    return "\n".join(indent + line for line in lines)


def render(out, version, description, repo_url, now):
    versions = collect(out)
    latest = versions.pop(version, None)
    if latest is None and versions:
        version = max(versions, key=version_key)
        latest = versions.pop(version)
    older = ""
    if versions:
        parts = ["<details>", "  <summary>Older versions</summary>"]
        for old in sorted(versions, key=version_key, reverse=True):
            parts += [f"  <h2>{html.escape(old)}</h2>", '  <div class="panel">', table(versions[old]), "  </div>"]
        parts.append("</details>")
        older = "\n".join("  " + line for line in parts)
    short, _, long = description.partition(": ")
    text = f"{short}: {long}." if long else short
    return string.Template(TEMPLATE.read_text()).substitute(
        version=html.escape(version),
        description=html.escape(text, quote=True),
        sources_url=html.escape(f"{repo_url.rstrip('/')}/medias.sources"),
        latest=table(latest or []),
        older=older,
        date=time.strftime("%Y-%m-%d", time.gmtime(now)),
    )


def write(out=ROOT / "dist", repo_url=None):
    _, version, description = cargo_meta()
    repo_url = repo_url or os.environ.get("MEDIAS_REPO_URL") or "debian"
    path = out / "index.html"
    path.write_text(render(out, version, description, repo_url, time.time()))
    return path


if __name__ == "__main__":
    parser = argparse.ArgumentParser(description=__doc__.splitlines()[0])
    parser.add_argument("--out", type=Path, default=ROOT / "dist", help="directory with the downloads")
    parser.add_argument("--url", help="public URL of dist/debian (default: $MEDIAS_REPO_URL)")
    args = parser.parse_args()
    print(f"wrote {write(args.out, args.url)}")