From 1d5af7dc3edeafb2e22834f9d7675dfc036e0b35 Mon Sep 17 00:00:00 2001 From: talwat <83217276+talwat@users.noreply.github.com> Date: Tue, 24 Sep 2024 10:32:42 +0200 Subject: [PATCH] feat: improve buffering so that filling the queue happens in the background --- Cargo.lock | 48 +++++++++++++++++++++++ Cargo.toml | 1 + src/main.rs | 5 ++- src/play.rs | 34 ----------------- src/player.rs | 103 ++++++++++++++++++++++++++++++++++++++++++++++++++ src/tracks.rs | 28 +++----------- 6 files changed, 161 insertions(+), 58 deletions(-) delete mode 100644 src/play.rs create mode 100644 src/player.rs diff --git a/Cargo.lock b/Cargo.lock index 14f6f7f..eaf49b4 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -349,6 +349,31 @@ dependencies = [ "windows", ] +[[package]] +name = "crossterm" +version = "0.28.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "829d955a0bb380ef178a640b91779e3987da38c9aea133b20614cfed8cdea9c6" +dependencies = [ + "bitflags 2.6.0", + "crossterm_winapi", + "mio", + "parking_lot", + "rustix", + "signal-hook", + "signal-hook-mio", + "winapi", +] + +[[package]] +name = "crossterm_winapi" +version = "0.9.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "acdd7c62a3665c7f6830a51635d9ac9b23ed385797f70a83bb8bafe9c572ab2b" +dependencies = [ + "winapi", +] + [[package]] name = "cssparser" version = "0.31.2" @@ -928,6 +953,7 @@ version = "0.1.0" dependencies = [ "bytes", "clap", + "crossterm", "eyre", "futures", "rand", @@ -1021,6 +1047,7 @@ checksum = "80e04d1dcff3aae0704555fe5fee3bcfaf3d1fdf8a7e521d5b9d2b42acb52cec" dependencies = [ "hermit-abi", "libc", + "log", "wasi", "windows-sys 0.52.0", ] @@ -1759,6 +1786,27 @@ version = "1.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0fda2ff0d084019ba4d7c6f371c95d8fd75ce3524c3cb8fb653a3023f6323e64" +[[package]] +name = "signal-hook" +version = "0.3.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8621587d4798caf8eb44879d42e56b9a93ea5dcd315a6487c357130095b62801" +dependencies = [ + "libc", + "signal-hook-registry", +] + +[[package]] +name = "signal-hook-mio" +version = "0.2.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "34db1a06d485c9142248b7a054f034b349b212551f3dfd19c94d45a754a217cd" +dependencies = [ + "libc", + "mio", + "signal-hook", +] + [[package]] name = "signal-hook-registry" version = "1.4.2" diff --git a/Cargo.toml b/Cargo.toml index d27ea91..006b0fb 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -13,3 +13,4 @@ eyre = "0.6.12" futures = "0.3.30" bytes = "1.7.2" rand = "0.8.5" +crossterm = "0.28.1" diff --git a/src/main.rs b/src/main.rs index e1a8598..8e257b0 100644 --- a/src/main.rs +++ b/src/main.rs @@ -2,6 +2,7 @@ use clap::{Parser, Subcommand}; mod scrape; mod tracks; +mod player; /// An extremely simple lofi player. #[derive(Parser)] @@ -15,7 +16,7 @@ struct Args { enum Commands { /// Scrapes the lofi girl website file server for mp3 files. Scrape, - /// Plays a single, random, track. + /// Starts the player. Play } @@ -25,6 +26,6 @@ async fn main() -> eyre::Result<()> { match cli.command { Commands::Scrape => scrape::scrape().await, - Commands::Play => tracks::random().await + Commands::Play => player::play().await } } \ No newline at end of file diff --git a/src/play.rs b/src/play.rs deleted file mode 100644 index 14ed0c3..0000000 --- a/src/play.rs +++ /dev/null @@ -1,34 +0,0 @@ -use std::{io::Cursor, time::Duration}; - -use rodio::{Decoder, OutputStream, Sink, Source}; -use tokio::time::sleep; - -pub async fn download() { - -} - -pub async fn play(track: &str) -> eyre::Result<()> { - eprintln!("downloading {}...", track); - let url = format!("https://lofigirl.com/wp-content/uploads/{}", track); - let file = Cursor::new(reqwest::get(url).await?.bytes().await?); - - let source = Decoder::new(file).unwrap(); - - let (stream, stream_handle) = OutputStream::try_default().unwrap(); - let sink = Sink::try_new(&stream_handle).unwrap(); - sink.append(source); - - eprintln!("playing {}...", track); - sink.sleep_until_end(); - - Ok(()) -} - -pub async fn random() -> eyre::Result<()> { - let tracks = include_str!("../data/tracks.txt"); - let tracks: Vec<&str> = tracks.split_ascii_whitespace().collect(); - - play(tracks[0]).await?; - - Ok(()) -} \ No newline at end of file diff --git a/src/player.rs b/src/player.rs new file mode 100644 index 0000000..f3cda2b --- /dev/null +++ b/src/player.rs @@ -0,0 +1,103 @@ +use std::{collections::VecDeque, sync::Arc, time::Duration}; + +use rodio::{Decoder, OutputStream, Sink, Source}; +use tokio::{ + sync::{mpsc, RwLock}, + task, + time::sleep, +}; + +/// The amount of songs to buffer up. +const BUFFER_SIZE: usize = 5; + +use crate::tracks::{self}; + +#[derive(Debug, PartialEq)] +pub struct Track { + pub name: &'static str, + pub data: tracks::Data, +} + +impl Track { + pub async fn random() -> eyre::Result { + let name = tracks::random().await?; + let data = tracks::download(&name).await?; + + Ok(Self { name, data }) + } +} + +pub struct Queue { + tracks: Arc>>, +} + +impl Queue { + pub async fn new() -> Self { + Self { + tracks: Arc::new(RwLock::new(VecDeque::with_capacity(5))), + } + } + + pub async fn get(&self) -> eyre::Result { + // This refills the queue in the background. + let tracks = self.tracks.clone(); + task::spawn(async move { + while tracks.read().await.len() < BUFFER_SIZE { + let track = Track::random().await.unwrap(); + tracks.write().await.push_back(track); + } + }); + + let track = self.tracks.write().await.pop_front(); + let track = match track { + Some(x) => x, + // If the queue is completely empty, then fallback to simply getting a new track. + // This is relevant particularly at the first song. + None => Track::random().await?, + }; + + Ok(track) + } +} + +pub async fn play() -> eyre::Result<()> { + let queue = Queue::new().await; + + let (stream, handle) = OutputStream::try_default().unwrap(); + let sink = Sink::try_new(&handle).unwrap(); + + crossterm::terminal::enable_raw_mode()?; + + // TODO: Reintroduce the Player struct and seperate + // input/display from song playing so that quits & skips + // are instant. + loop { + sink.stop(); + + let track = queue.get().await?; + sink.append(Decoder::new(track.data)?); + + match crossterm::event::read()? { + crossterm::event::Event::Key(event) => { + match event.code { + crossterm::event::KeyCode::Char(x) => { + if x == 's' { + continue; + } else if x == 'q' { + break; + } + } + _ => () + } + }, + _ => () + } + + sleep(Duration::from_secs(2)).await; + } + + crossterm::terminal::disable_raw_mode()?; + sink.stop(); + drop(stream); + Ok(()) +} diff --git a/src/tracks.rs b/src/tracks.rs index fc14148..9803a24 100644 --- a/src/tracks.rs +++ b/src/tracks.rs @@ -2,38 +2,22 @@ use std::io::Cursor; use bytes::Bytes; use rand::Rng; -use rodio::{Decoder, OutputStream, Sink}; -pub async fn download(track: &str) -> eyre::Result>> { +pub type Data = Cursor; + +pub async fn download(track: &str) -> eyre::Result { let url = format!("https://lofigirl.com/wp-content/uploads/{}", track); let file = Cursor::new(reqwest::get(url).await?.bytes().await?); - let source = Decoder::new(file).unwrap(); - Ok(source) + Ok(file) } -pub async fn play(source: Decoder>) -> eyre::Result<()> { - let (stream, stream_handle) = OutputStream::try_default()?; - let sink = Sink::try_new(&stream_handle)?; - sink.append(source); - - sink.sleep_until_end(); - - Ok(()) -} - -pub async fn random() -> eyre::Result<()> { +pub async fn random() -> eyre::Result<&'static str> { let tracks = include_str!("../data/tracks.txt"); let tracks: Vec<&str> = tracks.split_ascii_whitespace().collect(); let random = rand::thread_rng().gen_range(0..tracks.len()); let track = tracks[random]; - eprintln!("downloading {}...", track); - let source = download(track).await?; - - eprintln!("playing {}...", track); - play(source).await?; - - Ok(()) + Ok(track) } \ No newline at end of file