#!/usr/bin/env python3 """Validate Kodi repository artifacts for ViewIT. Usage: verify_repo_artifacts.py [--expect-branch ] """ from __future__ import annotations import argparse import hashlib import sys import xml.etree.ElementTree as ET import zipfile from pathlib import Path PLUGIN_ID = "plugin.video.viewit" REPO_ID = "repository.viewit" def _find_addon(root: ET.Element, addon_id: str) -> ET.Element: if root.tag == "addon" and (root.attrib.get("id") or "") == addon_id: return root for addon in root.findall("addon"): if (addon.attrib.get("id") or "") == addon_id: return addon raise ValueError(f"addon {addon_id} not found in addons.xml") def _read_zip_addon_version(zip_path: Path, addon_id: str) -> str: inner_path = f"{addon_id}/addon.xml" with zipfile.ZipFile(zip_path, "r") as archive: try: data = archive.read(inner_path) except KeyError as exc: raise ValueError(f"{zip_path.name}: missing {inner_path}") from exc root = ET.fromstring(data.decode("utf-8", errors="replace")) version = (root.attrib.get("version") or "").strip() if not version: raise ValueError(f"{zip_path.name}: addon.xml without version") return version def _check_md5(repo_dir: Path) -> list[str]: errors: list[str] = [] addons_xml = repo_dir / "addons.xml" md5_file = repo_dir / "addons.xml.md5" if not addons_xml.exists() or not md5_file.exists(): return errors expected = md5_file.read_text(encoding="ascii", errors="ignore").strip().lower() actual = hashlib.md5(addons_xml.read_bytes()).hexdigest() if expected != actual: errors.append("addons.xml.md5 does not match addons.xml") return errors def _check_repo_zip_branch(zip_path: Path, expected_branch: str) -> list[str]: errors: list[str] = [] inner_path = f"{REPO_ID}/addon.xml" with zipfile.ZipFile(zip_path, "r") as archive: try: data = archive.read(inner_path) except KeyError as exc: raise ValueError(f"{zip_path.name}: missing {inner_path}") from exc root = ET.fromstring(data.decode("utf-8", errors="replace")) info = root.find(".//dir/info") if info is None or not (info.text or "").strip(): errors.append(f"{zip_path.name}: missing repository info URL") return errors info_url = (info.text or "").strip() marker = f"/branch/{expected_branch}/addons.xml" if marker not in info_url: errors.append(f"{zip_path.name}: info URL does not point to branch '{expected_branch}'") return errors def main() -> int: parser = argparse.ArgumentParser() parser.add_argument("repo_dir", help="Path to repository root (contains addons.xml)") parser.add_argument("--expect-branch", default="", help="Expected branch in repository.viewit addon.xml URL") args = parser.parse_args() repo_dir = Path(args.repo_dir).resolve() addons_xml = repo_dir / "addons.xml" if not addons_xml.exists(): print(f"Missing: {addons_xml}", file=sys.stderr) return 2 errors: list[str] = [] try: root = ET.parse(addons_xml).getroot() plugin_node = _find_addon(root, PLUGIN_ID) repo_node = _find_addon(root, REPO_ID) except Exception as exc: print(f"Invalid addons.xml: {exc}", file=sys.stderr) return 2 plugin_version = (plugin_node.attrib.get("version") or "").strip() repo_version = (repo_node.attrib.get("version") or "").strip() if not plugin_version: errors.append("plugin.video.viewit has no version in addons.xml") if not repo_version: errors.append("repository.viewit has no version in addons.xml") plugin_zip = repo_dir / PLUGIN_ID / f"{PLUGIN_ID}-{plugin_version}.zip" repo_zip = repo_dir / REPO_ID / f"{REPO_ID}-{repo_version}.zip" if not plugin_zip.exists(): errors.append(f"Missing plugin zip: {plugin_zip}") if not repo_zip.exists(): errors.append(f"Missing repository zip: {repo_zip}") if plugin_zip.exists(): try: zip_version = _read_zip_addon_version(plugin_zip, PLUGIN_ID) if zip_version != plugin_version: errors.append( f"{plugin_zip.name}: version mismatch (zip={zip_version}, addons.xml={plugin_version})" ) except Exception as exc: errors.append(str(exc)) if repo_zip.exists(): try: zip_version = _read_zip_addon_version(repo_zip, REPO_ID) if zip_version != repo_version: errors.append(f"{repo_zip.name}: version mismatch (zip={zip_version}, addons.xml={repo_version})") if args.expect_branch: errors.extend(_check_repo_zip_branch(repo_zip, args.expect_branch)) except Exception as exc: errors.append(str(exc)) errors.extend(_check_md5(repo_dir)) if errors: print("Repository validation failed:") for line in errors: print(f"- {line}") return 1 print("Repository validation passed.") print(f"- plugin: {plugin_version}") print(f"- repository: {repo_version}") return 0 if __name__ == "__main__": raise SystemExit(main())