Skip to content

Download System

Domekologe edited this page Aug 7, 2026 · 5 revisions

Download System

🌐 English · Deutsch

Flow

  1. Episodes are added to the download queue via the UI (or by AutoSync / Seerr).
  2. A single global worker processes the queue sequentially — one job (with any number of episodes) at a time.
  3. For each episode the matching host extractor is invoked, the direct link resolved, and the stream saved as .mkv via yt-dlp/FFmpeg (audio/subtitle tracks are tagged correctly).
  4. On completion: Notifications, optional Jellyfin/Plex library refresh, optional Anime4K upscaling.

Screenshot: download queue

Queue entry statuses

Status Meaning
queued Waiting for the worker
running Currently downloading
completed All episodes succeeded
partial Partially successful — at least one episode ok, at least one failed
failed All episodes failed
cancelled Cancelled by the user

After a crash, running entries are automatically reset to queued on restart (crash recovery).

Error handling & watchdog

  • 3 attempts per episode (2 s pause in between). If the requested language simply isn't available for an episode, a readable message is stored without further retries ("Not available in: …").
  • Watchdog: Every episode download runs in a monitored thread. Hard timeout after 30 min (watchdog_hang_timeout), abort after 60 min without progress (watchdog_stall_timeout) — a hung yt-dlp can never block the queue permanently.
  • Failed episodes are stored on the entry with URL + error text and shown in the UI.

Queue actions

Action Description
Pause / Resume Global pause; persisted in the DB and survives restarts. The current episode still finishes
Cancel Aborts the running job immediately — kills the active subprocess, tears down a captcha browser that may be running, and deletes the partial .part / fragment files of the aborted download
Skip episode Skips only the currently running episode without counting it as an error
Retry single episode For partial/failed: retry just one failed episode
Restart Re-queue the whole entry
Move Reorder waiting entries
Clear completed Remove all finished entries

Captcha solving in the WebUI

When an extractor hits a captcha page (e.g. Cloudflare Turnstile), the app launches an invisible Patchright Chromium with stealth measures and a built-in ad blocker. The captcha view is streamed into the WebUI as a live screenshot; clicks in the UI are forwarded to the browser. Admins can solve any captcha, regular users only those of their own downloads.

Supported challenge types include Cloudflare Turnstile, reCAPTCHA/hCaptcha checkboxes (including several stacked in one modal and fake site-own "I'm not a robot" checkboxes) and ALTCHA proof-of-work widgets. A single modal can combine multiple challenge types and is only reported as solved once all of them are.

In background mode the solver never steals window focus; only the visible/manual mode brings the window to the front.

For troubleshooting, the environment variable MEDIAFORGE_CAPTCHA_DEBUG_LOG=1 mirrors the captcha browser's console/network errors into the app log — noisy, opt-in for debugging only.

Storage location & file names

  • Base: download path from the settings (default ~/Downloads) or a custom path per job.
  • Language separation (optional): one subfolder per language — german-dub, english-sub, german-sub, english-dub, english-dub-german-sub.
  • Naming template: by default {title} ({year}) [imdbid-{imdbid}]/Season {season}/{title} S{season}E{episode}.mkv (Configuration).

Subtitles

On by default (Settings → Downloads → Subtitles → Download subtitles, dl_subtitles, see Configuration).

When enabled, yt-dlp is asked for every subtitle rendition the source offers (writesubtitles, subtitleslangs: ["all"]). Auto-generated/ASR captions are deliberately not fetched (writeautomaticsub stays off) — they are machine-guessed text, not the source's own subtitles.

The sidecar files yt-dlp writes are then muxed into the finished .mkv as soft-sub tracks converted to SubRip (-c:s srt), each tagged with its ISO 639-2/B language code, so Jellyfin/Plex list them and you can switch them off again.

  • Language codes are normalised: de, de-DE, ger and deu all become deu; duplicates collapse into a single track; unknown codes become und.
  • Capped at 12 subtitle tracks (MAX_SUBTITLE_TRACKS) — a few sources advertise 40+ machine-translated renditions. Hitting the cap is logged.
  • Runs last: the mux is the very last step before the file is moved to its destination, deliberately after any upscale pass — upscaling re-encodes with a video-only mapping and would otherwise drop the subtitles again.
  • Failure is non-fatal by design: if the subtitle mux fails, the video is kept unchanged and a warning is logged. Losing a subtitle track never fails a download.
  • Three passes, cheapest first: the renditions yt-dlp finds in the stream, then the tracks the hoster carries out of band in its player config, and finally — only if you switch it on — an external lookup at OpenSubtitles.com. Each pass is asked only for the languages still missing after the previous one, so nothing is fetched twice.

OpenSubtitles.com (optional)

Off by default (Settings → Integrations → Third Party → OpenSubtitles, see Configuration). It runs as the last step of the subtitle chain and is only ever asked for the languages the finished file does not already have — so an episode that already brought German and English subtitles costs nothing, and your daily quota is not wasted.

  • Matching: first by moviehash (file size plus the first and last 64 KiB), which identifies the exact release and therefore subtitles that are actually in sync. If that finds nothing, a title search is used (query with season/episode number, or the IMDb/TMDB id when MediaForge knows it). Results are ranked by hash match first, then by download count.
  • Result: each subtitle is written next to the video as <name>.<lang>.srt and muxed into the .mkv as a tagged soft-sub track by the normal path — indistinguishable from a subtitle the hoster delivered.
  • Never fatal: a missing key, a failed login, a network error or an empty result only logs a line. Nothing here can fail a download.
  • Account & quota: an API key (opensubtitles.com → Consumers) and a user login are required, because downloads are bound to an account. The login token is cached for the process (~23 h) and follows the VIP host the login response returns. The remaining daily quota is read from every download response; once it is exhausted (or the API answers HTTP 406) further lookups are skipped until it resets instead of hammering the API.

Modules can add their own external subtitle source into this same step — see Module API.

Not the same as the "Sub" language options. This only affects sources that serve real subtitle tracks. MediaForge's German Sub / English Sub language options are something different: they select a video variant with the subtitles burned into the picture, which cannot be switched off.

Duplicate handling

Two optional checks that only run when the episode already exists on disk. Both are off by default (Settings → Downloads → Duplicate Handling, see Configuration).

  • Replace with better quality (dl_quality_upgrade): when the existing file already carries every track the job wanted, the hoster stream is probed with ffprobe (header only, no payload is downloaded; capped at 25 s) and compared with the file on disk. "Better" means a video height higher by more than 32 px, or the same height with a bitrate at least 20 % higher. If resolution or bitrate is unknown on either side, the stream does not count as better and the existing file is kept — deliberately conservative. When it is better, the episode is downloaded again and the old file is replaced outright; extra audio tracks the old file may have had are not preserved. Costs one extra ffprobe request per already-present episode.
  • Merge additional languages into the existing file (dl_audio_track_merge): with language separation on (or {language} in the naming template) a second language previously ended up as a separate, near-duplicate file, because the job only ever looked at its own target path. With this option the same episode is resolved across the sibling language folders (german-dub, english-sub, german-sub, english-dub, english-dub-german-sub, japanese-dub) and inside the episode's own folder — matched by its S01E01 / 1x01 marker, or by the exact file name for movies. The new audio track is then muxed into that existing file (ffmpeg -c copy) instead of creating a new one. The quality-upgrade check is skipped for such a merge target on purpose, so tracks from other languages are never thrown away.

Size tracking, the Jellyfin NFO hook and the after-download encoding hook all run against the real destination file — including a file that was merged into rather than newly created.

Disk space check

Before each job the worker checks free space at the target path. If it falls below the configured minimum (notif_disk_space_min_gb, default 5 GB, 0 = disabled), a warning is sent (max. once per hour) — the download still starts.

Statistics

Average speed (MB/s) and total size are recorded per job; aggregates are shown on the statistics page (/stats).

Why did this fail?

A failed queue entry no longer shows only a traceback. The raw error is classified into one of about twenty causes, and the queue shows the cause and a concrete next step first; the original text stays one click below, because that is what a bug report needs.

Causes cover disk and permission problems, captcha and rate limiting, dead hosters, missing episodes or languages, DNS/TLS/timeout/connection failures, the internal watchdogs, and ffmpeg. Anything that does not match a known pattern is reported as unknown with the raw text and no advice — a wrong explanation sends you to fix the wrong thing, which is worse than none.

Errors are grouped by cause with a count. A twelve-episode job that failed twelve times almost always failed for one reason, and saying it once is the difference between a queue entry somebody reads and one they scroll past.

The summary also reports whether retrying is worth it at all. A job that is half "disk full" is not fixed by pressing retry, so it is not offered as the obvious next step.

Bulk actions

Select several queue entries and apply one action to all of them: cancel, remove, retry, move to top, move to bottom.

Every id is processed independently and reported back individually. A selection of twenty where one entry was already gone must not fail the other nineteen — and "it didn't work" without saying which one is exactly how bulk actions get distrusted.

A kids account may not use retry, for the same reason it may not start a download at all (see Authentication). The other actions have the same standing as the single-entry buttons they batch.

Clone this wiki locally