mirror of
https://github.com/talwat/lowfi
synced 2025-09-10 02:30:46 +00:00
feat: prepare for 1.7.0 release
docs: explain music situation docs: more internal documentation feat: make timeout configurable chore: clean up some sections of code
This commit is contained in:
parent
dd9aab7118
commit
84887ae01b
@ -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
|
||||
|
2
Cargo.lock
generated
2
Cargo.lock
generated
@ -1508,7 +1508,7 @@ checksum = "a7a70ba024b9dc04c27ea2f0c0548feb474ec5c54bba33a7f72f873a39d07b24"
|
||||
|
||||
[[package]]
|
||||
name = "lowfi"
|
||||
version = "1.7.1-dev"
|
||||
version = "1.7.2-dev"
|
||||
dependencies = [
|
||||
"arc-swap",
|
||||
"atomic_float",
|
||||
|
@ -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"
|
||||
|
75
MUSIC.md
Normal file
75
MUSIC.md
Normal file
@ -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.
|
16
README.md
16
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/).
|
||||
|
||||
<!-- TODO: Make seperate write-up about using chillhop. -->
|
||||
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
|
||||
|
||||
|
@ -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<String>,
|
||||
|
||||
/// Internal song buffer size.
|
||||
|
@ -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(())
|
||||
}
|
||||
|
@ -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<Player>` 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,
|
||||
}
|
||||
|
@ -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<Vec<String>>,
|
||||
file: RwLock<File>,
|
||||
|
||||
/// 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<Self, BookmarkError> {
|
||||
/// Actually opens the bookmarks file itself.
|
||||
pub async fn open(write: bool) -> eyre::Result<File, BookmarkError> {
|
||||
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<Self, BookmarkError> {
|
||||
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
|
||||
|
@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -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?;
|
||||
|
@ -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",
|
||||
|
@ -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
|
||||
|
@ -44,7 +44,7 @@ where
|
||||
Kind: From<E>,
|
||||
{
|
||||
fn from((track, err): (T, E)) -> Self {
|
||||
Error {
|
||||
Self {
|
||||
track: track.into(),
|
||||
kind: Kind::from(err),
|
||||
}
|
||||
|
@ -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()
|
||||
|
Loading…
x
Reference in New Issue
Block a user