2026-09-06 09:52:04 -07:00
v1
2026-08-18 11:41:12 -07:00
2026-09-06 09:51:16 -07:00
2026-09-01 21:41:13 -07:00
2026-09-01 13:08:03 -07:00
v1
2026-08-18 11:41:12 -07:00
v1
2026-08-18 11:41:12 -07:00
2026-09-06 09:52:04 -07:00
2026-09-01 21:41:13 -07:00
2026-09-06 09:51:16 -07:00
2026-09-06 09:51:16 -07:00
2026-09-01 21:41:13 -07:00
v1
2026-08-18 11:41:12 -07:00
v1
2026-08-18 11:41:12 -07:00
v1
2026-08-18 11:41:12 -07:00
2026-08-18 14:01:49 -07:00

JellyDownloader

A polished macOS desktop app for browsing a Jellyfin server and downloading content — either the original file or a smaller server-side transcode — then watching it offline.

Built with Tauri v2 (Rust backend) and React + TypeScript (Vite).

Features

  • Sign in to any Jellyfin server (10.9+) with the standard MediaBrowser auth scheme — by password or by approving a Quick Connect code in another client, picking your account from the server's public users, with the server's login disclaimer shown. The session persists across launches and is re-validated on boot: a token revoked server-side signs you out, while an unreachable server leaves the session (and the offline library) alone. Signing out revokes the token via /Sessions/Logout.
  • Browse & search: home screen with library tiles, "next up", continue-watching and latest-added rows; poster grids with infinite scroll, sorting and genre / rating / year / watched filters (/Items/Filters2); debounced global search over /Search/Hints, grouped by movies / series / episodes / collections / people; rich item pages (backdrop hero, media info, season & episode lists, cast & crew, "up next", "more like this").
  • Download queue (Rust engine):
    • Original quality via /Items/{id}/Download — exact progress, pause/resume via HTTP Range, resumes across app restarts.
    • Reduced bitrate via server transcoding (/Videos/{id}/stream.mp4) with presets from 1080p·20Mbps down to 480p·1.5Mbps — estimated size & progress from bitrate × runtime.
    • Re-encode on this Mac instead: the original is pulled at full link speed and encoded here (x264/x265 or VideoToolbox), keeping the text subtitles and optionally burning one in — bitmap subtitles included.
    • Configurable parallelism (16, default 2), pause / resume / cancel / retry, queue persisted to disk. A download that fails in passing retries on its own with backoff; the queue can be reordered by dragging or "Start next"; an optional speed limit caps the total.
    • The Mac stays awake while the queue works, a notification says when a download finishes or fails, and the Dock icon carries the overall progress.
    • Files organized as Movies/Title (Year)/… and Shows/Series/Season NN/Series - SxxEyy - Name [Quality].ext with sanitized names; a title held in several versions gets the version in the name too, so they never collide.
  • Multiple versions: when a title exists on the server as more than one file (a 4K remux beside a 1080p encode, a director's cut beside the theatrical), the item page offers a version picker — the highest-fidelity one first — and the choice flows into streaming (MediaSourceId on PlaybackInfo) and into downloads. Single-version titles show no picker at all.
  • Offline library: completed downloads with cached posters, grouped by series and season with a filter and disk usage, bulk delete and "Delete watched" (works with the server offline). Text subtitles travel with a download as WebVTT sidecars. In-app player for MP4s with custom controls, keyboard shortcuts, playback speed, chapter marks, scrubbing thumbnails and autoplay of the next episode; "open in system player" and "reveal in folder" for everything else (e.g. MKVs).
  • Smart downloads: a series page can ask to keep the next few unwatched episodes downloaded, and to delete the watched ones behind you.
  • Sign-in polish: recent servers offered as chips, the machine's own name in the server's device list, and the session token in the macOS Keychain.
  • Settings: download folder, default and streaming quality, parallel downloads and connections, speed limit, keep-awake, notifications, autoplay, delete after watching, local encoder, colour scheme, sign out.

Installing

Download the latest JellyDownloader_<version>_aarch64.dmg from the releases page, open it, and drag JellyDownloader into Applications. Apple Silicon only for now.

The app isn't signed with an Apple Developer certificate, so the first time you open it macOS says it is damaged and refuses. That message means the download carries the quarantine flag, not that the file is broken. Clear the flag once from Terminal, then open the app as usual:

xattr -rd com.apple.quarantine /Applications/JellyDownloader.app

If you put the app somewhere other than Applications, point the command at that path. This is only ever needed for the first install: later versions are offered inside the app (Settings → About → Updates, or the notice in the sidebar) and install without it, because the app fetches them itself and nothing quarantines what it downloads.

Development

Prereqs: Rust (stable), Node 20+.

npm install
npm run tauri dev      # run the app
npm run tauri build    # package for the current platform

npm run typecheck      # tsc --noEmit
npm run lint           # eslint
npm test               # vitest unit tests
cargo test             # Rust unit tests (run in src-tauri/)

Releasing

The app updates itself. On launch it reads latest.json from this repo through the Gitea API (the endpoint is under plugins.updater in src-tauri/tauri.conf.json), and Settings → About offers any newer build. The app downloads and unpacks the bundle itself, so the new copy never carries macOS's quarantine attribute and Gatekeeper never inspects it — the terminal step is only needed for the first install of an unsigned build.

Updates are signed with a minisign key that the updater checks against the public key in tauri.conf.json. Generate it once and keep it out of the repo. If the key is ever replaced, apps built with the old public key stop accepting updates and have to be reinstalled by hand.

npm run tauri signer generate -- -w ~/.tauri/jellydownloader.key

To ship a version:

scripts/fetch-ffmpeg.sh                                    # once per checkout
scripts/release.sh 0.2.0                              # GITEA_TOKEN from the environment or a local .env

That bumps the version in package.json, src-tauri/tauri.conf.json and src-tauri/Cargo.toml, builds a signed .app.tar.gz, commits and tags v0.2.0, publishes a Gitea release with the bundle (and the DMG for first installs, if .dmg is in Gitea's allowed attachment types), then rewrites and pushes latest.json. The release notes, which the in-app updater shows, come from the version's section in CHANGELOG.md; pass --notes "text" or --notes-file path to override them. That last push is what makes installed apps see the new version. The token needs the write:repository scope, and the repo has to be public: installed apps fetch the manifest and the bundle anonymously.

Architecture

src/                     React frontend
  api/jellyfin.ts        Typed Jellyfin REST client (via tauri-plugin-http; tokens in headers, never URLs)
  api/qualities.ts       Quality presets, size estimation, download-request builder
  api/versions.ts        Media-source versions: labelling, default pick, file-name tags
  api/browse.ts          Collection-type mapping, library filter serialisation, search-hint adapter
  ipc/backend.ts         Typed Tauri command/event bindings
  stores/                zustand stores (auth, downloads, settings, libraries, toasts)
  views/                 Login, Home, Library, Search, Item, Downloads, Player, Settings
src-tauri/src/
  downloads/manager.rs   Download engine: tokio streaming, parallelism, pause/resume, persistence
  state.rs               Settings + session persistence
  commands.rs            Tauri commands (queue ops, library ops, open/reveal)

State lives in two JSON stores in the app data dir: queue.json (jobs) and library.json (completed downloads + metadata snapshots), plus a covers/ poster cache.

Throughput testing

tools/speedtest is a headless harness that replicates the download engine's exact hot loop (same reqwest config, same per-chunk checks, same buffered writes) against the server you're signed in to — useful for separating client, network, and server bottlenecks without the GUI:

cd tools/speedtest
cargo run --release -- <itemId> --mode original --parallel 4 --seconds 15
cargo run --release -- <itemId> --mode transcode --vbr 10000000 --maxw 1920

Compare cargo run (debug) vs --release to isolate build-profile effects, --parallel N for connection scaling, and --no-disk to exclude file I/O. Credentials are read from the app's config dir; findings so far: per-TCP-stream throughput is latency-bound (parallel streams scale), and transcode throughput is bound by the server's encoder (e.g. a shared QuickSync engine), not by this client.

Playback diagnostics

With "Keep a playback diagnostics log" on (Settings → Downloads & streaming; off by default), both players write every playback to ~/Library/Logs/com.oliverpelly.jellydownloader/playback.log (one JSON object per line, rolled over at 32 MB): frames presented off their schedule, the media clock stepping or stalling, the page's own frames stopping, every media event, and — from the Rust side — the timing of each read the offline player made through the loopback proxy. d in the offline player shows the live counters. tools/playback-lab has the analyser and an unattended run over synthetic files; see its README.

Notes

  • Transcoded downloads can't resume (the server can't seek a live transcode) — pausing one restarts it on resume; originals resume byte-exact.
  • The session token is stored locally in the app config directory, like most Jellyfin clients.

Licence

JellyDownloader is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License, version 3 or later. See LICENSE for the full text, and no warranty is provided.

The GPL is not an arbitrary choice. The app bundles an FFmpeg built with --enable-gpl --enable-version3, because client-side encoding needs x264/x265 and those are GPL-only. That makes the distributed whole GPLv3. scripts/fetch-ffmpeg.sh refuses any build configured --enable-nonfree, which FFmpeg itself declares not redistributable under any licence.

Third-party components and their obligations — FFmpeg, hls.js (Apache-2.0) and the three OFL-licensed fonts — are listed in THIRD-PARTY-NOTICES.md.

Trademark

Jellyfin is a trademark of the Jellyfin project. JellyDownloader is an independent third-party client and is not affiliated with, endorsed by, or sponsored by the Jellyfin project. It is intended for downloading from a Jellyfin server you own or are authorised to use.

S
Description
No description provided
Readme GPL-3.0 4.7 MiB
v0.3.1 Latest
2026-09-06 16:51:16 +00:00
Languages
TypeScript 63.7%
Rust 27.7%
CSS 6.6%
Shell 1.1%
Python 0.9%