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

| 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).
- 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.
| 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 |
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.
-
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).
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,geranddeuall becomedeu; duplicates collapse into a single track; unknown codes becomeund. -
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.
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 (querywith 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>.srtand muxed into the.mkvas 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.
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 withffprobe(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 extraffproberequest 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 itsS01E01/1x01marker, 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.
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.
Average speed (MB/s) and total size are recorded per job; aggregates are shown on the statistics page (/stats).
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.
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.
🇬🇧 English
Users
- Installation
- Getting Started
- Migration from AniWorld
- Configuration
- Web UI
- Download System
- Download History
- AutoSync
- Calendar
- Library
- Authentication
- Notifications
- Integrations
- SyncPlay
- Anime4K Upscaling
- Encoding
- Modules
- Theme Packs
- Backup
- Operations
- Audit Log
- Rules & Languages
- Docker
- Supported Sites
Developers
🇩🇪 Deutsch
Benutzer
- Installation
- Erste Schritte
- Umzug von AniWorld
- Konfiguration
- Web-UI
- Download-System
- Download-Verlauf
- AutoSync
- Kalender
- Bibliothek
- Authentifizierung
- Benachrichtigungen
- Integrationen
- SyncPlay
- Anime4K-Upscaling
- Encoding
- Module
- Theme-Pakete
- Backup
- Betrieb
- Audit-Log
- Regeln & Sprachen
- Docker
- Unterstützte Seiten
Entwickler