fix: tackle lots of clippy lints, along with removing outputstream from player struct

This commit is contained in:
talwat 2025-02-21 21:44:32 +01:00
parent a720e9d2cf
commit ece88de1ae
10 changed files with 79 additions and 88 deletions

2
Cargo.lock generated
View File

@ -1453,7 +1453,7 @@ checksum = "a7a70ba024b9dc04c27ea2f0c0548feb474ec5c54bba33a7f72f873a39d07b24"
[[package]] [[package]]
name = "lowfi" name = "lowfi"
version = "1.6.0" version = "1.6.1"
dependencies = [ dependencies = [
"Inflector", "Inflector",
"arc-swap", "arc-swap",

View File

@ -1,6 +1,6 @@
[package] [package]
name = "lowfi" name = "lowfi"
version = "1.6.0" version = "1.6.1"
edition = "2021" edition = "2021"
description = "An extremely simple lofi player." description = "An extremely simple lofi player."
license = "MIT" license = "MIT"

View File

@ -1,44 +1,6 @@
//! An extremely simple lofi player. //! An extremely simple lofi player.
#![warn(clippy::all, clippy::restriction, clippy::pedantic, clippy::nursery)] #![warn(clippy::all, 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,
clippy::arbitrary_source_item_ordering,
clippy::unused_trait_names
)]
use clap::{Parser, Subcommand}; use clap::{Parser, Subcommand};
@ -52,6 +14,10 @@ mod scrape;
/// An extremely simple lofi player. /// An extremely simple lofi player.
#[derive(Parser)] #[derive(Parser)]
#[command(about, version)] #[command(about, version)]
#[allow(
clippy::struct_excessive_bools,
reason = "señor clippy, i assure you this is not a state machine"
)]
struct Args { struct Args {
/// Whether to use an alternate terminal screen. /// Whether to use an alternate terminal screen.
#[clap(long, short)] #[clap(long, short)]

View File

@ -23,7 +23,7 @@ impl PersistentVolume {
/// Retrieves the config directory. /// Retrieves the config directory.
async fn config() -> eyre::Result<PathBuf> { async fn config() -> eyre::Result<PathBuf> {
let config = dirs::config_dir() let config = dirs::config_dir()
.ok_or(eyre!("Couldn't find config directory"))? .ok_or_else(|| eyre!("Couldn't find config directory"))?
.join(PathBuf::from("lowfi")); .join(PathBuf::from("lowfi"));
if !config.exists() { if !config.exists() {
@ -35,7 +35,7 @@ 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 f32::from(self.inner) / 100.0
} }
/// Loads the [`PersistentVolume`] from [`dirs::config_dir()`]. /// Loads the [`PersistentVolume`] from [`dirs::config_dir()`].
@ -64,17 +64,38 @@ impl PersistentVolume {
let config = Self::config().await?; let config = Self::config().await?;
let path = config.join(PathBuf::from("volume.txt")); let path = config.join(PathBuf::from("volume.txt"));
fs::write(path, ((volume * 100.0).abs().round() as u16).to_string()).await?; #[expect(
clippy::as_conversions,
clippy::cast_sign_loss,
clippy::cast_possible_truncation,
reason = "already rounded & absolute, therefore this should be safe"
)]
let percentage = (volume * 100.0).abs().round() as u16;
fs::write(path, percentage.to_string()).await?;
Ok(()) Ok(())
} }
} }
/// Wrapper around [`rodio::OutputStream`] to implement [Send], currently unsafely.
///
/// This is more of a temporary solution until cpal implements [Send] on it's output stream.
pub struct SendableOutputStream(pub rodio::OutputStream);
// SAFETY: This is necessary because [OutputStream] does not implement [Send],
// due to some limitation with Android's Audio API.
// I'm pretty sure nobody will use lowfi with android, so this is safe.
#[expect(clippy::non_send_fields_in_send_ty, reason = "yes")]
unsafe impl Send for SendableOutputStream {}
/// Initializes the audio server, and then safely stops /// Initializes the audio server, and then safely stops
/// it when the frontend quits. /// it when the frontend quits.
pub async fn play(args: Args) -> eyre::Result<()> { pub async fn play(args: Args) -> eyre::Result<()> {
// Actually initializes the player. // Actually initializes the player.
let player = Arc::new(Player::new(&args).await?); // Stream kept here in the master thread to keep it alive.
let (player, stream) = Player::new(&args).await?;
let player = Arc::new(player);
// Initialize the UI, as well as the internal communication channel. // Initialize the UI, as well as the internal communication channel.
let (tx, rx) = mpsc::channel(8); let (tx, rx) = mpsc::channel(8);
@ -90,6 +111,7 @@ pub async fn play(args: Args) -> eyre::Result<()> {
PersistentVolume::save(player.sink.volume()).await?; PersistentVolume::save(player.sink.volume()).await?;
player.sink.stop(); player.sink.stop();
ui.abort(); ui.abort();
drop(stream.0);
Ok(()) Ok(())
} }

View File

@ -2,11 +2,10 @@
//! This also has the code for the underlying //! This also has the code for the underlying
//! audio server which adds new tracks. //! audio server which adds new tracks.
use std::{collections::VecDeque, ffi::CString, sync::Arc, time::Duration}; use std::{collections::VecDeque, sync::Arc, time::Duration};
use arc_swap::ArcSwapOption; use arc_swap::ArcSwapOption;
use downloader::Downloader; use downloader::Downloader;
use libc::freopen;
use reqwest::Client; use reqwest::Client;
use rodio::{OutputStream, OutputStreamHandle, Sink}; use rodio::{OutputStream, OutputStreamHandle, Sink};
use tokio::{ use tokio::{
@ -23,7 +22,7 @@ use tokio::{
use mpris_server::{PlaybackStatus, PlayerInterface, Property}; use mpris_server::{PlaybackStatus, PlayerInterface, Property};
use crate::{ use crate::{
play::PersistentVolume, play::{PersistentVolume, SendableOutputStream},
tracks::{self, list::List}, tracks::{self, list::List},
Args, Args,
}; };
@ -53,6 +52,7 @@ pub enum Messages {
Init, Init,
/// Unpause the [Sink]. /// Unpause the [Sink].
#[allow(dead_code, reason = "this code may not be dead depending on features")]
Play, Play,
/// Pauses the [Sink]. /// Pauses the [Sink].
@ -109,20 +109,8 @@ pub struct Player {
/// 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
/// alive and functioning.
_stream: OutputStream,
} }
// SAFETY: This is necessary because [OutputStream] does not implement [Send],
// due to some limitation with Android's Audio API.
// I'm pretty sure nobody will use lowfi with android, so this is safe.
unsafe impl Send for Player {}
// SAFETY: See implementation for [Send].
unsafe impl Sync for Player {}
impl Player { impl Player {
/// This gets the output stream while also shutting up alsa with [libc]. /// This gets the output stream while also shutting up alsa with [libc].
/// Uses raw libc calls, and therefore is functional only on Linux. /// Uses raw libc calls, and therefore is functional only on Linux.
@ -182,7 +170,7 @@ impl Player {
/// Initializes the entire player, including audio devices & sink. /// Initializes the entire player, including audio devices & sink.
/// ///
/// This also will load the track list & persistent volume. /// This also will load the track list & persistent volume.
pub async fn new(args: &Args) -> eyre::Result<Self> { pub async fn new(args: &Args) -> eyre::Result<(Self, SendableOutputStream)> {
// Load the volume file. // Load the volume file.
let volume = PersistentVolume::load().await?; let volume = PersistentVolume::load().await?;
@ -199,7 +187,7 @@ impl Player {
// If we're not on Linux, then there's no problem. // If we're not on Linux, then there's no problem.
#[cfg(not(target_os = "linux"))] #[cfg(not(target_os = "linux"))]
let (_stream, handle) = OutputStream::try_default()?; let (stream, handle) = OutputStream::try_default()?;
let sink = Sink::try_new(&handle)?; let sink = Sink::try_new(&handle)?;
if args.paused { if args.paused {
@ -223,10 +211,9 @@ impl Player {
volume, volume,
list, list,
_handle: handle, _handle: handle,
_stream,
}; };
Ok(player) Ok((player, SendableOutputStream(stream)))
} }
/// 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.

View File

@ -1,7 +1,15 @@
//! The module which manages all user interface, including inputs. //! The module which manages all user interface, including inputs.
#![allow(
clippy::as_conversions,
clippy::cast_sign_loss,
clippy::cast_precision_loss,
clippy::cast_possible_truncation,
reason = "the ui is full of these because of various layout & positioning aspects, and for a simple music player making all casts safe is not worth the effort"
)]
use std::{ use std::{
fmt::Write, fmt::Write as _,
io::{stdout, Stdout}, io::{stdout, Stdout},
sync::{ sync::{
atomic::{AtomicUsize, Ordering}, atomic::{AtomicUsize, Ordering},
@ -15,7 +23,7 @@ use crate::Args;
use crossterm::{ use crossterm::{
cursor::{Hide, MoveTo, MoveToColumn, MoveUp, Show}, cursor::{Hide, MoveTo, MoveToColumn, MoveUp, Show},
event::{KeyboardEnhancementFlags, PopKeyboardEnhancementFlags, PushKeyboardEnhancementFlags}, event::{KeyboardEnhancementFlags, PopKeyboardEnhancementFlags, PushKeyboardEnhancementFlags},
style::{Print, Stylize}, style::{Print, Stylize as _},
terminal::{self, Clear, ClearType, EnterAlternateScreen, LeaveAlternateScreen}, terminal::{self, Clear, ClearType, EnterAlternateScreen, LeaveAlternateScreen},
}; };
@ -50,7 +58,7 @@ lazy_static! {
/// Sets the volume timer to one, effectively flashing the audio display in lowfi's UI. /// Sets the volume timer to one, effectively flashing the audio display in lowfi's UI.
/// ///
/// The amount of frames the audio display is visible for is determined by [AUDIO_BAR_DURATION]. /// The amount of frames the audio display is visible for is determined by [`AUDIO_BAR_DURATION`].
pub fn flash_audio() { pub fn flash_audio() {
VOLUME_TIMER.store(1, Ordering::Relaxed); VOLUME_TIMER.store(1, Ordering::Relaxed);
} }
@ -96,7 +104,7 @@ impl Window {
/// Actually draws the window, with each element in `content` being on a new line. /// Actually draws the window, with each element in `content` being on a new line.
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: u16 = content.len().try_into()?;
// Note that this will have a trailing newline, which we use later. // Note that this will have a trailing newline, which we use later.
let menu: String = content.into_iter().fold(String::new(), |mut output, x| { let menu: String = content.into_iter().fold(String::new(), |mut output, x| {

View File

@ -1,10 +1,10 @@
//! Various different individual components that //! Various different individual components that
//! appear in lowfi's UI, like the progress bar. //! appear in lowfi's UI, like the progress bar.
use std::{ops::Deref, sync::Arc, time::Duration}; use std::{ops::Deref as _, sync::Arc, time::Duration};
use crossterm::style::Stylize; use crossterm::style::Stylize as _;
use unicode_segmentation::UnicodeSegmentation; use unicode_segmentation::UnicodeSegmentation as _;
use crate::{player::Player, tracks::Info}; use crate::{player::Player, tracks::Info};

View File

@ -2,7 +2,7 @@
//! using [`crossterm`]. //! using [`crossterm`].
use crossterm::event::{self, EventStream, KeyCode, KeyEventKind, KeyModifiers}; use crossterm::event::{self, EventStream, KeyCode, KeyEventKind, KeyModifiers};
use futures::{FutureExt, StreamExt}; use futures::{FutureExt as _, StreamExt as _};
use tokio::sync::mpsc::Sender; use tokio::sync::mpsc::Sender;
use crate::player::{ui, Messages}; use crate::player::{ui, Messages};

View File

@ -5,9 +5,10 @@
use std::{io::Cursor, time::Duration}; use std::{io::Cursor, time::Duration};
use bytes::Bytes; use bytes::Bytes;
use inflector::Inflector; use eyre::OptionExt as _;
use rodio::{Decoder, Source}; use inflector::Inflector as _;
use unicode_width::UnicodeWidthStr; use rodio::{Decoder, Source as _};
use unicode_width::UnicodeWidthStr as _;
use url::form_urlencoded; use url::form_urlencoded;
pub mod list; pub mod list;
@ -36,6 +37,10 @@ pub struct Info {
impl Info { impl Info {
/// Decodes a URL string into normal UTF-8. /// Decodes a URL string into normal UTF-8.
fn decode_url(text: &str) -> String { fn decode_url(text: &str) -> String {
#[expect(
clippy::tuple_array_conversions,
reason = "the tuple contains smart pointers, so it's not really practical to use `into()`"
)]
form_urlencoded::parse(text.as_bytes()) form_urlencoded::parse(text.as_bytes())
.map(|(key, val)| [key, val].concat()) .map(|(key, val)| [key, val].concat())
.collect() .collect()
@ -44,11 +49,13 @@ impl Info {
/// Formats a name with [Inflector]. /// Formats a name with [Inflector].
/// This will also strip the first few numbers that are /// This will also strip the first few numbers that are
/// usually present on most lofi tracks. /// usually present on most lofi tracks.
fn format_name(name: &str) -> String { fn format_name(name: &str) -> eyre::Result<String> {
let split = name.split('/').last().unwrap(); let split = name
.split('/')
.last()
.ok_or_eyre("split is never supposed to return nothing")?;
let stripped = split.strip_suffix(".mp3").unwrap_or(split); let stripped = split.strip_suffix(".mp3").unwrap_or(split);
let formatted = Self::decode_url(stripped) let formatted = Self::decode_url(stripped)
.to_lowercase() .to_lowercase()
.to_title_case() .to_title_case()
@ -76,28 +83,28 @@ impl Info {
// If the entire name of the track is a number, then just return it. // If the entire name of the track is a number, then just return it.
if skip == formatted.len() { if skip == formatted.len() {
formatted Ok(formatted)
} else { } else {
#[expect( #[expect(
clippy::string_slice, clippy::string_slice,
reason = "We've already checked before that the bound is at an ASCII digit." reason = "We've already checked before that the bound is at an ASCII digit."
)] )]
String::from(&formatted[skip..]) Ok(String::from(&formatted[skip..]))
} }
} }
/// Creates a new [`TrackInfo`] from a possibly raw name & decoded track data. /// Creates a new [`TrackInfo`] from a possibly raw name & decoded track data.
pub fn new(name: TrackName, decoded: &DecodedData) -> Self { pub fn new(name: TrackName, decoded: &DecodedData) -> eyre::Result<Self> {
let name = match name { let name = match name {
TrackName::Raw(raw) => Self::format_name(&raw), TrackName::Raw(raw) => Self::format_name(&raw)?,
TrackName::Formatted(formatted) => formatted, TrackName::Formatted(formatted) => formatted,
}; };
Self { Ok(Self {
duration: decoded.total_duration(), duration: decoded.total_duration(),
width: name.width(), width: name.width(),
name, name,
} })
} }
} }
@ -116,7 +123,7 @@ impl Decoded {
/// 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

@ -2,8 +2,8 @@
//! as well as obtaining track names & downloading the raw mp3 data. //! as well as obtaining track names & downloading the raw mp3 data.
use bytes::Bytes; use bytes::Bytes;
use eyre::OptionExt; use eyre::OptionExt as _;
use rand::Rng; use rand::Rng as _;
use reqwest::Client; use reqwest::Client;
use tokio::fs; use tokio::fs;
@ -15,6 +15,7 @@ use super::Track;
#[derive(Clone)] #[derive(Clone)]
pub struct List { pub struct List {
/// The "name" of the list, usually derived from a filename. /// The "name" of the list, usually derived from a filename.
#[allow(dead_code, reason = "this code may not be dead depending on features")]
pub name: String, pub name: String,
/// Just the raw file, but seperated by `/n` (newlines). /// Just the raw file, but seperated by `/n` (newlines).
@ -90,7 +91,7 @@ impl List {
if let Some(arg) = tracks { if let Some(arg) = tracks {
// Check if the track is in ~/.local/share/lowfi, in which case we'll load that. // Check if the track is in ~/.local/share/lowfi, in which case we'll load that.
let name = dirs::data_dir() let name = dirs::data_dir()
.unwrap() .ok_or_eyre("data directory not found, are you *really* running this on wasm?")?
.join("lowfi") .join("lowfi")
.join(format!("{arg}.txt")); .join(format!("{arg}.txt"));