diff --git a/CHILLHOP.md b/CHILLHOP.md index 6aebf22..ddbd687 100644 --- a/CHILLHOP.md +++ b/CHILLHOP.md @@ -1,5 +1,10 @@ # Using the chillhop list +> [!WARNING] +> As of lowfi 1.7.0, the chillhop list is included by default. For a more +> detailed explanation, see [MUSIC.md](MUSIC.md). This document is included +> to preserve any old links or references. The instructions are still valid. + ## Linux ```sh diff --git a/Cargo.lock b/Cargo.lock index 172a6c6..3228180 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1508,7 +1508,7 @@ checksum = "a7a70ba024b9dc04c27ea2f0c0548feb474ec5c54bba33a7f72f873a39d07b24" [[package]] name = "lowfi" -version = "1.7.1-dev" +version = "1.7.2-dev" dependencies = [ "arc-swap", "atomic_float", diff --git a/Cargo.toml b/Cargo.toml index 9c56985..367be91 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "lowfi" -version = "1.7.1-dev" +version = "1.7.2-dev" edition = "2021" description = "An extremely simple lofi player." license = "MIT" diff --git a/MUSIC.md b/MUSIC.md new file mode 100644 index 0000000..e09d492 --- /dev/null +++ b/MUSIC.md @@ -0,0 +1,75 @@ +# The State of Lowfi's Music + +> [!WARNING] +> This document will be a bit long and has almost nothing to do with the actual +> usage of lowfi, just the music embedded by default. + +Before that though, some context. lowfi includes an extensive track list +embedded into the software, so you can download it and have it "just work" +out of the box. + +I always hated apps that required extensive configuration just to be usable. +Sometimes it's justified, but often, it's just pointless when most will end up +with the same set of "defaults" that aren't really defaults. + +Lowfi is so nice and simple because of the "plug and play" aspect, +but it's become a lot harder to continue it as of late. + +## The Lofi Girl List + +Originally, it was planned that lowfi would use music scraped from Lofi Girl's own +website. The scraper actually came before the rest of the program, believe it or not. + +However, after a long period of downtime, the Lofi Girl website was redone without the +mp3 track files. Those are now pretty much inaccessible aside from paying for individual +albums on bandcamp which gets very expensive very quickly. + +Doing this was never actually disallowed, but it is now simply impossible. So, the question was, +what to do next after losing lowfi's primary source of music? + +## Tracklists + +I was originally against the idea of custom tracklists, because of my almost purist +ideals of a 100% no config at all vision for lowfi. But eventually, I gave in, which proved +to be a very good decision in hindsight. Now, regardless of what choices I make on the music +which is embedded, all may opt out of that and choose whatever they like. + +This culminated in a few templates located in the `data` directory of this repository +which included a handful of tracklists, and in particular, the chillhop list by user +[danielwerg](https://github.com/danielwerg). + +## The Switch + +After `lofigirl.com` went down, I thought a bit and eventually decided +to just bite the bullet and switch to the chillhop list. This was despite the fact +that chillhop entirely bans third party players in their TOS. They also ban +scrapers, which I only learned after writing one. + +So, is lowfi really going to have to violate the TOS of it's own music provider? +Well, yes. I thought about it, and came to the conclusion that lowfi is probably +not much of a threat for a few reasons. + +Firstly, it emulates exactly the behavior of chillhop's own radio player. +The only difference is that one shoves you into a web browser, and the other, +into a nice terminal window. + +Then, I also realize that lowfi is just a small program used by few. +I'm not making money on any of this, and I think degrading the experience for my +fellow nerds who just want to listen to some lowfi without all the crap is not worth it. + +At the end of the day, lowfi has a distinct UserAgent. Should chillhop ever take issue with +it's behaviour, banning it is extremely simple. I don't want that to happen, but I +understand if it does. + +## Well, *I* Hate the Chillhop Music + +It's not as "lofi". It is almost certainly a compromise, that much I cannot even pretend to +deny. I find myself hitting the skip button almost three times as often with chillhop. + +If you are undeterred enough by TOS's to read this far, then you can use the `archive.txt` +list in the `data` folder. The list is a product of me worrying that the tracks on `lofigirl.com` +could've possibly been lost somehow, relating to the website going down. + +It's hosted on `archive.org`, and could be taken down at any point for any reason. +Being derived from my own local archive, it retains ~2700 out of the ~3700 tracks. +That's not perfect, the organization is also *bad*, but it exists. \ No newline at end of file diff --git a/README.md b/README.md index 52dd14e..cd7ef87 100644 --- a/README.md +++ b/README.md @@ -8,9 +8,8 @@ It'll do this as simply as it can: no albums, no ads, just lofi. ## Disclaimer As of the 1.7.0 version of lowfi, **all** of the audio files embedded -by default are from [chillhop](https://chillhop.com/). - - +by default are from [chillhop](https://chillhop.com/). Read +[MUSIC.md] for more information. ## Why? @@ -154,15 +153,10 @@ slightly tweak the UI or behaviour of the menu. The flags can be viewed with `lo ### Scraping -lowfi also has a `scrape` command which is usually not relevant, but -if you're trying to download some files from Lofi Girls' website, -it can be useful. +lowfi also has an optional `scrape` command enabled by the `scrape` feature. +It's usually not very useful, but is included for transparency's sake. -An example of scrape is as follows, - -`lowfi scrape --extension zip --include-full` - -where more information can be found by running `lowfi help scrape`. +More information can be found by running `lowfi help scrape`. ### Custom Track Lists diff --git a/src/main.rs b/src/main.rs index d731ebe..92254c1 100644 --- a/src/main.rs +++ b/src/main.rs @@ -16,6 +16,7 @@ mod scrapers; #[cfg(feature = "scrape")] use crate::scrapers::Source; + /// An extremely simple lofi player. #[derive(Parser, Clone)] #[command(about, version)] @@ -41,6 +42,10 @@ struct Args { #[clap(long, short, default_value_t = 12)] fps: u8, + /// Timeout in seconds for music downloads. + #[clap(long, default_value_t = 3)] + timeout: u64, + /// Include ALSA & other logs. #[clap(long, short)] debug: bool, @@ -50,7 +55,7 @@ struct Args { width: usize, /// Use a custom track list - #[clap(long, short, alias = "list", short_alias = 'l')] + #[clap(long, short, alias = "list", alias = "tracks", short_alias = 'l')] track_list: Option, /// Internal song buffer size. diff --git a/src/play.rs b/src/play.rs index 41306eb..0c70229 100644 --- a/src/play.rs +++ b/src/play.rs @@ -70,7 +70,9 @@ pub async fn play(args: Args) -> eyre::Result<(), player::Error> { drop(stream); player.sink.stop(); - ui.map(|x| x.abort()); + if let Some(x) = ui { + x.abort(); + } Ok(()) } diff --git a/src/player.rs b/src/player.rs index 580d4a0..118a159 100644 --- a/src/player.rs +++ b/src/player.rs @@ -41,10 +41,6 @@ pub use error::Error; #[cfg(feature = "mpris")] pub mod mpris; -/// The time to wait in between errors. -/// TODO: Make this configurable. -const TIMEOUT: Duration = Duration::from_secs(3); - /// Main struct responsible for queuing up & playing tracks. // TODO: Consider refactoring [Player] from being stored in an [Arc], into containing many smaller [Arc]s. // TODO: In other words, this would change the type from `Arc` to just `Player`. @@ -76,6 +72,9 @@ pub struct Player { /// The bookmarks, which are saved on quit. pub bookmarks: Bookmarks, + /// The timeout for track downloads, as a [Duration]. + timeout: Duration, + /// The actual list of tracks to be played. list: List, @@ -144,7 +143,7 @@ impl Player { "/", env!("CARGO_PKG_VERSION") )) - .timeout(TIMEOUT * 5) + .timeout(Duration::from_secs(args.timeout * 5)) .build()?; let player = Self { @@ -152,6 +151,7 @@ impl Player { buffer_size: args.buffer_size, current: ArcSwapOption::new(None), progress: AtomicF32::new(0.0), + timeout: Duration::from_secs(args.timeout), bookmarks, client, sink, @@ -301,7 +301,7 @@ impl Player { let current = player.current.load(); let current = current.as_ref().unwrap(); - player.bookmarks.bookmark(&¤t).await?; + player.bookmarks.bookmark(current).await?; } Message::Quit => break, } diff --git a/src/player/bookmark.rs b/src/player/bookmark.rs index c71b7a4..93f9ade 100644 --- a/src/player/bookmark.rs +++ b/src/player/bookmark.rs @@ -1,12 +1,15 @@ -use std::io::SeekFrom; +//! Module for handling saving, loading, and adding +//! bookmarks. + use std::sync::atomic::AtomicBool; use tokio::fs::{create_dir_all, File, OpenOptions}; -use tokio::io::{self, AsyncReadExt, AsyncSeekExt, AsyncWriteExt}; +use tokio::io::{self, AsyncReadExt, AsyncWriteExt}; use tokio::sync::RwLock; use crate::{data_dir, tracks}; +/// Errors that might occur while managing bookmarks. #[derive(Debug, thiserror::Error)] pub enum BookmarkError { #[error("data directory not found")] @@ -18,24 +21,36 @@ pub enum BookmarkError { /// Manages the bookmarks in the current player. pub struct Bookmarks { + /// The different entries in the bookmarks file. entries: RwLock>, - file: RwLock, + + /// The internal bookmarked register, which keeps track + /// of whether a track is bookmarked or not. + /// + /// This is much more efficient than checking every single frame. bookmarked: AtomicBool, } impl Bookmarks { - pub async fn load() -> eyre::Result { + /// Actually opens the bookmarks file itself. + pub async fn open(write: bool) -> eyre::Result { let data_dir = data_dir().map_err(|_| BookmarkError::DataDir)?; create_dir_all(data_dir.clone()).await?; - let mut file = OpenOptions::new() + OpenOptions::new() .create(true) - .write(true) + .write(write) .read(true) .append(false) - .truncate(false) + .truncate(true) .open(data_dir.join("bookmarks.txt")) - .await?; + .await + .map_err(BookmarkError::Io) + } + + /// Loads bookmarks from the `bookmarks.txt` file. + pub async fn load() -> eyre::Result { + let mut file = Self::open(false).await?; let mut text = String::new(); file.read_to_string(&mut text).await?; @@ -45,29 +60,27 @@ impl Bookmarks { .trim() .lines() .filter_map(|x| { - if !x.is_empty() { - Some(x.to_string()) - } else { + if x.is_empty() { None + } else { + Some(x.to_string()) } }) .collect(); Ok(Self { entries: RwLock::new(lines), - file: RwLock::new(file), bookmarked: AtomicBool::new(false), }) } + // Saves the bookmarks to the `bookmarks.txt` file. pub async fn save(&self) -> eyre::Result<(), BookmarkError> { + let mut file = Self::open(true).await?; let text = format!("noheader\n{}", self.entries.read().await.join("\n")); - let mut lock = self.file.write().await; - lock.seek(SeekFrom::Start(0)).await?; - lock.set_len(0).await?; - lock.write_all(text.as_bytes()).await?; - lock.flush().await?; + file.write_all(text.as_bytes()).await?; + file.flush().await?; Ok(()) } @@ -91,10 +104,14 @@ impl Bookmarks { Ok(()) } + /// Returns whether a track is bookmarked or not by using the internal + /// bookmarked register. pub fn bookmarked(&self) -> bool { self.bookmarked.load(std::sync::atomic::Ordering::Relaxed) } + /// Sets the internal bookmarked register by checking against + /// the current track's info. pub async fn set_bookmarked(&self, track: &tracks::Info) { let val = self.entries.read().await.contains(&track.to_entry()); self.bookmarked diff --git a/src/player/downloader.rs b/src/player/downloader.rs index 1c71e6b..4963139 100644 --- a/src/player/downloader.rs +++ b/src/player/downloader.rs @@ -8,7 +8,7 @@ use tokio::{ time::sleep, }; -use super::{Player, TIMEOUT}; +use super::Player; /// This struct is responsible for downloading tracks in the background. /// @@ -53,7 +53,7 @@ impl Downloader { } if !error.is_timeout() { - sleep(TIMEOUT).await; + sleep(self.player.timeout).await; } } } diff --git a/src/player/queue.rs b/src/player/queue.rs index 6997b0b..1fd665f 100644 --- a/src/player/queue.rs +++ b/src/player/queue.rs @@ -6,7 +6,7 @@ use tokio::{sync::mpsc::Sender, time::sleep}; use crate::{ messages::Message, - player::{downloader::Downloader, Player, TIMEOUT}, + player::{downloader::Downloader, Player}, tracks, }; @@ -76,7 +76,7 @@ impl Player { } if !error.is_timeout() { - sleep(TIMEOUT).await; + sleep(player.timeout).await; } tx.send(Message::TryAgain).await?; diff --git a/src/scrapers.rs b/src/scrapers.rs index 522726d..041a696 100644 --- a/src/scrapers.rs +++ b/src/scrapers.rs @@ -12,6 +12,7 @@ pub mod archive; pub mod chillhop; pub mod lofigirl; +/// Represents the different sources which can be scraped. #[derive(Clone, Copy, PartialEq, Eq, Debug, ValueEnum)] pub enum Source { Lofigirl, @@ -20,6 +21,7 @@ pub enum Source { } impl Source { + /// Gets the cache directory name, for example, `chillhop`. pub fn cache_dir(&self) -> &'static str { match self { Source::Lofigirl => "lofigirl", @@ -28,6 +30,7 @@ impl Source { } } + /// Gets the full root URL of the source. pub fn url(&self) -> &'static str { match self { Source::Chillhop => "https://chillhop.com", diff --git a/src/tracks.rs b/src/tracks.rs index 78aad75..7ecf6f8 100644 --- a/src/tracks.rs +++ b/src/tracks.rs @@ -135,7 +135,7 @@ impl Info { .collect() } - /// Formats a name with [convert_case]. + /// Formats a name with [`convert_case`]. /// /// This will also strip the first few numbers that are /// usually present on most lofi tracks and do some other diff --git a/src/tracks/error.rs b/src/tracks/error.rs index 1f763d1..49a1e3c 100644 --- a/src/tracks/error.rs +++ b/src/tracks/error.rs @@ -44,7 +44,7 @@ where Kind: From, { fn from((track, err): (T, E)) -> Self { - Error { + Self { track: track.into(), kind: Kind::from(err), } diff --git a/src/tracks/list.rs b/src/tracks/list.rs index bf0d26e..e62b7c7 100644 --- a/src/tracks/list.rs +++ b/src/tracks/list.rs @@ -133,7 +133,7 @@ impl List { let name = custom_name.map_or_else( || super::TrackName::Raw(path.clone()), - |formatted| super::TrackName::Formatted(formatted), + super::TrackName::Formatted, ); Ok(QueuedTrack { @@ -153,7 +153,7 @@ impl List { Self { lines, - path: path.map(|s| s.to_owned()), + path: path.map(ToOwned::to_owned), name: name.to_owned(), } } @@ -168,11 +168,9 @@ impl List { let raw = fs::read_to_string(path.clone()).await?; // Get rid of special noheader case for tracklists without a header. - let raw = if let Some(stripped) = raw.strip_prefix("noheader") { - stripped - } else { - &raw - }; + let raw = raw + .strip_prefix("noheader") + .map_or(raw.as_ref(), |stripped| stripped); let name = path .file_stem()