Commits
pkg-config's setup hook only scans `buildInputs` for `.pc` files, so
alsa-lib landing in `nativeBuildInputs` made cpal's ALSA link resolve
inconsistently in the dev shell — the cpal build script could run
pkg-config but wouldn't find libasound.pc.
Split the devShell into a proper nativeBuildInputs (tools including
pkg-config) and buildInputs (link-time libs: libiconv on Darwin,
alsa-lib on Linux). crane's commonArgs was already correct; this only
touches the devShell.
`current_list()` returns `BaseItem`s from the Jellyfin library and is
empty for the Devices (Chromecast/UPnP) and Settings (saved servers)
screens, which render their own row types. The Up/Down/PageDown
handlers were sizing against that empty list, so the cursor never
moved.
Added a screen-aware `list_len()` helper that reads the real backing
collection for Devices and Settings, and routed the nav handlers
through it.
Previously the decoder pushed samples at the source rate (e.g. 44.1 kHz
FLAC) into a cpal stream we asked to run at that same rate — but on
macOS and some Linux setups cpal silently keeps the OS device at its
own default (48 kHz here), so playback drifted (slow or fast depending
on the direction).
Fix:
- Open cpal at `device.default_output_config()` — the only spec every
backend is guaranteed to honor.
- Decode the first packet before opening cpal so we take sample rate
and channel count from the actual AudioBufferRef spec, not from the
codec_params metadata (which lies for HE-AAC / some Ogg streams).
- Route every packet through a stateful linear-interpolation resampler
+ channel converter, so the ring buffer holds samples already at the
device's output spec.
- Simplify the cpal callback to a plain copy from the ring.
- Track "produced_output_frames" instead of source frames so playhead
position matches wall-clock exactly.
Verified locally: 44.1 kHz FLAC on a 48 kHz macOS device now plays at
real-time speed (86 s wall-clock = 85 s playhead over a two-minute run).
cpal links against ALSA on Linux for the new symphonia audio path.
- Release workflow installs libasound2-dev before the Linux build.
- .deb Depends and .rpm Requires pull in the runtime library.
- flake.nix adds alsa-lib to both the crane buildInputs and the
devShell on Linux.
Local playback used to send everything through mpv. Audio now decodes
in-process via symphonia and outputs through cpal; mpv is spawned only
when a video item plays. Chromecast and UPnP still handle both.
- New SymphoniaPlayer: streaming HTTP media source, symphonia probe +
decode, SPSC ring buffer feeding a cpal output stream.
- New LocalRenderer: dispatches per QueueItem — audio→symphonia,
video→mpv — behind the existing Renderer trait.
- Preflight no longer aborts when mpv is missing; audio still works,
video prints an actionable hint.
- Player bar and help hints relabelled "mpv" → "local".
The status bar shows a keybinding cheat sheet whenever `status_message`
is `None`, but load results like `♪ 118 album(s)` were set on screen
load and never cleared, so the help was only visible for a blink before
being overwritten.
Stamp each status message with an `Instant` and treat entries older than
4s as `None` at draw time. The 200ms tick loop guarantees the redraw
that flips them back to the help text.
Discover MediaRenderers via SSDP and drive them with AVTransport +
RenderingControl SOAP actions, alongside the existing Chromecast path.
- linux-aarch64 now builds on ubuntu-24.04-arm (GitHub's native ARM
runner), so we drop the cross toolchain and the QEMU-flavored build.
Faster, and matches production glibc more closely.
- Both macos targets run on macos-latest. macos-latest is now
Apple-silicon; the x86_64-apple-darwin build uses rustup's cross
target (host aarch64 → x86_64) which works out of the box.
- Drop the `use_cross` matrix flag and the split cargo/cross build
steps — there's just one `cargo build` now.
Passing Limit=<omitted> to /Users/{id}/Items still let Jellyfin apply its
own silent cap on some deployments, so Music / Videos / Playlists / album
tracks / series episodes were being truncated at whatever that cap was.
Now items() paginates internally with Limit=500 + StartIndex, driven by
the server-reported TotalRecordCount. It keeps calling until either:
* a partial page comes back (we've hit the tail), or
* the collected count equals TotalRecordCount.
Same treatment for /Playlists/{id}/Items so long playlists load fully.
Also bump the http timeout to 120s so a very large single page can't
time out mid-response.
- Drop `darwin.apple_sdk.frameworks.SystemConfiguration` and `.Security`
references. `darwin.apple_sdk_11_0` was removed from nixpkgs as a
legacy compatibility stub — the current stdenv auto-links the SDK.
- Stop overriding crane's `nixpkgs` input (crane no longer exposes one),
which silences the `input 'crane' has an override for a non-existent
input 'nixpkgs'` warning.
- Regenerate flake.lock.
Chromecast:
- Content-Type follows the URL that stream_url() built — new
content_type_for_url() sniffs the extension (m3u8, mp4, mp3, flac,
opus, mkv, webm, …). Fixes the "metadata shows then stops" pattern
where we were labeling .m3u8 URLs as video/mp4 and the receiver
refused to play them.
- Queue advance fixed: get_status() is now called without a media
session id (None). Chromecast invalidates the previous session id
the instant playback ends, so polling with Some(id) returned an
empty list and we never saw the FINISHED signal. Empty entries
after having been Playing is now treated as end-of-track too.
- Idle-with-error is also treated as end-of-track so a single bad
stream can't hang the whole queue.
Streams:
- No more forced mp3/mp4 URLs. BaseItem now carries `container`
(fetched via Fields=Container,MediaSources), and stream_url(Direct)
uses it: FLAC stays .flac, MKV stays .mkv, Opus stays .opus.
Static=true keeps Jellyfin from unnecessarily transcoding.
- Both mpv and Chromecast now default to Direct; Chromecast decodes
H.264/MP3/AAC/FLAC/Opus/Vorbis/WebM/WAV natively so this is faster
and more reliable than the old HLS pipeline. `--hls` still forces
transcoding for edge codecs.
Release / packaging:
- .github/workflows/release.yml — cross-arch matrix for
linux amd64/aarch64 and darwin amd64/aarch64. Builds tarballs
(with checksums), .deb for both Linux arches, and .rpm for x86_64.
Publishes a GitHub release on `v*` tags and pushes .deb/.rpm to
Gemfury when FURY_TOKEN + FURY_ACCOUNT secrets are set.
- dist/debian/{amd64,arm64}/DEBIAN/control and dist/rpm/amd64/fin.spec
declare `mpv` as a runtime dependency so `apt install fin` /
`dnf install fin` pull it in automatically.
Docs:
- README: brew tap, apt/dnf via GitHub release AND via Gemfury repo,
Arch source install, prebuilt tarballs.
- Preview screenshot reference at .github/assets/preview.png.
TUI:
- Drop Home tab; default screen is Music. New order: Music, Videos,
Playlists, Queue, Search, Devices, Settings.
- Drill-in on Enter for MusicAlbum, Series, and Playlist; `x` plays
the whole container without drilling in. Esc pops the drill-in.
- Lists fetch every item (no `Limit`) — nothing gets truncated.
- Every row shares a fixed column layout (icon / title / subtitle /
right-aligned time) so columns line up across rows.
- Reactive search: immediate "searching…" status, 40ms debounce,
blinking cursor. Status bar highlights errors red, other messages
in primary teal.
Jellyfin API:
- Tolerant JSON parser normalizes response keys to PascalCase, so
Jellyfin 10.10 camelCase responses work alongside 10.8 PascalCase.
- Fallback from /Search/Hints to /Users/{id}/Items when hints return
no parsable results.
- Query params switched to camelCase; explicit includeMedia=true.
- 5s connect / 15s request timeouts.
Chromecast:
- Install rustls aws-lc-rs CryptoProvider at startup — rust_cast 0.20
ships rustls without a default provider and panicked on the first
TLS handshake ("Could not automatically determine the process-level
CryptoProvider").
Config / logging:
- Silence mdns_sd's cosmetic ERROR on ServiceDaemon shutdown at
default verbosity (-v bumps to warn).
pkg-config's setup hook only scans `buildInputs` for `.pc` files, so
alsa-lib landing in `nativeBuildInputs` made cpal's ALSA link resolve
inconsistently in the dev shell — the cpal build script could run
pkg-config but wouldn't find libasound.pc.
Split the devShell into a proper nativeBuildInputs (tools including
pkg-config) and buildInputs (link-time libs: libiconv on Darwin,
alsa-lib on Linux). crane's commonArgs was already correct; this only
touches the devShell.
`current_list()` returns `BaseItem`s from the Jellyfin library and is
empty for the Devices (Chromecast/UPnP) and Settings (saved servers)
screens, which render their own row types. The Up/Down/PageDown
handlers were sizing against that empty list, so the cursor never
moved.
Added a screen-aware `list_len()` helper that reads the real backing
collection for Devices and Settings, and routed the nav handlers
through it.
Previously the decoder pushed samples at the source rate (e.g. 44.1 kHz
FLAC) into a cpal stream we asked to run at that same rate — but on
macOS and some Linux setups cpal silently keeps the OS device at its
own default (48 kHz here), so playback drifted (slow or fast depending
on the direction).
Fix:
- Open cpal at `device.default_output_config()` — the only spec every
backend is guaranteed to honor.
- Decode the first packet before opening cpal so we take sample rate
and channel count from the actual AudioBufferRef spec, not from the
codec_params metadata (which lies for HE-AAC / some Ogg streams).
- Route every packet through a stateful linear-interpolation resampler
+ channel converter, so the ring buffer holds samples already at the
device's output spec.
- Simplify the cpal callback to a plain copy from the ring.
- Track "produced_output_frames" instead of source frames so playhead
position matches wall-clock exactly.
Verified locally: 44.1 kHz FLAC on a 48 kHz macOS device now plays at
real-time speed (86 s wall-clock = 85 s playhead over a two-minute run).
Local playback used to send everything through mpv. Audio now decodes
in-process via symphonia and outputs through cpal; mpv is spawned only
when a video item plays. Chromecast and UPnP still handle both.
- New SymphoniaPlayer: streaming HTTP media source, symphonia probe +
decode, SPSC ring buffer feeding a cpal output stream.
- New LocalRenderer: dispatches per QueueItem — audio→symphonia,
video→mpv — behind the existing Renderer trait.
- Preflight no longer aborts when mpv is missing; audio still works,
video prints an actionable hint.
- Player bar and help hints relabelled "mpv" → "local".
The status bar shows a keybinding cheat sheet whenever `status_message`
is `None`, but load results like `♪ 118 album(s)` were set on screen
load and never cleared, so the help was only visible for a blink before
being overwritten.
Stamp each status message with an `Instant` and treat entries older than
4s as `None` at draw time. The 200ms tick loop guarantees the redraw
that flips them back to the help text.
- linux-aarch64 now builds on ubuntu-24.04-arm (GitHub's native ARM
runner), so we drop the cross toolchain and the QEMU-flavored build.
Faster, and matches production glibc more closely.
- Both macos targets run on macos-latest. macos-latest is now
Apple-silicon; the x86_64-apple-darwin build uses rustup's cross
target (host aarch64 → x86_64) which works out of the box.
- Drop the `use_cross` matrix flag and the split cargo/cross build
steps — there's just one `cargo build` now.
Passing Limit=<omitted> to /Users/{id}/Items still let Jellyfin apply its
own silent cap on some deployments, so Music / Videos / Playlists / album
tracks / series episodes were being truncated at whatever that cap was.
Now items() paginates internally with Limit=500 + StartIndex, driven by
the server-reported TotalRecordCount. It keeps calling until either:
* a partial page comes back (we've hit the tail), or
* the collected count equals TotalRecordCount.
Same treatment for /Playlists/{id}/Items so long playlists load fully.
Also bump the http timeout to 120s so a very large single page can't
time out mid-response.
- Drop `darwin.apple_sdk.frameworks.SystemConfiguration` and `.Security`
references. `darwin.apple_sdk_11_0` was removed from nixpkgs as a
legacy compatibility stub — the current stdenv auto-links the SDK.
- Stop overriding crane's `nixpkgs` input (crane no longer exposes one),
which silences the `input 'crane' has an override for a non-existent
input 'nixpkgs'` warning.
- Regenerate flake.lock.
Chromecast:
- Content-Type follows the URL that stream_url() built — new
content_type_for_url() sniffs the extension (m3u8, mp4, mp3, flac,
opus, mkv, webm, …). Fixes the "metadata shows then stops" pattern
where we were labeling .m3u8 URLs as video/mp4 and the receiver
refused to play them.
- Queue advance fixed: get_status() is now called without a media
session id (None). Chromecast invalidates the previous session id
the instant playback ends, so polling with Some(id) returned an
empty list and we never saw the FINISHED signal. Empty entries
after having been Playing is now treated as end-of-track too.
- Idle-with-error is also treated as end-of-track so a single bad
stream can't hang the whole queue.
Streams:
- No more forced mp3/mp4 URLs. BaseItem now carries `container`
(fetched via Fields=Container,MediaSources), and stream_url(Direct)
uses it: FLAC stays .flac, MKV stays .mkv, Opus stays .opus.
Static=true keeps Jellyfin from unnecessarily transcoding.
- Both mpv and Chromecast now default to Direct; Chromecast decodes
H.264/MP3/AAC/FLAC/Opus/Vorbis/WebM/WAV natively so this is faster
and more reliable than the old HLS pipeline. `--hls` still forces
transcoding for edge codecs.
Release / packaging:
- .github/workflows/release.yml — cross-arch matrix for
linux amd64/aarch64 and darwin amd64/aarch64. Builds tarballs
(with checksums), .deb for both Linux arches, and .rpm for x86_64.
Publishes a GitHub release on `v*` tags and pushes .deb/.rpm to
Gemfury when FURY_TOKEN + FURY_ACCOUNT secrets are set.
- dist/debian/{amd64,arm64}/DEBIAN/control and dist/rpm/amd64/fin.spec
declare `mpv` as a runtime dependency so `apt install fin` /
`dnf install fin` pull it in automatically.
Docs:
- README: brew tap, apt/dnf via GitHub release AND via Gemfury repo,
Arch source install, prebuilt tarballs.
- Preview screenshot reference at .github/assets/preview.png.
TUI:
- Drop Home tab; default screen is Music. New order: Music, Videos,
Playlists, Queue, Search, Devices, Settings.
- Drill-in on Enter for MusicAlbum, Series, and Playlist; `x` plays
the whole container without drilling in. Esc pops the drill-in.
- Lists fetch every item (no `Limit`) — nothing gets truncated.
- Every row shares a fixed column layout (icon / title / subtitle /
right-aligned time) so columns line up across rows.
- Reactive search: immediate "searching…" status, 40ms debounce,
blinking cursor. Status bar highlights errors red, other messages
in primary teal.
Jellyfin API:
- Tolerant JSON parser normalizes response keys to PascalCase, so
Jellyfin 10.10 camelCase responses work alongside 10.8 PascalCase.
- Fallback from /Search/Hints to /Users/{id}/Items when hints return
no parsable results.
- Query params switched to camelCase; explicit includeMedia=true.
- 5s connect / 15s request timeouts.
Chromecast:
- Install rustls aws-lc-rs CryptoProvider at startup — rust_cast 0.20
ships rustls without a default provider and panicked on the first
TLS handshake ("Could not automatically determine the process-level
CryptoProvider").
Config / logging:
- Silence mdns_sd's cosmetic ERROR on ServiceDaemon shutdown at
default verbosity (-v bumps to warn).