From d6406d1e093df31a1df1beaa13ba3d46fdebf6f3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Hans=20M=C3=BCndelein?= Date: Sat, 17 Jun 2023 13:16:19 +0200 Subject: [PATCH] Add users artists album support --- crabidy-server/src/main.rs | 7 +- crabidy-server/src/provider.rs | 6 +- crabidy-server/src/rpc.rs | 10 +- tidaldy/src/lib.rs | 267 +++++++++++++++++++++-------- tidaldy/src/models.rs | 305 +++++++++++++++++++++++++++------ 5 files changed, 463 insertions(+), 132 deletions(-) diff --git a/crabidy-server/src/main.rs b/crabidy-server/src/main.rs index 0fc163d..8997d38 100644 --- a/crabidy-server/src/main.rs +++ b/crabidy-server/src/main.rs @@ -3,7 +3,7 @@ use crabidy_core::proto::crabidy::{ crabidy_service_server::CrabidyServiceServer, InitResponse, LibraryNode, PlayState, Track, }; use crabidy_core::{ProviderClient, ProviderError}; -use tracing::{debug_span, error, info, instrument, warn, Span}; +use tracing::{debug_span, error, info, instrument, level_filters, warn, Span}; use tracing_subscriber::{filter::Targets, prelude::*}; mod playback; @@ -22,8 +22,9 @@ async fn main() -> Result<(), Box> { } let (non_blocking, _guard) = tracing_appender::non_blocking(std::io::stderr()); - let targets_filter = - Targets::new().with_target("crabidy_server", tracing::level_filters::LevelFilter::DEBUG); + let targets_filter = Targets::new() + .with_target("crabidy_server", tracing::level_filters::LevelFilter::DEBUG) + .with_target("tidaldy", level_filters::LevelFilter::DEBUG); let subscriber = tracing_subscriber::fmt::layer() .with_writer(non_blocking) .with_file(true) diff --git a/crabidy-server/src/provider.rs b/crabidy-server/src/provider.rs index f0e4e72..4938004 100644 --- a/crabidy-server/src/provider.rs +++ b/crabidy-server/src/provider.rs @@ -148,6 +148,7 @@ impl ProviderClient for ProviderOrchestrator { } #[instrument(skip(self))] fn get_lib_root(&self) -> LibraryNode { + debug!("get_lib_root in provider manager"); let mut root_node = LibraryNode::new(); let child = LibraryNodeChild::new("node:tidal".to_owned(), "tidal".to_owned(), false); root_node.children.push(child); @@ -155,13 +156,16 @@ impl ProviderClient for ProviderOrchestrator { } #[instrument(skip(self))] async fn get_lib_node(&self, uuid: &str) -> Result { - debug!("get_lib_node"); + debug!("get_lib_node in provider manager"); if uuid == "node:/" { + debug!("get global root"); return Ok(self.get_lib_root()); } if uuid == "node:tidal" { + debug!("get tidal root"); return Ok(self.tidal_client.get_lib_root()); } + debug!("tidal node"); self.tidal_client.get_lib_node(uuid).in_current_span().await } } diff --git a/crabidy-server/src/rpc.rs b/crabidy-server/src/rpc.rs index 2623f10..61bb81d 100644 --- a/crabidy-server/src/rpc.rs +++ b/crabidy-server/src/rpc.rs @@ -91,10 +91,16 @@ impl CrabidyService for RpcService { .recv_async() .in_current_span() .await - .map_err(|_| Status::internal("Failed to receive response from provider channel"))?; + .map_err(|e| { + error!("{:?}", e); + Status::internal("Failed to receive response from provider channel") + })?; match result { Ok(node) => Ok(Response::new(GetLibraryNodeResponse { node: Some(node) })), - Err(err) => Err(Status::internal(err.to_string())), + Err(err) => { + error!("{:?}", err); + Err(Status::internal(err.to_string())) + } } } diff --git a/tidaldy/src/lib.rs b/tidaldy/src/lib.rs index 57c753a..e6238be 100644 --- a/tidaldy/src/lib.rs +++ b/tidaldy/src/lib.rs @@ -1,9 +1,11 @@ +use std::fmt::format; + /// Lots of stuff and especially the auth handling is shamelessly copied from /// https://github.com/MinisculeGirraffe/tdl use reqwest::Client as HttpClient; use serde::de::DeserializeOwned; use tokio::time::{sleep, Duration, Instant}; -use tracing::{debug, instrument}; +use tracing::{debug, error, info, instrument}; pub mod config; pub mod models; use async_trait::async_trait; @@ -75,13 +77,20 @@ impl crabidy_core::ProviderClient for Client { #[instrument(skip(self))] fn get_lib_root(&self) -> crabidy_core::proto::crabidy::LibraryNode { - debug!("get_lib_root"); + debug!("get_lib_root in tidaldy"); let global_root = crabidy_core::proto::crabidy::LibraryNode::new(); - let children = vec![crabidy_core::proto::crabidy::LibraryNodeChild::new( - "node:userplaylists".to_string(), - "playlists".to_string(), - false, - )]; + let children = vec![ + crabidy_core::proto::crabidy::LibraryNodeChild::new( + "node:userplaylists".to_string(), + "playlists".to_string(), + false, + ), + crabidy_core::proto::crabidy::LibraryNodeChild::new( + "node:userartists".to_string(), + "artists".to_string(), + false, + ), + ]; crabidy_core::proto::crabidy::LibraryNode { uuid: "node:tidal".to_string(), title: "tidal".to_string(), @@ -100,8 +109,9 @@ impl crabidy_core::ProviderClient for Client { let Some(user_id) = self.settings.login.user_id.clone() else { return Err(crabidy_core::ProviderError::UnknownUser) }; - debug!("get_lib_node {}", uuid); + debug!("get_lib_node in tidaldy{}", uuid); let (_kind, module, uuid) = split_uuid(uuid); + error!("module:{},uuid: {}", module, uuid); let node = match module.as_str() { "userplaylists" => { let mut node = crabidy_core::proto::crabidy::LibraryNode { @@ -138,6 +148,53 @@ impl crabidy_core::ProviderClient for Client { node.parent = Some("node:userplaylists".to_string()); node } + "userartists" => { + let mut node = crabidy_core::proto::crabidy::LibraryNode { + uuid: "node:userartists".to_string(), + title: "artists".to_string(), + parent: Some("node:tidal".to_string()), + tracks: Vec::new(), + children: Vec::new(), + is_queable: false, + }; + for artist in self.get_users_artists(&user_id).await? { + let child = crabidy_core::proto::crabidy::LibraryNodeChild::new( + format!("node:artist:{}", artist.item.id), + artist.item.name, + true, + ); + node.children.push(child); + } + node + } + "artist" => { + info!("artist"); + let mut node: crabidy_core::proto::crabidy::LibraryNode = + self.get_artist(&uuid).await?.into(); + let children: Vec = self + .get_artist_albums(&uuid) + .await? + .iter() + .map(|t| t.into()) + .collect(); + node.children = children; + node.parent = Some("node:userartists".to_string()); + node + } + "album" => { + let album = self.get_album(&uuid).await?; + let artis_id = album.artist.clone().unwrap().id; + let mut node: crabidy_core::proto::crabidy::LibraryNode = album.into(); + let tracks: Vec = self + .get_album_tracks(&uuid) + .await? + .iter() + .map(|t| t.into()) + .collect(); + node.tracks = tracks; + node.parent = Some(format!("node:artist:{}", artis_id)); + node + } _ => return Err(crabidy_core::ProviderError::MalformedUuid), }; Ok(node) @@ -166,17 +223,18 @@ impl Client { }) } - #[instrument] + #[instrument(skip(self))] pub fn get_user_id(&self) -> Option { self.settings.login.user_id.clone() } - #[instrument] + #[instrument(skip(self))] pub async fn make_request( &self, uri: &str, query: Option<&[(&str, String)]>, ) -> Result { + debug!("make_request {}", uri); let Some(ref access_token) = self.settings.login.access_token.clone() else { return Err(ClientError::AuthError( "No access token found".to_string(), @@ -199,18 +257,27 @@ impl Client { .bearer_auth(access_token) .query(¶ms) .send() - .await? + .await + .map_err(|e| { + error!("{:?}", e); + e + })? .json() - .await?; + .await + .map_err(|e| { + error!("{:?}", e); + e + })?; Ok(response) } - #[instrument] + #[instrument(skip(self))] pub async fn make_paginated_request( &self, uri: &str, query: Option<&[(&str, String)]>, ) -> Result, ClientError> { + debug!("make_paginated_request {}", uri); let Some(ref access_token) = self.settings.login.access_token.clone() else { return Err(ClientError::AuthError( "No access token found".to_string(), @@ -236,9 +303,17 @@ impl Client { .bearer_auth(access_token) .query(¶ms) .send() - .await? + .await + .map_err(|e| { + error!("{:?}", e); + e + })? .json() - .await?; + .await + .map_err(|e| { + error!("{:?}", e); + e + })?; let mut items = Vec::with_capacity(response.total_number_of_items); items.extend(response.items); while response.offset + limit < response.total_number_of_items { @@ -255,15 +330,23 @@ impl Client { .bearer_auth(access_token) .query(¶ms) .send() - .await? + .await + .map_err(|e| { + error!("{:?}", e); + e + })? .json() - .await?; + .await + .map_err(|e| { + error!("{:?}", e); + e + })?; items.extend(response.items); } Ok(items) } - #[instrument] + #[instrument(skip(self))] pub async fn make_explorer_request( &self, uri: &str, @@ -291,14 +374,22 @@ impl Client { .bearer_auth(access_token) .query(¶ms) .send() - .await? + .await + .map_err(|e| { + error!("{:?}", e); + e + })? .text() - .await?; + .await + .map_err(|e| { + error!("{:?}", e); + e + })?; println!("{:?}", response); Ok(()) } - #[instrument] + #[instrument(skip(self))] pub async fn search(&self, query: &str) -> Result<(), ClientError> { let query = vec![("query", query.to_string())]; self.make_explorer_request(&format!("search/artists"), Some(&query)) @@ -306,7 +397,7 @@ impl Client { Ok(()) } - #[instrument] + #[instrument(skip(self))] pub async fn get_playlist_tracks( &self, playlist_uuid: &str, @@ -316,21 +407,35 @@ impl Client { .await?) } - #[instrument] + #[instrument(skip(self))] pub async fn get_playlist(&self, playlist_uuid: &str) -> Result { Ok(self .make_request(&format!("playlists/{}", playlist_uuid), None) .await?) } - #[instrument] + #[instrument(skip(self))] + pub async fn get_artist(&self, artist_uuid: &str) -> Result { + Ok(self + .make_request(&format!("artists/{}", artist_uuid), None) + .await?) + } + + #[instrument(skip(self))] + pub async fn get_artist_albums(&self, artist_uuid: &str) -> Result, ClientError> { + Ok(self + .make_paginated_request(&format!("artists/{}/albums", artist_uuid), None) + .await?) + } + + #[instrument(skip(self))] pub async fn get_users_playlists(&self, user_id: u64) -> Result, ClientError> { Ok(self .make_paginated_request(&format!("users/{}/playlists", user_id), None) .await?) } - #[instrument] + #[instrument(skip(self))] pub async fn get_users_playlists_and_favorite_playlists( &self, user_id: &str, @@ -343,25 +448,7 @@ impl Client { .await?) } - #[instrument] - pub async fn explore_get_users_playlists_and_favorite_playlists( - &self, - user_id: u64, - ) -> Result<(), ClientError> { - let limit = 50; - let offset = 0; - let limit_param = ("limit", limit.to_string()); - let offset_param = ("offset", offset.to_string()); - let params: Vec<(&str, String)> = vec![limit_param, offset_param]; - self.make_explorer_request( - &format!("users/{}/playlistsAndFavoritePlaylists", user_id), - Some(¶ms[..]), - ) - .await?; - Ok(()) - } - - #[instrument] + #[instrument(skip(self))] pub async fn get_users_favorites(&self, user_id: u64) -> Result<(), ClientError> { self.make_explorer_request( &format!("users/{}/favorites", user_id), @@ -372,7 +459,18 @@ impl Client { Ok(()) } - #[instrument] + #[instrument(skip(self))] + pub async fn get_users_artists(&self, user_id: &str) -> Result, ClientError> { + Ok(self + .make_paginated_request( + &format!("users/{}/favorites/artists", user_id), + None, + // Some(&query), + ) + .await?) + } + + #[instrument(skip(self))] pub async fn get_user(&self, user_id: u64) -> Result<(), ClientError> { self.make_explorer_request( &format!("users/{}", user_id), @@ -383,7 +481,19 @@ impl Client { Ok(()) } - #[instrument] + #[instrument(skip(self))] + pub async fn get_album(&self, album_id: &str) -> Result { + self.make_request(&format!("albums/{}/", album_id), None) + .await + } + + #[instrument(skip(self))] + pub async fn get_album_tracks(&self, album_id: &str) -> Result, ClientError> { + self.make_paginated_request(&format!("albums/{}/tracks", album_id), None) + .await + } + + #[instrument(skip(self))] pub async fn get_track_playback(&self, track_id: &str) -> Result { let query = vec![ ("audioquality", "LOSSLESS".to_string()), @@ -397,14 +507,14 @@ impl Client { .await } - #[instrument] + #[instrument(skip(self))] pub async fn get_track(&self, track_id: &str) -> Result { let (_, track_id, _) = split_uuid(track_id); self.make_request(&format!("tracks/{}", track_id), None) .await } - #[instrument] + #[instrument(skip(self))] pub async fn login_web(&mut self) -> Result<(), ClientError> { let code_response = self.get_device_code().await?; let now = Instant::now(); @@ -443,7 +553,11 @@ impl Client { .get(format!("{}/sessions", self.settings.base_url)) .bearer_auth(access_token) .send() - .await? + .await + .map_err(|e| { + error!("{:?}", e); + e + })? .status() .is_success() { @@ -459,7 +573,7 @@ impl Client { Ok(()) } - #[instrument] + #[instrument(skip(self))] pub async fn refresh_access_token(&self) -> Result { let Some(refresh_token) = self.settings.login.refresh_token.clone() else { return Err(ClientError::AuthError( @@ -485,7 +599,11 @@ impl Client { ) .header("Content-Type", "application/x-www-form-urlencoded") .send() - .await?; + .await + .map_err(|e| { + error!("{:?}", e); + e + })?; if req.status().is_success() { let res = req.json::().await?; Ok(res) @@ -495,7 +613,7 @@ impl Client { )) } } - #[instrument] + #[instrument(skip(self))] async fn get_device_code(&self) -> Result { let req = DeviceAuthRequest { client_id: self.settings.oauth.client_id.clone(), @@ -512,7 +630,11 @@ impl Client { .header("Content-Type", "application/x-www-form-urlencoded") .body(payload) .send() - .await?; + .await + .map_err(|e| { + error!("{:?}", e); + e + })?; if !res.status().is_success() { return Err(ClientError::AuthError(res.status().to_string())); @@ -521,7 +643,7 @@ impl Client { Ok(code) } - #[instrument] + #[instrument(skip(self))] pub async fn check_auth_status( &self, device_code: &str, @@ -544,7 +666,11 @@ impl Client { .body(payload) .header("Content-Type", "application/x-www-form-urlencoded") .send() - .await?; + .await + .map_err(|e| { + error!("{:?}", e); + e + })?; if !res.status().is_success() { if res.status().is_client_error() { return Err(ClientError::AuthError(format!( @@ -564,25 +690,26 @@ impl Client { #[cfg(test)] mod tests { + use crabidy_core::ProviderClient; + use super::*; - fn setup() -> Client { - let settings = crate::config::Settings::default(); - Client::new(settings).expect("could not create tidaldy client") + async fn setup() -> Client { + let raw_toml_settings = + std::fs::read_to_string("/home/hans/.config/crabidy/tidaldy.toml").unwrap(); + Client::init(&raw_toml_settings).await.unwrap() } #[tokio::test] - async fn test_get_device_code() { - let client = setup(); - println!("{:#?}", client); - let response = client.get_device_code().await.unwrap(); - assert!(!response.device_code.is_empty()); - assert_eq!(response.device_code.len(), 36); - assert!(!response.user_code.is_empty()); - assert_eq!(response.user_code.len(), 5); - assert!(!response.verification_uri.is_empty()); - assert!(!response.verification_uri_complete.is_empty()); - assert!(response.expires_in == 300); - assert!(response.interval != 0); + async fn test() { + let client = setup().await; + let user = client.settings.login.user_id.clone().unwrap(); + let result = client.get_users_artists(&user).await.unwrap(); + println!("{:?}", result); + let result = client.get_artist("5293333").await.unwrap(); + println!("{:?}", result); + let result = client.get_album("244167550").await.unwrap(); + println!("{:?}", result); + assert!(false); } } diff --git a/tidaldy/src/models.rs b/tidaldy/src/models.rs index 258f81a..4b493b3 100644 --- a/tidaldy/src/models.rs +++ b/tidaldy/src/models.rs @@ -1,5 +1,6 @@ use std::{str::FromStr, string::FromUtf8Error}; +use crabidy_core::proto::crabidy::{LibraryNode, LibraryNodeChild}; use serde::{Deserialize, Serialize}; use serde_json::Value; use thiserror::Error; @@ -15,15 +16,58 @@ pub struct Page { #[derive(Default, Debug, Clone, PartialEq, Serialize, Deserialize)] #[serde(rename_all = "camelCase")] -pub struct Item { +pub struct ArtistItem { + pub created: String, + pub item: Artist, +} + +impl From for LibraryNode { + fn from(item: ArtistItem) -> Self { + Self { + uuid: format!("artist:{}", item.item.id), + title: item.item.name, + children: Vec::new(), + parent: None, + tracks: Vec::new(), + is_queable: true, + } + } +} + +#[derive(Default, Debug, Clone, PartialEq, Serialize, Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct Artist { pub id: i64, pub name: String, - pub artist_types: Vec, - pub url: String, - pub picture: Value, - pub popularity: i64, - pub artist_roles: Vec, - pub mixes: Mixes, + pub artist_types: Option>, + pub url: Option, + pub picture: Option, + pub popularity: Option, + pub artist_roles: Option>, + pub mixes: Option, +} + +impl From for LibraryNode { + fn from(artist: Artist) -> Self { + Self { + uuid: format!("node:artist:{}", artist.id), + title: artist.name, + children: Vec::new(), + parent: None, + tracks: Vec::new(), + is_queable: true, + } + } +} + +impl From for LibraryNodeChild { + fn from(artist: Artist) -> Self { + Self { + uuid: format!("node:artist:{}", artist.id), + title: artist.name, + is_queable: true, + } + } } #[derive(Default, Debug, Clone, PartialEq, Serialize, Deserialize)] @@ -126,43 +170,76 @@ impl TrackPlayback { } } +// #[derive(Default, Debug, Clone, PartialEq, Serialize, Deserialize)] +// #[serde(rename_all = "camelCase")] +// pub struct Track { +// pub id: u64, +// pub title: String, +// pub duration: u64, +// pub replay_gain: f64, +// pub peak: f64, +// pub allow_streaming: bool, +// pub stream_ready: bool, +// pub stream_start_date: Option, +// pub premium_streaming_only: bool, +// pub track_number: u64, +// pub volume_number: u64, +// pub version: Value, +// pub popularity: u64, +// pub copyright: Option, +// pub url: Option, +// pub isrc: Option, +// pub editable: bool, +// pub explicit: bool, +// pub audio_quality: String, +// pub audio_modes: Vec, +// pub artist: Artist, +// pub artists: Vec, +// pub album: Album, +// pub mixes: TrackMixes, +// } + #[derive(Default, Debug, Clone, PartialEq, Serialize, Deserialize)] #[serde(rename_all = "camelCase")] pub struct Track { - pub id: u64, + pub id: i64, pub title: String, - pub duration: u64, - pub replay_gain: f64, - pub peak: f64, - pub allow_streaming: bool, - pub stream_ready: bool, + pub duration: Option, + pub replay_gain: Option, + pub peak: Option, + pub allow_streaming: Option, + pub stream_ready: Option, + pub ad_supported_stream_ready: Option, pub stream_start_date: Option, - pub premium_streaming_only: bool, - pub track_number: u64, - pub volume_number: u64, - pub version: Value, - pub popularity: u64, + pub premium_streaming_only: Option, + pub track_number: Option, + pub volume_number: Option, + pub version: Option, + pub popularity: Option, pub copyright: Option, pub url: Option, pub isrc: Option, - pub editable: bool, - pub explicit: bool, - pub audio_quality: String, - pub audio_modes: Vec, - pub artist: Artist, - pub artists: Vec, - pub album: Album, - pub mixes: Mixes, + pub editable: Option, + pub explicit: Option, + pub audio_quality: Option, + pub audio_modes: Option>, + pub media_metadata: Option, + pub artist: Option, + pub artists: Option>, + pub album: Option, + pub mixes: Option, } - impl From for crabidy_core::proto::crabidy::Track { fn from(track: Track) -> Self { Self { uuid: format!("track:{}", track.id), title: track.title, - artist: track.artist.name, - album: Some(track.album.into()), - duration: Some(track.duration as u32 * 1000), + artist: match track.artist { + Some(a) => a.name.clone(), + None => "".to_string(), + }, + album: track.album.map(|a| a.into()), + duration: track.duration.map(|d| d as u32 * 1000), } } } @@ -172,42 +249,143 @@ impl From<&Track> for crabidy_core::proto::crabidy::Track { Self { uuid: format!("track:{}", track.id), title: track.title.clone(), - artist: track.artist.name.clone(), - album: Some(track.album.clone().into()), - duration: Some(track.duration as u32), + artist: match track.artist.as_ref() { + Some(a) => a.name.clone(), + None => "".to_string(), + }, + album: track.album.clone().map(|a| a.into()), + duration: track.duration.map(|d| d as u32 * 1000), } } } -#[derive(Default, Debug, Clone, PartialEq, Serialize, Deserialize)] -#[serde(rename_all = "camelCase")] -pub struct Artist { - pub id: i64, - pub name: String, - #[serde(rename = "type")] - pub type_field: String, - pub picture: Value, -} +// #[derive(Default, Debug, Clone, PartialEq, Serialize, Deserialize)] +// #[serde(rename_all = "camelCase")] +// pub struct Artist { +// pub id: i64, +// pub name: String, +// #[serde(rename = "type")] +// pub type_field: String, +// pub picture: Value, +// } -#[derive(Default, Debug, Clone, PartialEq, Serialize, Deserialize)] -#[serde(rename_all = "camelCase")] -pub struct Artist2 { - pub id: i64, - pub name: String, - #[serde(rename = "type")] - pub type_field: String, - pub picture: Value, -} +// #[derive(Default, Debug, Clone, PartialEq, Serialize, Deserialize)] +// #[serde(rename_all = "camelCase")] +// pub struct Artist2 { +// pub id: i64, +// pub name: String, +// #[serde(rename = "type")] +// pub type_field: String, +// pub picture: Value, +// } +// #[derive(Default, Debug, Clone, PartialEq, Serialize, Deserialize)] +// #[serde(rename_all = "camelCase")] +// pub struct Album { +// pub id: i64, +// pub title: String, +// pub cover: String, +// pub vibrant_color: String, +// pub video_cover: Value, +// pub release_date: Option, +// } +// +// #[derive(Default, Debug, Clone, PartialEq, Serialize, Deserialize)] +// #[serde(rename_all = "camelCase")] +// pub struct Root { +// pub id: i64, +// pub title: String, +// pub duration: i64, +// pub stream_ready: bool, +// pub ad_supported_stream_ready: bool, +// pub stream_start_date: String, +// pub allow_streaming: bool, +// pub premium_streaming_only: bool, +// pub number_of_tracks: i64, +// pub number_of_videos: i64, +// pub number_of_volumes: i64, +// pub release_date: String, +// pub copyright: String, +// #[serde(rename = "type")] +// pub type_field: String, +// pub version: Value, +// pub url: String, +// pub cover: String, +// pub vibrant_color: String, +// pub video_cover: Value, +// pub explicit: bool, +// pub upc: String, +// pub popularity: i64, +// pub audio_quality: String, +// pub audio_modes: Vec, +// pub media_metadata: MediaMetadata, +// pub artist: Artist, +// pub artists: Vec, +// } #[derive(Default, Debug, Clone, PartialEq, Serialize, Deserialize)] #[serde(rename_all = "camelCase")] pub struct Album { pub id: i64, pub title: String, - pub cover: String, - pub vibrant_color: String, - pub video_cover: Value, + pub cover: Option, + pub vibrant_color: Option, pub release_date: Option, + pub duration: Option, + pub stream_ready: Option, + pub ad_supported_stream_ready: Option, + pub stream_start_date: Option, + pub allow_streaming: Option, + pub premium_streaming_only: Option, + pub number_of_tracks: Option, + pub number_of_videos: Option, + pub number_of_volumes: Option, + pub copyright: Option, + #[serde(rename = "type")] + pub type_field: Option, + pub version: Option, + pub url: Option, + pub video_cover: Option, + pub explicit: Option, + pub upc: Option, + pub popularity: Option, + pub audio_quality: Option, + pub audio_modes: Option>, + pub media_metadata: Option, + pub artist: Option, + pub artists: Option>, +} + +impl From for crabidy_core::proto::crabidy::LibraryNode { + fn from(album: Album) -> Self { + Self { + uuid: format!("node:album:{}", album.id), + title: album.title, + children: Vec::new(), + parent: None, + tracks: Vec::new(), + is_queable: true, + } + } +} + +impl From for crabidy_core::proto::crabidy::LibraryNodeChild { + fn from(album: Album) -> Self { + Self { + uuid: format!("node:album:{}", album.id), + title: album.title, + is_queable: true, + } + } +} + +impl From<&Album> for crabidy_core::proto::crabidy::LibraryNodeChild { + fn from(album: &Album) -> Self { + Self { + uuid: format!("node:album:{}", album.id), + title: album.title.clone(), + is_queable: true, + } + } } impl From for crabidy_core::proto::crabidy::Album { @@ -221,11 +399,26 @@ impl From for crabidy_core::proto::crabidy::Album { #[derive(Default, Debug, Clone, PartialEq, Serialize, Deserialize)] #[serde(rename_all = "camelCase")] -pub struct Mixes { +pub struct MediaMetadata { + pub tags: Vec, +} + +#[derive(Default, Debug, Clone, PartialEq, Serialize, Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct TrackMixes { #[serde(rename = "TRACK_MIX")] pub track_mix: Option, } +#[derive(Default, Debug, Clone, PartialEq, Serialize, Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct ArtistMixes { + #[serde(rename = "MASTER_ARTIST_MIX")] + pub master_artist_mix: Option, + #[serde(rename = "ARTIST_MIX")] + pub artist_mix: Option, +} + #[derive(Deserialize, Serialize, Debug)] #[serde(rename_all(deserialize = "camelCase"))] pub struct PlaybackManifest { @@ -359,7 +552,7 @@ pub struct PlaylistTrack { pub artist: Artist, pub artists: Vec, pub album: Album, - pub mixes: Mixes, + pub mixes: TrackMixes, pub date_added: String, pub index: i64, pub item_uuid: String,