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]]
name = "lowfi"
version = "1.5.7"
version = "1.6.0"
dependencies = [
"Inflector",
"arc-swap",

View File

@ -1,6 +1,6 @@
[package]
name = "lowfi"
version = "1.5.7"
version = "1.6.0"
edition = "2021"
description = "An extremely simple lofi player."
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://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`
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.
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 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,
/// as well as clear the screen before drawing.
pub struct Window {
// Whether or not to include borders in the output.
/// Whether or not to include borders in the output.
borderless: bool,
/// The top & bottom borders, which are here since they can be
@ -72,12 +72,12 @@ impl Window {
/// * `width` - Width of the windows.
/// * `borderless` - Whether to include borders in the window, or not.
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);
[format!("{middle}"), format!("{middle}")]
} else {
[String::new(), String::new()]
};
Self {
@ -94,7 +94,7 @@ impl Window {
// Note that this will have a trailing newline, which we use later.
let menu: String = content.into_iter().fold(String::new(), |mut output, x| {
// Horizontal Padding & Border
let padding = if !self.borderless { "" } else { " " };
let padding = if self.borderless { " " } else { "" };
write!(output, "{padding} {} {padding}\r\n", x.reset()).unwrap();
output

View File

@ -45,25 +45,23 @@ impl Info {
/// This will also strip the first few numbers that are
/// usually present on most lofi tracks.
fn format_name(name: &str) -> String {
let formatted = Self::decode_url(
name.split('/')
.last()
.unwrap()
.strip_suffix(".mp3")
.unwrap(),
)
.to_lowercase()
.to_title_case()
// Inflector doesn't like contractions...
// Replaces a few very common ones.
// TODO: Properly handle these.
.replace(" S ", "'s ")
.replace(" T ", "'t ")
.replace(" D ", "'d ")
.replace(" Ve ", "'ve ")
.replace(" Ll ", "'ll ")
.replace(" Re ", "'re ")
.replace(" M ", "'m ");
let split = name.split('/').last().unwrap();
let stripped = split.strip_suffix(".mp3").unwrap_or(split);
let formatted = Self::decode_url(stripped)
.to_lowercase()
.to_title_case()
// Inflector doesn't like contractions...
// Replaces a few very common ones.
// TODO: Properly handle these.
.replace(" S ", "'s ")
.replace(" T ", "'t ")
.replace(" D ", "'d ")
.replace(" Ve ", "'ve ")
.replace(" Ll ", "'ll ")
.replace(" Re ", "'re ")
.replace(" M ", "'m ");
// This is incremented for each digit in front of the song name.
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. */)]
String::from(&formatted[skip..])
// If the entire name of the track is a number, then just return it.
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.
pub fn new(name: &str, decoded: &DecodedData) -> Self {
let name = Self::format_name(name);
/// Creates a new [`TrackInfo`] from a possibly raw name & decoded track data.
pub fn new(name: TrackName, decoded: &DecodedData) -> Self {
let name = match name {
TrackName::Raw(raw) => Self::format_name(&raw),
TrackName::Formatted(formatted) => formatted,
};
Self {
duration: decoded.total_duration(),
@ -107,16 +113,30 @@ impl Decoded {
/// This is equivalent to [`Track::decode`].
pub fn new(track: Track) -> eyre::Result<Self> {
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 })
}
}
/// 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.
pub struct Track {
/// This name is not formatted, and also includes the month & year of the track.
pub name: String,
/// Name of the track.
pub name: TrackName,
/// The raw data of the track, which is not decoded and
/// therefore much more memory efficient.

View File

@ -28,15 +28,25 @@ impl List {
self.lines[0].trim()
}
/// Gets the name of a random track.
fn random_name(&self) -> String {
/// Gets the path of a random track.
///
/// 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 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.
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.
@ -56,18 +66,19 @@ impl List {
/// Fetches and downloads a random track from the [List].
pub async fn random(&self, client: &Client) -> reqwest::Result<Track> {
let name = self.random_name();
let data = self.download(&name, client).await?;
let (path, custom_name) = self.random_path();
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 })
}
/// Parses text into a [List].
pub fn new(name: &str, text: &str) -> Self {
let lines: Vec<String> = text
.split_ascii_whitespace()
.map(ToOwned::to_owned)
.collect();
let lines: Vec<String> = text.trim().lines().map(|x| x.trim().to_owned()).collect();
Self {
lines,