Initial commit — ferrosonic terminal Subsonic client
Terminal-based Subsonic music client in Rust featuring bit-perfect audio playback via PipeWire sample rate switching, gapless playback, MPRIS2 desktop integration, cava audio visualizer with theme-matched gradients, 13 built-in color themes with custom TOML theme support, mouse controls, artist/album browser, playlist support, and play queue management. Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
235
src/subsonic/models.rs
Normal file
235
src/subsonic/models.rs
Normal file
@@ -0,0 +1,235 @@
|
||||
//! Subsonic API response models
|
||||
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
/// Wrapper for all Subsonic API responses
|
||||
#[derive(Debug, Deserialize)]
|
||||
pub struct SubsonicResponse<T> {
|
||||
#[serde(rename = "subsonic-response")]
|
||||
pub subsonic_response: SubsonicResponseInner<T>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Deserialize)]
|
||||
pub struct SubsonicResponseInner<T> {
|
||||
pub status: String,
|
||||
pub version: String,
|
||||
#[serde(default)]
|
||||
pub error: Option<ApiError>,
|
||||
#[serde(flatten)]
|
||||
pub data: Option<T>,
|
||||
}
|
||||
|
||||
/// API error response
|
||||
#[derive(Debug, Deserialize)]
|
||||
pub struct ApiError {
|
||||
pub code: i32,
|
||||
pub message: String,
|
||||
}
|
||||
|
||||
/// Artists response wrapper
|
||||
#[derive(Debug, Deserialize)]
|
||||
pub struct ArtistsData {
|
||||
pub artists: ArtistsIndex,
|
||||
}
|
||||
|
||||
#[derive(Debug, Deserialize)]
|
||||
pub struct ArtistsIndex {
|
||||
#[serde(default)]
|
||||
pub index: Vec<ArtistIndex>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Deserialize)]
|
||||
pub struct ArtistIndex {
|
||||
pub name: String,
|
||||
#[serde(default)]
|
||||
pub artist: Vec<Artist>,
|
||||
}
|
||||
|
||||
/// Artist
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub struct Artist {
|
||||
pub id: String,
|
||||
pub name: String,
|
||||
#[serde(default, rename = "albumCount")]
|
||||
pub album_count: Option<i32>,
|
||||
#[serde(default, rename = "coverArt")]
|
||||
pub cover_art: Option<String>,
|
||||
}
|
||||
|
||||
/// Artist detail with albums
|
||||
#[derive(Debug, Deserialize)]
|
||||
pub struct ArtistData {
|
||||
pub artist: ArtistDetail,
|
||||
}
|
||||
|
||||
#[derive(Debug, Deserialize)]
|
||||
pub struct ArtistDetail {
|
||||
pub id: String,
|
||||
pub name: String,
|
||||
#[serde(default)]
|
||||
pub album: Vec<Album>,
|
||||
}
|
||||
|
||||
/// Album
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub struct Album {
|
||||
pub id: String,
|
||||
pub name: String,
|
||||
#[serde(default)]
|
||||
pub artist: Option<String>,
|
||||
#[serde(default, rename = "artistId")]
|
||||
pub artist_id: Option<String>,
|
||||
#[serde(default, rename = "coverArt")]
|
||||
pub cover_art: Option<String>,
|
||||
#[serde(default, rename = "songCount")]
|
||||
pub song_count: Option<i32>,
|
||||
#[serde(default)]
|
||||
pub duration: Option<i32>,
|
||||
#[serde(default)]
|
||||
pub year: Option<i32>,
|
||||
#[serde(default)]
|
||||
pub genre: Option<String>,
|
||||
}
|
||||
|
||||
/// Album detail with songs
|
||||
#[derive(Debug, Deserialize)]
|
||||
pub struct AlbumData {
|
||||
pub album: AlbumDetail,
|
||||
}
|
||||
|
||||
#[derive(Debug, Deserialize)]
|
||||
pub struct AlbumDetail {
|
||||
pub id: String,
|
||||
pub name: String,
|
||||
#[serde(default)]
|
||||
pub artist: Option<String>,
|
||||
#[serde(default, rename = "artistId")]
|
||||
pub artist_id: Option<String>,
|
||||
#[serde(default)]
|
||||
pub year: Option<i32>,
|
||||
#[serde(default)]
|
||||
pub song: Vec<Child>,
|
||||
}
|
||||
|
||||
/// Song/Media item (called "Child" in Subsonic API)
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub struct Child {
|
||||
pub id: String,
|
||||
#[serde(default)]
|
||||
pub parent: Option<String>,
|
||||
#[serde(default, rename = "isDir")]
|
||||
pub is_dir: bool,
|
||||
pub title: String,
|
||||
#[serde(default)]
|
||||
pub album: Option<String>,
|
||||
#[serde(default)]
|
||||
pub artist: Option<String>,
|
||||
#[serde(default)]
|
||||
pub track: Option<i32>,
|
||||
#[serde(default)]
|
||||
pub year: Option<i32>,
|
||||
#[serde(default)]
|
||||
pub genre: Option<String>,
|
||||
#[serde(default, rename = "coverArt")]
|
||||
pub cover_art: Option<String>,
|
||||
#[serde(default)]
|
||||
pub size: Option<i64>,
|
||||
#[serde(default, rename = "contentType")]
|
||||
pub content_type: Option<String>,
|
||||
#[serde(default)]
|
||||
pub suffix: Option<String>,
|
||||
#[serde(default)]
|
||||
pub duration: Option<i32>,
|
||||
#[serde(default, rename = "bitRate")]
|
||||
pub bit_rate: Option<i32>,
|
||||
#[serde(default)]
|
||||
pub path: Option<String>,
|
||||
#[serde(default, rename = "discNumber")]
|
||||
pub disc_number: Option<i32>,
|
||||
}
|
||||
|
||||
impl Child {
|
||||
/// Format duration as MM:SS
|
||||
pub fn format_duration(&self) -> String {
|
||||
match self.duration {
|
||||
Some(d) => {
|
||||
let mins = d / 60;
|
||||
let secs = d % 60;
|
||||
format!("{:02}:{:02}", mins, secs)
|
||||
}
|
||||
None => "--:--".to_string(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Playlists response
|
||||
#[derive(Debug, Deserialize)]
|
||||
pub struct PlaylistsData {
|
||||
pub playlists: PlaylistsInner,
|
||||
}
|
||||
|
||||
#[derive(Debug, Deserialize)]
|
||||
pub struct PlaylistsInner {
|
||||
#[serde(default)]
|
||||
pub playlist: Vec<Playlist>,
|
||||
}
|
||||
|
||||
/// Playlist
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub struct Playlist {
|
||||
pub id: String,
|
||||
pub name: String,
|
||||
#[serde(default)]
|
||||
pub owner: Option<String>,
|
||||
#[serde(default, rename = "songCount")]
|
||||
pub song_count: Option<i32>,
|
||||
#[serde(default)]
|
||||
pub duration: Option<i32>,
|
||||
#[serde(default, rename = "coverArt")]
|
||||
pub cover_art: Option<String>,
|
||||
#[serde(default)]
|
||||
pub public: Option<bool>,
|
||||
#[serde(default)]
|
||||
pub comment: Option<String>,
|
||||
}
|
||||
|
||||
/// Playlist detail with songs
|
||||
#[derive(Debug, Deserialize)]
|
||||
pub struct PlaylistData {
|
||||
pub playlist: PlaylistDetail,
|
||||
}
|
||||
|
||||
#[derive(Debug, Deserialize)]
|
||||
pub struct PlaylistDetail {
|
||||
pub id: String,
|
||||
pub name: String,
|
||||
#[serde(default)]
|
||||
pub owner: Option<String>,
|
||||
#[serde(default, rename = "songCount")]
|
||||
pub song_count: Option<i32>,
|
||||
#[serde(default)]
|
||||
pub duration: Option<i32>,
|
||||
#[serde(default)]
|
||||
pub entry: Vec<Child>,
|
||||
}
|
||||
|
||||
/// Ping response (for testing connection)
|
||||
#[derive(Debug, Deserialize)]
|
||||
pub struct PingData {}
|
||||
|
||||
/// Search result
|
||||
#[derive(Debug, Deserialize)]
|
||||
pub struct SearchResult3Data {
|
||||
#[serde(rename = "searchResult3")]
|
||||
pub search_result3: SearchResult3,
|
||||
}
|
||||
|
||||
#[derive(Debug, Deserialize)]
|
||||
pub struct SearchResult3 {
|
||||
#[serde(default)]
|
||||
pub artist: Vec<Artist>,
|
||||
#[serde(default)]
|
||||
pub album: Vec<Album>,
|
||||
#[serde(default)]
|
||||
pub song: Vec<Child>,
|
||||
}
|
||||
Reference in New Issue
Block a user