diff --git a/Cargo.lock b/Cargo.lock index 0ffe60d..55bbb79 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1453,7 +1453,7 @@ checksum = "a7a70ba024b9dc04c27ea2f0c0548feb474ec5c54bba33a7f72f873a39d07b24" [[package]] name = "lowfi" -version = "1.5.7" +version = "1.6.0" dependencies = [ "Inflector", "arc-swap", diff --git a/Cargo.toml b/Cargo.toml index bc0a22c..c2850aa 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -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" diff --git a/README.md b/README.md index 19453df..ff58420 100644 --- a/README.md +++ b/README.md @@ -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. \ No newline at end of file diff --git a/data/chillhop.txt b/data/chillhop.txt new file mode 100644 index 0000000..3b541bb --- /dev/null +++ b/data/chillhop.txt @@ -0,0 +1,2 @@ +https://stream.chillhop.com/mp3/ +5430 \ No newline at end of file diff --git a/src/play.rs b/src/play.rs index 70ab6e4..dd6784f 100644 --- a/src/play.rs +++ b/src/play.rs @@ -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)); diff --git a/src/player/ui.rs b/src/player/ui.rs index 87c9c17..937caaa 100644 --- a/src/player/ui.rs +++ b/src/player/ui.rs @@ -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 diff --git a/src/tracks.rs b/src/tracks.rs index 3ca7c62..0ad8167 100644 --- a/src/tracks.rs +++ b/src/tracks.rs @@ -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 { 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. diff --git a/src/tracks/list.rs b/src/tracks/list.rs index 211d302..8cfe892 100644 --- a/src/tracks/list.rs +++ b/src/tracks/list.rs @@ -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) { // 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 { - 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 = text - .split_ascii_whitespace() - .map(ToOwned::to_owned) - .collect(); + let lines: Vec = text.trim().lines().map(|x| x.trim().to_owned()).collect(); Self { lines,