a Jellyfin & Subsonic client for the terminal — powered by mpv, Chromecast and UPnP MediaRenderer
mpv
chromecast
mpris
navidrome
jellyfin
upnp
tui
1{
2 description = "fin - a neon-electric Jellyfin TUI client for mpv & Chromecast";
3
4 inputs = {
5 nixpkgs.url = "github:NixOS/nixpkgs/nixos-unstable";
6
7 # Current crane doesn't expose a `nixpkgs` input, so we don't follow it.
8 crane.url = "github:ipetkov/crane";
9
10 fenix = {
11 url = "github:nix-community/fenix";
12 inputs.nixpkgs.follows = "nixpkgs";
13 inputs.rust-analyzer-src.follows = "";
14 };
15
16 flake-utils.url = "github:numtide/flake-utils";
17
18 advisory-db = {
19 url = "github:rustsec/advisory-db";
20 flake = false;
21 };
22 };
23
24 outputs = { self, nixpkgs, crane, fenix, flake-utils, advisory-db, ... }:
25 flake-utils.lib.eachDefaultSystem (system:
26 let
27 pkgs = import nixpkgs {
28 inherit system;
29 };
30
31 inherit (pkgs) lib;
32
33 craneLib = crane.mkLib pkgs;
34
35 src = craneLib.cleanCargoSource ./.;
36
37 # fin uses rustls end-to-end, so no openssl.
38 # mpv is a *runtime* requirement (we spawn it for local playback and
39 # the CLI does a preflight `mpv --version` at startup). We fold it
40 # into PATH via a makeWrapper post-fixup below.
41 commonArgs = {
42 inherit src;
43
44 pname = "fin";
45 version = "0.6.0";
46 strictDeps = true;
47
48 # rockbox-playback pulls in rockbox-codecs + rockbox-dsp, whose
49 # build scripts compile Rockbox's C codec/DSP sources with the `cc`
50 # crate — so a C compiler must be on PATH. `stdenv.cc` is the
51 # toolchain for this platform (clang on Darwin, gcc on Linux).
52 # TLS is pure-Rust (rustls), so still no openssl.
53 # Modern nixpkgs (post-25.05) auto-links the Darwin SDK, so no
54 # framework references here — `darwin.apple_sdk_11_0` was removed
55 # as a legacy compatibility stub.
56 nativeBuildInputs = [
57 pkgs.pkg-config
58 pkgs.stdenv.cc
59 ] ++ lib.optionals pkgs.stdenv.isDarwin [
60 # coreaudio-sys generates its CoreAudio bindings with bindgen at
61 # build time; bindgenHook provides libclang (LIBCLANG_PATH) and
62 # points clang at the Nix Apple SDK headers.
63 pkgs.rustPlatform.bindgenHook
64 ];
65
66 buildInputs = lib.optionals pkgs.stdenv.isDarwin [
67 pkgs.libiconv
68 ] ++ lib.optionals pkgs.stdenv.isLinux [
69 # cpal links against ALSA on Linux for the local audio output path.
70 # MPRIS needs no dbus dev lib: zbus is a pure-Rust D-Bus
71 # implementation that speaks to the session bus socket directly.
72 pkgs.alsa-lib
73 ];
74
75 # Workspace has one bin target — build just that.
76 cargoExtraArgs = "--locked --bin fin";
77 };
78
79 craneLibLLvmTools = craneLib.overrideToolchain
80 (fenix.packages.${system}.complete.withComponents [
81 "cargo"
82 "llvm-tools"
83 "rustc"
84 ]);
85
86 # Cache the dependency graph separately from the crate source.
87 cargoArtifacts = craneLib.buildDepsOnly commonArgs;
88
89 finUnwrapped = craneLib.buildPackage (commonArgs // {
90 inherit cargoArtifacts;
91 doCheck = false;
92 });
93
94 # Wrap the binary so `mpv` is always discoverable at runtime, even
95 # when installed via `nix profile install`.
96 fin = pkgs.symlinkJoin {
97 name = "fin-${finUnwrapped.version}";
98 paths = [ finUnwrapped ];
99 nativeBuildInputs = [ pkgs.makeWrapper ];
100 postBuild = ''
101 wrapProgram $out/bin/fin \
102 --prefix PATH : ${lib.makeBinPath [ pkgs.mpv ]}
103 '';
104 meta = {
105 description = "A neon-electric Jellyfin TUI client for mpv & Chromecast";
106 homepage = "https://github.com/tsirysndr/fin";
107 license = lib.licenses.mpl20;
108 mainProgram = "fin";
109 platforms = lib.platforms.unix;
110 };
111 };
112
113 in
114 {
115 checks = {
116 inherit fin;
117
118 fin-clippy = craneLib.cargoClippy (commonArgs // {
119 inherit cargoArtifacts;
120 cargoClippyExtraArgs = "--all-targets -- --deny warnings";
121 });
122
123 fin-doc = craneLib.cargoDoc (commonArgs // {
124 inherit cargoArtifacts;
125 });
126
127 fin-fmt = craneLib.cargoFmt {
128 inherit src;
129 };
130
131 fin-audit = craneLib.cargoAudit {
132 inherit src advisory-db;
133 };
134
135 fin-nextest = craneLib.cargoNextest (commonArgs // {
136 inherit cargoArtifacts;
137 partitions = 1;
138 partitionType = "count";
139 });
140 } // lib.optionalAttrs (system == "x86_64-linux") {
141 fin-coverage = craneLib.cargoTarpaulin (commonArgs // {
142 inherit cargoArtifacts;
143 });
144 };
145
146 packages = {
147 default = fin;
148 fin = fin;
149 fin-unwrapped = finUnwrapped;
150
151 fin-llvm-coverage = craneLibLLvmTools.cargoLlvmCov (commonArgs // {
152 inherit cargoArtifacts;
153 });
154 };
155
156 apps.default = flake-utils.lib.mkApp {
157 drv = fin;
158 name = "fin";
159 };
160
161 devShells.default = pkgs.mkShell {
162 inputsFrom = builtins.attrValues self.checks.${system};
163
164 # Build-time tools. pkg-config is required so cpal's build.rs can
165 # resolve libasound on Linux; stdenv.cc supplies the C compiler the
166 # rockbox-codecs / rockbox-dsp build scripts need to compile
167 # Rockbox's C sources.
168 nativeBuildInputs = with pkgs; [
169 cargo
170 rustc
171 rustfmt
172 clippy
173 rust-analyzer
174 mpv
175 pkg-config
176 stdenv.cc
177 ];
178
179 # Link-time libraries. Position matters: pkg-config only picks up
180 # `.pc` files from `buildInputs`, so alsa-lib MUST live here (not
181 # in nativeBuildInputs) for the cpal → ALSA link to resolve.
182 buildInputs = with pkgs; lib.optionals stdenv.isDarwin [
183 libiconv
184 ] ++ lib.optionals stdenv.isLinux [
185 alsa-lib
186 ];
187
188 shellHook = ''
189 echo "⚡ fin dev shell — mpv $(mpv --version | head -n1 | cut -d' ' -f2) ready"
190 '';
191 };
192 });
193}