Conversation
|
[Account-2 readiness review — 2026-09-19 · read-only adversarial verification pass; nothing in this PR, repo, or workspace was modified] Verdict: READY-WITH-NOTES for upstream filing (one stale body paragraph to fix first). The diff delivers all claimed mechanisms: PRE-SHIP — confidence high: the Design-notes paragraph still says "600 s is meant as a generous whole-track cap" — a leftover from before Two evaluate-only observations are ticketed on their canonical issues (#6 — unbounded post-kill cleanup read; #7 — blanket POST retry vs the issue's endpoint-scoped direction). Commit-subject length noted for the upstream squash (~88 chars vs upstream's short imperative style). Decision question for the owner: fix the stale 600 s paragraph at pickup? (implement-it / evaluate — nothing pre-selected.) |
60-second summary
Downloads can hang forever if the yt-dlp child or N_m3u8DL-RE stalls, and the DRM-critical POST requests (webplayback, license exchange) plus manifest/cover fetches give up on the first 429/5xx — so one transient server hiccup fails a whole track. This PR caps both download subprocess paths at 1800 s per track (kill + clean error on overrun) and adds retry policies: POST allowed on the existing main-client retry (it is the only client whose POSTs hit these two endpoints), and a 3-retry policy on the ad-hoc manifest/cover clients.
Verify in one command:
uv run python -c "import asyncio; from gamdl.utils import async_subprocess; asyncio.run(async_subprocess('sleep','10',timeout=2,silent=True))"→ the child is killed and aRuntimeError: Timed out after 2 seconds: sleep 10is raised at ~2 s; with no timeout argument the call completes as before. Rollback: revert the single commit; no config keys or CLI flags change.What
Bounded waits for the two external download paths and transient-failure retries for the fetch paths that previously had none. Successful downloads behave exactly as before.
Why
while process.is_alive(): await asyncio.sleep(0.1)(gamdl/downloader/base.py:282-283 pre-change) has no deadline — its terminate/killfinallycan never trigger;async_subprocessawaitedproc.communicate()with no timeout (gamdl/utils.py:20 pre-change).Retry(total=6, …)has noallowed_methods, and httpx-retries 0.4.6 retries only idempotent methods by default —get_webplayback(apple_music.py:725) andget_license_exchange(apple_music.py:760) are POSTs, so 429/5xx hard-fail the track on the DRM-critical path (mechanism matches Error fetching license exchange data (Status code: 429) glomatico/gamdl#306).get_response(manifest/lyrics fetches, used at interface/song.py:310,416,533 and music_video.py:193,358,385) andget_cover_byteseach built a freshhttpx.AsyncClientwith no retry transport — one transient blip fails the track.The change
gamdl/utils.py:DOWNLOAD_TIMEOUT_SECONDS = 1800;async_subprocesswrapscommunicate()inasyncio.wait_for, kills the child on overrun, and raises aRuntimeErrornaming the command and timeout.gamdl/downloader/base.py: the yt-dlp wait loop checks a monotonic deadline and raises on overrun; the existingfinally(terminate → grace join → kill) then runs unchanged.gamdl/api/apple_music.py:allowed_methodsadded to the existingRetry— scoped in practice, because the only POSTs on that client are the two above, whose JSON bodies are rewindable.gamdl/interface/base.py: shared_HTTP_RETRY(total=3, backoff 1, 429/5xx) applied viaRetryTransportto both ad-hoc clients; their existing per-call timeouts (60 s / 30 s) andfollow_redirectsbehavior are preserved.Verification
python3 -m py_compileclean on all four files; package builds viauv run.Retry(..., allowed_methods=…)accepts the new field and reports POST retryable under the pinned httpx-retries 0.4.6 (already a runtime dependency via the main client — this PR adds no new dependency);async_subprocess('sleep','10', timeout=2)kills the child and raises at 2.0 s; a fast-exiting command completes normally.silent=Trueruns); allowing POST retries also retries network-exception failures, not just 429/5xx — both POSTs are idempotent read-style fetches, so this is safe; the one-shot setup clients (get_token,get_account_info) are intentionally left unretried — a startup failure is user-visible and cheap to rerun.Design notes / questions
Attribution
This PR was produced with AI assistance under the direction of @JavaGT:
The final diff was verified against the described behavior. Happy to adjust or close any part of this — tell me what doesn't fit the project's direction.