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.

Wire shuffle and repeat into UPnP and Chromecast renderers

set_shuffle/set_repeat fell through to the trait's no-op defaults, so
the z / Shift+R toggles did nothing while casting. Both cast workers
already auto-advance through the shared PlaybackQueue, which honors
shuffle order and repeat modes — the overrides just update that queue
and mirror the new state so the TUI reflects it.

Tsiry Sandratraina (Jul 7, 2026, 4:44 PM +0300) 01cacad6 e8b34a89

+42 -5
+20 -1
crates/fin-player/src/cast.rs
··· 17 17 use rust_cast::CastDevice as RcDevice; 18 18 19 19 use crate::discovery::CastDevice; 20 - use crate::queue::{PlaybackQueue, QueueItem}; 20 + use crate::queue::{PlaybackQueue, QueueItem, RepeatMode}; 21 21 use crate::renderer::{PlaybackState, PlaybackStatus, Renderer, RendererKind}; 22 22 23 23 const DEFAULT_DESTINATION_ID: &str = "receiver-0"; ··· 584 584 585 585 async fn set_volume(&self, volume: f32) -> Result<()> { 586 586 self.send(|reply| CastCommand::Volume(volume, reply)).await 587 + } 588 + 589 + // Shuffle/repeat live entirely in our queue — the receiver only ever 590 + // sees one media item at a time, so no cast round-trip is needed. The 591 + // worker's auto-advance picks up the new order/mode on the next track end. 592 + async fn set_shuffle(&self, on: bool) -> Result<()> { 593 + self.queue.set_shuffle(on); 594 + let items = self.queue.items(); 595 + let mut s = self.state.lock(); 596 + s.shuffle = on; 597 + s.queue = items; 598 + s.current_index = self.queue.current_index(); 599 + Ok(()) 600 + } 601 + 602 + async fn set_repeat(&self, mode: RepeatMode) -> Result<()> { 603 + self.queue.set_repeat(mode); 604 + self.state.lock().repeat = mode; 605 + Ok(()) 587 606 } 588 607 589 608 fn state(&self) -> PlaybackState {
+2 -3
crates/fin-player/src/renderer.rs
··· 114 114 async fn seek(&self, position_secs: f64) -> anyhow::Result<()>; 115 115 async fn set_volume(&self, volume: f32) -> anyhow::Result<()>; 116 116 117 - /// Turn shuffle on or off. Default is a no-op — Chromecast and UPnP 118 - /// renderers have their own device-side queue models that we haven't 119 - /// wired shuffle into yet. 117 + /// Turn shuffle on or off. Default is a no-op for renderers with no 118 + /// queue of their own. 120 119 async fn set_shuffle(&self, _on: bool) -> anyhow::Result<()> { 121 120 Ok(()) 122 121 }
+20 -1
crates/fin-player/src/upnp.rs
··· 30 30 use tokio::time::timeout; 31 31 use tracing::{debug, error, warn}; 32 32 33 - use crate::queue::{PlaybackQueue, QueueItem}; 33 + use crate::queue::{PlaybackQueue, QueueItem, RepeatMode}; 34 34 use crate::renderer::{PlaybackState, PlaybackStatus, Renderer, RendererKind}; 35 35 36 36 const SSDP_ADDR: &str = "239.255.255.250:1900"; ··· 850 850 851 851 async fn set_volume(&self, volume: f32) -> Result<()> { 852 852 self.send(|reply| UpnpCommand::Volume(volume, reply)).await 853 + } 854 + 855 + // Shuffle/repeat live entirely in our queue — the device only ever sees 856 + // one URI at a time, so no SOAP round-trip is needed. The worker's 857 + // auto-advance picks up the new order/mode on the next track end. 858 + async fn set_shuffle(&self, on: bool) -> Result<()> { 859 + self.queue.set_shuffle(on); 860 + let items = self.queue.items(); 861 + let mut s = self.state.lock(); 862 + s.shuffle = on; 863 + s.queue = items; 864 + s.current_index = self.queue.current_index(); 865 + Ok(()) 866 + } 867 + 868 + async fn set_repeat(&self, mode: RepeatMode) -> Result<()> { 869 + self.queue.set_repeat(mode); 870 + self.state.lock().repeat = mode; 871 + Ok(()) 853 872 } 854 873 855 874 fn state(&self) -> PlaybackState {