a Jellyfin & Subsonic client for the terminal — powered by mpv, Chromecast and UPnP MediaRenderer
mpv chromecast mpris navidrome jellyfin upnp tui
0

Configure Feed

Select the types of activity you want to include in your feed.

fin / README.md
34 kB 731 lines
1# fin 2 3[![FlakeHub](https://img.shields.io/endpoint?url=https://flakehub.com/f/tsirysndr/fin/badge)](https://flakehub.com/flake/tsirysndr/fin) 4[![Release](https://github.com/tsirysndr/fin/actions/workflows/release.yml/badge.svg)](https://github.com/tsirysndr/fin/actions/workflows/release.yml) 5 6> a Jellyfin & Subsonic client for the terminal — powered by `rockbox-playback`, `mpv`, Chromecast, and UPnP 7 8![fin — neon-electric Jellyfin/Subsonic TUI](.github/assets/preview.png) 9 10`fin` is a Rust TUI + one-shot CLI that talks to your **Jellyfin** or 11**Subsonic** server (Navidrome, Airsonic, Gonic, Astiga, … — the flavour is 12auto-detected at login), searches your library, manages playlists, and pushes 13streams to your local machine 14(**rockbox-playback** for audio, **mpv** for video), any **Chromecast** on your 15network, or any **UPnP MediaRenderer** (Sonos, Kodi, Roon endpoints, Samsung/LG 16TVs, gmediarender, …). Local playback is now audio-native — HTTP streaming, 17decoding, resampling, and output all run in-process, and audio never touches 18mpv. Remote playback is fully queued, with client-side auto-advance. 19 20## Contents 21 22- [Features](#features) 23- [Install](#install) 24 - [npm — `npx @tsiry/fin`](#npm--npx-tsiryfin) 25 - [macOS / Linux — Homebrew](#macos--linux--homebrew) 26 - [Debian / Ubuntu — `.deb`](#debian--ubuntu--deb) 27 - [Fedora / RHEL / openSUSE — `.rpm`](#fedora--rhel--opensuse--rpm) 28 - [Arch — from AUR / source](#arch--from-aur--source) 29 - [Prebuilt tarballs](#prebuilt-tarballs) 30 - [From source](#from-source) 31 - [Nix](#nix) 32- [Getting started](#getting-started) 33- [Renderer selection](#renderer-selection) 34- [UPnP MediaRenderer — casting *to* fin](#upnp-mediarenderer--casting-to-fin) 35- [MPRIS — desktop media controls](#mpris--desktop-media-controls) 36- [All settings](#all-settings) 37- [Multiple servers](#multiple-servers) 38- [Sub-commands](#sub-commands) 39- [Keybindings (TUI)](#keybindings-tui) 40- [Playback modes & effects](#playback-modes--effects) 41 - [Shuffle](#shuffle) 42 - [Repeat](#repeat) 43 - [ReplayGain](#replaygain) 44 - [Crossfade](#crossfade) 45 - [Equalizer](#equalizer) 46 - [Bass & treble](#bass--treble) 47- [Queue persistence](#queue-persistence) 48- [Remote-renderer queue](#remote-renderer-queue) 49- [Supported audio formats](#supported-audio-formats) 50- [Streams & transcoding](#streams--transcoding) 51- [Development](#development) 52- [License](#license) 53 54## Features 55 56- **Ratatui-based TUI** with a neon-electric palette (teal / cyan / violet). 57- **In-process audio** — HTTP streaming + `rockbox-playback` decode (MP3, 58 FLAC, AAC, Opus, Vorbis, ALAC, WAV, and [many more](#supported-audio-formats)) 59 + resampling + `cpal` output, using Rockbox's own codecs and DSP. mpv is 60 used only for video. 61- **fzf-style instant search** — results update on every keystroke. 62- **Drill-in navigation** — Enter on an album lists its tracks, Enter on a 63 series lists its episodes, Enter on a playlist lists its items. `x` 64 plays the whole container in one go, `Shift+X` does the same shuffled. 65- **No list truncation** — Music, Videos, and Playlists fetch every item 66 the server has, so nothing stays hidden past an arbitrary limit. 67- **Three renderers**, one interface: 68 - **local** (default) — rockbox-playback for audio, mpv for video, spawned 69 only when needed. 70 - **chromecast** — device discovery via mDNS, playback through the Default 71 Media Receiver, with a **local queue** that auto-advances on `FINISHED`. 72 - **upnp** — SSDP discovery of any UPnP AV MediaRenderer, playback via 73 AVTransport (`SetAVTransportURI` / `Play` / `Pause` / `Stop` / `Seek`) 74 and volume via RenderingControl. Same auto-advancing queue. 75- **Cast *to* fin** — while the TUI runs, fin advertises itself as a 76 **UPnP MediaRenderer** on the LAN. Push streams at it from BubbleUPnP, 77 Kodi, Jellyfin's "Play On", or any other control point: audio decodes 78 in-process via rockbox-playback, video opens in mpv, and the pushed track lands 79 in the Now Playing bar with a `⇊ UPnP` badge. On by default; opt out with 80 `--no-media-renderer` or `media_renderer.enabled = false` 81 (see [UPnP MediaRenderer](#upnp-mediarenderer--casting-to-fin)). 82- **MPRIS** (Linux & the BSDs) — while the TUI runs, fin registers on the 83 D-Bus session bus as `org.mpris.MediaPlayer2.fin`, so media keys, 84 GNOME/KDE applets, waybar, and `playerctl` control playback — whichever 85 renderer is active, Chromecast and UPnP included 86 (see [MPRIS](#mpris--desktop-media-controls)). 87- **Playback modes** — shuffle, repeat-off/all/one, [ReplayGain](#replaygain) 88 (track / album), [crossfade](#crossfade) between adjacent tracks 89 (traditional cosine curves *or* additive DJ-mixed), and a 90 10-band [equalizer](#equalizer) powered by the Rockbox DSP pipeline. 91- **Queue persistence** — the audio queue, shuffle/repeat state, and the exact 92 playhead within the current track survive restarts. Restore lands paused; 93 `Space` picks up where you left off. 94- **Real queue management** — enqueue, play next, jump between tracks, remove 95 a single entry, clear the whole queue, and see the queue in its own tab 96 with a `▶` marker on the actually-playing track. 97- **Playlists** — browse, open, and play the playlists you've saved on the 98 server. 99- **Now Playing bar** with title, subtitle, elapsed / total time, a neon 100 progress gauge, volume, and mode badges (shuffle ⇄, repeat ↻/↺, 101 ReplayGain, crossfade ⋈/≈). 102- **CLI shortcuts** for scripting: `fin play "kind of blue"`, 103 `fin queue --chromecast "Living Room" "wednesday"`, 104 `fin play --upnp "Kitchen Speaker" "solaris"`, `fin devices`. 105- **All settings** are available as **CLI flags _or_ TOML keys** — one 106 workflow scales from ad-hoc invocation to per-machine config. 107- **Pure Rust TLS** (`rustls`) everywhere — no OpenSSL required. 108 109## Install 110 111Local audio needs no extra binaries — everything is baked into the `fin` 112binary. **`mpv`** is only needed on `$PATH` when you actually play video 113locally. Every install path below either bundles it or pulls it in as a 114soft dependency. 115 116### npm — `npx @tsiry/fin` 117 118```bash 119# Run without installing 120npx @tsiry/fin 121 122# Or install globally 123npm install -g @tsiry/fin 124``` 125 126Works on macOS / Linux / FreeBSD / NetBSD / OpenBSD (x64 and arm64; OpenBSD is 127x64 only). The prebuilt binary matching your platform is downloaded from the 128GitHub release and SHA-256 verified on install. Install **`mpv`** separately if 129you want local video playback. 130 131### macOS / Linux — Homebrew 132 133```bash 134brew install tsirysndr/tap/fin 135``` 136 137The formula pulls in `mpv` automatically. 138 139### Debian / Ubuntu — `.deb` 140 141Download the `.deb` for your architecture from the 142[latest release](https://github.com/tsirysndr/fin/releases/latest) and: 143 144```bash 145# amd64 146curl -LO https://github.com/tsirysndr/fin/releases/latest/download/fin_0.6.0_amd64.deb 147sudo apt install ./fin_0.6.0_amd64.deb 148 149# arm64 (Raspberry Pi 4/5, Apple-silicon VM, …) 150curl -LO https://github.com/tsirysndr/fin/releases/latest/download/fin_0.6.0_arm64.deb 151sudo apt install ./fin_0.6.0_arm64.deb 152``` 153 154`apt` will pull in `libasound2` (ALSA runtime for cpal) and `mpv` automatically. 155 156Or add the Gemfury apt repo once and `apt install` normally: 157 158```bash 159echo "deb [trusted=yes] https://apt.fury.io/tsiry/ /" \ 160 | sudo tee /etc/apt/sources.list.d/tsiry.list 161sudo apt update && sudo apt install fin 162``` 163 164### Fedora / RHEL / openSUSE — `.rpm` 165 166```bash 167sudo dnf install \ 168 https://github.com/tsirysndr/fin/releases/latest/download/fin-0.6.0-1.x86_64.rpm 169``` 170 171Or via the Gemfury yum repo: 172 173```bash 174sudo tee /etc/yum.repos.d/tsiry.repo <<'EOF' 175[tsiry] 176name=tsiry 177baseurl=https://yum.fury.io/tsiry/ 178enabled=1 179gpgcheck=0 180EOF 181sudo dnf install fin 182``` 183 184### Arch — from AUR / source 185 186`mpv` from the official repos, then: 187 188```bash 189sudo pacman -S mpv alsa-lib 190cargo install --git https://github.com/tsirysndr/fin --bin fin 191``` 192 193### Prebuilt tarballs 194 195For any other platform, grab the tarball for your arch from the 196[releases page](https://github.com/tsirysndr/fin/releases/latest): 197 198- `fin-<version>-linux-amd64.tar.gz` 199- `fin-<version>-linux-aarch64.tar.gz` 200- `fin-<version>-macos-amd64.tar.gz` 201- `fin-<version>-macos-aarch64.tar.gz` 202 203Each includes the `fin` binary + README + LICENSE. Install runtime deps 204yourself: 205 206```bash 207# macOS 208brew install mpv # video only; audio is in-process 209# Debian / Ubuntu 210sudo apt install libasound2 mpv 211# Arch 212sudo pacman -S alsa-lib mpv 213``` 214 215### From source 216 217```bash 218git clone https://github.com/tsirysndr/fin 219cd fin 220cargo install --path crates/fin 221``` 222 223Build-time on Linux needs `libasound2-dev` + `pkg-config` (cpal's ALSA 224backend); on macOS the Core Audio SDK is already in the toolchain. 225 226### Nix 227 228A flake is provided — mpv is baked into the wrapper, so no extra install 229step is needed: 230 231```bash 232# Use Cache 233cachix use tsirysndr 234 235# One-off run: 236nix run github:tsirysndr/fin 237 238# Install into your user profile: 239nix profile install github:tsirysndr/fin 240 241# Dev shell (rust toolchain + mpv + alsa-lib + clippy + rust-analyzer): 242nix develop 243``` 244 245## Getting started 246 247```bash 248# 1. Sign in 249fin login https://media.example.com 250 251# 2. Launch the TUI (default sub-command) 252fin 253 254# 3. Or drive it entirely from the shell 255fin search "daft punk" 256fin play "kind of blue" 257fin queue "wednesday season 1" 258fin devices # list Chromecasts + UPnP renderers on your LAN 259fin play --chromecast "Living Room" "solaris" 260fin play --upnp "Kitchen" "solaris" 261``` 262 263## Renderer selection 264 265Three ways to choose a renderer — all equivalent: 266 267| Shortcut flag | Long flag | Config key | 268|----------------------------------------|---------------------------|--------------------------------| 269| `--mpv` | `--renderer mpv` | `renderer = "mpv"` | 270| `--chromecast "Living Room"` | `--renderer chromecast` | `renderer = "chromecast"` | 271| `--upnp "Kitchen Speaker"` | `--renderer upnp` | `renderer = "upnp"` | 272| _(none — falls back to local)_ | | | 273 274The `--mpv` / `renderer = "mpv"` flag name is historical; it selects the 275**local** renderer, which uses rockbox-playback for audio and mpv for video. 276 277When you pass `--chromecast NAME` or `--upnp NAME`, the renderer is switched 278to that protocol automatically and the named device is preferred on connect. 279If the name is not found on the network, fin picks the first device discovered. 280 281## UPnP MediaRenderer — casting *to* fin 282 283The renderer table above is about fin *sending* streams elsewhere. This is 284the opposite direction: while the TUI runs, fin also shows up on the LAN as 285a **UPnP AV MediaRenderer** device, so any control point — BubbleUPnP, 286Kodi, Jellyfin's "Play On", gupnp tools, another fin casting with 287`--upnp`, … — can push media at this machine. 288 289Incoming streams take the same local path as everything else: **audio is 290decoded in-process by rockbox-playback** (never mpv), **video is handed to mpv**. 291The pushed track appears in the Now Playing bar with a violet `⇊ UPnP` 292badge and a one-shot status line (`⇊ receiving UPnP cast — <title>`), joins 293the queue like any other item, and answers the normal transport keys — 294`Space` pauses it, `s` stops it, `+`/`-` change volume. The control point 295sees those changes reflected back through AVTransport/RenderingControl 296state and GENA `LastChange` events. 297 298It's **on by default**. Three equivalent ways to turn it off: 299 300```bash 301fin --no-media-renderer # this run only 302FIN_NO_MEDIA_RENDERER=1 fin # environment 303``` 304 305```toml 306# config.toml — persistent 307[media_renderer] 308enabled = false 309``` 310 311Optional keys under the same table: 312 313```toml 314[media_renderer] 315enabled = true 316friendly_name = "office fin" # picker name; default: fin (<hostname>) 317port = 47899 # description/control port; 0 = ephemeral 318# uuid is generated on first launch and persisted so control points 319# recognize the device across restarts — no need to set it by hand. 320``` 321 322Playback pushed by a control point is **not scrobbled** — the item ids are 323foreign to your media server, so session reports/scrobbles are skipped for 324cast-in tracks. 325 326## MPRIS — desktop media controls 327 328On **Linux and the BSDs**, fin registers itself on the D-Bus session bus as 329`org.mpris.MediaPlayer2.fin` while the TUI runs. That's the standard 330desktop media-player interface, so it works with whatever already speaks 331MPRIS: 332 333- **hardware media keys** (play/pause/next/previous) — even when the 334 terminal isn't focused, 335- **desktop applets** — GNOME's calendar-dropdown media widget, KDE's 336 Media Player plasmoid and lock-screen controls, 337- **bars** — waybar's `mpris` module and friends, 338- **`playerctl`** for scripting: 339 340```bash 341playerctl -p fin play-pause 342playerctl -p fin next 343playerctl -p fin metadata # title, artist, duration, cover art 344playerctl -p fin position 30 # absolute seek (seconds) 345playerctl -p fin volume 0.5 346playerctl -p fin shuffle On 347playerctl -p fin loop Playlist # = repeat all 348``` 349 350MPRIS drives whatever renderer is currently active — switch to a 351Chromecast or UPnP device in the TUI and your media keys transparently 352control the cast session. Track metadata (title, artist, duration, cover 353art) is pushed to the desktop as it changes. 354 355There's nothing to configure and no system dependency to install (the 356D-Bus protocol is spoken natively — no `libdbus`). On a headless box with 357no session bus, fin simply logs a warning and carries on. A second fin 358instance registers as `org.mpris.MediaPlayer2.fin.instance<pid>`, per the 359MPRIS spec. 360 361## All settings 362 363Every setting exists as both a CLI flag and a TOML key. Flags win. 364 365| CLI flag | Env var | TOML key | Default | 366|-----------------------|---------------------|---------------------------|------------------| 367| `--server URL` | `FIN_SERVER` | `servers[].url` | _(none)_ | 368| `--server-name NAME` | `FIN_SERVER_NAME` | `current_server` | _(latest login)_ | 369| `--token TOKEN` | `FIN_TOKEN` | `servers[].access_token` | _(from login)_ | 370| `--user-id ID` | `FIN_USER_ID` | `servers[].user_id` | _(from login)_ | 371| `--user-name NAME` | | `servers[].user_name` | _(from login)_ | 372| `--device-id ID` | `FIN_DEVICE_ID` | `servers[].device_id` | random UUID | 373| `--renderer <mpv/chromecast/upnp>` | `FIN_RENDERER` | `renderer` | `mpv` | 374| `--mpv` | | `renderer = "mpv"` | | 375| `--chromecast [NAME]` | `FIN_CHROMECAST` | `last_chromecast` | | 376| `--upnp [NAME]` | `FIN_UPNP` | `last_upnp` | | 377| `--no-media-renderer` | `FIN_NO_MEDIA_RENDERER` | `media_renderer.enabled` | `true` | 378| `--media-renderer` | | `media_renderer.enabled = true` | | 379| | | `media_renderer.friendly_name` | `fin (<hostname>)` | 380| | | `media_renderer.port` | `0` _(ephemeral)_ | 381| | | `media_renderer.uuid` | _(generated once)_ | 382| `-v`, `-vv` | | _(log level)_ | `warn` | 383 384Audio-side playback settings live under TOML sub-tables and are toggled from 385the TUI (see [Playback modes & effects](#playback-modes--effects)): 386 387| TOML | Default | Notes | 388|-----------------------------------|----------------|----------------------------------------------------------| 389| `replaygain.mode` | `off` | `off` / `track` / `album` | 390| `replaygain.preamp_db` | `0.0` | additive in dB before clip guard | 391| `replaygain.prevent_clip` | `true` | caps gain so `linear * peak <= 1.0` | 392| `crossfade.mode` | `off` | `off` / `crossfade` / `mixed` | 393| `crossfade.duration_secs` | `5.0` | overlap window in seconds | 394| `eq_enabled` | `false` | toggle the Rockbox 10-band EQ pipeline | 395| `[[eq_band_settings]]` | ISO octave | 10 bands (see [Equalizer](#equalizer)); Rockbox-compatible | 396| `bass` | `0` | bass shelf gain in whole dB (−24…+24) | 397| `treble` | `0` | treble shelf gain in whole dB (−24…+24) | 398| `bass_cutoff` | `0` | bass shelf cutoff in Hz (`0` = Rockbox default 200) | 399| `treble_cutoff` | `0` | treble shelf cutoff in Hz (`0` = Rockbox default 3500) | 400 401Find the on-disk config with `fin config --path`; print it with 402`fin config --show`. 403 404While the TUI runs, logs are written to `fin.log` in the cache directory 405(next to `queue.json`) rather than stderr, so they can't corrupt the 406display; one-shot CLI commands log to stderr as usual. `-v` / `-vv` and 407`RUST_LOG` control the level either way. 408 409## Multiple servers 410 411fin authenticates against as many servers as you like — Jellyfin and 412Subsonic mixed freely — and keeps their credentials side-by-side in one 413config file: 414 415```bash 416fin login https://home.example.com --name home 417fin login https://work.example.com --name work 418fin login https://mom.dyndns.example --name mom 419 420fin server # list all servers (▍ marks the current one) 421fin server switch work # make `work` the active server 422fin server rm mom # remove one 423fin server rename home casa # rename `home` → `casa` 424 425# One-off — hit `work` without changing the current pointer: 426fin --server-name work search "spirited away" 427fin --server-name work play "spirited away" 428``` 429 430Inside the TUI, the Settings screen shows every saved server; **Enter** on 431one switches to it. **`t`** anywhere in the TUI cycles to the next server 432without leaving the current screen. 433 434## Sub-commands 435 436``` 437fin # launch the TUI (default) 438fin login <url> [--name N] # sign in and save credentials for server `N` 439fin logout [--name N] # remove server `N` (defaults to the current one) 440fin server # list saved servers 441fin server switch <name> # change the active server 442fin server rm <name> # remove one 443fin server rename <a> <b> # rename 444fin search <query> # print matches from the active library 445fin play <query> # search + play the top hit 446fin queue <query> # search + append to the current queue 447fin devices # list Chromecasts + UPnP MediaRenderers on the local network 448fin playlists # list playlists 449fin playlists --list <id> # dump items of a playlist 450fin config --show|--path # inspect config 451``` 452 453## Keybindings (TUI) 454 455Tab order — the default screen is **Music**: 456 457 `1` Music • `2` Videos • `3` Playlists • `4` Favorites • `5` Queue • `6` Search • `7` Devices • `8` Settings 458 459The **Videos** tab is Jellyfin-only — Subsonic has no video API, so on a 460Subsonic server the tab is hidden, `Tab`/`Shift+Tab` skip past it, and `2` 461shows a hint instead. The other number keys keep their meanings. 462 463| Key | Action | 464|------------------------------|-------------------------------------| 465| `?` | show / hide the full keyboard-shortcuts help modal | 466| `Tab` / `Shift+Tab` | next / prev screen | 467| `1``8` | jump to Music / Videos / Playlists / Favorites / Queue / Search / Devices / Settings | 468| `/` | jump to Search & focus input | 469| `↑` `↓` / `k` `j` | move selection | 470| `PgUp` / `PgDown` | jump 10 rows | 471| `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 | 472| `x` | play the highlighted container as one queue **without** drilling in (album → all tracks, playlist → all items) | 473| `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 | 474| `a` | enqueue the highlighted item | 475| `n` | play the highlighted item **next** | 476| `Shift+L` / `Shift+D` | **like / dislike** — add or remove the highlighted item (or the playing track) from Favorites; Jellyfin favorites, Subsonic stars | 477| `z` | toggle shuffle | 478| `Shift+R` | cycle repeat mode (off → all → one) | 479| `g` | cycle ReplayGain (off → track → album) | 480| `f` / `Shift+F` | cycle crossfade mode / cycle crossfade duration (3, 5, 8, 12 s) | 481| `Shift+E` | toggle 10-band Rockbox EQ | 482| `[` / `]` | (Settings) select previous / next EQ band | 483| `Shift+↑` / `Shift+↓` | (Settings) nudge the selected EQ band's gain by ±1 dB | 484| `b` / `Shift+B` | bass shelf −1 dB / +1 dB | 485| `y` / `Shift+Y` | treble shelf −1 dB / +1 dB | 486| `Space` or `p` | pause / resume | 487| `s` | stop | 488| `<` / `>` or `h` / `l` | previous / next track | 489| `+` / `-` | volume up / down | 490| `m` | switch to local renderer | 491| `t` | cycle to the next saved server | 492| `d` | (Queue screen) remove the highlighted entry | 493| `Shift+C` | (Queue screen) clear the entire queue | 494| `Esc` | pop the current drill-in (back to the parent list) | 495| `r` | refresh the current screen | 496| `Esc` | leave the search input / close open playlist | 497| `q` / `Ctrl-C` | quit | 498 499## Playback modes & effects 500 501**Shuffle and repeat work on every renderer** — the queue lives on the 502client, so they apply while casting to Chromecast or UPnP too. The audio 503*effects* (ReplayGain, crossfade, EQ, bass & treble) run only on the local 504renderer; Chromecast and UPnP receivers do their own DSP, so those toggles 505no-op there. Settings persist to `config.toml` and are mirrored back on 506next launch. 507 508### Shuffle 509 510`z` toggles shuffle. Enabling it reshuffles every item **after** the 511currently-playing track using a Fisher-Yates permutation — the playing track 512stays put so the audio doesn't jump. 513 514### Repeat 515 516`Shift+R` cycles the repeat mode off → all → one → off. `all` wraps in both 517directions (Prev at row 0 goes to the last item); `one` sticks on the 518current track until the mode changes. 519 520### ReplayGain 521 522`g` cycles Off → Track → Album → Off. Reads `REPLAYGAIN_TRACK_GAIN`, 523`REPLAYGAIN_ALBUM_GAIN`, and the matching peak tags off decoded tracks 524(Vorbis-comment or ID3v2), computes a linear gain multiplier 525(`10^((gain + preamp) / 20)`), and folds it into the sample push loop. If 526the requested scope's tag is missing, the other scope is used as a fallback. 527Clip prevention is on by default — it caps the multiplier so peaks stay ≤ 1.0. 528 529### Crossfade 530 531`f` cycles Off → **Crossfade****Mixed** → Off. 532 533- **Crossfade** — cosine/sine curves (out² + in² = 1); perceived loudness 534 stays constant across the overlap. 535- **Mixed** — no curves; both tracks play at full volume during the overlap 536 and sum additively (louder DJ-style mix). 537 538`Shift+F` cycles the duration through 3, 5, 8, 12 s, preserving the current 539mode. Duration is also editable directly in `config.toml`. 540 541Under the hood, `fin` runs a second decoder + cpal output stream during the 542overlap and the OS mixer sums them. The overlap kicks in both on natural 543end-of-track transitions AND when you Play a new album or jump to a new 544queue entry — so switching tracks manually still fades cleanly. 545 546### Equalizer 547 548`fin` links the Rockbox DSP pipeline 549([`rockbox-dsp`](https://crates.io/crates/rockbox-dsp)) for a fixed-point 55010-band EQ with high-quality biquad filters — band 0 is a low shelf, band 9 551a high shelf, bands 1–8 are peaking filters. On the Settings screen you'll 552see 10 vertical sliders with `dB` labels above and cutoff frequency labels 553below. The controls: 554 555| Key | Action | 556|----------------|-----------------------------------------------| 557| `E` | toggle EQ on / off | 558| `[` / `]` | move the highlighted band left / right | 559| `Shift+↑` / `↓`| bump the highlighted band's gain by ±1 dB | 560 561Adjustments persist to `config.toml` immediately. Fresh installs get the 562ISO-octave flat preset (32 Hz, 63, 125, 250, 500, 1 kHz, 2, 4, 8, 16 kHz, 563Q 7.0, 0 dB across the board) so the DSP is a bit-exact bypass until you 564start tweaking. Values in `[[eq_band_settings]]` use Rockbox tenths — `q = 56570` means Q 7.0, `gain = -125` means −12.5 dB — so a Rockbox preset drops 566in unchanged. 567 568**License note:** enabling EQ links the Rockbox DSP C sources (GPL-2.0-or-later), 569which makes the resulting `fin` binary GPL. The rest of `fin` remains MPL-2.0 570in source form. 571 572Behavioral notes: 573 574- **Only local playback runs through EQ.** Chromecast and UPnP receivers each 575 do their own DSP; the toggle is a no-op there. 576- During a crossfade the outgoing *and* incoming tracks both route through 577 the same Rockbox DSP config. The biquad delay lines get briefly stirred 578 when the two tracks alternate through the pipeline — the audible transient 579 is well under a millisecond at 48 kHz. 580 581### Bass & treble 582 583The Rockbox tone-control stage runs in the same DSP pipeline as the EQ — 584shelving filters at fixed cutoffs (default 200 Hz bass, 3500 Hz treble). 585Adjust with `b` / `Shift+B` for bass and `y` / `Shift+Y` for treble; every 586press is a 1 dB step in the ±24 dB range and is persisted to `config.toml` 587immediately. The Settings screen shows the current values, and the player 588bar shows a compact `B+3/T-2` badge whenever either is non-zero. 589 590Custom shelf cutoffs can be set in `config.toml` via `bass_cutoff` / 591`treble_cutoff` (Hz); `0` means the Rockbox defaults. The keys and this 592stage go through the same singleton pipeline as EQ, so the licensing note 593above applies. 594 595## Queue persistence 596 597The audio queue, shuffle/repeat state, and the exact playhead within the 598currently-playing track are written to `cache_dir/queue.json` on every 599mutation and every ~3 s while playing. Writes are debounced and atomic 600(rename-in-place), so a crash mid-write can't leave a truncated file. 601 602On startup, `fin` reads the snapshot and restores the queue paused at the 603saved position. `Space` (or `p`) resumes from exactly where you left off. 604Video items in a saved queue are filtered out silently — the persistence 605path lives on the audio side; the mpv-driven video path is transient. 606 607Find the file with `fin config --path` (adjacent to the config dir). 608 609## Remote-renderer queue 610 611For both Chromecast and UPnP, `fin` maintains the queue **on the client**, 612polls the device for its current transport state, and loads the next item 613automatically the moment the current one finishes (Chromecast: 614`IDLE / FINISHED`; UPnP: `STOPPED` after having been `PLAYING`). That means: 615 616- `a` (queue) and `n` (play-next) do the right thing while something is 617 already streaming. 618- Skipping (`>` / `<`) triggers a `load` for the next queue item 619 immediately — no waiting for the current one to finish. 620- Shuffle (`z`) and repeat (`Shift+R`) act on this client-side queue, so 621 they work while casting exactly like local playback. 622- Stopping clears the local queue and stops the receiver's playback. 623 624UPnP renderers without a `RenderingControl` service (rare, but it happens) 625still work for transport — volume changes are just no-ops on the device. 626 627## Supported audio formats 628 629Local audio is decoded in-process by 630[`rockbox-playback`](https://crates.io/crates/rockbox-playback), which uses 631Rockbox's own firmware codecs — the full 29-codec static-link set, all 632enabled. Any of these plays gapless, with ReplayGain, crossfade and the EQ 633straight through the same path: 634 635### Lossless 636 637| Format | Typical extension | Notes | 638| -------------------- | ----------------- | ----------------------------- | 639| FLAC | `.flac` | | 640| Apple Lossless | `.m4a` | ALAC | 641| WavPack | `.wv` | | 642| Monkey's Audio | `.ape` | | 643| True Audio | `.tta` | | 644| Shorten | `.shn` | | 645 646### Lossy 647 648| Format | Typical extension | Notes | 649| -------------------- | ----------------- | ----------------------------- | 650| MP3 / MP2 / MP1 | `.mp3` `.mp2` | MPEG audio (incl. MP3-in-ASF) | 651| AAC / HE-AAC | `.m4a` `.aac` | incl. AAC+ / SBR | 652| Ogg Vorbis | `.ogg` | | 653| Opus | `.opus` | | 654| Musepack | `.mpc` | SV7 / SV8 | 655| Speex | `.spx` | | 656| WMA v1 / v2 | `.wma` | in ASF | 657| WMA Professional | `.wma` | | 658| AC3 (A/52) | `.ac3` | | 659| RealAudio Cook | `.ra` `.rm` | | 660| AAC in RealMedia | `.rm` | | 661| ATRAC3 | `.oma` `.aa3` | Sony OMA + RealMedia | 662 663### Uncompressed / PCM 664 665| Format | Typical extension | Notes | 666| -------------------- | ----------------- | ----------------------------- | 667| WAV | `.wav` | PCM + the ADPCM family | 668| AIFF | `.aiff` `.aif` | | 669| Sun AU / SND | `.au` `.snd` | | 670| Sony Wave64 | `.w64` | | 671| SMAF | `.mmf` | Yamaha mobile | 672| Dialogic VOX | `.vox` | ADPCM | 673 674### Specialized / game audio 675 676| Format | Typical extension | Notes | 677| -------------------- | ----------------- | ----------------------------- | 678| CRI ADX | `.adx` | | 679| Amiga MOD | `.mod` | | 680 681> Chiptune emulator formats (SPC, SID, NSF/GBS/HES/KSS/AY/SGC/VGM/VTX, SAP) 682> are **not** supported — they need Rockbox's one-codec-at-a-time model and 683> are excluded from the static-link set fin builds against. 684 685## Streams & transcoding 686 687- Local **audio** uses the original stream — `rockbox-playback` decodes it 688 in-process with Rockbox's codecs (see 689 [Supported audio formats](#supported-audio-formats)), resampled to the 690 output device's rate. No transcoding round-trip; no mpv on the audio 691 path. 692- Local **video** shells out to **mpv** with `Static=true` — the fastest 693 direct-stream path, mpv handles any container Jellyfin can hand it. 694- **Chromecast** playback defaults to Jellyfin's HLS output (`main.m3u8`), 695 because the Default Media Receiver's codec matrix is much narrower than 696 mpv's. Jellyfin will transcode when it needs to. 697- **UPnP** playback uses the direct stream by default — most UPnP 698 MediaRenderers speak MP3 / AAC / FLAC natively and the direct path avoids 699 the transcode. Fall back to HLS with `--hls` if your renderer needs it. 700- Force one or the other from the CLI with `--hls` on `play`/`queue`. 701 702## Development 703 704```bash 705cargo check --workspace 706cargo build --release -p fin 707./target/release/fin --help 708cargo test --workspace 709``` 710 711The workspace layout: 712 713``` 714fin/ 715├── crates/ 716│ ├── fin/ # binary — clap CLI + startup 717│ ├── fin-config/ # TOML config file, credentials, mode enums 718│ ├── fin-jellyfin/ # Jellyfin HTTP API client 719│ ├── fin-subsonic/ # Subsonic HTTP API client (Navidrome, Airsonic, …) 720│ ├── fin-media/ # MediaClient trait over both backends + login probe 721│ ├── fin-mediarenderer/ # built-in UPnP MediaRenderer — casting *to* fin 722│ ├── fin-player/ # Renderer trait, queue, rockbox-playback audio path, 723│ │ # mpv video, Chromecast + UPnP, replaygain, 724│ │ # crossfade, queue persistence 725│ └── fin-tui/ # Ratatui neon TUI 726└── Cargo.toml # workspace + shared deps (rustls only, no openssl) 727``` 728 729## License 730 731`fin` is released under the [MPL-2.0](LICENSE).