Skip to content

Download progressive MP4 streams to disk - #40

Merged
ChrisonSimtian merged 3 commits into
mainfrom
feat/downloader-agent
Jul 26, 2026
Merged

Download progressive MP4 streams to disk#40
ChrisonSimtian merged 3 commits into
mainfrom
feat/downloader-agent

Conversation

@ChrisonSimtian

Copy link
Copy Markdown
Contributor

What & why

Turns a Sonarr grab into a file on disk — the last piece of the download path. The Downloader agent (previously a stub) polls the durable job table for the next Queued download and pulls its stream to disk, raw — no transcoding (HLS remux / ffmpeg stays a deferred orchestration step per DR-010).

SABnzbd add → DownloadJob (Queued)  →  DownloadWorker claims it  →  RawMp4DownloadProvider streams to disk  →  Completed(path, size)

Design: polling claimer, not a bus

The DownloadJob row is the work queue — its WorkerId + phase-transition methods (MarkClaiming, MarkCompleted, MarkDownloadFailed) and the repository's GetNextQueuedAsync are all built for a claiming worker. So the agent polls + claims; no cross-process Wolverine transport needed (Wolverine dropped from this host). A scale-out later can move to a Postgres/RabbitMQ transport.

Changes

  • IDownloadProvider now returns DownloadResult(OutputPath, SizeBytes). Infrastructure adapter RawMp4DownloadProvider streams the exact bytes the Mediathek serves to a .part temp file, then atomically moves it into the structured library path (FileNamingService). Uses a dedicated infinite-timeout HttpClient (not the factory one) so ServiceDefaults' standard-resilience total-timeout can't abort a long streaming download.
  • Application/Downloads/RunDownloadHandler (the Action): claims the job (→ Downloading), runs the provider, records Completed(path, size) or DownloadFailed(reason).
  • DownloadWorker BackgroundService in the Downloader agent polls GetNextQueuedAsync and runs the Action.
  • Deleted the dead, unwired ffmpeg leftover (IFfmpegDownloader + FfmpegDownloader + its duplicate DownloadResult) — superseded by the IDownloadProvider port. When ffmpeg is actually needed it becomes a new provider/post-step.

Tests / scope

RunDownloadHandler unit tests: claim→Completed, provider-throws→DownloadFailed, non-queued skipped, missing-episode fails without downloading. ./build.cmd Test green (Application 48 / Architecture 8 / Infrastructure 47).

A live full-download test is intentionally omitted (a real episode is hundreds of MB) — the raw byte-copy approach is already proven by the opt-in Download.VerifyRawMp4Async path from the crawler PRs. Progress is claimed→Completed (no per-tick % persistence yet); stale-Downloading reclaim on crash/shutdown is noted as future.

🤖 Generated with Claude Code

ChrisonSimtian and others added 3 commits July 26, 2026 22:55
Turns a Sonarr grab into a file on disk. The Downloader agent polls the durable
job table for the next Queued download and pulls its stream to disk — raw, no
transcoding (HLS remux / ffmpeg stays a deferred orchestration step, DR-010).

- IDownloadProvider now returns DownloadResult(OutputPath, SizeBytes); the
  Infrastructure adapter RawMp4DownloadProvider streams the exact bytes the
  Mediathek serves to a .part temp then atomically moves it into the structured
  library path (FileNamingService). Dedicated infinite-timeout HttpClient, not the
  factory one, so ServiceDefaults' resilience timeout can't abort a long stream.
- Application/Downloads/RunDownloadHandler (the Action): claims a Queued job
  (MarkClaiming → Downloading), runs the provider, records Completed(path,size) or
  DownloadFailed(reason).
- Downloader agent: DownloadWorker BackgroundService polls GetNextQueuedAsync and
  runs the Action; the DownloadJob row is the work queue (its WorkerId/phase design
  is built for a claiming worker) so no cross-process bus is needed — Wolverine
  dropped from this host.
- Deleted the dead, unwired ffmpeg leftover (IFfmpegDownloader + FfmpegDownloader +
  its duplicate DownloadResult) — superseded by the IDownloadProvider port.

./build.cmd Test green (Application 48). Live full-download stays manual; the raw
copy is already proven by the [Live] Download.VerifyRawMp4Async path.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Runs the production RawMp4DownloadProvider end-to-end for ARD "Extra 3",
KiKA "Die Biene Maja" and ZDF "heute-show": resolve the stream (same path as the
crawler live tests), pull the whole file to disk, assert it's a genuine MP4
(ftyp + many MB), then clean up. Tagged [Live] so CI skips it (these pull
hundreds of MB) — run locally with `./build.cmd TestLive`.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…t KiKA test

Running the [Live] full-download tests surfaced a real production bug: the
Downloader's dedicated HttpClient sent no User-Agent, and the Mediathek CDNs 403
UA-less requests. Set "Krautwatch/1.0" on it.

Proven end-to-end with the real provider: ARD "Extra 3" (~30 min, 2m39s) and ZDF
"heute-show" (~43 min, 2m57s) fully download to disk and verify as genuine MP4s.
KiKA "Die Biene Maja" resolves to a legacy ZDF Akamai asset that's geo-fenced to
Germany (403 AkamaiGHost regardless of UA/Referer) — the test tolerates that 403
so the suite passes from any location while still exercising the resolve+request path.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
@ChrisonSimtian
ChrisonSimtian merged commit 47bd27a into main Jul 26, 2026
1 check passed
@ChrisonSimtian
ChrisonSimtian deleted the feat/downloader-agent branch July 26, 2026 11:30
@ChrisonSimtian ChrisonSimtian added the enhancement New feature or request label Aug 2, 2026
@ChrisonSimtian ChrisonSimtian changed the title feat(downloader): wire the Downloader agent (raw progressive-MP4 pull) Download progressive MP4 streams to disk Aug 2, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

enhancement New feature or request

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant