feat: improve buffering so that filling the queue happens in the background

This commit is contained in:
talwat 2024-09-24 10:32:42 +02:00
parent f0e56ea2aa
commit 1d5af7dc3e
6 changed files with 161 additions and 58 deletions

48
Cargo.lock generated
View File

@ -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"

View File

@ -13,3 +13,4 @@ eyre = "0.6.12"
futures = "0.3.30"
bytes = "1.7.2"
rand = "0.8.5"
crossterm = "0.28.1"

View File

@ -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
}
}

View File

@ -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(())
}

103
src/player.rs Normal file
View File

@ -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<Self> {
let name = tracks::random().await?;
let data = tracks::download(&name).await?;
Ok(Self { name, data })
}
}
pub struct Queue {
tracks: Arc<RwLock<VecDeque<Track>>>,
}
impl Queue {
pub async fn new() -> Self {
Self {
tracks: Arc::new(RwLock::new(VecDeque::with_capacity(5))),
}
}
pub async fn get(&self) -> eyre::Result<Track> {
// 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(())
}

View File

@ -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<Decoder<Cursor<Bytes>>> {
pub type Data = Cursor<Bytes>;
pub async fn download(track: &str) -> eyre::Result<Data> {
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<Cursor<Bytes>>) -> 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)
}