mirror of
https://github.com/talwat/lowfi
synced 2025-12-15 11:15:26 +00:00
fix: small improvements in state & rng
This commit is contained in:
parent
af8d45905f
commit
905e0ee098
@ -41,6 +41,7 @@ fn silent_get_output_stream() -> crate::Result<rodio::OutputStream> {
|
||||
Ok(stream)
|
||||
}
|
||||
|
||||
/// Creates an audio stream, doing so silently on Linux.
|
||||
pub fn stream() -> crate::Result<rodio::OutputStream> {
|
||||
#[cfg(target_os = "linux")]
|
||||
let mut stream = silent_get_output_stream()?;
|
||||
|
||||
@ -46,6 +46,9 @@ pub struct Downloader {
|
||||
|
||||
/// The [`reqwest`] client to use for downloads.
|
||||
client: Client,
|
||||
|
||||
/// The RNG generator to use.
|
||||
rng: fastrand::Rng,
|
||||
}
|
||||
|
||||
impl Downloader {
|
||||
@ -73,6 +76,7 @@ impl Downloader {
|
||||
tx,
|
||||
tracks,
|
||||
client,
|
||||
rng: fastrand::Rng::new(),
|
||||
};
|
||||
|
||||
Ok(Handle {
|
||||
@ -84,15 +88,17 @@ impl Downloader {
|
||||
/// Actually runs the downloader, consuming it and beginning
|
||||
/// the cycle of downloading tracks and reporting to the
|
||||
/// rest of the program.
|
||||
async fn run(self) -> crate::Result<()> {
|
||||
async fn run(mut self) -> crate::Result<()> {
|
||||
const ERROR_TIMEOUT: Duration = Duration::from_secs(1);
|
||||
|
||||
loop {
|
||||
let result = self.tracks.random(&self.client, &PROGRESS).await;
|
||||
let result = self
|
||||
.tracks
|
||||
.random(&self.client, &PROGRESS, &mut self.rng)
|
||||
.await;
|
||||
match result {
|
||||
Ok(track) => {
|
||||
self.queue.send(track).await?;
|
||||
|
||||
if LOADING.load(atomic::Ordering::Relaxed) {
|
||||
self.tx.send(crate::Message::Loaded).await?;
|
||||
LOADING.store(false, atomic::Ordering::Relaxed);
|
||||
|
||||
15
src/error.rs
15
src/error.rs
@ -14,63 +14,48 @@ pub type Result<T> = std::result::Result<T, Error>;
|
||||
/// Central application error.
|
||||
#[derive(Debug, thiserror::Error)]
|
||||
pub enum Error {
|
||||
/// Errors while loading or saving the persistent volume settings.
|
||||
#[error("unable to load/save the persistent volume")]
|
||||
PersistentVolume(#[from] volume::Error),
|
||||
|
||||
/// Errors while loading or saving bookmarks.
|
||||
#[error("unable to load/save bookmarks")]
|
||||
Bookmarks(#[from] bookmark::Error),
|
||||
|
||||
/// Network request failures from `reqwest`.
|
||||
#[error("unable to fetch data")]
|
||||
Request(#[from] reqwest::Error),
|
||||
|
||||
/// Failure converting to/from a C string (FFI helpers).
|
||||
#[error("C string null error")]
|
||||
FfiNull(#[from] std::ffi::NulError),
|
||||
|
||||
/// Errors coming from the audio backend / stream handling.
|
||||
#[error("audio playing error")]
|
||||
Rodio(#[from] rodio::StreamError),
|
||||
|
||||
/// Failure to send an internal `Message` over the mpsc channel.
|
||||
#[error("couldn't send internal message")]
|
||||
Send(#[from] mpsc::error::SendError<crate::Message>),
|
||||
|
||||
/// Failure to enqueue a track into the queue channel.
|
||||
#[error("couldn't add track to the queue")]
|
||||
Queue(#[from] mpsc::error::SendError<tracks::Queued>),
|
||||
|
||||
/// Failure to broadcast UI updates.
|
||||
#[error("couldn't update UI state")]
|
||||
Broadcast(#[from] broadcast::error::SendError<ui::Update>),
|
||||
|
||||
/// Generic IO error.
|
||||
#[error("io error")]
|
||||
Io(#[from] std::io::Error),
|
||||
|
||||
/// Data directory was not found or could not be determined.
|
||||
#[error("directory not found")]
|
||||
Directory,
|
||||
|
||||
/// Downloader failed to provide the requested track.
|
||||
#[error("couldn't fetch track from downloader")]
|
||||
Download,
|
||||
|
||||
/// Integer parsing errors.
|
||||
#[error("couldn't parse integer")]
|
||||
Parse(#[from] std::num::ParseIntError),
|
||||
|
||||
/// Track subsystem error.
|
||||
#[error("track failure")]
|
||||
Track(#[from] tracks::Error),
|
||||
|
||||
/// UI subsystem error.
|
||||
#[error("ui failure")]
|
||||
UI(#[from] ui::Error),
|
||||
|
||||
/// Error returned when a spawned task join failed.
|
||||
#[error("join error")]
|
||||
JoinError(#[from] tokio::task::JoinError),
|
||||
}
|
||||
|
||||
24
src/main.rs
24
src/main.rs
@ -1,28 +1,26 @@
|
||||
//! An extremely simple lofi player.
|
||||
pub mod error;
|
||||
use crate::player::Player;
|
||||
use clap::{Parser, Subcommand};
|
||||
use futures_util::TryFutureExt;
|
||||
use std::path::PathBuf;
|
||||
|
||||
use clap::{Parser, Subcommand};
|
||||
mod tests;
|
||||
pub use error::{Error, Result};
|
||||
pub mod message;
|
||||
pub mod ui;
|
||||
use futures_util::TryFutureExt;
|
||||
pub use message::Message;
|
||||
|
||||
use crate::player::Player;
|
||||
pub mod audio;
|
||||
pub mod bookmark;
|
||||
pub mod download;
|
||||
pub mod error;
|
||||
pub mod message;
|
||||
pub mod player;
|
||||
#[cfg(feature = "scrape")]
|
||||
mod scrapers;
|
||||
mod tests;
|
||||
pub mod tracks;
|
||||
pub mod ui;
|
||||
pub mod volume;
|
||||
|
||||
#[cfg(feature = "scrape")]
|
||||
mod scrapers;
|
||||
|
||||
#[cfg(feature = "scrape")]
|
||||
use crate::scrapers::Source;
|
||||
pub use error::{Error, Result};
|
||||
pub use message::Message;
|
||||
|
||||
/// An extremely simple lofi player.
|
||||
#[derive(Parser, Clone)]
|
||||
|
||||
@ -226,18 +226,7 @@ impl Player {
|
||||
}
|
||||
|
||||
#[cfg(feature = "mpris")]
|
||||
match message {
|
||||
Message::ChangeVolume(_) | Message::SetVolume(_) => {
|
||||
self.ui.mpris.update_volume().await?
|
||||
}
|
||||
Message::Play | Message::Pause | Message::PlayPause => {
|
||||
self.ui.mpris.update_playback().await?
|
||||
}
|
||||
Message::Init | Message::Loaded | Message::Next => {
|
||||
self.ui.mpris.update_metadata().await?
|
||||
}
|
||||
_ => (),
|
||||
}
|
||||
self.ui.mpris.handle(&message).await?;
|
||||
}
|
||||
|
||||
self.close().await?;
|
||||
|
||||
@ -123,7 +123,7 @@ mod list {
|
||||
let text = "http://x/\npath!Display";
|
||||
let list = List::new("t", text, None);
|
||||
|
||||
let (p, d) = list.random_path();
|
||||
let (p, d) = list.random_path(&mut fastrand::Rng::new());
|
||||
assert_eq!(p, "path");
|
||||
assert_eq!(d, Some("Display".into()));
|
||||
}
|
||||
@ -133,7 +133,7 @@ mod list {
|
||||
let text = "http://x/\ntrackA";
|
||||
let list = List::new("t", text, None);
|
||||
|
||||
let (p, d) = list.random_path();
|
||||
let (p, d) = list.random_path(&mut fastrand::Rng::new());
|
||||
assert_eq!(p, "trackA");
|
||||
assert!(d.is_none());
|
||||
}
|
||||
@ -152,7 +152,7 @@ mod list {
|
||||
fn custom_display_with_exclamation() {
|
||||
let text = "http://base/\nfile.mp3!My Custom Name";
|
||||
let list = List::new("t", text, None);
|
||||
let (path, display) = list.random_path();
|
||||
let (path, display) = list.random_path(&mut fastrand::Rng::new());
|
||||
assert_eq!(path, "file.mp3");
|
||||
assert_eq!(display, Some("My Custom Name".into()));
|
||||
}
|
||||
@ -161,7 +161,7 @@ mod list {
|
||||
fn single_track() {
|
||||
let text = "base\nonly_track.mp3";
|
||||
let list = List::new("name", text, None);
|
||||
let (path, _) = list.random_path();
|
||||
let (path, _) = list.random_path(&mut fastrand::Rng::new());
|
||||
assert_eq!(path, "only_track.mp3");
|
||||
}
|
||||
|
||||
@ -171,7 +171,10 @@ mod list {
|
||||
let list = List::new("name", text, None);
|
||||
|
||||
let client = Client::new();
|
||||
let track = list.random(&client, &PROGRESS).await.unwrap();
|
||||
let track = list
|
||||
.random(&client, &PROGRESS, &mut fastrand::Rng::new())
|
||||
.await
|
||||
.unwrap();
|
||||
assert_eq!(track.display, "Apple Juice");
|
||||
assert_eq!(track.path, "https://stream.chillhop.com/mp3/9476");
|
||||
assert_eq!(track.data.len(), 3150424);
|
||||
|
||||
@ -49,13 +49,13 @@ impl List {
|
||||
///
|
||||
/// The second value in the tuple specifies whether the
|
||||
/// track has a custom display name.
|
||||
pub fn random_path(&self) -> (String, Option<String>) {
|
||||
pub fn random_path(&self, rng: &mut fastrand::Rng) -> (String, Option<String>) {
|
||||
// We're getting from 1 here, since the base is at `self.lines[0]`.
|
||||
//
|
||||
// We're also not pre-trimming `self.lines` into `base` & `tracks` due to
|
||||
// how rust vectors work, since it is slower to drain only a single element from
|
||||
// the start, so it's faster to just keep it in & work around it.
|
||||
let random = fastrand::usize(1..self.lines.len());
|
||||
let random = rng.usize(1..self.lines.len());
|
||||
let line = self.lines[random].clone();
|
||||
|
||||
if let Some((first, second)) = line.split_once('!') {
|
||||
@ -128,10 +128,15 @@ impl 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 occurred,
|
||||
/// 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> {
|
||||
let (path, display) = self.random_path();
|
||||
pub async fn random(
|
||||
&self,
|
||||
client: &Client,
|
||||
progress: &AtomicU8,
|
||||
rng: &mut fastrand::Rng,
|
||||
) -> tracks::Result<Queued> {
|
||||
let (path, display) = self.random_path(rng);
|
||||
let (data, path) = self.download(&path, client, Some(progress)).await?;
|
||||
|
||||
Queued::new(path, data, display)
|
||||
|
||||
@ -5,11 +5,23 @@ use crate::{
|
||||
Args,
|
||||
};
|
||||
|
||||
/// UI-specific parameters and options.
|
||||
#[derive(Copy, Clone, Debug, Default)]
|
||||
pub struct Params {
|
||||
/// Whether to include borders.
|
||||
pub borderless: bool,
|
||||
|
||||
/// Whether to include the bottom control bar.
|
||||
pub minimalist: bool,
|
||||
|
||||
/// Whether the visual part of the UI should be enabled.
|
||||
/// This only applies if the MPRIS feature is enabled.
|
||||
pub enabled: bool,
|
||||
|
||||
/// The total delta between frames, which takes into account
|
||||
/// the time it takes to actually render each frame.
|
||||
///
|
||||
/// Derived from the FPS.
|
||||
pub delta: Duration,
|
||||
}
|
||||
|
||||
|
||||
@ -268,8 +268,18 @@ pub struct Server {
|
||||
}
|
||||
|
||||
impl Server {
|
||||
/// Handles a player message to update the state of the MPRIS player.
|
||||
pub async fn handle(&mut self, message: &crate::Message) -> ui::Result<()> {
|
||||
match message {
|
||||
Message::ChangeVolume(_) | Message::SetVolume(_) => self.update_volume().await,
|
||||
Message::Play | Message::Pause | Message::PlayPause => self.update_playback().await,
|
||||
Message::Init | Message::Loaded | Message::Next => self.update_metadata().await,
|
||||
_ => Ok(()),
|
||||
}
|
||||
}
|
||||
|
||||
/// Shorthand to emit a `PropertiesChanged` signal, like when pausing/unpausing.
|
||||
pub async fn changed(
|
||||
async fn changed(
|
||||
&mut self,
|
||||
properties: impl IntoIterator<Item = mpris_server::Property> + Send + Sync,
|
||||
) -> ui::Result<()> {
|
||||
@ -284,7 +294,7 @@ impl Server {
|
||||
}
|
||||
|
||||
/// Updates the volume with the latest information.
|
||||
pub async fn update_volume(&mut self) -> ui::Result<()> {
|
||||
async fn update_volume(&mut self) -> ui::Result<()> {
|
||||
self.changed(vec![Property::Volume(self.player().sink.volume().into())])
|
||||
.await?;
|
||||
|
||||
@ -292,7 +302,7 @@ impl Server {
|
||||
}
|
||||
|
||||
/// Updates the playback with the latest information.
|
||||
pub async fn update_playback(&mut self) -> ui::Result<()> {
|
||||
async fn update_playback(&mut self) -> ui::Result<()> {
|
||||
let status = self.player().playback_status().await?;
|
||||
self.changed(vec![Property::PlaybackStatus(status)]).await?;
|
||||
|
||||
@ -300,7 +310,7 @@ impl Server {
|
||||
}
|
||||
|
||||
/// Updates the current track data with the current information.
|
||||
pub async fn update_metadata(&mut self) -> ui::Result<()> {
|
||||
async fn update_metadata(&mut self) -> ui::Result<()> {
|
||||
let metadata = self.player().metadata().await?;
|
||||
self.changed(vec![Property::Metadata(metadata)]).await?;
|
||||
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user