mirror of
https://github.com/talwat/lowfi
synced 2024-12-26 03:01:55 +00:00
feat: improve buffering so that filling the queue happens in the background
This commit is contained in:
parent
f0e56ea2aa
commit
1d5af7dc3e
48
Cargo.lock
generated
48
Cargo.lock
generated
@ -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"
|
||||
|
@ -13,3 +13,4 @@ eyre = "0.6.12"
|
||||
futures = "0.3.30"
|
||||
bytes = "1.7.2"
|
||||
rand = "0.8.5"
|
||||
crossterm = "0.28.1"
|
||||
|
@ -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
|
||||
}
|
||||
}
|
34
src/play.rs
34
src/play.rs
@ -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
103
src/player.rs
Normal 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(())
|
||||
}
|
@ -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)
|
||||
}
|
Loading…
Reference in New Issue
Block a user