chore: reintroduce clippy & fix a lot of errors

This commit is contained in:
Tal 2024-10-18 23:37:45 +02:00
parent 04d5ed335d
commit fe9429bfb3
11 changed files with 198 additions and 145 deletions

View File

@ -1,10 +1,52 @@
//! An extremely simple lofi player.
#![warn(clippy::all, clippy::restriction, clippy::pedantic, clippy::nursery)]
#![allow(
clippy::single_call_fn,
clippy::struct_excessive_bools,
clippy::implicit_return,
clippy::question_mark_used,
clippy::shadow_reuse,
clippy::indexing_slicing,
clippy::arithmetic_side_effects,
clippy::std_instead_of_core,
clippy::print_stdout,
clippy::float_arithmetic,
clippy::integer_division_remainder_used,
clippy::used_underscore_binding,
clippy::print_stderr,
clippy::semicolon_outside_block,
clippy::non_send_fields_in_send_ty,
clippy::non_ascii_literal,
clippy::let_underscore_untyped,
clippy::let_underscore_must_use,
clippy::shadow_unrelated,
clippy::std_instead_of_alloc,
clippy::partial_pub_fields,
clippy::unseparated_literal_suffix,
clippy::self_named_module_files,
// TODO: Disallow these lints later.
clippy::unwrap_used,
clippy::pattern_type_mismatch,
clippy::tuple_array_conversions,
clippy::as_conversions,
clippy::cast_possible_truncation,
clippy::cast_precision_loss,
clippy::wildcard_enum_match_arm,
clippy::integer_division,
clippy::cast_sign_loss,
clippy::cast_lossless,
)]
use clap::{Parser, Subcommand}; use clap::{Parser, Subcommand};
mod play; mod play;
mod player; mod player;
mod scrape;
mod tracks; mod tracks;
#[allow(clippy::all, clippy::pedantic, clippy::nursery, clippy::restriction)]
mod scrape;
/// An extremely simple lofi player. /// An extremely simple lofi player.
#[derive(Parser)] #[derive(Parser)]
#[command(about, version)] #[command(about, version)]

View File

@ -34,11 +34,11 @@ impl PersistentVolume {
} }
/// Returns the volume as a float from 0 to 1. /// Returns the volume as a float from 0 to 1.
pub fn float(&self) -> f32 { pub fn float(self) -> f32 {
self.inner as f32 / 100.0 self.inner as f32 / 100.0
} }
/// Loads the [PersistentVolume] from [dirs::config_dir()]. /// Loads the [`PersistentVolume`] from [`dirs::config_dir()`].
pub async fn load() -> eyre::Result<Self> { pub async fn load() -> eyre::Result<Self> {
let config = Self::config().await?; let config = Self::config().await?;
let volume = config.join(PathBuf::from("volume.txt")); let volume = config.join(PathBuf::from("volume.txt"));
@ -47,16 +47,16 @@ impl PersistentVolume {
let volume = if volume.exists() { let volume = if volume.exists() {
let contents = fs::read_to_string(volume).await?; let contents = fs::read_to_string(volume).await?;
let trimmed = contents.trim(); let trimmed = contents.trim();
let stripped = trimmed.strip_suffix("%").unwrap_or(&trimmed); let stripped = trimmed.strip_suffix("%").unwrap_or(trimmed);
stripped stripped
.parse() .parse()
.map_err(|_| eyre!("volume.txt file is invalid"))? .map_err(|_error| eyre!("volume.txt file is invalid"))?
} else { } else {
fs::write(&volume, "100").await?; fs::write(&volume, "100").await?;
100u16 100u16
}; };
Ok(PersistentVolume { inner: volume }) Ok(Self { inner: volume })
} }
/// Saves `volume` to `volume.txt`. /// Saves `volume` to `volume.txt`.

View File

@ -16,10 +16,11 @@ use tokio::{
RwLock, RwLock,
}, },
task, task,
time::sleep,
}; };
#[cfg(feature = "mpris")] #[cfg(feature = "mpris")]
use mpris_server::PlayerInterface; use mpris_server::{PlaybackStatus, PlayerInterface};
use crate::{ use crate::{
play::PersistentVolume, play::PersistentVolume,
@ -89,13 +90,10 @@ pub struct Player {
/// This is [`None`] when lowfi is buffering/loading. /// This is [`None`] when lowfi is buffering/loading.
current: ArcSwapOption<tracks::Info>, current: ArcSwapOption<tracks::Info>,
/// This is the MPRIS server, which is initialized later on in the /// The tracks, which is a [`VecDeque`] that holds
/// user interface.
#[cfg(feature = "mpris")]
mpris: tokio::sync::OnceCell<mpris_server::Server<mpris::Player>>,
/// The tracks, which is a [VecDeque] that holds
/// *undecoded* [Track]s. /// *undecoded* [Track]s.
///
/// This is populated specifically by the [Downloader].
tracks: RwLock<VecDeque<tracks::Track>>, tracks: RwLock<VecDeque<tracks::Track>>,
/// The actual list of tracks to be played. /// The actual list of tracks to be played.
@ -104,16 +102,16 @@ pub struct Player {
/// The initial volume level. /// The initial volume level.
volume: PersistentVolume, volume: PersistentVolume,
/// The web client, which can contain a UserAgent & some /// The web client, which can contain a `UserAgent` & some
/// settings that help lowfi work more effectively. /// settings that help lowfi work more effectively.
client: Client, client: Client,
/// The [OutputStreamHandle], which also can control some /// The [`OutputStreamHandle`], which also can control some
/// playback, is for now unused and is here just to keep it /// playback, is for now unused and is here just to keep it
/// alive so the playback can function properly. /// alive so the playback can function properly.
_handle: OutputStreamHandle, _handle: OutputStreamHandle,
/// The [OutputStream], which is just here to keep the playback /// The [`OutputStream`], which is just here to keep the playback
/// alive and functioning. /// alive and functioning.
_stream: OutputStream, _stream: OutputStream,
} }
@ -143,7 +141,9 @@ impl Player {
// First redirect to /dev/null, which basically silences alsa. // First redirect to /dev/null, which basically silences alsa.
let null = CString::new("/dev/null")?.as_ptr(); let null = CString::new("/dev/null")?.as_ptr();
// SAFETY: Simple enough to be impossible to fail. Hopefully. // SAFETY: Simple enough to be impossible to fail. Hopefully.
unsafe { freopen(null, mode, stderr) }; unsafe {
freopen(null, mode, stderr);
}
// Make the OutputStream while stderr is still redirected to /dev/null. // Make the OutputStream while stderr is still redirected to /dev/null.
let (stream, handle) = OutputStream::try_default()?; let (stream, handle) = OutputStream::try_default()?;
@ -151,16 +151,16 @@ impl Player {
// Redirect back to the current terminal, so that other output isn't silenced. // Redirect back to the current terminal, so that other output isn't silenced.
let tty = CString::new("/dev/tty")?.as_ptr(); let tty = CString::new("/dev/tty")?.as_ptr();
// SAFETY: See the first call to `freopen`. // SAFETY: See the first call to `freopen`.
unsafe { freopen(tty, mode, stderr) }; unsafe {
freopen(tty, mode, stderr);
}
Ok((stream, handle)) Ok((stream, handle))
} }
/// Just a shorthand for setting `current`. /// Just a shorthand for setting `current`.
async fn set_current(&self, info: tracks::Info) -> eyre::Result<()> { fn set_current(&self, info: tracks::Info) {
self.current.store(Some(Arc::new(info))); self.current.store(Some(Arc::new(info)));
Ok(())
} }
/// A shorthand for checking if `self.current` is [Some]. /// A shorthand for checking if `self.current` is [Some].
@ -170,7 +170,7 @@ impl Player {
/// Sets the volume of the sink, and also clamps the value to avoid negative/over 100% values. /// Sets the volume of the sink, and also clamps the value to avoid negative/over 100% values.
pub fn set_volume(&self, volume: f32) { pub fn set_volume(&self, volume: f32) {
self.sink.set_volume(volume.clamp(0.0, 1.0)) self.sink.set_volume(volume.clamp(0.0, 1.0));
} }
/// Initializes the entire player, including audio devices & sink. /// Initializes the entire player, including audio devices & sink.
@ -213,9 +213,6 @@ impl Player {
list, list,
_handle: handle, _handle: handle,
_stream, _stream,
#[cfg(feature = "mpris")]
mpris: tokio::sync::OnceCell::new(),
}; };
Ok(player) Ok(player)
@ -225,49 +222,28 @@ impl Player {
/// ///
/// This will also set `current` to the newly loaded song. /// This will also set `current` to the newly loaded song.
pub async fn next(&self) -> eyre::Result<tracks::Decoded> { pub async fn next(&self) -> eyre::Result<tracks::Decoded> {
let track = match self.tracks.write().await.pop_front() { let track = if let Some(track) = self.tracks.write().await.pop_front() {
Some(x) => x, track
} else {
// 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.
None => {
// Serves as an indicator that the queue is "loading". // Serves as an indicator that the queue is "loading".
// We're doing it here so that we don't get the "loading" display // We're doing it here so that we don't get the "loading" display
// for only a frame in the other case that the buffer is not empty. // for only a frame in the other case that the buffer is not empty.
self.current.store(None); self.current.store(None);
self.list.random(&self.client).await? self.list.random(&self.client).await?
}
}; };
let decoded = track.decode()?; let decoded = track.decode()?;
// Set the current track. // Set the current track.
self.set_current(decoded.info.clone()).await?; self.set_current(decoded.info.clone());
Ok(decoded) Ok(decoded)
} }
/// Shorthand to emit a `PropertiesChanged` signal, like when pausing/unpausing.
#[cfg(feature = "mpris")]
async fn mpris_changed(
&self,
properties: impl IntoIterator<Item = mpris_server::Property>,
) -> eyre::Result<()> {
self.mpris
.get()
.unwrap()
.properties_changed(properties)
.await?;
Ok(())
}
/// Shorthand to get the inner mpris server object.
#[cfg(feature = "mpris")]
fn mpris_innner(&self) -> &mpris::Player {
self.mpris.get().unwrap().imp()
}
/// This basically just calls [`Player::next`], and then appends the new track to the player. /// This basically just calls [`Player::next`], and then appends the new track to the player.
/// ///
/// This also notifies the background thread to get to work, and will send `TryAgain` /// This also notifies the background thread to get to work, and will send `TryAgain`
@ -296,14 +272,14 @@ impl Player {
Downloader::notify(&itx).await?; Downloader::notify(&itx).await?;
// Notify the audio server that the next song has actually been downloaded. // Notify the audio server that the next song has actually been downloaded.
tx.send(Messages::NewSong).await? tx.send(Messages::NewSong).await?;
} }
Err(error) => { Err(error) => {
if !error.downcast::<reqwest::Error>()?.is_timeout() { if !error.downcast::<reqwest::Error>()?.is_timeout() {
tokio::time::sleep(TIMEOUT).await; sleep(TIMEOUT).await;
} }
tx.send(Messages::TryAgain).await? tx.send(Messages::TryAgain).await?;
} }
}; };
@ -319,9 +295,18 @@ impl Player {
tx: Sender<Messages>, tx: Sender<Messages>,
mut rx: Receiver<Messages>, mut rx: Receiver<Messages>,
) -> eyre::Result<()> { ) -> eyre::Result<()> {
// Initialize the mpris player.
//
// We're initializing here, despite MPRIS being a "user interface",
// since we need to be able to *actively* write new information to MPRIS
// specifically when it occurs, unlike the UI which passively reads the
// information each frame. Blame MPRIS, not me.
#[cfg(feature = "mpris")]
let mpris = mpris::Server::new(Arc::clone(&player), tx.clone()).await?;
// `itx` is used to notify the `Downloader` when it needs to download new tracks. // `itx` is used to notify the `Downloader` when it needs to download new tracks.
let downloader = Downloader::new(player.clone()); let downloader = Downloader::new(Arc::clone(&player));
let (itx, downloader) = downloader.start().await; let (itx, downloader) = downloader.start();
// Start buffering tracks immediately. // Start buffering tracks immediately.
Downloader::notify(&itx).await?; Downloader::notify(&itx).await?;
@ -329,12 +314,13 @@ impl Player {
// Set the initial sink volume to the one specified. // Set the initial sink volume to the one specified.
player.set_volume(player.volume.float()); player.set_volume(player.volume.float());
// Whether the last signal was a `NewSong`. // Whether the last signal was a `NewSong`. This is helpful, since we
// This is helpful, since we only want to autoplay // only want to autoplay if there hasn't been any manual intervention.
// if there hasn't been any manual intervention. //
// In other words, this will be `true` after a new track has been fully
// loaded and it'll be `false` if a track is still currently loading.
let mut new = false; let mut new = false;
// TODO: Clean mpris_changed calls & streamline them somehow.
loop { loop {
let clone = Arc::clone(&player); let clone = Arc::clone(&player);
@ -353,7 +339,7 @@ impl Player {
// //
// It's also important to note that the condition is only checked at the // It's also important to note that the condition is only checked at the
// beginning of the loop, not throughout. // beginning of the loop, not throughout.
Ok(_) = task::spawn_blocking(move || clone.sink.sleep_until_end()), Ok(()) = task::spawn_blocking(move || clone.sink.sleep_until_end()),
if new => Messages::Next, if new => Messages::Next,
}; };
@ -370,27 +356,23 @@ impl Player {
// Handle the rest of the signal in the background, // Handle the rest of the signal in the background,
// as to not block the main audio thread. // as to not block the main audio thread.
task::spawn(Self::handle_next(player.clone(), itx.clone(), tx.clone())); task::spawn(Self::handle_next(
Arc::clone(&player),
itx.clone(),
tx.clone(),
));
} }
Messages::Play => { Messages::Play => {
player.sink.play(); player.sink.play();
#[cfg(feature = "mpris")] #[cfg(feature = "mpris")]
player mpris.playback(PlaybackStatus::Playing).await?;
.mpris_changed(vec![mpris_server::Property::PlaybackStatus(
mpris_server::PlaybackStatus::Playing,
)])
.await?;
} }
Messages::Pause => { Messages::Pause => {
player.sink.pause(); player.sink.pause();
#[cfg(feature = "mpris")] #[cfg(feature = "mpris")]
player mpris.playback(PlaybackStatus::Paused).await?;
.mpris_changed(vec![mpris_server::Property::PlaybackStatus(
mpris_server::PlaybackStatus::Paused,
)])
.await?;
} }
Messages::PlayPause => { Messages::PlayPause => {
if player.sink.is_paused() { if player.sink.is_paused() {
@ -400,18 +382,16 @@ impl Player {
} }
#[cfg(feature = "mpris")] #[cfg(feature = "mpris")]
player mpris
.mpris_changed(vec![mpris_server::Property::PlaybackStatus( .playback(mpris.player().playback_status().await?)
player.mpris_innner().playback_status().await?,
)])
.await?; .await?;
} }
Messages::ChangeVolume(change) => { Messages::ChangeVolume(change) => {
player.set_volume(player.sink.volume() + change); player.set_volume(player.sink.volume() + change);
#[cfg(feature = "mpris")] #[cfg(feature = "mpris")]
player mpris
.mpris_changed(vec![mpris_server::Property::Volume( .changed(vec![mpris_server::Property::Volume(
player.sink.volume().into(), player.sink.volume().into(),
)]) )])
.await?; .await?;
@ -425,13 +405,11 @@ impl Player {
new = true; new = true;
#[cfg(feature = "mpris")] #[cfg(feature = "mpris")]
player mpris
.mpris_changed(vec![ .changed(vec![
mpris_server::Property::Metadata( mpris_server::Property::Metadata(mpris.player().metadata().await?),
player.mpris_innner().metadata().await?,
),
mpris_server::Property::PlaybackStatus( mpris_server::Property::PlaybackStatus(
player.mpris_innner().playback_status().await?, mpris.player().playback_status().await?,
), ),
]) ])
.await?; .await?;

View File

@ -5,6 +5,7 @@ use std::sync::Arc;
use tokio::{ use tokio::{
sync::mpsc::{self, Receiver, Sender}, sync::mpsc::{self, Receiver, Sender},
task::{self, JoinHandle}, task::{self, JoinHandle},
time::sleep,
}; };
use super::{Player, BUFFER_SIZE, TIMEOUT}; use super::{Player, BUFFER_SIZE, TIMEOUT};
@ -42,7 +43,7 @@ impl Downloader {
} }
/// Actually starts & consumes the [Downloader]. /// Actually starts & consumes the [Downloader].
pub async fn start(mut self) -> (Sender<()>, JoinHandle<()>) { pub fn start(mut self) -> (Sender<()>, JoinHandle<()>) {
( (
self.tx, self.tx,
task::spawn(async move { task::spawn(async move {
@ -54,7 +55,7 @@ impl Downloader {
Ok(track) => self.player.tracks.write().await.push_back(track), Ok(track) => self.player.tracks.write().await.push_back(track),
Err(error) => { Err(error) => {
if !error.is_timeout() { if !error.is_timeout() {
tokio::time::sleep(TIMEOUT).await; sleep(TIMEOUT).await;
} }
} }
} }

View File

@ -1,7 +1,7 @@
use std::sync::Arc; use std::sync::Arc;
use mpris_server::{ use mpris_server::{
zbus::{fdo, Result}, zbus::{self, fdo, Result},
LoopStatus, Metadata, PlaybackRate, PlaybackStatus, PlayerInterface, RootInterface, Time, LoopStatus, Metadata, PlaybackRate, PlaybackStatus, PlayerInterface, RootInterface, Time,
TrackId, Volume, TrackId, Volume,
}; };
@ -11,7 +11,7 @@ use super::Messages;
const ERROR: fdo::Error = fdo::Error::Failed(String::new()); const ERROR: fdo::Error = fdo::Error::Failed(String::new());
/// The actual MPRIS server. /// The actual MPRIS player.
pub struct Player { pub struct Player {
pub player: Arc<super::Player>, pub player: Arc<super::Player>,
pub sender: Sender<Messages>, pub sender: Sender<Messages>,
@ -209,3 +209,42 @@ impl PlayerInterface for Player {
Ok(true) Ok(true)
} }
} }
/// A struct which contains the MPRIS [Server], and has some helper functions
/// to make it easier to work with.
pub struct Server {
inner: mpris_server::Server<Player>,
}
impl Server {
/// Shorthand to emit a `PropertiesChanged` signal, like when pausing/unpausing.
pub async fn changed(
&self,
properties: impl IntoIterator<Item = mpris_server::Property>,
) -> eyre::Result<()> {
self.inner.properties_changed(properties).await?;
Ok(())
}
/// Shorthand to emit a `PropertiesChanged` signal, specifically about playback.
pub async fn playback(&self, new: PlaybackStatus) -> zbus::Result<()> {
self.inner
.properties_changed(vec![mpris_server::Property::PlaybackStatus(new)])
.await
}
/// Shorthand to get the inner mpris player object.
pub fn player(&self) -> &Player {
self.inner.imp()
}
pub async fn new(player: Arc<super::Player>, sender: Sender<Messages>) -> eyre::Result<Self> {
let server =
mpris_server::Server::new("lowfi", crate::player::mpris::Player { player, sender })
.await
.unwrap();
Ok(Self { inner: server })
}
}

View File

@ -1,6 +1,7 @@
//! The module which manages all user interface, including inputs. //! The module which manages all user interface, including inputs.
use std::{ use std::{
fmt::Write,
io::{stdout, Stdout}, io::{stdout, Stdout},
sync::{ sync::{
atomic::{AtomicUsize, Ordering}, atomic::{AtomicUsize, Ordering},
@ -55,7 +56,11 @@ lazy_static! {
/// The main purpose of this struct is just to add the fancy border, /// The main purpose of this struct is just to add the fancy border,
/// as well as clear the screen before drawing. /// as well as clear the screen before drawing.
pub struct Window { pub struct Window {
/// The top & bottom borders, which are here since they can be
/// prerendered, as they don't change from window to window.
borders: [String; 2], borders: [String; 2],
/// The output, currently just an [`Stdout`].
out: Stdout, out: Stdout,
} }
@ -76,10 +81,11 @@ impl Window {
pub fn draw(&mut self, content: Vec<String>) -> eyre::Result<()> { pub fn draw(&mut self, content: Vec<String>) -> eyre::Result<()> {
let len = content.len() as u16; let len = content.len() as u16;
let menu: String = content let menu: String = content.into_iter().fold(String::new(), |mut output, x| {
.into_iter() write!(output, "│ {} │\r\n", x.reset()).unwrap();
.map(|x| format!("{}\r\n", x.reset()).to_string())
.collect(); output
});
// We're doing this because Windows is stupid and can't stand // We're doing this because Windows is stupid and can't stand
// writing to the last line repeatedly. Again, it's stupid. // writing to the last line repeatedly. Again, it's stupid.
@ -155,19 +161,6 @@ async fn interface(player: Arc<Player>, minimalist: bool) -> eyre::Result<()> {
} }
} }
/// The mpris server additionally needs a reference to the player,
/// since it frequently accesses the sink directly as well as
/// the current track.
#[cfg(feature = "mpris")]
async fn mpris(
player: Arc<Player>,
sender: Sender<Messages>,
) -> mpris_server::Server<crate::player::mpris::Player> {
mpris_server::Server::new("lowfi", crate::player::mpris::Player { player, sender })
.await
.unwrap()
}
/// Represents the terminal environment, and is used to properly /// Represents the terminal environment, and is used to properly
/// initialize and clean up the terminal. /// initialize and clean up the terminal.
pub struct Environment { pub struct Environment {
@ -230,7 +223,7 @@ impl Environment {
} }
impl Drop for Environment { impl Drop for Environment {
/// Just a wrapper for [Environment::cleanup] which ignores any errors thrown. /// Just a wrapper for [`Environment::cleanup`] which ignores any errors thrown.
fn drop(&mut self) { fn drop(&mut self) {
// Well, we're dropping it, so it doesn't really matter if there's an error. // Well, we're dropping it, so it doesn't really matter if there's an error.
let _ = self.cleanup(); let _ = self.cleanup();
@ -239,19 +232,10 @@ impl Drop for Environment {
/// Initializes the UI, this will also start taking input from the user. /// Initializes the UI, this will also start taking input from the user.
/// ///
/// `alternate` controls whether to use [EnterAlternateScreen] in order to hide /// `alternate` controls whether to use [`EnterAlternateScreen`] in order to hide
/// previous terminal history. /// previous terminal history.
pub async fn start(player: Arc<Player>, sender: Sender<Messages>, args: Args) -> eyre::Result<()> { pub async fn start(player: Arc<Player>, sender: Sender<Messages>, args: Args) -> eyre::Result<()> {
let environment = Environment::ready(args.alternate)?; let environment = Environment::ready(args.alternate)?;
#[cfg(feature = "mpris")]
{
player
.mpris
.get_or_init(|| mpris(player.clone(), sender.clone()))
.await;
}
let interface = task::spawn(interface(Arc::clone(&player), args.minimalist)); let interface = task::spawn(interface(Arc::clone(&player), args.minimalist));
input::listen(sender.clone()).await?; input::listen(sender.clone()).await?;

View File

@ -1,3 +1,6 @@
//! Various different individual components that
//! appear in lowfi's UI, like the progress bar.
use std::{ops::Deref, sync::Arc, time::Duration}; use std::{ops::Deref, sync::Arc, time::Duration};
use crossterm::style::Stylize; use crossterm::style::Stylize;
@ -9,7 +12,7 @@ pub 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;
format!("{:02}:{:02}", minutes, seconds) format!("{minutes:02}:{seconds:02}")
} }
/// Creates the progress bar, as well as all the padding needed. /// Creates the progress bar, as well as all the padding needed.
@ -55,8 +58,13 @@ pub fn audio_bar(volume: f32, percentage: &str, width: usize) -> String {
/// This represents the main "action" bars state. /// This represents the main "action" bars state.
enum ActionBar { enum ActionBar {
/// When the app is currently displaying "paused".
Paused(Info), Paused(Info),
/// When the app is currently displaying "playing".
Playing(Info), Playing(Info),
/// When the app is currently displaying "loading".
Loading, Loading,
} }
@ -72,12 +80,7 @@ impl ActionBar {
subject.map_or_else( subject.map_or_else(
|| (word.to_owned(), word.len()), || (word.to_owned(), word.len()),
|(subject, len)| { |(subject, len)| (format!("{} {}", word, subject.bold()), word.len() + 1 + len),
(
format!("{} {}", word, subject.clone().bold()),
word.len() + 1 + len,
)
},
) )
} }
} }
@ -97,6 +100,8 @@ pub fn action(player: &Player, current: Option<&Arc<Info>>, width: usize) -> Str
}) })
.format(); .format();
// TODO: Deal with dangerous string slicing.
#[allow(clippy::string_slice)]
if len > width { if len > width {
format!("{}...", &main[..=width]) format!("{}...", &main[..=width])
} else { } else {

View File

@ -1,3 +1,6 @@
//! Responsible for specifically recieving terminal input
//! using [`crossterm`].
use std::sync::atomic::Ordering; use std::sync::atomic::Ordering;
use crossterm::event::{self, EventStream, KeyCode, KeyEventKind, KeyModifiers}; use crossterm::event::{self, EventStream, KeyCode, KeyEventKind, KeyModifiers};
@ -48,9 +51,9 @@ pub async fn listen(sender: Sender<Messages>) -> eyre::Result<()> {
}, },
// Media keys // Media keys
KeyCode::Media(media) => match media { KeyCode::Media(media) => match media {
event::MediaKeyCode::Play => Messages::PlayPause, event::MediaKeyCode::Pause
event::MediaKeyCode::Pause => Messages::PlayPause, | event::MediaKeyCode::Play
event::MediaKeyCode::PlayPause => Messages::PlayPause, | event::MediaKeyCode::PlayPause => Messages::PlayPause,
event::MediaKeyCode::Stop => Messages::Pause, event::MediaKeyCode::Stop => Messages::Pause,
event::MediaKeyCode::TrackNext => Messages::Next, event::MediaKeyCode::TrackNext => Messages::Next,
event::MediaKeyCode::LowerVolume => Messages::ChangeVolume(-0.1), event::MediaKeyCode::LowerVolume => Messages::ChangeVolume(-0.1),

View File

@ -82,7 +82,7 @@ async fn scan(extension: &str, include_full: bool) -> eyre::Result<Vec<String>>
pub async fn scrape(extension: String, include_full: bool) -> eyre::Result<()> { pub async fn scrape(extension: String, include_full: bool) -> eyre::Result<()> {
let files = scan(&extension, include_full).await?; let files = scan(&extension, include_full).await?;
for file in files { for file in files {
println!("{}", file); println!("{file}");
} }
Ok(()) Ok(())

View File

@ -15,11 +15,11 @@ pub mod list;
/// Just a shorthand for a decoded [Bytes]. /// 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 [`Info`] struct, which has the name and duration of a track.
/// ///
/// This is not included in [Track] as the duration has to be acquired /// This is not included in [Track] as the duration has to be acquired
/// from the decoded data and not from the raw data. /// from the decoded data and not from the raw data.
#[derive(Debug, PartialEq, Clone)] #[derive(Debug, Eq, PartialEq, Clone)]
pub struct Info { pub struct Info {
/// 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,
@ -46,7 +46,7 @@ impl Info {
/// usually present on most lofi tracks. /// usually present on most lofi tracks.
fn format_name(name: &str) -> String { fn format_name(name: &str) -> String {
let formatted = Self::decode_url( let formatted = Self::decode_url(
name.split("/") name.split('/')
.last() .last()
.unwrap() .unwrap()
.strip_suffix(".mp3") .strip_suffix(".mp3")
@ -76,12 +76,13 @@ impl Info {
} }
} }
#[allow(clippy::string_slice, /* We've already checked before that the bound is at an ASCII digit. */)]
String::from(&formatted[skip..]) String::from(&formatted[skip..])
} }
/// Creates a new [`TrackInfo`] from a raw name & decoded track data. /// Creates a new [`TrackInfo`] from a raw name & decoded track data.
pub fn new(name: String, decoded: &DecodedData) -> Self { pub fn new(name: &str, decoded: &DecodedData) -> Self {
let name = Self::format_name(&name); let name = Self::format_name(name);
Self { Self {
duration: decoded.total_duration(), duration: decoded.total_duration(),
@ -103,10 +104,10 @@ pub struct Decoded {
impl Decoded { impl Decoded {
/// Creates a new track. /// Creates a new track.
/// This is equivalent to [Track::decode]. /// 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 = Info::new(track.name, &data); let info = Info::new(&track.name, &data);
Ok(Self { info, data }) Ok(Self { info, data })
} }

View File

@ -32,7 +32,7 @@ impl List {
// how rust vectors work, sinceslow to drain only a single element from // how rust vectors work, sinceslow to drain only a single element from
// the start, so it's faster to just keep it in & work around it. // the start, so it's faster to just keep it in & work around it.
let random = rand::thread_rng().gen_range(1..self.lines.len()); let random = rand::thread_rng().gen_range(1..self.lines.len());
self.lines[random].to_owned() self.lines[random].clone()
} }
/// Downloads a raw track, but doesn't decode it. /// Downloads a raw track, but doesn't decode it.
@ -59,13 +59,13 @@ impl List {
} }
/// Parses text into a [List]. /// Parses text into a [List].
pub fn new(text: &str) -> eyre::Result<Self> { pub fn new(text: &str) -> Self {
let lines: Vec<String> = text let lines: Vec<String> = text
.split_ascii_whitespace() .split_ascii_whitespace()
.map(|x| x.to_owned()) .map(ToOwned::to_owned)
.collect(); .collect();
Ok(Self { lines }) Self { lines }
} }
/// Reads a [List] from the filesystem using the CLI argument provided. /// Reads a [List] from the filesystem using the CLI argument provided.
@ -84,9 +84,9 @@ impl List {
fs::read_to_string(arg).await? fs::read_to_string(arg).await?
}; };
List::new(&raw) Ok(Self::new(&raw))
} else { } else {
List::new(include_str!("../../data/lofigirl.txt")) Ok(Self::new(include_str!("../../data/lofigirl.txt")))
} }
} }
} }