Update all dependencies to current versions

- tonic 0.9 -> 0.14 (tonic-prost/tonic-prost-build split), prost 0.14
- ratatui 0.20 -> 0.30 (Frame no longer generic, Line instead of Spans),
  crossterm 0.29
- rodio 0.17 -> 0.22: replace the custom symphonia decoder with rodio's
  built-in decoder, seeking (try_seek) and position tracking (get_pos);
  end-of-stream is now signalled via an EmptyCallback source with a
  generation counter so a replaced track can never emit a stale EOS
- replace the vendored stream-download crate with the published
  stream-download 0.24 (rustls), with a 30s open timeout
- reqwest 0.12->0.13 (rustls/webpki-roots/query features), base64 0.22
  Engine API, rand 0.10, flume 0.12, thiserror 2, dirs 6, toml 1
- unify everything under [workspace.dependencies]; drop unused deps
  (once_cell, serde_json in server; confique, secrecy in tidaldy)
- devenv: add protobuf (protoc) for prost-build

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
AI User 2026-07-19 21:41:10 +02:00
parent 91716daf84
commit 6eb5a87b15
33 changed files with 3283 additions and 2682 deletions

4201
Cargo.lock generated

File diff suppressed because it is too large Load Diff

View File

@ -1,9 +1,68 @@
[workspace] [workspace]
resolver = "2"
members = [ members = [
"audio-player", "audio-player",
"cbd-tui", "cbd-tui",
"crabidy-core", "crabidy-core",
"crabidy-server", "crabidy-server",
"stream-download",
"tidaldy", "tidaldy",
] ]
[workspace.package]
version = "0.1.0"
edition = "2021"
[workspace.dependencies]
anyhow = "1"
async-trait = "0.1"
base64 = "0.22"
chrono = { version = "0.4", default-features = false, features = ["clock"] }
clap = { version = "4", features = ["derive"] }
clap-serde-derive = "0.2"
crossterm = "0.29"
dirs = "6"
flume = "0.12"
futures = "0.3"
log = "0.4"
notify-rust = "4"
prost = "0.14"
rand = "0.10"
ratatui = "0.30"
reqwest = { version = "0.13", default-features = false, features = [
"json",
"query",
"rustls",
"webpki-roots",
"http2",
"hickory-dns",
"stream",
] }
rodio = { version = "0.22", default-features = false, features = [
"playback",
"symphonia-all",
] }
serde = { version = "1", features = ["derive"] }
serde_json = "1"
serde_urlencoded = "0.7"
stream-download = { version = "0.24", default-features = false, features = [
"reqwest",
"reqwest-rustls",
"temp-storage",
] }
thiserror = "2"
tokio = "1"
tokio-stream = "0.1"
toml = "1"
tonic = "0.14"
tonic-prost = "0.14"
tonic-prost-build = "0.14"
tracing = "0.1"
tracing-appender = "0.2"
tracing-log = "0.2"
tracing-subscriber = { version = "0.3", features = ["env-filter"] }
url = "2"
# Local crates
audio-player = { path = "audio-player" }
crabidy-core = { path = "crabidy-core" }
tidaldy = { path = "tidaldy" }

View File

@ -1,19 +1,17 @@
[package] [package]
name = "audio-player" name = "audio-player"
version = "0.1.0" version.workspace = true
edition = "2021" edition.workspace = true
[dependencies] [dependencies]
rodio = { version = "0.17.1", default-features = false, features = [ anyhow.workspace = true
"symphonia-all", flume.workspace = true
] } rodio.workspace = true
symphonia = { version = "0.5.3", features = ["all"] } stream-download.workspace = true
stream-download = { path = "../stream-download" } thiserror.workspace = true
anyhow = "1.0.71" tokio = { workspace = true, features = ["rt", "rt-multi-thread"] }
url = "2.4.0" tracing.workspace = true
flume = "0.10.14" url.workspace = true
thiserror = "1.0.40"
tracing = "0.1.37"
[dev-dependencies] [dev-dependencies]
tokio = { version = "1", features = ["full"] } tokio = { workspace = true, features = ["full"] }

View File

@ -1,318 +0,0 @@
use std::error::Error;
use std::fmt;
use std::time::Duration;
use flume::Sender;
use rodio::Source;
use symphonia::{
core::{
audio::{AudioBufferRef, SampleBuffer, SignalSpec},
codecs::{Decoder, DecoderOptions},
errors::Error as SymphoniaError,
formats::{FormatOptions, FormatReader, SeekMode, SeekTo, Track},
io::MediaSourceStream,
meta::{MetadataOptions, MetadataRevision},
probe::Hint,
units::{Time, TimeBase},
},
default::get_probe,
};
use tracing::warn;
use crate::player_engine::PlayerEngineCommand;
// Decoder errors are not considered fatal.
// The correct action is to just get a new packet and try again.
// But a decode error in more than 3 consecutive packets is fatal.
const MAX_DECODE_ERRORS: usize = 3;
#[derive(Clone)]
pub struct MediaInfo {
pub duration: Option<Duration>,
pub metadata: Option<MetadataRevision>,
pub track: Track,
}
pub struct SymphoniaDecoder {
decoder: Box<dyn Decoder>,
current_frame_offset: usize,
format: Box<dyn FormatReader>,
buffer: SampleBuffer<i16>,
spec: SignalSpec,
time_base: Option<TimeBase>,
duration: u64,
elapsed: u64,
metadata: Option<MetadataRevision>,
track: Track,
tx: Sender<PlayerEngineCommand>,
}
impl SymphoniaDecoder {
pub fn new(
mss: MediaSourceStream,
hint: Hint,
tx: Sender<PlayerEngineCommand>,
) -> Result<Self, DecoderError> {
match SymphoniaDecoder::init(mss, hint, tx) {
Err(e) => match e {
SymphoniaError::IoError(e) => Err(DecoderError::IoError(e.to_string())),
SymphoniaError::DecodeError(e) => Err(DecoderError::DecodeError(e)),
SymphoniaError::SeekError(_) => {
unreachable!("Seek errors should not occur during initialization")
}
SymphoniaError::Unsupported(_) => Err(DecoderError::UnrecognizedFormat),
SymphoniaError::LimitError(e) => Err(DecoderError::LimitError(e)),
SymphoniaError::ResetRequired => Err(DecoderError::ResetRequired),
},
Ok(Some(decoder)) => Ok(decoder),
Ok(None) => Err(DecoderError::NoStreams),
}
}
fn init(
mss: MediaSourceStream,
hint: Hint,
tx: Sender<PlayerEngineCommand>,
) -> symphonia::core::errors::Result<Option<SymphoniaDecoder>> {
let format_opts: FormatOptions = FormatOptions {
enable_gapless: true,
..Default::default()
};
let metadata_opts: MetadataOptions = Default::default();
let mut probed = get_probe().format(&hint, mss, &format_opts, &metadata_opts)?;
let track = match probed.format.default_track() {
Some(stream) => stream,
None => return Ok(None),
}
.clone();
let time_base = track.codec_params.time_base;
let duration = track
.codec_params
.n_frames
.map(|frames| track.codec_params.start_ts + frames)
.unwrap_or_default();
let mut _elapsed = 0;
let mut decoder = symphonia::default::get_codecs()
.make(&track.codec_params, &DecoderOptions { verify: true })?;
let mut decode_errors: usize = 0;
let decoded = loop {
let current_frame = probed.format.next_packet()?;
_elapsed = current_frame.ts();
match decoder.decode(&current_frame) {
Ok(decoded) => break decoded,
Err(e) => match e {
SymphoniaError::DecodeError(_) => {
decode_errors += 1;
if decode_errors > MAX_DECODE_ERRORS {
return Err(e);
} else {
continue;
}
}
_ => return Err(e),
},
}
};
let spec = decoded.spec().to_owned();
let buffer = SymphoniaDecoder::get_buffer(decoded, &spec);
// Prefer metadata that's provided in the container format, over other tags found during the
// probe operation.
let metadata = probed.format.metadata().current().cloned().or_else(|| {
probed
.metadata
.get()
.as_ref()
.and_then(|m| m.current().cloned())
});
Ok(Some(SymphoniaDecoder {
decoder,
current_frame_offset: 0,
format: probed.format,
buffer,
spec,
time_base,
duration,
elapsed: _elapsed,
metadata,
track,
tx,
}))
}
#[inline]
pub fn media_info(&self) -> MediaInfo {
MediaInfo {
duration: self.total_duration(),
metadata: self.metadata.clone(),
track: self.track.clone(),
}
}
#[inline]
pub fn elapsed(&self) -> Duration {
if let Some(tb) = self.time_base {
let time = tb.calc_time(self.elapsed);
return Duration::from_secs_f64(time.seconds as f64 + time.frac);
};
Duration::default()
}
#[inline]
pub fn seek(&mut self, time: Duration) -> Option<Duration> {
let nanos_per_sec = 1_000_000_000.0;
match self.format.seek(
SeekMode::Coarse,
SeekTo::Time {
time: Time::new(
time.as_secs(),
f64::from(time.subsec_nanos()) / nanos_per_sec,
),
track_id: None,
},
) {
Ok(seeked_to) => {
let base = TimeBase::new(1, self.sample_rate());
let time = base.calc_time(seeked_to.actual_ts);
Some(Duration::from_millis(
time.seconds * 1000 + ((time.frac * 60. * 1000.).round() as u64),
))
}
Err(_) => None,
}
}
#[inline]
fn get_buffer(decoded: AudioBufferRef, spec: &SignalSpec) -> SampleBuffer<i16> {
let duration = decoded.capacity() as u64;
let mut buffer = SampleBuffer::<i16>::new(duration, *spec);
buffer.copy_interleaved_ref(decoded);
buffer
}
}
impl Source for SymphoniaDecoder {
#[inline]
fn current_frame_len(&self) -> Option<usize> {
Some(self.buffer.samples().len())
}
#[inline]
fn channels(&self) -> u16 {
self.spec.channels.count() as u16
}
#[inline]
fn sample_rate(&self) -> u32 {
self.spec.rate
}
#[inline]
fn total_duration(&self) -> Option<Duration> {
match self.time_base {
Some(tb) => {
let time = tb.calc_time(self.duration);
Some(Duration::from_secs_f64(time.seconds as f64 + time.frac))
}
None => None,
}
}
}
impl Iterator for SymphoniaDecoder {
type Item = i16;
#[inline]
fn next(&mut self) -> Option<i16> {
if self.current_frame_offset == self.buffer.len() {
let mut decode_errors: usize = 0;
let decoded = loop {
match self.format.next_packet() {
Ok(packet) => {
self.elapsed = packet.ts();
match self.decoder.decode(&packet) {
Ok(decoded) => break decoded,
Err(e) => match e {
SymphoniaError::DecodeError(_) => {
decode_errors += 1;
if decode_errors > MAX_DECODE_ERRORS {
return None;
} else {
continue;
}
}
_ => return None,
},
}
}
Err(SymphoniaError::IoError(err)) => {
if err.kind() == std::io::ErrorKind::UnexpectedEof
&& err.to_string() == "end of stream"
{
self.tx
.send(PlayerEngineCommand::Eos)
.unwrap_or_else(|e| warn!("Send error {}", e));
return None;
}
}
Err(_) => return None,
}
};
self.spec = decoded.spec().to_owned();
self.buffer = SymphoniaDecoder::get_buffer(decoded, &self.spec);
self.current_frame_offset = 0;
}
let sample = *self.buffer.samples().get(self.current_frame_offset)?;
self.current_frame_offset += 1;
Some(sample)
}
}
/// Error that can happen when creating a decoder.
#[derive(Debug, Clone)]
pub enum DecoderError {
/// The format of the data has not been recognized.
UnrecognizedFormat,
/// An IO error occurred while reading, writing, or seeking the stream.
IoError(String),
/// The stream contained malformed data and could not be decoded or demuxed.
DecodeError(&'static str),
/// A default or user-defined limit was reached while decoding or demuxing the stream. Limits
/// are used to prevent denial-of-service attacks from malicious streams.
LimitError(&'static str),
/// The demuxer or decoder needs to be reset before continuing.
ResetRequired,
/// No streams were found by the decoder
NoStreams,
}
impl fmt::Display for DecoderError {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
let text = match self {
DecoderError::UnrecognizedFormat => "Unrecognized format",
DecoderError::IoError(msg) => &msg[..],
DecoderError::DecodeError(msg) => msg,
DecoderError::LimitError(msg) => msg,
DecoderError::ResetRequired => "Reset required",
DecoderError::NoStreams => "No streams",
};
write!(f, "{}", text)
}
}
impl Error for DecoderError {}

View File

@ -1,7 +1,5 @@
mod decoder;
mod player; mod player;
mod player_engine; mod player_engine;
pub use decoder::MediaInfo;
pub use player::{Player, PlayerError}; pub use player::{Player, PlayerError};
pub use player_engine::PlayerMessage; pub use player_engine::{MediaInfo, PlayerMessage};

View File

@ -3,13 +3,9 @@ use std::time::Duration;
use anyhow::Result; use anyhow::Result;
use flume::{Receiver, Sender}; use flume::{Receiver, Sender};
use tracing::{error, warn}; use tracing::error;
use crate::decoder::MediaInfo; use crate::player_engine::{MediaInfo, PlayerEngine, PlayerEngineCommand, PlayerMessage};
use crate::player_engine::{PlayerEngine, PlayerEngineCommand, PlayerMessage};
// TODO:
// * Emit buffering
pub enum PlayerError {} pub enum PlayerError {}
@ -20,82 +16,24 @@ pub struct Player {
impl Default for Player { impl Default for Player {
fn default() -> Self { fn default() -> Self {
let (tx_engine, rx_engine) = flume::bounded(10); let (tx_engine, rx_engine) = flume::bounded(16);
let (tx_player, messages): (Sender<PlayerMessage>, Receiver<PlayerMessage>) = let (tx_player, messages): (Sender<PlayerMessage>, Receiver<PlayerMessage>) =
flume::bounded(10); flume::bounded(16);
let tx_decoder = tx_engine.clone(); let tx_callbacks = tx_engine.clone();
// Capture the runtime handle here: the engine thread itself is not a
// tokio context but needs one to create http streams.
let runtime = tokio::runtime::Handle::try_current().ok();
thread::spawn(move || { thread::spawn(move || {
let mut player = match PlayerEngine::init(tx_decoder, tx_player) { let engine = match PlayerEngine::init(tx_callbacks, tx_player, runtime) {
Err(e) => { Err(e) => {
error!("Could not initialize player: {}", e); error!("Could not initialize player: {}", e);
return; return;
} }
Ok(engine) => engine, Ok(engine) => engine,
}; };
engine.run(rx_engine);
loop {
match rx_engine.recv() {
Ok(PlayerEngineCommand::Play(source_str, tx)) => {
tx.send(player.play(&source_str))
.unwrap_or_else(|e| warn!("Send error {}", e));
}
Ok(PlayerEngineCommand::Pause(tx)) => {
tx.send(player.pause())
.unwrap_or_else(|e| warn!("Send error {}", e));
}
Ok(PlayerEngineCommand::Unpause(tx)) => {
tx.send(player.unpause())
.unwrap_or_else(|e| warn!("Send error {}", e));
}
Ok(PlayerEngineCommand::Stop(tx)) => {
tx.send(player.stop())
.unwrap_or_else(|e| warn!("Send error {}", e));
}
Ok(PlayerEngineCommand::TogglePlay(tx)) => {
tx.send(player.toggle_play())
.unwrap_or_else(|e| warn!("Send error {}", e));
}
Ok(PlayerEngineCommand::Restart(tx)) => {
tx.send(player.restart())
.unwrap_or_else(|e| warn!("Send error {}", e));
}
Ok(PlayerEngineCommand::GetDuration(tx)) => {
tx.send(player.duration())
.unwrap_or_else(|e| warn!("Send error {}", e));
}
Ok(PlayerEngineCommand::GetElapsed(tx)) => {
tx.send(player.elapsed())
.unwrap_or_else(|e| warn!("Send error {}", e));
}
Ok(PlayerEngineCommand::SeekTo(time, tx)) => {
tx.send(player.seek_to(time))
.unwrap_or_else(|e| warn!("Send error {}", e));
}
Ok(PlayerEngineCommand::GetVolume(tx)) => {
tx.send(player.volume())
.unwrap_or_else(|e| warn!("Send error {}", e));
}
Ok(PlayerEngineCommand::GetPaused(tx)) => {
tx.send(player.is_paused())
.unwrap_or_else(|e| warn!("Send error {}", e));
}
Ok(PlayerEngineCommand::SetVolume(volume, tx)) => {
tx.send(player.set_volume(volume))
.unwrap_or_else(|e| warn!("Send error {}", e));
}
Ok(PlayerEngineCommand::SetElapsed(elapsed)) => {
player.handle_elapsed(elapsed);
}
Ok(PlayerEngineCommand::Eos) => {
player.handle_eos();
}
Err(e) => {
warn!("Recv error {}", e);
}
}
}
}); });
Self { Self {
@ -109,74 +47,96 @@ impl Player {
pub async fn play(&self, source_str: &str) -> Result<MediaInfo> { pub async fn play(&self, source_str: &str) -> Result<MediaInfo> {
let (tx, rx) = flume::bounded(1); let (tx, rx) = flume::bounded(1);
self.tx_engine self.tx_engine
.send(PlayerEngineCommand::Play(source_str.to_string(), tx))?; .send_async(PlayerEngineCommand::Play(source_str.to_string(), tx))
.await?;
rx.recv_async().await? rx.recv_async().await?
} }
pub async fn restart(&self) -> Result<MediaInfo> { pub async fn restart(&self) -> Result<MediaInfo> {
let (tx, rx) = flume::bounded(1); let (tx, rx) = flume::bounded(1);
self.tx_engine.send(PlayerEngineCommand::Restart(tx))?; self.tx_engine
.send_async(PlayerEngineCommand::Restart(tx))
.await?;
rx.recv_async().await? rx.recv_async().await?
} }
pub async fn elpased(&self) -> Result<Duration> { pub async fn elapsed(&self) -> Result<Duration> {
let (tx, rx) = flume::bounded(1); let (tx, rx) = flume::bounded(1);
self.tx_engine.send(PlayerEngineCommand::GetElapsed(tx))?; self.tx_engine
.send_async(PlayerEngineCommand::GetElapsed(tx))
.await?;
rx.recv_async().await? rx.recv_async().await?
} }
pub async fn duration(&self) -> Result<Duration> { pub async fn duration(&self) -> Result<Duration> {
let (tx, rx) = flume::bounded(1); let (tx, rx) = flume::bounded(1);
self.tx_engine.send(PlayerEngineCommand::GetDuration(tx))?; self.tx_engine
.send_async(PlayerEngineCommand::GetDuration(tx))
.await?;
rx.recv_async().await? rx.recv_async().await?
} }
pub async fn seek_to(&self, time: Duration) -> Result<Duration> { pub async fn seek_to(&self, time: Duration) -> Result<Duration> {
let (tx, rx) = flume::bounded(1); let (tx, rx) = flume::bounded(1);
self.tx_engine.send(PlayerEngineCommand::SeekTo(time, tx))?; self.tx_engine
.send_async(PlayerEngineCommand::SeekTo(time, tx))
.await?;
rx.recv_async().await? rx.recv_async().await?
} }
pub async fn volume(&self) -> Result<f32> { pub async fn volume(&self) -> Result<f32> {
let (tx, rx) = flume::bounded(1); let (tx, rx) = flume::bounded(1);
self.tx_engine.send(PlayerEngineCommand::GetVolume(tx))?; self.tx_engine
.send_async(PlayerEngineCommand::GetVolume(tx))
.await?;
Ok(rx.recv_async().await?) Ok(rx.recv_async().await?)
} }
pub async fn is_paused(&self) -> Result<bool> { pub async fn is_paused(&self) -> Result<bool> {
let (tx, rx) = flume::bounded(1); let (tx, rx) = flume::bounded(1);
self.tx_engine.send(PlayerEngineCommand::GetPaused(tx))?; self.tx_engine
.send_async(PlayerEngineCommand::GetPaused(tx))
.await?;
rx.recv_async().await? rx.recv_async().await?
} }
pub async fn set_volume(&self, volume: f32) -> Result<f32> { pub async fn set_volume(&self, volume: f32) -> Result<f32> {
let (tx, rx) = flume::bounded(1); let (tx, rx) = flume::bounded(1);
self.tx_engine self.tx_engine
.send(PlayerEngineCommand::SetVolume(volume, tx))?; .send_async(PlayerEngineCommand::SetVolume(volume, tx))
.await?;
Ok(rx.recv_async().await?) Ok(rx.recv_async().await?)
} }
pub async fn pause(&self) -> Result<()> { pub async fn pause(&self) -> Result<()> {
let (tx, rx) = flume::bounded(1); let (tx, rx) = flume::bounded(1);
self.tx_engine.send(PlayerEngineCommand::Pause(tx))?; self.tx_engine
.send_async(PlayerEngineCommand::Pause(tx))
.await?;
rx.recv_async().await? rx.recv_async().await?
} }
pub async fn unpause(&self) -> Result<()> { pub async fn unpause(&self) -> Result<()> {
let (tx, rx) = flume::bounded(1); let (tx, rx) = flume::bounded(1);
self.tx_engine.send(PlayerEngineCommand::Unpause(tx))?; self.tx_engine
.send_async(PlayerEngineCommand::Unpause(tx))
.await?;
rx.recv_async().await? rx.recv_async().await?
} }
pub async fn toggle_play(&self) -> Result<bool> { pub async fn toggle_play(&self) -> Result<bool> {
let (tx, rx) = flume::bounded(1); let (tx, rx) = flume::bounded(1);
self.tx_engine.send(PlayerEngineCommand::TogglePlay(tx))?; self.tx_engine
.send_async(PlayerEngineCommand::TogglePlay(tx))
.await?;
rx.recv_async().await? rx.recv_async().await?
} }
pub async fn stop(&self) -> Result<()> { pub async fn stop(&self) -> Result<()> {
let (tx, rx) = flume::bounded(1); let (tx, rx) = flume::bounded(1);
self.tx_engine.send(PlayerEngineCommand::Stop(tx))?; self.tx_engine
.send_async(PlayerEngineCommand::Stop(tx))
.await?;
rx.recv_async().await? rx.recv_async().await?
} }
} }

View File

@ -1,19 +1,23 @@
use flume::Sender; use std::fs::File;
use std::io::BufReader;
use std::path::Path; use std::path::Path;
use std::sync::atomic::AtomicU64;
use std::thread;
use std::time::Duration; use std::time::Duration;
use std::{fs::File, sync::atomic::Ordering};
use symphonia::core::probe::Hint; use anyhow::{anyhow, Context, Result};
use tracing::{debug, warn}; use flume::{Receiver, RecvTimeoutError, Sender};
use rodio::source::EmptyCallback;
use rodio::stream::{DeviceSinkBuilder, MixerDeviceSink};
use rodio::{Decoder, Source};
use stream_download::storage::temp::TempStorageProvider;
use stream_download::{Settings, StreamDownload};
use thiserror::Error;
use tracing::{debug, info, instrument, trace, warn};
use url::Url; use url::Url;
use crate::decoder::{MediaInfo, SymphoniaDecoder}; /// How long we wait for the initial prefetch of a network stream.
use anyhow::{anyhow, Result}; const STREAM_OPEN_TIMEOUT: Duration = Duration::from_secs(30);
use rodio::{OutputStream, OutputStreamHandle, Sink, Source}; /// Interval between elapsed-position updates while playing.
use stream_download::StreamDownload; const TICK_INTERVAL: Duration = Duration::from_millis(250);
use symphonia::core::io::{MediaSource, MediaSourceStream, MediaSourceStreamOptions};
use thiserror::Error;
pub enum PlayerEngineCommand { pub enum PlayerEngineCommand {
Play(String, Sender<Result<MediaInfo>>), Play(String, Sender<Result<MediaInfo>>),
@ -28,8 +32,10 @@ pub enum PlayerEngineCommand {
SeekTo(Duration, Sender<Result<Duration>>), SeekTo(Duration, Sender<Result<Duration>>),
GetVolume(Sender<f32>), GetVolume(Sender<f32>),
GetPaused(Sender<Result<bool>>), GetPaused(Sender<Result<bool>>),
Eos, /// End of stream for the source started by the given generation.
SetElapsed(Duration), /// Stale generations are ignored so an old track finishing can never
/// interfere with a newly started one.
Eos(u64),
} }
pub enum PlayerMessage { pub enum PlayerMessage {
@ -46,8 +52,10 @@ pub enum PlayerMessage {
EndOfStream, EndOfStream,
} }
// TODO: #[derive(Clone, Debug)]
// * Emit buffering pub struct MediaInfo {
pub duration: Option<Duration>,
}
#[derive(Debug, Error)] #[derive(Debug, Error)]
pub enum PlayerEngineError { pub enum PlayerEngineError {
@ -55,85 +63,195 @@ pub enum PlayerEngineError {
NotPlaying, NotPlaying,
} }
// Used for seeking in the stream
static SEEK_TO: AtomicU64 = AtomicU64::new(0);
pub struct PlayerEngine { pub struct PlayerEngine {
elapsed: Duration,
current_source: Option<String>, current_source: Option<String>,
media_info: Option<MediaInfo>, media_info: Option<MediaInfo>,
sink: Sink, /// Monotonically increasing id for the currently playing source. Used to
// We need to keep the stream around as it will stop playing when it's dropped /// discard end-of-stream callbacks from sources that were replaced.
_stream: OutputStream, generation: u64,
_handle: OutputStreamHandle, sink: rodio::Player,
// We need to keep the device sink around; audio stops when it's dropped.
_stream: MixerDeviceSink,
tx_engine: Sender<PlayerEngineCommand>, tx_engine: Sender<PlayerEngineCommand>,
tx_player: Sender<PlayerMessage>, tx_player: Sender<PlayerMessage>,
runtime: tokio::runtime::Handle,
// Present when the engine had to bring its own runtime because the
// creating thread was not inside a tokio context.
_owned_runtime: Option<tokio::runtime::Runtime>,
} }
impl PlayerEngine { impl PlayerEngine {
pub fn init( pub fn init(
tx_engine: Sender<PlayerEngineCommand>, tx_engine: Sender<PlayerEngineCommand>,
tx_player: Sender<PlayerMessage>, tx_player: Sender<PlayerMessage>,
runtime: Option<tokio::runtime::Handle>,
) -> Result<Self> { ) -> Result<Self> {
let (_stream, handle) = OutputStream::try_default()?; let stream =
let sink = Sink::try_new(&handle)?; DeviceSinkBuilder::open_default_sink().context("failed to open audio output device")?;
let sink = rodio::Player::connect_new(stream.mixer());
let (runtime, owned_runtime) = match runtime {
Some(handle) => (handle, None),
None => {
let rt = tokio::runtime::Builder::new_multi_thread()
.worker_threads(1)
.enable_all()
.build()
.context("failed to create tokio runtime for the player engine")?;
(rt.handle().clone(), Some(rt))
}
};
info!("audio output device opened");
Ok(Self { Ok(Self {
current_source: None, current_source: None,
media_info: None, media_info: None,
elapsed: Duration::default(), generation: 0,
sink, sink,
_stream, _stream: stream,
_handle: handle,
tx_engine, tx_engine,
tx_player, tx_player,
runtime,
_owned_runtime: owned_runtime,
}) })
} }
pub fn play(&mut self, source_str: &str) -> Result<MediaInfo> { /// Drives the engine until all command senders are dropped.
let tx_player = self.tx_player.clone(); pub fn run(mut self, rx_engine: Receiver<PlayerEngineCommand>) {
let tx_engine = self.tx_engine.clone(); loop {
match rx_engine.recv_timeout(TICK_INTERVAL) {
Ok(command) => self.handle_command(command),
Err(RecvTimeoutError::Timeout) => self.tick(),
Err(RecvTimeoutError::Disconnected) => {
debug!("player engine channel closed, shutting down");
break;
}
}
}
}
fn handle_command(&mut self, command: PlayerEngineCommand) {
match command {
PlayerEngineCommand::Play(source_str, tx) => {
send_reply(tx, self.play(&source_str));
}
PlayerEngineCommand::Pause(tx) => send_reply(tx, self.pause()),
PlayerEngineCommand::Unpause(tx) => send_reply(tx, self.unpause()),
PlayerEngineCommand::Stop(tx) => send_reply(tx, self.stop()),
PlayerEngineCommand::TogglePlay(tx) => send_reply(tx, self.toggle_play()),
PlayerEngineCommand::Restart(tx) => send_reply(tx, self.restart()),
PlayerEngineCommand::GetDuration(tx) => send_reply(tx, self.duration()),
PlayerEngineCommand::GetElapsed(tx) => send_reply(tx, self.elapsed()),
PlayerEngineCommand::SeekTo(time, tx) => send_reply(tx, self.seek_to(time)),
PlayerEngineCommand::SetVolume(volume, tx) => {
send_reply(tx, self.set_volume(volume));
}
PlayerEngineCommand::GetVolume(tx) => send_reply(tx, self.volume()),
PlayerEngineCommand::GetPaused(tx) => send_reply(tx, self.is_paused()),
PlayerEngineCommand::Eos(generation) => self.handle_eos(generation),
}
}
/// Emits an elapsed-position update while a source is playing.
fn tick(&self) {
if self.sink.empty() || self.sink.is_paused() {
return;
}
let duration = self
.media_info
.as_ref()
.and_then(|m| m.duration)
.unwrap_or_default();
// Dropping a tick when the channel is full is harmless.
let _ = self.tx_player.try_send(PlayerMessage::Elapsed {
duration,
elapsed: self.sink.get_pos(),
});
}
#[instrument(skip(self))]
pub fn play(&mut self, source_str: &str) -> Result<MediaInfo> {
self.reset(); self.reset();
let (source, hint) = self.get_source(source_str)?; let duration = self.start_source(source_str)?;
let mss = MediaSourceStream::new(source, MediaSourceStreamOptions::default()); let media_info = MediaInfo { duration };
let decoder = SymphoniaDecoder::new(mss, hint, self.tx_engine.clone())?;
let media_info = decoder.media_info(); self.media_info = Some(media_info.clone());
let media_info_copy = media_info.clone();
let duration = media_info.duration.unwrap_or_default();
self.media_info = Some(media_info);
self.current_source = Some(source_str.to_string()); self.current_source = Some(source_str.to_string());
tx_player self.notify(PlayerMessage::Duration {
.send(PlayerMessage::Duration { duration }) duration: duration.unwrap_or_default(),
.unwrap_or_else(|e| warn!("Send error {}", e));
// FIXME: regularly update metadata revision
let decoder = decoder.periodic_access(Duration::from_millis(250), move |src| {
let seek = SEEK_TO.load(Ordering::SeqCst);
if seek > 0 {
src.seek(Duration::from_secs(seek));
SEEK_TO.store(0, Ordering::SeqCst);
}
let elapsed = src.elapsed();
tx_engine
.send(PlayerEngineCommand::SetElapsed(elapsed))
.unwrap_or_else(|e| warn!("Send error {}", e));
tx_player
.send(PlayerMessage::Elapsed { elapsed, duration })
.unwrap_or_else(|e| warn!("Send error {}", e));
}); });
self.sink.append(decoder);
self.sink.play(); self.sink.play();
self.notify(PlayerMessage::Playing);
debug!(duration = ?duration, "started playback");
self.tx_player Ok(media_info)
.send(PlayerMessage::Playing) }
.unwrap_or_else(|e| warn!("Send error {}", e));
Ok(media_info_copy) /// Decodes the source and appends it (plus an end-of-stream callback) to
/// the sink. Returns the total duration if known.
fn start_source(&mut self, source_str: &str) -> Result<Option<Duration>> {
self.generation += 1;
let duration = match Url::parse(source_str) {
Ok(url) if matches!(url.scheme(), "http" | "https") => {
trace!(%url, "opening network stream");
let reader = self.runtime.block_on(async {
tokio::time::timeout(
STREAM_OPEN_TIMEOUT,
StreamDownload::new_http(
url.clone(),
TempStorageProvider::new(),
Settings::default(),
),
)
.await
.map_err(|_| anyhow!("timed out opening stream after {STREAM_OPEN_TIMEOUT:?}"))?
.context("failed to open http stream")
})?;
let mut builder = Decoder::builder().with_data(reader).with_seekable(true);
if let Some(extension) = Path::new(url.path()).extension().and_then(|e| e.to_str())
{
builder = builder.with_hint(extension);
}
let decoder = builder.build().context("failed to decode http stream")?;
let duration = decoder.total_duration();
self.sink.append(decoder);
duration
}
Ok(url) => return Err(anyhow!("Not a valid URL scheme: {}", url.scheme())),
Err(_) => {
trace!(path = source_str, "opening local file");
let file = File::open(source_str)
.with_context(|| format!("failed to open file {source_str}"))?;
let byte_len = file.metadata().ok().map(|m| m.len());
let mut builder = Decoder::builder()
.with_data(BufReader::new(file))
.with_seekable(true);
if let Some(len) = byte_len {
builder = builder.with_byte_len(len);
}
if let Some(extension) = Path::new(source_str).extension().and_then(|e| e.to_str())
{
builder = builder.with_hint(extension);
}
let decoder = builder.build().context("failed to decode file")?;
let duration = decoder.total_duration();
self.sink.append(decoder);
duration
}
};
// Fires only when the decoder ahead of it finished naturally; a
// stop/replace clears the queue before this source is ever played.
let tx_engine = self.tx_engine.clone();
let generation = self.generation;
self.sink.append(EmptyCallback::new(Box::new(move || {
if let Err(err) = tx_engine.try_send(PlayerEngineCommand::Eos(generation)) {
warn!("failed to send end-of-stream signal: {err}");
}
})));
Ok(duration)
} }
pub fn restart(&mut self) -> Result<MediaInfo> { pub fn restart(&mut self) -> Result<MediaInfo> {
@ -148,9 +266,7 @@ impl PlayerEngine {
return Err(PlayerEngineError::NotPlaying.into()); return Err(PlayerEngineError::NotPlaying.into());
} }
self.sink.pause(); self.sink.pause();
self.tx_player self.notify(PlayerMessage::Paused);
.send(PlayerMessage::Paused)
.unwrap_or_else(|e| warn!("Send error {}", e));
Ok(()) Ok(())
} }
@ -159,9 +275,7 @@ impl PlayerEngine {
return Err(PlayerEngineError::NotPlaying.into()); return Err(PlayerEngineError::NotPlaying.into());
} }
self.sink.play(); self.sink.play();
self.tx_player self.notify(PlayerMessage::Playing);
.send(PlayerMessage::Playing)
.unwrap_or_else(|e| warn!("Send error {}", e));
Ok(()) Ok(())
} }
@ -171,9 +285,11 @@ impl PlayerEngine {
} }
if self.sink.is_paused() { if self.sink.is_paused() {
self.sink.play(); self.sink.play();
self.notify(PlayerMessage::Playing);
Ok(true) Ok(true)
} else { } else {
self.sink.pause(); self.sink.pause();
self.notify(PlayerMessage::Paused);
Ok(false) Ok(false)
} }
} }
@ -183,9 +299,7 @@ impl PlayerEngine {
return Err(PlayerEngineError::NotPlaying.into()); return Err(PlayerEngineError::NotPlaying.into());
} }
self.reset(); self.reset();
self.tx_player self.notify(PlayerMessage::Stopped);
.send(PlayerMessage::Stopped)
.unwrap_or_else(|e| warn!("Send error {}", e));
Ok(()) Ok(())
} }
@ -212,18 +326,16 @@ impl PlayerEngine {
if self.is_stopped() { if self.is_stopped() {
return Err(PlayerEngineError::NotPlaying.into()); return Err(PlayerEngineError::NotPlaying.into());
} }
Ok(self.elapsed) Ok(self.sink.get_pos())
} }
pub fn seek_to(&self, time: Duration) -> Result<Duration> { pub fn seek_to(&self, time: Duration) -> Result<Duration> {
// We can seek between 1 second and the total duration of the track let duration = self.duration().unwrap_or_else(|_| self.sink.get_pos());
let duration = self.duration().unwrap_or(self.elapsed);
let time = time.clamp(Duration::from_secs(1), duration); let time = time.clamp(Duration::from_secs(1), duration);
SEEK_TO.store(time.as_secs(), Ordering::SeqCst); self.sink
// FIXME: ideally we would like to return once the seeking is successful .try_seek(time)
// then return the current elapsed time .map_err(|err| anyhow!("seek failed: {err}"))?;
// Cond-var might be needed to sleep this (seeking takes time) Ok(self.sink.get_pos())
Ok(time)
} }
pub fn volume(&self) -> f32 { pub fn volume(&self) -> f32 {
@ -235,54 +347,36 @@ impl PlayerEngine {
self.sink.volume() self.sink.volume()
} }
pub fn handle_eos(&mut self) { fn handle_eos(&mut self, generation: u64) {
self.reset(); if generation != self.generation {
self.tx_player debug!(
.send(PlayerMessage::EndOfStream) stale = generation,
.unwrap_or_else(|e| warn!("Send error {}", e)); current = self.generation,
"ignoring end-of-stream from replaced source"
);
return;
} }
debug!("end of stream");
pub fn handle_elapsed(&mut self, elapsed: Duration) { self.reset();
self.elapsed = elapsed; self.notify(PlayerMessage::EndOfStream);
} }
fn reset(&mut self) { fn reset(&mut self) {
self.elapsed = Duration::default();
self.current_source = None; self.current_source = None;
self.sink.pause(); self.media_info = None;
self.generation += 1;
self.sink.stop(); self.sink.stop();
} }
fn get_source(&self, source_str: &str) -> Result<(Box<dyn MediaSource>, Hint)> { fn notify(&self, message: PlayerMessage) {
match Url::parse(source_str) { self.tx_player
Ok(url) => { .send(message)
if let "http" | "https" = url.scheme() { .unwrap_or_else(|e| warn!("Send error {}", e));
let reader = StreamDownload::new_http(source_str.parse().unwrap());
let path = Path::new(url.path());
let hint = self.get_hint(path);
Ok((Box::new(reader), hint))
} else {
Err(anyhow!("Not a valid URL scheme: {}", url.scheme()))
}
}
Err(_) => {
let path = Path::new(source_str);
let hint = self.get_hint(path);
Ok((Box::new(File::open(path)?), hint))
}
} }
} }
fn get_hint(&self, path: &Path) -> Hint { fn send_reply<T>(tx: Sender<T>, value: T) {
// Create a hint to help the format registry guess what format reader is appropriate. if tx.send(value).is_err() {
let mut hint = Hint::new(); warn!("player engine reply receiver dropped");
// Provide the file extension as a hint.
if let Some(extension) = path.extension() {
if let Some(extension_str) = extension.to_str() {
hint.with_extension(extension_str);
}
}
hint
} }
} }

View File

@ -1,17 +1,15 @@
[package] [package]
name = "cbd-tui" name = "cbd-tui"
version = "0.1.0" version.workspace = true
edition = "2021" edition.workspace = true
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
[dependencies] [dependencies]
crossterm = "0.26.1" crabidy-core.workspace = true
crabidy-core = { path = "../crabidy-core" } crossterm.workspace = true
flume = "0.10.14" flume.workspace = true
ratatui = "0.20.1" notify-rust.workspace = true
tokio = { version = "1", features = ["full"] } ratatui.workspace = true
tokio-stream = "0.1" serde.workspace = true
tonic = "0.9" tokio = { workspace = true, features = ["full"] }
notify-rust = "4.8.0" tokio-stream.workspace = true
serde = "1.0.164" tonic.workspace = true

View File

@ -2,7 +2,6 @@ use std::collections::HashMap;
use flume::Sender; use flume::Sender;
use ratatui::{ use ratatui::{
backend::Backend,
layout::Rect, layout::Rect,
style::{Modifier, Style}, style::{Modifier, Style},
text::Span, text::Span,
@ -160,7 +159,7 @@ impl Library {
self.update_selection(); self.update_selection();
} }
pub fn render<B: Backend>(&mut self, f: &mut Frame<B>, area: Rect, focused: bool) { pub fn render(&mut self, f: &mut Frame, area: Rect, focused: bool) {
let library_items: Vec<ListItem> = self let library_items: Vec<ListItem> = self
.list .list
.iter() .iter()

View File

@ -5,7 +5,6 @@ mod queue;
use flume::Sender; use flume::Sender;
use ratatui::{ use ratatui::{
backend::Backend,
layout::{Constraint, Direction, Layout}, layout::{Constraint, Direction, Layout},
style::Color, style::Color,
Frame, Frame,
@ -105,8 +104,8 @@ impl App {
}; };
} }
pub fn render<B: Backend>(&mut self, f: &mut Frame<B>) { pub fn render(&mut self, f: &mut Frame) {
let full_screen = f.size(); let full_screen = f.area();
let library_focused = matches!(self.focus, UiFocus::Library); let library_focused = matches!(self.focus, UiFocus::Library);
let queue_focused = matches!(self.focus, UiFocus::Queue); let queue_focused = matches!(self.focus, UiFocus::Queue);
@ -114,7 +113,7 @@ impl App {
let main = Layout::default() let main = Layout::default()
.direction(Direction::Horizontal) .direction(Direction::Horizontal)
.constraints([Constraint::Percentage(50), Constraint::Percentage(50)].as_ref()) .constraints([Constraint::Percentage(50), Constraint::Percentage(50)].as_ref())
.split(f.size()); .split(f.area());
self.library.render(f, main[0], library_focused); self.library.render(f, main[0], library_focused);

View File

@ -5,10 +5,9 @@ use notify_rust::Notification;
use crabidy_core::proto::crabidy::{PlayState, QueueModifiers, Track, TrackPosition}; use crabidy_core::proto::crabidy::{PlayState, QueueModifiers, Track, TrackPosition};
use ratatui::{ use ratatui::{
backend::Backend,
layout::{Alignment, Constraint, Direction, Layout, Rect}, layout::{Alignment, Constraint, Direction, Layout, Rect},
style::{Color, Modifier, Style}, style::{Color, Modifier, Style},
text::{Span, Spans}, text::{Line, Span},
widgets::{Block, BorderType, Borders, LineGauge, Paragraph, Wrap}, widgets::{Block, BorderType, Borders, LineGauge, Paragraph, Wrap},
Frame, Frame,
}; };
@ -69,7 +68,7 @@ impl NowPlaying {
self.modifiers = mods.clone(); self.modifiers = mods.clone();
} }
pub fn render<B: Backend>(&self, f: &mut Frame<B>, area: Rect) { pub fn render(&self, f: &mut Frame, area: Rect) {
let now_playing_layout = Layout::default() let now_playing_layout = Layout::default()
.direction(Direction::Vertical) .direction(Direction::Vertical)
.constraints([Constraint::Max(8), Constraint::Max(1)]) .constraints([Constraint::Max(8), Constraint::Max(1)])
@ -91,9 +90,9 @@ impl NowPlaying {
self.modifiers.shuffle, self.modifiers.repeat self.modifiers.shuffle, self.modifiers.repeat
); );
vec![ vec![
Spans::from(Span::raw(mods)), Line::from(Span::raw(mods)),
Spans::from(Span::raw(play_text)), Line::from(Span::raw(play_text)),
Spans::from(vec![ Line::from(vec![
Span::styled( Span::styled(
track.title.to_string(), track.title.to_string(),
Style::default().add_modifier(Modifier::BOLD), Style::default().add_modifier(Modifier::BOLD),
@ -104,13 +103,13 @@ impl NowPlaying {
Style::default().add_modifier(Modifier::BOLD), Style::default().add_modifier(Modifier::BOLD),
), ),
]), ]),
Spans::from(Span::raw(album_text)), Line::from(Span::raw(album_text)),
] ]
} else { } else {
vec![ vec![
Spans::from(Span::raw("")), Line::from(Span::raw("")),
Spans::from(Span::raw("")), Line::from(Span::raw("")),
Spans::from(Span::raw("No track playing")), Line::from(Span::raw("No track playing")),
] ]
}; };
@ -173,7 +172,7 @@ impl NowPlaying {
}; };
let time_text = Span::raw(completion_text); let time_text = Span::raw(completion_text);
let time_p = Paragraph::new(Spans::from(time_text)); let time_p = Paragraph::new(Line::from(time_text));
f.render_widget(time_p, elapsed_layout[1]); f.render_widget(time_p, elapsed_layout[1]);
} }
} }

View File

@ -1,6 +1,5 @@
use flume::Sender; use flume::Sender;
use ratatui::{ use ratatui::{
backend::Backend,
layout::Rect, layout::Rect,
style::{Modifier, Style}, style::{Modifier, Style},
text::Span, text::Span,
@ -71,7 +70,7 @@ impl Queue {
self.update_selection(); self.update_selection();
} }
pub fn render<B: Backend>(&mut self, f: &mut Frame<B>, area: Rect, focused: bool) { pub fn render(&mut self, f: &mut Frame, area: Rect, focused: bool) {
let queue_items: Vec<ListItem> = self let queue_items: Vec<ListItem> = self
.list .list
.iter() .iter()

View File

@ -173,7 +173,7 @@ fn run_ui(tx: Sender<MessageFromUi>, rx: Receiver<MessageToUi>) {
app.now_playing.update_track(track.track); app.now_playing.update_track(track.track);
app.queue.update_position(track.queue_position as usize); app.queue.update_position(track.queue_position as usize);
} }
if let Some(ps) = PlayState::from_i32(init_data.play_state) { if let Some(ps) = PlayState::try_from(init_data.play_state).ok() {
app.now_playing.update_play_state(ps); app.now_playing.update_play_state(ps);
} }
if let Some(mods) = init_data.mods { if let Some(mods) = init_data.mods {
@ -190,7 +190,7 @@ fn run_ui(tx: Sender<MessageFromUi>, rx: Receiver<MessageToUi>) {
} }
StreamUpdate::Position(pos) => app.now_playing.update_position(pos), StreamUpdate::Position(pos) => app.now_playing.update_position(pos),
StreamUpdate::PlayState(play_state) => { StreamUpdate::PlayState(play_state) => {
if let Some(ps) = PlayState::from_i32(play_state) { if let Some(ps) = PlayState::try_from(play_state).ok() {
app.now_playing.update_play_state(ps); app.now_playing.update_play_state(ps);
} }
} }

View File

@ -1,21 +1,18 @@
[package] [package]
name = "crabidy-core" name = "crabidy-core"
version = "0.1.0" version.workspace = true
edition = "2021" edition.workspace = true
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
[dependencies] [dependencies]
async-trait = "0.1.68" async-trait.workspace = true
clap = "4.3.3" clap.workspace = true
clap-serde-derive = "0.2.0" clap-serde-derive.workspace = true
dirs = "5.0.1" dirs.workspace = true
prost = "0.11" prost.workspace = true
serde = "1.0.163" serde.workspace = true
toml = "0.7.4" toml.workspace = true
tonic = "0.9" tonic.workspace = true
tonic-prost.workspace = true
[build-dependencies] [build-dependencies]
async-trait = "0.1.68" tonic-prost-build.workspace = true
serde = "1.0.163"
tonic-build = "0.9"

View File

@ -1,4 +1,4 @@
fn main() -> Result<(), Box<dyn std::error::Error>> { fn main() -> Result<(), Box<dyn std::error::Error>> {
tonic_build::compile_protos("crabidy/v1/crabidy.proto")?; tonic_prost_build::compile_protos("crabidy/v1/crabidy.proto")?;
Ok(()) Ok(())
} }

View File

@ -1,32 +1,27 @@
[package] [package]
name = "crabidy-server" name = "crabidy-server"
version = "0.1.0" version.workspace = true
edition = "2021" edition.workspace = true
[[bin]] [[bin]]
name = "crabidy-server" name = "crabidy-server"
path = "src/main.rs" path = "src/main.rs"
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
[dependencies] [dependencies]
anyhow = "1.0.71" anyhow.workspace = true
tokio = { version = "1.28.0", features = ["full"] } async-trait.workspace = true
tidaldy = { path = "../tidaldy" } audio-player.workspace = true
crabidy-core = { path = "../crabidy-core" } crabidy-core.workspace = true
audio-player = { path = "../audio-player" } dirs.workspace = true
once_cell = "1.17.1" flume.workspace = true
serde_json = "1.0.96" futures.workspace = true
serde = "1.0.163" log.workspace = true
flume = "0.10.14" rand.workspace = true
tonic = "0.9.2" tidaldy.workspace = true
async-trait = "0.1.68" tokio = { workspace = true, features = ["full"] }
futures = "0.3.28" tokio-stream = { workspace = true, features = ["sync"] }
tokio-stream = { version = "0.1.14", features = ["sync"] } tonic.workspace = true
dirs = "5.0.1" tracing.workspace = true
tracing = "0.1.37" tracing-appender.workspace = true
tracing-subscriber = "0.3.17" tracing-log.workspace = true
tracing-appender = "0.2.2" tracing-subscriber.workspace = true
tracing-log = "0.1.3"
log = "0.4.18"
rand = "0.8.5"

View File

@ -1,5 +1,5 @@
use crabidy_core::proto::crabidy::{Queue, Track}; use crabidy_core::proto::crabidy::{Queue, Track};
use rand::{seq::SliceRandom, thread_rng}; use rand::{rng, seq::SliceRandom};
use std::time::SystemTime; use std::time::SystemTime;
use tracing::{debug, error}; use tracing::{debug, error};
@ -64,15 +64,15 @@ impl QueueManager {
} }
pub fn shuffle_all(&mut self) { pub fn shuffle_all(&mut self) {
self.play_order.shuffle(&mut thread_rng()); self.play_order.shuffle(&mut rng());
} }
pub fn shuffle_before(&mut self, pos: usize) { pub fn shuffle_before(&mut self, pos: usize) {
self.play_order[..pos].shuffle(&mut thread_rng()); self.play_order[..pos].shuffle(&mut rng());
} }
pub fn shuffle_behind(&mut self, pos: usize) { pub fn shuffle_behind(&mut self, pos: usize) {
self.play_order[pos + 1..].shuffle(&mut thread_rng()); self.play_order[pos + 1..].shuffle(&mut rng());
} }
pub fn current_track(&self) -> Option<Track> { pub fn current_track(&self) -> Option<Track> {
@ -131,7 +131,7 @@ impl QueueManager {
else { else {
error!("invalid current position"); error!("invalid current position");
error!("queue: {:#?}", self); error!("queue: {:#?}", self);
return false return false;
}; };
if self.shuffle { if self.shuffle {
self.play_order.swap(0, current_offset); self.play_order.swap(0, current_offset);
@ -184,14 +184,10 @@ impl QueueManager {
if *pos == self.current_position() as u32 { if *pos == self.current_position() as u32 {
play_next = true; play_next = true;
} }
let Some(offset) = self let Some(offset) = self.play_order.iter().position(|&i| i == *pos as usize) else {
.play_order
.iter()
.position(|&i| i == *pos as usize)
else {
error!("invalid current position"); error!("invalid current position");
error!("queue: {:#?}", self); error!("queue: {:#?}", self);
return None return None;
}; };
if offset < self.current_offset { if offset < self.current_offset {
self.current_offset -= 1; self.current_offset -= 1;

View File

@ -523,17 +523,18 @@ impl Playback {
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");
let Ok(_) = tx.send_async(ProviderMessage::FlattenNode { let Ok(_) = tx
.send_async(ProviderMessage::FlattenNode {
uuid: uuid.to_string(), uuid: uuid.to_string(),
result_tx, result_tx,
span, span,
}).in_current_span().await else { })
.in_current_span()
.await
else {
return Vec::new(); return Vec::new();
}; };
let Ok(tracks) = result_rx let Ok(tracks) = result_rx.recv_async().in_current_span().await else {
.recv_async()
.in_current_span()
.await else {
return Vec::new(); return Vec::new();
}; };
tracks tracks
@ -608,7 +609,7 @@ impl Playback {
{ {
let Ok(queue) = self.queue.lock() else { let Ok(queue) = self.queue.lock() else {
error!("poisend queue lock"); error!("poisend queue lock");
return 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();
@ -655,7 +656,7 @@ impl Playback {
{ {
let Ok(queue) = self.queue.lock() else { let Ok(queue) = self.queue.lock() else {
error!("poisend queue lock"); error!("poisend queue lock");
return 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();

View File

@ -76,7 +76,7 @@ impl ProviderOrchestrator {
nodes_to_go.push(node_uuid.to_string()); nodes_to_go.push(node_uuid.to_string());
while let Some(node_uuid) = nodes_to_go.pop() { while let Some(node_uuid) = nodes_to_go.pop() {
let Ok(node) = self.get_lib_node(&node_uuid).in_current_span().await else { let Ok(node) = self.get_lib_node(&node_uuid).in_current_span().await else {
continue continue;
}; };
if node.is_queable { if node.is_queable {
tracks.extend(node.tracks); tracks.extend(node.tracks);

View File

@ -12,6 +12,7 @@ let
extraPackages = with pkgs; [ extraPackages = with pkgs; [
pkg-config pkg-config
protobuf
]; ];
in in
{ {

View File

@ -1,10 +0,0 @@
# Generated by Cargo
# will have compiled files and executables
/target/
# Remove Cargo.lock from gitignore if creating an executable, leave it for libraries
# More information here https://doc.rust-lang.org/cargo/guide/cargo-toml-vs-cargo-lock.html
Cargo.lock
# These are backup files generated by rustfmt
**/*.rs.bk

View File

@ -1,32 +0,0 @@
[package]
edition = "2021"
name = "stream-download"
version = "0.1.0"
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
[dependencies]
async-trait = "0.1"
bytes = "1"
futures = "0.3"
futures-util = "0.3"
parking_lot = "0.12"
rangemap = "1"
reqwest = { version = "0.11", features = [
"rustls-tls",
"trust-dns",
"stream",
], default-features = false, optional = true }
symphonia = "0.5"
tempfile = "3"
tokio = { version = "1", features = ["sync", "macros"] }
tracing = "0.1"
[features]
default = ["http"]
http = ["reqwest"]
[dev-dependencies]
rodio = "0.17.1"
tracing-subscriber = "0.3.16"
tokio = { version = "1", features = ["sync", "macros", "rt-multi-thread"] }

View File

@ -1,21 +0,0 @@
MIT License
Copyright (c) 2023 Austin Schey
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.

View File

@ -1 +0,0 @@
# stream-download-rs

View File

@ -1,18 +0,0 @@
use stream_download::StreamDownload;
#[tokio::main]
async fn main() {
tracing_subscriber::fmt().init();
let (_stream, handle) = rodio::OutputStream::try_default().unwrap();
let sink = rodio::Sink::try_new(&handle).unwrap();
let reader = StreamDownload::new_http(
"https://dl.espressif.com/dl/audio/ff-16b-2c-44100hz.flac"
.parse()
.unwrap(),
);
sink.append(rodio::Decoder::new(reader).unwrap());
sink.sleep_until_end();
}

View File

@ -1,17 +0,0 @@
use stream_download::StreamDownload;
fn main() {
tracing_subscriber::fmt().init();
let (_stream, handle) = rodio::OutputStream::try_default().unwrap();
let sink = rodio::Sink::try_new(&handle).unwrap();
let reader = StreamDownload::new_http(
"https://dl.espressif.com/dl/audio/ff-16b-2c-44100hz.flac"
.parse()
.unwrap(),
);
sink.append(rodio::Decoder::new(reader).unwrap());
sink.sleep_until_end();
}

View File

@ -1,18 +0,0 @@
use stream_download::StreamDownload;
#[tokio::main]
async fn main() {
tracing_subscriber::fmt().init();
let (_stream, handle) = rodio::OutputStream::try_default().unwrap();
let sink = rodio::Sink::try_new(&handle).unwrap();
let reader = StreamDownload::new_http(
"https://uk1.internet-radio.com/proxy/pinknoise?mp=/stream"
.parse()
.unwrap(),
);
sink.append(rodio::Decoder::new(reader).unwrap());
sink.sleep_until_end();
}

View File

@ -1,81 +0,0 @@
use async_trait::async_trait;
use bytes::Bytes;
use futures::Stream;
use reqwest::Client;
use std::{
pin::Pin,
str::FromStr,
task::{self, Poll},
};
use tracing::{info, warn};
use crate::source::SourceStream;
pub struct HttpStream {
stream: Box<dyn Stream<Item = Result<Bytes, reqwest::Error>> + Unpin + Send + Sync>,
client: Client,
content_length: Option<u64>,
url: reqwest::Url,
}
impl Stream for HttpStream {
type Item = Result<Bytes, reqwest::Error>;
fn poll_next(mut self: Pin<&mut Self>, cx: &mut task::Context<'_>) -> Poll<Option<Self::Item>> {
Pin::new(&mut self.stream).poll_next(cx)
}
}
#[async_trait]
impl SourceStream for HttpStream {
type Url = reqwest::Url;
type Error = reqwest::Error;
async fn create(url: Self::Url) -> Self {
let client = Client::new();
info!("Requesting content length");
let response = client.get(url.as_str()).send().await.unwrap();
let mut content_length = None;
if let Some(length) = response.headers().get(reqwest::header::CONTENT_LENGTH) {
let length = u64::from_str(length.to_str().unwrap()).unwrap();
info!("Got content length {length}");
content_length = Some(length);
} else {
warn!("Content length header missing");
}
let stream = response.bytes_stream();
Self {
stream: Box::new(stream),
client,
content_length,
url,
}
}
async fn content_length(&self) -> Option<u64> {
self.content_length
}
async fn seek(&mut self, pos: u64) {
info!("Seeking");
self.stream = Box::new(
self.client
.get(self.url.as_str())
.header(
"Range",
format!(
"bytes={pos}-{}",
self.content_length
.map(|l| l.to_string())
.unwrap_or_default()
),
)
.send()
.await
.unwrap()
.bytes_stream(),
);
info!("Done seeking");
}
}

View File

@ -1,166 +0,0 @@
use source::{Source, SourceHandle, SourceStream};
use std::{
io::{self, BufReader, Read, Seek, SeekFrom},
thread,
};
use symphonia::core::io::MediaSource;
use tempfile::NamedTempFile;
use tracing::debug;
#[cfg(feature = "http")]
pub mod http;
pub mod source;
#[derive(Debug)]
pub struct StreamDownload {
output_reader: BufReader<NamedTempFile>,
handle: SourceHandle,
read_position: u64,
}
impl StreamDownload {
#[cfg(feature = "http")]
pub fn new_http(url: reqwest::Url) -> Self {
Self::new::<http::HttpStream>(url)
}
pub fn new<S: SourceStream>(url: S::Url) -> Self {
let tempfile = tempfile::Builder::new().tempfile().unwrap();
let source = Source::new(tempfile.reopen().unwrap());
let handle = source.source_handle();
if let Ok(handle) = tokio::runtime::Handle::try_current() {
handle.spawn(async move {
let stream = S::create(url).await;
source.download(stream).await;
});
} else {
thread::spawn(move || {
let rt = tokio::runtime::Builder::new_current_thread()
.enable_all()
.build()
.unwrap();
rt.block_on(async move {
let stream = S::create(url).await;
source.download(stream).await;
});
});
};
Self {
output_reader: BufReader::new(tempfile),
read_position: 0,
handle,
}
}
pub fn from_stream<S: SourceStream>(stream: S) -> Self {
let tempfile = tempfile::Builder::new().tempfile().unwrap();
let source = Source::new(tempfile.reopen().unwrap());
let handle = source.source_handle();
if let Ok(handle) = tokio::runtime::Handle::try_current() {
handle.spawn(async move {
source.download(stream).await;
});
} else {
thread::spawn(move || {
let rt = tokio::runtime::Builder::new_current_thread()
.enable_all()
.build()
.unwrap();
rt.block_on(async move {
source.download(stream).await;
});
});
};
Self {
output_reader: BufReader::new(tempfile),
handle,
read_position: 0,
}
}
}
impl Read for StreamDownload {
fn read(&mut self, buf: &mut [u8]) -> io::Result<usize> {
debug!("Read request buf len: {}", buf.len());
let requested_position = self.read_position + buf.len() as u64;
debug!(
"read: current position: {} requested position: {requested_position}",
self.read_position
);
if let Some(closest_set) = self.handle.downloaded().get(&self.read_position) {
debug!("Already downloaded {closest_set:?}");
if closest_set.end >= requested_position {
let read_len = self.output_reader.read(buf);
if let Ok(read_len) = read_len {
self.read_position += read_len as u64;
}
return read_len;
}
}
self.handle.request_position(requested_position);
debug!("waiting for position");
self.handle.wait_for_requested_position();
debug!("reached requested position {requested_position}");
self.output_reader.read(buf)
}
}
impl Seek for StreamDownload {
fn seek(&mut self, pos: SeekFrom) -> io::Result<u64> {
let seek_pos = match pos {
SeekFrom::Start(pos) => pos,
SeekFrom::End(pos) => {
if let Some(length) = self.handle.content_length() {
(length as i64 + pos) as u64
} else {
return Err(io::Error::new(
io::ErrorKind::Unsupported,
"Cannot seek from end when content length is unknown",
));
}
}
SeekFrom::Current(pos) => (self.read_position as i64 + pos) as u64,
};
if let Some(closest_set) = self.handle.downloaded().get(&seek_pos) {
if closest_set.end >= seek_pos {
let new_pos = self.output_reader.seek(pos);
if let Ok(new_pos) = new_pos {
self.read_position = new_pos;
}
}
}
self.handle.request_position(seek_pos);
debug!(
"seek: current position {seek_pos} requested position {:?}. waiting",
seek_pos
);
self.handle.seek(seek_pos);
self.handle.wait_for_requested_position();
debug!("reached seek position");
self.output_reader.seek(pos)
}
}
impl MediaSource for StreamDownload {
fn is_seekable(&self) -> bool {
true
}
// FIXME: Can this be implemented?
fn byte_len(&self) -> Option<u64> {
None
}
}

View File

@ -1,228 +0,0 @@
use async_trait::async_trait;
use bytes::Bytes;
use futures::{Stream, StreamExt};
use parking_lot::{Condvar, Mutex, RwLock, RwLockReadGuard};
use rangemap::RangeSet;
use std::{
error::Error,
fs::File,
io::{BufWriter, Seek, SeekFrom, Write},
sync::{
atomic::{AtomicI64, Ordering},
Arc,
},
};
use tokio::sync::mpsc;
use tracing::{debug, info, trace};
#[async_trait]
pub trait SourceStream:
Stream<Item = Result<Bytes, Self::Error>> + Unpin + Send + Sync + 'static
{
type Url: Send;
type Error: Error + Send;
async fn create(url: Self::Url) -> Self;
async fn content_length(&self) -> Option<u64>;
async fn seek(&mut self, position: u64);
}
#[derive(Debug, Clone)]
pub struct SourceHandle {
downloaded: Arc<RwLock<RangeSet<u64>>>,
requested_position: Arc<AtomicI64>,
position_reached: Arc<(Mutex<Waiter>, Condvar)>,
content_length_retrieved: Arc<(Mutex<bool>, Condvar)>,
content_length: Arc<AtomicI64>,
seek_tx: mpsc::Sender<u64>,
}
impl SourceHandle {
pub fn downloaded(&self) -> RwLockReadGuard<rangemap::RangeSet<u64>> {
self.downloaded.read()
}
pub fn request_position(&self, position: u64) {
self.requested_position
.store(position as i64, Ordering::SeqCst);
}
pub fn wait_for_requested_position(&self) {
let (mutex, cvar) = &*self.position_reached;
let mut waiter = mutex.lock();
if !waiter.stream_done {
debug!("Waiting for requested position");
cvar.wait_while(&mut waiter, |waiter| {
!waiter.stream_done && !waiter.position_reached
});
if !waiter.stream_done {
waiter.position_reached = false;
}
debug!("Position reached");
}
}
pub fn seek(&self, position: u64) {
self.seek_tx.try_send(position).ok();
}
pub fn content_length(&self) -> Option<u64> {
let (mutex, cvar) = &*self.content_length_retrieved;
let mut done = mutex.lock();
if !*done {
cvar.wait_while(&mut done, |done| !*done);
}
let length = self.content_length.load(Ordering::SeqCst);
if length > -1 {
Some(length as u64)
} else {
None
}
}
}
#[derive(Default, Debug)]
struct Waiter {
position_reached: bool,
stream_done: bool,
}
pub struct Source {
writer: BufWriter<File>,
downloaded: Arc<RwLock<RangeSet<u64>>>,
position: u64,
requested_position: Arc<AtomicI64>,
position_reached: Arc<(Mutex<Waiter>, Condvar)>,
content_length_retrieved: Arc<(Mutex<bool>, Condvar)>,
content_length: Arc<AtomicI64>,
seek_tx: mpsc::Sender<u64>,
seek_rx: mpsc::Receiver<u64>,
}
const PREFETCH_BYTES: u64 = 1024 * 256;
impl Source {
pub fn new(tempfile: File) -> Self {
let (seek_tx, seek_rx) = mpsc::channel(32);
Self {
writer: BufWriter::new(tempfile),
downloaded: Default::default(),
position: Default::default(),
requested_position: Arc::new(AtomicI64::new(-1)),
position_reached: Default::default(),
content_length_retrieved: Default::default(),
seek_tx,
seek_rx,
content_length: Default::default(),
}
}
pub async fn download<S: SourceStream>(mut self, mut stream: S) {
info!("Starting file download");
let content_length = stream.content_length().await;
if let Some(content_length) = content_length {
self.content_length
.swap(content_length as i64, Ordering::SeqCst);
} else {
self.content_length.swap(-1, Ordering::SeqCst);
}
{
let (mutex, cvar) = &*self.content_length_retrieved;
*mutex.lock() = true;
cvar.notify_all();
}
let mut initial_buffer = 0;
loop {
if let Some(bytes) = stream.next().await {
let bytes = bytes.unwrap();
self.writer.write_all(&bytes).unwrap();
initial_buffer += bytes.len() as u64;
trace!("Prefetch: {}/{} bytes", initial_buffer, PREFETCH_BYTES);
if initial_buffer >= PREFETCH_BYTES {
self.position += initial_buffer;
self.downloaded.write().insert(0..initial_buffer);
break;
}
} else {
info!("File shorter than prefetch length");
self.writer.flush().unwrap();
self.position += initial_buffer;
self.downloaded.write().insert(0..initial_buffer);
let (mutex, cvar) = &*self.position_reached;
(mutex.lock()).stream_done = true;
cvar.notify_all();
return;
}
}
info!("Prefetch complete");
loop {
tokio::select! {
bytes = stream.next() => {
if let Some(bytes) = bytes {
let bytes = bytes.unwrap();
let chunk_len = bytes.len() as u64;
self.writer.write_all(&bytes).unwrap();
let new_position = self.position + chunk_len;
trace!("Received response chunk. position={}", new_position);
self.downloaded.write().insert(self.position..new_position);
let requested = self.requested_position.load(Ordering::SeqCst);
if requested > -1 {
debug!("downloader: requested {requested} current {}", new_position);
}
if requested > -1 && new_position as i64 >= requested {
info!("Notifying");
self.requested_position.store(-1, Ordering::SeqCst);
let (mutex, cvar) = &*self.position_reached;
(mutex.lock()).position_reached = true;
cvar.notify_all();
}
self.position = new_position;
} else {
info!("Stream finished downloading");
self.writer.flush().unwrap();
let (mutex, cvar) = &*self.position_reached;
(mutex.lock()).stream_done = true;
cvar.notify_all();
return;
}
},
pos = self.seek_rx.recv() => {
if let Some(pos) = pos {
debug!("Received seek position {pos}");
let do_seek = {
let downloaded = self.downloaded.read();
if let Some(range) = downloaded.get(&pos) {
!range.contains(&self.position)
} else {
true
}
};
if do_seek {
stream.seek(pos).await;
self.writer.seek(SeekFrom::Start(pos)).unwrap();
self.position = pos;
}
}
}
}
}
}
pub fn source_handle(&self) -> SourceHandle {
SourceHandle {
downloaded: self.downloaded.clone(),
requested_position: self.requested_position.clone(),
position_reached: self.position_reached.clone(),
seek_tx: self.seek_tx.clone(),
content_length_retrieved: self.content_length_retrieved.clone(),
content_length: self.content_length.clone(),
}
}
}

View File

@ -1,25 +1,21 @@
[package] [package]
name = "tidaldy" name = "tidaldy"
version = "0.0.0" version.workspace = true
edition = "2021" edition.workspace = true
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
[dependencies] [dependencies]
async-trait = "0.1.68" async-trait.workspace = true
base64 = "0.21.0" base64.workspace = true
chrono = "0.4.24" chrono.workspace = true
confique = "0.2.3" crabidy-core.workspace = true
crabidy-core = { path = "../crabidy-core" } reqwest.workspace = true
reqwest = { version = "0.11.17", features = ["json", "rustls-tls", "trust-dns"], default-features = false } serde.workspace = true
secrecy = { version = "0.8.0", features = ["serde"] } serde_json.workspace = true
serde = { version = "1.0.162", features = ["derive"] } serde_urlencoded.workspace = true
serde_json = "1.0.96" thiserror.workspace = true
serde_urlencoded = "0.7.1" tokio = { workspace = true, features = ["time", "sync", "macros"] }
thiserror = "1.0.40" toml.workspace = true
tokio = { version = "1.28.1", features = ["full", "time"] } tracing.workspace = true
toml = "0.7.4"
tracing = "0.1.37"
[dev-dependencies] [dev-dependencies]
tokio = { version = "1.28.1", features = ["full"] } tokio = { workspace = true, features = ["full"] }

View File

@ -1,5 +1,3 @@
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;
@ -53,11 +51,11 @@ impl crabidy_core::ProviderClient for Client {
debug!("get_urls_for_track {}", track_uuid); debug!("get_urls_for_track {}", track_uuid);
let (_, track_uuid, _) = split_uuid(track_uuid); let (_, track_uuid, _) = split_uuid(track_uuid);
let Ok(playback) = self.get_track_playback(&track_uuid).await else { let Ok(playback) = self.get_track_playback(&track_uuid).await else {
return Err(crabidy_core::ProviderError::FetchError) return Err(crabidy_core::ProviderError::FetchError);
}; };
debug!("playback {:?}", playback); debug!("playback {:?}", playback);
let Ok(manifest) = playback.get_manifest() else { let Ok(manifest) = playback.get_manifest() else {
return Err(crabidy_core::ProviderError::FetchError) return Err(crabidy_core::ProviderError::FetchError);
}; };
debug!("manifest {:?}", manifest); debug!("manifest {:?}", manifest);
Ok(manifest.urls) Ok(manifest.urls)
@ -70,7 +68,7 @@ impl crabidy_core::ProviderClient for Client {
) -> Result<crabidy_core::proto::crabidy::Track, crabidy_core::ProviderError> { ) -> Result<crabidy_core::proto::crabidy::Track, crabidy_core::ProviderError> {
debug!("get_metadata_for_track {}", track_uuid); debug!("get_metadata_for_track {}", track_uuid);
let Ok(track) = self.get_track(track_uuid).await else { let Ok(track) = self.get_track(track_uuid).await else {
return Err(crabidy_core::ProviderError::FetchError) return Err(crabidy_core::ProviderError::FetchError);
}; };
Ok(track.into()) Ok(track.into())
} }
@ -107,7 +105,7 @@ impl crabidy_core::ProviderClient for Client {
uuid: &str, uuid: &str,
) -> Result<crabidy_core::proto::crabidy::LibraryNode, crabidy_core::ProviderError> { ) -> Result<crabidy_core::proto::crabidy::LibraryNode, crabidy_core::ProviderError> {
let Some(user_id) = self.settings.login.user_id.clone() else { let Some(user_id) = self.settings.login.user_id.clone() else {
return Err(crabidy_core::ProviderError::UnknownUser) return Err(crabidy_core::ProviderError::UnknownUser);
}; };
debug!("get_lib_node in tidaldy{}", uuid); debug!("get_lib_node in tidaldy{}", uuid);
let (_kind, module, uuid) = split_uuid(uuid); let (_kind, module, uuid) = split_uuid(uuid);
@ -236,14 +234,10 @@ impl Client {
) -> Result<T, ClientError> { ) -> Result<T, ClientError> {
debug!("make_request {}", uri); 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(),
))
}; };
let Some(country_code) = self.settings.login.country_code.clone() else { let Some(country_code) = self.settings.login.country_code.clone() else {
return Err(ClientError::AuthError( return Err(ClientError::AuthError("No country code found".to_string()));
"No country code found".to_string(),
))
}; };
let country_param = ("countryCode", country_code); let country_param = ("countryCode", country_code);
let mut params: Vec<&(&str, String)> = vec![&country_param]; let mut params: Vec<&(&str, String)> = vec![&country_param];
@ -279,14 +273,10 @@ impl Client {
) -> Result<Vec<T>, ClientError> { ) -> Result<Vec<T>, ClientError> {
debug!("make_paginated_request {}", uri); 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(),
))
}; };
let Some(country_code) = self.settings.login.country_code.clone() else { let Some(country_code) = self.settings.login.country_code.clone() else {
return Err(ClientError::AuthError( return Err(ClientError::AuthError("No country code found".to_string()));
"No country code found".to_string(),
))
}; };
let country_param = ("countryCode", country_code); let country_param = ("countryCode", country_code);
let limit = 50; let limit = 50;
@ -353,14 +343,10 @@ impl Client {
query: Option<&[(&str, String)]>, query: Option<&[(&str, String)]>,
) -> Result<(), ClientError> { ) -> Result<(), ClientError> {
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(),
))
}; };
let Some(country_code) = self.settings.login.country_code.clone() else { let Some(country_code) = self.settings.login.country_code.clone() else {
return Err(ClientError::AuthError( return Err(ClientError::AuthError("No country code found".to_string()));
"No country code found".to_string(),
))
}; };
let country_param = ("countryCode", country_code); let country_param = ("countryCode", country_code);
let mut params: Vec<&(&str, String)> = vec![&country_param]; let mut params: Vec<&(&str, String)> = vec![&country_param];
@ -543,9 +529,7 @@ impl Client {
#[instrument(skip(self))] #[instrument(skip(self))]
pub async fn login_config(&mut self) -> Result<(), ClientError> { pub async fn login_config(&mut self) -> Result<(), ClientError> {
let Some(access_token) = self.settings.login.access_token.clone() else { let Some(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(),
))
}; };
//return if our session is still valid //return if our session is still valid
if self if self
@ -576,9 +560,7 @@ impl Client {
#[instrument(skip(self))] #[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("No refresh token found".to_string()));
"No refresh token found".to_string(),
))
}; };
let data = DeviceAuthRequest { let data = DeviceAuthRequest {
client_id: self.settings.oauth.client_id.clone(), client_id: self.settings.oauth.client_id.clone(),

View File

@ -1,5 +1,6 @@
use std::{str::FromStr, string::FromUtf8Error}; use std::{str::FromStr, string::FromUtf8Error};
use base64::Engine as _;
use crabidy_core::proto::crabidy::{LibraryNode, LibraryNodeChild}; use crabidy_core::proto::crabidy::{LibraryNode, LibraryNodeChild};
use serde::{Deserialize, Serialize}; use serde::{Deserialize, Serialize};
use serde_json::Value; use serde_json::Value;
@ -432,7 +433,7 @@ pub struct PlaybackManifest {
impl FromStr for PlaybackManifest { impl FromStr for PlaybackManifest {
type Err = ClientError; type Err = ClientError;
fn from_str(input: &str) -> Result<PlaybackManifest, Self::Err> { fn from_str(input: &str) -> Result<PlaybackManifest, Self::Err> {
let decode = base64::decode(input)?; let decode = base64::engine::general_purpose::STANDARD.decode(input)?;
let json = String::from_utf8(decode)?; let json = String::from_utf8(decode)?;
let parsed: PlaybackManifest = serde_json::from_str(&json)?; let parsed: PlaybackManifest = serde_json::from_str(&json)?;
Ok(parsed) Ok(parsed)