From 09dd58664b0cc1c4d64791f12c01ded8437d2ff8 Mon Sep 17 00:00:00 2001 From: Tal <83217276+talwat@users.noreply.github.com> Date: Fri, 14 Nov 2025 18:42:20 +0100 Subject: [PATCH] feat: begin initial rewrite structure --- Cargo.lock | 28 --- Cargo.toml | 3 - README.md | 18 ++ src/{player => }/audio.rs | 4 +- src/download.rs | 27 +++ src/error.rs | 88 +++++++++ src/main.rs | 139 ++++--------- src/{messages.rs => message.rs} | 20 +- src/play.rs | 78 -------- src/player.rs | 314 ------------------------------ src/player/bookmark.rs | 107 ---------- src/player/downloader.rs | 78 -------- src/player/error.rs | 51 ----- src/player/mpris.rs | 281 --------------------------- src/player/persistent_volume.rs | 70 ------- src/player/queue.rs | 88 --------- src/player/ui.rs | 332 -------------------------------- src/player/ui/components.rs | 152 --------------- src/scrapers.rs | 88 --------- src/scrapers/archive.rs | 74 ------- src/scrapers/chillhop.rs | 223 --------------------- src/scrapers/lofigirl.rs | 87 --------- src/tracks.rs | 243 ----------------------- src/tracks/error.rs | 73 ------- src/tracks/list.rs | 189 ------------------ src/ui.rs | 58 ++++++ src/{player => }/ui/input.rs | 14 +- 27 files changed, 244 insertions(+), 2683 deletions(-) rename src/{player => }/audio.rs (97%) create mode 100644 src/download.rs create mode 100644 src/error.rs rename src/{messages.rs => message.rs} (54%) delete mode 100644 src/play.rs delete mode 100644 src/player.rs delete mode 100644 src/player/bookmark.rs delete mode 100644 src/player/downloader.rs delete mode 100644 src/player/error.rs delete mode 100644 src/player/mpris.rs delete mode 100644 src/player/persistent_volume.rs delete mode 100644 src/player/queue.rs delete mode 100644 src/player/ui.rs delete mode 100644 src/player/ui/components.rs delete mode 100644 src/scrapers.rs delete mode 100644 src/scrapers/archive.rs delete mode 100644 src/scrapers/chillhop.rs delete mode 100644 src/scrapers/lofigirl.rs delete mode 100644 src/tracks.rs delete mode 100644 src/tracks/error.rs delete mode 100644 src/tracks/list.rs create mode 100644 src/ui.rs rename src/{player => }/ui/input.rs (90%) diff --git a/Cargo.lock b/Cargo.lock index 3e9f7ae..0584ed3 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -110,12 +110,6 @@ dependencies = [ "windows-sys 0.59.0", ] -[[package]] -name = "arc-swap" -version = "1.7.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "69f7f8c3906b62b754cd5326047894316021dcfe5a194c8ea52bdd94934a3457" - [[package]] name = "arrayvec" version = "0.7.6" @@ -429,19 +423,6 @@ version = "0.7.4" source = "registry+https://github.com/rust-lang/crates.io-index" 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]] name = "colorchoice" version = "1.0.3" @@ -1510,11 +1491,9 @@ checksum = "a7a70ba024b9dc04c27ea2f0c0548feb474ec5c54bba33a7f72f873a39d07b24" name = "lowfi" version = "1.7.2" dependencies = [ - "arc-swap", "atomic_float", "bytes", "clap", - "color-eyre", "convert_case 0.8.0", "crossterm", "dirs", @@ -1523,7 +1502,6 @@ dependencies = [ "futures", "html-escape", "indicatif", - "lazy_static", "libc", "mpris-server", "regex", @@ -1917,12 +1895,6 @@ dependencies = [ "pin-project-lite", ] -[[package]] -name = "owo-colors" -version = "4.2.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "48dd4f4a2c8405440fd0462561f0e5806bd0f77e86f51c761481bdd4018b545e" - [[package]] name = "parking" version = "2.2.1" diff --git a/Cargo.toml b/Cargo.toml index 7cc13c7..52aca5f 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -27,12 +27,10 @@ clap = { version = "4.5.21", features = ["derive", "cargo"] } eyre = "0.6.12" fastrand = "2.3.0" thiserror = "2.0.12" -color-eyre = { version = "0.6.5", default-features = false } # Async tokio = { version = "1.41.1", features = ["macros", "rt-multi-thread", "fs"], default-features = false } futures = "0.3.31" -arc-swap = "1.7.1" # Data reqwest = { version = "0.12.9", features = ["stream"] } @@ -46,7 +44,6 @@ dirs = "6.0.0" # Misc convert_case = "0.8.0" -lazy_static = "1.5.0" url = "2.5.4" unicode-segmentation = "1.12.0" diff --git a/README.md b/README.md index 396d945..b6cdf16 100644 --- a/README.md +++ b/README.md @@ -5,6 +5,24 @@ It'll do this as simply as it can: no albums, no ads, just lofi. ![example image](media/example1.png) +## The Rewrite + +This branch serves as a rewrite for lowfi. The main focus is to make the code more +maintainable. This includes such things as: + +* Replacing `Mutex` & `Arc` with channels, massively improving performance. +* More clearly handling tracks in different phases of loading, instead of having +a mess of different structs. +* Making the UI code cleaner and easier to follow. +* Rethinking input & control of the player, especially with MPRIS in mind. +* Making track loading simpler and more consistent. + +This is an *internal rewrite*, and the goal is to retain every single feature. +If there is a feature present in the original version of lowfi that is not present +in the rewrite, then it is a bug and must be implemented. + +Currently, it is in an extremely early and non-functional state. + ## Disclaimer As of the 1.7.0 version of lowfi, **all** of the audio files embedded diff --git a/src/player/audio.rs b/src/audio.rs similarity index 97% rename from src/player/audio.rs rename to src/audio.rs index 2fc9e6d..4dbda0e 100644 --- a/src/player/audio.rs +++ b/src/audio.rs @@ -1,7 +1,7 @@ /// This gets the output stream while also shutting up alsa with [libc]. /// Uses raw libc calls, and therefore is functional only on Linux. #[cfg(target_os = "linux")] -pub fn silent_get_output_stream() -> eyre::Result { +pub fn silent_get_output_stream() -> eyre::Result { use libc::freopen; use rodio::OutputStreamBuilder; use std::ffi::CString; @@ -37,4 +37,4 @@ pub fn silent_get_output_stream() -> eyre::Result, + handle: crate::Handle, +} + +impl Downloader { + async fn downloader(tx: Sender<()>) -> crate::Result<()> { + + // todo + Ok(()) + } + + pub async fn init(buffer_size: usize) -> Self { + let (tx, rx) = mpsc::channel(buffer_size); + Self { + queue: rx, + handle: tokio::spawn(Self::downloader(tx)), + } + } +} + +pub async fn downloader() { + +} \ No newline at end of file diff --git a/src/error.rs b/src/error.rs new file mode 100644 index 0000000..fa0e8a1 --- /dev/null +++ b/src/error.rs @@ -0,0 +1,88 @@ +use tokio::sync::mpsc; + +pub type Result = std::result::Result; +#[derive(Debug, thiserror::Error)] +pub enum Kind { + #[error("unable to fetch data: {0}")] + Request(#[from] reqwest::Error), + + #[error("C string null error: {0}")] + FfiNull(#[from] std::ffi::NulError), + + #[error("audio playing error: {0}")] + Rodio(#[from] rodio::StreamError), + + #[error("couldn't send internal message: {0}")] + Send(#[from] mpsc::error::SendError), +} + +#[derive(Debug, Default)] +pub struct Context { + track: Option, +} + +impl std::fmt::Display for Context { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + if let Some(track) = &self.track { + write!(f, " ")?; + write!(f, "(track: {track})")?; + } + + Ok(()) + } +} + +#[derive(Debug, thiserror::Error)] +#[error("{kind}{context}")] +pub struct Error { + pub context: Context, + + #[source] + pub kind: Kind, +} + +impl From<(T, E)> for Error +where + T: Into, + Kind: From, +{ + fn from((track, err): (T, E)) -> Self { + Self { + context: Context { track: Some(track.into()) }, + kind: Kind::from(err), + } + } +} + +impl From for Error +where + Kind: From, +{ + fn from(err: E) -> Self { + Self { + context: Context::default(), + kind: Kind::from(err), + } + } +} + +pub trait WithContextExt { + fn context(self, name: impl Into) -> std::result::Result; +} + +impl WithContextExt for std::result::Result +where + (String, E): Into, + E: Into, +{ + fn context(self, name: impl Into) -> std::result::Result { + self.map_err(|e| { + let error = match e.into() { + Kind::Request(error) => Kind::Request(error.without_url()), + kind => kind, + }; + + (name.into(), error).into() + }) + } +} \ No newline at end of file diff --git a/src/main.rs b/src/main.rs index 870774a..8b53c2a 100644 --- a/src/main.rs +++ b/src/main.rs @@ -1,111 +1,52 @@ -//! An extremely simple lofi player. +pub mod error; +use crate::{download::Downloader, ui::UI}; +pub use error::{Error, Result}; +pub mod message; +pub mod ui; +pub use message::Message; +use tokio::sync::mpsc::{self, Receiver}; +pub mod audio; +pub mod download; -#![warn(clippy::all, clippy::pedantic, clippy::nursery)] +pub type Handle = tokio::task::JoinHandle>; -use clap::{Parser, Subcommand}; -use std::path::PathBuf; - -mod messages; -mod play; -mod player; -mod tracks; - -#[allow(clippy::all, clippy::pedantic, clippy::nursery, clippy::restriction)] -#[cfg(feature = "scrape")] -mod scrapers; - -#[cfg(feature = "scrape")] -use crate::scrapers::Source; - -/// An extremely simple lofi player. -#[derive(Parser, Clone)] -#[command(about, version)] -#[allow(clippy::struct_excessive_bools)] -struct Args { - /// Use an alternate terminal screen. - #[clap(long, short)] - alternate: bool, - - /// Hide the bottom control bar. - #[clap(long, short)] - minimalist: bool, - - /// Exclude borders in UI. - #[clap(long, short)] - borderless: bool, - - /// Start lowfi paused. - #[clap(long, short)] - paused: bool, - - /// FPS of the UI. - #[clap(long, short, default_value_t = 12)] - fps: u8, - - /// Timeout in seconds for music downloads. - #[clap(long, default_value_t = 3)] - timeout: u64, - - /// Include ALSA & other logs. - #[clap(long, short)] - debug: bool, - - /// Width of the player, from 0 to 32. - #[clap(long, short, default_value_t = 3)] - width: usize, - - /// Use a custom track list - #[clap(long, short, alias = "list", alias = "tracks", short_alias = 'l')] - track_list: Option, - - /// Internal song buffer size. - #[clap(long, short = 's', alias = "buffer", default_value_t = 5)] - buffer_size: usize, - - /// The command that was ran. - /// This is [None] if no command was specified. - #[command(subcommand)] - command: Option, +pub struct Player { + ui: UI, + downloader: Downloader, + sink: rodio::Sink, + stream: rodio::OutputStream, + rx: Receiver, } -/// Defines all of the extra commands lowfi can run. -#[derive(Subcommand, Clone)] -enum Commands { - /// Scrapes a music source for files. - #[cfg(feature = "scrape")] - Scrape { - // The source to scrape from. - source: scrapers::Source, - }, -} +impl Player { + pub async fn init() -> crate::Result { + #[cfg(target_os = "linux")] + let mut stream = audio::silent_get_output_stream()?; + #[cfg(not(target_os = "linux"))] + let mut stream = rodio::OutputStreamBuilder::open_default_stream()?; -/// Gets lowfi's data directory. -pub fn data_dir() -> eyre::Result { - let dir = dirs::data_dir() - .ok_or(player::Error::DataDir)? - .join("lowfi"); + stream.log_on_drop(false); + let sink = rodio::Sink::connect_new(stream.mixer()); + let (tx, rx) = mpsc::channel(8); - Ok(dir) + Ok(Self { + downloader: Downloader::init(5).await, + ui: UI::init(tx).await, + rx, + sink, + stream, + }) + } } #[tokio::main] -async fn main() -> eyre::Result<()> { - color_eyre::install()?; - - let cli = Args::parse(); - - if let Some(command) = cli.command { - match command { - #[cfg(feature = "scrape")] - Commands::Scrape { source } => match source { - Source::Archive => scrapers::archive::scrape().await?, - Source::Lofigirl => scrapers::lofigirl::scrape().await?, - Source::Chillhop => scrapers::chillhop::scrape().await?, - }, - } - } else { - play::play(cli).await?; - }; +pub async fn main() -> crate::Result<()> { + let mut player: Player = Player::init().await?; + player.ui.render(ui::Render { track: "test".to_owned() }).await?; + + while let Some(message) = player.rx.recv().await { + if message == Message::Quit { break }; + } Ok(()) } diff --git a/src/messages.rs b/src/message.rs similarity index 54% rename from src/messages.rs rename to src/message.rs index b33e0c6..5ce6a2d 100644 --- a/src/messages.rs +++ b/src/message.rs @@ -1,18 +1,14 @@ -/// Handles communication between the frontend & audio player. -#[derive(PartialEq, Debug, Clone, Copy)] +use crate::ui; + +/// Handles communication between different parts of the program. +#[derive(PartialEq, Debug, Clone)] pub enum Message { + /// Sent to update the UI with new information. + Render(ui::Render), + /// Notifies the audio server that it should update the track. Next, - /// Special in that this isn't sent in a "client to server" sort of way, - /// but rather is sent by a child of the server when a song has not only - /// been requested but also downloaded aswell. - NewSong, - - /// This signal is only sent if a track timed out. In that case, - /// lowfi will try again and again to retrieve the track. - TryAgain, - /// Similar to Next, but specific to the first track. Init, @@ -34,4 +30,4 @@ pub enum Message { /// Quits gracefully. Quit, -} +} \ No newline at end of file diff --git a/src/play.rs b/src/play.rs deleted file mode 100644 index 0c70229..0000000 --- a/src/play.rs +++ /dev/null @@ -1,78 +0,0 @@ -//! Responsible for the basic initialization & shutdown of the audio server & frontend. - -use crossterm::cursor::Show; -use crossterm::event::PopKeyboardEnhancementFlags; -use crossterm::terminal::{self, Clear, ClearType}; -use std::io::{stdout, IsTerminal}; -use std::process::exit; -use std::sync::Arc; -use std::{env, panic}; -use tokio::{sync::mpsc, task}; - -use crate::messages::Message; -use crate::player::persistent_volume::PersistentVolume; -use crate::player::Player; -use crate::player::{self, ui}; -use crate::Args; - -/// Initializes the audio server, and then safely stops -/// it when the frontend quits. -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. - // Stream kept here in the master thread to keep it alive. - let (player, stream) = Player::new(&args).await?; - let player = Arc::new(player); - - // Initialize the UI, as well as the internal communication channel. - let (tx, rx) = mpsc::channel(8); - let ui = if stdout().is_terminal() && !(env::var("LOWFI_DISABLE_UI") == Ok("1".to_owned())) { - Some(task::spawn(ui::start( - Arc::clone(&player), - tx.clone(), - args.clone(), - ))) - } else { - None - }; - - // Sends the player an "init" signal telling it to start playing a song straight away. - tx.send(Message::Init).await?; - - // Actually starts the player. - Player::play(Arc::clone(&player), tx.clone(), rx, args.debug).await?; - - // Save the volume.txt file for the next session. - PersistentVolume::save(player.sink.volume()) - .await - .map_err(player::Error::PersistentVolumeSave)?; - - // Save the bookmarks for the next session. - player.bookmarks.save().await?; - - drop(stream); - player.sink.stop(); - if let Some(x) = ui { - x.abort(); - } - - Ok(()) -} diff --git a/src/player.rs b/src/player.rs deleted file mode 100644 index 118a159..0000000 --- a/src/player.rs +++ /dev/null @@ -1,314 +0,0 @@ -//! Responsible for playing & queueing audio. -//! This also has the code for the underlying -//! audio server which adds new tracks. - -use std::{collections::VecDeque, sync::Arc, time::Duration}; - -use arc_swap::ArcSwapOption; -use atomic_float::AtomicF32; -use downloader::Downloader; -use reqwest::Client; -use rodio::{OutputStream, OutputStreamBuilder, Sink}; -use tokio::{ - select, - sync::{ - mpsc::{Receiver, Sender}, - RwLock, - }, - task, -}; - -#[cfg(feature = "mpris")] -use mpris_server::{PlaybackStatus, PlayerInterface, Property}; - -use crate::{ - messages::Message, - player::{self, bookmark::Bookmarks, persistent_volume::PersistentVolume}, - tracks::{self, list::List}, - Args, -}; - -pub mod audio; -pub mod bookmark; -pub mod downloader; -pub mod error; -pub mod persistent_volume; -pub mod queue; -pub mod ui; - -pub use error::Error; - -#[cfg(feature = "mpris")] -pub mod mpris; - -/// Main struct responsible for queuing up & playing tracks. -// TODO: Consider refactoring [Player] from being stored in an [Arc], into containing many smaller [Arc]s. -// TODO: In other words, this would change the type from `Arc` to just `Player`. -// TODO: -// TODO: This is conflicting, since then it'd clone ~10 smaller [Arc]s -// TODO: every single time, which could be even worse than having an -// TODO: [Arc] of an [Arc] in some cases (Like with [Sink] & [Client]). -pub struct Player { - /// [rodio]'s [`Sink`] which can control playback. - pub sink: Sink, - - /// The internal buffer size. - pub buffer_size: usize, - - /// The [`TrackInfo`] of the current track. - /// This is [`None`] when lowfi is buffering/loading. - current: ArcSwapOption, - - /// The current progress for downloading tracks, if - /// `current` is None. - progress: AtomicF32, - - /// The tracks, which is a [`VecDeque`] that holds - /// *undecoded* [Track]s. - /// - /// This is populated specifically by the [Downloader]. - tracks: RwLock>, - - /// The bookmarks, which are saved on quit. - pub bookmarks: Bookmarks, - - /// The timeout for track downloads, as a [Duration]. - timeout: Duration, - - /// The actual list of tracks to be played. - list: List, - - /// The initial volume level. - volume: PersistentVolume, - - /// The web client, which can contain a `UserAgent` & some - /// settings that help lowfi work more effectively. - client: Client, -} - -impl Player { - /// Just a shorthand for setting `current`. - fn set_current(&self, info: tracks::Info) { - self.current.store(Some(Arc::new(info))); - } - - /// A shorthand for checking if `self.current` is [Some]. - pub fn current_exists(&self) -> bool { - self.current.load().is_some() - } - - /// 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)); - } - - /// Initializes the entire player, including audio devices & sink. - /// - /// This also will load the track list & persistent volume. - pub async fn new(args: &Args) -> eyre::Result<(Self, OutputStream), player::Error> { - // Load the bookmarks. - let bookmarks = Bookmarks::load().await?; - - // Load the volume file. - let volume = PersistentVolume::load() - .await - .map_err(player::Error::PersistentVolumeLoad)?; - - // Load the track list. - let list = List::load(args.track_list.as_ref()) - .await - .map_err(player::Error::TrackListLoad)?; - - // We should only shut up alsa forcefully on Linux if we really have to. - #[cfg(target_os = "linux")] - let mut stream = if !args.alternate && !args.debug { - audio::silent_get_output_stream()? - } else { - OutputStreamBuilder::open_default_stream()? - }; - - #[cfg(not(target_os = "linux"))] - let mut stream = OutputStreamBuilder::open_default_stream()?; - - stream.log_on_drop(false); // Frankly, this is a stupid feature. Stop shoving your crap into my beloved stderr!!! - let sink = Sink::connect_new(stream.mixer()); - - if args.paused { - sink.pause(); - } - - let client = Client::builder() - .user_agent(concat!( - env!("CARGO_PKG_NAME"), - "/", - env!("CARGO_PKG_VERSION") - )) - .timeout(Duration::from_secs(args.timeout * 5)) - .build()?; - - let player = Self { - tracks: RwLock::new(VecDeque::with_capacity(args.buffer_size)), - buffer_size: args.buffer_size, - current: ArcSwapOption::new(None), - progress: AtomicF32::new(0.0), - timeout: Duration::from_secs(args.timeout), - bookmarks, - client, - sink, - volume, - list, - }; - - Ok((player, stream)) - } - - /// This is the main "audio server". - /// - /// `rx` & `tx` are used to communicate with it, for example when to - /// skip tracks or pause. - /// - /// This will also initialize a [Downloader] as well as an MPRIS server if enabled. - /// The [Downloader]s internal buffer size is determined by `buf_size`. - pub async fn play( - player: Arc, - tx: Sender, - mut rx: Receiver, - debug: bool, - ) -> eyre::Result<(), player::Error> { - // 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 - .inspect_err(|x| { - dbg!(x); - })?; - - // `itx` is used to notify the `Downloader` when it needs to download new tracks. - let downloader = Downloader::new(Arc::clone(&player)); - let (itx, downloader) = downloader.start(debug); - - // Start buffering tracks immediately. - Downloader::notify(&itx).await?; - - // 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. - // - // 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; - - loop { - let clone = Arc::clone(&player); - - let msg = select! { - biased; - - Some(x) = rx.recv() => x, - // This future will finish only at the end of the current track. - // The condition is a kind-of hack which gets around the quirks - // of `sleep_until_end`. - // - // That's because `sleep_until_end` will return instantly if the sink - // is uninitialized. That's why we put a check to make sure that the last - // signal we got was `NewSong`, since we shouldn't start waiting for the - // song to be over until it has actually started. - // - // 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()), - if new => Message::Next, - }; - - match msg { - Message::Next | Message::Init | Message::TryAgain => { - // We manually skipped, so we shouldn't actually wait for the song - // to be over until we recieve the `NewSong` signal. - new = false; - - // This basically just prevents `Next` while a song is still currently loading. - if msg == Message::Next && !player.current_exists() { - continue; - } - - // Handle the rest of the signal in the background, - // as to not block the main audio server thread. - task::spawn(Self::next( - Arc::clone(&player), - itx.clone(), - tx.clone(), - debug, - )); - } - Message::Play => { - player.sink.play(); - - #[cfg(feature = "mpris")] - mpris.playback(PlaybackStatus::Playing).await?; - } - Message::Pause => { - player.sink.pause(); - - #[cfg(feature = "mpris")] - mpris.playback(PlaybackStatus::Paused).await?; - } - Message::PlayPause => { - if player.sink.is_paused() { - player.sink.play(); - } else { - player.sink.pause(); - } - - #[cfg(feature = "mpris")] - mpris - .playback(mpris.player().playback_status().await?) - .await?; - } - Message::ChangeVolume(change) => { - player.set_volume(player.sink.volume() + change); - - #[cfg(feature = "mpris")] - mpris - .changed(vec![Property::Volume(player.sink.volume().into())]) - .await?; - } - // This basically just continues, but more importantly, it'll re-evaluate - // the select macro at the beginning of the loop. - // See the top section to find out why this matters. - Message::NewSong => { - // We've recieved `NewSong`, so on the next loop iteration we'll - // begin waiting for the song to be over in order to autoplay. - new = true; - - #[cfg(feature = "mpris")] - mpris - .changed(vec![ - Property::Metadata(mpris.player().metadata().await?), - Property::PlaybackStatus(mpris.player().playback_status().await?), - ]) - .await?; - - continue; - } - Message::Bookmark => { - let current = player.current.load(); - let current = current.as_ref().unwrap(); - - player.bookmarks.bookmark(current).await?; - } - Message::Quit => break, - } - } - - downloader.abort(); - - Ok(()) - } -} diff --git a/src/player/bookmark.rs b/src/player/bookmark.rs deleted file mode 100644 index 0ff6761..0000000 --- a/src/player/bookmark.rs +++ /dev/null @@ -1,107 +0,0 @@ -//! Module for handling saving, loading, and adding -//! bookmarks. - -use std::path::PathBuf; -use std::sync::atomic::AtomicBool; - -use tokio::sync::RwLock; -use tokio::{fs, io}; - -use crate::{data_dir, tracks}; - -/// Errors that might occur while managing bookmarks. -#[derive(Debug, thiserror::Error)] -pub enum BookmarkError { - #[error("data directory not found")] - DataDir, - - #[error("io failure")] - Io(#[from] io::Error), -} - -/// Manages the bookmarks in the current player. -pub struct Bookmarks { - /// The different entries in the bookmarks file. - entries: RwLock>, - - /// The internal bookmarked register, which keeps track - /// of whether a track is bookmarked or not. - /// - /// This is much more efficient than checking every single frame. - bookmarked: AtomicBool, -} - -impl Bookmarks { - /// Gets the path of the bookmarks file. - pub async fn path() -> eyre::Result { - let data_dir = data_dir().map_err(|_| BookmarkError::DataDir)?; - fs::create_dir_all(data_dir.clone()).await?; - - Ok(data_dir.join("bookmarks.txt")) - } - - /// Loads bookmarks from the `bookmarks.txt` file. - pub async fn load() -> eyre::Result { - let text = fs::read_to_string(Self::path().await?) - .await - .unwrap_or_default(); - - let lines: Vec = text - .trim_start_matches("noheader") - .trim() - .lines() - .filter_map(|x| { - if x.is_empty() { - None - } else { - Some(x.to_string()) - } - }) - .collect(); - - Ok(Self { - entries: RwLock::new(lines), - bookmarked: AtomicBool::new(false), - }) - } - - // Saves the bookmarks to the `bookmarks.txt` file. - pub async fn save(&self) -> eyre::Result<(), BookmarkError> { - let text = format!("noheader\n{}", self.entries.read().await.join("\n")); - fs::write(Self::path().await?, text).await?; - Ok(()) - } - - /// Bookmarks a given track with a full path and optional custom name. - /// - /// Returns whether the track is now bookmarked, or not. - pub async fn bookmark(&self, track: &tracks::Info) -> eyre::Result<(), BookmarkError> { - let entry = track.to_entry(); - let idx = self.entries.read().await.iter().position(|x| **x == entry); - - if let Some(idx) = idx { - self.entries.write().await.remove(idx); - } else { - self.entries.write().await.push(entry); - }; - - self.bookmarked - .swap(idx.is_none(), std::sync::atomic::Ordering::Relaxed); - - Ok(()) - } - - /// Returns whether a track is bookmarked or not by using the internal - /// bookmarked register. - pub fn bookmarked(&self) -> bool { - self.bookmarked.load(std::sync::atomic::Ordering::Relaxed) - } - - /// Sets the internal bookmarked register by checking against - /// the current track's info. - pub async fn set_bookmarked(&self, track: &tracks::Info) { - let val = self.entries.read().await.contains(&track.to_entry()); - self.bookmarked - .swap(val, std::sync::atomic::Ordering::Relaxed); - } -} diff --git a/src/player/downloader.rs b/src/player/downloader.rs deleted file mode 100644 index 4963139..0000000 --- a/src/player/downloader.rs +++ /dev/null @@ -1,78 +0,0 @@ -//! Contains the [`Downloader`] struct. - -use std::{error::Error, sync::Arc}; - -use tokio::{ - sync::mpsc::{self, Receiver, Sender}, - task::{self, JoinHandle}, - time::sleep, -}; - -use super::Player; - -/// This struct is responsible for downloading tracks in the background. -/// -/// This is not used for the first track or a track when the buffer is currently empty. -pub struct Downloader { - /// The player for the downloader to download to & with. - player: Arc, - - /// The internal reciever, which is used by the downloader to know - /// when to begin downloading more tracks. - rx: Receiver<()>, - - /// A copy of the internal sender, which can be useful for keeping - /// track of it. - tx: Sender<()>, -} - -impl Downloader { - /// Uses a sender recieved from [Sender] to notify the - /// download thread that it should resume downloading. - pub async fn notify(sender: &Sender<()>) -> Result<(), mpsc::error::SendError<()>> { - sender.send(()).await - } - - /// Initializes the [Downloader]. - /// - /// This also sends a [`Sender`] which can be used to notify - /// when the downloader needs to begin downloading more tracks. - pub fn new(player: Arc) -> Self { - let (tx, rx) = mpsc::channel(8); - Self { player, rx, tx } - } - - /// Push a new, random track onto the internal buffer. - pub async fn push_buffer(&self, debug: bool) { - let data = self.player.list.random(&self.player.client, None).await; - match data { - Ok(track) => self.player.tracks.write().await.push_back(track), - Err(error) => { - if debug { - panic!("{error} - {:?}", error.source()) - } - - if !error.is_timeout() { - sleep(self.player.timeout).await; - } - } - } - } - - /// Actually starts & consumes the [Downloader]. - pub fn start(mut self, debug: bool) -> (Sender<()>, JoinHandle<()>) { - let tx = self.tx.clone(); - - let handle = task::spawn(async move { - // Loop through each update notification. - while self.rx.recv().await == Some(()) { - // For each update notification, we'll push tracks until the buffer is completely full. - while self.player.tracks.read().await.len() < self.player.buffer_size { - self.push_buffer(debug).await; - } - } - }); - - (tx, handle) - } -} diff --git a/src/player/error.rs b/src/player/error.rs deleted file mode 100644 index e417e2e..0000000 --- a/src/player/error.rs +++ /dev/null @@ -1,51 +0,0 @@ -use std::ffi::NulError; - -use crate::{messages::Message, player::bookmark::BookmarkError}; -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), - - #[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 find data directory")] - DataDir, - - #[error("bookmarking load/unload failed")] - Bookmark(#[from] BookmarkError), -} diff --git a/src/player/mpris.rs b/src/player/mpris.rs deleted file mode 100644 index d260d72..0000000 --- a/src/player/mpris.rs +++ /dev/null @@ -1,281 +0,0 @@ -//! Contains the code for the MPRIS server & other helper functions. - -use std::{env, process, sync::Arc}; - -use mpris_server::{ - zbus::{self, fdo, Result}, - LoopStatus, Metadata, PlaybackRate, PlaybackStatus, PlayerInterface, Property, RootInterface, - Time, TrackId, Volume, -}; -use tokio::sync::mpsc::Sender; - -use super::ui; -use super::Message; - -const ERROR: fdo::Error = fdo::Error::Failed(String::new()); - -/// The actual MPRIS player. -pub struct Player { - /// A reference to the [`super::Player`] itself. - pub player: Arc, - - /// The audio server sender, which is used to communicate with - /// the audio sender for skips and a few other inputs. - pub sender: Sender, -} - -impl RootInterface for Player { - async fn raise(&self) -> fdo::Result<()> { - Err(ERROR) - } - - async fn quit(&self) -> fdo::Result<()> { - self.sender - .send(Message::Quit) - .await - .map_err(|_error| ERROR) - } - - async fn can_quit(&self) -> fdo::Result { - Ok(true) - } - - async fn fullscreen(&self) -> fdo::Result { - Ok(false) - } - - async fn set_fullscreen(&self, _: bool) -> Result<()> { - Ok(()) - } - - async fn can_set_fullscreen(&self) -> fdo::Result { - Ok(false) - } - - async fn can_raise(&self) -> fdo::Result { - Ok(false) - } - - async fn has_track_list(&self) -> fdo::Result { - Ok(false) - } - - async fn identity(&self) -> fdo::Result { - Ok("lowfi".to_owned()) - } - - async fn desktop_entry(&self) -> fdo::Result { - Ok("dev.talwat.lowfi".to_owned()) - } - - async fn supported_uri_schemes(&self) -> fdo::Result> { - Ok(vec!["https".to_owned()]) - } - - async fn supported_mime_types(&self) -> fdo::Result> { - Ok(vec!["audio/mpeg".to_owned()]) - } -} - -impl PlayerInterface for Player { - async fn next(&self) -> fdo::Result<()> { - self.sender - .send(Message::Next) - .await - .map_err(|_error| ERROR) - } - - async fn previous(&self) -> fdo::Result<()> { - Err(ERROR) - } - - async fn pause(&self) -> fdo::Result<()> { - self.sender - .send(Message::Pause) - .await - .map_err(|_error| ERROR) - } - - async fn play_pause(&self) -> fdo::Result<()> { - self.sender - .send(Message::PlayPause) - .await - .map_err(|_error| ERROR) - } - - async fn stop(&self) -> fdo::Result<()> { - self.pause().await - } - - async fn play(&self) -> fdo::Result<()> { - self.sender - .send(Message::Play) - .await - .map_err(|_error| ERROR) - } - - async fn seek(&self, _offset: Time) -> fdo::Result<()> { - Err(ERROR) - } - - async fn set_position(&self, _track_id: TrackId, _position: Time) -> fdo::Result<()> { - Err(ERROR) - } - - async fn open_uri(&self, _uri: String) -> fdo::Result<()> { - Err(ERROR) - } - - async fn playback_status(&self) -> fdo::Result { - Ok(if !self.player.current_exists() { - PlaybackStatus::Stopped - } else if self.player.sink.is_paused() { - PlaybackStatus::Paused - } else { - PlaybackStatus::Playing - }) - } - - async fn loop_status(&self) -> fdo::Result { - Err(ERROR) - } - - async fn set_loop_status(&self, _loop_status: LoopStatus) -> Result<()> { - Ok(()) - } - - async fn rate(&self) -> fdo::Result { - Ok(self.player.sink.speed().into()) - } - - async fn set_rate(&self, rate: PlaybackRate) -> Result<()> { - self.player.sink.set_speed(rate as f32); - Ok(()) - } - - async fn shuffle(&self) -> fdo::Result { - Ok(true) - } - - async fn set_shuffle(&self, _shuffle: bool) -> Result<()> { - Ok(()) - } - - async fn metadata(&self) -> fdo::Result { - let metadata = self - .player - .current - .load() - .as_ref() - .map_or_else(Metadata::new, |track| { - let mut metadata = Metadata::builder() - .title(track.display_name.clone()) - .album(self.player.list.name.clone()) - .build(); - - metadata.set_length( - track - .duration - .map(|x| Time::from_micros(x.as_micros() as i64)), - ); - - metadata - }); - - Ok(metadata) - } - - async fn volume(&self) -> fdo::Result { - Ok(self.player.sink.volume().into()) - } - - async fn set_volume(&self, volume: Volume) -> Result<()> { - self.player.set_volume(volume as f32); - ui::flash_audio(); - - Ok(()) - } - - async fn position(&self) -> fdo::Result