feat(downloads): M2 — worker download loop + cache/dedup
- downloads/formats.py: profile spec -> format_sig (cache key) + yt-dlp opts (format
selector, audio extract, sponsorblock, embed subs/chapters/thumbnail, jpg poster)
- downloads/storage.py: Plex tree path ({Channel}/Season {Year}/… [id].ext) + .nfo +
poster.jpg sidecars; sanitization; asset-file deletion with empty-dir pruning
- downloads/service.py: enqueue + get-or-create shared MediaAsset (race-safe) + instant
cache-hit path (ready asset -> job done, ref_count++)
- worker.py: N claim-loop threads (FOR UPDATE SKIP LOCKED), asset compare-and-set
pending->downloading so identical requests never double-download, throttled progress
writes, cooperative pause/cancel via the progress hook, crash-safe orphan recovery
Verified on localdev: real download -> Plex tree + .nfo + poster on the host bind-mount,
correct metadata/codecs/size; 2nd user = instant cache hit (shared asset, ref_count=2);
audio-only MP3 extraction path produces a distinct cached asset.