#!/usr/bin/env python3 """ Genera src/main/resources/static/podcast.xml escaneando los .mp3 presentes en static/audios/. Uso: python3 scripts/generar_podcast.py python3 scripts/generar_podcast.py --base-url https://taiage.tatvil.es El BASE_URL también se puede fijar con la variable de entorno TAIAGE_BASE_URL. Por defecto: https://taiage.tatvil.es """ import argparse import os import re from datetime import datetime, timezone, timedelta from pathlib import Path BASE_DIR = Path(__file__).parent.parent TEMAS_DIR = BASE_DIR / "src" / "main" / "resources" / "temas" AUDIOS_DIR = BASE_DIR / "src" / "main" / "resources" / "static" / "audios" OUTPUT = BASE_DIR / "src" / "main" / "resources" / "static" / "podcast.xml" DEFAULT_BASE_URL = os.environ.get("TAIAGE_BASE_URL", "https://taiage.tatvil.es") # Fecha base para asignar pubDate incrementales a cada episodio FECHA_BASE = datetime(2026, 4, 1, 10, 0, 0, tzinfo=timezone.utc) def titulo_desde_md(path_md: Path) -> str: """Lee el primer encabezado ## del .md fuente como título del episodio.""" if not path_md.exists(): return path_md.stem.replace('_', ' ').replace('-', ' ').capitalize() for linea in path_md.read_text(encoding="utf-8").splitlines(): linea = linea.strip() if linea.startswith('## '): return linea[3:].strip() if linea.startswith('# '): return linea[2:].strip() return path_md.stem.replace('_', ' ').replace('-', ' ').capitalize() def pub_date(delta_days: int) -> str: d = FECHA_BASE + timedelta(days=delta_days) return d.strftime("%a, %d %b %Y %H:%M:%S +0000") def item_xml(titulo: str, url: str, size: int, ep_num: int, day_offset: int) -> str: titulo_esc = titulo.replace('&', '&').replace('<', '<').replace('>', '>') return ( f" \n" f" {titulo_esc}\n" f" {titulo_esc}\n" f" \n" f" {url}\n" f" {pub_date(day_offset)}\n" f" {ep_num}\n" f" 1\n" f" full\n" f" " ) def recopilar_episodios(base_url: str) -> list[str]: items = [] ep = 1 day = 0 # ── Temas (bloque1..4, orden natural) ────────────────────────── for bloque_num in range(1, 5): mp3_dir = AUDIOS_DIR / f"bloque{bloque_num}" if not mp3_dir.exists(): continue for mp3 in sorted(mp3_dir.glob("tema*_audio.mp3")): # Buscar .md fuente para el título match = re.match(r'tema(\d+)', mp3.stem) if match: md_path = TEMAS_DIR / f"bloque{bloque_num}" / f"tema{match.group(1)}_audio.md" else: md_path = Path("/dev/null") titulo = titulo_desde_md(md_path) url = f"{base_url}/audios/bloque{bloque_num}/{mp3.name}" size = mp3.stat().st_size items.append(item_xml(titulo, url, size, ep, day)) ep += 1 day += 1 # ── Leyes ─────────────────────────────────────────────────────── leyes_mp3_dir = AUDIOS_DIR / "leyes" if leyes_mp3_dir.exists(): # Buscar .md fuente en temas/bloqueX/leyes/ leyes_md: dict[str, Path] = {} for md in TEMAS_DIR.glob("bloque*/leyes/*.md"): leyes_md[md.stem] = md for mp3 in sorted(leyes_mp3_dir.glob("*.mp3")): if mp3.name.endswith(".temp.mp3"): continue md_path = leyes_md.get(mp3.stem, Path("/dev/null")) titulo = titulo_desde_md(md_path) url = f"{base_url}/audios/leyes/{mp3.name}" size = mp3.stat().st_size items.append(item_xml(titulo, url, size, ep, day)) ep += 1 day += 1 return items def generar(base_url: str) -> None: items = recopilar_episodios(base_url) cover_url = f"{base_url}/images/podcast_cover.jpg" xml = ( '\n' '\n' ' \n' ' TAI AGE \u2013 Temario oposici\u00f3n\n' f' {base_url}\n' ' Audios del temario TAI \u2013 Administraci\u00f3n del Estado. Bloques I al IV.\n' ' es-es\n' ' TAI AGE\n' ' \n' ' false\n' f' \n' ' \n' f' {cover_url}\n' ' TAI AGE\n' f' {base_url}\n' ' \n' ) xml += "\n".join(items) + "\n" xml += " \n\n" OUTPUT.write_text(xml, encoding="utf-8") print(f"✅ podcast.xml generado: {len(items)} episodios → {OUTPUT.relative_to(BASE_DIR)}") if __name__ == "__main__": parser = argparse.ArgumentParser(description="Genera podcast.xml a partir de los .mp3 presentes") parser.add_argument("--base-url", default=DEFAULT_BASE_URL, help="URL pública base del servidor (ej: https://taiage.tatvil.es)") args = parser.parse_args() generar(args.base_url)