lowfi/src/player.rs
Tal e44a8b85c4 fix: error reporting
docs: fix typos
2025-12-06 17:28:11 +01:00

247 lines
7.8 KiB
Rust

use std::sync::Arc;
use tokio::sync::{
broadcast,
mpsc::{self, Receiver},
};
use crate::{
audio::waiter,
bookmark::Bookmarks,
download::{self, Downloader},
tracks::{self, List},
ui,
volume::PersistentVolume,
Message,
};
#[derive(Clone, Debug)]
/// Represents the currently known playback state.
///
/// * [`Current::Loading`] indicates the player is waiting for data.
/// * [`Current::Track`] indicates the player has a decoded track available.
pub enum Current {
/// Waiting for a track to arrive. The optional `Progress` is used to
/// indicate global download progress when present.
Loading(Option<download::Progress>),
/// A decoded track that can be played; contains the track `Info`.
Track(tracks::Info),
}
impl Default for Current {
fn default() -> Self {
// By default the player starts in a loading state with no progress.
Self::Loading(None)
}
}
impl Current {
/// Returns `true` if this `Current` value represents a loading state.
pub const fn loading(&self) -> bool {
matches!(self, Self::Loading(_))
}
}
/// The high-level application player.
///
/// `Player` composes the downloader, UI, audio sink and bookkeeping state.
/// It owns background `Handle`s and drives the main message loop in `run`.
pub struct Player {
/// Background downloader that fills the internal queue.
downloader: download::Handle,
/// Persistent bookmark storage used by the player.
bookmarks: Bookmarks,
/// Shared audio sink used for playback.
sink: Arc<rodio::Sink>,
/// Receiver for incoming `Message` commands.
rx: Receiver<crate::Message>,
/// Broadcast channel used to send UI updates.
broadcast: broadcast::Sender<ui::Update>,
/// Current playback state (loading or track).
current: Current,
/// UI handle for rendering and input.
ui: ui::Handle,
/// Notifies when a play head has been appended.
waiter: waiter::Handle,
}
impl Drop for Player {
fn drop(&mut self) {
// Ensure playback is stopped when the player is dropped.
self.sink.stop();
}
}
impl Player {
/// Returns the `Environment` currently used by the UI.
pub const fn environment(&self) -> ui::Environment {
self.ui.environment
}
/// Sets the in-memory current state and notifies the UI about the change.
///
/// If the new state is a `Track`, this will also update the bookmarked flag
/// based on persistent bookmarks.
pub fn set_current(&mut self, current: Current) -> crate::Result<()> {
self.current = current.clone();
self.update(ui::Update::Track(current))?;
let Current::Track(track) = &self.current else {
return Ok(());
};
let bookmarked = self.bookmarks.bookmarked(track);
self.update(ui::Update::Bookmarked(bookmarked))?;
Ok(())
}
/// Sends a `ui::Update` to the broadcast channel.
pub fn update(&mut self, update: ui::Update) -> crate::Result<()> {
self.broadcast.send(update)?;
Ok(())
}
/// Initialize a `Player` with the provided CLI `args` and audio `mixer`.
///
/// This sets up the audio sink, UI, downloader, bookmarks and persistent
/// volume state. The function returns a fully constructed `Player` ready
/// to be driven via `run`.
pub async fn init(
args: crate::Args,
environment: ui::Environment,
mixer: &rodio::mixer::Mixer,
) -> crate::Result<Self> {
let (tx, rx) = mpsc::channel(8);
if args.paused {
tx.send(Message::Pause).await?;
}
tx.send(Message::Init).await?;
let (utx, urx) = broadcast::channel(8);
let list = List::load(args.track_list.as_ref()).await?;
let sink = Arc::new(rodio::Sink::connect_new(mixer));
let state = ui::State::initial(Arc::clone(&sink), args.width, list.name.clone());
let volume = PersistentVolume::load().await?;
sink.set_volume(volume.float());
Ok(Self {
ui: ui::Handle::init(tx.clone(), environment, urx, state, &args).await?,
downloader: Downloader::init(
args.buffer_size as usize,
args.timeout,
list,
tx.clone(),
)?,
waiter: waiter::Handle::new(Arc::clone(&sink), tx),
bookmarks: Bookmarks::load().await?,
current: Current::default(),
broadcast: utx,
rx,
sink,
})
}
/// Persist state that should survive a run (bookmarks and volume).
pub async fn close(&self) -> crate::Result<()> {
self.bookmarks.save().await?;
PersistentVolume::save(self.sink.volume()).await?;
Ok(())
}
/// Play a queued track by decoding, appending to the sink and notifying
/// other subsystems that playback has changed.
pub fn play(&mut self, queued: tracks::Queued) -> crate::Result<()> {
let decoded = queued.decode()?;
self.sink.append(decoded.data);
self.set_current(Current::Track(decoded.info))?;
self.waiter.notify();
Ok(())
}
/// Drives the main message loop of the player.
///
/// This will return when a `Message::Quit` is received. It handles commands
/// coming from the frontend and updates playback/UI state accordingly.
pub async fn run(&mut self) -> crate::Result<()> {
while let Some(message) = self.rx.recv().await {
match message {
Message::Next | Message::Init | Message::Loaded => {
if message == Message::Next && self.current.loading() {
continue;
}
self.sink.stop();
match self.downloader.track() {
download::Output::Loading(progress) => {
self.set_current(Current::Loading(progress))?;
}
download::Output::Queued(queued) => self.play(queued)?,
}
}
Message::Play => {
self.sink.play();
}
Message::Pause => {
self.sink.pause();
}
Message::PlayPause => {
if self.sink.is_paused() {
self.sink.play();
} else {
self.sink.pause();
}
}
Message::ChangeVolume(change) => {
self.sink
.set_volume((self.sink.volume() + change).clamp(0.0, 1.0));
self.update(ui::Update::Volume)?;
}
Message::SetVolume(set) => {
self.sink.set_volume(set.clamp(0.0, 1.0));
self.update(ui::Update::Volume)?;
}
Message::Bookmark => {
let Current::Track(current) = &self.current else {
continue;
};
let bookmarked = self.bookmarks.bookmark(current)?;
self.update(ui::Update::Bookmarked(bookmarked))?;
}
Message::Quit => break,
}
#[cfg(feature = "mpris")]
match message {
Message::ChangeVolume(_) | Message::SetVolume(_) => {
self.ui.mpris.update_volume().await?
}
Message::Play | Message::Pause | Message::PlayPause => {
self.ui.mpris.update_playback().await?
}
Message::Init | Message::Loaded | Message::Next => {
self.ui.mpris.update_metadata().await?
}
_ => (),
}
}
self.close().await?;
Ok(())
}
}