diff --git a/Cargo.lock b/Cargo.lock index 55bbb79..b97b73e 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1453,7 +1453,7 @@ checksum = "a7a70ba024b9dc04c27ea2f0c0548feb474ec5c54bba33a7f72f873a39d07b24" [[package]] name = "lowfi" -version = "1.6.0" +version = "1.6.1" dependencies = [ "Inflector", "arc-swap", diff --git a/Cargo.toml b/Cargo.toml index c2850aa..3fb5db1 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "lowfi" -version = "1.6.0" +version = "1.6.1" edition = "2021" description = "An extremely simple lofi player." license = "MIT" diff --git a/src/main.rs b/src/main.rs index 278baa1..a598e90 100644 --- a/src/main.rs +++ b/src/main.rs @@ -1,44 +1,6 @@ //! 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, - clippy::arbitrary_source_item_ordering, - clippy::unused_trait_names -)] +#![warn(clippy::all, clippy::pedantic, clippy::nursery)] use clap::{Parser, Subcommand}; @@ -52,6 +14,10 @@ mod scrape; /// An extremely simple lofi player. #[derive(Parser)] #[command(about, version)] +#[allow( + clippy::struct_excessive_bools, + reason = "seƱor clippy, i assure you this is not a state machine" +)] struct Args { /// Whether to use an alternate terminal screen. #[clap(long, short)] diff --git a/src/play.rs b/src/play.rs index dd6784f..8f1534f 100644 --- a/src/play.rs +++ b/src/play.rs @@ -23,7 +23,7 @@ impl PersistentVolume { /// Retrieves the config directory. async fn config() -> eyre::Result { 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")); if !config.exists() { @@ -35,7 +35,7 @@ impl PersistentVolume { /// Returns the volume as a float from 0 to 1. pub fn float(self) -> f32 { - self.inner as f32 / 100.0 + f32::from(self.inner) / 100.0 } /// Loads the [`PersistentVolume`] from [`dirs::config_dir()`]. @@ -64,17 +64,38 @@ impl PersistentVolume { let config = Self::config().await?; 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(()) } } +/// 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 /// it when the frontend quits. pub async fn play(args: Args) -> eyre::Result<()> { // 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. let (tx, rx) = mpsc::channel(8); @@ -90,6 +111,7 @@ pub async fn play(args: Args) -> eyre::Result<()> { PersistentVolume::save(player.sink.volume()).await?; player.sink.stop(); ui.abort(); + drop(stream.0); Ok(()) } diff --git a/src/player.rs b/src/player.rs index 209cb4b..03dc2e8 100644 --- a/src/player.rs +++ b/src/player.rs @@ -2,11 +2,10 @@ //! This also has the code for the underlying //! 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 downloader::Downloader; -use libc::freopen; use reqwest::Client; use rodio::{OutputStream, OutputStreamHandle, Sink}; use tokio::{ @@ -23,7 +22,7 @@ use tokio::{ use mpris_server::{PlaybackStatus, PlayerInterface, Property}; use crate::{ - play::PersistentVolume, + play::{PersistentVolume, SendableOutputStream}, tracks::{self, list::List}, Args, }; @@ -53,6 +52,7 @@ pub enum Messages { Init, /// Unpause the [Sink]. + #[allow(dead_code, reason = "this code may not be dead depending on features")] Play, /// Pauses the [Sink]. @@ -109,20 +109,8 @@ pub struct Player { /// playback, is for now unused and is here just to keep it /// alive so the playback can function properly. _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 { /// This gets the output stream while also shutting up alsa with [libc]. /// 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. /// /// This also will load the track list & persistent volume. - pub async fn new(args: &Args) -> eyre::Result { + pub async fn new(args: &Args) -> eyre::Result<(Self, SendableOutputStream)> { // Load the volume file. let volume = PersistentVolume::load().await?; @@ -199,7 +187,7 @@ impl Player { // If we're not on Linux, then there's no problem. #[cfg(not(target_os = "linux"))] - let (_stream, handle) = OutputStream::try_default()?; + let (stream, handle) = OutputStream::try_default()?; let sink = Sink::try_new(&handle)?; if args.paused { @@ -223,10 +211,9 @@ impl Player { volume, list, _handle: handle, - _stream, }; - Ok(player) + Ok((player, SendableOutputStream(stream))) } /// This will play the next track, as well as refilling the buffer in the background. diff --git a/src/player/ui.rs b/src/player/ui.rs index 2b4ebf3..f6fb3a8 100644 --- a/src/player/ui.rs +++ b/src/player/ui.rs @@ -1,7 +1,15 @@ //! 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::{ - fmt::Write, + fmt::Write as _, io::{stdout, Stdout}, sync::{ atomic::{AtomicUsize, Ordering}, @@ -15,7 +23,7 @@ use crate::Args; use crossterm::{ cursor::{Hide, MoveTo, MoveToColumn, MoveUp, Show}, event::{KeyboardEnhancementFlags, PopKeyboardEnhancementFlags, PushKeyboardEnhancementFlags}, - style::{Print, Stylize}, + style::{Print, Stylize as _}, 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. /// -/// 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() { 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. pub fn draw(&mut self, content: Vec) -> 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. let menu: String = content.into_iter().fold(String::new(), |mut output, x| { diff --git a/src/player/ui/components.rs b/src/player/ui/components.rs index 85cb058..42ed7bc 100644 --- a/src/player/ui/components.rs +++ b/src/player/ui/components.rs @@ -1,10 +1,10 @@ //! 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 as _, sync::Arc, time::Duration}; -use crossterm::style::Stylize; -use unicode_segmentation::UnicodeSegmentation; +use crossterm::style::Stylize as _; +use unicode_segmentation::UnicodeSegmentation as _; use crate::{player::Player, tracks::Info}; diff --git a/src/player/ui/input.rs b/src/player/ui/input.rs index 3352d3e..00f4ae1 100644 --- a/src/player/ui/input.rs +++ b/src/player/ui/input.rs @@ -2,7 +2,7 @@ //! using [`crossterm`]. use crossterm::event::{self, EventStream, KeyCode, KeyEventKind, KeyModifiers}; -use futures::{FutureExt, StreamExt}; +use futures::{FutureExt as _, StreamExt as _}; use tokio::sync::mpsc::Sender; use crate::player::{ui, Messages}; diff --git a/src/tracks.rs b/src/tracks.rs index 2997f95..2167b10 100644 --- a/src/tracks.rs +++ b/src/tracks.rs @@ -5,9 +5,10 @@ use std::{io::Cursor, time::Duration}; use bytes::Bytes; -use inflector::Inflector; -use rodio::{Decoder, Source}; -use unicode_width::UnicodeWidthStr; +use eyre::OptionExt as _; +use inflector::Inflector as _; +use rodio::{Decoder, Source as _}; +use unicode_width::UnicodeWidthStr as _; use url::form_urlencoded; pub mod list; @@ -36,6 +37,10 @@ pub struct Info { impl Info { /// Decodes a URL string into normal UTF-8. 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()) .map(|(key, val)| [key, val].concat()) .collect() @@ -44,11 +49,13 @@ impl Info { /// 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: &str) -> String { - let split = name.split('/').last().unwrap(); + fn format_name(name: &str) -> eyre::Result { + 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 formatted = Self::decode_url(stripped) .to_lowercase() .to_title_case() @@ -76,28 +83,28 @@ impl Info { // If the entire name of the track is a number, then just return it. if skip == formatted.len() { - formatted + Ok(formatted) } else { #[expect( clippy::string_slice, 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. - pub fn new(name: TrackName, decoded: &DecodedData) -> Self { + pub fn new(name: TrackName, decoded: &DecodedData) -> eyre::Result { let name = match name { - TrackName::Raw(raw) => Self::format_name(&raw), + TrackName::Raw(raw) => Self::format_name(&raw)?, TrackName::Formatted(formatted) => formatted, }; - Self { + Ok(Self { duration: decoded.total_duration(), width: name.width(), name, - } + }) } } @@ -116,7 +123,7 @@ impl Decoded { /// This is equivalent to [`Track::decode`]. pub fn new(track: Track) -> eyre::Result { 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 }) } diff --git a/src/tracks/list.rs b/src/tracks/list.rs index 08be0bd..a3f845e 100644 --- a/src/tracks/list.rs +++ b/src/tracks/list.rs @@ -2,8 +2,8 @@ //! as well as obtaining track names & downloading the raw mp3 data. use bytes::Bytes; -use eyre::OptionExt; -use rand::Rng; +use eyre::OptionExt as _; +use rand::Rng as _; use reqwest::Client; use tokio::fs; @@ -15,6 +15,7 @@ use super::Track; #[derive(Clone)] pub struct List { /// 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, /// Just the raw file, but seperated by `/n` (newlines). @@ -90,7 +91,7 @@ impl List { if let Some(arg) = tracks { // Check if the track is in ~/.local/share/lowfi, in which case we'll load that. let name = dirs::data_dir() - .unwrap() + .ok_or_eyre("data directory not found, are you *really* running this on wasm?")? .join("lowfi") .join(format!("{arg}.txt"));