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

Configure Feed

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

Hide Videos tab on Subsonic servers

Subsonic has no video API, so the Videos tab only exists on Jellyfin:
the tab bar drops it, Tab/Shift+Tab skip past it, and the `2` key shows
a hint instead. Switching servers while on Videos bounces to Music.

Tsiry Sandratraina (Jul 5, 2026, 10:40 PM +0300) 07bc6a5f 0f6941c8

+66 -12
+3
CHANGELOG.md
··· 16 16 queue later. 17 17 18 18 ### Changed 19 + - **Videos tab is now Jellyfin-only** — Subsonic has no video API, so on a 20 + Subsonic server the tab is hidden from the bar, `Tab`/`Shift+Tab` skip it, 21 + and `2` shows a hint. Switching servers while on Videos bounces to Music. 19 22 - **ReplayGain now runs in the Rockbox DSP** — upgraded `rockbox-dsp` to 20 23 0.2.0 and moved gain application into its pre-gain (PGA) stage, the same 21 24 fixed-point pipeline as the EQ and tone controls. Tag extraction stays in
+4
README.md
··· 330 330 331 331 `1` Music • `2` Videos • `3` Playlists • `4` Favorites • `5` Queue • `6` Search • `7` Devices • `8` Settings 332 332 333 + The **Videos** tab is Jellyfin-only — Subsonic has no video API, so on a 334 + Subsonic server the tab is hidden, `Tab`/`Shift+Tab` skip past it, and `2` 335 + shows a hint instead. The other number keys keep their meanings. 336 + 333 337 | Key | Action | 334 338 |------------------------------|-------------------------------------| 335 339 | `?` | show / hide the full keyboard-shortcuts help modal |
+58 -11
crates/fin-tui/src/app.rs
··· 955 955 } 956 956 } 957 957 958 + /// Whether the active backend serves video at all. Subsonic is 959 + /// music-only, so the Videos tab exists only on Jellyfin. 960 + fn videos_available(&self) -> bool { 961 + self.jf().kind() == fin_config::ServerKind::Jellyfin 962 + } 963 + 964 + /// The tab set for the active backend — `Screen::ALL` minus Videos 965 + /// when the server is Subsonic. 966 + fn visible_screens(&self) -> Vec<Screen> { 967 + Screen::ALL 968 + .iter() 969 + .copied() 970 + .filter(|s| *s != Screen::Videos || self.videos_available()) 971 + .collect() 972 + } 973 + 974 + /// Neighbor of the current screen within the visible tab set (`dir` 975 + /// is +1 / −1), wrapping. Falls back to the first tab if the current 976 + /// screen isn't in the set. 977 + fn adjacent_screen(&self, dir: isize) -> Screen { 978 + let vis = self.visible_screens(); 979 + let n = vis.len() as isize; 980 + match vis.iter().position(|s| *s == self.screen) { 981 + Some(i) => vis[((i as isize + dir + n) % n) as usize], 982 + None => vis.first().copied().unwrap_or(Screen::Music), 983 + } 984 + } 985 + 986 + /// Bounce off screens the new backend doesn't offer — e.g. a server 987 + /// switch lands on Subsonic while the Videos tab is open. Call after 988 + /// any server switch, before reloading the screen. 989 + fn ensure_screen_supported(&mut self) { 990 + if self.screen == Screen::Videos && !self.videos_available() { 991 + self.screen = Screen::Music; 992 + *self.open_series.lock() = None; 993 + self.list_state.select(Some(0)); 994 + } 995 + } 996 + 958 997 async fn switch_to_mpv(&self) { 959 998 // Local playback: audio → symphonia, video → mpv. Persistence is 960 999 // wired in so switching to local while there's a saved queue picks ··· 1303 1342 app.should_quit = true; 1304 1343 } 1305 1344 (KeyCode::Tab, _) | (KeyCode::Right, KeyModifiers::CONTROL) => { 1306 - app.screen = app.screen.next(); 1345 + app.screen = app.adjacent_screen(1); 1307 1346 app.list_state.select(Some(0)); 1308 1347 app.load_screen().await; 1309 1348 } 1310 1349 (KeyCode::BackTab, _) | (KeyCode::Left, KeyModifiers::CONTROL) => { 1311 - app.screen = app.screen.prev(); 1350 + app.screen = app.adjacent_screen(-1); 1312 1351 app.list_state.select(Some(0)); 1313 1352 app.load_screen().await; 1314 1353 } ··· 1319 1358 app.load_screen().await; 1320 1359 } 1321 1360 (KeyCode::Char('2'), _) => { 1322 - app.screen = Screen::Videos; 1323 - *app.open_series.lock() = None; 1324 - app.list_state.select(Some(0)); 1325 - app.load_screen().await; 1361 + // Videos is a Jellyfin-only tab — Subsonic has no video API. 1362 + if app.videos_available() { 1363 + app.screen = Screen::Videos; 1364 + *app.open_series.lock() = None; 1365 + app.list_state.select(Some(0)); 1366 + app.load_screen().await; 1367 + } else { 1368 + app.set_status("Videos are Jellyfin-only — this server is Subsonic."); 1369 + } 1326 1370 } 1327 1371 (KeyCode::Char('3'), _) => { 1328 1372 app.screen = Screen::Playlists; ··· 1430 1474 if let Err(e) = app.switch_server(&name) { 1431 1475 app.set_status(format!("switch: {}", e)); 1432 1476 } else { 1477 + app.ensure_screen_supported(); 1433 1478 app.load_screen().await; 1434 1479 } 1435 1480 } ··· 1483 1528 } 1484 1529 (KeyCode::Char('t'), _) => { 1485 1530 app.cycle_server(); 1531 + app.ensure_screen_supported(); 1486 1532 app.load_screen().await; 1487 1533 } 1488 1534 (KeyCode::Char('z'), _) => { ··· 1755 1801 let block = neon_block("", false); 1756 1802 let inner = block.inner(area); 1757 1803 f.render_widget(block, area); 1758 - let labels: Vec<(&str, &str)> = Screen::ALL.iter().map(|s| (s.icon(), s.label())).collect(); 1759 - let selected = Screen::ALL 1760 - .iter() 1761 - .position(|s| *s == app.screen) 1762 - .unwrap_or(0); 1804 + // Tab set depends on the backend — Subsonic has no Videos tab. The 1805 + // number keys stay stable (2 = Videos on Jellyfin, a no-op hint on 1806 + // Subsonic) since the bar doesn't display digits. 1807 + let screens = app.visible_screens(); 1808 + let labels: Vec<(&str, &str)> = screens.iter().map(|s| (s.icon(), s.label())).collect(); 1809 + let selected = screens.iter().position(|s| *s == app.screen).unwrap_or(0); 1763 1810 f.render_widget(NeonTabs::new(&labels, selected), inner); 1764 1811 } 1765 1812
+1 -1
crates/fin-tui/src/widgets/help.rs
··· 33 33 title: "Navigation", 34 34 entries: &[ 35 35 HelpEntry { key: "Tab / Shift+Tab", description: "next / prev screen" }, 36 - HelpEntry { key: "1 … 8", description: "jump to Music / Videos / Playlists / Favorites / Queue / Search / Devices / Settings" }, 36 + HelpEntry { key: "1 … 8", description: "jump to Music / Videos / Playlists / Favorites / Queue / Search / Devices / Settings (Videos is Jellyfin-only — the tab is hidden on Subsonic servers)" }, 37 37 HelpEntry { key: "↑ ↓ / k j", description: "move selection" }, 38 38 HelpEntry { key: "PgUp / PgDown", description: "jump 10 rows" }, 39 39 HelpEntry { key: "Enter", description: "drill in / play leaf / connect (Devices) / switch server (Settings)" },