mirror of
https://github.com/talwat/lowfi
synced 2025-01-27 19:01:27 +00:00
chore: reintroduce clippy & fix a lot of errors
This commit is contained in:
parent
04d5ed335d
commit
fe9429bfb3
44
src/main.rs
44
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};
|
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)]
|
||||||
|
10
src/play.rs
10
src/play.rs
@ -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`.
|
||||||
|
134
src/player.rs
134
src/player.rs
@ -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?;
|
||||||
|
@ -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;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -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 })
|
||||||
|
}
|
||||||
|
}
|
||||||
|
@ -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?;
|
||||||
|
@ -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 {
|
||||||
|
@ -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),
|
||||||
|
@ -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(())
|
||||||
|
@ -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 })
|
||||||
}
|
}
|
||||||
|
@ -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")))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
Loading…
x
Reference in New Issue
Block a user