Refactor provider addressing to filesystem-like paths
Identifiers like node:tidal / node:playlist:<id> / track:<id> are replaced by absolute, hierarchical paths that encode the position in the library tree: / global root /tidal provider root /tidal/playlists/<id> playlist (tracks inside) /tidal/playlists/<id>/<track> track /tidal/artists/<id>/<album> album /tidal/artists/<id>/<album>/<t> track - proto: uuid -> path, uuids -> paths (same field tags, wire compatible); crabidy-core gains ROOT_PATH, parent_path, join_path, path_segments helpers with unit tests - ProviderClient gains is_track_path; the orchestrator routes by path prefix and exposes a single ResolveTracks command (track path -> that track, node path -> flattened subtree), replacing the track:-prefix sniffing in the playback loop - tidaldy parses paths into a typed TidalPath enum; node parents are derived from the request path, which removes the album.artist unwrap() panic; the network-dependent scratch test is #[ignore]d - TUI navigates by paths Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
parent
6d8bc7f166
commit
56bc0b0d04
|
|
@ -17,7 +17,7 @@ use super::{
|
||||||
|
|
||||||
pub struct Library {
|
pub struct Library {
|
||||||
title: String,
|
title: String,
|
||||||
uuid: String,
|
path: String,
|
||||||
list: Vec<UiItem>,
|
list: Vec<UiItem>,
|
||||||
list_state: ListState,
|
list_state: ListState,
|
||||||
parent: Option<String>,
|
parent: Option<String>,
|
||||||
|
|
@ -29,7 +29,7 @@ impl Library {
|
||||||
pub fn new(tx: Sender<MessageFromUi>) -> Self {
|
pub fn new(tx: Sender<MessageFromUi>) -> Self {
|
||||||
Self {
|
Self {
|
||||||
title: "Library".to_string(),
|
title: "Library".to_string(),
|
||||||
uuid: "node:/".to_string(),
|
path: crabidy_core::ROOT_PATH.to_string(),
|
||||||
list: Vec::new(),
|
list: Vec::new(),
|
||||||
list_state: ListState::default(),
|
list_state: ListState::default(),
|
||||||
positions: HashMap::new(),
|
positions: HashMap::new(),
|
||||||
|
|
@ -43,12 +43,12 @@ impl Library {
|
||||||
self.list
|
self.list
|
||||||
.iter()
|
.iter()
|
||||||
.filter(|i| i.marked)
|
.filter(|i| i.marked)
|
||||||
.map(|i| i.uuid.to_string())
|
.map(|i| i.path.to_string())
|
||||||
.collect(),
|
.collect(),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
if let Some(idx) = self.list_state.selected() {
|
if let Some(idx) = self.list_state.selected() {
|
||||||
return Some(vec![self.list[idx].uuid.to_string()]);
|
return Some(vec![self.list[idx].path.to_string()]);
|
||||||
}
|
}
|
||||||
None
|
None
|
||||||
}
|
}
|
||||||
|
|
@ -62,7 +62,7 @@ impl Library {
|
||||||
let item = &self.list[idx];
|
let item = &self.list[idx];
|
||||||
if let UiItemKind::Node = item.kind {
|
if let UiItemKind::Node = item.kind {
|
||||||
self.tx
|
self.tx
|
||||||
.send(MessageFromUi::GetLibraryNode(item.uuid.clone()));
|
.send(MessageFromUi::GetLibraryNode(item.path.clone()));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -99,7 +99,7 @@ impl Library {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
pub fn prev_selected(&self) -> usize {
|
pub fn prev_selected(&self) -> usize {
|
||||||
*self.positions.get(&self.uuid).unwrap_or(&0)
|
*self.positions.get(&self.path).unwrap_or(&0)
|
||||||
}
|
}
|
||||||
pub fn toggle_mark(&mut self) {
|
pub fn toggle_mark(&mut self) {
|
||||||
if let Some(idx) = self.list_state.selected() {
|
if let Some(idx) = self.list_state.selected() {
|
||||||
|
|
@ -124,7 +124,7 @@ impl Library {
|
||||||
}
|
}
|
||||||
|
|
||||||
// if children empty and tracks empty return
|
// if children empty and tracks empty return
|
||||||
self.uuid = node.uuid;
|
self.path = node.path;
|
||||||
self.title = node.title;
|
self.title = node.title;
|
||||||
self.parent = node.parent;
|
self.parent = node.parent;
|
||||||
self.select(Some(self.prev_selected()));
|
self.select(Some(self.prev_selected()));
|
||||||
|
|
@ -134,7 +134,7 @@ impl Library {
|
||||||
.tracks
|
.tracks
|
||||||
.iter()
|
.iter()
|
||||||
.map(|t| UiItem {
|
.map(|t| UiItem {
|
||||||
uuid: t.uuid.clone(),
|
path: t.path.clone(),
|
||||||
title: format!("{} - {}", t.artist, t.title),
|
title: format!("{} - {}", t.artist, t.title),
|
||||||
kind: UiItemKind::Track,
|
kind: UiItemKind::Track,
|
||||||
marked: false,
|
marked: false,
|
||||||
|
|
@ -147,7 +147,7 @@ impl Library {
|
||||||
.children
|
.children
|
||||||
.iter()
|
.iter()
|
||||||
.map(|c| UiItem {
|
.map(|c| UiItem {
|
||||||
uuid: c.uuid.clone(),
|
path: c.path.clone(),
|
||||||
title: c.title.clone(),
|
title: c.title.clone(),
|
||||||
kind: UiItemKind::Node,
|
kind: UiItemKind::Node,
|
||||||
marked: false,
|
marked: false,
|
||||||
|
|
@ -214,7 +214,7 @@ impl StatefulList for Library {
|
||||||
fn select(&mut self, idx: Option<usize>) {
|
fn select(&mut self, idx: Option<usize>) {
|
||||||
if let Some(pos) = idx {
|
if let Some(pos) = idx {
|
||||||
self.positions
|
self.positions
|
||||||
.entry(self.uuid.clone())
|
.entry(self.path.clone())
|
||||||
.and_modify(|e| *e = pos)
|
.and_modify(|e| *e = pos)
|
||||||
.or_insert(pos);
|
.or_insert(pos);
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -33,7 +33,7 @@ enum UiItemKind {
|
||||||
}
|
}
|
||||||
|
|
||||||
struct UiItem {
|
struct UiItem {
|
||||||
uuid: String,
|
path: String,
|
||||||
title: String,
|
title: String,
|
||||||
kind: UiItemKind,
|
kind: UiItemKind,
|
||||||
marked: bool,
|
marked: bool,
|
||||||
|
|
|
||||||
|
|
@ -59,7 +59,7 @@ impl Queue {
|
||||||
.iter()
|
.iter()
|
||||||
.enumerate()
|
.enumerate()
|
||||||
.map(|(i, t)| UiItem {
|
.map(|(i, t)| UiItem {
|
||||||
uuid: t.uuid.clone(),
|
path: t.path.clone(),
|
||||||
title: format!("{} - {}", t.artist, t.title),
|
title: format!("{} - {}", t.artist, t.title),
|
||||||
kind: UiItemKind::Track,
|
kind: UiItemKind::Track,
|
||||||
marked: false,
|
marked: false,
|
||||||
|
|
|
||||||
|
|
@ -89,7 +89,7 @@ async fn orchestrate<'a>(
|
||||||
info!(address = config.server.address, "connecting to server");
|
info!(address = config.server.address, "connecting to server");
|
||||||
let mut rpc_client = rpc::RpcClient::connect(&config.server.address).await?;
|
let mut rpc_client = rpc::RpcClient::connect(&config.server.address).await?;
|
||||||
|
|
||||||
if let Some(root_node) = rpc_client.get_library_node("node:/").await? {
|
if let Some(root_node) = rpc_client.get_library_node(crabidy_core::ROOT_PATH).await? {
|
||||||
tx.send(MessageToUi::ReplaceLibraryNode(root_node.clone()))?;
|
tx.send(MessageToUi::ReplaceLibraryNode(root_node.clone()))?;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -112,8 +112,8 @@ async fn poll(
|
||||||
select! {
|
select! {
|
||||||
Ok(msg) = &mut rx.recv_async() => {
|
Ok(msg) = &mut rx.recv_async() => {
|
||||||
match msg {
|
match msg {
|
||||||
MessageFromUi::GetLibraryNode(uuid) => {
|
MessageFromUi::GetLibraryNode(path) => {
|
||||||
if let Some(node) = rpc_client.get_library_node(&uuid).await? {
|
if let Some(node) = rpc_client.get_library_node(&path).await? {
|
||||||
tx.send(MessageToUi::ReplaceLibraryNode(node.clone()));
|
tx.send(MessageToUi::ReplaceLibraryNode(node.clone()));
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
|
||||||
|
|
@ -75,13 +75,13 @@ impl RpcClient {
|
||||||
|
|
||||||
pub async fn get_library_node(
|
pub async fn get_library_node(
|
||||||
&mut self,
|
&mut self,
|
||||||
uuid: &str,
|
path: &str,
|
||||||
) -> Result<Option<&LibraryNode>, Box<dyn Error>> {
|
) -> Result<Option<&LibraryNode>, Box<dyn Error>> {
|
||||||
if self.library_node_cache.contains_key(uuid) {
|
if self.library_node_cache.contains_key(path) {
|
||||||
return Ok(self.library_node_cache.get(uuid));
|
return Ok(self.library_node_cache.get(path));
|
||||||
}
|
}
|
||||||
let get_library_node_request = Request::new(GetLibraryNodeRequest {
|
let get_library_node_request = Request::new(GetLibraryNodeRequest {
|
||||||
uuid: uuid.to_string(),
|
path: path.to_string(),
|
||||||
});
|
});
|
||||||
let response = self
|
let response = self
|
||||||
.client
|
.client
|
||||||
|
|
@ -89,31 +89,31 @@ impl RpcClient {
|
||||||
.await?;
|
.await?;
|
||||||
if let Some(library_node) = response.into_inner().node {
|
if let Some(library_node) = response.into_inner().node {
|
||||||
self.library_node_cache
|
self.library_node_cache
|
||||||
.insert(uuid.to_string(), library_node);
|
.insert(path.to_string(), library_node);
|
||||||
return Ok(self.library_node_cache.get(uuid));
|
return Ok(self.library_node_cache.get(path));
|
||||||
}
|
}
|
||||||
Err(Box::new(RpcClientError::NotFound))
|
Err(Box::new(RpcClientError::NotFound))
|
||||||
}
|
}
|
||||||
|
|
||||||
pub async fn append_tracks(&mut self, uuids: Vec<String>) -> Result<(), Box<dyn Error>> {
|
pub async fn append_tracks(&mut self, paths: Vec<String>) -> Result<(), Box<dyn Error>> {
|
||||||
let append_request = Request::new(AppendRequest { uuids });
|
let append_request = Request::new(AppendRequest { paths });
|
||||||
self.client.append(append_request).await?;
|
self.client.append(append_request).await?;
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
pub async fn queue_tracks(&mut self, uuids: Vec<String>) -> Result<(), Box<dyn Error>> {
|
pub async fn queue_tracks(&mut self, paths: Vec<String>) -> Result<(), Box<dyn Error>> {
|
||||||
let queue_request = Request::new(QueueRequest { uuids });
|
let queue_request = Request::new(QueueRequest { paths });
|
||||||
self.client.queue(queue_request).await?;
|
self.client.queue(queue_request).await?;
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
pub async fn insert_tracks(
|
pub async fn insert_tracks(
|
||||||
&mut self,
|
&mut self,
|
||||||
uuids: Vec<String>,
|
paths: Vec<String>,
|
||||||
pos: usize,
|
pos: usize,
|
||||||
) -> Result<(), Box<dyn Error>> {
|
) -> Result<(), Box<dyn Error>> {
|
||||||
let insert_request = Request::new(InsertRequest {
|
let insert_request = Request::new(InsertRequest {
|
||||||
uuids,
|
paths,
|
||||||
position: pos as u32,
|
position: pos as u32,
|
||||||
});
|
});
|
||||||
self.client.insert(insert_request).await?;
|
self.client.insert(insert_request).await?;
|
||||||
|
|
@ -134,8 +134,8 @@ impl RpcClient {
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
pub async fn replace_queue(&mut self, uuids: Vec<String>) -> Result<(), Box<dyn Error>> {
|
pub async fn replace_queue(&mut self, paths: Vec<String>) -> Result<(), Box<dyn Error>> {
|
||||||
let replace_request = Request::new(ReplaceRequest { uuids });
|
let replace_request = Request::new(ReplaceRequest { paths });
|
||||||
self.client.replace(replace_request).await?;
|
self.client.replace(replace_request).await?;
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -45,7 +45,7 @@ message InitResponse {
|
||||||
|
|
||||||
// Library
|
// Library
|
||||||
message GetLibraryNodeRequest {
|
message GetLibraryNodeRequest {
|
||||||
string uuid = 1;
|
string path = 1;
|
||||||
}
|
}
|
||||||
message GetLibraryNodeResponse {
|
message GetLibraryNodeResponse {
|
||||||
LibraryNode node = 1;
|
LibraryNode node = 1;
|
||||||
|
|
@ -53,17 +53,17 @@ message GetLibraryNodeResponse {
|
||||||
|
|
||||||
// Queue
|
// Queue
|
||||||
message QueueRequest {
|
message QueueRequest {
|
||||||
repeated string uuids = 1;
|
repeated string paths = 1;
|
||||||
}
|
}
|
||||||
message QueueResponse {}
|
message QueueResponse {}
|
||||||
|
|
||||||
message ReplaceRequest {
|
message ReplaceRequest {
|
||||||
repeated string uuids = 1;
|
repeated string paths = 1;
|
||||||
}
|
}
|
||||||
message ReplaceResponse {}
|
message ReplaceResponse {}
|
||||||
|
|
||||||
message AppendRequest {
|
message AppendRequest {
|
||||||
repeated string uuids = 1;
|
repeated string paths = 1;
|
||||||
}
|
}
|
||||||
message AppendResponse {}
|
message AppendResponse {}
|
||||||
|
|
||||||
|
|
@ -74,7 +74,7 @@ message RemoveResponse {}
|
||||||
|
|
||||||
message InsertRequest {
|
message InsertRequest {
|
||||||
uint32 position = 1;
|
uint32 position = 1;
|
||||||
repeated string uuids = 2;
|
repeated string paths = 2;
|
||||||
}
|
}
|
||||||
message InsertResponse {}
|
message InsertResponse {}
|
||||||
|
|
||||||
|
|
@ -139,7 +139,7 @@ message RestartTrackResponse {}
|
||||||
|
|
||||||
// Data types
|
// Data types
|
||||||
message LibraryNodeChild {
|
message LibraryNodeChild {
|
||||||
string uuid = 1;
|
string path = 1;
|
||||||
string title = 2;
|
string title = 2;
|
||||||
bool is_queable = 3;
|
bool is_queable = 3;
|
||||||
}
|
}
|
||||||
|
|
@ -181,8 +181,8 @@ message Album {
|
||||||
}
|
}
|
||||||
|
|
||||||
message Track {
|
message Track {
|
||||||
// Including provider
|
// Full library path including provider
|
||||||
string uuid = 1;
|
string path = 1;
|
||||||
string artist = 2;
|
string artist = 2;
|
||||||
string title = 3;
|
string title = 3;
|
||||||
optional uint32 duration = 4;
|
optional uint32 duration = 4;
|
||||||
|
|
@ -190,8 +190,8 @@ message Track {
|
||||||
}
|
}
|
||||||
|
|
||||||
message LibraryNode {
|
message LibraryNode {
|
||||||
// Including provider
|
// Full library path including provider
|
||||||
string uuid = 1;
|
string path = 1;
|
||||||
string title = 2;
|
string title = 2;
|
||||||
repeated LibraryNodeChild children = 3;
|
repeated LibraryNodeChild children = 3;
|
||||||
optional string parent = 4;
|
optional string parent = 4;
|
||||||
|
|
|
||||||
|
|
@ -10,16 +10,24 @@ use proto::crabidy::{LibraryNode, LibraryNodeChild, Track};
|
||||||
|
|
||||||
pub mod proto;
|
pub mod proto;
|
||||||
|
|
||||||
|
/// A media provider addressed like a file system.
|
||||||
|
///
|
||||||
|
/// Every node and track has a `/`-separated absolute path whose first
|
||||||
|
/// segment names the provider, e.g. `/tidal/playlists/<id>/<track-id>`.
|
||||||
|
/// The path encodes the position in the library tree: ancestors are
|
||||||
|
/// obtained by trimming trailing segments.
|
||||||
#[async_trait]
|
#[async_trait]
|
||||||
pub trait ProviderClient: std::fmt::Debug + Send + Sync {
|
pub trait ProviderClient: std::fmt::Debug + Send + Sync {
|
||||||
async fn init(raw_toml_settings: &str) -> Result<Self, ProviderError>
|
async fn init(raw_toml_settings: &str) -> Result<Self, ProviderError>
|
||||||
where
|
where
|
||||||
Self: Sized;
|
Self: Sized;
|
||||||
fn settings(&self) -> String;
|
fn settings(&self) -> String;
|
||||||
async fn get_urls_for_track(&self, track_uuid: &str) -> Result<Vec<String>, ProviderError>;
|
/// Whether the path addresses a single track (as opposed to a node).
|
||||||
async fn get_metadata_for_track(&self, track_uuid: &str) -> Result<Track, ProviderError>;
|
fn is_track_path(&self, path: &str) -> bool;
|
||||||
|
async fn get_urls_for_track(&self, track_path: &str) -> Result<Vec<String>, ProviderError>;
|
||||||
|
async fn get_metadata_for_track(&self, track_path: &str) -> Result<Track, ProviderError>;
|
||||||
fn get_lib_root(&self) -> LibraryNode;
|
fn get_lib_root(&self) -> LibraryNode;
|
||||||
async fn get_lib_node(&self, list_uuid: &str) -> Result<LibraryNode, ProviderError>;
|
async fn get_lib_node(&self, path: &str) -> Result<LibraryNode, ProviderError>;
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Clone, Debug, Hash)]
|
#[derive(Clone, Debug, Hash)]
|
||||||
|
|
@ -28,7 +36,7 @@ pub enum ProviderError {
|
||||||
UnknownUser,
|
UnknownUser,
|
||||||
CouldNotLogin,
|
CouldNotLogin,
|
||||||
FetchError,
|
FetchError,
|
||||||
MalformedUuid,
|
MalformedPath,
|
||||||
InternalError,
|
InternalError,
|
||||||
Other,
|
Other,
|
||||||
}
|
}
|
||||||
|
|
@ -41,10 +49,39 @@ impl std::fmt::Display for ProviderError {
|
||||||
|
|
||||||
impl std::error::Error for ProviderError {}
|
impl std::error::Error for ProviderError {}
|
||||||
|
|
||||||
|
/// The path of the global library root.
|
||||||
|
pub const ROOT_PATH: &str = "/";
|
||||||
|
|
||||||
|
/// Returns the parent path, or `None` when at the root.
|
||||||
|
///
|
||||||
|
/// `/tidal/playlists/abc` -> `/tidal/playlists` -> `/tidal` -> `/`.
|
||||||
|
pub fn parent_path(path: &str) -> Option<&str> {
|
||||||
|
let trimmed = path.trim_end_matches('/');
|
||||||
|
if trimmed.is_empty() {
|
||||||
|
return None;
|
||||||
|
}
|
||||||
|
match trimmed.rfind('/') {
|
||||||
|
Some(0) => Some(ROOT_PATH),
|
||||||
|
Some(idx) => Some(&trimmed[..idx]),
|
||||||
|
None => None,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Appends a segment to a path.
|
||||||
|
pub fn join_path(base: &str, segment: &str) -> String {
|
||||||
|
let base = base.trim_end_matches('/');
|
||||||
|
format!("{base}/{segment}")
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Splits a path into its segments: `/tidal/playlists/x` -> ["tidal", "playlists", "x"].
|
||||||
|
pub fn path_segments(path: &str) -> Vec<&str> {
|
||||||
|
path.split('/').filter(|s| !s.is_empty()).collect()
|
||||||
|
}
|
||||||
|
|
||||||
impl LibraryNode {
|
impl LibraryNode {
|
||||||
pub fn new() -> Self {
|
pub fn new() -> Self {
|
||||||
Self {
|
Self {
|
||||||
uuid: "node:/".to_string(),
|
path: ROOT_PATH.to_string(),
|
||||||
title: "/".to_string(),
|
title: "/".to_string(),
|
||||||
children: Vec::new(),
|
children: Vec::new(),
|
||||||
parent: None,
|
parent: None,
|
||||||
|
|
@ -55,15 +92,46 @@ impl LibraryNode {
|
||||||
}
|
}
|
||||||
|
|
||||||
impl LibraryNodeChild {
|
impl LibraryNodeChild {
|
||||||
pub fn new(uuid: String, title: String, is_queable: bool) -> Self {
|
pub fn new(path: String, title: String, is_queable: bool) -> Self {
|
||||||
Self {
|
Self {
|
||||||
uuid,
|
path,
|
||||||
title,
|
title,
|
||||||
is_queable,
|
is_queable,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
mod tests {
|
||||||
|
use super::*;
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn parent_path_walks_up_to_root() {
|
||||||
|
assert_eq!(
|
||||||
|
parent_path("/tidal/playlists/abc"),
|
||||||
|
Some("/tidal/playlists")
|
||||||
|
);
|
||||||
|
assert_eq!(parent_path("/tidal/playlists"), Some("/tidal"));
|
||||||
|
assert_eq!(parent_path("/tidal"), Some("/"));
|
||||||
|
assert_eq!(parent_path("/"), None);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn join_path_appends_segments() {
|
||||||
|
assert_eq!(join_path("/", "tidal"), "/tidal");
|
||||||
|
assert_eq!(join_path("/tidal", "playlists"), "/tidal/playlists");
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn path_segments_splits() {
|
||||||
|
assert_eq!(path_segments("/"), Vec::<&str>::new());
|
||||||
|
assert_eq!(
|
||||||
|
path_segments("/tidal/artists/1/2"),
|
||||||
|
vec!["tidal", "artists", "1", "2"]
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
pub enum QueueError {
|
pub enum QueueError {
|
||||||
NotQueable,
|
NotQueable,
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -146,19 +146,17 @@ impl ProviderMessage {
|
||||||
#[derive(Debug)]
|
#[derive(Debug)]
|
||||||
pub enum ProviderCommand {
|
pub enum ProviderCommand {
|
||||||
GetLibraryNode {
|
GetLibraryNode {
|
||||||
uuid: String,
|
path: String,
|
||||||
result_tx: flume::Sender<Result<LibraryNode, ProviderError>>,
|
result_tx: flume::Sender<Result<LibraryNode, ProviderError>>,
|
||||||
},
|
},
|
||||||
GetTrack {
|
|
||||||
uuid: String,
|
|
||||||
result_tx: flume::Sender<Result<Track, ProviderError>>,
|
|
||||||
},
|
|
||||||
GetTrackUrls {
|
GetTrackUrls {
|
||||||
uuid: String,
|
path: String,
|
||||||
result_tx: flume::Sender<Result<Vec<String>, ProviderError>>,
|
result_tx: flume::Sender<Result<Vec<String>, ProviderError>>,
|
||||||
},
|
},
|
||||||
FlattenNode {
|
/// Resolves a path into playable tracks: a track path yields that single
|
||||||
uuid: String,
|
/// track, a node path yields all tracks reachable below it.
|
||||||
|
ResolveTracks {
|
||||||
|
path: String,
|
||||||
result_tx: flume::Sender<Vec<Track>>,
|
result_tx: flume::Sender<Vec<Track>>,
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
@ -167,9 +165,8 @@ impl ProviderCommand {
|
||||||
pub fn name(&self) -> &'static str {
|
pub fn name(&self) -> &'static str {
|
||||||
match self {
|
match self {
|
||||||
Self::GetLibraryNode { .. } => "get_library_node",
|
Self::GetLibraryNode { .. } => "get_library_node",
|
||||||
Self::GetTrack { .. } => "get_track",
|
|
||||||
Self::GetTrackUrls { .. } => "get_track_urls",
|
Self::GetTrackUrls { .. } => "get_track_urls",
|
||||||
Self::FlattenNode { .. } => "flatten_node",
|
Self::ResolveTracks { .. } => "resolve_tracks",
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -196,20 +193,20 @@ pub enum PlaybackCommand {
|
||||||
result_tx: flume::Sender<InitResponse>,
|
result_tx: flume::Sender<InitResponse>,
|
||||||
},
|
},
|
||||||
Replace {
|
Replace {
|
||||||
uuids: Vec<String>,
|
paths: Vec<String>,
|
||||||
},
|
},
|
||||||
Queue {
|
Queue {
|
||||||
uuids: Vec<String>,
|
paths: Vec<String>,
|
||||||
},
|
},
|
||||||
Append {
|
Append {
|
||||||
uuids: Vec<String>,
|
paths: Vec<String>,
|
||||||
},
|
},
|
||||||
Remove {
|
Remove {
|
||||||
positions: Vec<u32>,
|
positions: Vec<u32>,
|
||||||
},
|
},
|
||||||
Insert {
|
Insert {
|
||||||
position: u32,
|
position: u32,
|
||||||
uuids: Vec<String>,
|
paths: Vec<String>,
|
||||||
},
|
},
|
||||||
Clear {
|
Clear {
|
||||||
exclude_current: bool,
|
exclude_current: bool,
|
||||||
|
|
|
||||||
|
|
@ -96,8 +96,8 @@ impl Playback {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
PlaybackCommand::Replace { uuids } => {
|
PlaybackCommand::Replace { paths } => {
|
||||||
let all_tracks = self.resolve_tracks(uuids).await;
|
let all_tracks = self.resolve_tracks(paths).await;
|
||||||
debug!(count = all_tracks.len(), "replacing queue");
|
debug!(count = all_tracks.len(), "replacing queue");
|
||||||
let current = {
|
let current = {
|
||||||
let Ok(mut queue) = self.queue.lock() else {
|
let Ok(mut queue) = self.queue.lock() else {
|
||||||
|
|
@ -111,8 +111,8 @@ impl Playback {
|
||||||
self.play(current).await;
|
self.play(current).await;
|
||||||
}
|
}
|
||||||
|
|
||||||
PlaybackCommand::Queue { uuids } => {
|
PlaybackCommand::Queue { paths } => {
|
||||||
let all_tracks = self.resolve_tracks(uuids).await;
|
let all_tracks = self.resolve_tracks(paths).await;
|
||||||
debug!(count = all_tracks.len(), "queueing after current");
|
debug!(count = all_tracks.len(), "queueing after current");
|
||||||
let track = {
|
let track = {
|
||||||
let Ok(mut queue) = self.queue.lock() else {
|
let Ok(mut queue) = self.queue.lock() else {
|
||||||
|
|
@ -126,8 +126,8 @@ impl Playback {
|
||||||
self.play_if_some(track).await;
|
self.play_if_some(track).await;
|
||||||
}
|
}
|
||||||
|
|
||||||
PlaybackCommand::Append { uuids } => {
|
PlaybackCommand::Append { paths } => {
|
||||||
let all_tracks = self.resolve_tracks(uuids).await;
|
let all_tracks = self.resolve_tracks(paths).await;
|
||||||
debug!(count = all_tracks.len(), "appending to queue");
|
debug!(count = all_tracks.len(), "appending to queue");
|
||||||
let track = {
|
let track = {
|
||||||
let Ok(mut queue) = self.queue.lock() else {
|
let Ok(mut queue) = self.queue.lock() else {
|
||||||
|
|
@ -171,8 +171,8 @@ impl Playback {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
PlaybackCommand::Insert { position, uuids } => {
|
PlaybackCommand::Insert { position, paths } => {
|
||||||
let all_tracks = self.resolve_tracks(uuids).await;
|
let all_tracks = self.resolve_tracks(paths).await;
|
||||||
debug!(count = all_tracks.len(), position, "inserting into queue");
|
debug!(count = all_tracks.len(), position, "inserting into queue");
|
||||||
let track = {
|
let track = {
|
||||||
let Ok(mut queue) = self.queue.lock() else {
|
let Ok(mut queue) = self.queue.lock() else {
|
||||||
|
|
@ -294,7 +294,7 @@ impl Playback {
|
||||||
queue.next_track()
|
queue.next_track()
|
||||||
};
|
};
|
||||||
debug!(
|
debug!(
|
||||||
track = track.as_ref().map(|t| t.uuid.as_str()),
|
track = track.as_ref().map(|t| t.path.as_str()),
|
||||||
"advancing to next track"
|
"advancing to next track"
|
||||||
);
|
);
|
||||||
self.play_or_stop(track).await;
|
self.play_or_stop(track).await;
|
||||||
|
|
@ -309,7 +309,7 @@ impl Playback {
|
||||||
queue.prev_track()
|
queue.prev_track()
|
||||||
};
|
};
|
||||||
debug!(
|
debug!(
|
||||||
track = track.as_ref().map(|t| t.uuid.as_str()),
|
track = track.as_ref().map(|t| t.path.as_str()),
|
||||||
"going back to previous track"
|
"going back to previous track"
|
||||||
);
|
);
|
||||||
self.play_or_stop(track).await;
|
self.play_or_stop(track).await;
|
||||||
|
|
@ -360,67 +360,39 @@ impl Playback {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Resolves a mixed list of track and node identifiers into tracks by
|
/// Resolves a mixed list of track and node paths into tracks by asking
|
||||||
/// asking the provider orchestrator.
|
/// the provider orchestrator.
|
||||||
async fn resolve_tracks(&self, uuids: Vec<String>) -> Vec<Track> {
|
async fn resolve_tracks(&self, paths: Vec<String>) -> Vec<Track> {
|
||||||
let mut all_tracks = Vec::new();
|
let mut all_tracks = Vec::new();
|
||||||
for uuid in uuids {
|
for path in paths {
|
||||||
if is_track(&uuid) {
|
let (result_tx, result_rx) = flume::bounded(1);
|
||||||
match self.get_track(&uuid).await {
|
let message = ProviderMessage::new(ProviderCommand::ResolveTracks {
|
||||||
Ok(track) => all_tracks.push(track),
|
path: path.clone(),
|
||||||
Err(err) => warn!(uuid, "failed to resolve track: {err}"),
|
result_tx,
|
||||||
|
});
|
||||||
|
if let Err(err) = self.provider_tx.send_async(message).await {
|
||||||
|
error!("provider channel closed: {err}");
|
||||||
|
return all_tracks;
|
||||||
|
}
|
||||||
|
match result_rx.recv_async().await {
|
||||||
|
Ok(tracks) => {
|
||||||
|
if tracks.is_empty() {
|
||||||
|
warn!(path, "path resolved to no playable tracks");
|
||||||
}
|
}
|
||||||
} else {
|
|
||||||
let tracks = self.flatten_node(&uuid).await;
|
|
||||||
all_tracks.extend(tracks);
|
all_tracks.extend(tracks);
|
||||||
}
|
}
|
||||||
|
Err(err) => error!(path, "provider dropped resolve_tracks reply: {err}"),
|
||||||
|
}
|
||||||
}
|
}
|
||||||
trace!(count = all_tracks.len(), "resolved tracks");
|
trace!(count = all_tracks.len(), "resolved tracks");
|
||||||
all_tracks
|
all_tracks
|
||||||
}
|
}
|
||||||
|
|
||||||
#[instrument(skip(self))]
|
#[instrument(skip(self))]
|
||||||
async fn flatten_node(&self, uuid: &str) -> Vec<Track> {
|
async fn get_urls_for_track(&self, path: &str) -> Result<Vec<String>, ProviderError> {
|
||||||
let (result_tx, result_rx) = flume::bounded(1);
|
|
||||||
let message = ProviderMessage::new(ProviderCommand::FlattenNode {
|
|
||||||
uuid: uuid.to_string(),
|
|
||||||
result_tx,
|
|
||||||
});
|
|
||||||
if let Err(err) = self.provider_tx.send_async(message).await {
|
|
||||||
error!("provider channel closed: {err}");
|
|
||||||
return Vec::new();
|
|
||||||
}
|
|
||||||
match result_rx.recv_async().await {
|
|
||||||
Ok(tracks) => tracks,
|
|
||||||
Err(err) => {
|
|
||||||
error!("provider dropped flatten_node reply: {err}");
|
|
||||||
Vec::new()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#[instrument(skip(self))]
|
|
||||||
async fn get_track(&self, uuid: &str) -> Result<Track, ProviderError> {
|
|
||||||
let (result_tx, result_rx) = flume::bounded(1);
|
|
||||||
let message = ProviderMessage::new(ProviderCommand::GetTrack {
|
|
||||||
uuid: uuid.to_string(),
|
|
||||||
result_tx,
|
|
||||||
});
|
|
||||||
self.provider_tx
|
|
||||||
.send_async(message)
|
|
||||||
.await
|
|
||||||
.map_err(|_| ProviderError::InternalError)?;
|
|
||||||
result_rx
|
|
||||||
.recv_async()
|
|
||||||
.await
|
|
||||||
.map_err(|_| ProviderError::InternalError)?
|
|
||||||
}
|
|
||||||
|
|
||||||
#[instrument(skip(self))]
|
|
||||||
async fn get_urls_for_track(&self, uuid: &str) -> Result<Vec<String>, ProviderError> {
|
|
||||||
let (result_tx, result_rx) = flume::bounded(1);
|
let (result_tx, result_rx) = flume::bounded(1);
|
||||||
let message = ProviderMessage::new(ProviderCommand::GetTrackUrls {
|
let message = ProviderMessage::new(ProviderCommand::GetTrackUrls {
|
||||||
uuid: uuid.to_string(),
|
path: path.to_string(),
|
||||||
result_tx,
|
result_tx,
|
||||||
});
|
});
|
||||||
self.provider_tx
|
self.provider_tx
|
||||||
|
|
@ -440,7 +412,7 @@ impl Playback {
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Plays the given track if there is one, otherwise stops the player.
|
/// Plays the given track if there is one, otherwise stops the player.
|
||||||
#[instrument(skip(self, track), fields(track = track.as_ref().map(|t| t.uuid.as_str())))]
|
#[instrument(skip(self, track), fields(track = track.as_ref().map(|t| t.path.as_str())))]
|
||||||
async fn play_or_stop(&self, track: Option<Track>) {
|
async fn play_or_stop(&self, track: Option<Track>) {
|
||||||
if track.is_some() {
|
if track.is_some() {
|
||||||
self.play(track).await;
|
self.play(track).await;
|
||||||
|
|
@ -459,18 +431,18 @@ impl Playback {
|
||||||
/// Starts playback of the given track. When fetching stream URLs fails
|
/// Starts playback of the given track. When fetching stream URLs fails
|
||||||
/// the failing track is skipped and playback continues with the next
|
/// the failing track is skipped and playback continues with the next
|
||||||
/// track in the queue.
|
/// track in the queue.
|
||||||
#[instrument(skip(self, track), fields(track = track.as_ref().map(|t| t.uuid.as_str())))]
|
#[instrument(skip(self, track), fields(track = track.as_ref().map(|t| t.path.as_str())))]
|
||||||
async fn play(&self, track: Option<Track>) {
|
async fn play(&self, track: Option<Track>) {
|
||||||
let Some(track) = track else {
|
let Some(track) = track else {
|
||||||
debug!("nothing to play");
|
debug!("nothing to play");
|
||||||
return;
|
return;
|
||||||
};
|
};
|
||||||
let mut uuid = track.uuid.clone();
|
let mut path = track.path.clone();
|
||||||
let urls = loop {
|
let urls = loop {
|
||||||
match self.get_urls_for_track(&uuid).await {
|
match self.get_urls_for_track(&path).await {
|
||||||
Ok(urls) if !urls.is_empty() => break urls,
|
Ok(urls) if !urls.is_empty() => break urls,
|
||||||
Ok(_) => warn!(uuid, "provider returned no stream urls, skipping track"),
|
Ok(_) => warn!(path, "provider returned no stream urls, skipping track"),
|
||||||
Err(err) => warn!(uuid, "failed to fetch stream urls ({err}), skipping track"),
|
Err(err) => warn!(path, "failed to fetch stream urls ({err}), skipping track"),
|
||||||
}
|
}
|
||||||
let next = {
|
let next = {
|
||||||
let Ok(mut queue) = self.queue.lock() else {
|
let Ok(mut queue) = self.queue.lock() else {
|
||||||
|
|
@ -480,7 +452,7 @@ impl Playback {
|
||||||
queue.next_track()
|
queue.next_track()
|
||||||
};
|
};
|
||||||
match next {
|
match next {
|
||||||
Some(next_track) => uuid = next_track.uuid.clone(),
|
Some(next_track) => path = next_track.path.clone(),
|
||||||
None => {
|
None => {
|
||||||
error!("no playable track left in queue, stopping");
|
error!("no playable track left in queue, stopping");
|
||||||
self.stop_player().await;
|
self.stop_player().await;
|
||||||
|
|
@ -504,7 +476,3 @@ impl Playback {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fn is_track(uuid: &str) -> bool {
|
|
||||||
uuid.starts_with("track:")
|
|
||||||
}
|
|
||||||
|
|
|
||||||
|
|
@ -28,53 +28,57 @@ impl ProviderOrchestrator {
|
||||||
|
|
||||||
async fn handle_command(&self, command: ProviderCommand) {
|
async fn handle_command(&self, command: ProviderCommand) {
|
||||||
match command {
|
match command {
|
||||||
ProviderCommand::GetLibraryNode { uuid, result_tx } => {
|
ProviderCommand::GetLibraryNode { path, result_tx } => {
|
||||||
let result = self.get_lib_node(&uuid).await;
|
let result = self.get_lib_node(&path).await;
|
||||||
if let Err(err) = result_tx.send_async(result).await {
|
if let Err(err) = result_tx.send_async(result).await {
|
||||||
error!("failed to send get_library_node result: {err}");
|
error!("failed to send get_library_node result: {err}");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
ProviderCommand::GetTrack { uuid, result_tx } => {
|
ProviderCommand::GetTrackUrls { path, result_tx } => {
|
||||||
let result = self.get_metadata_for_track(&uuid).await;
|
let result = self.get_urls_for_track(&path).await;
|
||||||
if let Err(err) = result_tx.send_async(result).await {
|
|
||||||
error!("failed to send get_track result: {err}");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
ProviderCommand::GetTrackUrls { uuid, result_tx } => {
|
|
||||||
let result = self.get_urls_for_track(&uuid).await;
|
|
||||||
if let Err(err) = result_tx.send_async(result).await {
|
if let Err(err) = result_tx.send_async(result).await {
|
||||||
error!("failed to send get_track_urls result: {err}");
|
error!("failed to send get_track_urls result: {err}");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
ProviderCommand::FlattenNode { uuid, result_tx } => {
|
ProviderCommand::ResolveTracks { path, result_tx } => {
|
||||||
let result = self.flatten_node(&uuid).await;
|
let result = self.resolve_tracks(&path).await;
|
||||||
if let Err(err) = result_tx.send_async(result).await {
|
if let Err(err) = result_tx.send_async(result).await {
|
||||||
error!("failed to send flatten_node result: {err}");
|
error!("failed to send resolve_tracks result: {err}");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Collects all tracks reachable from the given node by walking its
|
/// Resolves a path into playable tracks. A track path resolves to that
|
||||||
/// queueable descendants.
|
/// single track; a node path is flattened by walking its queueable
|
||||||
|
/// descendants.
|
||||||
#[instrument(skip(self))]
|
#[instrument(skip(self))]
|
||||||
async fn flatten_node(&self, node_uuid: &str) -> Vec<Track> {
|
async fn resolve_tracks(&self, path: &str) -> Vec<Track> {
|
||||||
|
if self.is_track_path(path) {
|
||||||
|
return match self.get_metadata_for_track(path).await {
|
||||||
|
Ok(track) => vec![track],
|
||||||
|
Err(err) => {
|
||||||
|
warn!(path, "failed to resolve track: {err}");
|
||||||
|
Vec::new()
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
let mut tracks = Vec::new();
|
let mut tracks = Vec::new();
|
||||||
let mut nodes_to_go = vec![node_uuid.to_string()];
|
let mut nodes_to_go = vec![path.to_string()];
|
||||||
while let Some(node_uuid) = nodes_to_go.pop() {
|
while let Some(node_path) = nodes_to_go.pop() {
|
||||||
let node = match self.get_lib_node(&node_uuid).await {
|
let node = match self.get_lib_node(&node_path).await {
|
||||||
Ok(node) => node,
|
Ok(node) => node,
|
||||||
Err(err) => {
|
Err(err) => {
|
||||||
warn!(node = node_uuid, "skipping unreadable node: {err}");
|
warn!(node = node_path, "skipping unreadable node: {err}");
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
if node.is_queable {
|
if node.is_queable {
|
||||||
tracks.extend(node.tracks);
|
tracks.extend(node.tracks);
|
||||||
nodes_to_go.extend(node.children.into_iter().map(|c| c.uuid))
|
nodes_to_go.extend(node.children.into_iter().map(|c| c.path))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
debug!(count = tracks.len(), "flattened node into tracks");
|
debug!(count = tracks.len(), "resolved path into tracks");
|
||||||
tracks
|
tracks
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -119,33 +123,50 @@ impl ProviderClient for ProviderOrchestrator {
|
||||||
String::new()
|
String::new()
|
||||||
}
|
}
|
||||||
|
|
||||||
#[instrument(skip(self))]
|
/// Routes to the provider that owns the path.
|
||||||
async fn get_urls_for_track(&self, track_uuid: &str) -> Result<Vec<String>, ProviderError> {
|
fn is_track_path(&self, path: &str) -> bool {
|
||||||
self.tidal_client.get_urls_for_track(track_uuid).await
|
if path == "/tidal" || path.starts_with("/tidal/") {
|
||||||
|
return self.tidal_client.is_track_path(path);
|
||||||
|
}
|
||||||
|
false
|
||||||
}
|
}
|
||||||
|
|
||||||
#[instrument(skip(self))]
|
#[instrument(skip(self))]
|
||||||
async fn get_metadata_for_track(&self, track_uuid: &str) -> Result<Track, ProviderError> {
|
async fn get_urls_for_track(&self, track_path: &str) -> Result<Vec<String>, ProviderError> {
|
||||||
self.tidal_client.get_metadata_for_track(track_uuid).await
|
if track_path.starts_with("/tidal/") {
|
||||||
|
return self.tidal_client.get_urls_for_track(track_path).await;
|
||||||
|
}
|
||||||
|
warn!(path = track_path, "no provider owns this track path");
|
||||||
|
Err(ProviderError::MalformedPath)
|
||||||
|
}
|
||||||
|
|
||||||
|
#[instrument(skip(self))]
|
||||||
|
async fn get_metadata_for_track(&self, track_path: &str) -> Result<Track, ProviderError> {
|
||||||
|
if track_path.starts_with("/tidal/") {
|
||||||
|
return self.tidal_client.get_metadata_for_track(track_path).await;
|
||||||
|
}
|
||||||
|
warn!(path = track_path, "no provider owns this track path");
|
||||||
|
Err(ProviderError::MalformedPath)
|
||||||
}
|
}
|
||||||
|
|
||||||
fn get_lib_root(&self) -> LibraryNode {
|
fn get_lib_root(&self) -> LibraryNode {
|
||||||
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(tidaldy::PROVIDER_ROOT.to_owned(), "tidal".to_owned(), false);
|
||||||
root_node.children.push(child);
|
root_node.children.push(child);
|
||||||
root_node
|
root_node
|
||||||
}
|
}
|
||||||
|
|
||||||
#[instrument(skip(self))]
|
#[instrument(skip(self))]
|
||||||
async fn get_lib_node(&self, uuid: &str) -> Result<LibraryNode, ProviderError> {
|
async fn get_lib_node(&self, path: &str) -> Result<LibraryNode, ProviderError> {
|
||||||
if uuid == "node:/" {
|
if path == crabidy_core::ROOT_PATH {
|
||||||
debug!("serving global library root");
|
debug!("serving global library root");
|
||||||
return Ok(self.get_lib_root());
|
return Ok(self.get_lib_root());
|
||||||
}
|
}
|
||||||
if uuid == "node:tidal" {
|
if path == tidaldy::PROVIDER_ROOT || path.starts_with("/tidal/") {
|
||||||
debug!("serving tidal library root");
|
return self.tidal_client.get_lib_node(path).await;
|
||||||
return Ok(self.tidal_client.get_lib_root());
|
|
||||||
}
|
}
|
||||||
self.tidal_client.get_lib_node(uuid).await
|
warn!(path, "no provider owns this path");
|
||||||
|
Err(ProviderError::MalformedPath)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -67,18 +67,18 @@ impl CrabidyService for RpcService {
|
||||||
Ok(Response::new(response))
|
Ok(Response::new(response))
|
||||||
}
|
}
|
||||||
|
|
||||||
#[instrument(skip(self, request), fields(uuid))]
|
#[instrument(skip(self, request), fields(path))]
|
||||||
async fn get_library_node(
|
async fn get_library_node(
|
||||||
&self,
|
&self,
|
||||||
request: Request<GetLibraryNodeRequest>,
|
request: Request<GetLibraryNodeRequest>,
|
||||||
) -> Result<Response<GetLibraryNodeResponse>, Status> {
|
) -> Result<Response<GetLibraryNodeResponse>, Status> {
|
||||||
let uuid = request.into_inner().uuid;
|
let path = request.into_inner().path;
|
||||||
tracing::Span::current().record("uuid", uuid.as_str());
|
tracing::Span::current().record("path", path.as_str());
|
||||||
debug!("received get_library_node request");
|
debug!("received get_library_node request");
|
||||||
let (result_tx, result_rx) = flume::bounded(1);
|
let (result_tx, result_rx) = flume::bounded(1);
|
||||||
self.provider_tx
|
self.provider_tx
|
||||||
.send_async(ProviderMessage::new(ProviderCommand::GetLibraryNode {
|
.send_async(ProviderMessage::new(ProviderCommand::GetLibraryNode {
|
||||||
uuid,
|
path,
|
||||||
result_tx,
|
result_tx,
|
||||||
}))
|
}))
|
||||||
.await
|
.await
|
||||||
|
|
@ -99,40 +99,40 @@ impl CrabidyService for RpcService {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#[instrument(skip(self, request), fields(uuids))]
|
#[instrument(skip(self, request), fields(paths))]
|
||||||
async fn queue(
|
async fn queue(
|
||||||
&self,
|
&self,
|
||||||
request: Request<QueueRequest>,
|
request: Request<QueueRequest>,
|
||||||
) -> Result<Response<QueueResponse>, Status> {
|
) -> Result<Response<QueueResponse>, Status> {
|
||||||
let uuids = request.into_inner().uuids;
|
let paths = request.into_inner().paths;
|
||||||
tracing::Span::current().record("uuids", format!("{uuids:?}"));
|
tracing::Span::current().record("paths", format!("{paths:?}"));
|
||||||
debug!("received queue request");
|
debug!("received queue request");
|
||||||
self.send_playback(PlaybackCommand::Queue { uuids }).await?;
|
self.send_playback(PlaybackCommand::Queue { paths }).await?;
|
||||||
Ok(Response::new(QueueResponse {}))
|
Ok(Response::new(QueueResponse {}))
|
||||||
}
|
}
|
||||||
|
|
||||||
#[instrument(skip(self, request), fields(uuids))]
|
#[instrument(skip(self, request), fields(paths))]
|
||||||
async fn replace(
|
async fn replace(
|
||||||
&self,
|
&self,
|
||||||
request: Request<ReplaceRequest>,
|
request: Request<ReplaceRequest>,
|
||||||
) -> Result<Response<ReplaceResponse>, Status> {
|
) -> Result<Response<ReplaceResponse>, Status> {
|
||||||
let uuids = request.into_inner().uuids;
|
let paths = request.into_inner().paths;
|
||||||
tracing::Span::current().record("uuids", format!("{uuids:?}"));
|
tracing::Span::current().record("paths", format!("{paths:?}"));
|
||||||
debug!("received replace request");
|
debug!("received replace request");
|
||||||
self.send_playback(PlaybackCommand::Replace { uuids })
|
self.send_playback(PlaybackCommand::Replace { paths })
|
||||||
.await?;
|
.await?;
|
||||||
Ok(Response::new(ReplaceResponse {}))
|
Ok(Response::new(ReplaceResponse {}))
|
||||||
}
|
}
|
||||||
|
|
||||||
#[instrument(skip(self, request), fields(uuids))]
|
#[instrument(skip(self, request), fields(paths))]
|
||||||
async fn append(
|
async fn append(
|
||||||
&self,
|
&self,
|
||||||
request: Request<AppendRequest>,
|
request: Request<AppendRequest>,
|
||||||
) -> Result<Response<AppendResponse>, Status> {
|
) -> Result<Response<AppendResponse>, Status> {
|
||||||
let uuids = request.into_inner().uuids;
|
let paths = request.into_inner().paths;
|
||||||
tracing::Span::current().record("uuids", format!("{uuids:?}"));
|
tracing::Span::current().record("paths", format!("{paths:?}"));
|
||||||
debug!("received append request");
|
debug!("received append request");
|
||||||
self.send_playback(PlaybackCommand::Append { uuids })
|
self.send_playback(PlaybackCommand::Append { paths })
|
||||||
.await?;
|
.await?;
|
||||||
Ok(Response::new(AppendResponse {}))
|
Ok(Response::new(AppendResponse {}))
|
||||||
}
|
}
|
||||||
|
|
@ -150,18 +150,18 @@ impl CrabidyService for RpcService {
|
||||||
Ok(Response::new(RemoveResponse {}))
|
Ok(Response::new(RemoveResponse {}))
|
||||||
}
|
}
|
||||||
|
|
||||||
#[instrument(skip(self, request), fields(uuids, position))]
|
#[instrument(skip(self, request), fields(paths, position))]
|
||||||
async fn insert(
|
async fn insert(
|
||||||
&self,
|
&self,
|
||||||
request: Request<InsertRequest>,
|
request: Request<InsertRequest>,
|
||||||
) -> Result<Response<InsertResponse>, Status> {
|
) -> Result<Response<InsertResponse>, Status> {
|
||||||
let req = request.into_inner();
|
let req = request.into_inner();
|
||||||
tracing::Span::current().record("uuids", format!("{:?}", req.uuids));
|
tracing::Span::current().record("paths", format!("{:?}", req.paths));
|
||||||
tracing::Span::current().record("position", req.position);
|
tracing::Span::current().record("position", req.position);
|
||||||
debug!("received insert request");
|
debug!("received insert request");
|
||||||
self.send_playback(PlaybackCommand::Insert {
|
self.send_playback(PlaybackCommand::Insert {
|
||||||
position: req.position,
|
position: req.position,
|
||||||
uuids: req.uuids,
|
paths: req.paths,
|
||||||
})
|
})
|
||||||
.await?;
|
.await?;
|
||||||
Ok(Response::new(InsertResponse {}))
|
Ok(Response::new(InsertResponse {}))
|
||||||
|
|
|
||||||
|
|
@ -39,23 +39,30 @@ impl crabidy_core::ProviderClient for Client {
|
||||||
fn settings(&self) -> String {
|
fn settings(&self) -> String {
|
||||||
toml::to_string_pretty(&self.settings).unwrap_or_default()
|
toml::to_string_pretty(&self.settings).unwrap_or_default()
|
||||||
}
|
}
|
||||||
|
fn is_track_path(&self, path: &str) -> bool {
|
||||||
|
matches!(
|
||||||
|
parse_path(path),
|
||||||
|
Ok(TidalPath::PlaylistTrack { .. } | TidalPath::AlbumTrack { .. })
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
#[instrument(skip(self))]
|
#[instrument(skip(self))]
|
||||||
async fn get_urls_for_track(
|
async fn get_urls_for_track(
|
||||||
&self,
|
&self,
|
||||||
track_uuid: &str,
|
track_path: &str,
|
||||||
) -> Result<Vec<String>, crabidy_core::ProviderError> {
|
) -> Result<Vec<String>, crabidy_core::ProviderError> {
|
||||||
let (_, track_uuid, _) = split_uuid(track_uuid);
|
let track_id = track_id_from_path(track_path)?;
|
||||||
let playback = self.get_track_playback(&track_uuid).await.map_err(|err| {
|
let playback = self.get_track_playback(track_id).await.map_err(|err| {
|
||||||
warn!(track = track_uuid, "failed to fetch playback info: {err}");
|
warn!(track = track_id, "failed to fetch playback info: {err}");
|
||||||
crabidy_core::ProviderError::FetchError
|
crabidy_core::ProviderError::FetchError
|
||||||
})?;
|
})?;
|
||||||
trace!(?playback, "got playback info");
|
trace!(?playback, "got playback info");
|
||||||
let manifest = playback.get_manifest().map_err(|err| {
|
let manifest = playback.get_manifest().map_err(|err| {
|
||||||
warn!(track = track_uuid, "failed to decode manifest: {err}");
|
warn!(track = track_id, "failed to decode manifest: {err}");
|
||||||
crabidy_core::ProviderError::FetchError
|
crabidy_core::ProviderError::FetchError
|
||||||
})?;
|
})?;
|
||||||
debug!(
|
debug!(
|
||||||
track = track_uuid,
|
track = track_id,
|
||||||
urls = manifest.urls.len(),
|
urls = manifest.urls.len(),
|
||||||
"resolved stream urls"
|
"resolved stream urls"
|
||||||
);
|
);
|
||||||
|
|
@ -65,37 +72,38 @@ impl crabidy_core::ProviderClient for Client {
|
||||||
#[instrument(skip(self))]
|
#[instrument(skip(self))]
|
||||||
async fn get_metadata_for_track(
|
async fn get_metadata_for_track(
|
||||||
&self,
|
&self,
|
||||||
track_uuid: &str,
|
track_path: &str,
|
||||||
) -> Result<crabidy_core::proto::crabidy::Track, crabidy_core::ProviderError> {
|
) -> Result<crabidy_core::proto::crabidy::Track, crabidy_core::ProviderError> {
|
||||||
let track = self.get_track(track_uuid).await.map_err(|err| {
|
let track_id = track_id_from_path(track_path)?;
|
||||||
warn!(track = track_uuid, "failed to fetch track metadata: {err}");
|
let track = self.get_track(track_id).await.map_err(|err| {
|
||||||
|
warn!(track = track_id, "failed to fetch track metadata: {err}");
|
||||||
crabidy_core::ProviderError::FetchError
|
crabidy_core::ProviderError::FetchError
|
||||||
})?;
|
})?;
|
||||||
Ok(track.into())
|
let parent = crabidy_core::parent_path(track_path)
|
||||||
|
.unwrap_or(PROVIDER_ROOT)
|
||||||
|
.to_string();
|
||||||
|
Ok(track.to_proto(&parent))
|
||||||
}
|
}
|
||||||
|
|
||||||
#[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");
|
crabidy_core::proto::crabidy::LibraryNode {
|
||||||
let global_root = crabidy_core::proto::crabidy::LibraryNode::new();
|
path: PROVIDER_ROOT.to_string(),
|
||||||
let children = vec![
|
title: "tidal".to_string(),
|
||||||
|
parent: Some(crabidy_core::ROOT_PATH.to_string()),
|
||||||
|
tracks: Vec::new(),
|
||||||
|
children: vec![
|
||||||
crabidy_core::proto::crabidy::LibraryNodeChild::new(
|
crabidy_core::proto::crabidy::LibraryNodeChild::new(
|
||||||
"node:userplaylists".to_string(),
|
format!("{PROVIDER_ROOT}/playlists"),
|
||||||
"playlists".to_string(),
|
"playlists".to_string(),
|
||||||
false,
|
false,
|
||||||
),
|
),
|
||||||
crabidy_core::proto::crabidy::LibraryNodeChild::new(
|
crabidy_core::proto::crabidy::LibraryNodeChild::new(
|
||||||
"node:userartists".to_string(),
|
format!("{PROVIDER_ROOT}/artists"),
|
||||||
"artists".to_string(),
|
"artists".to_string(),
|
||||||
false,
|
false,
|
||||||
),
|
),
|
||||||
];
|
],
|
||||||
crabidy_core::proto::crabidy::LibraryNode {
|
|
||||||
uuid: "node:tidal".to_string(),
|
|
||||||
title: "tidal".to_string(),
|
|
||||||
parent: Some(format!("{}", global_root.uuid)),
|
|
||||||
tracks: Vec::new(),
|
|
||||||
children,
|
|
||||||
is_queable: false,
|
is_queable: false,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -103,19 +111,23 @@ impl crabidy_core::ProviderClient for Client {
|
||||||
#[instrument(skip(self))]
|
#[instrument(skip(self))]
|
||||||
async fn get_lib_node(
|
async fn get_lib_node(
|
||||||
&self,
|
&self,
|
||||||
uuid: &str,
|
path: &str,
|
||||||
) -> Result<crabidy_core::proto::crabidy::LibraryNode, crabidy_core::ProviderError> {
|
) -> Result<crabidy_core::proto::crabidy::LibraryNode, crabidy_core::ProviderError> {
|
||||||
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);
|
||||||
};
|
};
|
||||||
let (_kind, module, uuid) = split_uuid(uuid);
|
let parsed = parse_path(path)?;
|
||||||
debug!(module, uuid, "resolving library node");
|
debug!(?parsed, "resolving library node");
|
||||||
let node = match module.as_str() {
|
let parent = crabidy_core::parent_path(path)
|
||||||
"userplaylists" => {
|
.unwrap_or(crabidy_core::ROOT_PATH)
|
||||||
|
.to_string();
|
||||||
|
let node = match parsed {
|
||||||
|
TidalPath::Root => self.get_lib_root(),
|
||||||
|
TidalPath::Playlists => {
|
||||||
let mut node = crabidy_core::proto::crabidy::LibraryNode {
|
let mut node = crabidy_core::proto::crabidy::LibraryNode {
|
||||||
uuid: "node:userplaylists".to_string(),
|
path: path.to_string(),
|
||||||
title: "playlists".to_string(),
|
title: "playlists".to_string(),
|
||||||
parent: Some("node:tidal".to_string()),
|
parent: Some(parent),
|
||||||
tracks: Vec::new(),
|
tracks: Vec::new(),
|
||||||
children: Vec::new(),
|
children: Vec::new(),
|
||||||
is_queable: false,
|
is_queable: false,
|
||||||
|
|
@ -124,88 +136,157 @@ impl crabidy_core::ProviderClient for Client {
|
||||||
.get_users_playlists_and_favorite_playlists(&user_id)
|
.get_users_playlists_and_favorite_playlists(&user_id)
|
||||||
.await?
|
.await?
|
||||||
{
|
{
|
||||||
let child = crabidy_core::proto::crabidy::LibraryNodeChild::new(
|
node.children
|
||||||
format!("node:playlist:{}", playlist.playlist.uuid),
|
.push(crabidy_core::proto::crabidy::LibraryNodeChild::new(
|
||||||
|
crabidy_core::join_path(path, &playlist.playlist.uuid),
|
||||||
playlist.playlist.title,
|
playlist.playlist.title,
|
||||||
true,
|
true,
|
||||||
);
|
));
|
||||||
node.children.push(child);
|
|
||||||
}
|
}
|
||||||
node
|
node
|
||||||
}
|
}
|
||||||
"playlist" => {
|
TidalPath::Playlist(playlist_id) => {
|
||||||
let mut node: crabidy_core::proto::crabidy::LibraryNode =
|
let playlist = self.get_playlist(playlist_id).await?;
|
||||||
self.get_playlist(&uuid).await?.into();
|
let tracks = self
|
||||||
let tracks: Vec<crabidy_core::proto::crabidy::Track> = self
|
.get_playlist_tracks(playlist_id)
|
||||||
.get_playlist_tracks(&uuid)
|
|
||||||
.await?
|
.await?
|
||||||
.iter()
|
.iter()
|
||||||
.map(|t| t.into())
|
.map(|t| t.to_proto(path))
|
||||||
.collect();
|
.collect();
|
||||||
node.tracks = tracks;
|
crabidy_core::proto::crabidy::LibraryNode {
|
||||||
node.parent = Some("node:userplaylists".to_string());
|
path: path.to_string(),
|
||||||
node
|
title: playlist.title,
|
||||||
|
parent: Some(parent),
|
||||||
|
tracks,
|
||||||
|
children: Vec::new(),
|
||||||
|
is_queable: true,
|
||||||
}
|
}
|
||||||
"userartists" => {
|
}
|
||||||
|
TidalPath::Artists => {
|
||||||
let mut node = crabidy_core::proto::crabidy::LibraryNode {
|
let mut node = crabidy_core::proto::crabidy::LibraryNode {
|
||||||
uuid: "node:userartists".to_string(),
|
path: path.to_string(),
|
||||||
title: "artists".to_string(),
|
title: "artists".to_string(),
|
||||||
parent: Some("node:tidal".to_string()),
|
parent: Some(parent),
|
||||||
tracks: Vec::new(),
|
tracks: Vec::new(),
|
||||||
children: Vec::new(),
|
children: Vec::new(),
|
||||||
is_queable: false,
|
is_queable: false,
|
||||||
};
|
};
|
||||||
for artist in self.get_users_artists(&user_id).await? {
|
for artist in self.get_users_artists(&user_id).await? {
|
||||||
let child = crabidy_core::proto::crabidy::LibraryNodeChild::new(
|
node.children
|
||||||
format!("node:artist:{}", artist.item.id),
|
.push(crabidy_core::proto::crabidy::LibraryNodeChild::new(
|
||||||
|
crabidy_core::join_path(path, &artist.item.id.to_string()),
|
||||||
artist.item.name,
|
artist.item.name,
|
||||||
true,
|
true,
|
||||||
);
|
));
|
||||||
node.children.push(child);
|
|
||||||
}
|
}
|
||||||
node
|
node
|
||||||
}
|
}
|
||||||
"artist" => {
|
TidalPath::Artist(artist_id) => {
|
||||||
let mut node: crabidy_core::proto::crabidy::LibraryNode =
|
let artist = self.get_artist(artist_id).await?;
|
||||||
self.get_artist(&uuid).await?.into();
|
let children = self
|
||||||
let children: Vec<crabidy_core::proto::crabidy::LibraryNodeChild> = self
|
.get_artist_albums(artist_id)
|
||||||
.get_artist_albums(&uuid)
|
|
||||||
.await?
|
.await?
|
||||||
.iter()
|
.iter()
|
||||||
.map(|t| t.into())
|
.map(|album| {
|
||||||
|
crabidy_core::proto::crabidy::LibraryNodeChild::new(
|
||||||
|
crabidy_core::join_path(path, &album.id.to_string()),
|
||||||
|
album.title.clone(),
|
||||||
|
true,
|
||||||
|
)
|
||||||
|
})
|
||||||
.collect();
|
.collect();
|
||||||
node.children = children;
|
crabidy_core::proto::crabidy::LibraryNode {
|
||||||
node.parent = Some("node:userartists".to_string());
|
path: path.to_string(),
|
||||||
node
|
title: artist.name,
|
||||||
|
parent: Some(parent),
|
||||||
|
tracks: Vec::new(),
|
||||||
|
children,
|
||||||
|
is_queable: true,
|
||||||
}
|
}
|
||||||
"album" => {
|
}
|
||||||
let album = self.get_album(&uuid).await?;
|
TidalPath::Album { album, .. } => {
|
||||||
let artis_id = album.artist.clone().unwrap().id;
|
let album_data = self.get_album(album).await?;
|
||||||
let mut node: crabidy_core::proto::crabidy::LibraryNode = album.into();
|
let tracks = self
|
||||||
let tracks: Vec<crabidy_core::proto::crabidy::Track> = self
|
.get_album_tracks(album)
|
||||||
.get_album_tracks(&uuid)
|
|
||||||
.await?
|
.await?
|
||||||
.iter()
|
.iter()
|
||||||
.map(|t| t.into())
|
.map(|t| t.to_proto(path))
|
||||||
.collect();
|
.collect();
|
||||||
node.tracks = tracks;
|
crabidy_core::proto::crabidy::LibraryNode {
|
||||||
node.parent = Some(format!("node:artist:{}", artis_id));
|
path: path.to_string(),
|
||||||
node
|
title: album_data.title,
|
||||||
|
parent: Some(parent),
|
||||||
|
tracks,
|
||||||
|
children: Vec::new(),
|
||||||
|
is_queable: true,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
TidalPath::PlaylistTrack { .. } | TidalPath::AlbumTrack { .. } => {
|
||||||
|
warn!(path, "get_lib_node called with a track path");
|
||||||
|
return Err(crabidy_core::ProviderError::MalformedPath);
|
||||||
}
|
}
|
||||||
_ => return Err(crabidy_core::ProviderError::MalformedUuid),
|
|
||||||
};
|
};
|
||||||
Ok(node)
|
Ok(node)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#[instrument]
|
/// The root of this provider in the global library tree.
|
||||||
fn split_uuid(uuid: &str) -> (String, String, String) {
|
pub const PROVIDER_ROOT: &str = "/tidal";
|
||||||
let mut split = uuid.splitn(3, ':');
|
|
||||||
(
|
/// A parsed tidal library path. The position in the tree is fully encoded
|
||||||
split.next().unwrap_or("").to_string(),
|
/// in the path itself.
|
||||||
split.next().unwrap_or("").to_string(),
|
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||||||
split.next().unwrap_or("").to_string(),
|
pub enum TidalPath<'a> {
|
||||||
)
|
Root,
|
||||||
|
Playlists,
|
||||||
|
Playlist(&'a str),
|
||||||
|
PlaylistTrack {
|
||||||
|
playlist: &'a str,
|
||||||
|
track: &'a str,
|
||||||
|
},
|
||||||
|
Artists,
|
||||||
|
Artist(&'a str),
|
||||||
|
Album {
|
||||||
|
artist: &'a str,
|
||||||
|
album: &'a str,
|
||||||
|
},
|
||||||
|
AlbumTrack {
|
||||||
|
artist: &'a str,
|
||||||
|
album: &'a str,
|
||||||
|
track: &'a str,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn parse_path(path: &str) -> Result<TidalPath<'_>, crabidy_core::ProviderError> {
|
||||||
|
let segments = crabidy_core::path_segments(path);
|
||||||
|
match segments.as_slice() {
|
||||||
|
["tidal"] => Ok(TidalPath::Root),
|
||||||
|
["tidal", "playlists"] => Ok(TidalPath::Playlists),
|
||||||
|
["tidal", "playlists", playlist] => Ok(TidalPath::Playlist(playlist)),
|
||||||
|
["tidal", "playlists", playlist, track] => Ok(TidalPath::PlaylistTrack { playlist, track }),
|
||||||
|
["tidal", "artists"] => Ok(TidalPath::Artists),
|
||||||
|
["tidal", "artists", artist] => Ok(TidalPath::Artist(artist)),
|
||||||
|
["tidal", "artists", artist, album] => Ok(TidalPath::Album { artist, album }),
|
||||||
|
["tidal", "artists", artist, album, track] => Ok(TidalPath::AlbumTrack {
|
||||||
|
artist,
|
||||||
|
album,
|
||||||
|
track,
|
||||||
|
}),
|
||||||
|
_ => {
|
||||||
|
warn!(path, "malformed tidal path");
|
||||||
|
Err(crabidy_core::ProviderError::MalformedPath)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn track_id_from_path(path: &str) -> Result<&str, crabidy_core::ProviderError> {
|
||||||
|
match parse_path(path)? {
|
||||||
|
TidalPath::PlaylistTrack { track, .. } | TidalPath::AlbumTrack { track, .. } => Ok(track),
|
||||||
|
_ => {
|
||||||
|
warn!(path, "expected a track path");
|
||||||
|
Err(crabidy_core::ProviderError::MalformedPath)
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Client {
|
impl Client {
|
||||||
|
|
@ -494,7 +575,6 @@ impl Client {
|
||||||
|
|
||||||
#[instrument(skip(self))]
|
#[instrument(skip(self))]
|
||||||
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);
|
|
||||||
self.make_request(&format!("tracks/{}", track_id), None)
|
self.make_request(&format!("tracks/{}", track_id), None)
|
||||||
.await
|
.await
|
||||||
}
|
}
|
||||||
|
|
@ -689,6 +769,7 @@ mod tests {
|
||||||
}
|
}
|
||||||
|
|
||||||
#[tokio::test]
|
#[tokio::test]
|
||||||
|
#[ignore = "requires a local tidal config and network access"]
|
||||||
async fn test() {
|
async fn test() {
|
||||||
let client = setup().await;
|
let client = setup().await;
|
||||||
let user = client.settings.login.user_id.clone().unwrap();
|
let user = client.settings.login.user_id.clone().unwrap();
|
||||||
|
|
|
||||||
|
|
@ -1,7 +1,6 @@
|
||||||
use std::{str::FromStr, string::FromUtf8Error};
|
use std::{str::FromStr, string::FromUtf8Error};
|
||||||
|
|
||||||
use base64::Engine as _;
|
use base64::Engine as _;
|
||||||
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;
|
||||||
|
|
@ -22,19 +21,6 @@ pub struct ArtistItem {
|
||||||
pub item: Artist,
|
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)]
|
#[derive(Default, Debug, Clone, PartialEq, Serialize, Deserialize)]
|
||||||
#[serde(rename_all = "camelCase")]
|
#[serde(rename_all = "camelCase")]
|
||||||
pub struct Artist {
|
pub struct Artist {
|
||||||
|
|
@ -48,29 +34,6 @@ pub struct Artist {
|
||||||
pub mixes: Option<ArtistMixes>,
|
pub mixes: Option<ArtistMixes>,
|
||||||
}
|
}
|
||||||
|
|
||||||
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)]
|
||||||
#[serde(rename_all = "camelCase")]
|
#[serde(rename_all = "camelCase")]
|
||||||
pub struct ArtistRole {
|
pub struct ArtistRole {
|
||||||
|
|
@ -230,36 +193,24 @@ pub struct Track {
|
||||||
pub album: Option<Album>,
|
pub album: Option<Album>,
|
||||||
pub mixes: Option<TrackMixes>,
|
pub mixes: Option<TrackMixes>,
|
||||||
}
|
}
|
||||||
impl From<Track> for crabidy_core::proto::crabidy::Track {
|
|
||||||
fn from(track: Track) -> Self {
|
impl Track {
|
||||||
Self {
|
/// Converts to the wire representation, placing the track under the
|
||||||
uuid: format!("track:{}", track.id),
|
/// given parent node path.
|
||||||
title: track.title,
|
pub fn to_proto(&self, parent_path: &str) -> crabidy_core::proto::crabidy::Track {
|
||||||
artist: match track.artist {
|
crabidy_core::proto::crabidy::Track {
|
||||||
Some(a) => a.name.clone(),
|
path: crabidy_core::join_path(parent_path, &self.id.to_string()),
|
||||||
None => "".to_string(),
|
title: self.title.clone(),
|
||||||
},
|
artist: self
|
||||||
album: track.album.map(|a| a.into()),
|
.artist
|
||||||
duration: track.duration.map(|d| d as u32 * 1000),
|
.as_ref()
|
||||||
|
.map(|a| a.name.clone())
|
||||||
|
.unwrap_or_default(),
|
||||||
|
album: self.album.clone().map(|a| a.into()),
|
||||||
|
duration: self.duration.map(|d| d as u32 * 1000),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl From<&Track> for crabidy_core::proto::crabidy::Track {
|
|
||||||
fn from(track: &Track) -> Self {
|
|
||||||
Self {
|
|
||||||
uuid: format!("track:{}", track.id),
|
|
||||||
title: track.title.clone(),
|
|
||||||
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)]
|
// #[derive(Default, Debug, Clone, PartialEq, Serialize, Deserialize)]
|
||||||
// #[serde(rename_all = "camelCase")]
|
// #[serde(rename_all = "camelCase")]
|
||||||
// pub struct Artist {
|
// pub struct Artist {
|
||||||
|
|
@ -356,39 +307,6 @@ pub struct Album {
|
||||||
pub artists: Option<Vec<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 {
|
||||||
fn from(album: Album) -> Self {
|
fn from(album: Album) -> Self {
|
||||||
Self {
|
Self {
|
||||||
|
|
@ -478,12 +396,6 @@ pub struct PlaylistAndFavorite {
|
||||||
pub playlist: Playlist,
|
pub playlist: Playlist,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl From<PlaylistAndFavorite> for crabidy_core::proto::crabidy::LibraryNode {
|
|
||||||
fn from(a: PlaylistAndFavorite) -> Self {
|
|
||||||
a.playlist.into()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Default, Debug, Clone, PartialEq, Serialize, Deserialize)]
|
#[derive(Default, Debug, Clone, PartialEq, Serialize, Deserialize)]
|
||||||
#[serde(rename_all = "camelCase")]
|
#[serde(rename_all = "camelCase")]
|
||||||
pub struct Playlist {
|
pub struct Playlist {
|
||||||
|
|
@ -507,19 +419,6 @@ pub struct Playlist {
|
||||||
pub last_item_added_at: Option<String>,
|
pub last_item_added_at: Option<String>,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl From<Playlist> for crabidy_core::proto::crabidy::LibraryNode {
|
|
||||||
fn from(a: Playlist) -> Self {
|
|
||||||
crabidy_core::proto::crabidy::LibraryNode {
|
|
||||||
title: a.title,
|
|
||||||
uuid: format!("node:playlist:{}", a.uuid),
|
|
||||||
tracks: Vec::new(),
|
|
||||||
parent: None,
|
|
||||||
children: Vec::new(),
|
|
||||||
is_queable: true,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Default, Debug, Clone, PartialEq, Serialize, Deserialize)]
|
#[derive(Default, Debug, Clone, PartialEq, Serialize, Deserialize)]
|
||||||
#[serde(rename_all = "camelCase")]
|
#[serde(rename_all = "camelCase")]
|
||||||
pub struct Creator {
|
pub struct Creator {
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue