feat: make decoding lazy & improve song name formatting

This commit is contained in:
Tal 2024-09-26 21:00:03 +02:00
parent 04547195e4
commit 93a668bae0
6 changed files with 96 additions and 42 deletions

14
Cargo.lock generated
View File

@ -2,6 +2,16 @@
# It is not intended for manual editing. # It is not intended for manual editing.
version = 3 version = 3
[[package]]
name = "Inflector"
version = "0.11.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "fe438c63458706e03479442743baae6c88256498e6431708f6dfc520a26515d3"
dependencies = [
"lazy_static",
"regex",
]
[[package]] [[package]]
name = "addr2line" name = "addr2line"
version = "0.24.1" version = "0.24.1"
@ -972,16 +982,16 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a7a70ba024b9dc04c27ea2f0c0548feb474ec5c54bba33a7f72f873a39d07b24" checksum = "a7a70ba024b9dc04c27ea2f0c0548feb474ec5c54bba33a7f72f873a39d07b24"
[[package]] [[package]]
name = "lowifi" name = "lowfi"
version = "0.1.0" version = "0.1.0"
dependencies = [ dependencies = [
"Inflector",
"arc-swap", "arc-swap",
"bytes", "bytes",
"clap", "clap",
"crossterm", "crossterm",
"eyre", "eyre",
"futures", "futures",
"itertools",
"rand", "rand",
"reqwest", "reqwest",
"rodio", "rodio",

View File

@ -4,7 +4,6 @@ version = "0.1.0"
edition = "2021" edition = "2021"
[dependencies] [dependencies]
# Basics # Basics
clap = { version = "4.5.18", features = ["derive", "cargo"] } clap = { version = "4.5.18", features = ["derive", "cargo"] }
eyre = "0.6.12" eyre = "0.6.12"
@ -23,3 +22,4 @@ bytes = "1.7.2"
scraper = "0.20.0" scraper = "0.20.0"
rodio = { version = "0.19.0", features = ["mp3"], default-features = false } rodio = { version = "0.19.0", features = ["mp3"], default-features = false }
crossterm = "0.28.1" crossterm = "0.28.1"
Inflector = "0.11.4"

View File

@ -24,7 +24,7 @@ enum Commands {
/// Whether to include the full HTTP URL or just the distinguishing part. /// Whether to include the full HTTP URL or just the distinguishing part.
#[clap(long, short)] #[clap(long, short)]
include_full: bool, include_full: bool,
} },
} }
#[tokio::main] #[tokio::main]
@ -36,7 +36,7 @@ async fn main() -> eyre::Result<()> {
Commands::Scrape { Commands::Scrape {
extention, extention,
include_full, include_full,
} => scrape::scrape(extention, include_full).await } => scrape::scrape(extention, include_full).await,
} }
} else { } else {
play::play().await play::play().await

View File

@ -12,7 +12,7 @@ use tokio::{
task, task,
}; };
use crate::tracks::{Track, TrackInfo}; use crate::tracks::{DecodedTrack, Track, TrackInfo};
pub mod ui; pub mod ui;
@ -62,9 +62,7 @@ impl Player {
} }
/// This will play the next track, as well as refilling the buffer in the background. /// This will play the next track, as well as refilling the buffer in the background.
pub async fn next(queue: Arc<Player>) -> eyre::Result<Track> { pub async fn next(queue: Arc<Player>) -> eyre::Result<DecodedTrack> {
queue.current.store(None);
let track = queue.tracks.write().await.pop_front(); let track = queue.tracks.write().await.pop_front();
let track = match track { let track = match track {
Some(x) => x, Some(x) => x,
@ -73,9 +71,10 @@ impl Player {
None => Track::random(&queue.client).await?, None => Track::random(&queue.client).await?,
}; };
queue.set_current(track.info).await?; let decoded = track.decode()?;
queue.set_current(decoded.info.clone()).await?;
Ok(track) Ok(decoded)
} }
/// This is the main "audio server". /// This is the main "audio server".
@ -115,10 +114,15 @@ impl Player {
match msg { match msg {
Messages::Next | Messages::Init => { Messages::Next | Messages::Init => {
// Serves as an indicator that the queue is "loading".
// This is also set by Player::next.
queue.current.store(None);
// Notify the background downloader that there's an empty spot
// in the buffer.
itx.send(()).await?; itx.send(()).await?;
queue.sink.stop(); queue.sink.stop();
let track = Player::next(queue.clone()).await?; let track = Player::next(queue.clone()).await?;
queue.sink.append(track.data); queue.sink.append(track.data);
} }

View File

@ -32,14 +32,14 @@ enum Action {
impl Action { impl Action {
fn format(&self) -> (String, usize) { fn format(&self) -> (String, usize) {
let (word, subject) = match self { let (word, subject) = match self {
Action::Playing(x) => ("playing", Some(x.format_name())), Action::Playing(x) => ("playing", Some(x.name.clone())),
Action::Paused(x) => ("paused", Some(x.format_name())), Action::Paused(x) => ("paused", Some(x.name.clone())),
Action::Loading => ("loading", None), Action::Loading => ("loading", None),
}; };
if let Some(subject) = subject { if let Some(subject) = subject {
( (
format!("{} {}", word, subject.bold()), format!("{} {}", word, subject.clone().bold()),
word.len() + 1 + subject.len(), word.len() + 1 + subject.len(),
) )
} else { } else {
@ -55,10 +55,12 @@ async fn interface(queue: Arc<Player>) -> eyre::Result<()> {
loop { loop {
let (mut main, len) = match queue.current.load().as_ref() { let (mut main, len) = match queue.current.load().as_ref() {
Some(x) => { Some(x) => {
let name = (*x.clone()).clone();
if queue.sink.is_paused() { if queue.sink.is_paused() {
Action::Paused(*x.clone()) Action::Paused(name)
} else { } else {
Action::Playing(*x.clone()) Action::Playing(name)
} }
} }
None => Action::Loading, None => Action::Loading,
@ -87,7 +89,7 @@ async fn interface(queue: Arc<Player>) -> eyre::Result<()> {
let progress = format!( let progress = format!(
" [{}{}] {}/{} ", " [{}{}] {}/{} ",
"/".repeat(filled as usize), "/".repeat(filled as usize),
" ".repeat(PROGRESS_WIDTH - filled), " ".repeat(PROGRESS_WIDTH.saturating_sub(filled)),
format_duration(&elapsed), format_duration(&elapsed),
format_duration(&duration), format_duration(&duration),
); );
@ -112,7 +114,7 @@ async fn interface(queue: Arc<Player>) -> eyre::Result<()> {
MoveUp(4) MoveUp(4)
)?; )?;
sleep(Duration::from_secs_f32(0.25)).await; sleep(Duration::from_secs_f32(1.0 / 60.0)).await;
} }
} }

View File

@ -1,19 +1,17 @@
use std::{io::Cursor, time::Duration}; use std::{io::Cursor, time::Duration};
use bytes::Bytes; use bytes::Bytes;
use inflector::Inflector;
use rand::Rng; use rand::Rng;
use reqwest::Client; use reqwest::Client;
use rodio::{Decoder, Source}; use rodio::{Decoder, Source};
pub type Data = Decoder<Cursor<Bytes>>; async fn download(track: &str, client: &Client) -> eyre::Result<Bytes> {
async fn download(track: &str, client: &Client) -> eyre::Result<Data> {
let url = format!("https://lofigirl.com/wp-content/uploads/{}", track); let url = format!("https://lofigirl.com/wp-content/uploads/{}", track);
let response = client.get(url).send().await?; let response = client.get(url).send().await?;
let file = Cursor::new(response.bytes().await?); let data = response.bytes().await?;
let source = Decoder::new(file)?;
Ok(source) Ok(data)
} }
async fn random() -> eyre::Result<&'static str> { async fn random() -> eyre::Result<&'static str> {
@ -26,44 +24,84 @@ async fn random() -> eyre::Result<&'static str> {
Ok(track) Ok(track)
} }
#[derive(Debug, PartialEq, Clone, Copy)] pub type DecodedData = Decoder<Cursor<Bytes>>;
/// The TrackInfo struct, which has the name and duration of a track.
///
/// This is not included in [Track] as the duration has to be acquired
/// from the decoded data and not from the raw data.
#[derive(Debug, PartialEq, Clone)]
pub struct TrackInfo { pub struct TrackInfo {
pub name: &'static str, /// This is a formatted name, so it doesn't include the full path.
pub name: String,
pub duration: Option<Duration>, pub duration: Option<Duration>,
} }
impl TrackInfo { impl TrackInfo {
pub fn format_name(&self) -> &'static str { fn format_name(name: &'static str) -> String {
self.name let mut formatted = name
.split("/") .split("/")
.nth(2) .nth(2)
.unwrap() .unwrap()
.strip_suffix(".mp3") .strip_suffix(".mp3")
.unwrap() .unwrap()
.to_title_case();
let mut skip = 0;
for character in unsafe { formatted.as_bytes_mut() } {
if character.is_ascii_digit() {
skip += 1;
} else {
break;
}
}
String::from(&formatted[skip..])
}
pub fn new(name: &'static str, decoded: &DecodedData) -> Self {
Self {
duration: decoded.total_duration(),
name: Self::format_name(name),
}
} }
} }
/// The main track struct, which includes the actual decoded file /// This struct is seperate from [Track] since it is generated lazily from
/// as well as some basic information about it. /// a track, and not when the track is first downloaded.
pub struct Track { pub struct DecodedTrack {
pub info: TrackInfo, pub info: TrackInfo,
pub data: DecodedData,
}
/// TODO: Make decoding lazy, since decoded files take up more memory than raw ones. impl DecodedTrack {
pub data: Data, pub fn new(track: Track) -> eyre::Result<Self> {
let data = Decoder::new(Cursor::new(track.data))?;
let info = TrackInfo::new(track.name, &data);
Ok(Self { info, data })
}
}
/// The main track struct, which only includes data & the track name.
pub struct Track {
pub name: &'static str,
pub data: Bytes,
} }
impl Track { impl Track {
/// Fetches, downloads, and decodes a random track from the tracklist. /// Fetches and downloads a random track from the tracklist.
pub async fn random(client: &Client) -> eyre::Result<Self> { pub async fn random(client: &Client) -> eyre::Result<Self> {
let name = random().await?; let name = random().await?;
let data = download(&name, client).await?; let data = download(&name, client).await?;
Ok(Self { Ok(Self { data, name })
info: TrackInfo { }
name,
duration: data.total_duration(), /// This will actually decode and format the track,
}, /// returning a [`DecodedTrack`] which can be played
data, /// and also has a duration & formatted name.
}) pub fn decode(self) -> eyre::Result<DecodedTrack> {
DecodedTrack::new(self)
} }
} }