From 65f75747650d3fd7bd74bf53e6d8aa5176d138a8 Mon Sep 17 00:00:00 2001 From: Tal <83217276+talwat@users.noreply.github.com> Date: Fri, 27 Sep 2024 14:58:59 +0200 Subject: [PATCH] chore: fix some more lints and make the code more compact --- src/main.rs | 12 +++------- src/play.rs | 4 ++-- src/player.rs | 19 +++++++-------- src/player/ui.rs | 60 ++++++++++++++++++++++++++++-------------------- src/tracks.rs | 26 +++++++++++++++++---- 5 files changed, 71 insertions(+), 50 deletions(-) diff --git a/src/main.rs b/src/main.rs index a6057b0..c9631a3 100644 --- a/src/main.rs +++ b/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, } +/// Defines all of the extra commands lowfi can run. #[derive(Subcommand)] enum Commands { /// Scrapes the lofi girl website file server for files. diff --git a/src/play.rs b/src/play.rs index fbdb113..06eca6c 100644 --- a/src/play.rs +++ b/src/play.rs @@ -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(); diff --git a/src/player.rs b/src/player.rs index 82cca4b..4f13f2a 100644 --- a/src/player.rs +++ b/src/player.rs @@ -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) -> eyre::Result { - let track = queue.tracks.write().await.pop_front(); - let track = match track { + pub async fn next(queue: Arc) -> eyre::Result { + 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, mut rx: Receiver) -> eyre::Result<()> { + pub async fn play(queue: Arc, 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. @@ -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 => { diff --git a/src/player/ui.rs b/src/player/ui.rs index b0cde6a..cd524b8 100644 --- a/src/player/ui.rs +++ b/src/player/ui.rs @@ -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) -> 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) -> eyre::Result<()> { /// Initializes the UI, this will also start taking input from the user. pub async fn start(queue: Arc, sender: Sender) -> 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, sender: Sender) -> eyre::Result //crossterm::execute!(stderr(), LeaveAlternateScreen)?; crossterm::execute!(stderr(), Clear(ClearType::FromCursorDown), Show)?; - crossterm::terminal::disable_raw_mode()?; + terminal::disable_raw_mode()?; Ok(()) } diff --git a/src/tracks.rs b/src/tracks.rs index 466873a..8a89417 100644 --- a/src/tracks.rs +++ b/src/tracks.rs @@ -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 { 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 { 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>; /// The TrackInfo struct, which has the name and duration of a track. @@ -38,10 +42,16 @@ pub type DecodedData = Decoder>; 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, } 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 { 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 { - let name = random().await?; + let name = random(); let data = download(name, client).await?; Ok(Self { data, name })