mirror of
https://github.com/talwat/lowfi
synced 2025-08-18 15:43:01 +00:00
chore: even more error handling improvements
This commit is contained in:
parent
9f7c895154
commit
1884d2ebed
22
Cargo.lock
generated
22
Cargo.lock
generated
@ -433,6 +433,19 @@ version = "0.7.4"
|
|||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "f46ad14479a25103f283c0f10005961cf086d8dc42205bb44c46ac563475dca6"
|
checksum = "f46ad14479a25103f283c0f10005961cf086d8dc42205bb44c46ac563475dca6"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "color-eyre"
|
||||||
|
version = "0.6.5"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "e5920befb47832a6d61ee3a3a846565cfa39b331331e68a3b1d1116630f2f26d"
|
||||||
|
dependencies = [
|
||||||
|
"backtrace",
|
||||||
|
"eyre",
|
||||||
|
"indenter",
|
||||||
|
"once_cell",
|
||||||
|
"owo-colors",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "colorchoice"
|
name = "colorchoice"
|
||||||
version = "1.0.3"
|
version = "1.0.3"
|
||||||
@ -1392,6 +1405,7 @@ dependencies = [
|
|||||||
"arc-swap",
|
"arc-swap",
|
||||||
"bytes",
|
"bytes",
|
||||||
"clap",
|
"clap",
|
||||||
|
"color-eyre",
|
||||||
"crossterm",
|
"crossterm",
|
||||||
"dirs",
|
"dirs",
|
||||||
"eyre",
|
"eyre",
|
||||||
@ -1777,6 +1791,12 @@ dependencies = [
|
|||||||
"pin-project-lite",
|
"pin-project-lite",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "owo-colors"
|
||||||
|
version = "4.2.2"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "48dd4f4a2c8405440fd0462561f0e5806bd0f77e86f51c761481bdd4018b545e"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "parking"
|
name = "parking"
|
||||||
version = "2.2.1"
|
version = "2.2.1"
|
||||||
@ -3113,7 +3133,7 @@ version = "0.1.9"
|
|||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "cf221c93e13a30d793f7645a0e7762c55d169dbb0a49671918a2319d289b10bb"
|
checksum = "cf221c93e13a30d793f7645a0e7762c55d169dbb0a49671918a2319d289b10bb"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"windows-sys 0.48.0",
|
"windows-sys 0.59.0",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
|
@ -25,6 +25,8 @@ extra-audio-formats = ["rodio/default"]
|
|||||||
clap = { version = "4.5.21", features = ["derive", "cargo"] }
|
clap = { version = "4.5.21", features = ["derive", "cargo"] }
|
||||||
eyre = { version = "0.6.12" }
|
eyre = { version = "0.6.12" }
|
||||||
rand = "0.8.5"
|
rand = "0.8.5"
|
||||||
|
thiserror = "2.0.12"
|
||||||
|
color-eyre = { version = "0.6.5", default-features = false }
|
||||||
|
|
||||||
# Async
|
# Async
|
||||||
tokio = { version = "1.41.1", features = [
|
tokio = { version = "1.41.1", features = [
|
||||||
@ -52,4 +54,3 @@ lazy_static = "1.5.0"
|
|||||||
libc = "0.2.167"
|
libc = "0.2.167"
|
||||||
url = "2.5.4"
|
url = "2.5.4"
|
||||||
unicode-segmentation = "1.12.0"
|
unicode-segmentation = "1.12.0"
|
||||||
thiserror = "2.0.12"
|
|
||||||
|
@ -219,13 +219,13 @@ For example, in this list:
|
|||||||
https://lofigirl.com/wp-content/uploads/
|
https://lofigirl.com/wp-content/uploads/
|
||||||
2023/06/Foudroie-Finding-The-Edge-V2.mp3
|
2023/06/Foudroie-Finding-The-Edge-V2.mp3
|
||||||
2023/04/2-In-Front-Of-Me.mp3
|
2023/04/2-In-Front-Of-Me.mp3
|
||||||
https://file-examples.com/storage/fea570b16e6703ef79e65b4/2017/11/file_example_MP3_5MG.mp3
|
https://file-examples.com/storage/fe85f7a43b689349d9c8f18/2017/11/file_example_MP3_1MG.mp3
|
||||||
```
|
```
|
||||||
|
|
||||||
lowfi would download these three URLs:
|
lowfi would download these three URLs:
|
||||||
|
|
||||||
- `https://lofigirl.com/wp-content/uploads/2023/06/Foudroie-Finding-The-Edge-V2.mp3`
|
- `https://lofigirl.com/wp-content/uploads/2023/06/Foudroie-Finding-The-Edge-V2.mp3`
|
||||||
- `https://file-examples.com/storage/fea570b16e6703ef79e65b4/2017/11/file_example_MP3_5MG.mp3`
|
- `https://file-examples.com/storage/fe85f7a43b689349d9c8f18/2017/11/file_example_MP3_1MG.mp3`
|
||||||
- `https://lofigirl.com/wp-content/uploads/2023/04/2-In-Front-Of-Me.mp3`
|
- `https://lofigirl.com/wp-content/uploads/2023/04/2-In-Front-Of-Me.mp3`
|
||||||
|
|
||||||
Additionally, you may also specify a custom display name for the track which is indicated by a `!`.
|
Additionally, you may also specify a custom display name for the track which is indicated by a `!`.
|
||||||
|
@ -1,16 +0,0 @@
|
|||||||
https://archive.org/download/jack-stauber-s-micropop-extended-micropops/Jack%20Stauber-%27s%20Micropop%20-%20
|
|
||||||
Al%20Dente.mp3
|
|
||||||
Baby%20Hotline.mp3
|
|
||||||
Cupid.mp3
|
|
||||||
Deploy.mp3
|
|
||||||
Dinner%20Is%20Not%20Over.mp3
|
|
||||||
Fighter.mp3
|
|
||||||
Inchman.mp3
|
|
||||||
Keyman.mp3
|
|
||||||
Out%20the%20Ox.mp3
|
|
||||||
Tea%20Errors.mp3
|
|
||||||
The%20Ballad%20of%20Hamantha.mp3
|
|
||||||
There%27s%20Something%20Happening.mp3
|
|
||||||
Those%20Eggs%20Aren%27t%20Dippy%20.mp3
|
|
||||||
Today%20Today.mp3
|
|
||||||
Two%20Time.mp3
|
|
@ -1,4 +1,4 @@
|
|||||||
https://lofigirl.com/wp-content/uploads/
|
https://lofigirl.com/wp-content/uploads/
|
||||||
2023/06/Foudroie-Finding-The-Edge-V2.mp3
|
2023/06/Foudroie-Finding-The-Edge-V2.mp3
|
||||||
2023/04/2-In-Front-Of-Me.mp3
|
2023/04/2-In-Front-Of-Me.mp3
|
||||||
https://file-examples.com/storage/fea570b16e6703ef79e65b4/2017/11/file_example_MP3_5MG.mp3
|
https://file-examples.com/storage/fe85f7a43b689349d9c8f18/2017/11/file_example_MP3_1MG.mp3
|
21
src/main.rs
21
src/main.rs
@ -2,10 +2,8 @@
|
|||||||
|
|
||||||
#![warn(clippy::all, clippy::pedantic, clippy::nursery)]
|
#![warn(clippy::all, clippy::pedantic, clippy::nursery)]
|
||||||
|
|
||||||
use std::path::PathBuf;
|
|
||||||
|
|
||||||
use clap::{Parser, Subcommand};
|
use clap::{Parser, Subcommand};
|
||||||
use eyre::OptionExt;
|
use std::path::PathBuf;
|
||||||
|
|
||||||
mod messages;
|
mod messages;
|
||||||
mod play;
|
mod play;
|
||||||
@ -81,9 +79,9 @@ enum Commands {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// Gets lowfi's data directory.
|
/// Gets lowfi's data directory.
|
||||||
pub fn data_dir() -> eyre::Result<PathBuf> {
|
pub fn data_dir() -> eyre::Result<PathBuf, player::Error> {
|
||||||
let dir = dirs::data_dir()
|
let dir = dirs::data_dir()
|
||||||
.ok_or_eyre("data directory not found, are you *really* running this on wasm?")?
|
.ok_or(player::Error::DataDir)?
|
||||||
.join("lowfi");
|
.join("lowfi");
|
||||||
|
|
||||||
Ok(dir)
|
Ok(dir)
|
||||||
@ -91,8 +89,7 @@ pub fn data_dir() -> eyre::Result<PathBuf> {
|
|||||||
|
|
||||||
#[tokio::main]
|
#[tokio::main]
|
||||||
async fn main() -> eyre::Result<()> {
|
async fn main() -> eyre::Result<()> {
|
||||||
#[cfg(target_os = "android")]
|
color_eyre::install()?;
|
||||||
compile_error!("Android Audio API not supported due to threading shenanigans");
|
|
||||||
|
|
||||||
let cli = Args::parse();
|
let cli = Args::parse();
|
||||||
|
|
||||||
@ -100,12 +97,14 @@ async fn main() -> eyre::Result<()> {
|
|||||||
match command {
|
match command {
|
||||||
// TODO: Actually distinguish between sources.
|
// TODO: Actually distinguish between sources.
|
||||||
Commands::Scrape {
|
Commands::Scrape {
|
||||||
source,
|
source: _,
|
||||||
extension,
|
extension,
|
||||||
include_full,
|
include_full,
|
||||||
} => scrapers::lofigirl::scrape(extension, include_full).await,
|
} => scrapers::lofigirl::scrape(extension, include_full).await?,
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
play::play(cli).await
|
play::play(cli).await?;
|
||||||
}
|
};
|
||||||
|
|
||||||
|
Ok(())
|
||||||
}
|
}
|
||||||
|
@ -1,6 +1,6 @@
|
|||||||
/// Handles communication between the frontend & audio player.
|
/// Handles communication between the frontend & audio player.
|
||||||
#[derive(PartialEq, Debug, Clone, Copy)]
|
#[derive(PartialEq, Debug, Clone, Copy)]
|
||||||
pub enum Messages {
|
pub enum Message {
|
||||||
/// Notifies the audio server that it should update the track.
|
/// Notifies the audio server that it should update the track.
|
||||||
Next,
|
Next,
|
||||||
|
|
||||||
|
112
src/play.rs
112
src/play.rs
@ -1,89 +1,42 @@
|
|||||||
//! Responsible for the basic initialization & shutdown of the audio server & frontend.
|
//! Responsible for the basic initialization & shutdown of the audio server & frontend.
|
||||||
|
|
||||||
use std::env;
|
use crossterm::cursor::Show;
|
||||||
|
use crossterm::event::PopKeyboardEnhancementFlags;
|
||||||
|
use crossterm::terminal::{self, Clear, ClearType};
|
||||||
use std::io::{stdout, IsTerminal};
|
use std::io::{stdout, IsTerminal};
|
||||||
use std::path::PathBuf;
|
use std::process::exit;
|
||||||
use std::sync::Arc;
|
use std::sync::Arc;
|
||||||
|
use std::{env, panic};
|
||||||
use eyre::eyre;
|
|
||||||
use tokio::fs;
|
|
||||||
use tokio::{sync::mpsc, task};
|
use tokio::{sync::mpsc, task};
|
||||||
|
|
||||||
use crate::messages::Messages;
|
use crate::messages::Message;
|
||||||
use crate::player::ui;
|
use crate::player::persistent_volume::PersistentVolume;
|
||||||
use crate::player::Player;
|
use crate::player::Player;
|
||||||
|
use crate::player::{self, ui};
|
||||||
use crate::Args;
|
use crate::Args;
|
||||||
|
|
||||||
/// This is the representation of the persistent volume,
|
|
||||||
/// which is loaded at startup and saved on shutdown.
|
|
||||||
#[derive(Clone, Copy)]
|
|
||||||
pub struct PersistentVolume {
|
|
||||||
/// The volume, as a percentage.
|
|
||||||
inner: u16,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl PersistentVolume {
|
|
||||||
/// Retrieves the config directory.
|
|
||||||
async fn config() -> eyre::Result<PathBuf> {
|
|
||||||
let config = dirs::config_dir()
|
|
||||||
.ok_or_else(|| eyre!("Couldn't find config directory"))?
|
|
||||||
.join(PathBuf::from("lowfi"));
|
|
||||||
|
|
||||||
if !config.exists() {
|
|
||||||
fs::create_dir_all(&config).await?;
|
|
||||||
}
|
|
||||||
|
|
||||||
Ok(config)
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Returns the volume as a float from 0 to 1.
|
|
||||||
pub fn float(self) -> f32 {
|
|
||||||
f32::from(self.inner) / 100.0
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Loads the [`PersistentVolume`] from [`dirs::config_dir()`].
|
|
||||||
pub async fn load() -> eyre::Result<Self> {
|
|
||||||
let config = Self::config().await?;
|
|
||||||
let volume = config.join(PathBuf::from("volume.txt"));
|
|
||||||
|
|
||||||
// Basically just read from the volume file if it exists, otherwise return 100.
|
|
||||||
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);
|
|
||||||
stripped
|
|
||||||
.parse()
|
|
||||||
.map_err(|_error| eyre!("volume.txt file is invalid"))?
|
|
||||||
} else {
|
|
||||||
fs::write(&volume, "100").await?;
|
|
||||||
100u16
|
|
||||||
};
|
|
||||||
|
|
||||||
Ok(Self { inner: volume })
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Saves `volume` to `volume.txt`.
|
|
||||||
pub async fn save(volume: f32) -> eyre::Result<()> {
|
|
||||||
let config = Self::config().await?;
|
|
||||||
let path = config.join(PathBuf::from("volume.txt"));
|
|
||||||
|
|
||||||
// Already rounded & absolute, therefore this should be safe.
|
|
||||||
#[expect(
|
|
||||||
clippy::as_conversions,
|
|
||||||
clippy::cast_sign_loss,
|
|
||||||
clippy::cast_possible_truncation
|
|
||||||
)]
|
|
||||||
let percentage = (volume * 100.0).abs().round() as u16;
|
|
||||||
|
|
||||||
fs::write(path, percentage.to_string()).await?;
|
|
||||||
|
|
||||||
Ok(())
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// 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<(), player::Error> {
|
||||||
|
// TODO: This isn't a great way of doing things,
|
||||||
|
// but it's better than vanilla behaviour at least.
|
||||||
|
let eyre_hook = panic::take_hook();
|
||||||
|
|
||||||
|
panic::set_hook(Box::new(move |x| {
|
||||||
|
let mut lock = stdout().lock();
|
||||||
|
crossterm::execute!(
|
||||||
|
lock,
|
||||||
|
Clear(ClearType::FromCursorDown),
|
||||||
|
Show,
|
||||||
|
PopKeyboardEnhancementFlags
|
||||||
|
)
|
||||||
|
.unwrap();
|
||||||
|
terminal::disable_raw_mode().unwrap();
|
||||||
|
|
||||||
|
eyre_hook(x);
|
||||||
|
exit(1)
|
||||||
|
}));
|
||||||
|
|
||||||
// Actually initializes the player.
|
// Actually initializes the player.
|
||||||
// Stream kept here in the master thread to keep it alive.
|
// Stream kept here in the master thread to keep it alive.
|
||||||
let (player, stream) = Player::new(&args).await?;
|
let (player, stream) = Player::new(&args).await?;
|
||||||
@ -102,16 +55,19 @@ pub async fn play(args: Args) -> eyre::Result<()> {
|
|||||||
};
|
};
|
||||||
|
|
||||||
// Sends the player an "init" signal telling it to start playing a song straight away.
|
// Sends the player an "init" signal telling it to start playing a song straight away.
|
||||||
tx.send(Messages::Init).await?;
|
tx.send(Message::Init).await?;
|
||||||
|
|
||||||
// Actually starts the player.
|
// Actually starts the player.
|
||||||
Player::play(Arc::clone(&player), tx.clone(), rx, args.debug).await?;
|
Player::play(Arc::clone(&player), tx.clone(), rx, args.debug).await?;
|
||||||
|
|
||||||
// Save the volume.txt file for the next session.
|
// Save the volume.txt file for the next session.
|
||||||
PersistentVolume::save(player.sink.volume()).await?;
|
PersistentVolume::save(player.sink.volume())
|
||||||
|
.await
|
||||||
|
.map_err(player::Error::PersistentVolumeSave)?;
|
||||||
|
|
||||||
drop(stream);
|
drop(stream);
|
||||||
player.sink.stop();
|
player.sink.stop();
|
||||||
ui.and_then(|x| Some(x.abort()));
|
ui.map(|x| x.abort());
|
||||||
|
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
@ -13,7 +13,6 @@ use std::{
|
|||||||
|
|
||||||
use arc_swap::ArcSwapOption;
|
use arc_swap::ArcSwapOption;
|
||||||
use downloader::Downloader;
|
use downloader::Downloader;
|
||||||
use eyre::Context;
|
|
||||||
use reqwest::Client;
|
use reqwest::Client;
|
||||||
use rodio::{OutputStream, OutputStreamBuilder, Sink};
|
use rodio::{OutputStream, OutputStreamBuilder, Sink};
|
||||||
use tokio::{
|
use tokio::{
|
||||||
@ -29,8 +28,8 @@ use tokio::{
|
|||||||
use mpris_server::{PlaybackStatus, PlayerInterface, Property};
|
use mpris_server::{PlaybackStatus, PlayerInterface, Property};
|
||||||
|
|
||||||
use crate::{
|
use crate::{
|
||||||
messages::Messages,
|
messages::Message,
|
||||||
play::PersistentVolume,
|
player::{self, persistent_volume::PersistentVolume},
|
||||||
tracks::{self, list::List},
|
tracks::{self, list::List},
|
||||||
Args,
|
Args,
|
||||||
};
|
};
|
||||||
@ -38,9 +37,13 @@ use crate::{
|
|||||||
pub mod audio;
|
pub mod audio;
|
||||||
pub mod bookmark;
|
pub mod bookmark;
|
||||||
pub mod downloader;
|
pub mod downloader;
|
||||||
|
pub mod error;
|
||||||
|
pub mod persistent_volume;
|
||||||
pub mod queue;
|
pub mod queue;
|
||||||
pub mod ui;
|
pub mod ui;
|
||||||
|
|
||||||
|
pub use error::Error;
|
||||||
|
|
||||||
#[cfg(feature = "mpris")]
|
#[cfg(feature = "mpris")]
|
||||||
pub mod mpris;
|
pub mod mpris;
|
||||||
|
|
||||||
@ -104,14 +107,16 @@ 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, OutputStream)> {
|
pub async fn new(args: &Args) -> eyre::Result<(Self, OutputStream), player::Error> {
|
||||||
// Load the volume file.
|
// Load the volume file.
|
||||||
let volume = PersistentVolume::load().await?;
|
let volume = PersistentVolume::load()
|
||||||
|
.await
|
||||||
|
.map_err(player::Error::PersistentVolumeLoad)?;
|
||||||
|
|
||||||
// Load the track list.
|
// Load the track list.
|
||||||
let list = List::load(args.track_list.as_ref())
|
let list = List::load(args.track_list.as_ref())
|
||||||
.await
|
.await
|
||||||
.wrap_err("unable to load the track list")?;
|
.map_err(player::Error::TrackListLoad)?;
|
||||||
|
|
||||||
// We should only shut up alsa forcefully on Linux if we really have to.
|
// We should only shut up alsa forcefully on Linux if we really have to.
|
||||||
#[cfg(target_os = "linux")]
|
#[cfg(target_os = "linux")]
|
||||||
@ -160,10 +165,10 @@ impl Player {
|
|||||||
/// The [Downloader]s internal buffer size is determined by `buf_size`.
|
/// The [Downloader]s internal buffer size is determined by `buf_size`.
|
||||||
pub async fn play(
|
pub async fn play(
|
||||||
player: Arc<Self>,
|
player: Arc<Self>,
|
||||||
tx: Sender<Messages>,
|
tx: Sender<Message>,
|
||||||
mut rx: Receiver<Messages>,
|
mut rx: Receiver<Message>,
|
||||||
debug: bool,
|
debug: bool,
|
||||||
) -> eyre::Result<()> {
|
) -> eyre::Result<(), player::Error> {
|
||||||
// Initialize the mpris player.
|
// Initialize the mpris player.
|
||||||
//
|
//
|
||||||
// We're initializing here, despite MPRIS being a "user interface",
|
// We're initializing here, despite MPRIS being a "user interface",
|
||||||
@ -213,11 +218,11 @@ 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 => Message::Next,
|
||||||
};
|
};
|
||||||
|
|
||||||
match msg {
|
match msg {
|
||||||
Messages::Next | Messages::Init | Messages::TryAgain => {
|
Message::Next | Message::Init | Message::TryAgain => {
|
||||||
player.bookmarked.swap(false, Ordering::Relaxed);
|
player.bookmarked.swap(false, Ordering::Relaxed);
|
||||||
|
|
||||||
// We manually skipped, so we shouldn't actually wait for the song
|
// We manually skipped, so we shouldn't actually wait for the song
|
||||||
@ -225,7 +230,7 @@ impl Player {
|
|||||||
new = false;
|
new = false;
|
||||||
|
|
||||||
// This basically just prevents `Next` while a song is still currently loading.
|
// This basically just prevents `Next` while a song is still currently loading.
|
||||||
if msg == Messages::Next && !player.current_exists() {
|
if msg == Message::Next && !player.current_exists() {
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -238,19 +243,19 @@ impl Player {
|
|||||||
debug,
|
debug,
|
||||||
));
|
));
|
||||||
}
|
}
|
||||||
Messages::Play => {
|
Message::Play => {
|
||||||
player.sink.play();
|
player.sink.play();
|
||||||
|
|
||||||
#[cfg(feature = "mpris")]
|
#[cfg(feature = "mpris")]
|
||||||
mpris.playback(PlaybackStatus::Playing).await?;
|
mpris.playback(PlaybackStatus::Playing).await?;
|
||||||
}
|
}
|
||||||
Messages::Pause => {
|
Message::Pause => {
|
||||||
player.sink.pause();
|
player.sink.pause();
|
||||||
|
|
||||||
#[cfg(feature = "mpris")]
|
#[cfg(feature = "mpris")]
|
||||||
mpris.playback(PlaybackStatus::Paused).await?;
|
mpris.playback(PlaybackStatus::Paused).await?;
|
||||||
}
|
}
|
||||||
Messages::PlayPause => {
|
Message::PlayPause => {
|
||||||
if player.sink.is_paused() {
|
if player.sink.is_paused() {
|
||||||
player.sink.play();
|
player.sink.play();
|
||||||
} else {
|
} else {
|
||||||
@ -262,7 +267,7 @@ impl Player {
|
|||||||
.playback(mpris.player().playback_status().await?)
|
.playback(mpris.player().playback_status().await?)
|
||||||
.await?;
|
.await?;
|
||||||
}
|
}
|
||||||
Messages::ChangeVolume(change) => {
|
Message::ChangeVolume(change) => {
|
||||||
player.set_volume(player.sink.volume() + change);
|
player.set_volume(player.sink.volume() + change);
|
||||||
|
|
||||||
#[cfg(feature = "mpris")]
|
#[cfg(feature = "mpris")]
|
||||||
@ -273,7 +278,7 @@ impl Player {
|
|||||||
// This basically just continues, but more importantly, it'll re-evaluate
|
// This basically just continues, but more importantly, it'll re-evaluate
|
||||||
// the select macro at the beginning of the loop.
|
// the select macro at the beginning of the loop.
|
||||||
// See the top section to find out why this matters.
|
// See the top section to find out why this matters.
|
||||||
Messages::NewSong => {
|
Message::NewSong => {
|
||||||
// We've recieved `NewSong`, so on the next loop iteration we'll
|
// We've recieved `NewSong`, so on the next loop iteration we'll
|
||||||
// begin waiting for the song to be over in order to autoplay.
|
// begin waiting for the song to be over in order to autoplay.
|
||||||
new = true;
|
new = true;
|
||||||
@ -288,7 +293,7 @@ impl Player {
|
|||||||
|
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
Messages::Bookmark => {
|
Message::Bookmark => {
|
||||||
let current = player.current.load();
|
let current = player.current.load();
|
||||||
let current = current.as_ref().unwrap();
|
let current = current.as_ref().unwrap();
|
||||||
|
|
||||||
@ -300,11 +305,12 @@ impl Player {
|
|||||||
None
|
None
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
.await?;
|
.await
|
||||||
|
.map_err(player::Error::Bookmark)?;
|
||||||
|
|
||||||
player.bookmarked.swap(bookmarked, Ordering::Relaxed);
|
player.bookmarked.swap(bookmarked, Ordering::Relaxed);
|
||||||
}
|
}
|
||||||
Messages::Quit => break,
|
Message::Quit => break,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -1,10 +1,13 @@
|
|||||||
#[cfg(target_os = "linux")]
|
#[cfg(target_os = "linux")]
|
||||||
use rodio::OutputStream;
|
use rodio::OutputStream;
|
||||||
|
|
||||||
|
#[cfg(target_os = "linux")]
|
||||||
|
use crate::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.
|
||||||
#[cfg(target_os = "linux")]
|
#[cfg(target_os = "linux")]
|
||||||
pub fn silent_get_output_stream() -> eyre::Result<OutputStream> {
|
pub fn silent_get_output_stream() -> eyre::Result<OutputStream, player::Error> {
|
||||||
use libc::freopen;
|
use libc::freopen;
|
||||||
use rodio::OutputStreamBuilder;
|
use rodio::OutputStreamBuilder;
|
||||||
use std::ffi::CString;
|
use std::ffi::CString;
|
||||||
|
@ -9,7 +9,7 @@ use crate::data_dir;
|
|||||||
///
|
///
|
||||||
/// Returns whether the track is now bookmarked, or not.
|
/// Returns whether the track is now bookmarked, or not.
|
||||||
pub async fn bookmark(path: String, custom: Option<String>) -> eyre::Result<bool> {
|
pub async fn bookmark(path: String, custom: Option<String>) -> eyre::Result<bool> {
|
||||||
let mut entry = format!("{path}");
|
let mut entry = path.to_string();
|
||||||
if let Some(custom) = custom {
|
if let Some(custom) = custom {
|
||||||
entry.push('!');
|
entry.push('!');
|
||||||
entry.push_str(&custom);
|
entry.push_str(&custom);
|
||||||
@ -25,6 +25,7 @@ pub async fn bookmark(path: String, custom: Option<String>) -> eyre::Result<bool
|
|||||||
.write(true)
|
.write(true)
|
||||||
.read(true)
|
.read(true)
|
||||||
.append(false)
|
.append(false)
|
||||||
|
.truncate(true)
|
||||||
.open(data_dir.join("bookmarks.txt"))
|
.open(data_dir.join("bookmarks.txt"))
|
||||||
.await?;
|
.await?;
|
||||||
|
|
||||||
|
@ -72,6 +72,6 @@ impl Downloader {
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
return (tx, handle);
|
(tx, handle)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
51
src/player/error.rs
Normal file
51
src/player/error.rs
Normal file
@ -0,0 +1,51 @@
|
|||||||
|
use std::ffi::NulError;
|
||||||
|
|
||||||
|
use crate::messages::Message;
|
||||||
|
use tokio::sync::mpsc::error::SendError;
|
||||||
|
|
||||||
|
#[cfg(feature = "mpris")]
|
||||||
|
use mpris_server::zbus::{self, fdo};
|
||||||
|
|
||||||
|
/// Any errors which might occur when running or initializing the lowfi player.
|
||||||
|
#[derive(thiserror::Error, Debug)]
|
||||||
|
pub enum Error {
|
||||||
|
#[error("unable to load the persistent volume")]
|
||||||
|
PersistentVolumeLoad(eyre::Error),
|
||||||
|
|
||||||
|
#[error("unable to save the persistent volume")]
|
||||||
|
PersistentVolumeSave(eyre::Error),
|
||||||
|
|
||||||
|
#[error("sending internal message failed")]
|
||||||
|
Communication(#[from] SendError<Message>),
|
||||||
|
|
||||||
|
#[error("unable to load track list")]
|
||||||
|
TrackListLoad(eyre::Error),
|
||||||
|
|
||||||
|
#[error("interfacing with audio failed")]
|
||||||
|
Stream(#[from] rodio::StreamError),
|
||||||
|
|
||||||
|
#[error("NUL error, if you see this, something has gone VERY wrong")]
|
||||||
|
Nul(#[from] NulError),
|
||||||
|
|
||||||
|
#[error("unable to send or prepare network request")]
|
||||||
|
Reqwest(#[from] reqwest::Error),
|
||||||
|
|
||||||
|
#[cfg(feature = "mpris")]
|
||||||
|
#[error("mpris bus error")]
|
||||||
|
ZBus(#[from] zbus::Error),
|
||||||
|
|
||||||
|
// TODO: This has a terrible error message, mainly because I barely understand
|
||||||
|
// what this error even represents. What does fdo mean?!?!? Why, MPRIS!?!?
|
||||||
|
#[cfg(feature = "mpris")]
|
||||||
|
#[error("mpris fdo (zbus interface) error")]
|
||||||
|
Fdo(#[from] fdo::Error),
|
||||||
|
|
||||||
|
#[error("unable to notify downloader")]
|
||||||
|
DownloaderNotify(#[from] SendError<()>),
|
||||||
|
|
||||||
|
#[error("unable to bookmark track")]
|
||||||
|
Bookmark(eyre::Error),
|
||||||
|
|
||||||
|
#[error("unable to find data directory")]
|
||||||
|
DataDir,
|
||||||
|
}
|
@ -10,7 +10,7 @@ use mpris_server::{
|
|||||||
use tokio::sync::mpsc::Sender;
|
use tokio::sync::mpsc::Sender;
|
||||||
|
|
||||||
use super::ui;
|
use super::ui;
|
||||||
use super::Messages;
|
use super::Message;
|
||||||
|
|
||||||
const ERROR: fdo::Error = fdo::Error::Failed(String::new());
|
const ERROR: fdo::Error = fdo::Error::Failed(String::new());
|
||||||
|
|
||||||
@ -21,7 +21,7 @@ pub struct Player {
|
|||||||
|
|
||||||
/// The audio server sender, which is used to communicate with
|
/// The audio server sender, which is used to communicate with
|
||||||
/// the audio sender for skips and a few other inputs.
|
/// the audio sender for skips and a few other inputs.
|
||||||
pub sender: Sender<Messages>,
|
pub sender: Sender<Message>,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl RootInterface for Player {
|
impl RootInterface for Player {
|
||||||
@ -31,7 +31,7 @@ impl RootInterface for Player {
|
|||||||
|
|
||||||
async fn quit(&self) -> fdo::Result<()> {
|
async fn quit(&self) -> fdo::Result<()> {
|
||||||
self.sender
|
self.sender
|
||||||
.send(Messages::Quit)
|
.send(Message::Quit)
|
||||||
.await
|
.await
|
||||||
.map_err(|_error| ERROR)
|
.map_err(|_error| ERROR)
|
||||||
}
|
}
|
||||||
@ -80,7 +80,7 @@ impl RootInterface for Player {
|
|||||||
impl PlayerInterface for Player {
|
impl PlayerInterface for Player {
|
||||||
async fn next(&self) -> fdo::Result<()> {
|
async fn next(&self) -> fdo::Result<()> {
|
||||||
self.sender
|
self.sender
|
||||||
.send(Messages::Next)
|
.send(Message::Next)
|
||||||
.await
|
.await
|
||||||
.map_err(|_error| ERROR)
|
.map_err(|_error| ERROR)
|
||||||
}
|
}
|
||||||
@ -91,14 +91,14 @@ impl PlayerInterface for Player {
|
|||||||
|
|
||||||
async fn pause(&self) -> fdo::Result<()> {
|
async fn pause(&self) -> fdo::Result<()> {
|
||||||
self.sender
|
self.sender
|
||||||
.send(Messages::Pause)
|
.send(Message::Pause)
|
||||||
.await
|
.await
|
||||||
.map_err(|_error| ERROR)
|
.map_err(|_error| ERROR)
|
||||||
}
|
}
|
||||||
|
|
||||||
async fn play_pause(&self) -> fdo::Result<()> {
|
async fn play_pause(&self) -> fdo::Result<()> {
|
||||||
self.sender
|
self.sender
|
||||||
.send(Messages::PlayPause)
|
.send(Message::PlayPause)
|
||||||
.await
|
.await
|
||||||
.map_err(|_error| ERROR)
|
.map_err(|_error| ERROR)
|
||||||
}
|
}
|
||||||
@ -109,7 +109,7 @@ impl PlayerInterface for Player {
|
|||||||
|
|
||||||
async fn play(&self) -> fdo::Result<()> {
|
async fn play(&self) -> fdo::Result<()> {
|
||||||
self.sender
|
self.sender
|
||||||
.send(Messages::Play)
|
.send(Message::Play)
|
||||||
.await
|
.await
|
||||||
.map_err(|_error| ERROR)
|
.map_err(|_error| ERROR)
|
||||||
}
|
}
|
||||||
@ -247,10 +247,8 @@ impl Server {
|
|||||||
pub async fn changed(
|
pub async fn changed(
|
||||||
&self,
|
&self,
|
||||||
properties: impl IntoIterator<Item = mpris_server::Property> + Send + Sync,
|
properties: impl IntoIterator<Item = mpris_server::Property> + Send + Sync,
|
||||||
) -> eyre::Result<()> {
|
) -> zbus::Result<()> {
|
||||||
self.inner.properties_changed(properties).await?;
|
self.inner.properties_changed(properties).await
|
||||||
|
|
||||||
Ok(())
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Shorthand to emit a `PropertiesChanged` signal, specifically about playback.
|
/// Shorthand to emit a `PropertiesChanged` signal, specifically about playback.
|
||||||
@ -266,7 +264,10 @@ impl Server {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// Creates a new MPRIS server.
|
/// Creates a new MPRIS server.
|
||||||
pub async fn new(player: Arc<super::Player>, sender: Sender<Messages>) -> eyre::Result<Self> {
|
pub async fn new(
|
||||||
|
player: Arc<super::Player>,
|
||||||
|
sender: Sender<Message>,
|
||||||
|
) -> eyre::Result<Self, zbus::Error> {
|
||||||
let suffix = if env::var("LOWFI_FIXED_MPRIS_NAME").is_ok_and(|x| x == "1") {
|
let suffix = if env::var("LOWFI_FIXED_MPRIS_NAME").is_ok_and(|x| x == "1") {
|
||||||
String::from("lowfi")
|
String::from("lowfi")
|
||||||
} else {
|
} else {
|
||||||
|
70
src/player/persistent_volume.rs
Normal file
70
src/player/persistent_volume.rs
Normal file
@ -0,0 +1,70 @@
|
|||||||
|
use eyre::eyre;
|
||||||
|
use std::path::PathBuf;
|
||||||
|
use tokio::fs;
|
||||||
|
|
||||||
|
/// This is the representation of the persistent volume,
|
||||||
|
/// which is loaded at startup and saved on shutdown.
|
||||||
|
#[derive(Clone, Copy)]
|
||||||
|
pub struct PersistentVolume {
|
||||||
|
/// The volume, as a percentage.
|
||||||
|
inner: u16,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl PersistentVolume {
|
||||||
|
/// Retrieves the config directory.
|
||||||
|
async fn config() -> eyre::Result<PathBuf> {
|
||||||
|
let config = dirs::config_dir()
|
||||||
|
.ok_or_else(|| eyre!("Couldn't find config directory"))?
|
||||||
|
.join(PathBuf::from("lowfi"));
|
||||||
|
|
||||||
|
if !config.exists() {
|
||||||
|
fs::create_dir_all(&config).await?;
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(config)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Returns the volume as a float from 0 to 1.
|
||||||
|
pub fn float(self) -> f32 {
|
||||||
|
f32::from(self.inner) / 100.0
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Loads the [`PersistentVolume`] from [`dirs::config_dir()`].
|
||||||
|
pub async fn load() -> eyre::Result<Self> {
|
||||||
|
let config = Self::config().await?;
|
||||||
|
let volume = config.join(PathBuf::from("volume.txt"));
|
||||||
|
|
||||||
|
// Basically just read from the volume file if it exists, otherwise return 100.
|
||||||
|
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);
|
||||||
|
stripped
|
||||||
|
.parse()
|
||||||
|
.map_err(|_error| eyre!("volume.txt file is invalid"))?
|
||||||
|
} else {
|
||||||
|
fs::write(&volume, "100").await?;
|
||||||
|
100u16
|
||||||
|
};
|
||||||
|
|
||||||
|
Ok(Self { inner: volume })
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Saves `volume` to `volume.txt`.
|
||||||
|
pub async fn save(volume: f32) -> eyre::Result<()> {
|
||||||
|
let config = Self::config().await?;
|
||||||
|
let path = config.join(PathBuf::from("volume.txt"));
|
||||||
|
|
||||||
|
// Already rounded & absolute, therefore this should be safe.
|
||||||
|
#[expect(
|
||||||
|
clippy::as_conversions,
|
||||||
|
clippy::cast_sign_loss,
|
||||||
|
clippy::cast_possible_truncation
|
||||||
|
)]
|
||||||
|
let percentage = (volume * 100.0).abs().round() as u16;
|
||||||
|
|
||||||
|
fs::write(path, percentage.to_string()).await?;
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
}
|
@ -2,7 +2,7 @@ use std::sync::Arc;
|
|||||||
use tokio::{sync::mpsc::Sender, time::sleep};
|
use tokio::{sync::mpsc::Sender, time::sleep};
|
||||||
|
|
||||||
use crate::{
|
use crate::{
|
||||||
messages::Messages,
|
messages::Message,
|
||||||
player::{downloader::Downloader, Player, TIMEOUT},
|
player::{downloader::Downloader, Player, TIMEOUT},
|
||||||
tracks,
|
tracks,
|
||||||
};
|
};
|
||||||
@ -43,7 +43,7 @@ impl Player {
|
|||||||
pub async fn next(
|
pub async fn next(
|
||||||
player: Arc<Self>,
|
player: Arc<Self>,
|
||||||
itx: Sender<()>,
|
itx: Sender<()>,
|
||||||
tx: Sender<Messages>,
|
tx: Sender<Message>,
|
||||||
debug: bool,
|
debug: bool,
|
||||||
) -> eyre::Result<()> {
|
) -> eyre::Result<()> {
|
||||||
// Stop the sink.
|
// Stop the sink.
|
||||||
@ -61,18 +61,18 @@ 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(Message::NewSong).await?;
|
||||||
}
|
}
|
||||||
Err(error) => {
|
Err(error) => {
|
||||||
if !error.is_timeout() {
|
if !error.is_timeout() {
|
||||||
if debug {
|
if debug {
|
||||||
panic!("{:?}", error)
|
panic!("{error:?}")
|
||||||
}
|
}
|
||||||
|
|
||||||
sleep(TIMEOUT).await;
|
sleep(TIMEOUT).await;
|
||||||
}
|
}
|
||||||
|
|
||||||
tx.send(Messages::TryAgain).await?;
|
tx.send(Message::TryAgain).await?;
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@ -32,7 +32,8 @@ use thiserror::Error;
|
|||||||
use tokio::{sync::mpsc::Sender, task, time::sleep};
|
use tokio::{sync::mpsc::Sender, task, time::sleep};
|
||||||
use unicode_segmentation::UnicodeSegmentation;
|
use unicode_segmentation::UnicodeSegmentation;
|
||||||
|
|
||||||
use super::{Messages, Player};
|
use super::Player;
|
||||||
|
use crate::messages::Message;
|
||||||
|
|
||||||
mod components;
|
mod components;
|
||||||
mod input;
|
mod input;
|
||||||
@ -47,8 +48,8 @@ pub enum UIError {
|
|||||||
#[error("unable to write output")]
|
#[error("unable to write output")]
|
||||||
Write(#[from] std::io::Error),
|
Write(#[from] std::io::Error),
|
||||||
|
|
||||||
#[error("sending message to backend failed")]
|
#[error("sending message to backend from ui failed")]
|
||||||
Communication(#[from] tokio::sync::mpsc::error::SendError<Messages>),
|
Communication(#[from] tokio::sync::mpsc::error::SendError<Message>),
|
||||||
}
|
}
|
||||||
|
|
||||||
/// How long the audio bar will be visible for when audio is adjusted.
|
/// How long the audio bar will be visible for when audio is adjusted.
|
||||||
@ -204,7 +205,7 @@ async fn interface(
|
|||||||
|
|
||||||
window.draw(menu, false)?;
|
window.draw(menu, false)?;
|
||||||
|
|
||||||
let delta = 1.0 / (fps as f32);
|
let delta = 1.0 / f32::from(fps);
|
||||||
sleep(Duration::from_secs_f32(delta)).await;
|
sleep(Duration::from_secs_f32(delta)).await;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -284,10 +285,11 @@ impl Drop for Environment {
|
|||||||
/// previous terminal history.
|
/// previous terminal history.
|
||||||
pub async fn start(
|
pub async fn start(
|
||||||
player: Arc<Player>,
|
player: Arc<Player>,
|
||||||
sender: Sender<Messages>,
|
sender: Sender<Message>,
|
||||||
args: Args,
|
args: Args,
|
||||||
) -> eyre::Result<(), UIError> {
|
) -> eyre::Result<(), UIError> {
|
||||||
let environment = Environment::ready(args.alternate)?;
|
let environment = Environment::ready(args.alternate)?;
|
||||||
|
|
||||||
let interface = task::spawn(interface(
|
let interface = task::spawn(interface(
|
||||||
Arc::clone(&player),
|
Arc::clone(&player),
|
||||||
args.minimalist,
|
args.minimalist,
|
||||||
|
@ -88,7 +88,7 @@ impl ActionBar {
|
|||||||
|(subject, len)| {
|
|(subject, len)| {
|
||||||
(
|
(
|
||||||
format!("{} {}{}", word, if star { "*" } else { "" }, subject.bold()),
|
format!("{} {}{}", word, if star { "*" } else { "" }, subject.bold()),
|
||||||
word.len() + 1 + len + if star { 1 } else { 0 },
|
word.len() + 1 + len + usize::from(star),
|
||||||
)
|
)
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
|
@ -7,11 +7,11 @@ use tokio::sync::mpsc::Sender;
|
|||||||
|
|
||||||
use crate::player::{
|
use crate::player::{
|
||||||
ui::{self, UIError},
|
ui::{self, UIError},
|
||||||
Messages,
|
Message,
|
||||||
};
|
};
|
||||||
|
|
||||||
/// Starts the listener to recieve input from the terminal for various events.
|
/// Starts the listener to recieve input from the terminal for various events.
|
||||||
pub async fn listen(sender: Sender<Messages>) -> eyre::Result<(), UIError> {
|
pub async fn listen(sender: Sender<Message>) -> eyre::Result<(), UIError> {
|
||||||
let mut reader = EventStream::new();
|
let mut reader = EventStream::new();
|
||||||
|
|
||||||
loop {
|
loop {
|
||||||
@ -25,29 +25,29 @@ pub async fn listen(sender: Sender<Messages>) -> eyre::Result<(), UIError> {
|
|||||||
|
|
||||||
let messages = match event.code {
|
let messages = match event.code {
|
||||||
// Arrow key volume controls.
|
// Arrow key volume controls.
|
||||||
KeyCode::Up => Messages::ChangeVolume(0.1),
|
KeyCode::Up => Message::ChangeVolume(0.1),
|
||||||
KeyCode::Right => Messages::ChangeVolume(0.01),
|
KeyCode::Right => Message::ChangeVolume(0.01),
|
||||||
KeyCode::Down => Messages::ChangeVolume(-0.1),
|
KeyCode::Down => Message::ChangeVolume(-0.1),
|
||||||
KeyCode::Left => Messages::ChangeVolume(-0.01),
|
KeyCode::Left => Message::ChangeVolume(-0.01),
|
||||||
KeyCode::Char(character) => match character.to_ascii_lowercase() {
|
KeyCode::Char(character) => match character.to_ascii_lowercase() {
|
||||||
// Ctrl+C
|
// Ctrl+C
|
||||||
'c' if event.modifiers == KeyModifiers::CONTROL => Messages::Quit,
|
'c' if event.modifiers == KeyModifiers::CONTROL => Message::Quit,
|
||||||
|
|
||||||
// Quit
|
// Quit
|
||||||
'q' => Messages::Quit,
|
'q' => Message::Quit,
|
||||||
|
|
||||||
// Skip/Next
|
// Skip/Next
|
||||||
's' | 'n' | 'l' => Messages::Next,
|
's' | 'n' | 'l' => Message::Next,
|
||||||
|
|
||||||
// Pause
|
// Pause
|
||||||
'p' | ' ' => Messages::PlayPause,
|
'p' | ' ' => Message::PlayPause,
|
||||||
|
|
||||||
// Volume up & down
|
// Volume up & down
|
||||||
'+' | '=' | 'k' => Messages::ChangeVolume(0.1),
|
'+' | '=' | 'k' => Message::ChangeVolume(0.1),
|
||||||
'-' | '_' | 'j' => Messages::ChangeVolume(-0.1),
|
'-' | '_' | 'j' => Message::ChangeVolume(-0.1),
|
||||||
|
|
||||||
// Bookmark
|
// Bookmark
|
||||||
'b' => Messages::Bookmark,
|
'b' => Message::Bookmark,
|
||||||
|
|
||||||
_ => continue,
|
_ => continue,
|
||||||
},
|
},
|
||||||
@ -55,18 +55,18 @@ pub async fn listen(sender: Sender<Messages>) -> eyre::Result<(), UIError> {
|
|||||||
KeyCode::Media(media) => match media {
|
KeyCode::Media(media) => match media {
|
||||||
event::MediaKeyCode::Pause
|
event::MediaKeyCode::Pause
|
||||||
| event::MediaKeyCode::Play
|
| event::MediaKeyCode::Play
|
||||||
| event::MediaKeyCode::PlayPause => Messages::PlayPause,
|
| event::MediaKeyCode::PlayPause => Message::PlayPause,
|
||||||
event::MediaKeyCode::Stop => Messages::Pause,
|
event::MediaKeyCode::Stop => Message::Pause,
|
||||||
event::MediaKeyCode::TrackNext => Messages::Next,
|
event::MediaKeyCode::TrackNext => Message::Next,
|
||||||
event::MediaKeyCode::LowerVolume => Messages::ChangeVolume(-0.1),
|
event::MediaKeyCode::LowerVolume => Message::ChangeVolume(-0.1),
|
||||||
event::MediaKeyCode::RaiseVolume => Messages::ChangeVolume(0.1),
|
event::MediaKeyCode::RaiseVolume => Message::ChangeVolume(0.1),
|
||||||
event::MediaKeyCode::MuteVolume => Messages::ChangeVolume(-1.0),
|
event::MediaKeyCode::MuteVolume => Message::ChangeVolume(-1.0),
|
||||||
_ => continue,
|
_ => continue,
|
||||||
},
|
},
|
||||||
_ => continue,
|
_ => continue,
|
||||||
};
|
};
|
||||||
|
|
||||||
if let Messages::ChangeVolume(_) = messages {
|
if let Message::ChangeVolume(_) = messages {
|
||||||
ui::flash_audio();
|
ui::flash_audio();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -51,8 +51,8 @@ pub enum TrackError {
|
|||||||
}
|
}
|
||||||
|
|
||||||
impl TrackError {
|
impl TrackError {
|
||||||
pub fn is_timeout(&self) -> bool {
|
pub const fn is_timeout(&self) -> bool {
|
||||||
return matches!(self, TrackError::Timeout);
|
matches!(self, Self::Timeout)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -124,10 +124,8 @@ 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(
|
// The tuple contains smart pointers, so it's not really practical to use `into()`.
|
||||||
clippy::tuple_array_conversions,
|
#[allow(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()
|
||||||
@ -172,10 +170,8 @@ impl Info {
|
|||||||
if skip == formatted.len() {
|
if skip == formatted.len() {
|
||||||
Ok(formatted)
|
Ok(formatted)
|
||||||
} else {
|
} else {
|
||||||
#[expect(
|
// We've already checked before that the bound is at an ASCII digit.
|
||||||
clippy::string_slice,
|
#[allow(clippy::string_slice)]
|
||||||
reason = "We've already checked before that the bound is at an ASCII digit."
|
|
||||||
)]
|
|
||||||
Ok(String::from(&formatted[skip..]))
|
Ok(String::from(&formatted[skip..]))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -17,7 +17,7 @@ use super::QueuedTrack;
|
|||||||
#[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")]
|
#[allow(dead_code)]
|
||||||
pub name: String,
|
pub name: String,
|
||||||
|
|
||||||
/// Just the raw file, but seperated by `/n` (newlines).
|
/// Just the raw file, but seperated by `/n` (newlines).
|
||||||
@ -61,11 +61,11 @@ impl List {
|
|||||||
};
|
};
|
||||||
|
|
||||||
let data: Bytes = if let Some(x) = full_path.strip_prefix("file://") {
|
let data: Bytes = if let Some(x) = full_path.strip_prefix("file://") {
|
||||||
let path = if x.starts_with("~") {
|
let path = if x.starts_with('~') {
|
||||||
let home_path = dirs::home_dir().ok_or(TrackError::InvalidPath)?;
|
let home_path = dirs::home_dir().ok_or(TrackError::InvalidPath)?;
|
||||||
let home = home_path.to_str().ok_or(TrackError::InvalidPath)?;
|
let home = home_path.to_str().ok_or(TrackError::InvalidPath)?;
|
||||||
|
|
||||||
x.replace("~", home)
|
x.replace('~', home)
|
||||||
} else {
|
} else {
|
||||||
x.to_owned()
|
x.to_owned()
|
||||||
};
|
};
|
||||||
@ -97,14 +97,15 @@ impl List {
|
|||||||
let (path, custom_name) = self.random_path();
|
let (path, custom_name) = self.random_path();
|
||||||
let (data, full_path) = self.download(&path, client).await?;
|
let (data, full_path) = self.download(&path, client).await?;
|
||||||
|
|
||||||
let name = custom_name.map_or(super::TrackName::Raw(path.clone()), |formatted| {
|
let name = custom_name.map_or_else(
|
||||||
super::TrackName::Formatted(formatted)
|
|| super::TrackName::Raw(path.clone()),
|
||||||
});
|
|formatted| super::TrackName::Formatted(formatted),
|
||||||
|
);
|
||||||
|
|
||||||
Ok(QueuedTrack {
|
Ok(QueuedTrack {
|
||||||
name,
|
name,
|
||||||
data,
|
|
||||||
full_path,
|
full_path,
|
||||||
|
data,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Loading…
x
Reference in New Issue
Block a user