mirror of
https://github.com/talwat/lowfi
synced 2025-02-21 15:02:03 +00:00
add functionality for custom track names
This commit is contained in:
parent
6a6823d078
commit
923ac05cf8
2
Cargo.lock
generated
2
Cargo.lock
generated
@ -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",
|
||||||
|
@ -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"
|
||||||
|
@ -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
2
data/chillhop.txt
Normal file
@ -0,0 +1,2 @@
|
|||||||
|
https://stream.chillhop.com/mp3/
|
||||||
|
5430
|
@ -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));
|
||||||
|
|
||||||
|
@ -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
|
||||||
|
@ -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.
|
||||||
|
@ -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,
|
||||||
|
Loading…
x
Reference in New Issue
Block a user