diff --git a/Cargo.lock b/Cargo.lock index d228c91..f356672 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2,6 +2,16 @@ # It is not intended for manual editing. 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]] name = "addr2line" version = "0.24.1" @@ -972,16 +982,16 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a7a70ba024b9dc04c27ea2f0c0548feb474ec5c54bba33a7f72f873a39d07b24" [[package]] -name = "lowifi" +name = "lowfi" version = "0.1.0" dependencies = [ + "Inflector", "arc-swap", "bytes", "clap", "crossterm", "eyre", "futures", - "itertools", "rand", "reqwest", "rodio", diff --git a/Cargo.toml b/Cargo.toml index 406fc81..1924fe4 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -4,7 +4,6 @@ version = "0.1.0" edition = "2021" [dependencies] - # Basics clap = { version = "4.5.18", features = ["derive", "cargo"] } eyre = "0.6.12" @@ -23,3 +22,4 @@ bytes = "1.7.2" scraper = "0.20.0" rodio = { version = "0.19.0", features = ["mp3"], default-features = false } crossterm = "0.28.1" +Inflector = "0.11.4" diff --git a/src/main.rs b/src/main.rs index 97b0e28..aba3a68 100644 --- a/src/main.rs +++ b/src/main.rs @@ -24,7 +24,7 @@ enum Commands { /// Whether to include the full HTTP URL or just the distinguishing part. #[clap(long, short)] include_full: bool, - } + }, } #[tokio::main] @@ -36,7 +36,7 @@ async fn main() -> eyre::Result<()> { Commands::Scrape { extention, include_full, - } => scrape::scrape(extention, include_full).await + } => scrape::scrape(extention, include_full).await, } } else { play::play().await diff --git a/src/player.rs b/src/player.rs index 7d6d3ac..ecefa2b 100644 --- a/src/player.rs +++ b/src/player.rs @@ -12,7 +12,7 @@ use tokio::{ task, }; -use crate::tracks::{Track, TrackInfo}; +use crate::tracks::{DecodedTrack, Track, TrackInfo}; pub mod ui; @@ -62,9 +62,7 @@ impl Player { } /// This will play the next track, as well as refilling the buffer in the background. - pub async fn next(queue: Arc) -> eyre::Result { - queue.current.store(None); - + pub async fn next(queue: Arc) -> eyre::Result { let track = queue.tracks.write().await.pop_front(); let track = match track { Some(x) => x, @@ -73,9 +71,10 @@ impl Player { 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". @@ -115,10 +114,15 @@ impl Player { match msg { 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?; queue.sink.stop(); - let track = Player::next(queue.clone()).await?; queue.sink.append(track.data); } diff --git a/src/player/ui.rs b/src/player/ui.rs index 6a5879c..9b73f2c 100644 --- a/src/player/ui.rs +++ b/src/player/ui.rs @@ -32,14 +32,14 @@ enum Action { impl Action { fn format(&self) -> (String, usize) { let (word, subject) = match self { - Action::Playing(x) => ("playing", Some(x.format_name())), - Action::Paused(x) => ("paused", Some(x.format_name())), + Action::Playing(x) => ("playing", Some(x.name.clone())), + Action::Paused(x) => ("paused", Some(x.name.clone())), Action::Loading => ("loading", None), }; if let Some(subject) = subject { ( - format!("{} {}", word, subject.bold()), + format!("{} {}", word, subject.clone().bold()), word.len() + 1 + subject.len(), ) } else { @@ -55,10 +55,12 @@ async fn interface(queue: Arc) -> eyre::Result<()> { loop { let (mut main, len) = match queue.current.load().as_ref() { Some(x) => { + let name = (*x.clone()).clone(); + if queue.sink.is_paused() { - Action::Paused(*x.clone()) + Action::Paused(name) } else { - Action::Playing(*x.clone()) + Action::Playing(name) } } None => Action::Loading, @@ -87,7 +89,7 @@ async fn interface(queue: Arc) -> eyre::Result<()> { let progress = format!( " [{}{}] {}/{} ", "/".repeat(filled as usize), - " ".repeat(PROGRESS_WIDTH - filled), + " ".repeat(PROGRESS_WIDTH.saturating_sub(filled)), format_duration(&elapsed), format_duration(&duration), ); @@ -112,7 +114,7 @@ async fn interface(queue: Arc) -> eyre::Result<()> { MoveUp(4) )?; - sleep(Duration::from_secs_f32(0.25)).await; + sleep(Duration::from_secs_f32(1.0 / 60.0)).await; } } diff --git a/src/tracks.rs b/src/tracks.rs index 9832f03..efb2087 100644 --- a/src/tracks.rs +++ b/src/tracks.rs @@ -1,19 +1,17 @@ use std::{io::Cursor, time::Duration}; use bytes::Bytes; +use inflector::Inflector; use rand::Rng; use reqwest::Client; use rodio::{Decoder, Source}; -pub type Data = Decoder>; - -async fn download(track: &str, client: &Client) -> eyre::Result { +async fn download(track: &str, client: &Client) -> eyre::Result { let url = format!("https://lofigirl.com/wp-content/uploads/{}", track); let response = client.get(url).send().await?; - let file = Cursor::new(response.bytes().await?); - let source = Decoder::new(file)?; + let data = response.bytes().await?; - Ok(source) + Ok(data) } async fn random() -> eyre::Result<&'static str> { @@ -26,44 +24,84 @@ async fn random() -> eyre::Result<&'static str> { Ok(track) } -#[derive(Debug, PartialEq, Clone, Copy)] +pub type DecodedData = Decoder>; + +/// 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 name: &'static str, + /// This is a formatted name, so it doesn't include the full path. + pub name: String, pub duration: Option, } impl TrackInfo { - pub fn format_name(&self) -> &'static str { - self.name + fn format_name(name: &'static str) -> String { + let mut formatted = name .split("/") .nth(2) .unwrap() .strip_suffix(".mp3") .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 -/// as well as some basic information about it. -pub struct Track { +/// This struct is seperate from [Track] since it is generated lazily from +/// a track, and not when the track is first downloaded. +pub struct DecodedTrack { pub info: TrackInfo, + pub data: DecodedData, +} - /// TODO: Make decoding lazy, since decoded files take up more memory than raw ones. - pub data: Data, +impl DecodedTrack { + pub fn new(track: Track) -> eyre::Result { + 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 { - /// 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 { let name = random().await?; let data = download(&name, client).await?; - Ok(Self { - info: TrackInfo { - name, - duration: data.total_duration(), - }, - data, - }) + Ok(Self { data, name }) + } + + /// This will actually decode and format the track, + /// returning a [`DecodedTrack`] which can be played + /// and also has a duration & formatted name. + pub fn decode(self) -> eyre::Result { + DecodedTrack::new(self) } }