add functionality for custom track names

This commit is contained in:
Tal 2025-02-15 12:51:53 +01:00
parent 6a6823d078
commit 923ac05cf8
8 changed files with 87 additions and 44 deletions

2
Cargo.lock generated
View File

@ -1453,7 +1453,7 @@ checksum = "a7a70ba024b9dc04c27ea2f0c0548feb474ec5c54bba33a7f72f873a39d07b24"
[[package]] [[package]]
name = "lowfi" name = "lowfi"
version = "1.5.7" version = "1.6.0"
dependencies = [ dependencies = [
"Inflector", "Inflector",
"arc-swap", "arc-swap",

View File

@ -1,6 +1,6 @@
[package] [package]
name = "lowfi" name = "lowfi"
version = "1.5.7" version = "1.6.0"
edition = "2021" edition = "2021"
description = "An extremely simple lofi player." description = "An extremely simple lofi player."
license = "MIT" license = "MIT"

View File

@ -173,3 +173,12 @@ lowfi would download these three URLs:
- `https://lofigirl.com/wp-content/uploads/2023/06/Foudroie-Finding-The-Edge-V2.mp3` - `https://lofigirl.com/wp-content/uploads/2023/06/Foudroie-Finding-The-Edge-V2.mp3`
- `https://file-examples.com/storage/fea570b16e6703ef79e65b4/2017/11/file_example_MP3_5MG.mp3` - `https://file-examples.com/storage/fea570b16e6703ef79e65b4/2017/11/file_example_MP3_5MG.mp3`
- `https://lofigirl.com/wp-content/uploads/2023/04/2-In-Front-Of-Me.mp3` - `https://lofigirl.com/wp-content/uploads/2023/04/2-In-Front-Of-Me.mp3`
Additionally, you may also specify a custom display name for the track which is indicated by a `!`.
For example, if you had an entry like this:
```
2023/04/2-In-Front-Of-Me.mp3!custom name
```
Then lowfi would download from the first section, and display the second.

2
data/chillhop.txt Normal file
View File

@ -0,0 +1,2 @@
https://stream.chillhop.com/mp3/
5430

View File

@ -76,6 +76,7 @@ pub async fn play(args: Args) -> eyre::Result<()> {
// Actually initializes the player. // Actually initializes the player.
let player = Arc::new(Player::new(&args).await?); let player = Arc::new(Player::new(&args).await?);
// Initialize the UI, as well as the internal communication channel.
let (tx, rx) = mpsc::channel(8); let (tx, rx) = mpsc::channel(8);
let ui = task::spawn(ui::start(Arc::clone(&player), tx.clone(), args)); let ui = task::spawn(ui::start(Arc::clone(&player), tx.clone(), args));

View File

@ -53,7 +53,7 @@ lazy_static! {
/// The main purpose of this struct is just to add the fancy border, /// The main purpose of this struct is just to add the fancy border,
/// as well as clear the screen before drawing. /// as well as clear the screen before drawing.
pub struct Window { pub struct Window {
// Whether or not to include borders in the output. /// Whether or not to include borders in the output.
borderless: bool, borderless: bool,
/// The top & bottom borders, which are here since they can be /// The top & bottom borders, which are here since they can be
@ -72,12 +72,12 @@ impl Window {
/// * `width` - Width of the windows. /// * `width` - Width of the windows.
/// * `borderless` - Whether to include borders in the window, or not. /// * `borderless` - Whether to include borders in the window, or not.
pub fn new(width: usize, borderless: bool) -> Self { pub fn new(width: usize, borderless: bool) -> Self {
let borders = if !borderless { let borders = if borderless {
[String::new(), String::new()]
} else {
let middle = "".repeat(width + 2); let middle = "".repeat(width + 2);
[format!("{middle}"), format!("{middle}")] [format!("{middle}"), format!("{middle}")]
} else {
[String::new(), String::new()]
}; };
Self { Self {
@ -94,7 +94,7 @@ impl Window {
// Note that this will have a trailing newline, which we use later. // Note that this will have a trailing newline, which we use later.
let menu: String = content.into_iter().fold(String::new(), |mut output, x| { let menu: String = content.into_iter().fold(String::new(), |mut output, x| {
// Horizontal Padding & Border // Horizontal Padding & Border
let padding = if !self.borderless { "" } else { " " }; let padding = if self.borderless { " " } else { "" };
write!(output, "{padding} {} {padding}\r\n", x.reset()).unwrap(); write!(output, "{padding} {} {padding}\r\n", x.reset()).unwrap();
output output

View File

@ -45,25 +45,23 @@ impl Info {
/// This will also strip the first few numbers that are /// This will also strip the first few numbers that are
/// usually present on most lofi tracks. /// usually present on most lofi tracks.
fn format_name(name: &str) -> String { fn format_name(name: &str) -> String {
let formatted = Self::decode_url( let split = name.split('/').last().unwrap();
name.split('/')
.last() let stripped = split.strip_suffix(".mp3").unwrap_or(split);
.unwrap()
.strip_suffix(".mp3") let formatted = Self::decode_url(stripped)
.unwrap(), .to_lowercase()
) .to_title_case()
.to_lowercase() // Inflector doesn't like contractions...
.to_title_case() // Replaces a few very common ones.
// Inflector doesn't like contractions... // TODO: Properly handle these.
// Replaces a few very common ones. .replace(" S ", "'s ")
// TODO: Properly handle these. .replace(" T ", "'t ")
.replace(" S ", "'s ") .replace(" D ", "'d ")
.replace(" T ", "'t ") .replace(" Ve ", "'ve ")
.replace(" D ", "'d ") .replace(" Ll ", "'ll ")
.replace(" Ve ", "'ve ") .replace(" Re ", "'re ")
.replace(" Ll ", "'ll ") .replace(" M ", "'m ");
.replace(" Re ", "'re ")
.replace(" M ", "'m ");
// This is incremented for each digit in front of the song name. // This is incremented for each digit in front of the song name.
let mut skip = 0; let mut skip = 0;
@ -76,13 +74,21 @@ impl Info {
} }
} }
#[allow(clippy::string_slice, /* We've already checked before that the bound is at an ASCII digit. */)] // If the entire name of the track is a number, then just return it.
String::from(&formatted[skip..]) if skip == formatted.len() {
formatted
} else {
#[allow(clippy::string_slice, /* We've already checked before that the bound is at an ASCII digit. */)]
String::from(&formatted[skip..])
}
} }
/// Creates a new [`TrackInfo`] from a raw name & decoded track data. /// Creates a new [`TrackInfo`] from a possibly raw name & decoded track data.
pub fn new(name: &str, decoded: &DecodedData) -> Self { pub fn new(name: TrackName, decoded: &DecodedData) -> Self {
let name = Self::format_name(name); let name = match name {
TrackName::Raw(raw) => Self::format_name(&raw),
TrackName::Formatted(formatted) => formatted,
};
Self { Self {
duration: decoded.total_duration(), duration: decoded.total_duration(),
@ -107,16 +113,30 @@ impl Decoded {
/// This is equivalent to [`Track::decode`]. /// This is equivalent to [`Track::decode`].
pub fn new(track: Track) -> eyre::Result<Self> { pub fn new(track: Track) -> eyre::Result<Self> {
let data = Decoder::new(Cursor::new(track.data))?; let data = Decoder::new(Cursor::new(track.data))?;
let info = Info::new(&track.name, &data); let info = Info::new(track.name, &data);
Ok(Self { info, data }) Ok(Self { info, data })
} }
} }
/// Specifies a track's name, and specifically,
/// whether it has already been formatted or if it
/// is still in it's raw form.
#[derive(Debug, Clone)]
pub enum TrackName {
/// Pulled straight from the list,
/// with no splitting done at all.
Raw(String),
/// If a track has a custom specified name
/// in the list, then it should be defined with this variant.
Formatted(String),
}
/// The main track struct, which only includes data & the track name. /// The main track struct, which only includes data & the track name.
pub struct Track { pub struct Track {
/// This name is not formatted, and also includes the month & year of the track. /// Name of the track.
pub name: String, pub name: TrackName,
/// The raw data of the track, which is not decoded and /// The raw data of the track, which is not decoded and
/// therefore much more memory efficient. /// therefore much more memory efficient.

View File

@ -28,15 +28,25 @@ impl List {
self.lines[0].trim() self.lines[0].trim()
} }
/// Gets the name of a random track. /// Gets the path of a random track.
fn random_name(&self) -> String { ///
/// The second value in the tuple specifies whether the
/// track has a custom display name.
fn random_path(&self) -> (String, Option<String>) {
// We're getting from 1 here, since the base is at `self.lines[0]`. // We're getting from 1 here, since the base is at `self.lines[0]`.
// //
// We're also not pre-trimming `self.lines` into `base` & `tracks` due to // We're also not pre-trimming `self.lines` into `base` & `tracks` due to
// how rust vectors work, sinceslow to drain only a single element from // how rust vectors work, since it is slower to drain only a single element from
// the start, so it's faster to just keep it in & work around it. // the start, so it's faster to just keep it in & work around it.
let random = rand::thread_rng().gen_range(1..self.lines.len()); let random = rand::thread_rng().gen_range(1..self.lines.len());
self.lines[random].clone() let line = self.lines[random].clone();
let split: Vec<&str> = line.split('!').collect();
if split.len() == 1 {
(line, None)
} else {
(split[0].to_owned(), Some(split[1].to_owned()))
}
} }
/// Downloads a raw track, but doesn't decode it. /// Downloads a raw track, but doesn't decode it.
@ -56,18 +66,19 @@ impl List {
/// Fetches and downloads a random track from the [List]. /// Fetches and downloads a random track from the [List].
pub async fn random(&self, client: &Client) -> reqwest::Result<Track> { pub async fn random(&self, client: &Client) -> reqwest::Result<Track> {
let name = self.random_name(); let (path, custom_name) = self.random_path();
let data = self.download(&name, client).await?; let data = self.download(&path, client).await?;
let name = custom_name.map_or(super::TrackName::Raw(path), |formatted| {
super::TrackName::Formatted(formatted)
});
Ok(Track { name, data }) Ok(Track { name, data })
} }
/// Parses text into a [List]. /// Parses text into a [List].
pub fn new(name: &str, text: &str) -> Self { pub fn new(name: &str, text: &str) -> Self {
let lines: Vec<String> = text let lines: Vec<String> = text.trim().lines().map(|x| x.trim().to_owned()).collect();
.split_ascii_whitespace()
.map(ToOwned::to_owned)
.collect();
Self { Self {
lines, lines,