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}; | use clap::{Parser, Subcommand}; | ||||||
| 
 | 
 | ||||||
| mod play; | mod play; | ||||||
| @ -18,10 +9,13 @@ mod tracks; | |||||||
| #[derive(Parser)] | #[derive(Parser)] | ||||||
| #[command(about)] | #[command(about)] | ||||||
| struct Args { | struct Args { | ||||||
|  |     /// The command that was ran.
 | ||||||
|  |     /// This is [None] if no command was specified.
 | ||||||
|     #[command(subcommand)] |     #[command(subcommand)] | ||||||
|     command: Option<Commands>, |     command: Option<Commands>, | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
|  | /// Defines all of the extra commands lowfi can run.
 | ||||||
| #[derive(Subcommand)] | #[derive(Subcommand)] | ||||||
| enum Commands { | enum Commands { | ||||||
|     /// Scrapes the lofi girl website file server for files.
 |     /// 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 (tx, rx) = mpsc::channel(8); | ||||||
| 
 | 
 | ||||||
|     let player = Arc::new(Player::new().await?); |     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?; |     tx.send(Messages::Init).await?; | ||||||
| 
 | 
 | ||||||
|     ui::start(player.clone(), tx.clone()).await?; |     ui::start(Arc::clone(&player), tx.clone()).await?; | ||||||
| 
 | 
 | ||||||
|     audio.abort(); |     audio.abort(); | ||||||
|     player.sink.stop(); |     player.sink.stop(); | ||||||
|  | |||||||
| @ -93,9 +93,8 @@ impl Player { | |||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|     /// This will play the next track, as well as refilling the buffer in the background.
 |     /// 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> { |     pub async fn next(queue: Arc<Self>) -> eyre::Result<DecodedTrack> { | ||||||
|         let track = queue.tracks.write().await.pop_front(); |         let track = match queue.tracks.write().await.pop_front() { | ||||||
|         let track = match track { |  | ||||||
|             Some(x) => x, |             Some(x) => x, | ||||||
|             // If the queue is completely empty, then fallback to simply getting a new track.
 |             // If the queue is completely empty, then fallback to simply getting a new track.
 | ||||||
|             // This is relevant particularly at the first song.
 |             // 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
 |     /// `rx` is used to communicate with it, for example when to
 | ||||||
|     /// skip tracks or pause.
 |     /// 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,
 |         // This is an internal channel which serves pretty much only one purpose,
 | ||||||
|         // which is to notify the buffer refiller to get back to work.
 |         // 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.
 |         // 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.
 |         // This refills the queue in the background.
 | ||||||
|         task::spawn({ |         task::spawn({ | ||||||
|             let queue = queue.clone(); |             let queue = Arc::clone(&queue); | ||||||
| 
 | 
 | ||||||
|             async move { |             async move { | ||||||
|                 while let Some(()) = irx.recv().await { |                 while irx.recv().await == Some(()) { | ||||||
|                     while queue.tracks.read().await.len() < BUFFER_SIZE { |                     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); |                         queue.tracks.write().await.push_back(track); | ||||||
|                     } |                     } | ||||||
|                 } |                 } | ||||||
| @ -141,7 +142,7 @@ impl Player { | |||||||
|                 Some(x) = rx.recv() => x, |                 Some(x) = rx.recv() => x, | ||||||
| 
 | 
 | ||||||
|                 // This future will finish only at the end of the current track.
 |                 // 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 { |             match msg { | ||||||
| @ -155,7 +156,7 @@ impl Player { | |||||||
|                     itx.send(()).await?; |                     itx.send(()).await?; | ||||||
| 
 | 
 | ||||||
|                     queue.sink.stop(); |                     queue.sink.stop(); | ||||||
|                     let track = Player::next(queue.clone()).await?; |                     let track = Self::next(Arc::clone(&queue)).await?; | ||||||
|                     queue.sink.append(track.data); |                     queue.sink.append(track.data); | ||||||
|                 } |                 } | ||||||
|                 Messages::Pause => { |                 Messages::Pause => { | ||||||
|  | |||||||
| @ -1,3 +1,5 @@ | |||||||
|  | //! The module which manages all user interface, including inputs.
 | ||||||
|  | 
 | ||||||
| use std::{io::stderr, sync::Arc, time::Duration}; | use std::{io::stderr, sync::Arc, time::Duration}; | ||||||
| 
 | 
 | ||||||
| use crate::tracks::TrackInfo; | use crate::tracks::TrackInfo; | ||||||
| @ -5,8 +7,9 @@ use crate::tracks::TrackInfo; | |||||||
| use super::Player; | use super::Player; | ||||||
| use crossterm::{ | use crossterm::{ | ||||||
|     cursor::{Hide, MoveToColumn, MoveUp, Show}, |     cursor::{Hide, MoveToColumn, MoveUp, Show}, | ||||||
|  |     event, | ||||||
|     style::{Print, Stylize}, |     style::{Print, Stylize}, | ||||||
|     terminal::{Clear, ClearType}, |     terminal::{self, Clear, ClearType}, | ||||||
| }; | }; | ||||||
| use tokio::{ | use tokio::{ | ||||||
|     sync::mpsc::Sender, |     sync::mpsc::Sender, | ||||||
| @ -16,6 +19,7 @@ use tokio::{ | |||||||
| 
 | 
 | ||||||
| use super::Messages; | use super::Messages; | ||||||
| 
 | 
 | ||||||
|  | /// 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; | ||||||
|     let minutes = duration.as_secs() / 60; |     let minutes = duration.as_secs() / 60; | ||||||
| @ -31,43 +35,49 @@ enum ActionBar { | |||||||
| } | } | ||||||
| 
 | 
 | ||||||
| impl 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) { |     fn format(&self) -> (String, usize) { | ||||||
|         let (word, subject) = match self { |         let (word, subject) = match self { | ||||||
|             ActionBar::Playing(x) => ("playing", Some(x.name.clone())), |             Self::Playing(x) => ("playing", Some(x.name.clone())), | ||||||
|             ActionBar::Paused(x) => ("paused", Some(x.name.clone())), |             Self::Paused(x) => ("paused", Some(x.name.clone())), | ||||||
|             ActionBar::Loading => ("loading", None), |             Self::Loading => ("loading", None), | ||||||
|         }; |         }; | ||||||
| 
 | 
 | ||||||
|         if let Some(subject) = subject { |         subject.map_or_else( | ||||||
|             ( |             || (word.to_owned(), word.len()), | ||||||
|                 format!("{} {}", word, subject.clone().bold()), |             |subject| { | ||||||
|                 word.len() + 1 + subject.len(), |                 ( | ||||||
|             ) |                     format!("{} {}", word, subject.clone().bold()), | ||||||
|         } else { |                     word.len() + 1 + subject.len(), | ||||||
|             (word.to_string(), word.len()) |                 ) | ||||||
|         } |             }, | ||||||
|  |         ) | ||||||
|     } |     } | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
| /// The code for the interface itself.
 | /// The code for the interface itself.
 | ||||||
| async fn interface(queue: Arc<Player>) -> eyre::Result<()> { | async fn interface(queue: Arc<Player>) -> eyre::Result<()> { | ||||||
|  |     /// The total width of the UI.
 | ||||||
|     const WIDTH: usize = 27; |     const WIDTH: usize = 27; | ||||||
|  | 
 | ||||||
|  |     /// The width of the progress bar, not including the borders (`[` and `]`) or padding.
 | ||||||
|     const PROGRESS_WIDTH: usize = WIDTH - 16; |     const PROGRESS_WIDTH: usize = WIDTH - 16; | ||||||
| 
 | 
 | ||||||
|     loop { |     loop { | ||||||
|         let (mut main, len) = match queue.current.load().as_ref() { |         let (mut main, len) = queue | ||||||
|             Some(x) => { |             .current | ||||||
|                 let name = (*x.clone()).clone(); |             .load() | ||||||
| 
 |             .as_ref() | ||||||
|  |             .map_or(ActionBar::Loading, |x| { | ||||||
|  |                 let name = (*Arc::clone(&x)).clone(); | ||||||
|                 if queue.sink.is_paused() { |                 if queue.sink.is_paused() { | ||||||
|                     ActionBar::Paused(name) |                     ActionBar::Paused(name) | ||||||
|                 } else { |                 } else { | ||||||
|                     ActionBar::Playing(name) |                     ActionBar::Playing(name) | ||||||
|                 } |                 } | ||||||
|             } |             }) | ||||||
|             None => ActionBar::Loading, |             .format(); | ||||||
|         } |  | ||||||
|         .format(); |  | ||||||
| 
 | 
 | ||||||
|         if len > WIDTH { |         if len > WIDTH { | ||||||
|             main = format!("{}...", &main[..=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.
 | /// 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::terminal::enable_raw_mode()?; |     terminal::enable_raw_mode()?; | ||||||
|     crossterm::execute!(stderr(), Hide)?; |     crossterm::execute!(stderr(), Hide)?; | ||||||
|     //crossterm::execute!(stderr(), EnterAlternateScreen, MoveTo(0, 0))?;
 |     //crossterm::execute!(stderr(), EnterAlternateScreen, MoveTo(0, 0))?;
 | ||||||
| 
 | 
 | ||||||
|     task::spawn(interface(queue.clone())); |     task::spawn(interface(Arc::clone(&queue))); | ||||||
| 
 | 
 | ||||||
|     loop { |     loop { | ||||||
|         let crossterm::event::Event::Key(event) = crossterm::event::read()? else { |         let event::Event::Key(event) = event::read()? else { | ||||||
|             continue; |             continue; | ||||||
|         }; |         }; | ||||||
| 
 | 
 | ||||||
|         let crossterm::event::KeyCode::Char(code) = event.code else { |         let event::KeyCode::Char(code) = event.code else { | ||||||
|             continue; |             continue; | ||||||
|         }; |         }; | ||||||
| 
 | 
 | ||||||
| @ -155,7 +165,7 @@ pub async fn start(queue: Arc<Player>, sender: Sender<Messages>) -> eyre::Result | |||||||
| 
 | 
 | ||||||
|     //crossterm::execute!(stderr(), LeaveAlternateScreen)?;
 |     //crossterm::execute!(stderr(), LeaveAlternateScreen)?;
 | ||||||
|     crossterm::execute!(stderr(), Clear(ClearType::FromCursorDown), Show)?; |     crossterm::execute!(stderr(), Clear(ClearType::FromCursorDown), Show)?; | ||||||
|     crossterm::terminal::disable_raw_mode()?; |     terminal::disable_raw_mode()?; | ||||||
| 
 | 
 | ||||||
|     Ok(()) |     Ok(()) | ||||||
| } | } | ||||||
|  | |||||||
| @ -10,6 +10,7 @@ use rand::Rng; | |||||||
| use reqwest::Client; | use reqwest::Client; | ||||||
| use rodio::{Decoder, Source}; | use rodio::{Decoder, Source}; | ||||||
| 
 | 
 | ||||||
|  | /// Downloads a raw track, but doesn't decode it.
 | ||||||
| async fn download(track: &str, client: &Client) -> eyre::Result<Bytes> { | async fn download(track: &str, client: &Client) -> eyre::Result<Bytes> { | ||||||
|     let url = format!("https://lofigirl.com/wp-content/uploads/{}", track); |     let url = format!("https://lofigirl.com/wp-content/uploads/{}", track); | ||||||
|     let response = client.get(url).send().await?; |     let response = client.get(url).send().await?; | ||||||
| @ -18,16 +19,19 @@ async fn download(track: &str, client: &Client) -> eyre::Result<Bytes> { | |||||||
|     Ok(data) |     Ok(data) | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
| async fn random() -> eyre::Result<&'static str> { | /// Gets a random track from `tracks.txt` and returns it.
 | ||||||
|     let tracks = include_str!("../data/tracks.txt"); | fn random() -> &'static str { | ||||||
|     let tracks: Vec<&str> = tracks.split_ascii_whitespace().collect(); |     let tracks: Vec<&str> = include_str!("../data/tracks.txt") | ||||||
|  |         .split_ascii_whitespace() | ||||||
|  |         .collect(); | ||||||
| 
 | 
 | ||||||
|     let random = rand::thread_rng().gen_range(0..tracks.len()); |     let random = rand::thread_rng().gen_range(0..tracks.len()); | ||||||
|     let track = tracks[random]; |     let track = tracks[random]; | ||||||
| 
 | 
 | ||||||
|     Ok(track) |     track | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
|  | /// Just a shorthand for a decoded [Bytes].
 | ||||||
| pub type DecodedData = Decoder<Cursor<Bytes>>; | pub type DecodedData = Decoder<Cursor<Bytes>>; | ||||||
| 
 | 
 | ||||||
| /// The TrackInfo struct, which has the name and duration of a track.
 | /// 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 { | pub struct TrackInfo { | ||||||
|     /// This is a formatted name, so it doesn't include the full path.
 |     /// This is a formatted name, so it doesn't include the full path.
 | ||||||
|     pub name: String, |     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>, |     pub duration: Option<Duration>, | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
| impl TrackInfo { | 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 { |     fn format_name(name: &'static str) -> String { | ||||||
|         let mut formatted = name |         let mut formatted = name | ||||||
|             .split("/") |             .split("/") | ||||||
| @ -52,6 +62,9 @@ impl TrackInfo { | |||||||
|             .to_title_case(); |             .to_title_case(); | ||||||
| 
 | 
 | ||||||
|         let mut skip = 0; |         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() } { |         for character in unsafe { formatted.as_bytes_mut() } { | ||||||
|             if character.is_ascii_digit() { |             if character.is_ascii_digit() { | ||||||
|                 skip += 1; |                 skip += 1; | ||||||
| @ -63,6 +76,7 @@ impl TrackInfo { | |||||||
|         String::from(&formatted[skip..]) |         String::from(&formatted[skip..]) | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|  |     /// Creates a new [`TrackInfo`] from a raw name & decoded track data.
 | ||||||
|     pub fn new(name: &'static str, decoded: &DecodedData) -> Self { |     pub fn new(name: &'static str, decoded: &DecodedData) -> Self { | ||||||
|         Self { |         Self { | ||||||
|             duration: decoded.total_duration(), |             duration: decoded.total_duration(), | ||||||
| @ -82,6 +96,8 @@ pub struct DecodedTrack { | |||||||
| } | } | ||||||
| 
 | 
 | ||||||
| impl DecodedTrack { | impl DecodedTrack { | ||||||
|  |     /// Creates a new track.
 | ||||||
|  |     /// This is equivalent to [Track::decode].
 | ||||||
|     pub fn new(track: Track) -> eyre::Result<Self> { |     pub fn new(track: Track) -> eyre::Result<Self> { | ||||||
|         let data = Decoder::new(Cursor::new(track.data))?; |         let data = Decoder::new(Cursor::new(track.data))?; | ||||||
|         let info = TrackInfo::new(track.name, &data); |         let info = TrackInfo::new(track.name, &data); | ||||||
| @ -103,7 +119,7 @@ pub struct Track { | |||||||
| impl Track { | impl Track { | ||||||
|     /// Fetches and downloads a random track from the tracklist.
 |     /// Fetches and downloads a random track from the tracklist.
 | ||||||
|     pub async fn random(client: &Client) -> eyre::Result<Self> { |     pub async fn random(client: &Client) -> eyre::Result<Self> { | ||||||
|         let name = random().await?; |         let name = random(); | ||||||
|         let data = download(name, client).await?; |         let data = download(name, client).await?; | ||||||
| 
 | 
 | ||||||
|         Ok(Self { data, name }) |         Ok(Self { data, name }) | ||||||
|  | |||||||
		Loading…
	
	
			
			x
			
			
		
	
		Reference in New Issue
	
	Block a user