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
This commit is contained in:
talwat 2024-10-01 19:28:46 +02:00
parent 1278dc534c
commit fd2d37d635
7 changed files with 97 additions and 41 deletions

3
Cargo.lock generated
View File

@ -982,7 +982,7 @@ checksum = "a7a70ba024b9dc04c27ea2f0c0548feb474ec5c54bba33a7f72f873a39d07b24"
[[package]] [[package]]
name = "lowfi" name = "lowfi"
version = "1.1.1" version = "1.2.0"
dependencies = [ dependencies = [
"Inflector", "Inflector",
"arc-swap", "arc-swap",
@ -991,6 +991,7 @@ dependencies = [
"crossterm", "crossterm",
"eyre", "eyre",
"futures", "futures",
"lazy_static",
"rand", "rand",
"reqwest", "reqwest",
"rodio", "rodio",

View File

@ -1,6 +1,6 @@
[package] [package]
name = "lowfi" name = "lowfi"
version = "1.1.1" version = "1.2.0"
edition = "2021" edition = "2021"
description = "An extremely simple lofi player." description = "An extremely simple lofi player."
license = "MIT" license = "MIT"
@ -31,3 +31,4 @@ scraper = "0.20.0"
rodio = { version = "0.19.0", features = ["mp3"], default-features = false } rodio = { version = "0.19.0", features = ["mp3"], default-features = false }
crossterm = "0.28.1" crossterm = "0.28.1"
Inflector = "0.11.4" Inflector = "0.11.4"
lazy_static = "1.5.0"

View File

@ -1,7 +1,8 @@
//! Responsible for the basic initialization & shutdown of the audio server & frontend. //! 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::{ use tokio::{
sync::mpsc::{self}, sync::mpsc::{self},
task::{self}, task::{self},
@ -13,6 +14,12 @@ use crate::player::{ui, Messages};
/// Initializes the audio server, and then safely stops /// Initializes the audio server, and then safely stops
/// it when the frontend quits. /// it when the frontend quits.
pub async fn play() -> eyre::Result<()> { 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 (tx, rx) = mpsc::channel(8);
let player = Arc::new(Player::new().await?); let player = Arc::new(Player::new().await?);

View File

@ -5,12 +5,13 @@
use std::{collections::VecDeque, sync::Arc, time::Duration}; use std::{collections::VecDeque, sync::Arc, time::Duration};
use arc_swap::ArcSwapOption; use arc_swap::ArcSwapOption;
use downloader::Downloader;
use reqwest::Client; use reqwest::Client;
use rodio::{OutputStream, OutputStreamHandle, Sink}; use rodio::{OutputStream, OutputStreamHandle, Sink};
use tokio::{ use tokio::{
select, select,
sync::{ sync::{
mpsc::{self, Receiver, Sender}, mpsc::{Receiver, Sender},
RwLock, RwLock,
}, },
task, task,
@ -18,6 +19,7 @@ use tokio::{
use crate::tracks::{DecodedTrack, Track, TrackInfo}; use crate::tracks::{DecodedTrack, Track, TrackInfo};
pub mod downloader;
pub mod ui; pub mod ui;
/// Handles communication between the frontend & audio player. /// Handles communication between the frontend & audio player.
@ -122,39 +124,22 @@ impl Player {
/// This is the main "audio server". /// 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. /// skip tracks or pause.
pub async fn play( pub async fn play(
queue: Arc<Self>, player: Arc<Self>,
tx: Sender<Messages>, tx: Sender<Messages>,
mut rx: Receiver<Messages>, mut rx: Receiver<Messages>,
) -> eyre::Result<()> { ) -> eyre::Result<()> {
// This is an internal channel which serves pretty much only one purpose, // `itx` is used to notify the `Downloader` when it needs to download new tracks.
// which is to notify the buffer refiller to get back to work. let (downloader, itx) = Downloader::new(player.clone());
// This channel is useful to prevent needing to check with some infinite loop. downloader.start().await;
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);
}
}
}
});
// Start buffering tracks immediately. // Start buffering tracks immediately.
itx.send(()).await?; itx.send(()).await?;
loop { loop {
let clone = Arc::clone(&queue); let clone = Arc::clone(&player);
let msg = select! { let msg = select! {
Some(x) = rx.recv() => x, Some(x) = rx.recv() => x,
@ -166,17 +151,17 @@ impl Player {
Messages::Next | Messages::Init | Messages::TryAgain => { Messages::Next | Messages::Init | Messages::TryAgain => {
// Skip as early as possible so that music doesn't play // Skip as early as possible so that music doesn't play
// while lowfi is "loading". // while lowfi is "loading".
queue.sink.stop(); player.sink.stop();
// Serves as an indicator that the queue is "loading". // Serves as an indicator that the queue is "loading".
// This is also set by Player::next. // 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 { match track {
Ok(track) => { Ok(track) => {
queue.sink.append(track.data); player.sink.append(track.data);
// Notify the background downloader that there's an empty spot // Notify the background downloader that there's an empty spot
// in the buffer. // in the buffer.
@ -192,10 +177,10 @@ impl Player {
}; };
} }
Messages::Pause => { Messages::Pause => {
if queue.sink.is_paused() { if player.sink.is_paused() {
queue.sink.play(); player.sink.play();
} else { } else {
queue.sink.pause(); player.sink.pause();
} }
} }
} }

52
src/player/downloader.rs Normal file
View File

@ -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<Player>,
/// 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<Player>) -> (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);
}
}
});
}
}

View File

@ -6,7 +6,7 @@ use crate::tracks::TrackInfo;
use super::Player; use super::Player;
use crossterm::{ use crossterm::{
cursor::{Hide, MoveToColumn, MoveUp, Show}, cursor::{Hide, MoveToColumn, MoveUp, RestorePosition, Show},
event::{self, KeyCode, KeyModifiers}, event::{self, KeyCode, KeyModifiers},
style::{Print, Stylize}, style::{Print, Stylize},
terminal::{self, Clear, ClearType}, terminal::{self, Clear, ClearType},
@ -19,6 +19,11 @@ use tokio::{
use super::Messages; 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. /// Small helper function to format durations.
fn format_duration(duration: &Duration) -> String { fn format_duration(duration: &Duration) -> String {
let seconds = duration.as_secs() % 60; let seconds = duration.as_secs() % 60;
@ -126,14 +131,19 @@ async fn interface(queue: Arc<Player>) -> eyre::Result<()> {
MoveUp(4) 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. /// Initializes the UI, this will also start taking input from the user.
pub async fn start(queue: Arc<Player>, sender: Sender<Messages>) -> eyre::Result<()> { pub async fn start(queue: Arc<Player>, sender: Sender<Messages>) -> eyre::Result<()> {
crossterm::execute!(
stderr(),
RestorePosition,
Clear(ClearType::FromCursorDown),
Hide
)?;
terminal::enable_raw_mode()?; terminal::enable_raw_mode()?;
crossterm::execute!(stderr(), Hide)?;
//crossterm::execute!(stderr(), EnterAlternateScreen, MoveTo(0, 0))?; //crossterm::execute!(stderr(), EnterAlternateScreen, MoveTo(0, 0))?;
task::spawn(interface(Arc::clone(&queue))); task::spawn(interface(Arc::clone(&queue)));

View File

@ -1,14 +1,14 @@
//! Has all of the functions for the `scrape` command. //! Has all of the functions for the `scrape` command.
use std::sync::LazyLock;
use futures::{stream::FuturesUnordered, StreamExt}; use futures::{stream::FuturesUnordered, StreamExt};
use lazy_static::lazy_static;
use scraper::{Html, Selector}; use scraper::{Html, Selector};
const BASE_URL: &str = "https://lofigirl.com/wp-content/uploads/"; const BASE_URL: &str = "https://lofigirl.com/wp-content/uploads/";
static SELECTOR: LazyLock<Selector> = lazy_static! {
LazyLock::new(|| Selector::parse("html > body > pre > a").unwrap()); static ref SELECTOR: Selector = Selector::parse("html > body > pre > a").unwrap();
}
async fn parse(path: &str) -> eyre::Result<Vec<String>> { async fn parse(path: &str) -> eyre::Result<Vec<String>> {
let response = reqwest::get(format!("{}{}", BASE_URL, path)).await?; let response = reqwest::get(format!("{}{}", BASE_URL, path)).await?;