mirror of
				https://github.com/talwat/lowfi
				synced 2025-10-30 18:58:45 +00:00 
			
		
		
		
	feat: initial commit
This commit is contained in:
		
						commit
						f0e56ea2aa
					
				
							
								
								
									
										1
									
								
								.gitignore
									
									
									
									
										vendored
									
									
										Normal file
									
								
							
							
						
						
									
										1
									
								
								.gitignore
									
									
									
									
										vendored
									
									
										Normal file
									
								
							| @ -0,0 +1 @@ | |||||||
|  | /target | ||||||
							
								
								
									
										2537
									
								
								Cargo.lock
									
									
									
										generated
									
									
									
										Normal file
									
								
							
							
						
						
									
										2537
									
								
								Cargo.lock
									
									
									
										generated
									
									
									
										Normal file
									
								
							
										
											
												File diff suppressed because it is too large
												Load Diff
											
										
									
								
							
							
								
								
									
										15
									
								
								Cargo.toml
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										15
									
								
								Cargo.toml
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,15 @@ | |||||||
|  | [package] | ||||||
|  | name = "lowifi" | ||||||
|  | version = "0.1.0" | ||||||
|  | edition = "2021" | ||||||
|  | 
 | ||||||
|  | [dependencies] | ||||||
|  | clap = { version = "4.5.18", features = ["derive", "cargo"] } | ||||||
|  | reqwest = { version = "0.12.7", features = ["blocking"] } | ||||||
|  | tokio = { version = "1.40.0", features = ["full"] } | ||||||
|  | scraper = "0.20.0" | ||||||
|  | rodio = { version = "0.19.0", features = ["minimp3"], default-features = false } | ||||||
|  | eyre = "0.6.12" | ||||||
|  | futures = "0.3.30" | ||||||
|  | bytes = "1.7.2" | ||||||
|  | rand = "0.8.5" | ||||||
							
								
								
									
										10
									
								
								README.md
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										10
									
								
								README.md
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,10 @@ | |||||||
|  | # lowfi | ||||||
|  | 
 | ||||||
|  | lowfi is a tiny rust app that serves a single purpose: play lofi. | ||||||
|  | It'll do this as simply as it can: no albums, no ads, just lofi. | ||||||
|  | 
 | ||||||
|  | ## Why? | ||||||
|  | 
 | ||||||
|  | I really hate modern music platforms, and I wanted a small, "suckless" | ||||||
|  | app that would literally just play lofi without video so I could use it | ||||||
|  | whenever. | ||||||
							
								
								
									
										3771
									
								
								data/tracks.txt
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										3771
									
								
								data/tracks.txt
									
									
									
									
									
										Normal file
									
								
							
										
											
												File diff suppressed because it is too large
												Load Diff
											
										
									
								
							
							
								
								
									
										30
									
								
								src/main.rs
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										30
									
								
								src/main.rs
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,30 @@ | |||||||
|  | use clap::{Parser, Subcommand}; | ||||||
|  | 
 | ||||||
|  | mod scrape; | ||||||
|  | mod tracks; | ||||||
|  | 
 | ||||||
|  | /// An extremely simple lofi player.
 | ||||||
|  | #[derive(Parser)] | ||||||
|  | #[command(about)] | ||||||
|  | struct Args { | ||||||
|  |     #[command(subcommand)] | ||||||
|  |     command: Commands, | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | #[derive(Subcommand)] | ||||||
|  | enum Commands { | ||||||
|  |     /// Scrapes the lofi girl website file server for mp3 files.
 | ||||||
|  |     Scrape, | ||||||
|  |     /// Plays a single, random, track.
 | ||||||
|  |     Play | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | #[tokio::main] | ||||||
|  | async fn main() -> eyre::Result<()> { | ||||||
|  |     let cli = Args::parse(); | ||||||
|  | 
 | ||||||
|  |     match cli.command { | ||||||
|  |         Commands::Scrape => scrape::scrape().await, | ||||||
|  |         Commands::Play => tracks::random().await | ||||||
|  |     } | ||||||
|  | } | ||||||
							
								
								
									
										34
									
								
								src/play.rs
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										34
									
								
								src/play.rs
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,34 @@ | |||||||
|  | use std::{io::Cursor, time::Duration}; | ||||||
|  | 
 | ||||||
|  | use rodio::{Decoder, OutputStream, Sink, Source}; | ||||||
|  | use tokio::time::sleep; | ||||||
|  | 
 | ||||||
|  | pub async fn download() { | ||||||
|  |     
 | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | pub async fn play(track: &str) -> eyre::Result<()> { | ||||||
|  |     eprintln!("downloading {}...", track); | ||||||
|  |     let url = format!("https://lofigirl.com/wp-content/uploads/{}", track); | ||||||
|  |     let file = Cursor::new(reqwest::get(url).await?.bytes().await?); | ||||||
|  | 
 | ||||||
|  |     let source = Decoder::new(file).unwrap(); | ||||||
|  | 
 | ||||||
|  |     let (stream, stream_handle) = OutputStream::try_default().unwrap(); | ||||||
|  |     let sink = Sink::try_new(&stream_handle).unwrap(); | ||||||
|  |     sink.append(source); | ||||||
|  | 
 | ||||||
|  |     eprintln!("playing {}...", track); | ||||||
|  |     sink.sleep_until_end(); | ||||||
|  | 
 | ||||||
|  |     Ok(()) | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | pub async fn random() -> eyre::Result<()> { | ||||||
|  |     let tracks = include_str!("../data/tracks.txt"); | ||||||
|  |     let tracks: Vec<&str> = tracks.split_ascii_whitespace().collect(); | ||||||
|  | 
 | ||||||
|  |     play(tracks[0]).await?; | ||||||
|  | 
 | ||||||
|  |     Ok(()) | ||||||
|  | } | ||||||
							
								
								
									
										69
									
								
								src/scrape.rs
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										69
									
								
								src/scrape.rs
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,69 @@ | |||||||
|  | use std::sync::LazyLock; | ||||||
|  | 
 | ||||||
|  | use futures::{stream::FuturesUnordered, StreamExt}; | ||||||
|  | use scraper::{Html, Selector}; | ||||||
|  | 
 | ||||||
|  | static SELECTOR: LazyLock<Selector> = LazyLock::new(|| { | ||||||
|  |     Selector::parse("html > body > pre > a").unwrap() | ||||||
|  | }); | ||||||
|  | 
 | ||||||
|  | async fn parse(path: &str) -> eyre::Result<Vec<String>> { | ||||||
|  |     let response = reqwest::get(format!("https://lofigirl.com/wp-content/uploads/{}", path)).await?; | ||||||
|  |     let document = response.text().await?; | ||||||
|  | 
 | ||||||
|  |     let html = Html::parse_document(&document); | ||||||
|  |     Ok(html.select(&SELECTOR).skip(5).map(|x| String::from(x.attr("href").unwrap())).collect()) | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | /// This function basically just scans the entire file server, and returns a list of paths to mp3 files.
 | ||||||
|  | /// 
 | ||||||
|  | /// It's a bit hacky, and basically works by checking all of the years, then months, and then all of the files.
 | ||||||
|  | /// This is done as a way to avoid recursion, since async rust really hates recursive functions.
 | ||||||
|  | async fn scan() -> eyre::Result<Vec<String>> { | ||||||
|  |     let items = parse("").await?; | ||||||
|  |     
 | ||||||
|  |     let years: Vec<u32> = items.iter().filter_map(|x| { | ||||||
|  |         let year = x.strip_suffix("/")?; | ||||||
|  |         year.parse().ok() | ||||||
|  |     }).collect(); | ||||||
|  | 
 | ||||||
|  |     // A little bit of async to run all of the months concurrently.
 | ||||||
|  |     let mut futures = FuturesUnordered::new(); | ||||||
|  |     
 | ||||||
|  |     for year in years { | ||||||
|  |         let months = parse(&year.to_string()).await?; | ||||||
|  |         
 | ||||||
|  |         for month in months { | ||||||
|  |             futures.push(async move { | ||||||
|  |                 let path = format!("{}/{}", year, month); | ||||||
|  | 
 | ||||||
|  |                 let items = parse(&path).await.unwrap(); | ||||||
|  |                 let items = items.into_iter().filter_map(|x| { | ||||||
|  |                     if x.ends_with(".mp3") { | ||||||
|  |                         Some(format!("{path}{x}")) | ||||||
|  |                     } else { | ||||||
|  |                         None | ||||||
|  |                     } | ||||||
|  |                 }).collect::<Vec<String>>(); | ||||||
|  | 
 | ||||||
|  |                 items | ||||||
|  |             }); | ||||||
|  |         } | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     let mut files = Vec::new(); | ||||||
|  |     while let Some(mut result) = futures.next().await { | ||||||
|  |         files.append(&mut result); | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     eyre::Result::Ok(files) | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | pub async fn scrape() -> eyre::Result<()> { | ||||||
|  |     let files = scan().await?; | ||||||
|  |     for file in files { | ||||||
|  |         println!("{}", file); | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     Ok(()) | ||||||
|  | } | ||||||
							
								
								
									
										39
									
								
								src/tracks.rs
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										39
									
								
								src/tracks.rs
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,39 @@ | |||||||
|  | use std::io::Cursor; | ||||||
|  | 
 | ||||||
|  | use bytes::Bytes; | ||||||
|  | use rand::Rng; | ||||||
|  | use rodio::{Decoder, OutputStream, Sink}; | ||||||
|  | 
 | ||||||
|  | pub async fn download(track: &str) -> eyre::Result<Decoder<Cursor<Bytes>>> { | ||||||
|  |     let url = format!("https://lofigirl.com/wp-content/uploads/{}", track); | ||||||
|  |     let file = Cursor::new(reqwest::get(url).await?.bytes().await?); | ||||||
|  |     let source = Decoder::new(file).unwrap(); | ||||||
|  |     
 | ||||||
|  |     Ok(source) | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | pub async fn play(source: Decoder<Cursor<Bytes>>) -> eyre::Result<()> { | ||||||
|  |     let (stream, stream_handle) = OutputStream::try_default()?; | ||||||
|  |     let sink = Sink::try_new(&stream_handle)?; | ||||||
|  |     sink.append(source); | ||||||
|  | 
 | ||||||
|  |     sink.sleep_until_end(); | ||||||
|  | 
 | ||||||
|  |     Ok(()) | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | pub async fn random() -> eyre::Result<()> { | ||||||
|  |     let tracks = include_str!("../data/tracks.txt"); | ||||||
|  |     let tracks: Vec<&str> = tracks.split_ascii_whitespace().collect(); | ||||||
|  | 
 | ||||||
|  |     let random = rand::thread_rng().gen_range(0..tracks.len()); | ||||||
|  |     let track = tracks[random]; | ||||||
|  | 
 | ||||||
|  |     eprintln!("downloading {}...", track); | ||||||
|  |     let source = download(track).await?; | ||||||
|  | 
 | ||||||
|  |     eprintln!("playing {}...", track); | ||||||
|  |     play(source).await?; | ||||||
|  | 
 | ||||||
|  |     Ok(()) | ||||||
|  | } | ||||||
		Loading…
	
	
			
			x
			
			
		
	
		Reference in New Issue
	
	Block a user