npeter83
|
7148335c09
|
feat(downloads): M3 — per-user quota + retention GC
- downloads/quota.py: per-user limits (DownloadQuota row or sysconfig defaults; unlimited
bypass), footprint = distinct ready-asset bytes the user holds, check_enqueue (max_jobs +
max_bytes hard at enqueue), at_concurrency_limit (worker-side max_concurrent), usage snapshot
- downloads/gc.py: run_download_gc — pre-expiry warning (once, gc_notified flag), TTL deletion
(files + row; FK SET NULL orphans holders' jobs -> 'expired'), LRU eviction over a total-cache
cap; notifies holders on each event
- migration 0038: media_assets.gc_notified
- config/sysconfig: download_total_max_bytes (0=unlimited) in the downloads group
- service.enqueue enforces quota; worker claim skips users at their concurrency limit
- scheduler: download_gc job registered (default 360 min, admin-tunable)
Verified on localdev: footprint accounting, max_jobs block, pre-expiry warning (notifies all
holders), TTL deletion (asset+files gone, dirs pruned, jobs orphaned, holders notified).
|
2026-07-03 00:26:40 +02:00 |
|
npeter83
|
1ae4333922
|
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.
|
2026-07-03 00:08:18 +02:00 |
|
npeter83
|
317750e491
|
feat(downloads): M1 — schema, config, worker container, deps
- models: MediaAsset (shared file cache, unique on source+format_sig), DownloadJob
(per-user request), DownloadProfile (presets), DownloadShare (ACL), DownloadQuota
- migrations 0036 (5 tables) + 0037 (6 builtin presets)
- sysconfig 'downloads' group (per-user defaults, retention/GC, layout, worker concurrency)
- config: DOWNLOAD_ROOT / WORKER_ENABLED env + download defaults
- deps: yt-dlp in requirements, ffmpeg in Dockerfile runtime stage
- worker: dedicated container (python -m app.worker), thin idle entry (loop lands in M2)
- localdev compose: worker service + downloads bind-mount; gitignore downloads/
|
2026-07-02 23:57:40 +02:00 |
|