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};
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.

View File

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

View File

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

View File

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

View File

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