fin#
a Jellyfin client for the terminal — powered by
mpv, Chromecast, and UPnP

fin is a Rust TUI + one-shot CLI that talks to your Jellyfin server, searches
your library, manages playlists, and pushes streams to your local mpv
window, any Chromecast on your network, or any UPnP MediaRenderer (Sonos,
Kodi, Roon endpoints, Samsung/LG TVs, gmediarender, …). Remote playback is
fully queued — enqueue, play-next, skip, resume, all from the terminal.
Features#
- Ratatui-based TUI with a neon-electric palette (teal / cyan / violet).
- fzf-style instant search — results update on every keystroke.
- Drill-in navigation — Enter on an album lists its tracks, Enter on a
series lists its episodes, Enter on a playlist lists its items.
xplays the whole container in one go. - No list truncation — Music, Videos, and Playlists fetch every item the server has, so nothing stays hidden past an arbitrary limit.
- Three renderers, one interface:
mpv(local) — spawned automatically and driven via its JSON IPC socket.chromecast— device discovery via mDNS, playback through the Default Media Receiver, with a local queue that auto-advances onFINISHED.upnp— SSDP discovery of any UPnP AV MediaRenderer, playback via AVTransport (SetAVTransportURI/Play/Pause/Stop/Seek) and volume via RenderingControl. Same auto-advancing queue.
- Real queue management — enqueue, play next, jump between tracks, and see the whole queue in its own tab. Works identically for all renderers.
- Playlists — browse, open, and play the playlists you've saved on the server.
- Now Playing bar with title, subtitle, elapsed / total time, a neon progress gauge, volume, and the active renderer.
- CLI shortcuts for scripting:
fin play "kind of blue",fin queue --chromecast "Living Room" "wednesday",fin play --upnp "Kitchen Speaker" "solaris",fin devices. - All settings are available as CLI flags or TOML keys — one workflow scales from ad-hoc invocation to per-machine config.
- Pure Rust TLS (
rustls) everywhere — no OpenSSL required.
Install#
fin needs mpv on your $PATH at runtime. Every install path below
either bundles it or pulls it in as a dependency.
macOS / Linux — Homebrew#
brew install tsirysndr/tap/fin
The formula pulls in mpv automatically.
Debian / Ubuntu — .deb#
Download the .deb for your architecture from the
latest release and:
# amd64
curl -LO https://github.com/tsirysndr/fin/releases/latest/download/fin_0.2.0_amd64.deb
sudo apt install ./fin_0.2.0_amd64.deb
# arm64 (Raspberry Pi 4/5, Apple-silicon VM, …)
curl -LO https://github.com/tsirysndr/fin/releases/latest/download/fin_0.2.0_arm64.deb
sudo apt install ./fin_0.2.0_arm64.deb
apt will pull in mpv automatically as a dependency.
Or add the Gemfury apt repo once and apt install normally:
echo "deb [trusted=yes] https://apt.fury.io/tsirysndr/ /" \
| sudo tee /etc/apt/sources.list.d/tsirysndr.list
sudo apt update && sudo apt install fin
Fedora / RHEL / openSUSE — .rpm#
sudo dnf install \
https://github.com/tsirysndr/fin/releases/latest/download/fin-0.2.0-1.x86_64.rpm
Or via the Gemfury yum repo:
sudo tee /etc/yum.repos.d/tsirysndr.repo <<'EOF'
[tsirysndr]
name=tsirysndr
baseurl=https://yum.fury.io/tsirysndr/
enabled=1
gpgcheck=0
EOF
sudo dnf install fin
Arch — from AUR / source#
mpv from the official repos, then:
sudo pacman -S mpv
cargo install --git https://github.com/tsirysndr/fin --bin fin
Prebuilt tarballs#
For any other platform, grab the tarball for your arch from the releases page:
fin-<version>-linux-amd64.tar.gzfin-<version>-linux-aarch64.tar.gzfin-<version>-macos-amd64.tar.gzfin-<version>-macos-aarch64.tar.gz
Each includes the fin binary + README + LICENSE. Install mpv yourself:
# macOS
brew install mpv
# Debian / Ubuntu
sudo apt install mpv
# Arch
sudo pacman -S mpv
From source#
git clone https://github.com/tsirysndr/fin
cd fin
cargo install --path crates/fin
Nix#
A flake is provided — mpv is baked into the wrapper, so no extra install step is needed:
# One-off run:
nix run github:tsirysndr/fin
# Install into your user profile:
nix profile install github:tsirysndr/fin
# Dev shell (rust toolchain + mpv + clippy + rust-analyzer):
nix develop
Getting started#
# 1. Sign in
fin login https://media.example.com
# 2. Launch the TUI (default sub-command)
fin
# 3. Or drive it entirely from the shell
fin search "daft punk"
fin play "kind of blue"
fin queue "wednesday season 1"
fin devices # list Chromecasts + UPnP renderers on your LAN
fin play --chromecast "Living Room" "solaris"
fin play --upnp "Kitchen" "solaris"
Renderer selection#
Three ways to choose a renderer — all equivalent:
| Shortcut flag | Long flag | Config key |
|---|---|---|
--mpv |
--renderer mpv |
renderer = "mpv" |
--chromecast "Living Room" |
--renderer chromecast |
renderer = "chromecast" |
--upnp "Kitchen Speaker" |
--renderer upnp |
renderer = "upnp" |
| (none — falls back to mpv) |
When you pass --chromecast NAME or --upnp NAME, the renderer is switched
to that protocol automatically and the named device is preferred on connect.
If the name is not found on the network, fin picks the first device discovered.
All settings#
Every setting exists as both a CLI flag and a TOML key. Flags win.
| CLI flag | Env var | TOML key | Default |
|---|---|---|---|
--server URL |
FIN_SERVER |
servers[].url |
(none) |
--server-name NAME |
FIN_SERVER_NAME |
current_server |
(latest login) |
--token TOKEN |
FIN_TOKEN |
servers[].access_token |
(from login) |
--user-id ID |
FIN_USER_ID |
servers[].user_id |
(from login) |
--user-name NAME |
servers[].user_name |
(from login) | |
--device-id ID |
FIN_DEVICE_ID |
servers[].device_id |
random UUID |
--renderer <mpv/chromecast/upnp> |
FIN_RENDERER |
renderer |
mpv |
--mpv |
renderer = "mpv" |
||
--chromecast [NAME] |
FIN_CHROMECAST |
last_chromecast |
|
--upnp [NAME] |
FIN_UPNP |
last_upnp |
|
-v, -vv |
(log level) | warn |
Find the on-disk config with fin config --path; print it with
fin config --show.
Multiple servers#
fin authenticates against as many Jellyfin servers as you like and keeps their tokens side-by-side in one config file:
fin login https://home.example.com --name home
fin login https://work.example.com --name work
fin login https://mom.dyndns.example --name mom
fin server # list all servers (▍ marks the current one)
fin server switch work # make `work` the active server
fin server rm mom # remove one
fin server rename home casa # rename `home` → `casa`
# One-off — hit `work` without changing the current pointer:
fin --server-name work search "spirited away"
fin --server-name work play "spirited away"
Inside the TUI, the Settings screen shows every saved server; Enter on
one switches to it. t anywhere in the TUI cycles to the next server
without leaving the current screen.
Sub-commands#
fin # launch the TUI (default)
fin login <url> [--name N] # sign in and save credentials for server `N`
fin logout [--name N] # remove server `N` (defaults to the current one)
fin server # list saved servers
fin server switch <name> # change the active server
fin server rm <name> # remove one
fin server rename <a> <b> # rename
fin search <query> # print matches from the active library
fin play <query> # search + play the top hit
fin queue <query> # search + append to the current queue
fin devices # list Chromecasts + UPnP MediaRenderers on the local network
fin playlists # list playlists
fin playlists --list <id> # dump items of a playlist
fin config --show|--path # inspect config
Keybindings (TUI)#
Tab order — the default screen is Music:
1 Music • 2 Videos • 3 Playlists • 4 Queue • 5 Search • 6 Devices • 7 Settings
| Key | Action |
|---|---|
Tab / Shift+Tab |
next / prev screen |
1…7 |
jump to Music / Videos / Playlists / Queue / Search / Devices / Settings |
/ |
jump to Search & focus input |
↑ ↓ / k j |
move selection |
PgUp / PgDown |
jump 10 rows |
Enter |
drill in on a container (album, series, playlist) — plays a leaf (track, episode, movie); on Devices → connect to the selected Chromecast / UPnP renderer; on Settings → switch server |
x |
play the highlighted container as one queue without drilling in (album → all tracks, playlist → all items) |
a |
enqueue the highlighted item |
n |
play the highlighted item next |
Space or p |
pause / resume |
s |
stop |
< / > or h / l |
previous / next track |
+ / - |
volume up / down |
m |
switch to local mpv renderer |
t |
cycle to the next saved Jellyfin server |
Esc |
pop the current drill-in (back to the parent list) |
r |
refresh the current screen |
Esc |
leave the search input / close open playlist |
q / Ctrl-C |
quit |
Remote-renderer queue#
For both Chromecast and UPnP, fin maintains the queue on the client,
polls the device for its current transport state, and loads the next item
automatically the moment the current one finishes (Chromecast:
IDLE / FINISHED; UPnP: STOPPED after having been PLAYING). That means:
a(queue) andn(play-next) do the right thing while something is already streaming.- Skipping (
>/<) triggers aloadfor the next queue item immediately — no waiting for the current one to finish. - Stopping clears the local queue and stops the receiver's playback.
UPnP renderers without a RenderingControl service (rare, but it happens)
still work for transport — volume changes are just no-ops on the device.
Streams & transcoding#
- Local mpv playback uses the original stream (
Static=true), which is the fastest path and lets mpv handle any container Jellyfin can direct-stream. - Chromecast playback defaults to Jellyfin's HLS output (
main.m3u8), because the Default Media Receiver's codec matrix is much narrower than mpv's. This means Jellyfin will transcode when it needs to. - UPnP playback uses the direct stream by default — most UPnP
MediaRenderers speak MP3 / AAC / FLAC natively and the direct path avoids
the transcode. Fall back to HLS with
--hlsif your renderer needs it. - Force one or the other from the CLI with
--hlsonplay/queue.
Development#
cargo check --workspace
cargo build --release -p fin
./target/release/fin --help
The workspace layout:
fin/
├── crates/
│ ├── fin/ # binary — clap CLI + startup
│ ├── fin-config/ # TOML config file & credentials
│ ├── fin-jellyfin/ # Jellyfin HTTP API client
│ ├── fin-player/ # Renderer trait + mpv + Chromecast + UPnP + queue
│ └── fin-tui/ # Ratatui neon TUI
└── Cargo.toml # workspace + shared deps (rustls only, no openssl)
License#
fin is released under the MPL-2.0.