a Jellyfin & Subsonic client for the terminal — powered by mpv, Chromecast and UPnP MediaRenderer
mpv
chromecast
mpris
navidrome
jellyfin
upnp
tui
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}