From fd2d37d6357e9ab113f5970c90cc6aec19057fdf Mon Sep 17 00:00:00 2001 From: talwat <83217276+talwat@users.noreply.github.com> Date: Tue, 1 Oct 2024 19:28:46 +0200 Subject: [PATCH] fix: fix issue #1 as well as several others fix: split downloader into a seperate struct for readability fix: use `lazy_static` to reduce MSRV fix: reduce frame delta --- Cargo.lock | 3 ++- Cargo.toml | 3 ++- src/play.rs | 9 ++++++- src/player.rs | 47 +++++++++++++----------------------- src/player/downloader.rs | 52 ++++++++++++++++++++++++++++++++++++++++ src/player/ui.rs | 16 ++++++++++--- src/scrape.rs | 8 +++---- 7 files changed, 97 insertions(+), 41 deletions(-) create mode 100644 src/player/downloader.rs diff --git a/Cargo.lock b/Cargo.lock index a76f412..ddf4e04 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -982,7 +982,7 @@ checksum = "a7a70ba024b9dc04c27ea2f0c0548feb474ec5c54bba33a7f72f873a39d07b24" [[package]] name = "lowfi" -version = "1.1.1" +version = "1.2.0" dependencies = [ "Inflector", "arc-swap", @@ -991,6 +991,7 @@ dependencies = [ "crossterm", "eyre", "futures", + "lazy_static", "rand", "reqwest", "rodio", diff --git a/Cargo.toml b/Cargo.toml index 61f4948..c2efda7 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "lowfi" -version = "1.1.1" +version = "1.2.0" edition = "2021" description = "An extremely simple lofi player." license = "MIT" @@ -31,3 +31,4 @@ scraper = "0.20.0" rodio = { version = "0.19.0", features = ["mp3"], default-features = false } crossterm = "0.28.1" Inflector = "0.11.4" +lazy_static = "1.5.0" diff --git a/src/play.rs b/src/play.rs index bf529e4..2e187e1 100644 --- a/src/play.rs +++ b/src/play.rs @@ -1,7 +1,8 @@ //! Responsible for the basic initialization & shutdown of the audio server & frontend. -use std::sync::Arc; +use std::{io::stderr, sync::Arc}; +use crossterm::cursor::SavePosition; use tokio::{ sync::mpsc::{self}, task::{self}, @@ -13,6 +14,12 @@ use crate::player::{ui, Messages}; /// Initializes the audio server, and then safely stops /// it when the frontend quits. pub async fn play() -> eyre::Result<()> { + // Save the position. This is important since later on we can revert to this position + // and clear any potential error messages that may have showed up. + // TODO: Figure how to set some sort of flag to hide error messages within rodio, + // TODO: Instead of just ignoring & clearing them after. + crossterm::execute!(stderr(), SavePosition)?; + let (tx, rx) = mpsc::channel(8); let player = Arc::new(Player::new().await?); diff --git a/src/player.rs b/src/player.rs index c5b5015..2c79921 100644 --- a/src/player.rs +++ b/src/player.rs @@ -5,12 +5,13 @@ use std::{collections::VecDeque, sync::Arc, time::Duration}; use arc_swap::ArcSwapOption; +use downloader::Downloader; use reqwest::Client; use rodio::{OutputStream, OutputStreamHandle, Sink}; use tokio::{ select, sync::{ - mpsc::{self, Receiver, Sender}, + mpsc::{Receiver, Sender}, RwLock, }, task, @@ -18,6 +19,7 @@ use tokio::{ use crate::tracks::{DecodedTrack, Track, TrackInfo}; +pub mod downloader; pub mod ui; /// Handles communication between the frontend & audio player. @@ -122,39 +124,22 @@ impl Player { /// This is the main "audio server". /// - /// `rx` is used to communicate with it, for example when to + /// `rx` & `ts` are used to communicate with it, for example when to /// skip tracks or pause. pub async fn play( - queue: Arc, + player: Arc, tx: Sender, mut rx: Receiver, ) -> eyre::Result<()> { - // This is an internal channel which serves pretty much only one purpose, - // which is to notify the buffer refiller to get back to work. - // This channel is useful to prevent needing to check with some infinite loop. - let (itx, mut irx) = mpsc::channel(8); - - // This refills the queue in the background. - task::spawn({ - let queue = Arc::clone(&queue); - - async move { - while irx.recv().await == Some(()) { - while queue.tracks.read().await.len() < BUFFER_SIZE { - let Ok(track) = Track::random(&queue.client).await else { - continue; - }; - queue.tracks.write().await.push_back(track); - } - } - } - }); + // `itx` is used to notify the `Downloader` when it needs to download new tracks. + let (downloader, itx) = Downloader::new(player.clone()); + downloader.start().await; // Start buffering tracks immediately. itx.send(()).await?; loop { - let clone = Arc::clone(&queue); + let clone = Arc::clone(&player); let msg = select! { Some(x) = rx.recv() => x, @@ -166,17 +151,17 @@ impl Player { Messages::Next | Messages::Init | Messages::TryAgain => { // Skip as early as possible so that music doesn't play // while lowfi is "loading". - queue.sink.stop(); + player.sink.stop(); // Serves as an indicator that the queue is "loading". // This is also set by Player::next. - queue.current.store(None); + player.current.store(None); - let track = Self::next(Arc::clone(&queue)).await; + let track = Self::next(Arc::clone(&player)).await; match track { Ok(track) => { - queue.sink.append(track.data); + player.sink.append(track.data); // Notify the background downloader that there's an empty spot // in the buffer. @@ -192,10 +177,10 @@ impl Player { }; } Messages::Pause => { - if queue.sink.is_paused() { - queue.sink.play(); + if player.sink.is_paused() { + player.sink.play(); } else { - queue.sink.pause(); + player.sink.pause(); } } } diff --git a/src/player/downloader.rs b/src/player/downloader.rs new file mode 100644 index 0000000..ed38ce4 --- /dev/null +++ b/src/player/downloader.rs @@ -0,0 +1,52 @@ +//! Contains the [`Downloader`] struct. + +use std::sync::Arc; + +use tokio::{ + sync::mpsc::{self, Receiver, Sender}, + task, +}; + +use crate::tracks::Track; + +use super::{Player, BUFFER_SIZE}; + +/// This struct is responsible for downloading tracks in the background. +/// +/// This is not used for the first track or a track when the buffer is currently empty. +pub struct Downloader { + /// The player for the downloader to download to & with. + player: Arc, + + /// The internal reciever, which is used by the downloader to know + /// when to begin downloading more tracks. + rx: Receiver<()>, +} + +impl Downloader { + /// Initializes the [Downloader]. + /// + /// This also sends a [`Sender`] which can be used to notify + /// when the downloader needs to begin downloading more tracks. + pub fn new(player: Arc) -> (Self, Sender<()>) { + let (tx, rx) = mpsc::channel(8); + (Self { player, rx }, tx) + } + + /// Actually starts & consumes the [Downloader]. + pub async fn start(mut self) { + task::spawn(async move { + // Loop through each update notification. + while self.rx.recv().await == Some(()) { + // For each update notification, we'll push tracks until the buffer is completely full. + while self.player.tracks.read().await.len() < BUFFER_SIZE { + let Ok(track) = Track::random(&self.player.client).await else { + continue; + }; + + self.player.tracks.write().await.push_back(track); + } + } + }); + } +} diff --git a/src/player/ui.rs b/src/player/ui.rs index d5e9ba9..638773c 100644 --- a/src/player/ui.rs +++ b/src/player/ui.rs @@ -6,7 +6,7 @@ use crate::tracks::TrackInfo; use super::Player; use crossterm::{ - cursor::{Hide, MoveToColumn, MoveUp, Show}, + cursor::{Hide, MoveToColumn, MoveUp, RestorePosition, Show}, event::{self, KeyCode, KeyModifiers}, style::{Print, Stylize}, terminal::{self, Clear, ClearType}, @@ -19,6 +19,11 @@ use tokio::{ use super::Messages; +/// How long to wait in between frames. +/// This is fairly arbitrary, but an ideal value should be enough to feel +/// snappy but not require too many resources. +const FRAME_DELTA: f32 = 5.0 / 60.0; + /// Small helper function to format durations. fn format_duration(duration: &Duration) -> String { let seconds = duration.as_secs() % 60; @@ -126,14 +131,19 @@ async fn interface(queue: Arc) -> eyre::Result<()> { MoveUp(4) )?; - sleep(Duration::from_secs_f32(10.0 / 60.0)).await; + sleep(Duration::from_secs_f32(FRAME_DELTA)).await; } } /// Initializes the UI, this will also start taking input from the user. pub async fn start(queue: Arc, sender: Sender) -> eyre::Result<()> { + crossterm::execute!( + stderr(), + RestorePosition, + Clear(ClearType::FromCursorDown), + Hide + )?; terminal::enable_raw_mode()?; - crossterm::execute!(stderr(), Hide)?; //crossterm::execute!(stderr(), EnterAlternateScreen, MoveTo(0, 0))?; task::spawn(interface(Arc::clone(&queue))); diff --git a/src/scrape.rs b/src/scrape.rs index f38b163..deec452 100644 --- a/src/scrape.rs +++ b/src/scrape.rs @@ -1,14 +1,14 @@ //! Has all of the functions for the `scrape` command. -use std::sync::LazyLock; - use futures::{stream::FuturesUnordered, StreamExt}; +use lazy_static::lazy_static; use scraper::{Html, Selector}; const BASE_URL: &str = "https://lofigirl.com/wp-content/uploads/"; -static SELECTOR: LazyLock = - LazyLock::new(|| Selector::parse("html > body > pre > a").unwrap()); +lazy_static! { + static ref SELECTOR: Selector = Selector::parse("html > body > pre > a").unwrap(); +} async fn parse(path: &str) -> eyre::Result> { let response = reqwest::get(format!("{}{}", BASE_URL, path)).await?;