mirror of
https://github.com/talwat/lowfi
synced 2025-01-28 19:31:27 +00:00
feat: make decoding lazy & improve song name formatting
This commit is contained in:
parent
04547195e4
commit
93a668bae0
14
Cargo.lock
generated
14
Cargo.lock
generated
@ -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",
|
||||||
|
@ -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"
|
||||||
|
@ -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
|
||||||
|
@ -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);
|
||||||
}
|
}
|
||||||
|
@ -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;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -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)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
Loading…
x
Reference in New Issue
Block a user