diff --git a/tools/collect_hbnj_all.py b/tools/collect_hbnj_all.py index 0dfbe27..02e0419 100644 --- a/tools/collect_hbnj_all.py +++ b/tools/collect_hbnj_all.py @@ -2,6 +2,7 @@ import argparse import re +import sys import time from datetime import datetime, timezone from pathlib import Path @@ -15,11 +16,27 @@ def main() -> int: parser.add_argument("--date", required=True, help="Broadcast date in YYYYMMDD form") parser.add_argument("--out", type=Path, required=True) parser.add_argument("--delay", type=float, default=0.5, help="Delay between region requests") + parser.add_argument( + "--retries", + type=int, + default=3, + help="Attempts per region before failing the complete build", + ) + parser.add_argument( + "--retry-delay", + type=float, + default=1.0, + help="Initial retry delay in seconds (doubles after each failure)", + ) args = parser.parse_args() if not re.fullmatch(r"\d{8}", args.date): raise SystemExit("--date must use YYYYMMDD") if args.delay < 0: raise SystemExit("--delay cannot be negative") + if args.retries < 1: + raise SystemExit("--retries must be at least 1") + if args.retry_delay < 0: + raise SystemExit("--retry-delay cannot be negative") areas: list[dict[str, object]] = [] channels: list[dict[str, object]] = [] @@ -34,15 +51,30 @@ def main() -> int: f"group={group_id} area={area_name}", flush=True, ) - source_url, source = fetch(group_id, args.date) - region = parse_region( - source, - group_id=group_id, - area_id=area_id, - area_name=area_name, - prefecture_raw=prefecture_raw, - source_url=source_url, - ) + for attempt in range(1, args.retries + 1): + try: + source_url, source = fetch(group_id, args.date) + region = parse_region( + source, + group_id=group_id, + area_id=area_id, + area_name=area_name, + prefecture_raw=prefecture_raw, + source_url=source_url, + ) + break + except Exception as error: + if attempt == args.retries: + raise + wait = args.retry_delay * (2 ** (attempt - 1)) + print( + f" attempt {attempt}/{args.retries} failed: " + f"{type(error).__name__}: {error}; retrying in {wait:g}s", + file=sys.stderr, + flush=True, + ) + if wait: + time.sleep(wait) area = dict(region["area"]) areas.append(area) channels.extend(region["channels"]) diff --git a/tools/run_hbnj_daily.ps1 b/tools/run_hbnj_daily.ps1 index d7c7a55..6da0086 100644 --- a/tools/run_hbnj_daily.ps1 +++ b/tools/run_hbnj_daily.ps1 @@ -8,9 +8,16 @@ $log = Join-Path $logDirectory "hbnj-update-$stamp.log" Push-Location $workspace try { $env:PYTHONIOENCODING = "utf-8" + # Windows PowerShell promotes native stderr to an ErrorRecord. This Python + # installation emits a harmless prefix warning on stderr, so temporarily + # allow the process to finish and judge success by its actual exit code. + $previousErrorActionPreference = $ErrorActionPreference + $ErrorActionPreference = "Continue" & py -3 "tools\update_hbnj_daily.py" *>&1 | Tee-Object -FilePath $log - if ($LASTEXITCODE -ne 0) { - throw "HBNJ updater exited with code $LASTEXITCODE" + $updaterExitCode = $LASTEXITCODE + $ErrorActionPreference = $previousErrorActionPreference + if ($updaterExitCode -ne 0) { + throw "HBNJ updater exited with code $updaterExitCode" } } finally { diff --git a/tools/update_hbnj_daily.py b/tools/update_hbnj_daily.py index 51eb089..79443b7 100644 --- a/tools/update_hbnj_daily.py +++ b/tools/update_hbnj_daily.py @@ -7,14 +7,14 @@ import subprocess import sys import tempfile -from datetime import datetime +from datetime import datetime, timedelta, timezone from pathlib import Path -from zoneinfo import ZoneInfo WORKSPACE = Path(__file__).resolve().parents[1] PRIVATE = WORKSPACE / "channels" / "hbnj" / "private" CURRENT = WORKSPACE / "channels" / "hbnj" / "generated" / "current" +JAPAN_STANDARD_TIME = timezone(timedelta(hours=9), name="JST") def run(*arguments: str) -> None: @@ -56,8 +56,12 @@ def main() -> int: help="Broadcast date in YYYYMMDD form (default: current date in Japan)", ) parser.add_argument("--delay", type=float, default=0.5) + parser.add_argument("--retries", type=int, default=3) + parser.add_argument("--retry-delay", type=float, default=1.0) args = parser.parse_args() - broadcast_date = args.date or datetime.now(ZoneInfo("Asia/Tokyo")).strftime("%Y%m%d") + # Japan has observed UTC+09:00 year-round since 1951, so this avoids an + # unnecessary dependency on the optional Windows IANA/tzdata package. + broadcast_date = args.date or datetime.now(JAPAN_STANDARD_TIME).strftime("%Y%m%d") if len(broadcast_date) != 8 or not broadcast_date.isdigit(): raise SystemExit("--date must use YYYYMMDD") @@ -74,6 +78,10 @@ def main() -> int: str(guide), "--delay", str(args.delay), + "--retries", + str(args.retries), + "--retry-delay", + str(args.retry_delay), ) run("tools/validate_hbnj_guide.py", str(guide)) run("tools/pack_hbnj_guide.py", str(guide), "--out-dir", str(payloads))