Downloads¶
fetchly wraps yt-dlp in a web UI: format selection happens in menus, the job runs in the background, and the result lands in a per-job directory on your data volume.
Supported platforms¶
| Platform | Video | Audio | Detected from |
|---|---|---|---|
| YouTube | youtube.com, youtu.be | ||
| TikTok | tiktok.com | ||
instagram.com | |||
facebook.com, fb.watch |
The platform is detected from the URL and decides which cookie jar (if any) is passed to yt-dlp. URLs that match no known platform are rejected at submit time.
Format selection¶
| Type | Quality | Behaviour |
|---|---|---|
video | max | Best available video + audio stream |
video | medium | Capped at 720p |
video | small | Capped at 480p |
audio | any | Audio extracted, encoded to MP3 |
Universally playable output¶
Settings → Processing → Downloads → Universally playable output (H.264/AAC) (off by default; the watermark turns it on)
This is the one setting that decides what max quality actually means: the highest resolution the source has, or a file that plays on every device. On YouTube you cannot have both, because YouTube only encodes H.264 up to 1080p — 1440p and 2160p exist exclusively as VP9 or AV1.
Off — max is a pure download and remux. yt-dlp picks the best rendition by resolution, and the streams are muxed into the container they belong in: .mp4 when the source is H.264/AAC anyway, .webm for VP9/Opus, .mkv for AV1. No encoder runs, so the file is bit-for-bit what the platform serves, at the highest resolution available, in the smallest file the codec can manage. The cost is reach: Safari, iOS, most smart TVs and a good deal of editing software cannot open VP9 or AV1. Jobs whose result falls into that category are marked Limited playback in the job list and in the details dialog, so a file that will not open on your phone is visible here rather than a surprise there.
On — the finished file is guaranteed to be H.264/AAC in MP4. fetchly keeps that promise as cheaply as it can, in two steps:
- At format selection, yt-dlp is told to sort
vcodec:h264ahead of resolution (-S vcodec:h264,lang,quality,res,fps,hdr:12,acodec:aac). Where a compatible rendition exists — which on YouTube is nearly always — it is simply downloaded and remuxed. No encoder runs, and no quality is lost; you only give up the resolutions that exist solely in VP9/AV1. - Only if that fails — a source that has no H.264 rendition at all — the file is re-encoded afterwards, and then only as far as necessary: if just the audio codec is wrong the video is stream-copied (
-c:v copy) and only the audio becomes AAC.
Note the asymmetry: turning the switch on costs resolution but almost never CPU; turning it off costs reach but nothing else. There is no setting that spends CPU to give you 4K H.264, because transcoding a 2160p AV1 source to H.264 would take longer than the download, produce a much larger file, and still look worse than the source it came from.
The capped qualities (medium, small) are unaffected: they always re-encode to H.264/AAC in MP4 and are therefore universally playable regardless of this switch.
The watermark requires it
Burning in the watermark means running libx264 over the video anyway, so the compatible container comes along for free and the switch is forced on and locked while the watermark is active. Your own choice is stored untouched and applies again as soon as you turn the watermark off.
Concurrent fragments¶
Settings → Processing → Downloads → Parallel fragments per download (Automatic or 1–16, default Automatic)
Parallel fragment downloads for DASH/HLS sources. Progressive single-file downloads ignore it. Raise it on a fast link; lower it if a platform throttles you.
Automatic picks the value for each download from the host's CPU quota and free memory — 2 fragments on a small or currently loaded container, up to 8 on a big idle host. The setting hint names the value it resolves to right now. See Resources & Workers.
Video watermark¶
Settings → Processing → Watermark → Show fetchly watermark (on by default)
Burns the fetchly logo into the bottom-right corner of every downloaded video, with the public hostname on a second line once one is set. Audio-only jobs are untouched.
The badge is composited once per hostname and output size and cached under data/watermark-cache/, so each encode only alpha-blends a still image into the corner. On medium and small quality it rides along in the transcode fetchly already runs and costs nothing measurable. max quality is otherwise a pure download and remux, so it gains an x264 pass that a 4K download will feel — turn the switch off to leave max downloads untouched. That pass picks its preset and CRF from the source resolution (medium/CRF 16 up to 576p, fast/CRF 18 up to 1080p, veryfast/CRF 20 above), because a small low-bitrate source is re-quantized much more visibly than a high-bitrate 4K one. Audio is stream-copied unless the compatibility promise needs it re-encoded.
The hostname line uses the Roboto Flex font shipped with the app UI (app/static/fonts/); no system font package is required. If that file is missing, the logo is drawn alone and a warning is logged. See Application Settings for the full note.
Your own logo¶
Settings → Processing → Watermark → Custom logo
Drop an SVG or a PNG on the zone (or click it to pick a file) to replace the fetchly logo with your own. The logo in use — yours or the built-in one — stays on screen underneath as a small preview on a checkerboard, so transparency reads as transparency; the trash button beside it puts the built-in artwork back. The bundled file is never overwritten, so this is always reversible.
A PNG is uploaded byte for byte. It is already what fetchly stores, so there is nothing to convert and no re-encode to cost you quality. It needs an alpha channel: a logo with no transparency is a box over the video, not artwork on it.
An SVG is rasterized in your browser, and only the resulting PNG is uploaded. That is deliberate, and worth knowing about:
- The static ffmpeg builds in the Docker image have no SVG decoder, so the server could not render your file even if it wanted to.
- An SVG is a document that can carry scripts. Storing one and serving it back from fetchly's own origin would be a stored-XSS primitive. Keeping only flat pixels removes that class of problem entirely. (Drawing an SVG through an
<img>, which is what the drop zone does, puts it in the browser's secure static mode: no scripts run and no external references load.)
Either way, one PNG ends up at data/logo/watermark-logo.png — a single file in its own directory on the data volume, replaced on each upload and deleted when you restore the built-in logo.
Both sides validate, but only one of them is a boundary. The browser checks the file before uploading it: an SVG has to parse, carry no <script>, <foreignObject> or on… handlers, reference nothing outside itself, and have a determinable size from viewBox or width/height; a PNG has to decode and land inside the size and shape bounds. The server then re-derives everything from the uploaded bytes and refuses the file unless all of it holds:
| Check | Requirement |
|---|---|
| Format | Decodes as PNG (ffprobe), non-zero size |
| Size | 32–4096 px per side, at most 2 MB |
| Shape | Aspect ratio between 1:10 and 20:1 — a wide logo is normal, a tall sliver is not |
| Transparency | An alpha channel, and at least one non-opaque pixel |
The transparency rule is the one that is about suitability rather than validity: a logo with no transparency is not artwork on the video, it is a box over it, so it is refused with that explanation rather than silently accepted.
A custom logo keeps its own proportions — the badge is laid out from the uploaded image's aspect ratio, not the bundled logo's — and gets its own entries in the badge cache, so replacing it can never leave the previous artwork on a video.
The pipeline¶
flowchart TD
S[Submit URL + type + quality] --> M[Metadata probe, 8 s budget]
M --> Q[Insert job as queued]
Q --> W[Worker thread picks it up]
W --> D[yt-dlp download]
D --> T{Needs transcoding?}
T -- yes --> F[ffmpeg]
T -- no --> N[Keep source]
F --> A{Audio job?}
N --> A
A -- yes --> B[BPM analysis]
A -- no --> DONE[done]
B --> AD[analysis_done] At submit time fetchly spends at most 8 seconds probing the URL for a title and metadata so the job list has something readable straight away. If the probe times out, the job is still created and the worker fills the metadata in later.
Duplicate detection¶
Submitting a URL that already has an active or completed job with the same type and quality returns 409 Conflict with the existing job attached. The UI surfaces this and lets you confirm a second copy.
Errored and cancelled jobs deliberately do not count, so retrying a failed download is never blocked.
Cancelling and retrying¶
| Action | Endpoint | Notes |
|---|---|---|
| Cancel | POST /api/jobs/{job_id}/cancel | Terminates the running subprocess |
| Retry | POST /api/jobs/{job_id}/retry | Requeues the same URL, type, and quality |
| Remove all | POST /api/jobs/remove-all | Deletes every job and its artifacts |
Output files¶
Each job gets its own directory under the data volume:
/app/data/<job-uuid>/
├── <title>.source.<ext> # what yt-dlp produced
├── <title>.mp3 # transcoded audio, when applicable
├── thumbnail.jpg # normalized thumbnail
├── trim_<start>_<end>.wav # trim outputs
└── trim_<...>_vocals.mp3 # Lalal.ai stems
The .source marker keeps the original download distinguishable from derived files. The name handed to the browser can differ from the name on disk — a detected tempo is folded in as a _<bpm>bpm tag at download time, while the on-disk name stays plain so the MP3 cache and the stem lookup keep matching. See BPM Analysis.
Limits¶
| Limit | Default | Configure in |
|---|---|---|
| Max input size | 4 GiB | Settings → General → Runtime limits |
| Download timeout | 60 min | Settings → General → Runtime limits |
| Transcode timeout | 120 min | Settings → General → Runtime limits |
| Submit rate | 10/minute | fixed |
See Application Settings.
Gated content¶
Public URLs work signed out. Age- or login-gated content needs an imported browser session — see Platform Cookies. A missing, invalid, or expired jar is skipped rather than failing the job: the download simply runs signed out.
YouTube JS challenges
YouTube serves JavaScript challenges that yt-dlp solves through yt-dlp-ejs and a deno runtime. Both are bundled in the container. In a standalone install, deno must be on PATH or some YouTube downloads will fail.