fin#
a Jellyfin & Subsonic client for the terminal — powered by
rockbox-playback,mpv, Chromecast, and UPnP

fin is a Rust TUI + one-shot CLI that talks to your Jellyfin or
Subsonic server (Navidrome, Airsonic, Gonic, Astiga, … — the flavour is
auto-detected at login), searches your library, manages playlists, and pushes
streams to your local machine
(rockbox-playback for audio, mpv for video), any Chromecast on your
network, or any UPnP MediaRenderer (Sonos, Kodi, Roon endpoints, Samsung/LG
TVs, gmediarender, …). Local playback is now audio-native — HTTP streaming,
decoding, resampling, and output all run in-process, and audio never touches
mpv. Remote playback is fully queued, with client-side auto-advance.
Contents#
- Features
- Install
- Getting started
- Renderer selection
- UPnP MediaRenderer — casting to fin
- MPRIS — desktop media controls
- All settings
- Multiple servers
- Sub-commands
- Keybindings (TUI)
- Playback modes & effects
- Queue persistence
- Remote-renderer queue
- Supported audio formats
- Streams & transcoding
- Development
- License
Features#
- Ratatui-based TUI with a neon-electric palette (teal / cyan / violet).
- In-process audio — HTTP streaming +
rockbox-playbackdecode (MP3, FLAC, AAC, Opus, Vorbis, ALAC, WAV, and many more)- resampling +
cpaloutput, using Rockbox's own codecs and DSP. mpv is used only for video.
- resampling +
- 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,Shift+Xdoes the same shuffled. - 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:
- local (default) — rockbox-playback for audio, mpv for video, spawned only when needed.
- chromecast — device discovery via mDNS, playback through the Default
Media Receiver, with a local queue that auto-advances on
FINISHED. - upnp — SSDP discovery of any UPnP AV MediaRenderer, playback via
AVTransport (
SetAVTransportURI/Play/Pause/Stop/Seek) and volume via RenderingControl. Same auto-advancing queue.
- Cast to fin — while the TUI runs, fin advertises itself as a
UPnP MediaRenderer on the LAN. Push streams at it from BubbleUPnP,
Kodi, Jellyfin's "Play On", or any other control point: audio decodes
in-process via rockbox-playback, video opens in mpv, and the pushed track lands
in the Now Playing bar with a
⇊ UPnPbadge. On by default; opt out with--no-media-rendererormedia_renderer.enabled = false(see UPnP MediaRenderer). - MPRIS (Linux & the BSDs) — while the TUI runs, fin registers on the
D-Bus session bus as
org.mpris.MediaPlayer2.fin, so media keys, GNOME/KDE applets, waybar, andplayerctlcontrol playback — whichever renderer is active, Chromecast and UPnP included (see MPRIS). - Playback modes — shuffle, repeat-off/all/one, ReplayGain (track / album), crossfade between adjacent tracks (traditional cosine curves or additive DJ-mixed), and a 10-band equalizer powered by the Rockbox DSP pipeline.
- Queue persistence — the audio queue, shuffle/repeat state, and the exact
playhead within the current track survive restarts. Restore lands paused;
Spacepicks up where you left off. - Real queue management — enqueue, play next, jump between tracks, remove
a single entry, clear the whole queue, and see the queue in its own tab
with a
▶marker on the actually-playing track. - 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 mode badges (shuffle ⇄, repeat ↻/↺, ReplayGain, crossfade ⋈/≈).
- 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#
Local audio needs no extra binaries — everything is baked into the fin
binary. mpv is only needed on $PATH when you actually play video
locally. Every install path below either bundles it or pulls it in as a
soft 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.6.0_amd64.deb
sudo apt install ./fin_0.6.0_amd64.deb
# arm64 (Raspberry Pi 4/5, Apple-silicon VM, …)
curl -LO https://github.com/tsirysndr/fin/releases/latest/download/fin_0.6.0_arm64.deb
sudo apt install ./fin_0.6.0_arm64.deb
apt will pull in libasound2 (ALSA runtime for cpal) and mpv automatically.
Or add the Gemfury apt repo once and apt install normally:
echo "deb [trusted=yes] https://apt.fury.io/tsiry/ /" \
| sudo tee /etc/apt/sources.list.d/tsiry.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.6.0-1.x86_64.rpm
Or via the Gemfury yum repo:
sudo tee /etc/yum.repos.d/tsiry.repo <<'EOF'
[tsiry]
name=tsiry
baseurl=https://yum.fury.io/tsiry/
enabled=1
gpgcheck=0
EOF
sudo dnf install fin
Arch — from AUR / source#
mpv from the official repos, then:
sudo pacman -S mpv alsa-lib
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 runtime deps
yourself:
# macOS
brew install mpv # video only; audio is in-process
# Debian / Ubuntu
sudo apt install libasound2 mpv
# Arch
sudo pacman -S alsa-lib mpv
From source#
git clone https://github.com/tsirysndr/fin
cd fin
cargo install --path crates/fin
Build-time on Linux needs libasound2-dev + pkg-config (cpal's ALSA
backend); on macOS the Core Audio SDK is already in the toolchain.
Nix#
A flake is provided — mpv is baked into the wrapper, so no extra install step is needed:
# Use Cache
cachix use tsirysndr
# One-off run:
nix run github:tsirysndr/fin
# Install into your user profile:
nix profile install github:tsirysndr/fin
# Dev shell (rust toolchain + mpv + alsa-lib + 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 local) |
The --mpv / renderer = "mpv" flag name is historical; it selects the
local renderer, which uses rockbox-playback for audio and mpv for video.
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.
UPnP MediaRenderer — casting to fin#
The renderer table above is about fin sending streams elsewhere. This is
the opposite direction: while the TUI runs, fin also shows up on the LAN as
a UPnP AV MediaRenderer device, so any control point — BubbleUPnP,
Kodi, Jellyfin's "Play On", gupnp tools, another fin casting with
--upnp, … — can push media at this machine.
Incoming streams take the same local path as everything else: audio is
decoded in-process by rockbox-playback (never mpv), video is handed to mpv.
The pushed track appears in the Now Playing bar with a violet ⇊ UPnP
badge and a one-shot status line (⇊ receiving UPnP cast — <title>), joins
the queue like any other item, and answers the normal transport keys —
Space pauses it, s stops it, +/- change volume. The control point
sees those changes reflected back through AVTransport/RenderingControl
state and GENA LastChange events.
It's on by default. Three equivalent ways to turn it off:
fin --no-media-renderer # this run only
FIN_NO_MEDIA_RENDERER=1 fin # environment
# config.toml — persistent
[media_renderer]
enabled = false
Optional keys under the same table:
[media_renderer]
enabled = true
friendly_name = "office fin" # picker name; default: fin (<hostname>)
port = 47899 # description/control port; 0 = ephemeral
# uuid is generated on first launch and persisted so control points
# recognize the device across restarts — no need to set it by hand.
Playback pushed by a control point is not scrobbled — the item ids are foreign to your media server, so session reports/scrobbles are skipped for cast-in tracks.
MPRIS — desktop media controls#
On Linux and the BSDs, fin registers itself on the D-Bus session bus as
org.mpris.MediaPlayer2.fin while the TUI runs. That's the standard
desktop media-player interface, so it works with whatever already speaks
MPRIS:
- hardware media keys (play/pause/next/previous) — even when the terminal isn't focused,
- desktop applets — GNOME's calendar-dropdown media widget, KDE's Media Player plasmoid and lock-screen controls,
- bars — waybar's
mprismodule and friends, playerctlfor scripting:
playerctl -p fin play-pause
playerctl -p fin next
playerctl -p fin metadata # title, artist, duration, cover art
playerctl -p fin position 30 # absolute seek (seconds)
playerctl -p fin volume 0.5
playerctl -p fin shuffle On
playerctl -p fin loop Playlist # = repeat all
MPRIS drives whatever renderer is currently active — switch to a Chromecast or UPnP device in the TUI and your media keys transparently control the cast session. Track metadata (title, artist, duration, cover art) is pushed to the desktop as it changes.
There's nothing to configure and no system dependency to install (the
D-Bus protocol is spoken natively — no libdbus). On a headless box with
no session bus, fin simply logs a warning and carries on. A second fin
instance registers as org.mpris.MediaPlayer2.fin.instance<pid>, per the
MPRIS spec.
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 |
|
--no-media-renderer |
FIN_NO_MEDIA_RENDERER |
media_renderer.enabled |
true |
--media-renderer |
media_renderer.enabled = true |
||
media_renderer.friendly_name |
fin (<hostname>) |
||
media_renderer.port |
0 (ephemeral) |
||
media_renderer.uuid |
(generated once) | ||
-v, -vv |
(log level) | warn |
Audio-side playback settings live under TOML sub-tables and are toggled from the TUI (see Playback modes & effects):
| TOML | Default | Notes |
|---|---|---|
replaygain.mode |
off |
off / track / album |
replaygain.preamp_db |
0.0 |
additive in dB before clip guard |
replaygain.prevent_clip |
true |
caps gain so linear * peak <= 1.0 |
crossfade.mode |
off |
off / crossfade / mixed |
crossfade.duration_secs |
5.0 |
overlap window in seconds |
eq_enabled |
false |
toggle the Rockbox 10-band EQ pipeline |
[[eq_band_settings]] |
ISO octave | 10 bands (see Equalizer); Rockbox-compatible |
bass |
0 |
bass shelf gain in whole dB (−24…+24) |
treble |
0 |
treble shelf gain in whole dB (−24…+24) |
bass_cutoff |
0 |
bass shelf cutoff in Hz (0 = Rockbox default 200) |
treble_cutoff |
0 |
treble shelf cutoff in Hz (0 = Rockbox default 3500) |
Find the on-disk config with fin config --path; print it with
fin config --show.
While the TUI runs, logs are written to fin.log in the cache directory
(next to queue.json) rather than stderr, so they can't corrupt the
display; one-shot CLI commands log to stderr as usual. -v / -vv and
RUST_LOG control the level either way.
Multiple servers#
fin authenticates against as many servers as you like — Jellyfin and Subsonic mixed freely — and keeps their credentials 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 Favorites • 5 Queue • 6 Search • 7 Devices • 8 Settings
The Videos tab is Jellyfin-only — Subsonic has no video API, so on a
Subsonic server the tab is hidden, Tab/Shift+Tab skip past it, and 2
shows a hint instead. The other number keys keep their meanings.
| Key | Action |
|---|---|
? |
show / hide the full keyboard-shortcuts help modal |
Tab / Shift+Tab |
next / prev screen |
1…8 |
jump to Music / Videos / Playlists / Favorites / 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 Queue → jump the playhead to the selected entry; 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) |
Shift+X |
shuffle-play — same pool as x for a highlighted container; on flat views (Favorites, Videos, open album/playlist) the whole list, in random order, with shuffle mode switched on |
a |
enqueue the highlighted item |
n |
play the highlighted item next |
Shift+L / Shift+D |
like / dislike — add or remove the highlighted item (or the playing track) from Favorites; Jellyfin favorites, Subsonic stars |
z |
toggle shuffle |
Shift+R |
cycle repeat mode (off → all → one) |
g |
cycle ReplayGain (off → track → album) |
f / Shift+F |
cycle crossfade mode / cycle crossfade duration (3, 5, 8, 12 s) |
Shift+E |
toggle 10-band Rockbox EQ |
[ / ] |
(Settings) select previous / next EQ band |
Shift+↑ / Shift+↓ |
(Settings) nudge the selected EQ band's gain by ±1 dB |
b / Shift+B |
bass shelf −1 dB / +1 dB |
y / Shift+Y |
treble shelf −1 dB / +1 dB |
Space or p |
pause / resume |
s |
stop |
< / > or h / l |
previous / next track |
+ / - |
volume up / down |
m |
switch to local renderer |
t |
cycle to the next saved server |
d |
(Queue screen) remove the highlighted entry |
Shift+C |
(Queue screen) clear the entire queue |
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 |
Playback modes & effects#
Shuffle and repeat work on every renderer — the queue lives on the
client, so they apply while casting to Chromecast or UPnP too. The audio
effects (ReplayGain, crossfade, EQ, bass & treble) run only on the local
renderer; Chromecast and UPnP receivers do their own DSP, so those toggles
no-op there. Settings persist to config.toml and are mirrored back on
next launch.
Shuffle#
z toggles shuffle. Enabling it reshuffles every item after the
currently-playing track using a Fisher-Yates permutation — the playing track
stays put so the audio doesn't jump.
Repeat#
Shift+R cycles the repeat mode off → all → one → off. all wraps in both
directions (Prev at row 0 goes to the last item); one sticks on the
current track until the mode changes.
ReplayGain#
g cycles Off → Track → Album → Off. Reads REPLAYGAIN_TRACK_GAIN,
REPLAYGAIN_ALBUM_GAIN, and the matching peak tags off decoded tracks
(Vorbis-comment or ID3v2), computes a linear gain multiplier
(10^((gain + preamp) / 20)), and folds it into the sample push loop. If
the requested scope's tag is missing, the other scope is used as a fallback.
Clip prevention is on by default — it caps the multiplier so peaks stay ≤ 1.0.
Crossfade#
f cycles Off → Crossfade → Mixed → Off.
- Crossfade — cosine/sine curves (out² + in² = 1); perceived loudness stays constant across the overlap.
- Mixed — no curves; both tracks play at full volume during the overlap and sum additively (louder DJ-style mix).
Shift+F cycles the duration through 3, 5, 8, 12 s, preserving the current
mode. Duration is also editable directly in config.toml.
Under the hood, fin runs a second decoder + cpal output stream during the
overlap and the OS mixer sums them. The overlap kicks in both on natural
end-of-track transitions AND when you Play a new album or jump to a new
queue entry — so switching tracks manually still fades cleanly.
Equalizer#
fin links the Rockbox DSP pipeline
(rockbox-dsp) for a fixed-point
10-band EQ with high-quality biquad filters — band 0 is a low shelf, band 9
a high shelf, bands 1–8 are peaking filters. On the Settings screen you'll
see 10 vertical sliders with dB labels above and cutoff frequency labels
below. The controls:
| Key | Action |
|---|---|
E |
toggle EQ on / off |
[ / ] |
move the highlighted band left / right |
Shift+↑ / ↓ |
bump the highlighted band's gain by ±1 dB |
Adjustments persist to config.toml immediately. Fresh installs get the
ISO-octave flat preset (32 Hz, 63, 125, 250, 500, 1 kHz, 2, 4, 8, 16 kHz,
Q 7.0, 0 dB across the board) so the DSP is a bit-exact bypass until you
start tweaking. Values in [[eq_band_settings]] use Rockbox tenths — q = 70 means Q 7.0, gain = -125 means −12.5 dB — so a Rockbox preset drops
in unchanged.
License note: enabling EQ links the Rockbox DSP C sources (GPL-2.0-or-later),
which makes the resulting fin binary GPL. The rest of fin remains MPL-2.0
in source form.
Behavioral notes:
- Only local playback runs through EQ. Chromecast and UPnP receivers each do their own DSP; the toggle is a no-op there.
- During a crossfade the outgoing and incoming tracks both route through the same Rockbox DSP config. The biquad delay lines get briefly stirred when the two tracks alternate through the pipeline — the audible transient is well under a millisecond at 48 kHz.
Bass & treble#
The Rockbox tone-control stage runs in the same DSP pipeline as the EQ —
shelving filters at fixed cutoffs (default 200 Hz bass, 3500 Hz treble).
Adjust with b / Shift+B for bass and y / Shift+Y for treble; every
press is a 1 dB step in the ±24 dB range and is persisted to config.toml
immediately. The Settings screen shows the current values, and the player
bar shows a compact B+3/T-2 badge whenever either is non-zero.
Custom shelf cutoffs can be set in config.toml via bass_cutoff /
treble_cutoff (Hz); 0 means the Rockbox defaults. The keys and this
stage go through the same singleton pipeline as EQ, so the licensing note
above applies.
Queue persistence#
The audio queue, shuffle/repeat state, and the exact playhead within the
currently-playing track are written to cache_dir/queue.json on every
mutation and every ~3 s while playing. Writes are debounced and atomic
(rename-in-place), so a crash mid-write can't leave a truncated file.
On startup, fin reads the snapshot and restores the queue paused at the
saved position. Space (or p) resumes from exactly where you left off.
Video items in a saved queue are filtered out silently — the persistence
path lives on the audio side; the mpv-driven video path is transient.
Find the file with fin config --path (adjacent to the config dir).
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. - Shuffle (
z) and repeat (Shift+R) act on this client-side queue, so they work while casting exactly like local playback. - 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.
Supported audio formats#
Local audio is decoded in-process by
rockbox-playback, which uses
Rockbox's own firmware codecs — the full 29-codec static-link set, all
enabled. Any of these plays gapless, with ReplayGain, crossfade and the EQ
straight through the same path:
Lossless#
| Format | Typical extension | Notes |
|---|---|---|
| FLAC | .flac |
|
| Apple Lossless | .m4a |
ALAC |
| WavPack | .wv |
|
| Monkey's Audio | .ape |
|
| True Audio | .tta |
|
| Shorten | .shn |
Lossy#
| Format | Typical extension | Notes |
|---|---|---|
| MP3 / MP2 / MP1 | .mp3 .mp2 |
MPEG audio (incl. MP3-in-ASF) |
| AAC / HE-AAC | .m4a .aac |
incl. AAC+ / SBR |
| Ogg Vorbis | .ogg |
|
| Opus | .opus |
|
| Musepack | .mpc |
SV7 / SV8 |
| Speex | .spx |
|
| WMA v1 / v2 | .wma |
in ASF |
| WMA Professional | .wma |
|
| AC3 (A/52) | .ac3 |
|
| RealAudio Cook | .ra .rm |
|
| AAC in RealMedia | .rm |
|
| ATRAC3 | .oma .aa3 |
Sony OMA + RealMedia |
Uncompressed / PCM#
| Format | Typical extension | Notes |
|---|---|---|
| WAV | .wav |
PCM + the ADPCM family |
| AIFF | .aiff .aif |
|
| Sun AU / SND | .au .snd |
|
| Sony Wave64 | .w64 |
|
| SMAF | .mmf |
Yamaha mobile |
| Dialogic VOX | .vox |
ADPCM |
Specialized / game audio#
| Format | Typical extension | Notes |
|---|---|---|
| CRI ADX | .adx |
|
| Amiga MOD | .mod |
Chiptune emulator formats (SPC, SID, NSF/GBS/HES/KSS/AY/SGC/VGM/VTX, SAP) are not supported — they need Rockbox's one-codec-at-a-time model and are excluded from the static-link set fin builds against.
Streams & transcoding#
- Local audio uses the original stream —
rockbox-playbackdecodes it in-process with Rockbox's codecs (see Supported audio formats), resampled to the output device's rate. No transcoding round-trip; no mpv on the audio path. - Local video shells out to mpv with
Static=true— the fastest direct-stream path, mpv handles any container Jellyfin can hand it. - Chromecast playback defaults to Jellyfin's HLS output (
main.m3u8), because the Default Media Receiver's codec matrix is much narrower than mpv's. 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
cargo test --workspace
The workspace layout:
fin/
├── crates/
│ ├── fin/ # binary — clap CLI + startup
│ ├── fin-config/ # TOML config file, credentials, mode enums
│ ├── fin-jellyfin/ # Jellyfin HTTP API client
│ ├── fin-subsonic/ # Subsonic HTTP API client (Navidrome, Airsonic, …)
│ ├── fin-media/ # MediaClient trait over both backends + login probe
│ ├── fin-mediarenderer/ # built-in UPnP MediaRenderer — casting *to* fin
│ ├── fin-player/ # Renderer trait, queue, rockbox-playback audio path,
│ │ # mpv video, Chromecast + UPnP, replaygain,
│ │ # crossfade, queue persistence
│ └── fin-tui/ # Ratatui neon TUI
└── Cargo.toml # workspace + shared deps (rustls only, no openssl)
License#
fin is released under the MPL-2.0.