Initial implementation of qobuz-qt
- Rust backend (qobuz-backend static lib): Qobuz API client (reqwest/tokio), Symphonia audio decoder, CPAL audio output, extern "C" FFI bridge - Qt 6 frontend mirroring spotify-qt layout: toolbar with playback controls, left library dock, central track list, right search panel - Auth: email/password login with MD5-signed requests; session token persisted via QSettings - Playback: double-click a track → Rust fetches stream URL → Symphonia decodes → CPAL outputs to default audio device - Dark Fusion palette matching spotify-qt feel Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
371
rust/src/api/client.rs
Normal file
371
rust/src/api/client.rs
Normal file
@@ -0,0 +1,371 @@
|
||||
use anyhow::{bail, Result};
|
||||
use reqwest::Client;
|
||||
use serde_json::Value;
|
||||
use std::time::{SystemTime, UNIX_EPOCH};
|
||||
|
||||
use super::models::*;
|
||||
|
||||
const BASE_URL: &str = "https://www.qobuz.com/api.json/0.2/";
|
||||
const USER_AGENT: &str =
|
||||
"Dalvik/2.1.0 (Linux; U; Android 9; Nexus 6P Build/PQ3A.190801.002) QobuzMobileAndroid/9.7.0.3-b26022717";
|
||||
const APP_VERSION: &str = "9.7.0.3";
|
||||
pub const DEFAULT_APP_ID: &str = "312369995";
|
||||
pub const DEFAULT_APP_SECRET: &str = "e79f8b9be485692b0e5f9dd895826368";
|
||||
|
||||
pub struct QobuzClient {
|
||||
http: Client,
|
||||
pub auth_token: Option<String>,
|
||||
app_id: String,
|
||||
app_secret: String,
|
||||
}
|
||||
|
||||
impl QobuzClient {
|
||||
pub fn new() -> Result<Self> {
|
||||
Self::new_with_config(None, None)
|
||||
}
|
||||
|
||||
pub fn new_with_config(app_id: Option<&str>, app_secret: Option<&str>) -> Result<Self> {
|
||||
let app_id = app_id.unwrap_or(DEFAULT_APP_ID).to_string();
|
||||
let app_secret = app_secret.unwrap_or(DEFAULT_APP_SECRET).to_string();
|
||||
|
||||
let http = Client::builder()
|
||||
.user_agent(USER_AGENT)
|
||||
.default_headers({
|
||||
let mut h = reqwest::header::HeaderMap::new();
|
||||
h.insert("X-App-Id", app_id.parse()?);
|
||||
h.insert("X-App-Version", APP_VERSION.parse()?);
|
||||
h.insert("X-Device-Platform", "android".parse()?);
|
||||
h.insert("X-Device-Model", "Nexus 6P".parse()?);
|
||||
h.insert("X-Device-Os-Version", "9".parse()?);
|
||||
h
|
||||
})
|
||||
.build()?;
|
||||
|
||||
Ok(Self {
|
||||
http,
|
||||
auth_token: None,
|
||||
app_id,
|
||||
app_secret,
|
||||
})
|
||||
}
|
||||
|
||||
pub fn set_auth_token(&mut self, token: String) {
|
||||
self.auth_token = Some(token);
|
||||
}
|
||||
|
||||
fn ts() -> u64 {
|
||||
SystemTime::now()
|
||||
.duration_since(UNIX_EPOCH)
|
||||
.unwrap()
|
||||
.as_secs()
|
||||
}
|
||||
|
||||
fn request_sig(&self, method: &str, params: &mut Vec<(&str, String)>, ts: u64) -> String {
|
||||
params.sort_by_key(|(k, _)| *k);
|
||||
let mut s = method.replace('/', "");
|
||||
for (k, v) in params.iter() {
|
||||
s.push_str(k);
|
||||
s.push_str(v);
|
||||
}
|
||||
s.push_str(&ts.to_string());
|
||||
s.push_str(&self.app_secret);
|
||||
format!("{:x}", md5::compute(s.as_bytes()))
|
||||
}
|
||||
|
||||
fn url(&self, method: &str) -> String {
|
||||
format!("{}{}", BASE_URL, method)
|
||||
}
|
||||
|
||||
async fn check_response(resp: reqwest::Response) -> Result<Value> {
|
||||
let status = resp.status();
|
||||
let body: Value = resp.json().await?;
|
||||
if !status.is_success() {
|
||||
let msg = body
|
||||
.get("message")
|
||||
.and_then(|m| m.as_str())
|
||||
.unwrap_or("unknown API error");
|
||||
bail!("HTTP {}: {}", status, msg);
|
||||
}
|
||||
Ok(body)
|
||||
}
|
||||
|
||||
fn get_request(&self, method: &str) -> reqwest::RequestBuilder {
|
||||
let mut builder = self.http.get(self.url(method));
|
||||
builder = builder.query(&[("app_id", self.app_id.as_str())]);
|
||||
if let Some(token) = &self.auth_token {
|
||||
builder = builder.header("Authorization", format!("Bearer {}", token));
|
||||
}
|
||||
builder
|
||||
}
|
||||
|
||||
// --- Auth ---
|
||||
|
||||
pub async fn login(&mut self, email: &str, password: &str) -> Result<OAuthLoginResponse> {
|
||||
match self.oauth2_login(email, password).await {
|
||||
Ok(r) => Ok(r),
|
||||
Err(_) => self.legacy_login(email, password).await,
|
||||
}
|
||||
}
|
||||
|
||||
async fn oauth2_login(&mut self, email: &str, password: &str) -> Result<OAuthLoginResponse> {
|
||||
let ts = Self::ts();
|
||||
let mut sign_params: Vec<(&str, String)> = vec![
|
||||
("password", password.to_string()),
|
||||
("username", email.to_string()),
|
||||
];
|
||||
let sig = self.request_sig("oauth2login", &mut sign_params, ts);
|
||||
|
||||
let resp = self
|
||||
.http
|
||||
.get(self.url("oauth2/login"))
|
||||
.query(&[
|
||||
("app_id", self.app_id.as_str()),
|
||||
("username", email),
|
||||
("password", password),
|
||||
("request_ts", ts.to_string().as_str()),
|
||||
("request_sig", sig.as_str()),
|
||||
])
|
||||
.send()
|
||||
.await?;
|
||||
|
||||
let status = resp.status();
|
||||
let body: Value = resp.json().await?;
|
||||
if !status.is_success() {
|
||||
let msg = body.get("message").and_then(|m| m.as_str()).unwrap_or("login failed");
|
||||
bail!("oauth2 login failed ({}): {}", status, msg);
|
||||
}
|
||||
|
||||
self.extract_and_store_token(serde_json::from_value(body)?)
|
||||
}
|
||||
|
||||
async fn legacy_login(&mut self, email: &str, password: &str) -> Result<OAuthLoginResponse> {
|
||||
let ts = Self::ts();
|
||||
let mut sign_params: Vec<(&str, String)> = vec![
|
||||
("email", email.to_string()),
|
||||
("password", password.to_string()),
|
||||
];
|
||||
let sig = self.request_sig("userlogin", &mut sign_params, ts);
|
||||
|
||||
let resp = self
|
||||
.http
|
||||
.get(self.url("user/login"))
|
||||
.query(&[
|
||||
("app_id", self.app_id.as_str()),
|
||||
("email", email),
|
||||
("password", password),
|
||||
("request_ts", ts.to_string().as_str()),
|
||||
("request_sig", sig.as_str()),
|
||||
])
|
||||
.send()
|
||||
.await?;
|
||||
|
||||
let status = resp.status();
|
||||
let body: Value = resp.json().await?;
|
||||
if !status.is_success() {
|
||||
let msg = body.get("message").and_then(|m| m.as_str()).unwrap_or("login failed");
|
||||
bail!("user login failed ({}): {}", status, msg);
|
||||
}
|
||||
|
||||
self.extract_and_store_token(serde_json::from_value(body)?)
|
||||
}
|
||||
|
||||
fn extract_and_store_token(&mut self, login: OAuthLoginResponse) -> Result<OAuthLoginResponse> {
|
||||
if let Some(token) = login
|
||||
.oauth2
|
||||
.as_ref()
|
||||
.and_then(|o| o.access_token.clone())
|
||||
.or_else(|| login.user_auth_token.clone())
|
||||
{
|
||||
self.auth_token = Some(token);
|
||||
}
|
||||
Ok(login)
|
||||
}
|
||||
|
||||
// --- User ---
|
||||
|
||||
pub async fn get_user(&self) -> Result<UserDto> {
|
||||
let resp = self.get_request("user/get").send().await?;
|
||||
let body = Self::check_response(resp).await?;
|
||||
let user: UserDto = serde_json::from_value(body["user"].clone())
|
||||
.or_else(|_| serde_json::from_value(body.clone()))?;
|
||||
Ok(user)
|
||||
}
|
||||
|
||||
// --- Track ---
|
||||
|
||||
pub async fn get_track(&self, track_id: i64) -> Result<TrackDto> {
|
||||
let resp = self
|
||||
.get_request("track/get")
|
||||
.query(&[("track_id", track_id.to_string())])
|
||||
.send()
|
||||
.await?;
|
||||
let body = Self::check_response(resp).await?;
|
||||
Ok(serde_json::from_value(body)?)
|
||||
}
|
||||
|
||||
pub async fn get_track_url(&self, track_id: i64, format: Format) -> Result<TrackFileUrlDto> {
|
||||
let ts = Self::ts();
|
||||
let intent = "stream";
|
||||
let mut sign_params: Vec<(&str, String)> = vec![
|
||||
("format_id", format.id().to_string()),
|
||||
("intent", intent.to_string()),
|
||||
("track_id", track_id.to_string()),
|
||||
];
|
||||
let sig = self.request_sig("trackgetFileUrl", &mut sign_params, ts);
|
||||
|
||||
let resp = self
|
||||
.get_request("track/getFileUrl")
|
||||
.query(&[
|
||||
("track_id", track_id.to_string()),
|
||||
("format_id", format.id().to_string()),
|
||||
("intent", intent.to_string()),
|
||||
("request_ts", ts.to_string()),
|
||||
("request_sig", sig),
|
||||
])
|
||||
.send()
|
||||
.await?;
|
||||
|
||||
let body = Self::check_response(resp).await?;
|
||||
Ok(serde_json::from_value(body)?)
|
||||
}
|
||||
|
||||
// --- Album ---
|
||||
|
||||
pub async fn get_album(&self, album_id: &str) -> Result<AlbumDto> {
|
||||
let resp = self
|
||||
.get_request("album/get")
|
||||
.query(&[("album_id", album_id), ("limit", "50"), ("offset", "0")])
|
||||
.send()
|
||||
.await?;
|
||||
let body = Self::check_response(resp).await?;
|
||||
Ok(serde_json::from_value(body)?)
|
||||
}
|
||||
|
||||
// --- Artist ---
|
||||
|
||||
pub async fn get_artist(&self, artist_id: i64) -> Result<ArtistDto> {
|
||||
let resp = self
|
||||
.get_request("artist/get")
|
||||
.query(&[
|
||||
("artist_id", artist_id.to_string()),
|
||||
("extra", "albums,biography".to_string()),
|
||||
])
|
||||
.send()
|
||||
.await?;
|
||||
let body = Self::check_response(resp).await?;
|
||||
Ok(serde_json::from_value(body)?)
|
||||
}
|
||||
|
||||
// --- Search ---
|
||||
|
||||
pub async fn search(&self, query: &str, offset: u32, limit: u32) -> Result<SearchCatalogDto> {
|
||||
let resp = self
|
||||
.get_request("catalog/search")
|
||||
.query(&[
|
||||
("query", query),
|
||||
("offset", &offset.to_string()),
|
||||
("limit", &limit.to_string()),
|
||||
])
|
||||
.send()
|
||||
.await?;
|
||||
let body = Self::check_response(resp).await?;
|
||||
Ok(serde_json::from_value(body)?)
|
||||
}
|
||||
|
||||
// --- Favorites / Library ---
|
||||
|
||||
pub async fn get_user_playlists(&self, offset: u32, limit: u32) -> Result<UserPlaylistsDto> {
|
||||
let resp = self
|
||||
.get_request("playlist/getUserPlaylists")
|
||||
.query(&[("offset", &offset.to_string()), ("limit", &limit.to_string())])
|
||||
.send()
|
||||
.await?;
|
||||
let body = Self::check_response(resp).await?;
|
||||
Ok(serde_json::from_value(body)?)
|
||||
}
|
||||
|
||||
pub async fn get_playlist(&self, playlist_id: i64, offset: u32, limit: u32) -> Result<PlaylistDto> {
|
||||
let resp = self
|
||||
.get_request("playlist/get")
|
||||
.query(&[
|
||||
("playlist_id", &playlist_id.to_string()),
|
||||
("extra", &"tracks".to_string()),
|
||||
("offset", &offset.to_string()),
|
||||
("limit", &limit.to_string()),
|
||||
])
|
||||
.send()
|
||||
.await?;
|
||||
let body = Self::check_response(resp).await?;
|
||||
Ok(serde_json::from_value(body)?)
|
||||
}
|
||||
|
||||
pub async fn get_fav_tracks(&self, offset: u32, limit: u32) -> Result<SearchResultItems<TrackDto>> {
|
||||
let resp = self
|
||||
.get_request("favorite/getUserFavorites")
|
||||
.query(&[("type", "tracks"), ("offset", &offset.to_string()), ("limit", &limit.to_string())])
|
||||
.send()
|
||||
.await?;
|
||||
let body = Self::check_response(resp).await?;
|
||||
Ok(serde_json::from_value(body["tracks"].clone())?)
|
||||
}
|
||||
|
||||
pub async fn get_fav_albums(&self, offset: u32, limit: u32) -> Result<SearchResultItems<AlbumDto>> {
|
||||
let resp = self
|
||||
.get_request("favorite/getUserFavorites")
|
||||
.query(&[("type", "albums"), ("offset", &offset.to_string()), ("limit", &limit.to_string())])
|
||||
.send()
|
||||
.await?;
|
||||
let body = Self::check_response(resp).await?;
|
||||
Ok(serde_json::from_value(body["albums"].clone())?)
|
||||
}
|
||||
|
||||
pub async fn get_fav_artists(&self, offset: u32, limit: u32) -> Result<SearchResultItems<FavArtistDto>> {
|
||||
let resp = self
|
||||
.get_request("favorite/getUserFavorites")
|
||||
.query(&[("type", "artists"), ("offset", &offset.to_string()), ("limit", &limit.to_string())])
|
||||
.send()
|
||||
.await?;
|
||||
let body = Self::check_response(resp).await?;
|
||||
Ok(serde_json::from_value(body["artists"].clone())?)
|
||||
}
|
||||
|
||||
pub async fn add_fav_track(&self, track_id: i64) -> Result<()> {
|
||||
let resp = self
|
||||
.get_request("favorite/create")
|
||||
.query(&[("type", "tracks"), ("track_ids", &track_id.to_string())])
|
||||
.send()
|
||||
.await?;
|
||||
Self::check_response(resp).await?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub async fn remove_fav_track(&self, track_id: i64) -> Result<()> {
|
||||
let resp = self
|
||||
.get_request("favorite/delete")
|
||||
.query(&[("type", "tracks"), ("track_ids", &track_id.to_string())])
|
||||
.send()
|
||||
.await?;
|
||||
Self::check_response(resp).await?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub async fn add_fav_album(&self, album_id: &str) -> Result<()> {
|
||||
let resp = self
|
||||
.get_request("favorite/create")
|
||||
.query(&[("type", "albums"), ("album_ids", album_id)])
|
||||
.send()
|
||||
.await?;
|
||||
Self::check_response(resp).await?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub async fn remove_fav_album(&self, album_id: &str) -> Result<()> {
|
||||
let resp = self
|
||||
.get_request("favorite/delete")
|
||||
.query(&[("type", "albums"), ("album_ids", album_id)])
|
||||
.send()
|
||||
.await?;
|
||||
Self::check_response(resp).await?;
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
5
rust/src/api/mod.rs
Normal file
5
rust/src/api/mod.rs
Normal file
@@ -0,0 +1,5 @@
|
||||
pub mod client;
|
||||
pub mod models;
|
||||
|
||||
pub use client::QobuzClient;
|
||||
pub use models::*;
|
||||
260
rust/src/api/models.rs
Normal file
260
rust/src/api/models.rs
Normal file
@@ -0,0 +1,260 @@
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
// --- Auth ---
|
||||
|
||||
#[derive(Debug, Deserialize, Clone, Serialize)]
|
||||
pub struct OAuthDto {
|
||||
pub token_type: Option<String>,
|
||||
pub access_token: Option<String>,
|
||||
pub refresh_token: Option<String>,
|
||||
pub expires_in: Option<i64>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Deserialize, Serialize)]
|
||||
pub struct OAuthLoginResponse {
|
||||
pub status: Option<String>,
|
||||
pub user: Option<UserDto>,
|
||||
pub oauth2: Option<OAuthDto>,
|
||||
pub user_auth_token: Option<String>,
|
||||
}
|
||||
|
||||
// --- User ---
|
||||
|
||||
#[derive(Debug, Deserialize, Clone, Serialize)]
|
||||
pub struct UserDto {
|
||||
pub id: Option<i64>,
|
||||
pub login: Option<String>,
|
||||
pub firstname: Option<String>,
|
||||
pub lastname: Option<String>,
|
||||
pub email: Option<String>,
|
||||
pub display_name: Option<String>,
|
||||
pub country_code: Option<String>,
|
||||
pub subscription: Option<SubscriptionDto>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Deserialize, Clone, Serialize)]
|
||||
pub struct SubscriptionDto {
|
||||
pub description: Option<String>,
|
||||
pub end_date: Option<String>,
|
||||
pub is_recurring: Option<bool>,
|
||||
pub offer: Option<String>,
|
||||
}
|
||||
|
||||
// --- Track ---
|
||||
|
||||
#[derive(Debug, Deserialize, Clone, Serialize)]
|
||||
pub struct TrackDto {
|
||||
pub id: i64,
|
||||
pub title: Option<String>,
|
||||
pub duration: Option<i64>,
|
||||
pub track_number: Option<i32>,
|
||||
pub album: Option<AlbumDto>,
|
||||
pub performer: Option<ArtistDto>,
|
||||
pub composer: Option<ArtistDto>,
|
||||
pub work: Option<String>,
|
||||
pub media_number: Option<i32>,
|
||||
pub streamable: Option<bool>,
|
||||
pub purchasable: Option<bool>,
|
||||
pub hires: Option<bool>,
|
||||
pub hires_streamable: Option<bool>,
|
||||
pub audio_info: Option<AudioInfoDto>,
|
||||
pub maximum_bit_depth: Option<i32>,
|
||||
pub maximum_sampling_rate: Option<f64>,
|
||||
pub maximum_channel_count: Option<i32>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Deserialize, Clone, Serialize)]
|
||||
pub struct AudioInfoDto {
|
||||
pub replaygain_track_gain: Option<f64>,
|
||||
pub replaygain_track_peak: Option<f64>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Deserialize, Clone, Serialize)]
|
||||
pub struct TrackFileUrlDto {
|
||||
pub track_id: Option<i64>,
|
||||
pub duration: Option<i64>,
|
||||
pub url: Option<String>,
|
||||
pub format_id: Option<i32>,
|
||||
pub mime_type: Option<String>,
|
||||
pub sampling_rate: Option<f64>,
|
||||
pub bit_depth: Option<i32>,
|
||||
}
|
||||
|
||||
// --- Album ---
|
||||
|
||||
#[derive(Debug, Deserialize, Clone, Serialize)]
|
||||
pub struct AlbumDto {
|
||||
pub id: Option<String>,
|
||||
pub title: Option<String>,
|
||||
pub artist: Option<ArtistDto>,
|
||||
pub tracks_count: Option<i32>,
|
||||
pub duration: Option<i64>,
|
||||
pub genre: Option<GenreDto>,
|
||||
pub image: Option<ImageDto>,
|
||||
pub label: Option<LabelDto>,
|
||||
pub release_date_original: Option<String>,
|
||||
pub maximum_bit_depth: Option<i32>,
|
||||
pub maximum_sampling_rate: Option<f64>,
|
||||
pub hires_streamable: Option<bool>,
|
||||
pub streamable: Option<bool>,
|
||||
pub tracks: Option<TracksWrapper>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Deserialize, Clone, Serialize)]
|
||||
pub struct TracksWrapper {
|
||||
pub items: Option<Vec<TrackDto>>,
|
||||
pub total: Option<i32>,
|
||||
pub offset: Option<i32>,
|
||||
pub limit: Option<i32>,
|
||||
}
|
||||
|
||||
// --- Artist ---
|
||||
|
||||
#[derive(Debug, Deserialize, Clone, Serialize)]
|
||||
pub struct ArtistDto {
|
||||
pub id: Option<i64>,
|
||||
pub name: Option<String>,
|
||||
pub albums_count: Option<i32>,
|
||||
pub image: Option<ImageDto>,
|
||||
pub biography: Option<BiographyDto>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Deserialize, Clone, Serialize)]
|
||||
pub struct BiographyDto {
|
||||
pub content: Option<String>,
|
||||
pub summary: Option<String>,
|
||||
}
|
||||
|
||||
// --- Genre ---
|
||||
|
||||
#[derive(Debug, Deserialize, Clone, Serialize)]
|
||||
pub struct GenreDto {
|
||||
pub id: Option<i64>,
|
||||
pub name: Option<String>,
|
||||
pub slug: Option<String>,
|
||||
}
|
||||
|
||||
// --- Image ---
|
||||
|
||||
#[derive(Debug, Deserialize, Clone, Serialize)]
|
||||
pub struct ImageDto {
|
||||
pub small: Option<String>,
|
||||
pub thumbnail: Option<String>,
|
||||
pub large: Option<String>,
|
||||
pub back: Option<String>,
|
||||
}
|
||||
|
||||
// --- Label ---
|
||||
|
||||
#[derive(Debug, Deserialize, Clone, Serialize)]
|
||||
pub struct LabelDto {
|
||||
pub id: Option<i64>,
|
||||
pub name: Option<String>,
|
||||
}
|
||||
|
||||
// --- Search ---
|
||||
|
||||
#[derive(Debug, Deserialize, Serialize)]
|
||||
pub struct SearchCatalogDto {
|
||||
pub query: Option<String>,
|
||||
pub albums: Option<SearchResultItems<AlbumDto>>,
|
||||
pub tracks: Option<SearchResultItems<TrackDto>>,
|
||||
pub artists: Option<SearchResultItems<ArtistDto>>,
|
||||
pub playlists: Option<SearchResultItems<PlaylistDto>>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Deserialize, Clone, Serialize)]
|
||||
pub struct SearchResultItems<T> {
|
||||
pub items: Option<Vec<T>>,
|
||||
pub total: Option<i32>,
|
||||
pub offset: Option<i32>,
|
||||
pub limit: Option<i32>,
|
||||
}
|
||||
|
||||
// --- Playlist ---
|
||||
|
||||
#[derive(Debug, Deserialize, Clone, Serialize)]
|
||||
pub struct PlaylistDto {
|
||||
pub id: Option<i64>,
|
||||
pub name: Option<String>,
|
||||
pub tracks_count: Option<i32>,
|
||||
pub duration: Option<i64>,
|
||||
pub description: Option<String>,
|
||||
pub owner: Option<PlaylistOwnerDto>,
|
||||
pub image_rectangle: Option<Vec<String>>,
|
||||
pub tracks: Option<TracksWrapper>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Deserialize, Clone, Serialize)]
|
||||
pub struct PlaylistOwnerDto {
|
||||
pub id: Option<i64>,
|
||||
pub name: Option<String>,
|
||||
}
|
||||
|
||||
// --- User library ---
|
||||
|
||||
#[derive(Debug, Deserialize, Serialize)]
|
||||
pub struct UserPlaylistsDto {
|
||||
pub playlists: Option<SearchResultItems<PlaylistDto>>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Deserialize, Serialize)]
|
||||
pub struct FavArtistDto {
|
||||
pub id: Option<i64>,
|
||||
pub name: Option<String>,
|
||||
pub albums_count: Option<i32>,
|
||||
pub image: Option<ImageDto>,
|
||||
}
|
||||
|
||||
// --- Format ---
|
||||
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
|
||||
pub enum Format {
|
||||
Mp3 = 5,
|
||||
Cd = 6,
|
||||
HiRes96 = 7,
|
||||
HiRes192 = 27,
|
||||
}
|
||||
|
||||
impl Format {
|
||||
pub fn id(self) -> i32 {
|
||||
self as i32
|
||||
}
|
||||
|
||||
pub fn from_id(id: i32) -> Self {
|
||||
match id {
|
||||
5 => Format::Mp3,
|
||||
6 => Format::Cd,
|
||||
7 => Format::HiRes96,
|
||||
27 => Format::HiRes192,
|
||||
_ => Format::Cd,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn label(self) -> &'static str {
|
||||
match self {
|
||||
Format::Mp3 => "MP3 320",
|
||||
Format::Cd => "CD 16-bit",
|
||||
Format::HiRes96 => "Hi-Res 24-bit/96kHz",
|
||||
Format::HiRes192 => "Hi-Res 24-bit/192kHz",
|
||||
}
|
||||
}
|
||||
|
||||
pub fn all() -> &'static [Format] {
|
||||
&[Format::HiRes192, Format::HiRes96, Format::Cd, Format::Mp3]
|
||||
}
|
||||
}
|
||||
|
||||
// --- QWS ---
|
||||
|
||||
#[derive(Debug, Deserialize, Clone, Serialize)]
|
||||
pub struct QwsTokenResponse {
|
||||
pub jwt_qws: Option<QwsToken>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Deserialize, Clone, Serialize)]
|
||||
pub struct QwsToken {
|
||||
pub exp: Option<i64>,
|
||||
pub jwt: Option<String>,
|
||||
pub endpoint: Option<String>,
|
||||
}
|
||||
Reference in New Issue
Block a user