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