Added connected devices. Improved logging. UI fixes

This commit is contained in:
Ultradesu
2026-06-10 23:30:03 +01:00
parent bcee68eb4e
commit 02a396c146
25 changed files with 2540 additions and 314 deletions
+93 -19
View File
@@ -22,7 +22,10 @@ pub struct LoginResponse {
}
#[derive(Debug, Deserialize)]
#[allow(dead_code, reason = "rendered by the profile view in a later milestone")]
#[allow(
dead_code,
reason = "rendered by the profile view in a later milestone"
)]
pub struct MeStats {
pub liked_tracks: i64,
pub playlists: i64,
@@ -31,7 +34,10 @@ pub struct MeStats {
}
#[derive(Debug, Deserialize)]
#[allow(dead_code, reason = "rendered by the profile view in a later milestone")]
#[allow(
dead_code,
reason = "rendered by the profile view in a later milestone"
)]
pub struct MeResponse {
pub id: i64,
pub name: String,
@@ -55,28 +61,37 @@ pub struct ArtistCard {
pub track_count: i64,
}
#[derive(Debug, Clone, Deserialize)]
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct ArtistRef {
#[allow(dead_code, reason = "navigation to artists from track rows later")]
pub id: i64,
pub name: String,
}
#[derive(Debug, Clone, Deserialize)]
/// Serialize keeps every field the backend sent us, so device-sync payloads
/// (play_from_index, queue_add) carry full track objects like the web does.
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct TrackItem {
#[allow(dead_code, reason = "playback engine consumes this in milestone 3")]
pub id: i64,
pub title: String,
/// Absent in the artist-appearance variant of track payloads.
#[serde(default)]
pub track_number: Option<i32>,
pub duration_seconds: f64,
#[serde(default)]
pub artists: Vec<ArtistRef>,
#[serde(default)]
pub featured_artists: Vec<ArtistRef>,
#[allow(dead_code, reason = "jump-to-release navigation later")]
#[serde(default)]
pub release_id: i64,
#[serde(default)]
pub release_title: String,
#[allow(dead_code, reason = "shown in queue/now-playing later")]
pub release_year: Option<i32>,
/// Server-relative path to `/api/player/stream/{id}`.
#[serde(default)]
pub stream_url: String,
#[allow(dead_code, reason = "now-playing artwork in milestone 3")]
pub cover_url: Option<String>,
@@ -103,21 +118,6 @@ impl TrackItem {
format!("{}:{:02}", total / 60, total % 60)
}
/// Compact "FLAC 929k 24.3MB" suffix for track rows.
pub fn tech_label_short(&self) -> String {
let mut parts = Vec::new();
if let Some(format) = &self.audio_format {
parts.push(format.to_uppercase());
}
if let Some(bitrate) = self.audio_bitrate {
parts.push(format!("{bitrate}k"));
}
if let Some(bytes) = self.file_size_bytes {
parts.push(format!("{:.1}MB", bytes as f64 / 1_048_576.0));
}
parts.join(" ")
}
/// Full tech line for the status bar, including the sample rate.
pub fn tech_label_full(&self) -> String {
let mut parts = Vec::new();
@@ -157,6 +157,10 @@ pub struct ArtistDetail {
pub total_play_count: i64,
pub top_tracks: Vec<TrackItem>,
pub releases: Vec<ReleaseCard>,
/// Tracks where this artist is featured (the only content for artists
/// without own releases).
#[serde(default)]
pub featured_tracks: Vec<TrackItem>,
}
#[derive(Debug, Clone, Deserialize)]
@@ -208,6 +212,76 @@ pub struct LikesResponse {
pub track_ids: Vec<i64>,
}
#[derive(Debug, Clone, Deserialize)]
pub struct DeviceDto {
pub id: String,
pub name: String,
pub kind: String,
#[allow(dead_code, reason = "server-side flag; we compare ids directly")]
pub is_current: bool,
pub is_active: bool,
#[allow(dead_code, reason = "freshness display later")]
pub last_seen_ms: i64,
}
#[derive(Debug, Deserialize)]
pub struct DeviceCommandDto {
#[allow(dead_code, reason = "commands are applied in poll order")]
pub id: Option<String>,
pub command: String,
#[serde(default)]
pub payload: serde_json::Value,
}
/// Mirrors the backend's PlayerDevicePlaybackStateDto; tracks stay raw JSON
/// so unknown fields survive the round trip between clients.
#[derive(Debug, Clone, Default, Serialize, Deserialize)]
pub struct DevicePlaybackState {
#[serde(default)]
pub track: Option<serde_json::Value>,
#[serde(default)]
pub tracks: Vec<serde_json::Value>,
#[serde(default)]
pub index: i32,
#[serde(default)]
pub position_seconds: f64,
#[serde(default)]
pub duration_seconds: f64,
#[serde(default)]
pub paused: bool,
#[serde(default)]
pub shuffle: bool,
#[serde(default = "default_repeat_mode")]
pub repeat_mode: String,
#[serde(default = "default_volume")]
pub volume: f64,
#[serde(default)]
pub updated_at_ms: i64,
}
fn default_repeat_mode() -> String {
"off".to_string()
}
fn default_volume() -> f64 {
1.0
}
#[derive(Debug, Deserialize)]
pub struct DevicePollResponse {
#[allow(dead_code, reason = "echo of our own id")]
pub device_id: String,
pub active_device_id: Option<String>,
#[serde(default)]
pub devices: Vec<DeviceDto>,
#[serde(default)]
pub commands: Vec<DeviceCommandDto>,
#[serde(default)]
#[allow(dead_code, reason = "Jam control is out of scope for the TUI v1")]
pub current_jam_id: Option<String>,
pub playback_state: Option<DevicePlaybackState>,
}
#[derive(Debug, Default, Deserialize)]
pub struct SearchResults {
pub artists: Vec<ArtistCard>,