Regenerate episode pages and sitemap during publish
add_episode_to_sitemap() was removed in the previous commit, so nothing was writing the sitemap any more. Publishing now shells out to generate_episode_pages.py --sitemap right after the transcript is copied into website/transcripts/, which builds the new episode's static page and rebuilds the sitemap from the feed. Deliberately non-fatal: by this point the audio is live on Castopod and the RSS feed has been rebuilt, so a generator failure must not abort the publish. It warns and moves on, and the page lands on the next run. Covered for non-zero exit, timeout, and a missing generator script. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
+28
-1
@@ -974,6 +974,31 @@ def sync_episode_media_to_bunny(episode_id: int, already_uploaded: set):
|
|||||||
Path(tmp_path).unlink(missing_ok=True)
|
Path(tmp_path).unlink(missing_ok=True)
|
||||||
|
|
||||||
|
|
||||||
|
def regenerate_website_pages() -> bool:
|
||||||
|
"""Rebuild the static episode pages and sitemap after a publish.
|
||||||
|
|
||||||
|
Never fatal. By the time this runs the audio is already live on Castopod
|
||||||
|
and the RSS feed has been rebuilt, so a generator failure must not abort
|
||||||
|
the publish — it just means the new episode's page lands on the next run.
|
||||||
|
"""
|
||||||
|
script = Path(__file__).parent / "generate_episode_pages.py"
|
||||||
|
try:
|
||||||
|
result = subprocess.run(
|
||||||
|
[sys.executable, str(script), "--sitemap"],
|
||||||
|
capture_output=True, text=True, timeout=300,
|
||||||
|
)
|
||||||
|
except (subprocess.TimeoutExpired, OSError) as e:
|
||||||
|
print(f" Warning: could not run episode page generation: {e}")
|
||||||
|
return False
|
||||||
|
|
||||||
|
if result.returncode != 0:
|
||||||
|
print(f" Warning: episode page generation failed: {result.stderr[-300:]}")
|
||||||
|
return False
|
||||||
|
|
||||||
|
print(" Episode pages and sitemap regenerated")
|
||||||
|
return True
|
||||||
|
|
||||||
|
|
||||||
def generate_social_image(episode_number: int, description: str, output_path: str) -> str:
|
def generate_social_image(episode_number: int, description: str, output_path: str) -> str:
|
||||||
"""Generate a social media image with cover art, episode number, and description."""
|
"""Generate a social media image with cover art, episode number, and description."""
|
||||||
from PIL import Image, ImageDraw, ImageFont
|
from PIL import Image, ImageDraw, ImageFont
|
||||||
@@ -1817,7 +1842,9 @@ def main():
|
|||||||
shutil.copy2(str(transcript_path), str(website_transcript_path))
|
shutil.copy2(str(transcript_path), str(website_transcript_path))
|
||||||
print(f" Transcript copied to website/transcripts/")
|
print(f" Transcript copied to website/transcripts/")
|
||||||
|
|
||||||
# Sitemap is regenerated wholesale by generate_episode_pages.py --sitemap
|
# Build this episode's static page and regenerate the sitemap wholesale.
|
||||||
|
# generate_episode_pages.py is the only writer of sitemap.xml.
|
||||||
|
regenerate_website_pages()
|
||||||
|
|
||||||
# Sync any remaining episode media to BunnyCDN (cover art, etc.)
|
# Sync any remaining episode media to BunnyCDN (cover art, etc.)
|
||||||
print(" Syncing remaining episode media to CDN...")
|
print(" Syncing remaining episode media to CDN...")
|
||||||
|
|||||||
@@ -0,0 +1,80 @@
|
|||||||
|
"""Publishing must rebuild the static episode pages and the sitemap.
|
||||||
|
|
||||||
|
Before static pages existed, publish_episode.py appended one entry to
|
||||||
|
sitemap.xml itself. That appender is gone — generate_episode_pages.py owns the
|
||||||
|
sitemap now — so the publish has to invoke it, or a newly published episode
|
||||||
|
would have no page and never reach the sitemap.
|
||||||
|
|
||||||
|
A generator failure must never abort a publish: by the time this runs the audio
|
||||||
|
is already live on Castopod and the RSS feed has been rebuilt.
|
||||||
|
"""
|
||||||
|
|
||||||
|
import subprocess
|
||||||
|
import sys
|
||||||
|
from pathlib import Path
|
||||||
|
|
||||||
|
sys.path.insert(0, str(Path(__file__).resolve().parent.parent))
|
||||||
|
|
||||||
|
import publish_episode
|
||||||
|
|
||||||
|
|
||||||
|
class _Result:
|
||||||
|
def __init__(self, returncode=0, stderr=""):
|
||||||
|
self.returncode = returncode
|
||||||
|
self.stderr = stderr
|
||||||
|
self.stdout = ""
|
||||||
|
|
||||||
|
|
||||||
|
def test_invokes_the_generator_with_sitemap(monkeypatch):
|
||||||
|
calls = []
|
||||||
|
monkeypatch.setattr(subprocess, "run", lambda cmd, **kw: calls.append(cmd) or _Result())
|
||||||
|
|
||||||
|
assert publish_episode.regenerate_website_pages() is True
|
||||||
|
|
||||||
|
assert len(calls) == 1
|
||||||
|
cmd = calls[0]
|
||||||
|
assert "generate_episode_pages.py" in " ".join(cmd)
|
||||||
|
assert "--sitemap" in cmd
|
||||||
|
|
||||||
|
|
||||||
|
def test_uses_the_running_interpreter(monkeypatch):
|
||||||
|
"""Must not shell out to a bare 'python' that may not have the venv."""
|
||||||
|
calls = []
|
||||||
|
monkeypatch.setattr(subprocess, "run", lambda cmd, **kw: calls.append(cmd) or _Result())
|
||||||
|
|
||||||
|
publish_episode.regenerate_website_pages()
|
||||||
|
|
||||||
|
assert calls[0][0] == sys.executable
|
||||||
|
|
||||||
|
|
||||||
|
def test_generator_failure_is_not_fatal(monkeypatch):
|
||||||
|
monkeypatch.setattr(subprocess, "run",
|
||||||
|
lambda cmd, **kw: _Result(returncode=1, stderr="boom"))
|
||||||
|
|
||||||
|
assert publish_episode.regenerate_website_pages() is False
|
||||||
|
|
||||||
|
|
||||||
|
def test_generator_timeout_is_not_fatal(monkeypatch):
|
||||||
|
def _boom(cmd, **kw):
|
||||||
|
raise subprocess.TimeoutExpired(cmd, 300)
|
||||||
|
|
||||||
|
monkeypatch.setattr(subprocess, "run", _boom)
|
||||||
|
|
||||||
|
assert publish_episode.regenerate_website_pages() is False
|
||||||
|
|
||||||
|
|
||||||
|
def test_missing_generator_is_not_fatal(monkeypatch):
|
||||||
|
def _boom(cmd, **kw):
|
||||||
|
raise OSError("no such file")
|
||||||
|
|
||||||
|
monkeypatch.setattr(subprocess, "run", _boom)
|
||||||
|
|
||||||
|
assert publish_episode.regenerate_website_pages() is False
|
||||||
|
|
||||||
|
|
||||||
|
def test_publish_flow_calls_it_after_copying_the_transcript():
|
||||||
|
"""Guards the wiring, not just the helper."""
|
||||||
|
source = Path(publish_episode.__file__).read_text()
|
||||||
|
copy_at = source.index("Transcript copied to website/transcripts/")
|
||||||
|
call_at = source.index("regenerate_website_pages()", copy_at)
|
||||||
|
assert call_at > copy_at
|
||||||
Reference in New Issue
Block a user