mirror of
				https://github.com/talwat/lowfi
				synced 2025-10-30 18:58:45 +00:00 
			
		
		
		
	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:
		
							parent
							
								
									1278dc534c
								
							
						
					
					
						commit
						fd2d37d635
					
				
							
								
								
									
										3
									
								
								Cargo.lock
									
									
									
										generated
									
									
									
								
							
							
						
						
									
										3
									
								
								Cargo.lock
									
									
									
										generated
									
									
									
								
							| @ -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", | ||||
|  | ||||
| @ -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" | ||||
|  | ||||
| @ -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?); | ||||
|  | ||||
| @ -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<Self>, | ||||
|         player: Arc<Self>, | ||||
|         tx: Sender<Messages>, | ||||
|         mut rx: Receiver<Messages>, | ||||
|     ) -> 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(); | ||||
|                     } | ||||
|                 } | ||||
|             } | ||||
|  | ||||
							
								
								
									
										52
									
								
								src/player/downloader.rs
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										52
									
								
								src/player/downloader.rs
									
									
									
									
									
										Normal 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); | ||||
|                 } | ||||
|             } | ||||
|         }); | ||||
|     } | ||||
| } | ||||
| @ -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<Player>) -> 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<Player>, sender: Sender<Messages>) -> 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))); | ||||
|  | ||||
| @ -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<Selector> = | ||||
|     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<Vec<String>> { | ||||
|     let response = reqwest::get(format!("{}{}", BASE_URL, path)).await?; | ||||
|  | ||||
		Loading…
	
	
			
			x
			
			
		
	
		Reference in New Issue
	
	Block a user