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:
parent
91716daf84
commit
6eb5a87b15
File diff suppressed because it is too large
Load Diff
61
Cargo.toml
61
Cargo.toml
|
|
@ -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" }
|
||||||
|
|
|
||||||
|
|
@ -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"] }
|
||||||
|
|
|
||||||
|
|
@ -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(¤t_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 {}
|
|
||||||
|
|
@ -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};
|
||||||
|
|
|
||||||
|
|
@ -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?
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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);
|
|
||||||
|
fn send_reply<T>(tx: Sender<T>, value: T) {
|
||||||
Ok((Box::new(reader), hint))
|
if tx.send(value).is_err() {
|
||||||
} else {
|
warn!("player engine reply receiver dropped");
|
||||||
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 {
|
|
||||||
// Create a hint to help the format registry guess what format reader is appropriate.
|
|
||||||
let mut hint = Hint::new();
|
|
||||||
// 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
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
|
|
|
||||||
|
|
@ -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()
|
||||||
|
|
|
||||||
|
|
@ -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);
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -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]);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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()
|
||||||
|
|
|
||||||
|
|
@ -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);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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"
|
|
||||||
|
|
|
||||||
|
|
@ -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(())
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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"
|
|
||||||
|
|
|
||||||
|
|
@ -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;
|
||||||
|
|
|
||||||
|
|
@ -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();
|
||||||
|
|
|
||||||
|
|
@ -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);
|
||||||
|
|
|
||||||
|
|
@ -12,6 +12,7 @@ let
|
||||||
|
|
||||||
extraPackages = with pkgs; [
|
extraPackages = with pkgs; [
|
||||||
pkg-config
|
pkg-config
|
||||||
|
protobuf
|
||||||
];
|
];
|
||||||
in
|
in
|
||||||
{
|
{
|
||||||
|
|
|
||||||
|
|
@ -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
|
|
||||||
|
|
@ -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"] }
|
|
||||||
|
|
@ -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.
|
|
||||||
|
|
@ -1 +0,0 @@
|
||||||
# stream-download-rs
|
|
||||||
|
|
@ -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();
|
|
||||||
}
|
|
||||||
|
|
@ -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();
|
|
||||||
}
|
|
||||||
|
|
@ -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();
|
|
||||||
}
|
|
||||||
|
|
@ -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");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -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
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -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(),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -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"] }
|
||||||
|
|
|
||||||
|
|
@ -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(),
|
||||||
|
|
|
||||||
|
|
@ -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)
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue