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 / crates / fin-player / src / renderer.rs
6.0 kB 179 lines
1use async_trait::async_trait; 2use serde::{Deserialize, Serialize}; 3 4use fin_config::EqBand; 5 6use crate::crossfade::CrossfadeSettings; 7use crate::persist::PersistedQueue; 8use crate::queue::{QueueItem, RepeatMode}; 9use crate::replaygain::ReplayGainSettings; 10 11#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)] 12#[serde(rename_all = "lowercase")] 13pub enum PlaybackStatus { 14 Idle, 15 Buffering, 16 Playing, 17 Paused, 18 Stopped, 19} 20 21#[derive(Debug, Clone, Serialize, Deserialize)] 22pub struct PlaybackState { 23 pub status: PlaybackStatus, 24 pub position_secs: f64, 25 pub duration_secs: f64, 26 pub volume: f32, 27 pub now_playing: Option<QueueItem>, 28 pub queue: Vec<QueueItem>, 29 pub current_index: Option<usize>, 30 #[serde(default)] 31 pub shuffle: bool, 32 #[serde(default)] 33 pub repeat: RepeatMode, 34 /// Whatever ReplayGain settings the renderer is currently honoring — 35 /// mirrored on state so the TUI can show a badge without a separate 36 /// query path. 37 #[serde(default)] 38 pub replaygain: ReplayGainSettings, 39 #[serde(default)] 40 pub crossfade: CrossfadeSettings, 41 /// Whether the Rockbox EQ is currently active. Mirrored on state so the 42 /// TUI can render an indicator without a separate query path. 43 #[serde(default)] 44 pub eq_enabled: bool, 45 #[serde(default)] 46 pub eq_band_count: usize, 47 #[serde(default)] 48 pub bass_db: i32, 49 #[serde(default)] 50 pub treble_db: i32, 51} 52 53impl Default for PlaybackState { 54 fn default() -> Self { 55 Self { 56 status: PlaybackStatus::Idle, 57 position_secs: 0.0, 58 duration_secs: 0.0, 59 volume: 1.0, 60 now_playing: None, 61 queue: Vec::new(), 62 current_index: None, 63 shuffle: false, 64 repeat: RepeatMode::Off, 65 replaygain: ReplayGainSettings::default(), 66 crossfade: CrossfadeSettings::default(), 67 eq_enabled: false, 68 eq_band_count: 0, 69 bass_db: 0, 70 treble_db: 0, 71 } 72 } 73} 74 75#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)] 76#[serde(rename_all = "lowercase")] 77pub enum RendererKind { 78 Mpv, 79 Chromecast, 80 Upnp, 81} 82 83impl RendererKind { 84 pub fn label(&self) -> &'static str { 85 match self { 86 Self::Mpv => "mpv", 87 Self::Chromecast => "chromecast", 88 Self::Upnp => "upnp", 89 } 90 } 91} 92 93/// Common interface implemented by both the local mpv renderer and the 94/// Chromecast renderer. Every method is async because Chromecast operations 95/// round-trip a TLS control connection. 96#[async_trait] 97pub trait Renderer: Send + Sync { 98 fn kind(&self) -> RendererKind; 99 100 /// Replace the queue and start playing at `start_index`. 101 async fn play(&self, items: Vec<QueueItem>, start_index: usize) -> anyhow::Result<()>; 102 103 /// Append items to the current queue without interrupting playback. 104 async fn enqueue(&self, items: Vec<QueueItem>) -> anyhow::Result<()>; 105 106 /// Insert items at the front of the queue (play next). 107 async fn play_next(&self, items: Vec<QueueItem>) -> anyhow::Result<()>; 108 109 async fn pause(&self) -> anyhow::Result<()>; 110 async fn resume(&self) -> anyhow::Result<()>; 111 async fn stop(&self) -> anyhow::Result<()>; 112 async fn next(&self) -> anyhow::Result<()>; 113 async fn previous(&self) -> anyhow::Result<()>; 114 async fn seek(&self, position_secs: f64) -> anyhow::Result<()>; 115 async fn set_volume(&self, volume: f32) -> anyhow::Result<()>; 116 117 /// Turn shuffle on or off. Default is a no-op for renderers with no 118 /// queue of their own. 119 async fn set_shuffle(&self, _on: bool) -> anyhow::Result<()> { 120 Ok(()) 121 } 122 123 /// Set the repeat mode. See `set_shuffle` for why the default is a no-op. 124 async fn set_repeat(&self, _mode: RepeatMode) -> anyhow::Result<()> { 125 Ok(()) 126 } 127 128 /// Populate the queue + playhead from a persisted snapshot without 129 /// starting playback. The next `resume()` (or explicit `play`) kicks 130 /// the loaded track, seeking to `snapshot.position_secs` on load. 131 async fn restore(&self, _snapshot: PersistedQueue) -> anyhow::Result<()> { 132 Ok(()) 133 } 134 135 /// Remove one entry from the queue by index without disrupting the 136 /// currently-playing track — unless that item is the one being removed, 137 /// in which case playback advances to the next entry (or stops if the 138 /// queue is now empty). Default is a no-op. 139 async fn remove_from_queue(&self, _index: usize) -> anyhow::Result<()> { 140 Ok(()) 141 } 142 143 /// Update ReplayGain settings (mode, preamp, clip prevention). Non-local 144 /// renderers currently no-op — device-side receivers apply their own 145 /// loudness normalization. 146 async fn set_replaygain(&self, _settings: ReplayGainSettings) -> anyhow::Result<()> { 147 Ok(()) 148 } 149 150 /// Update crossfade settings (mode + duration). Only the local 151 /// RockboxPlayer implements this; Chromecast + UPnP receivers each 152 /// manage their own track transitions. 153 async fn set_crossfade(&self, _settings: CrossfadeSettings) -> anyhow::Result<()> { 154 Ok(()) 155 } 156 157 /// Enable/disable the Rockbox 10-band equalizer and load the band 158 /// coefficients. `bands` is truncated to `EQ_NUM_BANDS`. Only the local 159 /// RockboxPlayer implements this — non-local receivers each apply 160 /// their own EQ (or none). 161 async fn set_eq(&self, _enabled: bool, _bands: Vec<EqBand>) -> anyhow::Result<()> { 162 Ok(()) 163 } 164 165 /// Bass/treble shelf gains in whole dB, plus optional cutoffs in Hz 166 /// (0 = Rockbox defaults 200 Hz bass, 3500 Hz treble). Only the local 167 /// RockboxPlayer applies these; non-local receivers no-op. 168 async fn set_tone( 169 &self, 170 _bass_db: i32, 171 _treble_db: i32, 172 _bass_cutoff_hz: i32, 173 _treble_cutoff_hz: i32, 174 ) -> anyhow::Result<()> { 175 Ok(()) 176 } 177 178 fn state(&self) -> PlaybackState; 179}