chore: fix some more lints and make the code more compact

This commit is contained in:
Tal 2024-09-27 14:58:59 +02:00
parent 8110b8418a
commit 65f7574765
5 changed files with 71 additions and 50 deletions

View File

@ -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.

View File

@ -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();

View File

@ -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 => {

View File

@ -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,42 +35,48 @@ 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()),
|subject| {
( (
format!("{} {}", word, subject.clone().bold()), format!("{} {}", word, subject.clone().bold()),
word.len() + 1 + subject.len(), word.len() + 1 + subject.len(),
) )
} else { },
(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 {
@ -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(())
} }

View File

@ -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 })