mirror of
https://github.com/talwat/lowfi
synced 2025-11-28 13:29:20 +00:00
feat: fix progress
This commit is contained in:
parent
bf1b5f4f4e
commit
bd525b0813
@ -21,10 +21,6 @@ pub enum Error {
|
|||||||
pub struct Bookmarks {
|
pub struct Bookmarks {
|
||||||
/// The different entries in the bookmarks file.
|
/// The different entries in the bookmarks file.
|
||||||
entries: Vec<String>,
|
entries: Vec<String>,
|
||||||
|
|
||||||
/// The internal bookmarked register, which keeps track
|
|
||||||
/// of whether a track is bookmarked or not.
|
|
||||||
bookmarked: bool,
|
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Bookmarks {
|
impl Bookmarks {
|
||||||
@ -55,10 +51,7 @@ impl Bookmarks {
|
|||||||
})
|
})
|
||||||
.collect();
|
.collect();
|
||||||
|
|
||||||
Ok(Self {
|
Ok(Self { entries })
|
||||||
entries,
|
|
||||||
bookmarked: false,
|
|
||||||
})
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Saves the bookmarks to the `bookmarks.txt` file.
|
// Saves the bookmarks to the `bookmarks.txt` file.
|
||||||
@ -71,7 +64,7 @@ impl Bookmarks {
|
|||||||
/// Bookmarks a given track with a full path and optional custom name.
|
/// Bookmarks a given track with a full path and optional custom name.
|
||||||
///
|
///
|
||||||
/// Returns whether the track is now bookmarked, or not.
|
/// Returns whether the track is now bookmarked, or not.
|
||||||
pub async fn bookmark(&mut self, track: &tracks::Info) -> Result<()> {
|
pub async fn bookmark(&mut self, track: &tracks::Info) -> Result<bool> {
|
||||||
let entry = track.to_entry();
|
let entry = track.to_entry();
|
||||||
let idx = self.entries.iter().position(|x| **x == entry);
|
let idx = self.entries.iter().position(|x| **x == entry);
|
||||||
|
|
||||||
@ -81,19 +74,12 @@ impl Bookmarks {
|
|||||||
self.entries.push(entry);
|
self.entries.push(entry);
|
||||||
};
|
};
|
||||||
|
|
||||||
self.bookmarked = idx.is_none();
|
Ok(idx.is_none())
|
||||||
Ok(())
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Returns whether a track is bookmarked or not by using the internal
|
|
||||||
/// bookmarked register.
|
|
||||||
pub fn bookmarked(&self) -> bool {
|
|
||||||
self.bookmarked
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Sets the internal bookmarked register by checking against
|
/// Sets the internal bookmarked register by checking against
|
||||||
/// the current track's info.
|
/// the current track's info.
|
||||||
pub async fn set_bookmarked(&mut self, track: &tracks::Info) {
|
pub fn bookmarked(&mut self, track: &tracks::Info) -> bool {
|
||||||
self.bookmarked = self.entries.contains(&track.to_entry());
|
self.entries.contains(&track.to_entry())
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,5 +1,5 @@
|
|||||||
use std::{
|
use std::{
|
||||||
sync::{atomic::AtomicU8, Arc},
|
sync::atomic::{self, AtomicU8},
|
||||||
time::Duration,
|
time::Duration,
|
||||||
};
|
};
|
||||||
|
|
||||||
@ -11,61 +11,90 @@ use tokio::{
|
|||||||
|
|
||||||
use crate::tracks;
|
use crate::tracks;
|
||||||
|
|
||||||
|
static PROGRESS: AtomicU8 = AtomicU8::new(0);
|
||||||
|
pub type Progress = &'static AtomicU8;
|
||||||
|
|
||||||
|
pub fn progress() -> Progress {
|
||||||
|
&PROGRESS
|
||||||
|
}
|
||||||
|
|
||||||
pub struct Downloader {
|
pub struct Downloader {
|
||||||
/// TODO: Actually have a track type here.
|
queue: Sender<tracks::Queued>,
|
||||||
pub progress: Arc<AtomicU8>,
|
tx: Sender<crate::Message>,
|
||||||
queue: Receiver<tracks::Queued>,
|
tracks: tracks::List,
|
||||||
handle: JoinHandle<crate::Result<()>>,
|
client: Client,
|
||||||
|
timeout: Duration,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Downloader {
|
impl Downloader {
|
||||||
pub async fn track(&mut self) -> Option<tracks::Queued> {
|
pub async fn init(size: usize, tracks: tracks::List, tx: Sender<crate::Message>) -> Handle {
|
||||||
return self.queue.recv().await;
|
let client = Client::new();
|
||||||
|
|
||||||
|
let (qtx, qrx) = mpsc::channel(size);
|
||||||
|
let downloader = Self {
|
||||||
|
queue: qtx,
|
||||||
|
tx,
|
||||||
|
tracks,
|
||||||
|
client,
|
||||||
|
timeout: Duration::from_secs(1),
|
||||||
|
};
|
||||||
|
|
||||||
|
Handle {
|
||||||
|
queue: qrx,
|
||||||
|
handle: tokio::spawn(downloader.run()),
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async fn downloader(
|
async fn run(self) -> crate::Result<()> {
|
||||||
tx: Sender<tracks::Queued>,
|
|
||||||
tracks: tracks::List,
|
|
||||||
client: Client,
|
|
||||||
progress: Arc<AtomicU8>,
|
|
||||||
timeout: Duration,
|
|
||||||
) -> crate::Result<()> {
|
|
||||||
loop {
|
loop {
|
||||||
let result = tracks.random(&client, progress.as_ref()).await;
|
let progress = if PROGRESS.load(atomic::Ordering::Relaxed) == 0 {
|
||||||
|
Some(&PROGRESS)
|
||||||
|
} else {
|
||||||
|
None
|
||||||
|
};
|
||||||
|
|
||||||
|
let result = self.tracks.random(&self.client, progress).await;
|
||||||
match result {
|
match result {
|
||||||
Ok(track) => tx.send(track).await?,
|
Ok(track) => {
|
||||||
|
self.queue.send(track).await?;
|
||||||
|
|
||||||
|
if progress.is_some() {
|
||||||
|
self.tx.send(crate::Message::Loaded).await?;
|
||||||
|
}
|
||||||
|
}
|
||||||
Err(error) => {
|
Err(error) => {
|
||||||
|
PROGRESS.store(0, atomic::Ordering::Relaxed);
|
||||||
if !error.timeout() {
|
if !error.timeout() {
|
||||||
tokio::time::sleep(timeout).await;
|
tokio::time::sleep(self.timeout).await;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
pub struct Handle {
|
||||||
|
queue: Receiver<tracks::Queued>,
|
||||||
|
handle: JoinHandle<crate::Result<()>>,
|
||||||
|
}
|
||||||
|
|
||||||
pub async fn init(
|
pub enum Output {
|
||||||
size: usize,
|
Loading(Progress),
|
||||||
tracks: tracks::List,
|
Queued(tracks::Queued),
|
||||||
client: Client,
|
}
|
||||||
progress: Arc<AtomicU8>,
|
|
||||||
) -> Self {
|
|
||||||
let (tx, rx) = mpsc::channel(size);
|
|
||||||
|
|
||||||
Self {
|
impl Handle {
|
||||||
queue: rx,
|
pub async fn track(&mut self) -> Output {
|
||||||
progress: progress.clone(),
|
match self.queue.try_recv() {
|
||||||
handle: tokio::spawn(Self::downloader(
|
Ok(queued) => Output::Queued(queued),
|
||||||
tx,
|
Err(_) => {
|
||||||
tracks,
|
PROGRESS.store(0, atomic::Ordering::Relaxed);
|
||||||
client,
|
Output::Loading(&PROGRESS)
|
||||||
progress,
|
}
|
||||||
Duration::from_secs(1),
|
|
||||||
)),
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Drop for Downloader {
|
impl Drop for Handle {
|
||||||
fn drop(&mut self) {
|
fn drop(&mut self) {
|
||||||
self.handle.abort();
|
self.handle.abort();
|
||||||
}
|
}
|
||||||
|
|||||||
12
src/error.rs
12
src/error.rs
@ -1,4 +1,4 @@
|
|||||||
use tokio::sync::mpsc;
|
use tokio::sync::{broadcast, mpsc};
|
||||||
|
|
||||||
use crate::{bookmark, tracks, ui, volume};
|
use crate::{bookmark, tracks, ui, volume};
|
||||||
|
|
||||||
@ -26,19 +26,25 @@ pub enum Error {
|
|||||||
#[error("couldn't add track to the queue: {0}")]
|
#[error("couldn't add track to the queue: {0}")]
|
||||||
Queue(#[from] mpsc::error::SendError<tracks::Queued>),
|
Queue(#[from] mpsc::error::SendError<tracks::Queued>),
|
||||||
|
|
||||||
|
#[error("couldn't update UI state: {0}")]
|
||||||
|
Broadcast(#[from] broadcast::error::SendError<ui::Update>),
|
||||||
|
|
||||||
#[error("io error: {0}")]
|
#[error("io error: {0}")]
|
||||||
Io(#[from] std::io::Error),
|
Io(#[from] std::io::Error),
|
||||||
|
|
||||||
#[error("directory not found")]
|
#[error("directory not found")]
|
||||||
Directory,
|
Directory,
|
||||||
|
|
||||||
|
#[error("couldn't fetch track from downloader")]
|
||||||
|
Download,
|
||||||
|
|
||||||
#[error("couldn't parse integer: {0}")]
|
#[error("couldn't parse integer: {0}")]
|
||||||
Parse(#[from] std::num::ParseIntError),
|
Parse(#[from] std::num::ParseIntError),
|
||||||
|
|
||||||
#[error("track error: {0}")]
|
#[error("track failure")]
|
||||||
Track(#[from] tracks::Error),
|
Track(#[from] tracks::Error),
|
||||||
|
|
||||||
#[error("ui error: {0}")]
|
#[error("ui failure")]
|
||||||
UI(#[from] ui::Error),
|
UI(#[from] ui::Error),
|
||||||
|
|
||||||
#[cfg(feature = "mpris")]
|
#[cfg(feature = "mpris")]
|
||||||
|
|||||||
@ -108,7 +108,7 @@ async fn main() -> eyre::Result<()> {
|
|||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
let player = Player::init(args).await?;
|
let player = Player::init(args).await?;
|
||||||
player.play().await?;
|
player.run().await?;
|
||||||
};
|
};
|
||||||
|
|
||||||
Ok(())
|
Ok(())
|
||||||
|
|||||||
@ -1,14 +1,12 @@
|
|||||||
use crate::ui;
|
|
||||||
|
|
||||||
/// Handles communication between different parts of the program.
|
/// Handles communication between different parts of the program.
|
||||||
#[derive(PartialEq, Debug, Clone)]
|
#[derive(PartialEq, Debug, Clone)]
|
||||||
pub enum Message {
|
pub enum Message {
|
||||||
/// Sent to update the UI with new information.
|
|
||||||
Render(ui::Update),
|
|
||||||
|
|
||||||
/// Notifies the audio server that it should update the track.
|
/// Notifies the audio server that it should update the track.
|
||||||
Next,
|
Next,
|
||||||
|
|
||||||
|
/// When a track is loaded after a caller previously being told to wait.
|
||||||
|
Loaded,
|
||||||
|
|
||||||
/// Similar to Next, but specific to the first track.
|
/// Similar to Next, but specific to the first track.
|
||||||
Init,
|
Init,
|
||||||
|
|
||||||
|
|||||||
147
src/player.rs
147
src/player.rs
@ -1,21 +1,36 @@
|
|||||||
use std::sync::{atomic::AtomicU8, Arc};
|
use std::sync::Arc;
|
||||||
|
|
||||||
use reqwest::Client;
|
use tokio::sync::{
|
||||||
use tokio::sync::mpsc::{self, Receiver};
|
broadcast,
|
||||||
|
mpsc::{self, Receiver, Sender},
|
||||||
|
};
|
||||||
|
|
||||||
use crate::{
|
use crate::{
|
||||||
bookmark::Bookmarks, download::Downloader, tracks::List, ui::UI, volume::PersistentVolume,
|
bookmark::Bookmarks,
|
||||||
|
download::{self, Downloader},
|
||||||
|
tracks::{self, List},
|
||||||
|
ui,
|
||||||
|
volume::PersistentVolume,
|
||||||
Message,
|
Message,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
#[derive(Clone, Debug)]
|
||||||
|
pub enum Current {
|
||||||
|
Loading(download::Progress),
|
||||||
|
Track(tracks::Info),
|
||||||
|
}
|
||||||
|
|
||||||
pub struct Player {
|
pub struct Player {
|
||||||
ui: UI,
|
ui: ui::Handle,
|
||||||
|
downloader: download::Handle,
|
||||||
volume: PersistentVolume,
|
volume: PersistentVolume,
|
||||||
bookmarks: Bookmarks,
|
bookmarks: Bookmarks,
|
||||||
downloader: Downloader,
|
|
||||||
sink: Arc<rodio::Sink>,
|
sink: Arc<rodio::Sink>,
|
||||||
stream: rodio::OutputStream,
|
|
||||||
rx: Receiver<crate::Message>,
|
rx: Receiver<crate::Message>,
|
||||||
|
broadcast: broadcast::Sender<ui::Update>,
|
||||||
|
current: Current,
|
||||||
|
_tx: Sender<crate::Message>,
|
||||||
|
_stream: rodio::OutputStream,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Drop for Player {
|
impl Drop for Player {
|
||||||
@ -25,6 +40,25 @@ impl Drop for Player {
|
|||||||
}
|
}
|
||||||
|
|
||||||
impl Player {
|
impl Player {
|
||||||
|
pub async fn set_current(&mut self, current: Current) -> crate::Result<()> {
|
||||||
|
self.current = current.clone();
|
||||||
|
self.update(ui::Update::Track(current)).await?;
|
||||||
|
|
||||||
|
let Current::Track(track) = &self.current else {
|
||||||
|
return Ok(());
|
||||||
|
};
|
||||||
|
|
||||||
|
let bookmarked = self.bookmarks.bookmarked(&track);
|
||||||
|
self.update(ui::Update::Bookmarked(bookmarked)).await?;
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
pub async fn update(&mut self, update: ui::Update) -> crate::Result<()> {
|
||||||
|
self.broadcast.send(update)?;
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
pub async fn init(args: crate::Args) -> crate::Result<Self> {
|
pub async fn init(args: crate::Args) -> crate::Result<Self> {
|
||||||
#[cfg(target_os = "linux")]
|
#[cfg(target_os = "linux")]
|
||||||
let mut stream = audio::silent_get_output_stream()?;
|
let mut stream = audio::silent_get_output_stream()?;
|
||||||
@ -33,42 +67,105 @@ impl Player {
|
|||||||
stream.log_on_drop(false);
|
stream.log_on_drop(false);
|
||||||
let sink = Arc::new(rodio::Sink::connect_new(stream.mixer()));
|
let sink = Arc::new(rodio::Sink::connect_new(stream.mixer()));
|
||||||
|
|
||||||
let progress = Arc::new(AtomicU8::new(0));
|
|
||||||
let (tx, rx) = mpsc::channel(8);
|
let (tx, rx) = mpsc::channel(8);
|
||||||
let ui = UI::init(tx, progress.clone(), sink.clone(), &args).await?;
|
tx.send(Message::Init).await?;
|
||||||
|
let (utx, urx) = broadcast::channel(8);
|
||||||
|
let current = Current::Loading(download::progress());
|
||||||
|
|
||||||
|
let state = ui::State::initial(sink.clone(), &args, current.clone());
|
||||||
|
let ui = ui::Handle::init(tx.clone(), urx, state.clone(), &args).await?;
|
||||||
|
|
||||||
let volume = PersistentVolume::load().await?;
|
let volume = PersistentVolume::load().await?;
|
||||||
let bookmarks = Bookmarks::load().await?;
|
let bookmarks = Bookmarks::load().await?;
|
||||||
|
|
||||||
let client = Client::new();
|
|
||||||
let list = List::load(args.track_list.as_ref()).await?;
|
let list = List::load(args.track_list.as_ref()).await?;
|
||||||
let downloader = Downloader::init(args.buffer_size, list, client, progress).await;
|
let downloader = Downloader::init(args.buffer_size, list, tx.clone()).await;
|
||||||
|
|
||||||
Ok(Self {
|
Ok(Self {
|
||||||
|
current,
|
||||||
downloader,
|
downloader,
|
||||||
ui,
|
ui,
|
||||||
|
broadcast: utx,
|
||||||
rx,
|
rx,
|
||||||
sink,
|
sink,
|
||||||
stream,
|
|
||||||
bookmarks,
|
bookmarks,
|
||||||
volume,
|
volume,
|
||||||
|
_stream: stream,
|
||||||
|
_tx: tx,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
pub async fn play(mut self) -> crate::Result<()> {
|
pub async fn close(&self) -> crate::Result<()> {
|
||||||
// self.ui
|
self.bookmarks.save().await?;
|
||||||
// .render(ui::Update {
|
self.volume.save().await?;
|
||||||
// track: None,
|
|
||||||
// bookmarked: false,
|
|
||||||
// })
|
|
||||||
// .await?;
|
|
||||||
|
|
||||||
while let Some(message) = self.rx.recv().await {
|
|
||||||
if message == Message::Quit {
|
|
||||||
break;
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub async fn play(&mut self, queued: tracks::Queued) -> crate::Result<()> {
|
||||||
|
let decoded = queued.decode()?;
|
||||||
|
self.sink.append(decoded.data);
|
||||||
|
self.set_current(Current::Track(decoded.info)).await?;
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
pub async fn run(mut self) -> crate::Result<()> {
|
||||||
|
while let Some(message) = self.rx.recv().await {
|
||||||
|
match message {
|
||||||
|
Message::Next | Message::Init | Message::Loaded => {
|
||||||
|
self.sink.stop();
|
||||||
|
match self.downloader.track().await {
|
||||||
|
download::Output::Loading(progress) => {
|
||||||
|
self.set_current(Current::Loading(progress)).await?
|
||||||
|
}
|
||||||
|
download::Output::Queued(queued) => self.play(queued).await?,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
Message::Play => {
|
||||||
|
self.sink.play();
|
||||||
|
|
||||||
|
// #[cfg(feature = "mpris")]
|
||||||
|
// mpris.playback(PlaybackStatus::Playing).await?;
|
||||||
|
}
|
||||||
|
Message::Pause => {
|
||||||
|
self.sink.pause();
|
||||||
|
|
||||||
|
// #[cfg(feature = "mpris")]
|
||||||
|
// mpris.playback(PlaybackStatus::Paused).await?;
|
||||||
|
}
|
||||||
|
Message::PlayPause => {
|
||||||
|
if self.sink.is_paused() {
|
||||||
|
self.sink.play();
|
||||||
|
} else {
|
||||||
|
self.sink.pause();
|
||||||
|
}
|
||||||
|
|
||||||
|
// #[cfg(feature = "mpris")]
|
||||||
|
// mpris
|
||||||
|
// .playback(mpris.player().playback_status().await?)
|
||||||
|
// .await?;
|
||||||
|
}
|
||||||
|
Message::ChangeVolume(change) => {
|
||||||
|
self.sink.set_volume(self.sink.volume() + change);
|
||||||
|
|
||||||
|
// #[cfg(feature = "mpris")]
|
||||||
|
// mpris
|
||||||
|
// .changed(vec![Property::Volume(player.sink.volume().into())])
|
||||||
|
// .await?;
|
||||||
|
}
|
||||||
|
Message::Bookmark => {
|
||||||
|
let Current::Track(current) = &self.current else {
|
||||||
|
continue;
|
||||||
|
};
|
||||||
|
|
||||||
|
self.bookmarks.bookmark(current).await?;
|
||||||
|
}
|
||||||
|
Message::Quit => break,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// self.close().await?;
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -15,7 +15,7 @@
|
|||||||
//! 2. [`Info`] created from decoded data.
|
//! 2. [`Info`] created from decoded data.
|
||||||
//! 3. [`Decoded`] made from [`Info`] and the original decoded data.
|
//! 3. [`Decoded`] made from [`Info`] and the original decoded data.
|
||||||
|
|
||||||
use std::{io::Cursor, time::Duration};
|
use std::{fmt::Debug, io::Cursor, time::Duration};
|
||||||
|
|
||||||
use bytes::Bytes;
|
use bytes::Bytes;
|
||||||
use rodio::{Decoder, Source as _};
|
use rodio::{Decoder, Source as _};
|
||||||
@ -35,6 +35,7 @@ pub type DecodedData = Decoder<Cursor<Bytes>>;
|
|||||||
/// Tracks which are still waiting in the queue, and can't be played yet.
|
/// Tracks which are still waiting in the queue, and can't be played yet.
|
||||||
///
|
///
|
||||||
/// This means that only the data & track name are included.
|
/// This means that only the data & track name are included.
|
||||||
|
#[derive(PartialEq)]
|
||||||
pub struct Queued {
|
pub struct Queued {
|
||||||
/// Display name of the track.
|
/// Display name of the track.
|
||||||
pub display: String,
|
pub display: String,
|
||||||
@ -47,6 +48,16 @@ pub struct Queued {
|
|||||||
pub data: Bytes,
|
pub data: Bytes,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
impl Debug for Queued {
|
||||||
|
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||||
|
f.debug_struct("Queued")
|
||||||
|
.field("display", &self.display)
|
||||||
|
.field("path", &self.path)
|
||||||
|
.field("data", &self.data.len())
|
||||||
|
.finish()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
impl Queued {
|
impl Queued {
|
||||||
/// This will actually decode and format the track,
|
/// This will actually decode and format the track,
|
||||||
/// returning a [`DecodedTrack`] which can be played
|
/// returning a [`DecodedTrack`] which can be played
|
||||||
|
|||||||
@ -25,8 +25,6 @@ pub enum Kind {
|
|||||||
#[error("{kind} (track: {track:?})")]
|
#[error("{kind} (track: {track:?})")]
|
||||||
pub struct Error {
|
pub struct Error {
|
||||||
pub track: Option<String>,
|
pub track: Option<String>,
|
||||||
|
|
||||||
#[source]
|
|
||||||
pub kind: Kind,
|
pub kind: Kind,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -70,16 +70,16 @@ impl List {
|
|||||||
&self,
|
&self,
|
||||||
track: &str,
|
track: &str,
|
||||||
client: &Client,
|
client: &Client,
|
||||||
progress: &AtomicU8,
|
progress: Option<&AtomicU8>,
|
||||||
) -> tracks::Result<(Bytes, String)> {
|
) -> tracks::Result<(Bytes, String)> {
|
||||||
// If the track has a protocol, then we should ignore the base for it.
|
// If the track has a protocol, then we should ignore the base for it.
|
||||||
let full_path = if track.contains("://") {
|
let path = if track.contains("://") {
|
||||||
track.to_owned()
|
track.to_owned()
|
||||||
} else {
|
} else {
|
||||||
format!("{}{}", self.base(), track)
|
format!("{}{}", self.base(), track)
|
||||||
};
|
};
|
||||||
|
|
||||||
let data: Bytes = if let Some(x) = full_path.strip_prefix("file://") {
|
let data: Bytes = if let Some(x) = path.strip_prefix("file://") {
|
||||||
let path = if x.starts_with('~') {
|
let path = if x.starts_with('~') {
|
||||||
let home_path = dirs::home_dir()
|
let home_path = dirs::home_dir()
|
||||||
.ok_or(error::Kind::InvalidPath)
|
.ok_or(error::Kind::InvalidPath)
|
||||||
@ -97,7 +97,11 @@ impl List {
|
|||||||
let result = tokio::fs::read(path.clone()).await.track(x)?;
|
let result = tokio::fs::read(path.clone()).await.track(x)?;
|
||||||
result.into()
|
result.into()
|
||||||
} else {
|
} else {
|
||||||
let response = client.get(full_path.clone()).send().await.track(track)?;
|
let response = client.get(path.clone()).send().await.track(track)?;
|
||||||
|
let Some(progress) = progress else {
|
||||||
|
let bytes = response.bytes().await.track(track)?;
|
||||||
|
return Ok((bytes, path));
|
||||||
|
};
|
||||||
|
|
||||||
let total = response
|
let total = response
|
||||||
.content_length()
|
.content_length()
|
||||||
@ -119,14 +123,18 @@ impl List {
|
|||||||
bytes.into()
|
bytes.into()
|
||||||
};
|
};
|
||||||
|
|
||||||
Ok((data, full_path))
|
Ok((data, path))
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Fetches and downloads a random track from the [List].
|
/// Fetches and downloads a random track from the [List].
|
||||||
///
|
///
|
||||||
/// The Result's error is a bool, which is true if a timeout error occured,
|
/// The Result's error is a bool, which is true if a timeout error occured,
|
||||||
/// and false otherwise. This tells lowfi if it shouldn't wait to try again.
|
/// and false otherwise. This tells lowfi if it shouldn't wait to try again.
|
||||||
pub async fn random(&self, client: &Client, progress: &AtomicU8) -> tracks::Result<Queued> {
|
pub async fn random(
|
||||||
|
&self,
|
||||||
|
client: &Client,
|
||||||
|
progress: Option<&AtomicU8>,
|
||||||
|
) -> tracks::Result<Queued> {
|
||||||
let (path, display) = self.random_path();
|
let (path, display) = self.random_path();
|
||||||
let (data, path) = self.download(&path, client, progress).await?;
|
let (data, path) = self.download(&path, client, progress).await?;
|
||||||
|
|
||||||
|
|||||||
110
src/ui.rs
110
src/ui.rs
@ -1,16 +1,12 @@
|
|||||||
use std::{
|
use std::sync::Arc;
|
||||||
sync::{atomic::AtomicU8, Arc},
|
|
||||||
time::Duration,
|
|
||||||
};
|
|
||||||
|
|
||||||
use crate::{
|
use crate::{
|
||||||
tracks,
|
player::Current,
|
||||||
ui::{environment::Environment, window::Window},
|
ui::{self, environment::Environment, window::Window},
|
||||||
Args, Message,
|
Args,
|
||||||
};
|
};
|
||||||
use rodio::Sink;
|
|
||||||
use tokio::{
|
use tokio::{
|
||||||
sync::mpsc::{self, Receiver, Sender},
|
sync::{broadcast, mpsc::Sender},
|
||||||
task::JoinHandle,
|
task::JoinHandle,
|
||||||
};
|
};
|
||||||
mod components;
|
mod components;
|
||||||
@ -32,74 +28,64 @@ pub enum Error {
|
|||||||
Write(#[from] std::io::Error),
|
Write(#[from] std::io::Error),
|
||||||
|
|
||||||
#[error("sending message to backend from ui failed")]
|
#[error("sending message to backend from ui failed")]
|
||||||
Communication(#[from] tokio::sync::mpsc::error::SendError<Message>),
|
CrateSend(#[from] tokio::sync::mpsc::error::SendError<crate::Message>),
|
||||||
|
|
||||||
|
#[error("sharing state between backend and frontend failed")]
|
||||||
|
UiSend(#[from] tokio::sync::broadcast::error::SendError<Update>),
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[derive(Clone)]
|
||||||
pub struct State {
|
pub struct State {
|
||||||
pub sink: Arc<rodio::Sink>,
|
pub sink: Arc<rodio::Sink>,
|
||||||
pub progress: Arc<AtomicU8>,
|
pub current: Current,
|
||||||
pub track: Option<tracks::Info>,
|
|
||||||
pub bookmarked: bool,
|
pub bookmarked: bool,
|
||||||
width: usize,
|
width: usize,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl State {
|
impl State {
|
||||||
pub fn update(&mut self, update: Update) {
|
pub fn initial(sink: Arc<rodio::Sink>, args: &Args, current: Current) -> Self {
|
||||||
self.track = update.track;
|
let width = 21 + args.width.min(32) * 2;
|
||||||
self.bookmarked = update.bookmarked;
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn initial(sink: Arc<rodio::Sink>, width: usize, progress: Arc<AtomicU8>) -> Self {
|
|
||||||
Self {
|
Self {
|
||||||
width,
|
width,
|
||||||
sink,
|
sink,
|
||||||
progress,
|
current,
|
||||||
track: None,
|
|
||||||
bookmarked: false,
|
bookmarked: false,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
#[derive(Debug, Clone, PartialEq, Default)]
|
|
||||||
pub struct Update {
|
#[derive(Debug, Clone)]
|
||||||
pub track: Option<tracks::Info>,
|
pub enum Update {
|
||||||
pub bookmarked: bool,
|
Track(Current),
|
||||||
|
Bookmarked(bool),
|
||||||
|
Quit,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Debug)]
|
#[derive(Debug)]
|
||||||
struct Handles {
|
struct Tasks {
|
||||||
render: JoinHandle<Result<()>>,
|
render: JoinHandle<Result<()>>,
|
||||||
input: JoinHandle<Result<()>>,
|
input: JoinHandle<Result<()>>,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Copy, Clone, Debug)]
|
|
||||||
struct Params {
|
|
||||||
borderless: bool,
|
|
||||||
minimalist: bool,
|
|
||||||
delta: Duration,
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Debug)]
|
#[derive(Debug)]
|
||||||
pub struct UI {
|
pub struct Handle {
|
||||||
pub utx: Sender<Message>,
|
tasks: Tasks,
|
||||||
handles: Handles,
|
|
||||||
_environment: Environment,
|
_environment: Environment,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Drop for UI {
|
impl Drop for Handle {
|
||||||
fn drop(&mut self) {
|
fn drop(&mut self) {
|
||||||
self.handles.input.abort();
|
self.tasks.input.abort();
|
||||||
self.handles.render.abort();
|
self.tasks.render.abort();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl UI {
|
impl Handle {
|
||||||
pub async fn render(&mut self, data: Update) -> Result<()> {
|
async fn ui(
|
||||||
self.utx.send(Message::Render(data)).await?;
|
mut rx: broadcast::Receiver<Update>,
|
||||||
|
mut state: State,
|
||||||
Ok(())
|
params: interface::Params,
|
||||||
}
|
) -> Result<()> {
|
||||||
|
|
||||||
async fn ui(mut rx: Receiver<Message>, mut state: State, params: Params) -> Result<()> {
|
|
||||||
let mut interval = tokio::time::interval(params.delta);
|
let mut interval = tokio::time::interval(params.delta);
|
||||||
let mut window = Window::new(state.width, params.borderless);
|
let mut window = Window::new(state.width, params.borderless);
|
||||||
|
|
||||||
@ -108,45 +94,29 @@ impl UI {
|
|||||||
|
|
||||||
if let Ok(message) = rx.try_recv() {
|
if let Ok(message) = rx.try_recv() {
|
||||||
match message {
|
match message {
|
||||||
Message::Render(update) => state.update(update),
|
Update::Track(track) => state.current = track,
|
||||||
Message::Quit => break,
|
Update::Bookmarked(bookmarked) => state.bookmarked = bookmarked,
|
||||||
_ => continue,
|
Update::Quit => break,
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
interval.tick().await;
|
interval.tick().await;
|
||||||
}
|
}
|
||||||
|
|
||||||
// environment.cleanup()?;
|
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
pub async fn init(
|
pub async fn init(
|
||||||
tx: Sender<Message>,
|
tx: Sender<crate::Message>,
|
||||||
progress: Arc<AtomicU8>,
|
updater: broadcast::Receiver<ui::Update>,
|
||||||
sink: Arc<Sink>,
|
state: State,
|
||||||
args: &Args,
|
args: &Args,
|
||||||
) -> Result<Self> {
|
) -> Result<Self> {
|
||||||
let environment = Environment::ready(args.alternate)?;
|
let environment = Environment::ready(args.alternate)?;
|
||||||
|
|
||||||
let (utx, urx) = mpsc::channel(8);
|
|
||||||
let delta = 1.0 / f32::from(args.fps);
|
|
||||||
let delta = Duration::from_secs_f32(delta);
|
|
||||||
let width = 21 + args.width.min(32) * 2;
|
|
||||||
|
|
||||||
Ok(Self {
|
Ok(Self {
|
||||||
utx,
|
|
||||||
_environment: environment,
|
_environment: environment,
|
||||||
handles: Handles {
|
tasks: Tasks {
|
||||||
render: tokio::spawn(Self::ui(
|
render: tokio::spawn(Self::ui(updater, state, interface::Params::from(args))),
|
||||||
urx,
|
|
||||||
State::initial(sink, width, progress),
|
|
||||||
Params {
|
|
||||||
delta,
|
|
||||||
minimalist: args.minimalist,
|
|
||||||
borderless: args.borderless,
|
|
||||||
},
|
|
||||||
)),
|
|
||||||
input: tokio::spawn(input::listen(tx)),
|
input: tokio::spawn(input::listen(tx)),
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
|
|||||||
@ -6,7 +6,7 @@ use std::time::Duration;
|
|||||||
use crossterm::style::Stylize as _;
|
use crossterm::style::Stylize as _;
|
||||||
use unicode_segmentation::UnicodeSegmentation as _;
|
use unicode_segmentation::UnicodeSegmentation as _;
|
||||||
|
|
||||||
use crate::{tracks, ui};
|
use crate::{player::Current, tracks, ui};
|
||||||
|
|
||||||
/// Small helper function to format durations.
|
/// Small helper function to format durations.
|
||||||
pub fn format_duration(duration: &Duration) -> String {
|
pub fn format_duration(duration: &Duration) -> String {
|
||||||
@ -19,14 +19,14 @@ pub fn format_duration(duration: &Duration) -> String {
|
|||||||
/// Creates the progress bar, as well as all the padding needed.
|
/// Creates the progress bar, as well as all the padding needed.
|
||||||
pub fn progress_bar(state: &ui::State, width: usize) -> String {
|
pub fn progress_bar(state: &ui::State, width: usize) -> String {
|
||||||
let mut duration = Duration::new(0, 0);
|
let mut duration = Duration::new(0, 0);
|
||||||
let elapsed = if state.track.is_some() {
|
let elapsed = if matches!(&state.current, Current::Track(_)) {
|
||||||
state.sink.get_pos()
|
state.sink.get_pos()
|
||||||
} else {
|
} else {
|
||||||
Duration::new(0, 0)
|
Duration::new(0, 0)
|
||||||
};
|
};
|
||||||
|
|
||||||
let mut filled = 0;
|
let mut filled = 0;
|
||||||
if let Some(current) = &state.track {
|
if let Current::Track(current) = &state.current {
|
||||||
if let Some(x) = current.duration {
|
if let Some(x) = current.duration {
|
||||||
duration = x;
|
duration = x;
|
||||||
|
|
||||||
@ -106,33 +106,22 @@ impl ActionBar {
|
|||||||
/// Creates the top/action bar, which has the name of the track and it's status.
|
/// Creates the top/action bar, which has the name of the track and it's status.
|
||||||
/// This also creates all the needed padding.
|
/// This also creates all the needed padding.
|
||||||
pub fn action(state: &ui::State, width: usize) -> String {
|
pub fn action(state: &ui::State, width: usize) -> String {
|
||||||
let (main, len) = state
|
let action = match state.current.clone() {
|
||||||
.track
|
Current::Loading(progress) => {
|
||||||
.as_ref()
|
ActionBar::Loading(progress.load(std::sync::atomic::Ordering::Relaxed))
|
||||||
.map_or_else(
|
}
|
||||||
|| {
|
Current::Track(info) => {
|
||||||
ActionBar::Loading(
|
if state.sink.volume() < 0.01 {
|
||||||
state
|
ActionBar::Muted
|
||||||
.progress
|
} else if state.sink.is_paused() {
|
||||||
.load(std::sync::atomic::Ordering::Acquire)
|
ActionBar::Paused(info)
|
||||||
.into(),
|
} else {
|
||||||
)
|
ActionBar::Playing(info)
|
||||||
},
|
}
|
||||||
|info| {
|
}
|
||||||
if state.sink.volume() < 0.01 {
|
};
|
||||||
return ActionBar::Muted;
|
|
||||||
}
|
|
||||||
|
|
||||||
let info = info.clone();
|
|
||||||
if state.sink.is_paused() {
|
|
||||||
ActionBar::Paused(info)
|
|
||||||
} else {
|
|
||||||
ActionBar::Playing(info)
|
|
||||||
}
|
|
||||||
},
|
|
||||||
)
|
|
||||||
.format(state.bookmarked);
|
|
||||||
|
|
||||||
|
let (main, len) = action.format(state.bookmarked);
|
||||||
if len > width {
|
if len > width {
|
||||||
let chopped: String = main.graphemes(true).take(width + 1).collect();
|
let chopped: String = main.graphemes(true).take(width + 1).collect();
|
||||||
|
|
||||||
|
|||||||
@ -1,9 +1,34 @@
|
|||||||
use crate::ui::{self, components, window::Window};
|
use std::time::Duration;
|
||||||
|
|
||||||
|
use crate::{
|
||||||
|
ui::{self, components, window::Window},
|
||||||
|
Args,
|
||||||
|
};
|
||||||
|
|
||||||
|
#[derive(Copy, Clone, Debug)]
|
||||||
|
pub struct Params {
|
||||||
|
pub borderless: bool,
|
||||||
|
pub minimalist: bool,
|
||||||
|
pub delta: Duration,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl From<&Args> for Params {
|
||||||
|
fn from(args: &Args) -> Self {
|
||||||
|
let delta = 1.0 / f32::from(args.fps);
|
||||||
|
let delta = Duration::from_secs_f32(delta);
|
||||||
|
|
||||||
|
Self {
|
||||||
|
delta,
|
||||||
|
minimalist: args.minimalist,
|
||||||
|
borderless: args.borderless,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/// The code for the terminal interface itself.
|
/// The code for the terminal interface itself.
|
||||||
///
|
///
|
||||||
/// * `minimalist` - All this does is hide the bottom control bar.
|
/// * `minimalist` - All this does is hide the bottom control bar.
|
||||||
pub async fn draw(state: &ui::State, window: &mut Window, params: ui::Params) -> super::Result<()> {
|
pub async fn draw(state: &ui::State, window: &mut Window, params: Params) -> super::Result<()> {
|
||||||
let action = components::action(&state, state.width);
|
let action = components::action(&state, state.width);
|
||||||
|
|
||||||
let volume = state.sink.volume();
|
let volume = state.sink.volume();
|
||||||
@ -25,7 +50,7 @@ pub async fn draw(state: &ui::State, window: &mut Window, params: ui::Params) ->
|
|||||||
|
|
||||||
let controls = components::controls(state.width);
|
let controls = components::controls(state.width);
|
||||||
|
|
||||||
let menu = match (params.minimalist, &state.track) {
|
let menu = match (params.minimalist, &state.current) {
|
||||||
(true, _) => vec![action, middle],
|
(true, _) => vec![action, middle],
|
||||||
// (false, Some(x)) => vec![x.path.clone(), action, middle, controls],
|
// (false, Some(x)) => vec![x.path.clone(), action, middle, controls],
|
||||||
_ => vec![action, middle, controls],
|
_ => vec![action, middle, controls],
|
||||||
|
|||||||
@ -62,7 +62,7 @@ impl PersistentVolume {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// Saves `volume` to `volume.txt`.
|
/// Saves `volume` to `volume.txt`.
|
||||||
pub async fn save(volume: f32) -> Result<()> {
|
pub async fn save(&self) -> Result<()> {
|
||||||
let config = Self::config().await?;
|
let config = Self::config().await?;
|
||||||
let path = config.join(PathBuf::from("volume.txt"));
|
let path = config.join(PathBuf::from("volume.txt"));
|
||||||
|
|
||||||
@ -72,9 +72,7 @@ impl PersistentVolume {
|
|||||||
clippy::cast_sign_loss,
|
clippy::cast_sign_loss,
|
||||||
clippy::cast_possible_truncation
|
clippy::cast_possible_truncation
|
||||||
)]
|
)]
|
||||||
let percentage = (volume * 100.0).abs().round() as u16;
|
fs::write(path, self.inner.to_string()).await?;
|
||||||
|
|
||||||
fs::write(path, percentage.to_string()).await?;
|
|
||||||
|
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user