Compare commits
20 Commits
v0.1.0-dev
...
main
| Author | SHA1 | Date |
|---|---|---|
|
|
a1987869c2 | |
|
|
d6406d1e09 | |
|
|
24e968ac08 | |
|
|
a4303b9b70 | |
|
|
efc36e88f8 | |
|
|
6db11131c0 | |
|
|
f8a77ee6ed | |
|
|
97ebb44ca5 | |
|
|
dfd2e0af92 | |
|
|
5c50544523 | |
|
|
73cc79d776 | |
|
|
a7c2fe391b | |
|
|
2f89886e5d | |
|
|
4b34fa7233 | |
|
|
902c0b903f | |
|
|
5d1a62c630 | |
|
|
02f47d682b | |
|
|
6ac13a710c | |
|
|
18671683ff | |
|
|
e71398d243 |
|
|
@ -3,14 +3,14 @@ run-name: CI release
|
||||||
on:
|
on:
|
||||||
push:
|
push:
|
||||||
tags:
|
tags:
|
||||||
- '*'
|
- 'v*'
|
||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
release:
|
release:
|
||||||
runs-on: rust-cross
|
runs-on: rust-cross
|
||||||
name: stable / cross-${{ matrix.target }}
|
name: stable / cross-${{ matrix.target }}
|
||||||
strategy:
|
strategy:
|
||||||
fail-fast: false
|
fail-fast: true
|
||||||
matrix:
|
matrix:
|
||||||
target: ["armv7-unknown-linux-gnueabihf", "aarch64-unknown-linux-gnu", "x86_64-unknown-linux-gnu"]
|
target: ["armv7-unknown-linux-gnueabihf", "aarch64-unknown-linux-gnu", "x86_64-unknown-linux-gnu"]
|
||||||
steps:
|
steps:
|
||||||
|
|
|
||||||
BIN
Draft.rnote
BIN
Draft.rnote
Binary file not shown.
|
|
@ -222,6 +222,7 @@ impl PlayerEngine {
|
||||||
SEEK_TO.store(time.as_secs(), Ordering::SeqCst);
|
SEEK_TO.store(time.as_secs(), Ordering::SeqCst);
|
||||||
// FIXME: ideally we would like to return once the seeking is successful
|
// FIXME: ideally we would like to return once the seeking is successful
|
||||||
// then return the current elapsed time
|
// then return the current elapsed time
|
||||||
|
// Cond-var might be needed to sleep this (seeking takes time)
|
||||||
Ok(time)
|
Ok(time)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,6 @@
|
||||||
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>);
|
||||||
|
|
|
||||||
|
|
@ -65,6 +65,7 @@ pub enum MessageFromUi {
|
||||||
InsertTracks(Vec<String>, usize),
|
InsertTracks(Vec<String>, usize),
|
||||||
RemoveTracks(Vec<usize>),
|
RemoveTracks(Vec<usize>),
|
||||||
ReplaceQueue(Vec<String>),
|
ReplaceQueue(Vec<String>),
|
||||||
|
ClearQueue(bool),
|
||||||
NextTrack,
|
NextTrack,
|
||||||
PrevTrack,
|
PrevTrack,
|
||||||
RestartTrack,
|
RestartTrack,
|
||||||
|
|
|
||||||
|
|
@ -45,10 +45,21 @@ impl NowPlaying {
|
||||||
}
|
}
|
||||||
pub fn update_track(&mut self, active: Option<Track>) {
|
pub fn update_track(&mut self, active: Option<Track>) {
|
||||||
if let Some(track) = &active {
|
if let Some(track) = &active {
|
||||||
|
let body = if let Some(ref album) = track.album {
|
||||||
|
format!(
|
||||||
|
"{} by {}\n\n{} ({})",
|
||||||
|
track.title,
|
||||||
|
track.artist,
|
||||||
|
album.title,
|
||||||
|
// FIXME: get out year and format differently if it's missing
|
||||||
|
album.release_date()
|
||||||
|
)
|
||||||
|
} else {
|
||||||
|
format!("{} by {}", track.title, track.artist,)
|
||||||
|
};
|
||||||
Notification::new()
|
Notification::new()
|
||||||
.summary("Crabidy playing")
|
.summary("Now playing")
|
||||||
// FIXME: album
|
.body(&body)
|
||||||
.body(&format!("{} by {}", track.title, track.artist))
|
|
||||||
.show()
|
.show()
|
||||||
.unwrap();
|
.unwrap();
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -41,6 +41,9 @@ impl Queue {
|
||||||
self.tx.send(MessageFromUi::SetCurrentTrack(pos));
|
self.tx.send(MessageFromUi::SetCurrentTrack(pos));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
pub fn select_current(&mut self) {
|
||||||
|
self.select(Some(self.current_position));
|
||||||
|
}
|
||||||
pub fn remove_track(&mut self) {
|
pub fn remove_track(&mut self) {
|
||||||
if let Some(pos) = self.selected() {
|
if let Some(pos) = self.selected() {
|
||||||
// FIXME: mark multiple tracks on queue and remove them
|
// FIXME: mark multiple tracks on queue and remove them
|
||||||
|
|
|
||||||
|
|
@ -1,9 +1,4 @@
|
||||||
use crabidy_core::{
|
use crabidy_core::{clap, clap_serde_derive, serde::Serialize, ClapSerde};
|
||||||
clap::{self},
|
|
||||||
clap_serde_derive,
|
|
||||||
serde::Serialize,
|
|
||||||
ClapSerde,
|
|
||||||
};
|
|
||||||
|
|
||||||
#[derive(ClapSerde, Serialize, Debug)]
|
#[derive(ClapSerde, Serialize, Debug)]
|
||||||
#[clap(author, version, about)]
|
#[clap(author, version, about)]
|
||||||
|
|
|
||||||
|
|
@ -123,6 +123,9 @@ async fn poll(
|
||||||
MessageFromUi::ToggleRepeat => {
|
MessageFromUi::ToggleRepeat => {
|
||||||
rpc_client.toggle_repeat().await?
|
rpc_client.toggle_repeat().await?
|
||||||
}
|
}
|
||||||
|
MessageFromUi::ClearQueue(exclude_current) => {
|
||||||
|
rpc_client.clear_queue(exclude_current).await?
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
Some(resp) = rpc_client.update_stream.next() => {
|
Some(resp) = rpc_client.update_stream.next() => {
|
||||||
|
|
@ -300,12 +303,21 @@ fn run_ui(tx: Sender<MessageFromUi>, rx: Receiver<MessageToUi>) {
|
||||||
(UiFocus::Queue, KeyModifiers::CONTROL, KeyCode::Char('u')) => {
|
(UiFocus::Queue, KeyModifiers::CONTROL, KeyCode::Char('u')) => {
|
||||||
app.queue.up();
|
app.queue.up();
|
||||||
}
|
}
|
||||||
|
(UiFocus::Queue, KeyModifiers::NONE, KeyCode::Char('o')) => {
|
||||||
|
app.queue.select_current();
|
||||||
|
}
|
||||||
(UiFocus::Queue, KeyModifiers::NONE, KeyCode::Enter) => {
|
(UiFocus::Queue, KeyModifiers::NONE, KeyCode::Enter) => {
|
||||||
app.queue.play_selected();
|
app.queue.play_selected();
|
||||||
}
|
}
|
||||||
(UiFocus::Queue, KeyModifiers::NONE, KeyCode::Char('d')) => {
|
(UiFocus::Queue, KeyModifiers::NONE, KeyCode::Char('d')) => {
|
||||||
app.queue.remove_track();
|
app.queue.remove_track();
|
||||||
}
|
}
|
||||||
|
(UiFocus::Queue, KeyModifiers::NONE, KeyCode::Char('c')) => {
|
||||||
|
tx.send(MessageFromUi::ClearQueue(true));
|
||||||
|
}
|
||||||
|
(UiFocus::Queue, KeyModifiers::SHIFT, KeyCode::Char('C')) => {
|
||||||
|
tx.send(MessageFromUi::ClearQueue(false));
|
||||||
|
}
|
||||||
_ => {}
|
_ => {}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,7 +1,7 @@
|
||||||
use crabidy_core::proto::crabidy::{
|
use crabidy_core::proto::crabidy::{
|
||||||
crabidy_service_client::CrabidyServiceClient, AppendRequest, ChangeVolumeRequest,
|
crabidy_service_client::CrabidyServiceClient, AppendRequest, ChangeVolumeRequest,
|
||||||
GetLibraryNodeRequest, GetUpdateStreamRequest, GetUpdateStreamResponse, InitRequest,
|
ClearQueueRequest, GetLibraryNodeRequest, GetUpdateStreamRequest, GetUpdateStreamResponse,
|
||||||
InitResponse, InsertRequest, LibraryNode, NextRequest, PrevRequest, QueueRequest,
|
InitRequest, InitResponse, InsertRequest, LibraryNode, NextRequest, PrevRequest, QueueRequest,
|
||||||
RemoveRequest, ReplaceRequest, RestartTrackRequest, SetCurrentRequest, ToggleMuteRequest,
|
RemoveRequest, ReplaceRequest, RestartTrackRequest, SetCurrentRequest, ToggleMuteRequest,
|
||||||
TogglePlayRequest, ToggleRepeatRequest, ToggleShuffleRequest,
|
TogglePlayRequest, ToggleRepeatRequest, ToggleShuffleRequest,
|
||||||
};
|
};
|
||||||
|
|
@ -128,6 +128,12 @@ impl RpcClient {
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub async fn clear_queue(&mut self, exclude_current: bool) -> Result<(), Box<dyn Error>> {
|
||||||
|
let clear_queue_request = Request::new(ClearQueueRequest { exclude_current });
|
||||||
|
self.client.clear_queue(clear_queue_request).await?;
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
pub async fn replace_queue(&mut self, uuids: Vec<String>) -> Result<(), Box<dyn Error>> {
|
pub async fn replace_queue(&mut self, uuids: Vec<String>) -> Result<(), Box<dyn Error>> {
|
||||||
let replace_request = Request::new(ReplaceRequest { uuids });
|
let replace_request = Request::new(ReplaceRequest { uuids });
|
||||||
self.client.replace(replace_request).await?;
|
self.client.replace(replace_request).await?;
|
||||||
|
|
|
||||||
|
|
@ -14,6 +14,7 @@ service CrabidyService {
|
||||||
rpc Append(AppendRequest) returns (AppendResponse);
|
rpc Append(AppendRequest) returns (AppendResponse);
|
||||||
rpc Remove(RemoveRequest) returns (RemoveResponse);
|
rpc Remove(RemoveRequest) returns (RemoveResponse);
|
||||||
rpc Insert(InsertRequest) returns (InsertResponse);
|
rpc Insert(InsertRequest) returns (InsertResponse);
|
||||||
|
rpc ClearQueue(ClearQueueRequest) returns (ClearQueueResponse);
|
||||||
rpc SetCurrent(SetCurrentRequest) returns (SetCurrentResponse);
|
rpc SetCurrent(SetCurrentRequest) returns (SetCurrentResponse);
|
||||||
rpc ToggleShuffle(ToggleShuffleRequest) returns (ToggleShuffleResponse);
|
rpc ToggleShuffle(ToggleShuffleRequest) returns (ToggleShuffleResponse);
|
||||||
rpc ToggleRepeat(ToggleRepeatRequest) returns (ToggleRepeatResponse);
|
rpc ToggleRepeat(ToggleRepeatRequest) returns (ToggleRepeatResponse);
|
||||||
|
|
@ -93,6 +94,11 @@ message SaveQueueRequest {
|
||||||
}
|
}
|
||||||
message SaveQueueResponse {}
|
message SaveQueueResponse {}
|
||||||
|
|
||||||
|
message ClearQueueRequest {
|
||||||
|
bool exclude_current = 1;
|
||||||
|
}
|
||||||
|
message ClearQueueResponse {}
|
||||||
|
|
||||||
// Stream
|
// Stream
|
||||||
message GetUpdateStreamRequest {}
|
message GetUpdateStreamRequest {}
|
||||||
message GetUpdateStreamResponse {
|
message GetUpdateStreamResponse {
|
||||||
|
|
|
||||||
|
|
@ -1,9 +1,9 @@
|
||||||
use crabidy_core::proto::crabidy::{Queue, Track};
|
use crabidy_core::proto::crabidy::{Queue, Track};
|
||||||
use rand::{seq::SliceRandom, thread_rng};
|
use rand::{seq::SliceRandom, thread_rng};
|
||||||
use std::time::SystemTime;
|
use std::time::SystemTime;
|
||||||
use tracing::debug;
|
use tracing::{debug, error};
|
||||||
|
|
||||||
#[derive(Clone)]
|
#[derive(Clone, Debug)]
|
||||||
pub struct QueueManager {
|
pub struct QueueManager {
|
||||||
created_at: SystemTime,
|
created_at: SystemTime,
|
||||||
current_offset: usize,
|
current_offset: usize,
|
||||||
|
|
@ -16,7 +16,11 @@ pub struct QueueManager {
|
||||||
impl From<QueueManager> for Queue {
|
impl From<QueueManager> for Queue {
|
||||||
fn from(queue_manager: QueueManager) -> Self {
|
fn from(queue_manager: QueueManager) -> Self {
|
||||||
Self {
|
Self {
|
||||||
timestamp: queue_manager.created_at.elapsed().unwrap().as_secs(),
|
timestamp: queue_manager
|
||||||
|
.created_at
|
||||||
|
.elapsed()
|
||||||
|
.expect("failed to get elapsed time")
|
||||||
|
.as_secs(),
|
||||||
current_position: queue_manager.current_position() as u32,
|
current_position: queue_manager.current_position() as u32,
|
||||||
tracks: queue_manager.tracks,
|
tracks: queue_manager.tracks,
|
||||||
}
|
}
|
||||||
|
|
@ -42,6 +46,10 @@ impl QueueManager {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub fn is_last_track(&self) -> bool {
|
||||||
|
self.current_position() == self.tracks.len() - 1
|
||||||
|
}
|
||||||
|
|
||||||
pub fn shuffle_on(&mut self) {
|
pub fn shuffle_on(&mut self) {
|
||||||
self.shuffle = true;
|
self.shuffle = true;
|
||||||
self.shuffle_before(self.current_offset);
|
self.shuffle_before(self.current_offset);
|
||||||
|
|
@ -50,6 +58,8 @@ impl QueueManager {
|
||||||
|
|
||||||
pub fn shuffle_off(&mut self) {
|
pub fn shuffle_off(&mut self) {
|
||||||
self.shuffle = false;
|
self.shuffle = false;
|
||||||
|
let pos = self.current_position();
|
||||||
|
self.current_offset = pos;
|
||||||
self.play_order = (0..self.tracks.len()).collect();
|
self.play_order = (0..self.tracks.len()).collect();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -75,6 +85,9 @@ impl QueueManager {
|
||||||
|
|
||||||
pub fn next_track(&mut self) -> Option<Track> {
|
pub fn next_track(&mut self) -> Option<Track> {
|
||||||
let len = self.tracks.len();
|
let len = self.tracks.len();
|
||||||
|
if len == 0 {
|
||||||
|
return None;
|
||||||
|
};
|
||||||
if self.current_offset < len - 1 {
|
if self.current_offset < len - 1 {
|
||||||
self.current_offset += 1;
|
self.current_offset += 1;
|
||||||
let current_pos = self.current_position();
|
let current_pos = self.current_position();
|
||||||
|
|
@ -111,11 +124,15 @@ impl QueueManager {
|
||||||
if self.shuffle {
|
if self.shuffle {
|
||||||
self.shuffle_all();
|
self.shuffle_all();
|
||||||
}
|
}
|
||||||
let current_offset = self
|
let Some(current_offset) = self
|
||||||
.play_order
|
.play_order
|
||||||
.iter()
|
.iter()
|
||||||
.position(|&i| i == current_position as usize)
|
.position(|&i| i == current_position as usize)
|
||||||
.unwrap();
|
else {
|
||||||
|
error!("invalid current position");
|
||||||
|
error!("queue: {:#?}", self);
|
||||||
|
return false
|
||||||
|
};
|
||||||
if self.shuffle {
|
if self.shuffle {
|
||||||
self.play_order.swap(0, current_offset);
|
self.play_order.swap(0, current_offset);
|
||||||
self.current_offset = 0;
|
self.current_offset = 0;
|
||||||
|
|
@ -142,36 +159,19 @@ impl QueueManager {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn append_tracks(&mut self, tracks: &[Track]) {
|
pub fn append_tracks(&mut self, tracks: &[Track]) -> Option<Track> {
|
||||||
let len = self.tracks.len();
|
let len = self.tracks.len();
|
||||||
|
let is_empty = len == 0;
|
||||||
let order_additions: Vec<usize> = (len..len + tracks.len()).collect();
|
let order_additions: Vec<usize> = (len..len + tracks.len()).collect();
|
||||||
self.play_order.extend(order_additions);
|
self.play_order.extend(order_additions);
|
||||||
self.tracks.extend(tracks.iter().cloned());
|
self.tracks.extend(tracks.iter().cloned());
|
||||||
if self.shuffle {
|
if self.shuffle {
|
||||||
self.shuffle_behind(self.current_offset);
|
self.shuffle_behind(self.current_offset);
|
||||||
}
|
}
|
||||||
}
|
if is_empty {
|
||||||
|
self.current_track()
|
||||||
pub fn queue_tracks(&mut self, tracks: &[Track]) {
|
} else {
|
||||||
let len = self.tracks.len();
|
None
|
||||||
if len == 0 {
|
|
||||||
self.replace_with_tracks(tracks);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
let pos = self.current_position();
|
|
||||||
let order_additions: Vec<usize> = (len..len + tracks.len()).collect();
|
|
||||||
self.play_order.extend(order_additions);
|
|
||||||
let tail: Vec<Track> = self
|
|
||||||
.tracks
|
|
||||||
.splice((self.current_position() + 1).., tracks.to_vec())
|
|
||||||
.collect();
|
|
||||||
self.tracks.extend(tail);
|
|
||||||
self.play_order
|
|
||||||
.iter_mut()
|
|
||||||
.filter(|i| (pos as usize) < **i)
|
|
||||||
.for_each(|i| *i += len);
|
|
||||||
if self.shuffle {
|
|
||||||
self.shuffle_behind(self.current_offset);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -184,11 +184,15 @@ impl QueueManager {
|
||||||
if *pos == self.current_position() as u32 {
|
if *pos == self.current_position() as u32 {
|
||||||
play_next = true;
|
play_next = true;
|
||||||
}
|
}
|
||||||
let offset = self
|
let Some(offset) = self
|
||||||
.play_order
|
.play_order
|
||||||
.iter()
|
.iter()
|
||||||
.position(|&i| i == *pos as usize)
|
.position(|&i| i == *pos as usize)
|
||||||
.unwrap();
|
else {
|
||||||
|
error!("invalid current position");
|
||||||
|
error!("queue: {:#?}", self);
|
||||||
|
return None
|
||||||
|
};
|
||||||
if offset < self.current_offset {
|
if offset < self.current_offset {
|
||||||
self.current_offset -= 1;
|
self.current_offset -= 1;
|
||||||
}
|
}
|
||||||
|
|
@ -206,8 +210,11 @@ impl QueueManager {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn insert_tracks(&mut self, position: u32, tracks: &[Track]) {
|
pub fn insert_tracks(&mut self, position: u32, tracks: &[Track]) -> Option<Track> {
|
||||||
let len = self.tracks.len();
|
let len = self.tracks.len();
|
||||||
|
if len == 0 {
|
||||||
|
return self.replace_with_tracks(tracks);
|
||||||
|
}
|
||||||
let order_additions: Vec<usize> = (len..len + tracks.len()).collect();
|
let order_additions: Vec<usize> = (len..len + tracks.len()).collect();
|
||||||
self.play_order.extend(order_additions);
|
self.play_order.extend(order_additions);
|
||||||
let tail: Vec<Track> = self
|
let tail: Vec<Track> = self
|
||||||
|
|
@ -215,9 +222,57 @@ impl QueueManager {
|
||||||
.splice((position as usize + 1).., tracks.to_vec())
|
.splice((position as usize + 1).., tracks.to_vec())
|
||||||
.collect();
|
.collect();
|
||||||
self.tracks.extend(tail);
|
self.tracks.extend(tail);
|
||||||
|
let mut changed: Vec<usize> = Vec::new();
|
||||||
|
// in shuffle mode, it might be that we played already postions which are behind
|
||||||
|
// the insertion point and which postions are shifted by the lenght of the inserted
|
||||||
|
// track
|
||||||
|
for i in self
|
||||||
|
.play_order
|
||||||
|
.iter_mut()
|
||||||
|
.take(self.current_offset)
|
||||||
|
.filter(|i| (position as usize) < **i)
|
||||||
|
{
|
||||||
|
*i += len;
|
||||||
|
changed.push(*i);
|
||||||
|
}
|
||||||
|
if !self.shuffle {
|
||||||
|
// if we don't shuffle, there should be no positions alredy played behind the
|
||||||
|
// current track
|
||||||
|
assert!(changed.is_empty());
|
||||||
|
}
|
||||||
|
// the newly inserted indices need to replaced with the ones that we already handled
|
||||||
|
self.play_order
|
||||||
|
.iter_mut()
|
||||||
|
.skip(self.current_offset)
|
||||||
|
.for_each(|i| {
|
||||||
|
if changed.contains(i) {
|
||||||
|
*i -= len;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
if self.shuffle {
|
if self.shuffle {
|
||||||
self.shuffle_behind(self.current_offset);
|
self.shuffle_behind(self.current_offset);
|
||||||
}
|
}
|
||||||
|
None
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn queue_tracks(&mut self, tracks: &[Track]) -> Option<Track> {
|
||||||
|
let pos = self.current_position();
|
||||||
|
self.insert_tracks(pos as u32, tracks)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn clear(&mut self, exclude_current: bool) -> bool {
|
||||||
|
let current_track = self.current_track();
|
||||||
|
self.current_offset = 0;
|
||||||
|
self.tracks.clear();
|
||||||
|
|
||||||
|
if exclude_current {
|
||||||
|
if let Some(track) = current_track {
|
||||||
|
self.tracks.push(track);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
!exclude_current
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -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, info, instrument, warn, Span};
|
use tracing::{debug_span, error, info, instrument, level_filters, warn, Span};
|
||||||
use tracing_subscriber::{filter::Targets, prelude::*};
|
use tracing_subscriber::{filter::Targets, prelude::*};
|
||||||
|
|
||||||
mod playback;
|
mod playback;
|
||||||
|
|
@ -22,8 +22,9 @@ 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 =
|
let targets_filter = Targets::new()
|
||||||
Targets::new().with_target("crabidy_server", tracing::level_filters::LevelFilter::DEBUG);
|
.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)
|
||||||
|
|
@ -39,7 +40,9 @@ async fn main() -> Result<(), Box<dyn std::error::Error>> {
|
||||||
info!("audio player started initialized");
|
info!("audio player started initialized");
|
||||||
|
|
||||||
let (update_tx, _) = tokio::sync::broadcast::channel(2048);
|
let (update_tx, _) = tokio::sync::broadcast::channel(2048);
|
||||||
let orchestrator = ProviderOrchestrator::init("").await.unwrap();
|
let orchestrator = ProviderOrchestrator::init("")
|
||||||
|
.await
|
||||||
|
.expect("failed to init orchestrator");
|
||||||
|
|
||||||
let playback = Playback::new(update_tx.clone(), orchestrator.provider_tx.clone());
|
let playback = Playback::new(update_tx.clone(), orchestrator.provider_tx.clone());
|
||||||
|
|
||||||
|
|
@ -76,44 +79,51 @@ fn poll_play_bus(rx: flume::Receiver<PlayerMessage>, tx: flume::Sender<PlaybackM
|
||||||
let span = debug_span!("play-chan");
|
let span = debug_span!("play-chan");
|
||||||
match msg {
|
match msg {
|
||||||
PlayerMessage::EndOfStream => {
|
PlayerMessage::EndOfStream => {
|
||||||
tx.send(PlaybackMessage::Next { span }).unwrap();
|
if let Err(err) = tx.send(PlaybackMessage::Next { span }) {
|
||||||
|
error!("failed to send next message: {}", err);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
PlayerMessage::Stopped => {
|
PlayerMessage::Stopped => {
|
||||||
tx.send(PlaybackMessage::StateChanged {
|
if let Err(err) = tx.send(PlaybackMessage::StateChanged {
|
||||||
state: PlayState::Stopped,
|
state: PlayState::Stopped,
|
||||||
span,
|
span,
|
||||||
})
|
}) {
|
||||||
.unwrap();
|
error!("failed to send stopped message: {}", err);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
PlayerMessage::Paused => {
|
PlayerMessage::Paused => {
|
||||||
tx.send(PlaybackMessage::StateChanged {
|
if let Err(err) = tx.send(PlaybackMessage::StateChanged {
|
||||||
state: PlayState::Paused,
|
state: PlayState::Paused,
|
||||||
span,
|
span,
|
||||||
})
|
}) {
|
||||||
.unwrap();
|
error!("failed to send paused message: {}", err);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
PlayerMessage::Playing => {
|
PlayerMessage::Playing => {
|
||||||
tx.send(PlaybackMessage::StateChanged {
|
if let Err(err) = tx.send(PlaybackMessage::StateChanged {
|
||||||
state: PlayState::Playing,
|
state: PlayState::Playing,
|
||||||
span,
|
span,
|
||||||
})
|
}) {
|
||||||
.unwrap();
|
error!("failed to send playing message: {}", err);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
PlayerMessage::Elapsed { duration, elapsed } => {
|
PlayerMessage::Elapsed { duration, elapsed } => {
|
||||||
tx.send(PlaybackMessage::PostitionChanged {
|
if let Err(err) = tx.send(PlaybackMessage::PostitionChanged {
|
||||||
duration: duration.as_millis() as u32,
|
duration: duration.as_millis() as u32,
|
||||||
position: elapsed.as_millis() as u32,
|
position: elapsed.as_millis() as u32,
|
||||||
span,
|
span,
|
||||||
})
|
}) {
|
||||||
.unwrap();
|
error!("failed to send elapsed message: {}", err);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
PlayerMessage::Duration { duration } => {
|
PlayerMessage::Duration { duration } => {
|
||||||
tx.send(PlaybackMessage::PostitionChanged {
|
if let Err(err) = tx.send(PlaybackMessage::PostitionChanged {
|
||||||
duration: duration.as_millis() as u32,
|
duration: duration.as_millis() as u32,
|
||||||
position: 0,
|
position: 0,
|
||||||
span,
|
span,
|
||||||
})
|
}) {
|
||||||
.unwrap();
|
error!("failed to send duration message: {}", err);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -170,6 +180,10 @@ pub enum PlaybackMessage {
|
||||||
uuids: Vec<String>,
|
uuids: Vec<String>,
|
||||||
span: Span,
|
span: Span,
|
||||||
},
|
},
|
||||||
|
Clear {
|
||||||
|
exclude_current: bool,
|
||||||
|
span: Span,
|
||||||
|
},
|
||||||
SetCurrent {
|
SetCurrent {
|
||||||
position: u32,
|
position: u32,
|
||||||
span: Span,
|
span: Span,
|
||||||
|
|
|
||||||
|
|
@ -51,7 +51,10 @@ impl Playback {
|
||||||
let repeat;
|
let repeat;
|
||||||
let shuffle;
|
let shuffle;
|
||||||
let response = {
|
let response = {
|
||||||
let queue = self.queue.lock().unwrap();
|
let Ok(queue) = self.queue.lock() else {
|
||||||
|
error!("failed to get queue lock");
|
||||||
|
continue;
|
||||||
|
};
|
||||||
debug!("got queue lock");
|
debug!("got queue lock");
|
||||||
repeat = queue.repeat;
|
repeat = queue.repeat;
|
||||||
shuffle = queue.shuffle;
|
shuffle = queue.shuffle;
|
||||||
|
|
@ -69,13 +72,11 @@ impl Playback {
|
||||||
trace!("position {:?}", position);
|
trace!("position {:?}", position);
|
||||||
let play_state = {
|
let play_state = {
|
||||||
debug!("getting play state lock");
|
debug!("getting play state lock");
|
||||||
match *self.state.lock().unwrap() {
|
let Ok(play_state) = self.state.lock() else {
|
||||||
PlayState::Playing => PlayState::Playing,
|
error!("failed to get play state lock");
|
||||||
PlayState::Paused => PlayState::Paused,
|
continue;
|
||||||
PlayState::Stopped => PlayState::Stopped,
|
};
|
||||||
PlayState::Loading => PlayState::Loading,
|
*play_state
|
||||||
_ => PlayState::Unspecified,
|
|
||||||
}
|
|
||||||
};
|
};
|
||||||
trace!("play_state {:?}", play_state);
|
trace!("play_state {:?}", play_state);
|
||||||
debug!("released play state lock");
|
debug!("released play state lock");
|
||||||
|
|
@ -90,7 +91,9 @@ impl Playback {
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
trace!("response {:?}", response);
|
trace!("response {:?}", response);
|
||||||
result_tx.send(response).unwrap();
|
if let Err(err) = result_tx.send(response) {
|
||||||
|
error!("failed to send response: {:#?}", err);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
PlaybackMessage::Replace { uuids, span } => {
|
PlaybackMessage::Replace { uuids, span } => {
|
||||||
let _e = span.enter();
|
let _e = span.enter();
|
||||||
|
|
@ -108,12 +111,17 @@ impl Playback {
|
||||||
}
|
}
|
||||||
trace!("got tracks {:?}", all_tracks);
|
trace!("got tracks {:?}", all_tracks);
|
||||||
let current = {
|
let current = {
|
||||||
let mut queue = self.queue.lock().unwrap();
|
let Ok(mut queue) = self.queue.lock() else {
|
||||||
|
debug!("got queue lock");
|
||||||
|
continue;
|
||||||
|
};
|
||||||
debug!("got queue lock");
|
debug!("got queue lock");
|
||||||
queue.replace_with_tracks(&all_tracks);
|
queue.replace_with_tracks(&all_tracks);
|
||||||
let queue_update_tx = self.update_tx.clone();
|
let queue_update_tx = self.update_tx.clone();
|
||||||
let update = StreamUpdate::Queue(queue.clone().into());
|
let update = StreamUpdate::Queue(queue.clone().into());
|
||||||
queue_update_tx.send(update).unwrap();
|
if let Err(err) = queue_update_tx.send(update) {
|
||||||
|
trace!("{:?}", err)
|
||||||
|
};
|
||||||
queue.current_track()
|
queue.current_track()
|
||||||
};
|
};
|
||||||
debug!("got current {:?}", current);
|
debug!("got current {:?}", current);
|
||||||
|
|
@ -135,17 +143,22 @@ impl Playback {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
trace!("got tracks {:?}", all_tracks);
|
trace!("got tracks {:?}", all_tracks);
|
||||||
{
|
let track = {
|
||||||
let mut queue = self.queue.lock().unwrap();
|
let Ok(mut queue) = self.queue.lock() else {
|
||||||
debug!("got queue lock");
|
debug!("got queue lock");
|
||||||
queue.queue_tracks(&all_tracks);
|
continue;
|
||||||
|
};
|
||||||
|
debug!("got queue lock");
|
||||||
|
let track = queue.queue_tracks(&all_tracks);
|
||||||
let queue_update_tx = self.update_tx.clone();
|
let queue_update_tx = self.update_tx.clone();
|
||||||
let update = StreamUpdate::Queue(queue.clone().into());
|
let update = StreamUpdate::Queue(queue.clone().into());
|
||||||
if let Err(err) = queue_update_tx.send(update) {
|
if let Err(err) = queue_update_tx.send(update) {
|
||||||
error!("{:?}", err)
|
trace!("{:?}", err)
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
track
|
||||||
|
};
|
||||||
debug!("que lock released");
|
debug!("que lock released");
|
||||||
|
self.play(track).in_current_span().await;
|
||||||
}
|
}
|
||||||
|
|
||||||
PlaybackMessage::Append { uuids, span } => {
|
PlaybackMessage::Append { uuids, span } => {
|
||||||
|
|
@ -163,37 +176,61 @@ impl Playback {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
trace!("got tracks {:?}", all_tracks);
|
trace!("got tracks {:?}", all_tracks);
|
||||||
{
|
let track = {
|
||||||
let mut queue = self.queue.lock().unwrap();
|
let Ok(mut queue) = self.queue.lock() else {
|
||||||
debug!("got queue lock");
|
debug!("got queue lock");
|
||||||
queue.append_tracks(&all_tracks);
|
continue;
|
||||||
|
};
|
||||||
|
debug!("got queue lock");
|
||||||
|
let track = queue.append_tracks(&all_tracks);
|
||||||
let queue_update_tx = self.update_tx.clone();
|
let queue_update_tx = self.update_tx.clone();
|
||||||
let update = StreamUpdate::Queue(queue.clone().into());
|
let update = StreamUpdate::Queue(queue.clone().into());
|
||||||
if let Err(err) = queue_update_tx.send(update) {
|
if let Err(err) = queue_update_tx.send(update) {
|
||||||
error!("{:?}", err)
|
trace!("{:?}", err)
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
track
|
||||||
|
};
|
||||||
debug!("queue lock released");
|
debug!("queue lock released");
|
||||||
|
self.play(track).in_current_span().await;
|
||||||
}
|
}
|
||||||
|
|
||||||
PlaybackMessage::Remove { positions, span } => {
|
PlaybackMessage::Remove { positions, span } => {
|
||||||
let _e = span.enter();
|
let _e = span.enter();
|
||||||
|
let is_last;
|
||||||
debug!("removing");
|
debug!("removing");
|
||||||
let track = {
|
let track = {
|
||||||
let mut queue = self.queue.lock().unwrap();
|
let Ok(mut queue) = self.queue.lock() else {
|
||||||
debug!("got queue lock");
|
debug!("got queue lock");
|
||||||
|
continue;
|
||||||
|
};
|
||||||
|
debug!("got queue lock");
|
||||||
|
is_last = queue.is_last_track();
|
||||||
let track = queue.remove_tracks(&positions);
|
let track = queue.remove_tracks(&positions);
|
||||||
let queue_update_tx = self.update_tx.clone();
|
let queue_update_tx = self.update_tx.clone();
|
||||||
let update = StreamUpdate::Queue(queue.clone().into());
|
let update = StreamUpdate::Queue(queue.clone().into());
|
||||||
queue_update_tx.send(update).unwrap();
|
if let Err(err) = queue_update_tx.send(update) {
|
||||||
|
trace!("{:?}", err)
|
||||||
|
};
|
||||||
track
|
track
|
||||||
};
|
};
|
||||||
debug!("queue lock released");
|
debug!("queue lock released");
|
||||||
let state = *self.state.lock().unwrap();
|
let state = {
|
||||||
|
let Ok(state) = self.state.lock() else {
|
||||||
|
error!("failed to get play state lock");
|
||||||
|
continue;
|
||||||
|
};
|
||||||
|
*state
|
||||||
|
};
|
||||||
if state == PlayState::Playing {
|
if state == PlayState::Playing {
|
||||||
|
if is_last {
|
||||||
|
if let Err(err) = self.player.stop().in_current_span().await {
|
||||||
|
error!("{:?}", err)
|
||||||
|
}
|
||||||
|
} else {
|
||||||
self.play(track).in_current_span().await;
|
self.play(track).in_current_span().await;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
PlaybackMessage::Insert {
|
PlaybackMessage::Insert {
|
||||||
position,
|
position,
|
||||||
|
|
@ -214,15 +251,50 @@ impl Playback {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
trace!("got tracks {:?}", all_tracks);
|
trace!("got tracks {:?}", all_tracks);
|
||||||
{
|
let track = {
|
||||||
let mut queue = self.queue.lock().unwrap();
|
let Ok(mut queue) = self.queue.lock() else {
|
||||||
debug!("got queue lock");
|
debug!("got queue lock");
|
||||||
queue.insert_tracks(position, &all_tracks);
|
continue;
|
||||||
|
};
|
||||||
|
debug!("got queue lock");
|
||||||
|
let track = queue.insert_tracks(position, &all_tracks);
|
||||||
let queue_update_tx = self.update_tx.clone();
|
let queue_update_tx = self.update_tx.clone();
|
||||||
let update = StreamUpdate::Queue(queue.clone().into());
|
let update = StreamUpdate::Queue(queue.clone().into());
|
||||||
queue_update_tx.send(update).unwrap();
|
if let Err(err) = queue_update_tx.send(update) {
|
||||||
}
|
trace!("{:?}", err)
|
||||||
|
};
|
||||||
|
track
|
||||||
|
};
|
||||||
debug!("queue lock released");
|
debug!("queue lock released");
|
||||||
|
self.play(track).in_current_span().await;
|
||||||
|
}
|
||||||
|
|
||||||
|
PlaybackMessage::Clear {
|
||||||
|
exclude_current,
|
||||||
|
span,
|
||||||
|
} => {
|
||||||
|
let _e = span.enter();
|
||||||
|
debug!("clearing");
|
||||||
|
let should_stop = {
|
||||||
|
let Ok(mut queue) = self.queue.lock() else {
|
||||||
|
debug!("got queue lock");
|
||||||
|
continue;
|
||||||
|
};
|
||||||
|
debug!("got queue lock");
|
||||||
|
let should_stop = queue.clear(exclude_current);
|
||||||
|
let queue_update_tx = self.update_tx.clone();
|
||||||
|
let update = StreamUpdate::Queue(queue.clone().into());
|
||||||
|
if let Err(err) = queue_update_tx.send(update) {
|
||||||
|
trace!("{:?}", err)
|
||||||
|
};
|
||||||
|
should_stop
|
||||||
|
};
|
||||||
|
debug!("queue lock released");
|
||||||
|
if should_stop {
|
||||||
|
if let Err(err) = self.player.stop().in_current_span().await {
|
||||||
|
error!("{:?}", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
PlaybackMessage::SetCurrent {
|
PlaybackMessage::SetCurrent {
|
||||||
|
|
@ -232,7 +304,10 @@ impl Playback {
|
||||||
let _e = span.enter();
|
let _e = span.enter();
|
||||||
debug!("setting current");
|
debug!("setting current");
|
||||||
let track = {
|
let track = {
|
||||||
let mut queue = self.queue.lock().unwrap();
|
let Ok(mut queue) = self.queue.lock() else {
|
||||||
|
debug!("got queue lock");
|
||||||
|
continue;
|
||||||
|
};
|
||||||
debug!("got queue lock");
|
debug!("got queue lock");
|
||||||
queue.set_current_position(queue_position);
|
queue.set_current_position(queue_position);
|
||||||
queue.current_track()
|
queue.current_track()
|
||||||
|
|
@ -247,7 +322,10 @@ impl Playback {
|
||||||
let shuffle;
|
let shuffle;
|
||||||
let repeat;
|
let repeat;
|
||||||
{
|
{
|
||||||
let mut queue = self.queue.lock().unwrap();
|
let Ok(mut queue) = self.queue.lock() else {
|
||||||
|
debug!("got queue lock");
|
||||||
|
continue;
|
||||||
|
};
|
||||||
debug!("got queue lock");
|
debug!("got queue lock");
|
||||||
repeat = queue.repeat;
|
repeat = queue.repeat;
|
||||||
if queue.shuffle {
|
if queue.shuffle {
|
||||||
|
|
@ -261,7 +339,7 @@ impl Playback {
|
||||||
let queue_update_tx = self.update_tx.clone();
|
let queue_update_tx = self.update_tx.clone();
|
||||||
let update = StreamUpdate::Mods(QueueModifiers { shuffle, repeat });
|
let update = StreamUpdate::Mods(QueueModifiers { shuffle, repeat });
|
||||||
if let Err(err) = queue_update_tx.send(update) {
|
if let Err(err) = queue_update_tx.send(update) {
|
||||||
error!("{:?}", err)
|
trace!("{:?}", err)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -271,7 +349,10 @@ impl Playback {
|
||||||
let shuffle;
|
let shuffle;
|
||||||
let repeat;
|
let repeat;
|
||||||
{
|
{
|
||||||
let mut queue = self.queue.lock().unwrap();
|
let Ok(mut queue) = self.queue.lock() else {
|
||||||
|
debug!("got queue lock");
|
||||||
|
continue;
|
||||||
|
};
|
||||||
debug!("got queue lock");
|
debug!("got queue lock");
|
||||||
shuffle = queue.shuffle;
|
shuffle = queue.shuffle;
|
||||||
if queue.repeat {
|
if queue.repeat {
|
||||||
|
|
@ -285,7 +366,7 @@ impl Playback {
|
||||||
let queue_update_tx = self.update_tx.clone();
|
let queue_update_tx = self.update_tx.clone();
|
||||||
let update = StreamUpdate::Mods(QueueModifiers { shuffle, repeat });
|
let update = StreamUpdate::Mods(QueueModifiers { shuffle, repeat });
|
||||||
if let Err(err) = queue_update_tx.send(update) {
|
if let Err(err) = queue_update_tx.send(update) {
|
||||||
error!("{:?}", err)
|
trace!("{:?}", err)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -293,7 +374,13 @@ impl Playback {
|
||||||
let _e = span.enter();
|
let _e = span.enter();
|
||||||
debug!("toggling play");
|
debug!("toggling play");
|
||||||
{
|
{
|
||||||
let state = *self.state.lock().unwrap();
|
let state = {
|
||||||
|
let Ok(state) = self.state.lock() else {
|
||||||
|
debug!("got state lock");
|
||||||
|
continue;
|
||||||
|
};
|
||||||
|
*state
|
||||||
|
};
|
||||||
debug!("got state lock");
|
debug!("got state lock");
|
||||||
if state == PlayState::Playing {
|
if state == PlayState::Playing {
|
||||||
if let Err(err) = self.player.pause().await {
|
if let Err(err) = self.player.pause().await {
|
||||||
|
|
@ -337,7 +424,10 @@ impl Playback {
|
||||||
let _e = span.enter();
|
let _e = span.enter();
|
||||||
debug!("nexting");
|
debug!("nexting");
|
||||||
let track = {
|
let track = {
|
||||||
let mut queue = self.queue.lock().unwrap();
|
let Ok(mut queue) = self.queue.lock() else {
|
||||||
|
debug!("got queue lock");
|
||||||
|
continue;
|
||||||
|
};
|
||||||
debug!("got queue lock");
|
debug!("got queue lock");
|
||||||
queue.next_track()
|
queue.next_track()
|
||||||
};
|
};
|
||||||
|
|
@ -350,7 +440,10 @@ impl Playback {
|
||||||
let _e = span.enter();
|
let _e = span.enter();
|
||||||
debug!("preving");
|
debug!("preving");
|
||||||
let track = {
|
let track = {
|
||||||
let mut queue = self.queue.lock().unwrap();
|
let Ok(mut queue) = self.queue.lock() else {
|
||||||
|
debug!("got queue lock");
|
||||||
|
continue;
|
||||||
|
};
|
||||||
debug!("got queue lock");
|
debug!("got queue lock");
|
||||||
queue.prev_track()
|
queue.prev_track()
|
||||||
};
|
};
|
||||||
|
|
@ -363,14 +456,18 @@ impl Playback {
|
||||||
debug!("state changed");
|
debug!("state changed");
|
||||||
|
|
||||||
let play_state = {
|
let play_state = {
|
||||||
*self.state.lock().unwrap() = state;
|
let Ok(mut state_lock) = self.state.lock() else {
|
||||||
|
debug!("got state lock");
|
||||||
|
continue;
|
||||||
|
};
|
||||||
|
*state_lock = state;
|
||||||
state
|
state
|
||||||
};
|
};
|
||||||
debug!("released state lock and got play state {:?}", play_state);
|
debug!("released state lock and got play state {:?}", play_state);
|
||||||
let active_track_tx = self.update_tx.clone();
|
let active_track_tx = self.update_tx.clone();
|
||||||
let update = StreamUpdate::PlayState(play_state as i32);
|
let update = StreamUpdate::PlayState(play_state as i32);
|
||||||
if let Err(err) = active_track_tx.send(update) {
|
if let Err(err) = active_track_tx.send(update) {
|
||||||
error!("{:?}", err)
|
trace!("{:?}", err)
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -388,7 +485,7 @@ impl Playback {
|
||||||
let update_tx = self.update_tx.clone();
|
let update_tx = self.update_tx.clone();
|
||||||
let update = StreamUpdate::Volume(volume);
|
let update = StreamUpdate::Volume(volume);
|
||||||
if let Err(err) = update_tx.send(update) {
|
if let Err(err) = update_tx.send(update) {
|
||||||
error!("{:?}", err)
|
trace!("{:?}", err)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -398,7 +495,7 @@ impl Playback {
|
||||||
let update_tx = self.update_tx.clone();
|
let update_tx = self.update_tx.clone();
|
||||||
let update = StreamUpdate::Mute(muted);
|
let update = StreamUpdate::Mute(muted);
|
||||||
if let Err(err) = update_tx.send(update) {
|
if let Err(err) = update_tx.send(update) {
|
||||||
error!("{:?}", err)
|
trace!("{:?}", err)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -412,7 +509,7 @@ impl Playback {
|
||||||
let update_tx = self.update_tx.clone();
|
let update_tx = self.update_tx.clone();
|
||||||
let update = StreamUpdate::Position(TrackPosition { duration, position });
|
let update = StreamUpdate::Position(TrackPosition { duration, position });
|
||||||
if let Err(err) = update_tx.send(update) {
|
if let Err(err) = update_tx.send(update) {
|
||||||
error!("{:?}", err)
|
trace!("{:?}", err)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -422,6 +519,7 @@ impl Playback {
|
||||||
|
|
||||||
#[instrument(skip(self))]
|
#[instrument(skip(self))]
|
||||||
async fn flatten_node(&self, uuid: &str) -> Vec<Track> {
|
async fn flatten_node(&self, uuid: &str) -> Vec<Track> {
|
||||||
|
debug!("flattening node");
|
||||||
let tx = self.provider_tx.clone();
|
let tx = self.provider_tx.clone();
|
||||||
let (result_tx, result_rx) = flume::bounded(1);
|
let (result_tx, result_rx) = flume::bounded(1);
|
||||||
let span = debug_span!("prov-chan");
|
let span = debug_span!("prov-chan");
|
||||||
|
|
@ -464,6 +562,7 @@ impl Playback {
|
||||||
|
|
||||||
#[instrument(skip(self))]
|
#[instrument(skip(self))]
|
||||||
async fn get_urls_for_track(&self, uuid: &str) -> Result<Vec<String>, ProviderError> {
|
async fn get_urls_for_track(&self, uuid: &str) -> Result<Vec<String>, ProviderError> {
|
||||||
|
debug!("getting urls for track");
|
||||||
let tx = self.provider_tx.clone();
|
let tx = self.provider_tx.clone();
|
||||||
let (result_tx, result_rx) = flume::bounded(1);
|
let (result_tx, result_rx) = flume::bounded(1);
|
||||||
let span = tracing::trace_span!("prov-chan");
|
let span = tracing::trace_span!("prov-chan");
|
||||||
|
|
@ -484,6 +583,7 @@ impl Playback {
|
||||||
|
|
||||||
#[instrument(skip(self))]
|
#[instrument(skip(self))]
|
||||||
async fn play_or_stop(&self, track: Option<Track>) {
|
async fn play_or_stop(&self, track: Option<Track>) {
|
||||||
|
debug!("play or stop");
|
||||||
if let Some(track) = track {
|
if let Some(track) = track {
|
||||||
let mut uuid = track.uuid.clone();
|
let mut uuid = track.uuid.clone();
|
||||||
let urls = loop {
|
let urls = loop {
|
||||||
|
|
@ -492,7 +592,10 @@ impl Playback {
|
||||||
Err(err) => {
|
Err(err) => {
|
||||||
warn!("no urls found for track {:?}: {}", track.uuid, err);
|
warn!("no urls found for track {:?}: {}", track.uuid, err);
|
||||||
uuid = {
|
uuid = {
|
||||||
let mut queue = self.queue.lock().unwrap();
|
let Ok(mut queue) = self.queue.lock() else {
|
||||||
|
debug!("got queue lock");
|
||||||
|
continue;
|
||||||
|
};
|
||||||
if let Some(track) = queue.next_track() {
|
if let Some(track) = queue.next_track() {
|
||||||
track.uuid.clone()
|
track.uuid.clone()
|
||||||
} else {
|
} else {
|
||||||
|
|
@ -503,7 +606,10 @@ impl Playback {
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
{
|
{
|
||||||
let queue = self.queue.lock().unwrap();
|
let Ok(queue) = self.queue.lock() else {
|
||||||
|
error!("poisend queue lock");
|
||||||
|
return
|
||||||
|
};
|
||||||
let queue_update_tx = self.update_tx.clone();
|
let queue_update_tx = self.update_tx.clone();
|
||||||
let track = queue.current_track();
|
let track = queue.current_track();
|
||||||
let update = StreamUpdate::QueueTrack(QueueTrack {
|
let update = StreamUpdate::QueueTrack(QueueTrack {
|
||||||
|
|
@ -511,7 +617,7 @@ impl Playback {
|
||||||
track,
|
track,
|
||||||
});
|
});
|
||||||
if let Err(err) = queue_update_tx.send(update) {
|
if let Err(err) = queue_update_tx.send(update) {
|
||||||
error!("{:?}", err)
|
trace!("{:?}", err)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if let Err(err) = self.player.play(&urls[0]).await {
|
if let Err(err) = self.player.play(&urls[0]).await {
|
||||||
|
|
@ -524,6 +630,7 @@ impl Playback {
|
||||||
|
|
||||||
#[instrument(skip(self))]
|
#[instrument(skip(self))]
|
||||||
async fn play(&self, track: Option<Track>) {
|
async fn play(&self, track: Option<Track>) {
|
||||||
|
debug!("play");
|
||||||
if let Some(track) = track {
|
if let Some(track) = track {
|
||||||
let mut uuid = track.uuid.clone();
|
let mut uuid = track.uuid.clone();
|
||||||
let urls = loop {
|
let urls = loop {
|
||||||
|
|
@ -532,7 +639,10 @@ impl Playback {
|
||||||
Err(err) => {
|
Err(err) => {
|
||||||
warn!("no urls found for track {:?}: {}", track.uuid, err);
|
warn!("no urls found for track {:?}: {}", track.uuid, err);
|
||||||
uuid = {
|
uuid = {
|
||||||
let mut queue = self.queue.lock().unwrap();
|
let Ok(mut queue) = self.queue.lock() else {
|
||||||
|
debug!("poisend queue lock");
|
||||||
|
return;
|
||||||
|
};
|
||||||
if let Some(track) = queue.next_track() {
|
if let Some(track) = queue.next_track() {
|
||||||
track.uuid.clone()
|
track.uuid.clone()
|
||||||
} else {
|
} else {
|
||||||
|
|
@ -543,7 +653,10 @@ impl Playback {
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
{
|
{
|
||||||
let queue = self.queue.lock().unwrap();
|
let Ok(queue) = self.queue.lock() else {
|
||||||
|
error!("poisend queue lock");
|
||||||
|
return
|
||||||
|
};
|
||||||
let queue_update_tx = self.update_tx.clone();
|
let queue_update_tx = self.update_tx.clone();
|
||||||
let track = queue.current_track();
|
let track = queue.current_track();
|
||||||
let update = StreamUpdate::QueueTrack(QueueTrack {
|
let update = StreamUpdate::QueueTrack(QueueTrack {
|
||||||
|
|
@ -551,7 +664,7 @@ impl Playback {
|
||||||
track,
|
track,
|
||||||
});
|
});
|
||||||
if let Err(err) = queue_update_tx.send(update) {
|
if let Err(err) = queue_update_tx.send(update) {
|
||||||
error!("{:?}", err)
|
trace!("{:?}", err)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if let Err(err) = self.player.play(&urls[0]).await {
|
if let Err(err) = self.player.play(&urls[0]).await {
|
||||||
|
|
|
||||||
|
|
@ -28,11 +28,9 @@ impl ProviderOrchestrator {
|
||||||
} => {
|
} => {
|
||||||
let _e = span.enter();
|
let _e = span.enter();
|
||||||
let result = self.get_lib_node(&uuid).in_current_span().await;
|
let result = self.get_lib_node(&uuid).in_current_span().await;
|
||||||
result_tx
|
if let Err(err) = result_tx.send_async(result).in_current_span().await {
|
||||||
.send_async(result)
|
error!("failed to send result: {}", err);
|
||||||
.in_current_span()
|
}
|
||||||
.await
|
|
||||||
.unwrap();
|
|
||||||
}
|
}
|
||||||
ProviderMessage::GetTrack {
|
ProviderMessage::GetTrack {
|
||||||
uuid,
|
uuid,
|
||||||
|
|
@ -41,11 +39,9 @@ impl ProviderOrchestrator {
|
||||||
} => {
|
} => {
|
||||||
let _e = span.enter();
|
let _e = span.enter();
|
||||||
let result = self.get_metadata_for_track(&uuid).in_current_span().await;
|
let result = self.get_metadata_for_track(&uuid).in_current_span().await;
|
||||||
result_tx
|
if let Err(err) = result_tx.send_async(result).in_current_span().await {
|
||||||
.send_async(result)
|
error!("failed to send result: {}", err);
|
||||||
.in_current_span()
|
}
|
||||||
.await
|
|
||||||
.unwrap();
|
|
||||||
}
|
}
|
||||||
ProviderMessage::GetTrackUrls {
|
ProviderMessage::GetTrackUrls {
|
||||||
uuid,
|
uuid,
|
||||||
|
|
@ -54,11 +50,9 @@ impl ProviderOrchestrator {
|
||||||
} => {
|
} => {
|
||||||
let _e = span.enter();
|
let _e = span.enter();
|
||||||
let result = self.get_urls_for_track(&uuid).in_current_span().await;
|
let result = self.get_urls_for_track(&uuid).in_current_span().await;
|
||||||
result_tx
|
if let Err(err) = result_tx.send_async(result).in_current_span().await {
|
||||||
.send_async(result)
|
error!("failed to send result: {}", err);
|
||||||
.in_current_span()
|
}
|
||||||
.await
|
|
||||||
.unwrap();
|
|
||||||
}
|
}
|
||||||
ProviderMessage::FlattenNode {
|
ProviderMessage::FlattenNode {
|
||||||
uuid,
|
uuid,
|
||||||
|
|
@ -67,11 +61,9 @@ impl ProviderOrchestrator {
|
||||||
} => {
|
} => {
|
||||||
let _e = span.enter();
|
let _e = span.enter();
|
||||||
let result = self.flatten_node(&uuid).in_current_span().await;
|
let result = self.flatten_node(&uuid).in_current_span().await;
|
||||||
result_tx
|
if let Err(err) = result_tx.send_async(result).in_current_span().await {
|
||||||
.send_async(result)
|
error!("failed to send result: {}", err);
|
||||||
.in_current_span()
|
}
|
||||||
.await
|
|
||||||
.unwrap();
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -118,7 +110,7 @@ impl ProviderClient for ProviderOrchestrator {
|
||||||
tidaldy::Client::init(&raw_toml_settings)
|
tidaldy::Client::init(&raw_toml_settings)
|
||||||
.in_current_span()
|
.in_current_span()
|
||||||
.await
|
.await
|
||||||
.unwrap(),
|
.expect("Failed to init Tidal clienta"),
|
||||||
);
|
);
|
||||||
let new_toml_config = tidal_client.settings();
|
let new_toml_config = tidal_client.settings();
|
||||||
if let Err(err) = tokio::fs::write(&config_file, new_toml_config)
|
if let Err(err) = tokio::fs::write(&config_file, new_toml_config)
|
||||||
|
|
@ -140,6 +132,7 @@ impl ProviderClient for ProviderOrchestrator {
|
||||||
}
|
}
|
||||||
#[instrument(skip(self))]
|
#[instrument(skip(self))]
|
||||||
async fn get_urls_for_track(&self, track_uuid: &str) -> Result<Vec<String>, ProviderError> {
|
async fn get_urls_for_track(&self, track_uuid: &str) -> Result<Vec<String>, ProviderError> {
|
||||||
|
debug!("get_urls_for_track");
|
||||||
self.tidal_client
|
self.tidal_client
|
||||||
.get_urls_for_track(track_uuid)
|
.get_urls_for_track(track_uuid)
|
||||||
.in_current_span()
|
.in_current_span()
|
||||||
|
|
@ -155,6 +148,7 @@ 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);
|
||||||
|
|
@ -162,13 +156,16 @@ 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");
|
debug!("get_lib_node in provider manager");
|
||||||
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
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,13 +1,13 @@
|
||||||
use crate::{PlaybackMessage, ProviderMessage};
|
use crate::{PlaybackMessage, ProviderMessage};
|
||||||
use crabidy_core::proto::crabidy::{
|
use crabidy_core::proto::crabidy::{
|
||||||
crabidy_service_server::CrabidyService, get_update_stream_response::Update as StreamUpdate,
|
crabidy_service_server::CrabidyService, get_update_stream_response::Update as StreamUpdate,
|
||||||
AppendRequest, AppendResponse, ChangeVolumeRequest, ChangeVolumeResponse,
|
AppendRequest, AppendResponse, ChangeVolumeRequest, ChangeVolumeResponse, ClearQueueRequest,
|
||||||
GetLibraryNodeRequest, GetLibraryNodeResponse, GetUpdateStreamRequest, GetUpdateStreamResponse,
|
ClearQueueResponse, GetLibraryNodeRequest, GetLibraryNodeResponse, GetUpdateStreamRequest,
|
||||||
InitRequest, InitResponse, InsertRequest, InsertResponse, NextRequest, NextResponse,
|
GetUpdateStreamResponse, InitRequest, InitResponse, InsertRequest, InsertResponse, NextRequest,
|
||||||
PrevRequest, PrevResponse, QueueRequest, QueueResponse, RemoveRequest, RemoveResponse,
|
NextResponse, PrevRequest, PrevResponse, QueueRequest, QueueResponse, RemoveRequest,
|
||||||
ReplaceRequest, ReplaceResponse, RestartTrackRequest, RestartTrackResponse, SaveQueueRequest,
|
RemoveResponse, ReplaceRequest, ReplaceResponse, RestartTrackRequest, RestartTrackResponse,
|
||||||
SaveQueueResponse, SetCurrentRequest, SetCurrentResponse, StopRequest, StopResponse,
|
SaveQueueRequest, SaveQueueResponse, SetCurrentRequest, SetCurrentResponse, StopRequest,
|
||||||
ToggleMuteRequest, ToggleMuteResponse, TogglePlayRequest, TogglePlayResponse,
|
StopResponse, ToggleMuteRequest, ToggleMuteResponse, TogglePlayRequest, TogglePlayResponse,
|
||||||
ToggleRepeatRequest, ToggleRepeatResponse, ToggleShuffleRequest, ToggleShuffleResponse,
|
ToggleRepeatRequest, ToggleRepeatResponse, ToggleShuffleRequest, ToggleShuffleResponse,
|
||||||
};
|
};
|
||||||
use futures::TryStreamExt;
|
use futures::TryStreamExt;
|
||||||
|
|
@ -91,10 +91,16 @@ impl CrabidyService for RpcService {
|
||||||
.recv_async()
|
.recv_async()
|
||||||
.in_current_span()
|
.in_current_span()
|
||||||
.await
|
.await
|
||||||
.map_err(|_| Status::internal("Failed to receive response from provider channel"))?;
|
.map_err(|e| {
|
||||||
|
error!("{:?}", e);
|
||||||
|
Status::internal("Failed to receive response from provider channel")
|
||||||
|
})?;
|
||||||
match result {
|
match result {
|
||||||
Ok(node) => Ok(Response::new(GetLibraryNodeResponse { node: Some(node) })),
|
Ok(node) => Ok(Response::new(GetLibraryNodeResponse { node: Some(node) })),
|
||||||
Err(err) => Err(Status::internal(err.to_string())),
|
Err(err) => {
|
||||||
|
error!("{:?}", err);
|
||||||
|
Err(Status::internal(err.to_string()))
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -201,6 +207,28 @@ impl CrabidyService for RpcService {
|
||||||
Ok(Response::new(reply))
|
Ok(Response::new(reply))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[instrument(skip(self, request), fields(exclude_current))]
|
||||||
|
async fn clear_queue(
|
||||||
|
&self,
|
||||||
|
request: tonic::Request<ClearQueueRequest>,
|
||||||
|
) -> std::result::Result<tonic::Response<ClearQueueResponse>, tonic::Status> {
|
||||||
|
let exclude_current = request.into_inner().exclude_current;
|
||||||
|
Span::current().record("exclude_current", exclude_current);
|
||||||
|
debug!("Received clear_queue request");
|
||||||
|
let playback_tx = self.playback_tx.clone();
|
||||||
|
let span = debug_span!("play-chan");
|
||||||
|
playback_tx
|
||||||
|
.send_async(PlaybackMessage::Clear {
|
||||||
|
exclude_current,
|
||||||
|
span,
|
||||||
|
})
|
||||||
|
.in_current_span()
|
||||||
|
.await
|
||||||
|
.map_err(|_| Status::internal("Failed to send request via channel"))?;
|
||||||
|
let reply = ClearQueueResponse {};
|
||||||
|
Ok(Response::new(reply))
|
||||||
|
}
|
||||||
|
|
||||||
#[instrument(skip(self, request), fields(position))]
|
#[instrument(skip(self, request), fields(position))]
|
||||||
async fn set_current(
|
async fn set_current(
|
||||||
&self,
|
&self,
|
||||||
|
|
@ -228,11 +256,13 @@ impl CrabidyService for RpcService {
|
||||||
debug!("Received toggle_shuffle request");
|
debug!("Received toggle_shuffle request");
|
||||||
let playback_tx = self.playback_tx.clone();
|
let playback_tx = self.playback_tx.clone();
|
||||||
let span = debug_span!("play-chan");
|
let span = debug_span!("play-chan");
|
||||||
playback_tx
|
if let Err(err) = playback_tx
|
||||||
.send_async(PlaybackMessage::ToggleShuffle { span })
|
.send_async(PlaybackMessage::ToggleShuffle { span })
|
||||||
.in_current_span()
|
.in_current_span()
|
||||||
.await
|
.await
|
||||||
.unwrap();
|
{
|
||||||
|
error!("Failed to send request via channel: {}", err);
|
||||||
|
}
|
||||||
let reply = ToggleShuffleResponse {};
|
let reply = ToggleShuffleResponse {};
|
||||||
Ok(Response::new(reply))
|
Ok(Response::new(reply))
|
||||||
}
|
}
|
||||||
|
|
@ -245,11 +275,13 @@ impl CrabidyService for RpcService {
|
||||||
debug!("Received toggle_repeat request");
|
debug!("Received toggle_repeat request");
|
||||||
let playback_tx = self.playback_tx.clone();
|
let playback_tx = self.playback_tx.clone();
|
||||||
let span = debug_span!("play-chan");
|
let span = debug_span!("play-chan");
|
||||||
playback_tx
|
if let Err(err) = playback_tx
|
||||||
.send_async(PlaybackMessage::ToggleRepeat { span })
|
.send_async(PlaybackMessage::ToggleRepeat { span })
|
||||||
.in_current_span()
|
.in_current_span()
|
||||||
.await
|
.await
|
||||||
.unwrap();
|
{
|
||||||
|
error!("Failed to send request via channel: {}", err);
|
||||||
|
}
|
||||||
let reply = ToggleRepeatResponse {};
|
let reply = ToggleRepeatResponse {};
|
||||||
Ok(Response::new(reply))
|
Ok(Response::new(reply))
|
||||||
}
|
}
|
||||||
|
|
@ -278,6 +310,7 @@ impl CrabidyService for RpcService {
|
||||||
|
|
||||||
Ok(Response::new(Box::pin(output_stream)))
|
Ok(Response::new(Box::pin(output_stream)))
|
||||||
}
|
}
|
||||||
|
|
||||||
#[instrument(skip(self, _request))]
|
#[instrument(skip(self, _request))]
|
||||||
async fn save_queue(
|
async fn save_queue(
|
||||||
&self,
|
&self,
|
||||||
|
|
@ -297,11 +330,13 @@ impl CrabidyService for RpcService {
|
||||||
debug!("Received toggle_play request");
|
debug!("Received toggle_play request");
|
||||||
let playback_tx = self.playback_tx.clone();
|
let playback_tx = self.playback_tx.clone();
|
||||||
let span = debug_span!("play-chan");
|
let span = debug_span!("play-chan");
|
||||||
playback_tx
|
if let Err(err) = playback_tx
|
||||||
.send_async(PlaybackMessage::TogglePlay { span })
|
.send_async(PlaybackMessage::TogglePlay { span })
|
||||||
.in_current_span()
|
.in_current_span()
|
||||||
.await
|
.await
|
||||||
.unwrap();
|
{
|
||||||
|
error!("Failed to send request via channel: {}", err);
|
||||||
|
}
|
||||||
let reply = TogglePlayResponse {};
|
let reply = TogglePlayResponse {};
|
||||||
Ok(Response::new(reply))
|
Ok(Response::new(reply))
|
||||||
}
|
}
|
||||||
|
|
@ -314,11 +349,13 @@ impl CrabidyService for RpcService {
|
||||||
debug!("Received stop request");
|
debug!("Received stop request");
|
||||||
let playback_tx = self.playback_tx.clone();
|
let playback_tx = self.playback_tx.clone();
|
||||||
let span = debug_span!("play-chan");
|
let span = debug_span!("play-chan");
|
||||||
playback_tx
|
if let Err(err) = playback_tx
|
||||||
.send_async(PlaybackMessage::Stop { span })
|
.send_async(PlaybackMessage::Stop { span })
|
||||||
.in_current_span()
|
.in_current_span()
|
||||||
.await
|
.await
|
||||||
.unwrap();
|
{
|
||||||
|
error!("Failed to send request via channel: {}", err);
|
||||||
|
}
|
||||||
let reply = StopResponse {};
|
let reply = StopResponse {};
|
||||||
Ok(Response::new(reply))
|
Ok(Response::new(reply))
|
||||||
}
|
}
|
||||||
|
|
@ -333,11 +370,13 @@ impl CrabidyService for RpcService {
|
||||||
debug!("Received change_volume request");
|
debug!("Received change_volume request");
|
||||||
let playback_tx = self.playback_tx.clone();
|
let playback_tx = self.playback_tx.clone();
|
||||||
let span = debug_span!("play-chan");
|
let span = debug_span!("play-chan");
|
||||||
playback_tx
|
if let Err(err) = playback_tx
|
||||||
.send_async(PlaybackMessage::ChangeVolume { delta, span })
|
.send_async(PlaybackMessage::ChangeVolume { delta, span })
|
||||||
.in_current_span()
|
.in_current_span()
|
||||||
.await
|
.await
|
||||||
.unwrap();
|
{
|
||||||
|
error!("Failed to send request via channel: {}", err);
|
||||||
|
}
|
||||||
let reply = ChangeVolumeResponse {};
|
let reply = ChangeVolumeResponse {};
|
||||||
Ok(Response::new(reply))
|
Ok(Response::new(reply))
|
||||||
}
|
}
|
||||||
|
|
@ -350,11 +389,13 @@ impl CrabidyService for RpcService {
|
||||||
debug!("Received toggle_mute request");
|
debug!("Received toggle_mute request");
|
||||||
let playback_tx = self.playback_tx.clone();
|
let playback_tx = self.playback_tx.clone();
|
||||||
let span = debug_span!("play-chan");
|
let span = debug_span!("play-chan");
|
||||||
playback_tx
|
if let Err(err) = playback_tx
|
||||||
.send_async(PlaybackMessage::ToggleMute { span })
|
.send_async(PlaybackMessage::ToggleMute { span })
|
||||||
.in_current_span()
|
.in_current_span()
|
||||||
.await
|
.await
|
||||||
.unwrap();
|
{
|
||||||
|
error!("Failed to send request via channel: {}", err);
|
||||||
|
}
|
||||||
let reply = ToggleMuteResponse {};
|
let reply = ToggleMuteResponse {};
|
||||||
Ok(Response::new(reply))
|
Ok(Response::new(reply))
|
||||||
}
|
}
|
||||||
|
|
@ -367,11 +408,13 @@ impl CrabidyService for RpcService {
|
||||||
debug!("Received next request");
|
debug!("Received next request");
|
||||||
let playback_tx = self.playback_tx.clone();
|
let playback_tx = self.playback_tx.clone();
|
||||||
let span = debug_span!("play-chan");
|
let span = debug_span!("play-chan");
|
||||||
playback_tx
|
if let Err(err) = playback_tx
|
||||||
.send_async(PlaybackMessage::Next { span })
|
.send_async(PlaybackMessage::Next { span })
|
||||||
.in_current_span()
|
.in_current_span()
|
||||||
.await
|
.await
|
||||||
.unwrap();
|
{
|
||||||
|
error!("Failed to send request via channel: {}", err);
|
||||||
|
}
|
||||||
let reply = NextResponse {};
|
let reply = NextResponse {};
|
||||||
Ok(Response::new(reply))
|
Ok(Response::new(reply))
|
||||||
}
|
}
|
||||||
|
|
@ -384,11 +427,13 @@ impl CrabidyService for RpcService {
|
||||||
debug!("Received prev request");
|
debug!("Received prev request");
|
||||||
let playback_tx = self.playback_tx.clone();
|
let playback_tx = self.playback_tx.clone();
|
||||||
let span = debug_span!("play-chan");
|
let span = debug_span!("play-chan");
|
||||||
playback_tx
|
if let Err(err) = playback_tx
|
||||||
.send_async(PlaybackMessage::Prev { span })
|
.send_async(PlaybackMessage::Prev { span })
|
||||||
.in_current_span()
|
.in_current_span()
|
||||||
.await
|
.await
|
||||||
.unwrap();
|
{
|
||||||
|
error!("Failed to send request via channel: {}", err);
|
||||||
|
}
|
||||||
let reply = PrevResponse {};
|
let reply = PrevResponse {};
|
||||||
Ok(Response::new(reply))
|
Ok(Response::new(reply))
|
||||||
}
|
}
|
||||||
|
|
@ -401,11 +446,13 @@ impl CrabidyService for RpcService {
|
||||||
debug!("Received restart_track request");
|
debug!("Received restart_track request");
|
||||||
let playback_tx = self.playback_tx.clone();
|
let playback_tx = self.playback_tx.clone();
|
||||||
let span = debug_span!("play-chan");
|
let span = debug_span!("play-chan");
|
||||||
playback_tx
|
if let Err(err) = playback_tx
|
||||||
.send_async(PlaybackMessage::RestartTrack { span })
|
.send_async(PlaybackMessage::RestartTrack { span })
|
||||||
.in_current_span()
|
.in_current_span()
|
||||||
.await
|
.await
|
||||||
.unwrap();
|
{
|
||||||
|
error!("Failed to send request via channel: {}", err);
|
||||||
|
}
|
||||||
let reply = RestartTrackResponse {};
|
let reply = RestartTrackResponse {};
|
||||||
Ok(Response::new(reply))
|
Ok(Response::new(reply))
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,9 +1,11 @@
|
||||||
|
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, instrument};
|
use tracing::{debug, error, info, instrument};
|
||||||
pub mod config;
|
pub mod config;
|
||||||
pub mod models;
|
pub mod models;
|
||||||
use async_trait::async_trait;
|
use async_trait::async_trait;
|
||||||
|
|
@ -75,13 +77,20 @@ 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");
|
debug!("get_lib_root in tidaldy");
|
||||||
let global_root = crabidy_core::proto::crabidy::LibraryNode::new();
|
let global_root = crabidy_core::proto::crabidy::LibraryNode::new();
|
||||||
let children = vec![crabidy_core::proto::crabidy::LibraryNodeChild::new(
|
let children = vec![
|
||||||
|
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(),
|
||||||
|
|
@ -100,8 +109,9 @@ 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 {}", uuid);
|
debug!("get_lib_node in tidaldy{}", 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 {
|
||||||
|
|
@ -138,6 +148,53 @@ 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)
|
||||||
|
|
@ -166,17 +223,18 @@ impl Client {
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
#[instrument]
|
#[instrument(skip(self))]
|
||||||
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]
|
#[instrument(skip(self))]
|
||||||
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(),
|
||||||
|
|
@ -199,18 +257,27 @@ 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]
|
#[instrument(skip(self))]
|
||||||
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(),
|
||||||
|
|
@ -236,9 +303,17 @@ 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 {
|
||||||
|
|
@ -255,15 +330,23 @@ 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]
|
#[instrument(skip(self))]
|
||||||
pub async fn make_explorer_request(
|
pub async fn make_explorer_request(
|
||||||
&self,
|
&self,
|
||||||
uri: &str,
|
uri: &str,
|
||||||
|
|
@ -291,14 +374,22 @@ 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]
|
#[instrument(skip(self))]
|
||||||
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))
|
||||||
|
|
@ -306,7 +397,7 @@ impl Client {
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
#[instrument]
|
#[instrument(skip(self))]
|
||||||
pub async fn get_playlist_tracks(
|
pub async fn get_playlist_tracks(
|
||||||
&self,
|
&self,
|
||||||
playlist_uuid: &str,
|
playlist_uuid: &str,
|
||||||
|
|
@ -316,21 +407,35 @@ impl Client {
|
||||||
.await?)
|
.await?)
|
||||||
}
|
}
|
||||||
|
|
||||||
#[instrument]
|
#[instrument(skip(self))]
|
||||||
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]
|
#[instrument(skip(self))]
|
||||||
|
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]
|
#[instrument(skip(self))]
|
||||||
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,
|
||||||
|
|
@ -343,25 +448,7 @@ impl Client {
|
||||||
.await?)
|
.await?)
|
||||||
}
|
}
|
||||||
|
|
||||||
#[instrument]
|
#[instrument(skip(self))]
|
||||||
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),
|
||||||
|
|
@ -372,7 +459,18 @@ impl Client {
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
#[instrument]
|
#[instrument(skip(self))]
|
||||||
|
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),
|
||||||
|
|
@ -383,7 +481,19 @@ impl Client {
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
#[instrument]
|
#[instrument(skip(self))]
|
||||||
|
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()),
|
||||||
|
|
@ -397,14 +507,14 @@ impl Client {
|
||||||
.await
|
.await
|
||||||
}
|
}
|
||||||
|
|
||||||
#[instrument]
|
#[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);
|
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]
|
#[instrument(skip(self))]
|
||||||
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();
|
||||||
|
|
@ -443,7 +553,11 @@ 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()
|
||||||
{
|
{
|
||||||
|
|
@ -459,7 +573,7 @@ impl Client {
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
#[instrument]
|
#[instrument(skip(self))]
|
||||||
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(
|
||||||
|
|
@ -485,7 +599,11 @@ 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)
|
||||||
|
|
@ -495,7 +613,7 @@ impl Client {
|
||||||
))
|
))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
#[instrument]
|
#[instrument(skip(self))]
|
||||||
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(),
|
||||||
|
|
@ -512,7 +630,11 @@ 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()));
|
||||||
|
|
@ -521,7 +643,7 @@ impl Client {
|
||||||
Ok(code)
|
Ok(code)
|
||||||
}
|
}
|
||||||
|
|
||||||
#[instrument]
|
#[instrument(skip(self))]
|
||||||
pub async fn check_auth_status(
|
pub async fn check_auth_status(
|
||||||
&self,
|
&self,
|
||||||
device_code: &str,
|
device_code: &str,
|
||||||
|
|
@ -544,7 +666,11 @@ 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!(
|
||||||
|
|
@ -564,25 +690,26 @@ impl Client {
|
||||||
|
|
||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
mod tests {
|
mod tests {
|
||||||
|
use crabidy_core::ProviderClient;
|
||||||
|
|
||||||
use super::*;
|
use super::*;
|
||||||
|
|
||||||
fn setup() -> Client {
|
async fn setup() -> Client {
|
||||||
let settings = crate::config::Settings::default();
|
let raw_toml_settings =
|
||||||
Client::new(settings).expect("could not create tidaldy client")
|
std::fs::read_to_string("/home/hans/.config/crabidy/tidaldy.toml").unwrap();
|
||||||
|
Client::init(&raw_toml_settings).await.unwrap()
|
||||||
}
|
}
|
||||||
|
|
||||||
#[tokio::test]
|
#[tokio::test]
|
||||||
async fn test_get_device_code() {
|
async fn test() {
|
||||||
let client = setup();
|
let client = setup().await;
|
||||||
println!("{:#?}", client);
|
let user = client.settings.login.user_id.clone().unwrap();
|
||||||
let response = client.get_device_code().await.unwrap();
|
let result = client.get_users_artists(&user).await.unwrap();
|
||||||
assert!(!response.device_code.is_empty());
|
println!("{:?}", result);
|
||||||
assert_eq!(response.device_code.len(), 36);
|
let result = client.get_artist("5293333").await.unwrap();
|
||||||
assert!(!response.user_code.is_empty());
|
println!("{:?}", result);
|
||||||
assert_eq!(response.user_code.len(), 5);
|
let result = client.get_album("244167550").await.unwrap();
|
||||||
assert!(!response.verification_uri.is_empty());
|
println!("{:?}", result);
|
||||||
assert!(!response.verification_uri_complete.is_empty());
|
assert!(false);
|
||||||
assert!(response.expires_in == 300);
|
|
||||||
assert!(response.interval != 0);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,6 @@
|
||||||
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;
|
||||||
|
|
@ -15,15 +16,58 @@ 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 Item {
|
pub struct ArtistItem {
|
||||||
|
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: Vec<String>,
|
pub artist_types: Option<Vec<String>>,
|
||||||
pub url: String,
|
pub url: Option<String>,
|
||||||
pub picture: Value,
|
pub picture: Option<Value>,
|
||||||
pub popularity: i64,
|
pub popularity: Option<i64>,
|
||||||
pub artist_roles: Vec<ArtistRole>,
|
pub artist_roles: Option<Vec<ArtistRole>>,
|
||||||
pub mixes: Mixes,
|
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)]
|
||||||
|
|
@ -126,43 +170,76 @@ impl TrackPlayback {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// #[derive(Default, Debug, Clone, PartialEq, Serialize, Deserialize)]
|
||||||
|
// #[serde(rename_all = "camelCase")]
|
||||||
|
// pub struct Track {
|
||||||
|
// pub id: u64,
|
||||||
|
// pub title: String,
|
||||||
|
// pub duration: u64,
|
||||||
|
// pub replay_gain: f64,
|
||||||
|
// pub peak: f64,
|
||||||
|
// pub allow_streaming: bool,
|
||||||
|
// pub stream_ready: bool,
|
||||||
|
// pub stream_start_date: Option<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: u64,
|
pub id: i64,
|
||||||
pub title: String,
|
pub title: String,
|
||||||
pub duration: u64,
|
pub duration: Option<i64>,
|
||||||
pub replay_gain: f64,
|
pub replay_gain: Option<f64>,
|
||||||
pub peak: f64,
|
pub peak: Option<f64>,
|
||||||
pub allow_streaming: bool,
|
pub allow_streaming: Option<bool>,
|
||||||
pub stream_ready: bool,
|
pub stream_ready: Option<bool>,
|
||||||
|
pub ad_supported_stream_ready: Option<bool>,
|
||||||
pub stream_start_date: Option<String>,
|
pub stream_start_date: Option<String>,
|
||||||
pub premium_streaming_only: bool,
|
pub premium_streaming_only: Option<bool>,
|
||||||
pub track_number: u64,
|
pub track_number: Option<i64>,
|
||||||
pub volume_number: u64,
|
pub volume_number: Option<i64>,
|
||||||
pub version: Value,
|
pub version: Option<Value>,
|
||||||
pub popularity: u64,
|
pub popularity: Option<i64>,
|
||||||
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: bool,
|
pub editable: Option<bool>,
|
||||||
pub explicit: bool,
|
pub explicit: Option<bool>,
|
||||||
pub audio_quality: String,
|
pub audio_quality: Option<String>,
|
||||||
pub audio_modes: Vec<String>,
|
pub audio_modes: Option<Vec<String>>,
|
||||||
pub artist: Artist,
|
pub media_metadata: Option<MediaMetadata>,
|
||||||
pub artists: Vec<Artist>,
|
pub artist: Option<Artist>,
|
||||||
pub album: Album,
|
pub artists: Option<Vec<Artist>>,
|
||||||
pub mixes: Mixes,
|
pub album: Option<Album>,
|
||||||
|
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: track.artist.name,
|
artist: match track.artist {
|
||||||
album: Some(track.album.into()),
|
Some(a) => a.name.clone(),
|
||||||
duration: Some(track.duration as u32),
|
None => "".to_string(),
|
||||||
|
},
|
||||||
|
album: track.album.map(|a| a.into()),
|
||||||
|
duration: track.duration.map(|d| d as u32 * 1000),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -172,42 +249,143 @@ impl From<&Track> for crabidy_core::proto::crabidy::Track {
|
||||||
Self {
|
Self {
|
||||||
uuid: format!("track:{}", track.id),
|
uuid: format!("track:{}", track.id),
|
||||||
title: track.title.clone(),
|
title: track.title.clone(),
|
||||||
artist: track.artist.name.clone(),
|
artist: match track.artist.as_ref() {
|
||||||
album: Some(track.album.clone().into()),
|
Some(a) => a.name.clone(),
|
||||||
duration: Some(track.duration as u32),
|
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 {
|
||||||
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: String,
|
pub cover: Option<String>,
|
||||||
pub vibrant_color: String,
|
pub vibrant_color: Option<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 {
|
||||||
|
|
@ -221,11 +399,26 @@ 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 Mixes {
|
pub struct MediaMetadata {
|
||||||
|
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 {
|
||||||
|
|
@ -359,7 +552,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: Mixes,
|
pub mixes: TrackMixes,
|
||||||
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