mirror of
				https://github.com/talwat/lowfi
				synced 2025-10-30 18:58:45 +00:00 
			
		
		
		
	chore: fix some more lints and make the code more compact
This commit is contained in:
		
							parent
							
								
									8110b8418a
								
							
						
					
					
						commit
						65f7574765
					
				
							
								
								
									
										12
									
								
								src/main.rs
									
									
									
									
									
								
							
							
						
						
									
										12
									
								
								src/main.rs
									
									
									
									
									
								
							| @ -1,12 +1,3 @@ | ||||
| #![warn(
 | ||||
|     clippy::all, | ||||
|     clippy::restriction, | ||||
|     clippy::pedantic, | ||||
|     clippy::nursery, | ||||
|     clippy::cargo | ||||
| )] | ||||
| #![allow(clippy::single_call_fn)] | ||||
| 
 | ||||
| use clap::{Parser, Subcommand}; | ||||
| 
 | ||||
| mod play; | ||||
| @ -18,10 +9,13 @@ mod tracks; | ||||
| #[derive(Parser)] | ||||
| #[command(about)] | ||||
| struct Args { | ||||
|     /// The command that was ran.
 | ||||
|     /// This is [None] if no command was specified.
 | ||||
|     #[command(subcommand)] | ||||
|     command: Option<Commands>, | ||||
| } | ||||
| 
 | ||||
| /// Defines all of the extra commands lowfi can run.
 | ||||
| #[derive(Subcommand)] | ||||
| enum Commands { | ||||
|     /// Scrapes the lofi girl website file server for files.
 | ||||
|  | ||||
| @ -16,10 +16,10 @@ pub async fn play() -> eyre::Result<()> { | ||||
|     let (tx, rx) = mpsc::channel(8); | ||||
| 
 | ||||
|     let player = Arc::new(Player::new().await?); | ||||
|     let audio = task::spawn(Player::play(player.clone(), rx)); | ||||
|     let audio = task::spawn(Player::play(Arc::clone(&player), rx)); | ||||
|     tx.send(Messages::Init).await?; | ||||
| 
 | ||||
|     ui::start(player.clone(), tx.clone()).await?; | ||||
|     ui::start(Arc::clone(&player), tx.clone()).await?; | ||||
| 
 | ||||
|     audio.abort(); | ||||
|     player.sink.stop(); | ||||
|  | ||||
| @ -93,9 +93,8 @@ impl Player { | ||||
|     } | ||||
| 
 | ||||
|     /// This will play the next track, as well as refilling the buffer in the background.
 | ||||
|     pub async fn next(queue: Arc<Player>) -> eyre::Result<DecodedTrack> { | ||||
|         let track = queue.tracks.write().await.pop_front(); | ||||
|         let track = match track { | ||||
|     pub async fn next(queue: Arc<Self>) -> eyre::Result<DecodedTrack> { | ||||
|         let track = match queue.tracks.write().await.pop_front() { | ||||
|             Some(x) => x, | ||||
|             // If the queue is completely empty, then fallback to simply getting a new track.
 | ||||
|             // This is relevant particularly at the first song.
 | ||||
| @ -112,7 +111,7 @@ impl Player { | ||||
|     ///
 | ||||
|     /// `rx` is used to communicate with it, for example when to
 | ||||
|     /// skip tracks or pause.
 | ||||
|     pub async fn play(queue: Arc<Player>, mut rx: Receiver<Messages>) -> eyre::Result<()> { | ||||
|     pub async fn play(queue: Arc<Self>, 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.
 | ||||
| @ -120,12 +119,14 @@ impl Player { | ||||
| 
 | ||||
|         // This refills the queue in the background.
 | ||||
|         task::spawn({ | ||||
|             let queue = queue.clone(); | ||||
|             let queue = Arc::clone(&queue); | ||||
| 
 | ||||
|             async move { | ||||
|                 while let Some(()) = irx.recv().await { | ||||
|                 while irx.recv().await == Some(()) { | ||||
|                     while queue.tracks.read().await.len() < BUFFER_SIZE { | ||||
|                         let track = Track::random(&queue.client).await.unwrap(); | ||||
|                         let Ok(track) = Track::random(&queue.client).await else { | ||||
|                             continue; | ||||
|                         }; | ||||
|                         queue.tracks.write().await.push_back(track); | ||||
|                     } | ||||
|                 } | ||||
| @ -141,7 +142,7 @@ impl Player { | ||||
|                 Some(x) = rx.recv() => x, | ||||
| 
 | ||||
|                 // This future will finish only at the end of the current track.
 | ||||
|                 Ok(()) = task::spawn_blocking(move || clone.sink.sleep_until_end()) => Messages::Next, | ||||
|                 Ok(_) = task::spawn_blocking(move || clone.sink.sleep_until_end()) => Messages::Next, | ||||
|             }; | ||||
| 
 | ||||
|             match msg { | ||||
| @ -155,7 +156,7 @@ impl Player { | ||||
|                     itx.send(()).await?; | ||||
| 
 | ||||
|                     queue.sink.stop(); | ||||
|                     let track = Player::next(queue.clone()).await?; | ||||
|                     let track = Self::next(Arc::clone(&queue)).await?; | ||||
|                     queue.sink.append(track.data); | ||||
|                 } | ||||
|                 Messages::Pause => { | ||||
|  | ||||
| @ -1,3 +1,5 @@ | ||||
| //! The module which manages all user interface, including inputs.
 | ||||
| 
 | ||||
| use std::{io::stderr, sync::Arc, time::Duration}; | ||||
| 
 | ||||
| use crate::tracks::TrackInfo; | ||||
| @ -5,8 +7,9 @@ use crate::tracks::TrackInfo; | ||||
| use super::Player; | ||||
| use crossterm::{ | ||||
|     cursor::{Hide, MoveToColumn, MoveUp, Show}, | ||||
|     event, | ||||
|     style::{Print, Stylize}, | ||||
|     terminal::{Clear, ClearType}, | ||||
|     terminal::{self, Clear, ClearType}, | ||||
| }; | ||||
| use tokio::{ | ||||
|     sync::mpsc::Sender, | ||||
| @ -16,6 +19,7 @@ use tokio::{ | ||||
| 
 | ||||
| use super::Messages; | ||||
| 
 | ||||
| /// Small helper function to format durations.
 | ||||
| fn format_duration(duration: &Duration) -> String { | ||||
|     let seconds = duration.as_secs() % 60; | ||||
|     let minutes = duration.as_secs() / 60; | ||||
| @ -31,43 +35,49 @@ enum ActionBar { | ||||
| } | ||||
| 
 | ||||
| impl ActionBar { | ||||
|     /// Formats the action bar to be displayed.
 | ||||
|     /// The second value is the character length of the result.
 | ||||
|     fn format(&self) -> (String, usize) { | ||||
|         let (word, subject) = match self { | ||||
|             ActionBar::Playing(x) => ("playing", Some(x.name.clone())), | ||||
|             ActionBar::Paused(x) => ("paused", Some(x.name.clone())), | ||||
|             ActionBar::Loading => ("loading", None), | ||||
|             Self::Playing(x) => ("playing", Some(x.name.clone())), | ||||
|             Self::Paused(x) => ("paused", Some(x.name.clone())), | ||||
|             Self::Loading => ("loading", None), | ||||
|         }; | ||||
| 
 | ||||
|         if let Some(subject) = subject { | ||||
|             ( | ||||
|                 format!("{} {}", word, subject.clone().bold()), | ||||
|                 word.len() + 1 + subject.len(), | ||||
|             ) | ||||
|         } else { | ||||
|             (word.to_string(), word.len()) | ||||
|         } | ||||
|         subject.map_or_else( | ||||
|             || (word.to_owned(), word.len()), | ||||
|             |subject| { | ||||
|                 ( | ||||
|                     format!("{} {}", word, subject.clone().bold()), | ||||
|                     word.len() + 1 + subject.len(), | ||||
|                 ) | ||||
|             }, | ||||
|         ) | ||||
|     } | ||||
| } | ||||
| 
 | ||||
| /// The code for the interface itself.
 | ||||
| async fn interface(queue: Arc<Player>) -> eyre::Result<()> { | ||||
|     /// The total width of the UI.
 | ||||
|     const WIDTH: usize = 27; | ||||
| 
 | ||||
|     /// The width of the progress bar, not including the borders (`[` and `]`) or padding.
 | ||||
|     const PROGRESS_WIDTH: usize = WIDTH - 16; | ||||
| 
 | ||||
|     loop { | ||||
|         let (mut main, len) = match queue.current.load().as_ref() { | ||||
|             Some(x) => { | ||||
|                 let name = (*x.clone()).clone(); | ||||
| 
 | ||||
|         let (mut main, len) = queue | ||||
|             .current | ||||
|             .load() | ||||
|             .as_ref() | ||||
|             .map_or(ActionBar::Loading, |x| { | ||||
|                 let name = (*Arc::clone(&x)).clone(); | ||||
|                 if queue.sink.is_paused() { | ||||
|                     ActionBar::Paused(name) | ||||
|                 } else { | ||||
|                     ActionBar::Playing(name) | ||||
|                 } | ||||
|             } | ||||
|             None => ActionBar::Loading, | ||||
|         } | ||||
|         .format(); | ||||
|             }) | ||||
|             .format(); | ||||
| 
 | ||||
|         if len > WIDTH { | ||||
|             main = format!("{}...", &main[..=WIDTH]); | ||||
| @ -122,18 +132,18 @@ async fn interface(queue: Arc<Player>) -> eyre::Result<()> { | ||||
| 
 | ||||
| /// 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::terminal::enable_raw_mode()?; | ||||
|     terminal::enable_raw_mode()?; | ||||
|     crossterm::execute!(stderr(), Hide)?; | ||||
|     //crossterm::execute!(stderr(), EnterAlternateScreen, MoveTo(0, 0))?;
 | ||||
| 
 | ||||
|     task::spawn(interface(queue.clone())); | ||||
|     task::spawn(interface(Arc::clone(&queue))); | ||||
| 
 | ||||
|     loop { | ||||
|         let crossterm::event::Event::Key(event) = crossterm::event::read()? else { | ||||
|         let event::Event::Key(event) = event::read()? else { | ||||
|             continue; | ||||
|         }; | ||||
| 
 | ||||
|         let crossterm::event::KeyCode::Char(code) = event.code else { | ||||
|         let event::KeyCode::Char(code) = event.code else { | ||||
|             continue; | ||||
|         }; | ||||
| 
 | ||||
| @ -155,7 +165,7 @@ pub async fn start(queue: Arc<Player>, sender: Sender<Messages>) -> eyre::Result | ||||
| 
 | ||||
|     //crossterm::execute!(stderr(), LeaveAlternateScreen)?;
 | ||||
|     crossterm::execute!(stderr(), Clear(ClearType::FromCursorDown), Show)?; | ||||
|     crossterm::terminal::disable_raw_mode()?; | ||||
|     terminal::disable_raw_mode()?; | ||||
| 
 | ||||
|     Ok(()) | ||||
| } | ||||
|  | ||||
| @ -10,6 +10,7 @@ use rand::Rng; | ||||
| use reqwest::Client; | ||||
| use rodio::{Decoder, Source}; | ||||
| 
 | ||||
| /// Downloads a raw track, but doesn't decode it.
 | ||||
| async fn download(track: &str, client: &Client) -> eyre::Result<Bytes> { | ||||
|     let url = format!("https://lofigirl.com/wp-content/uploads/{}", track); | ||||
|     let response = client.get(url).send().await?; | ||||
| @ -18,16 +19,19 @@ async fn download(track: &str, client: &Client) -> eyre::Result<Bytes> { | ||||
|     Ok(data) | ||||
| } | ||||
| 
 | ||||
| async fn random() -> eyre::Result<&'static str> { | ||||
|     let tracks = include_str!("../data/tracks.txt"); | ||||
|     let tracks: Vec<&str> = tracks.split_ascii_whitespace().collect(); | ||||
| /// Gets a random track from `tracks.txt` and returns it.
 | ||||
| fn random() -> &'static str { | ||||
|     let tracks: Vec<&str> = include_str!("../data/tracks.txt") | ||||
|         .split_ascii_whitespace() | ||||
|         .collect(); | ||||
| 
 | ||||
|     let random = rand::thread_rng().gen_range(0..tracks.len()); | ||||
|     let track = tracks[random]; | ||||
| 
 | ||||
|     Ok(track) | ||||
|     track | ||||
| } | ||||
| 
 | ||||
| /// Just a shorthand for a decoded [Bytes].
 | ||||
| pub type DecodedData = Decoder<Cursor<Bytes>>; | ||||
| 
 | ||||
| /// The TrackInfo struct, which has the name and duration of a track.
 | ||||
| @ -38,10 +42,16 @@ pub type DecodedData = Decoder<Cursor<Bytes>>; | ||||
| pub struct TrackInfo { | ||||
|     /// This is a formatted name, so it doesn't include the full path.
 | ||||
|     pub name: String, | ||||
| 
 | ||||
|     /// The duration of the track, this is an [Option] because there are
 | ||||
|     /// cases where the duration of a track is unknown.
 | ||||
|     pub duration: Option<Duration>, | ||||
| } | ||||
| 
 | ||||
| impl TrackInfo { | ||||
|     /// Formats a name with [Inflector].
 | ||||
|     /// This will also strip the first few numbers that are
 | ||||
|     /// usually present on most lofi tracks.
 | ||||
|     fn format_name(name: &'static str) -> String { | ||||
|         let mut formatted = name | ||||
|             .split("/") | ||||
| @ -52,6 +62,9 @@ impl TrackInfo { | ||||
|             .to_title_case(); | ||||
| 
 | ||||
|         let mut skip = 0; | ||||
| 
 | ||||
|         // SAFETY: All of the track names originate with the `'static` lifetime,
 | ||||
|         // SAFETY: so basically this has already been checked.
 | ||||
|         for character in unsafe { formatted.as_bytes_mut() } { | ||||
|             if character.is_ascii_digit() { | ||||
|                 skip += 1; | ||||
| @ -63,6 +76,7 @@ impl TrackInfo { | ||||
|         String::from(&formatted[skip..]) | ||||
|     } | ||||
| 
 | ||||
|     /// Creates a new [`TrackInfo`] from a raw name & decoded track data.
 | ||||
|     pub fn new(name: &'static str, decoded: &DecodedData) -> Self { | ||||
|         Self { | ||||
|             duration: decoded.total_duration(), | ||||
| @ -82,6 +96,8 @@ pub struct DecodedTrack { | ||||
| } | ||||
| 
 | ||||
| impl DecodedTrack { | ||||
|     /// Creates a new track.
 | ||||
|     /// This is equivalent to [Track::decode].
 | ||||
|     pub fn new(track: Track) -> eyre::Result<Self> { | ||||
|         let data = Decoder::new(Cursor::new(track.data))?; | ||||
|         let info = TrackInfo::new(track.name, &data); | ||||
| @ -103,7 +119,7 @@ pub struct Track { | ||||
| impl Track { | ||||
|     /// Fetches and downloads a random track from the tracklist.
 | ||||
|     pub async fn random(client: &Client) -> eyre::Result<Self> { | ||||
|         let name = random().await?; | ||||
|         let name = random(); | ||||
|         let data = download(name, client).await?; | ||||
| 
 | ||||
|         Ok(Self { data, name }) | ||||
|  | ||||
		Loading…
	
	
			
			x
			
			
		
	
		Reference in New Issue
	
	Block a user