diff --git a/src/main.rs b/src/main.rs index 20f16a1..92aab0d 100644 --- a/src/main.rs +++ b/src/main.rs @@ -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}; mod play; mod player; -mod scrape; mod tracks; +#[allow(clippy::all, clippy::pedantic, clippy::nursery, clippy::restriction)] +mod scrape; + /// An extremely simple lofi player. #[derive(Parser)] #[command(about, version)] diff --git a/src/play.rs b/src/play.rs index 85835d7..70ab6e4 100644 --- a/src/play.rs +++ b/src/play.rs @@ -34,11 +34,11 @@ impl PersistentVolume { } /// 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 } - /// Loads the [PersistentVolume] from [dirs::config_dir()]. + /// Loads the [`PersistentVolume`] from [`dirs::config_dir()`]. pub async fn load() -> eyre::Result { let config = Self::config().await?; let volume = config.join(PathBuf::from("volume.txt")); @@ -47,16 +47,16 @@ impl PersistentVolume { let volume = if volume.exists() { let contents = fs::read_to_string(volume).await?; let trimmed = contents.trim(); - let stripped = trimmed.strip_suffix("%").unwrap_or(&trimmed); + let stripped = trimmed.strip_suffix("%").unwrap_or(trimmed); stripped .parse() - .map_err(|_| eyre!("volume.txt file is invalid"))? + .map_err(|_error| eyre!("volume.txt file is invalid"))? } else { fs::write(&volume, "100").await?; 100u16 }; - Ok(PersistentVolume { inner: volume }) + Ok(Self { inner: volume }) } /// Saves `volume` to `volume.txt`. diff --git a/src/player.rs b/src/player.rs index 5ebac3b..b8cec56 100644 --- a/src/player.rs +++ b/src/player.rs @@ -16,10 +16,11 @@ use tokio::{ RwLock, }, task, + time::sleep, }; #[cfg(feature = "mpris")] -use mpris_server::PlayerInterface; +use mpris_server::{PlaybackStatus, PlayerInterface}; use crate::{ play::PersistentVolume, @@ -89,13 +90,10 @@ pub struct Player { /// This is [`None`] when lowfi is buffering/loading. current: ArcSwapOption, - /// This is the MPRIS server, which is initialized later on in the - /// user interface. - #[cfg(feature = "mpris")] - mpris: tokio::sync::OnceCell>, - - /// The tracks, which is a [VecDeque] that holds + /// The tracks, which is a [`VecDeque`] that holds /// *undecoded* [Track]s. + /// + /// This is populated specifically by the [Downloader]. tracks: RwLock>, /// The actual list of tracks to be played. @@ -104,16 +102,16 @@ pub struct Player { /// The initial volume level. 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. 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 /// alive so the playback can function properly. _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. _stream: OutputStream, } @@ -143,7 +141,9 @@ impl Player { // First redirect to /dev/null, which basically silences alsa. let null = CString::new("/dev/null")?.as_ptr(); // 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. 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. let tty = CString::new("/dev/tty")?.as_ptr(); // SAFETY: See the first call to `freopen`. - unsafe { freopen(tty, mode, stderr) }; + unsafe { + freopen(tty, mode, stderr); + } Ok((stream, handle)) } /// 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))); - - Ok(()) } /// 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. 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. @@ -213,9 +213,6 @@ impl Player { list, _handle: handle, _stream, - - #[cfg(feature = "mpris")] - mpris: tokio::sync::OnceCell::new(), }; Ok(player) @@ -225,49 +222,28 @@ impl Player { /// /// This will also set `current` to the newly loaded song. pub async fn next(&self) -> eyre::Result { - let track = match self.tracks.write().await.pop_front() { - Some(x) => x, + let track = if let Some(track) = self.tracks.write().await.pop_front() { + track + } else { // If the queue is completely empty, then fallback to simply getting a new track. // This is relevant particularly at the first song. - None => { - // Serves as an indicator that the queue is "loading". - // 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. - self.current.store(None); - self.list.random(&self.client).await? - } + // Serves as an indicator that the queue is "loading". + // 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. + self.current.store(None); + + self.list.random(&self.client).await? }; let decoded = track.decode()?; // Set the current track. - self.set_current(decoded.info.clone()).await?; + self.set_current(decoded.info.clone()); Ok(decoded) } - /// Shorthand to emit a `PropertiesChanged` signal, like when pausing/unpausing. - #[cfg(feature = "mpris")] - async fn mpris_changed( - &self, - properties: impl IntoIterator, - ) -> 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 also notifies the background thread to get to work, and will send `TryAgain` @@ -296,14 +272,14 @@ impl Player { Downloader::notify(&itx).await?; // Notify the audio server that the next song has actually been downloaded. - tx.send(Messages::NewSong).await? + tx.send(Messages::NewSong).await?; } Err(error) => { if !error.downcast::()?.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, mut rx: Receiver, ) -> 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. - let downloader = Downloader::new(player.clone()); - let (itx, downloader) = downloader.start().await; + let downloader = Downloader::new(Arc::clone(&player)); + let (itx, downloader) = downloader.start(); // Start buffering tracks immediately. Downloader::notify(&itx).await?; @@ -329,12 +314,13 @@ impl Player { // Set the initial sink volume to the one specified. player.set_volume(player.volume.float()); - // Whether the last signal was a `NewSong`. - // This is helpful, since we only want to autoplay - // if there hasn't been any manual intervention. + // Whether the last signal was a `NewSong`. This is helpful, since we + // only want to autoplay 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; - // TODO: Clean mpris_changed calls & streamline them somehow. loop { 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 // 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, }; @@ -370,27 +356,23 @@ impl Player { // Handle the rest of the signal in the background, // 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 => { player.sink.play(); #[cfg(feature = "mpris")] - player - .mpris_changed(vec![mpris_server::Property::PlaybackStatus( - mpris_server::PlaybackStatus::Playing, - )]) - .await?; + mpris.playback(PlaybackStatus::Playing).await?; } Messages::Pause => { player.sink.pause(); #[cfg(feature = "mpris")] - player - .mpris_changed(vec![mpris_server::Property::PlaybackStatus( - mpris_server::PlaybackStatus::Paused, - )]) - .await?; + mpris.playback(PlaybackStatus::Paused).await?; } Messages::PlayPause => { if player.sink.is_paused() { @@ -400,18 +382,16 @@ impl Player { } #[cfg(feature = "mpris")] - player - .mpris_changed(vec![mpris_server::Property::PlaybackStatus( - player.mpris_innner().playback_status().await?, - )]) + mpris + .playback(mpris.player().playback_status().await?) .await?; } Messages::ChangeVolume(change) => { player.set_volume(player.sink.volume() + change); #[cfg(feature = "mpris")] - player - .mpris_changed(vec![mpris_server::Property::Volume( + mpris + .changed(vec![mpris_server::Property::Volume( player.sink.volume().into(), )]) .await?; @@ -425,13 +405,11 @@ impl Player { new = true; #[cfg(feature = "mpris")] - player - .mpris_changed(vec![ - mpris_server::Property::Metadata( - player.mpris_innner().metadata().await?, - ), + mpris + .changed(vec![ + mpris_server::Property::Metadata(mpris.player().metadata().await?), mpris_server::Property::PlaybackStatus( - player.mpris_innner().playback_status().await?, + mpris.player().playback_status().await?, ), ]) .await?; diff --git a/src/player/downloader.rs b/src/player/downloader.rs index e276e5a..c74854a 100644 --- a/src/player/downloader.rs +++ b/src/player/downloader.rs @@ -5,6 +5,7 @@ use std::sync::Arc; use tokio::{ sync::mpsc::{self, Receiver, Sender}, task::{self, JoinHandle}, + time::sleep, }; use super::{Player, BUFFER_SIZE, TIMEOUT}; @@ -42,7 +43,7 @@ impl Downloader { } /// Actually starts & consumes the [Downloader]. - pub async fn start(mut self) -> (Sender<()>, JoinHandle<()>) { + pub fn start(mut self) -> (Sender<()>, JoinHandle<()>) { ( self.tx, task::spawn(async move { @@ -54,7 +55,7 @@ impl Downloader { Ok(track) => self.player.tracks.write().await.push_back(track), Err(error) => { if !error.is_timeout() { - tokio::time::sleep(TIMEOUT).await; + sleep(TIMEOUT).await; } } } diff --git a/src/player/mpris.rs b/src/player/mpris.rs index ddfde16..8eeaeec 100644 --- a/src/player/mpris.rs +++ b/src/player/mpris.rs @@ -1,7 +1,7 @@ use std::sync::Arc; use mpris_server::{ - zbus::{fdo, Result}, + zbus::{self, fdo, Result}, LoopStatus, Metadata, PlaybackRate, PlaybackStatus, PlayerInterface, RootInterface, Time, TrackId, Volume, }; @@ -11,7 +11,7 @@ use super::Messages; const ERROR: fdo::Error = fdo::Error::Failed(String::new()); -/// The actual MPRIS server. +/// The actual MPRIS player. pub struct Player { pub player: Arc, pub sender: Sender, @@ -209,3 +209,42 @@ impl PlayerInterface for Player { 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, +} + +impl Server { + /// Shorthand to emit a `PropertiesChanged` signal, like when pausing/unpausing. + pub async fn changed( + &self, + properties: impl IntoIterator, + ) -> 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, sender: Sender) -> eyre::Result { + let server = + mpris_server::Server::new("lowfi", crate::player::mpris::Player { player, sender }) + .await + .unwrap(); + + Ok(Self { inner: server }) + } +} diff --git a/src/player/ui.rs b/src/player/ui.rs index 8e211d2..a084a60 100644 --- a/src/player/ui.rs +++ b/src/player/ui.rs @@ -1,6 +1,7 @@ //! The module which manages all user interface, including inputs. use std::{ + fmt::Write, io::{stdout, Stdout}, sync::{ atomic::{AtomicUsize, Ordering}, @@ -55,7 +56,11 @@ lazy_static! { /// The main purpose of this struct is just to add the fancy border, /// as well as clear the screen before drawing. 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], + + /// The output, currently just an [`Stdout`]. out: Stdout, } @@ -76,10 +81,11 @@ impl Window { pub fn draw(&mut self, content: Vec) -> eyre::Result<()> { let len = content.len() as u16; - let menu: String = content - .into_iter() - .map(|x| format!("│ {} │\r\n", x.reset()).to_string()) - .collect(); + let menu: String = content.into_iter().fold(String::new(), |mut output, x| { + write!(output, "│ {} │\r\n", x.reset()).unwrap(); + + output + }); // We're doing this because Windows is stupid and can't stand // writing to the last line repeatedly. Again, it's stupid. @@ -155,19 +161,6 @@ async fn interface(player: Arc, 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, - sender: Sender, -) -> mpris_server::Server { - mpris_server::Server::new("lowfi", crate::player::mpris::Player { player, sender }) - .await - .unwrap() -} - /// Represents the terminal environment, and is used to properly /// initialize and clean up the terminal. pub struct Environment { @@ -230,7 +223,7 @@ impl 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) { // Well, we're dropping it, so it doesn't really matter if there's an error. let _ = self.cleanup(); @@ -239,19 +232,10 @@ impl Drop for Environment { /// 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. pub async fn start(player: Arc, sender: Sender, args: Args) -> eyre::Result<()> { 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)); input::listen(sender.clone()).await?; diff --git a/src/player/ui/components.rs b/src/player/ui/components.rs index d24ce82..015cc16 100644 --- a/src/player/ui/components.rs +++ b/src/player/ui/components.rs @@ -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 crossterm::style::Stylize; @@ -9,7 +12,7 @@ pub fn format_duration(duration: &Duration) -> String { let seconds = 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. @@ -55,8 +58,13 @@ pub fn audio_bar(volume: f32, percentage: &str, width: usize) -> String { /// This represents the main "action" bars state. enum ActionBar { + /// When the app is currently displaying "paused". Paused(Info), + + /// When the app is currently displaying "playing". Playing(Info), + + /// When the app is currently displaying "loading". Loading, } @@ -72,12 +80,7 @@ impl ActionBar { subject.map_or_else( || (word.to_owned(), word.len()), - |(subject, len)| { - ( - format!("{} {}", word, subject.clone().bold()), - word.len() + 1 + len, - ) - }, + |(subject, len)| (format!("{} {}", word, subject.bold()), word.len() + 1 + len), ) } } @@ -97,6 +100,8 @@ pub fn action(player: &Player, current: Option<&Arc>, width: usize) -> Str }) .format(); + // TODO: Deal with dangerous string slicing. + #[allow(clippy::string_slice)] if len > width { format!("{}...", &main[..=width]) } else { diff --git a/src/player/ui/input.rs b/src/player/ui/input.rs index 1d6247d..ded9505 100644 --- a/src/player/ui/input.rs +++ b/src/player/ui/input.rs @@ -1,3 +1,6 @@ +//! Responsible for specifically recieving terminal input +//! using [`crossterm`]. + use std::sync::atomic::Ordering; use crossterm::event::{self, EventStream, KeyCode, KeyEventKind, KeyModifiers}; @@ -48,9 +51,9 @@ pub async fn listen(sender: Sender) -> eyre::Result<()> { }, // Media keys KeyCode::Media(media) => match media { - event::MediaKeyCode::Play => Messages::PlayPause, - event::MediaKeyCode::Pause => Messages::PlayPause, - event::MediaKeyCode::PlayPause => Messages::PlayPause, + event::MediaKeyCode::Pause + | event::MediaKeyCode::Play + | event::MediaKeyCode::PlayPause => Messages::PlayPause, event::MediaKeyCode::Stop => Messages::Pause, event::MediaKeyCode::TrackNext => Messages::Next, event::MediaKeyCode::LowerVolume => Messages::ChangeVolume(-0.1), diff --git a/src/scrape.rs b/src/scrape.rs index 9942d8a..a811a51 100644 --- a/src/scrape.rs +++ b/src/scrape.rs @@ -82,7 +82,7 @@ async fn scan(extension: &str, include_full: bool) -> eyre::Result> pub async fn scrape(extension: String, include_full: bool) -> eyre::Result<()> { let files = scan(&extension, include_full).await?; for file in files { - println!("{}", file); + println!("{file}"); } Ok(()) diff --git a/src/tracks.rs b/src/tracks.rs index 030b8bf..3ca7c62 100644 --- a/src/tracks.rs +++ b/src/tracks.rs @@ -15,11 +15,11 @@ pub mod list; /// Just a shorthand for a decoded [Bytes]. pub type DecodedData = Decoder>; -/// 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 /// from the decoded data and not from the raw data. -#[derive(Debug, PartialEq, Clone)] +#[derive(Debug, Eq, PartialEq, Clone)] pub struct Info { /// This is a formatted name, so it doesn't include the full path. pub name: String, @@ -46,7 +46,7 @@ impl Info { /// usually present on most lofi tracks. fn format_name(name: &str) -> String { let formatted = Self::decode_url( - name.split("/") + name.split('/') .last() .unwrap() .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..]) } /// Creates a new [`TrackInfo`] from a raw name & decoded track data. - pub fn new(name: String, decoded: &DecodedData) -> Self { - let name = Self::format_name(&name); + pub fn new(name: &str, decoded: &DecodedData) -> Self { + let name = Self::format_name(name); Self { duration: decoded.total_duration(), @@ -103,10 +104,10 @@ pub struct Decoded { impl Decoded { /// Creates a new track. - /// This is equivalent to [Track::decode]. + /// 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 1f641eb..f1a0220 100644 --- a/src/tracks/list.rs +++ b/src/tracks/list.rs @@ -32,7 +32,7 @@ impl List { // 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. 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. @@ -59,13 +59,13 @@ impl List { } /// Parses text into a [List]. - pub fn new(text: &str) -> eyre::Result { + pub fn new(text: &str) -> Self { let lines: Vec = text .split_ascii_whitespace() - .map(|x| x.to_owned()) + .map(ToOwned::to_owned) .collect(); - Ok(Self { lines }) + Self { lines } } /// Reads a [List] from the filesystem using the CLI argument provided. @@ -84,9 +84,9 @@ impl List { fs::read_to_string(arg).await? }; - List::new(&raw) + Ok(Self::new(&raw)) } else { - List::new(include_str!("../../data/lofigirl.txt")) + Ok(Self::new(include_str!("../../data/lofigirl.txt"))) } } }