mirror of
				https://github.com/talwat/lowfi
				synced 2025-10-30 18:58:45 +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]] | ||||
| name = "lowfi" | ||||
| version = "1.5.7" | ||||
| version = "1.6.0" | ||||
| dependencies = [ | ||||
|  "Inflector", | ||||
|  "arc-swap", | ||||
|  | ||||
| @ -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" | ||||
|  | ||||
| @ -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
									
								
							
							
						
						
									
										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.
 | ||||
|     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)); | ||||
| 
 | ||||
|  | ||||
| @ -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 | ||||
|  | ||||
| @ -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.
 | ||||
|  | ||||
| @ -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, | ||||
|  | ||||
		Loading…
	
	
			
			x
			
			
		
	
		Reference in New Issue
	
	Block a user