mirror of
https://github.com/talwat/lowfi
synced 2025-12-15 11:15:26 +00:00
Compare commits
4 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
905e0ee098 | ||
|
|
af8d45905f | ||
|
|
4f3fa02cb4 | ||
|
|
702f29978f |
100
Cargo.lock
generated
100
Cargo.lock
generated
@ -286,9 +286,9 @@ checksum = "b35204fbdc0b3f4446b89fc1ac2cf84a8a68971995d0bf2e925ec7cd960f9cb3"
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "cc"
|
name = "cc"
|
||||||
version = "1.2.48"
|
version = "1.2.49"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "c481bdbf0ed3b892f6f806287d72acd515b352a4ec27a208489b8c1bc839633a"
|
checksum = "90583009037521a116abf44494efecd645ba48b6622457080f080b85544e2215"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"find-msvc-tools",
|
"find-msvc-tools",
|
||||||
"shlex",
|
"shlex",
|
||||||
@ -737,21 +737,6 @@ dependencies = [
|
|||||||
"new_debug_unreachable",
|
"new_debug_unreachable",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "futures"
|
|
||||||
version = "0.3.31"
|
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "65bc07b1a8bc7c85c5f2e110c476c7389b4554ba72af57d8445ea63a576b0876"
|
|
||||||
dependencies = [
|
|
||||||
"futures-channel",
|
|
||||||
"futures-core",
|
|
||||||
"futures-executor",
|
|
||||||
"futures-io",
|
|
||||||
"futures-sink",
|
|
||||||
"futures-task",
|
|
||||||
"futures-util",
|
|
||||||
]
|
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "futures-channel"
|
name = "futures-channel"
|
||||||
version = "0.3.31"
|
version = "0.3.31"
|
||||||
@ -759,7 +744,6 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
|
|||||||
checksum = "2dff15bf788c671c1934e366d07e30c1814a8ef514e1af724a602e8a2fbe1b10"
|
checksum = "2dff15bf788c671c1934e366d07e30c1814a8ef514e1af724a602e8a2fbe1b10"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"futures-core",
|
"futures-core",
|
||||||
"futures-sink",
|
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
@ -768,17 +752,6 @@ version = "0.3.31"
|
|||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "05f29059c0c2090612e8d742178b0580d2dc940c837851ad723096f87af6663e"
|
checksum = "05f29059c0c2090612e8d742178b0580d2dc940c837851ad723096f87af6663e"
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "futures-executor"
|
|
||||||
version = "0.3.31"
|
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "1e28d1d997f585e54aebc3f97d39e72338912123a67330d723fdbb564d646c9f"
|
|
||||||
dependencies = [
|
|
||||||
"futures-core",
|
|
||||||
"futures-task",
|
|
||||||
"futures-util",
|
|
||||||
]
|
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "futures-io"
|
name = "futures-io"
|
||||||
version = "0.3.31"
|
version = "0.3.31"
|
||||||
@ -827,7 +800,6 @@ version = "0.3.31"
|
|||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "9fa08315bb612088cc391249efdc3bc77536f16c91f6cf495e6fbe85b20a4a81"
|
checksum = "9fa08315bb612088cc391249efdc3bc77536f16c91f6cf495e6fbe85b20a4a81"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"futures-channel",
|
|
||||||
"futures-core",
|
"futures-core",
|
||||||
"futures-io",
|
"futures-io",
|
||||||
"futures-macro",
|
"futures-macro",
|
||||||
@ -1055,11 +1027,9 @@ dependencies = [
|
|||||||
"percent-encoding",
|
"percent-encoding",
|
||||||
"pin-project-lite",
|
"pin-project-lite",
|
||||||
"socket2",
|
"socket2",
|
||||||
"system-configuration",
|
|
||||||
"tokio",
|
"tokio",
|
||||||
"tower-service",
|
"tower-service",
|
||||||
"tracing",
|
"tracing",
|
||||||
"windows-registry",
|
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
@ -1319,7 +1289,7 @@ dependencies = [
|
|||||||
"dirs",
|
"dirs",
|
||||||
"eyre",
|
"eyre",
|
||||||
"fastrand",
|
"fastrand",
|
||||||
"futures",
|
"futures-util",
|
||||||
"html-escape",
|
"html-escape",
|
||||||
"indicatif",
|
"indicatif",
|
||||||
"libc",
|
"libc",
|
||||||
@ -1387,12 +1357,6 @@ dependencies = [
|
|||||||
"autocfg",
|
"autocfg",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "mime"
|
|
||||||
version = "0.3.17"
|
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "6877bb514081ee2a7ff5ef9de3281f14a4dd4bceac4c09388074a6b5df8a139a"
|
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "mio"
|
name = "mio"
|
||||||
version = "1.1.1"
|
version = "1.1.1"
|
||||||
@ -1926,7 +1890,6 @@ checksum = "9d0946410b9f7b082a427e4ef5c8ff541a88b357bc6c637c40db3a68ac70a36f"
|
|||||||
dependencies = [
|
dependencies = [
|
||||||
"base64",
|
"base64",
|
||||||
"bytes",
|
"bytes",
|
||||||
"encoding_rs",
|
|
||||||
"futures-core",
|
"futures-core",
|
||||||
"futures-util",
|
"futures-util",
|
||||||
"h2",
|
"h2",
|
||||||
@ -1939,7 +1902,6 @@ dependencies = [
|
|||||||
"hyper-util",
|
"hyper-util",
|
||||||
"js-sys",
|
"js-sys",
|
||||||
"log",
|
"log",
|
||||||
"mime",
|
|
||||||
"native-tls",
|
"native-tls",
|
||||||
"percent-encoding",
|
"percent-encoding",
|
||||||
"pin-project-lite",
|
"pin-project-lite",
|
||||||
@ -2507,27 +2469,6 @@ dependencies = [
|
|||||||
"syn",
|
"syn",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "system-configuration"
|
|
||||||
version = "0.6.1"
|
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "3c879d448e9d986b661742763247d3693ed13609438cf3d006f51f5368a5ba6b"
|
|
||||||
dependencies = [
|
|
||||||
"bitflags 2.10.0",
|
|
||||||
"core-foundation",
|
|
||||||
"system-configuration-sys",
|
|
||||||
]
|
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "system-configuration-sys"
|
|
||||||
version = "0.6.0"
|
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "8e1d1b10ced5ca923a1fcb8d03e96b8d3268065d724548c0211415ff6ac6bac4"
|
|
||||||
dependencies = [
|
|
||||||
"core-foundation-sys",
|
|
||||||
"libc",
|
|
||||||
]
|
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "tempfile"
|
name = "tempfile"
|
||||||
version = "3.23.0"
|
version = "3.23.0"
|
||||||
@ -2672,9 +2613,9 @@ dependencies = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "toml_edit"
|
name = "toml_edit"
|
||||||
version = "0.23.7"
|
version = "0.23.9"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "6485ef6d0d9b5d0ec17244ff7eb05310113c3f316f2d14200d4de56b3cb98f8d"
|
checksum = "5d7cbc3b4b49633d57a0509303158ca50de80ae32c265093b24c414705807832"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"indexmap",
|
"indexmap",
|
||||||
"toml_datetime",
|
"toml_datetime",
|
||||||
@ -3062,7 +3003,7 @@ version = "0.54.0"
|
|||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "12661b9c89351d684a50a8a643ce5f608e20243b9fb84687800163429f161d65"
|
checksum = "12661b9c89351d684a50a8a643ce5f608e20243b9fb84687800163429f161d65"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"windows-result 0.1.2",
|
"windows-result",
|
||||||
"windows-targets 0.52.6",
|
"windows-targets 0.52.6",
|
||||||
]
|
]
|
||||||
|
|
||||||
@ -3072,17 +3013,6 @@ version = "0.2.1"
|
|||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "f0805222e57f7521d6a62e36fa9163bc891acd422f971defe97d64e70d0a4fe5"
|
checksum = "f0805222e57f7521d6a62e36fa9163bc891acd422f971defe97d64e70d0a4fe5"
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "windows-registry"
|
|
||||||
version = "0.6.1"
|
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "02752bf7fbdcce7f2a27a742f798510f3e5ad88dbe84871e5168e2120c3d5720"
|
|
||||||
dependencies = [
|
|
||||||
"windows-link",
|
|
||||||
"windows-result 0.4.1",
|
|
||||||
"windows-strings",
|
|
||||||
]
|
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "windows-result"
|
name = "windows-result"
|
||||||
version = "0.1.2"
|
version = "0.1.2"
|
||||||
@ -3092,24 +3022,6 @@ dependencies = [
|
|||||||
"windows-targets 0.52.6",
|
"windows-targets 0.52.6",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "windows-result"
|
|
||||||
version = "0.4.1"
|
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "7781fa89eaf60850ac3d2da7af8e5242a5ea78d1a11c49bf2910bb5a73853eb5"
|
|
||||||
dependencies = [
|
|
||||||
"windows-link",
|
|
||||||
]
|
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "windows-strings"
|
|
||||||
version = "0.5.1"
|
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "7837d08f69c77cf6b07689544538e017c1bfcf57e34b4c0ff58e6c2cd3b37091"
|
|
||||||
dependencies = [
|
|
||||||
"windows-link",
|
|
||||||
]
|
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "windows-sys"
|
name = "windows-sys"
|
||||||
version = "0.45.0"
|
version = "0.45.0"
|
||||||
|
|||||||
18
Cargo.toml
18
Cargo.toml
@ -20,7 +20,13 @@ repository = "https://github.com/talwat/lowfi"
|
|||||||
[features]
|
[features]
|
||||||
mpris = ["dep:mpris-server"]
|
mpris = ["dep:mpris-server"]
|
||||||
extra-audio-formats = ["rodio/default"]
|
extra-audio-formats = ["rodio/default"]
|
||||||
scrape = ["dep:serde", "dep:serde_json", "dep:html-escape", "dep:scraper", "dep:indicatif"]
|
scrape = [
|
||||||
|
"dep:serde",
|
||||||
|
"dep:serde_json",
|
||||||
|
"dep:html-escape",
|
||||||
|
"dep:scraper",
|
||||||
|
"dep:indicatif",
|
||||||
|
]
|
||||||
|
|
||||||
[dependencies]
|
[dependencies]
|
||||||
# Basics
|
# Basics
|
||||||
@ -30,12 +36,12 @@ fastrand = "2.3.0"
|
|||||||
thiserror = "2.0.12"
|
thiserror = "2.0.12"
|
||||||
|
|
||||||
# Async
|
# Async
|
||||||
tokio = { version = "1.41.1", features = ["macros", "rt", "fs"], default-features = false }
|
tokio = { version = "1.41.1", features = ["macros", "rt", "fs", "io-util", "sync", "time"], default-features = false }
|
||||||
|
futures-util = { version = "0.3.31", default-features = false }
|
||||||
arc-swap = "1.7.1"
|
arc-swap = "1.7.1"
|
||||||
futures = "0.3.31"
|
|
||||||
|
|
||||||
# Data
|
# Data
|
||||||
reqwest = { version = "0.12.9", features = ["stream"] }
|
reqwest = { version = "0.12.9", features = ["stream", "http2", "default-tls"], default-features = false }
|
||||||
bytes = "1.9.0"
|
bytes = "1.9.0"
|
||||||
|
|
||||||
# I/O
|
# I/O
|
||||||
@ -44,9 +50,9 @@ rodio = { version = "0.21.1", features = ["symphonia-mp3", "playback"], default-
|
|||||||
mpris-server = { version = "0.9.0", optional = true }
|
mpris-server = { version = "0.9.0", optional = true }
|
||||||
dirs = "6.0.0"
|
dirs = "6.0.0"
|
||||||
|
|
||||||
# Misc
|
# Text processing
|
||||||
unicode-segmentation = "1.12.0"
|
unicode-segmentation = "1.12.0"
|
||||||
url = "2.5.4"
|
url = { version = "2.5.4", default-features = false }
|
||||||
|
|
||||||
# Scraper
|
# Scraper
|
||||||
serde = { version = "1.0.219", features = ["derive"], optional = true }
|
serde = { version = "1.0.219", features = ["derive"], optional = true }
|
||||||
|
|||||||
@ -41,6 +41,7 @@ fn silent_get_output_stream() -> crate::Result<rodio::OutputStream> {
|
|||||||
Ok(stream)
|
Ok(stream)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Creates an audio stream, doing so silently on Linux.
|
||||||
pub fn stream() -> crate::Result<rodio::OutputStream> {
|
pub fn stream() -> crate::Result<rodio::OutputStream> {
|
||||||
#[cfg(target_os = "linux")]
|
#[cfg(target_os = "linux")]
|
||||||
let mut stream = silent_get_output_stream()?;
|
let mut stream = silent_get_output_stream()?;
|
||||||
|
|||||||
@ -46,6 +46,9 @@ pub struct Downloader {
|
|||||||
|
|
||||||
/// The [`reqwest`] client to use for downloads.
|
/// The [`reqwest`] client to use for downloads.
|
||||||
client: Client,
|
client: Client,
|
||||||
|
|
||||||
|
/// The RNG generator to use.
|
||||||
|
rng: fastrand::Rng,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Downloader {
|
impl Downloader {
|
||||||
@ -73,6 +76,7 @@ impl Downloader {
|
|||||||
tx,
|
tx,
|
||||||
tracks,
|
tracks,
|
||||||
client,
|
client,
|
||||||
|
rng: fastrand::Rng::new(),
|
||||||
};
|
};
|
||||||
|
|
||||||
Ok(Handle {
|
Ok(Handle {
|
||||||
@ -84,15 +88,17 @@ impl Downloader {
|
|||||||
/// Actually runs the downloader, consuming it and beginning
|
/// Actually runs the downloader, consuming it and beginning
|
||||||
/// the cycle of downloading tracks and reporting to the
|
/// the cycle of downloading tracks and reporting to the
|
||||||
/// rest of the program.
|
/// rest of the program.
|
||||||
async fn run(self) -> crate::Result<()> {
|
async fn run(mut self) -> crate::Result<()> {
|
||||||
const ERROR_TIMEOUT: Duration = Duration::from_secs(1);
|
const ERROR_TIMEOUT: Duration = Duration::from_secs(1);
|
||||||
|
|
||||||
loop {
|
loop {
|
||||||
let result = self.tracks.random(&self.client, &PROGRESS).await;
|
let result = self
|
||||||
|
.tracks
|
||||||
|
.random(&self.client, &PROGRESS, &mut self.rng)
|
||||||
|
.await;
|
||||||
match result {
|
match result {
|
||||||
Ok(track) => {
|
Ok(track) => {
|
||||||
self.queue.send(track).await?;
|
self.queue.send(track).await?;
|
||||||
|
|
||||||
if LOADING.load(atomic::Ordering::Relaxed) {
|
if LOADING.load(atomic::Ordering::Relaxed) {
|
||||||
self.tx.send(crate::Message::Loaded).await?;
|
self.tx.send(crate::Message::Loaded).await?;
|
||||||
LOADING.store(false, atomic::Ordering::Relaxed);
|
LOADING.store(false, atomic::Ordering::Relaxed);
|
||||||
|
|||||||
15
src/error.rs
15
src/error.rs
@ -14,63 +14,48 @@ pub type Result<T> = std::result::Result<T, Error>;
|
|||||||
/// Central application error.
|
/// Central application error.
|
||||||
#[derive(Debug, thiserror::Error)]
|
#[derive(Debug, thiserror::Error)]
|
||||||
pub enum Error {
|
pub enum Error {
|
||||||
/// Errors while loading or saving the persistent volume settings.
|
|
||||||
#[error("unable to load/save the persistent volume")]
|
#[error("unable to load/save the persistent volume")]
|
||||||
PersistentVolume(#[from] volume::Error),
|
PersistentVolume(#[from] volume::Error),
|
||||||
|
|
||||||
/// Errors while loading or saving bookmarks.
|
|
||||||
#[error("unable to load/save bookmarks")]
|
#[error("unable to load/save bookmarks")]
|
||||||
Bookmarks(#[from] bookmark::Error),
|
Bookmarks(#[from] bookmark::Error),
|
||||||
|
|
||||||
/// Network request failures from `reqwest`.
|
|
||||||
#[error("unable to fetch data")]
|
#[error("unable to fetch data")]
|
||||||
Request(#[from] reqwest::Error),
|
Request(#[from] reqwest::Error),
|
||||||
|
|
||||||
/// Failure converting to/from a C string (FFI helpers).
|
|
||||||
#[error("C string null error")]
|
#[error("C string null error")]
|
||||||
FfiNull(#[from] std::ffi::NulError),
|
FfiNull(#[from] std::ffi::NulError),
|
||||||
|
|
||||||
/// Errors coming from the audio backend / stream handling.
|
|
||||||
#[error("audio playing error")]
|
#[error("audio playing error")]
|
||||||
Rodio(#[from] rodio::StreamError),
|
Rodio(#[from] rodio::StreamError),
|
||||||
|
|
||||||
/// Failure to send an internal `Message` over the mpsc channel.
|
|
||||||
#[error("couldn't send internal message")]
|
#[error("couldn't send internal message")]
|
||||||
Send(#[from] mpsc::error::SendError<crate::Message>),
|
Send(#[from] mpsc::error::SendError<crate::Message>),
|
||||||
|
|
||||||
/// Failure to enqueue a track into the queue channel.
|
|
||||||
#[error("couldn't add track to the queue")]
|
#[error("couldn't add track to the queue")]
|
||||||
Queue(#[from] mpsc::error::SendError<tracks::Queued>),
|
Queue(#[from] mpsc::error::SendError<tracks::Queued>),
|
||||||
|
|
||||||
/// Failure to broadcast UI updates.
|
|
||||||
#[error("couldn't update UI state")]
|
#[error("couldn't update UI state")]
|
||||||
Broadcast(#[from] broadcast::error::SendError<ui::Update>),
|
Broadcast(#[from] broadcast::error::SendError<ui::Update>),
|
||||||
|
|
||||||
/// Generic IO error.
|
|
||||||
#[error("io error")]
|
#[error("io error")]
|
||||||
Io(#[from] std::io::Error),
|
Io(#[from] std::io::Error),
|
||||||
|
|
||||||
/// Data directory was not found or could not be determined.
|
|
||||||
#[error("directory not found")]
|
#[error("directory not found")]
|
||||||
Directory,
|
Directory,
|
||||||
|
|
||||||
/// Downloader failed to provide the requested track.
|
|
||||||
#[error("couldn't fetch track from downloader")]
|
#[error("couldn't fetch track from downloader")]
|
||||||
Download,
|
Download,
|
||||||
|
|
||||||
/// Integer parsing errors.
|
|
||||||
#[error("couldn't parse integer")]
|
#[error("couldn't parse integer")]
|
||||||
Parse(#[from] std::num::ParseIntError),
|
Parse(#[from] std::num::ParseIntError),
|
||||||
|
|
||||||
/// Track subsystem error.
|
|
||||||
#[error("track failure")]
|
#[error("track failure")]
|
||||||
Track(#[from] tracks::Error),
|
Track(#[from] tracks::Error),
|
||||||
|
|
||||||
/// UI subsystem error.
|
|
||||||
#[error("ui failure")]
|
#[error("ui failure")]
|
||||||
UI(#[from] ui::Error),
|
UI(#[from] ui::Error),
|
||||||
|
|
||||||
/// Error returned when a spawned task join failed.
|
|
||||||
#[error("join error")]
|
#[error("join error")]
|
||||||
JoinError(#[from] tokio::task::JoinError),
|
JoinError(#[from] tokio::task::JoinError),
|
||||||
}
|
}
|
||||||
|
|||||||
40
src/main.rs
40
src/main.rs
@ -1,27 +1,26 @@
|
|||||||
//! An extremely simple lofi player.
|
//! An extremely simple lofi player.
|
||||||
pub mod error;
|
use crate::player::Player;
|
||||||
|
use clap::{Parser, Subcommand};
|
||||||
|
use futures_util::TryFutureExt;
|
||||||
use std::path::PathBuf;
|
use std::path::PathBuf;
|
||||||
|
|
||||||
use clap::{Parser, Subcommand};
|
|
||||||
mod tests;
|
|
||||||
pub use error::{Error, Result};
|
|
||||||
pub mod message;
|
|
||||||
pub mod ui;
|
|
||||||
pub use message::Message;
|
|
||||||
|
|
||||||
use crate::player::Player;
|
|
||||||
pub mod audio;
|
pub mod audio;
|
||||||
pub mod bookmark;
|
pub mod bookmark;
|
||||||
pub mod download;
|
pub mod download;
|
||||||
|
pub mod error;
|
||||||
|
pub mod message;
|
||||||
pub mod player;
|
pub mod player;
|
||||||
|
#[cfg(feature = "scrape")]
|
||||||
|
mod scrapers;
|
||||||
|
mod tests;
|
||||||
pub mod tracks;
|
pub mod tracks;
|
||||||
|
pub mod ui;
|
||||||
pub mod volume;
|
pub mod volume;
|
||||||
|
|
||||||
#[cfg(feature = "scrape")]
|
|
||||||
mod scrapers;
|
|
||||||
|
|
||||||
#[cfg(feature = "scrape")]
|
#[cfg(feature = "scrape")]
|
||||||
use crate::scrapers::Source;
|
use crate::scrapers::Source;
|
||||||
|
pub use error::{Error, Result};
|
||||||
|
pub use message::Message;
|
||||||
|
|
||||||
/// An extremely simple lofi player.
|
/// An extremely simple lofi player.
|
||||||
#[derive(Parser, Clone)]
|
#[derive(Parser, Clone)]
|
||||||
@ -96,16 +95,6 @@ pub fn data_dir() -> crate::Result<PathBuf> {
|
|||||||
Ok(dir)
|
Ok(dir)
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Simply creates and runs the player, so that the [`Result`] of both operations
|
|
||||||
/// can be easily handled by the [`main`] function.
|
|
||||||
async fn player(args: Args, environment: ui::Environment) -> crate::Result<()> {
|
|
||||||
let stream = audio::stream()?;
|
|
||||||
let mut player = Player::init(args, environment, stream.mixer()).await?;
|
|
||||||
player.run().await?;
|
|
||||||
|
|
||||||
Ok(())
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Program entry point.
|
/// Program entry point.
|
||||||
///
|
///
|
||||||
/// Parses CLI arguments, initializes the audio stream and player, then
|
/// Parses CLI arguments, initializes the audio stream and player, then
|
||||||
@ -126,9 +115,12 @@ async fn main() -> eyre::Result<()> {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
let stream = audio::stream()?;
|
||||||
let environment = ui::Environment::ready(args.alternate)?;
|
let environment = ui::Environment::ready(args.alternate)?;
|
||||||
let result = player(args, environment).await;
|
let result = Player::init(args, environment, stream.mixer())
|
||||||
environment.cleanup(result.is_ok())?;
|
.and_then(Player::run)
|
||||||
|
.await;
|
||||||
|
|
||||||
|
environment.cleanup(result.is_ok())?;
|
||||||
Ok(result?)
|
Ok(result?)
|
||||||
}
|
}
|
||||||
|
|||||||
@ -176,7 +176,7 @@ impl Player {
|
|||||||
///
|
///
|
||||||
/// This will return when a `Message::Quit` is received. It handles commands
|
/// This will return when a `Message::Quit` is received. It handles commands
|
||||||
/// coming from the frontend and updates playback/UI state accordingly.
|
/// coming from the frontend and updates playback/UI state accordingly.
|
||||||
pub async fn run(&mut self) -> crate::Result<()> {
|
pub async fn run(mut self) -> crate::Result<()> {
|
||||||
while let Some(message) = self.rx.recv().await {
|
while let Some(message) = self.rx.recv().await {
|
||||||
match message {
|
match message {
|
||||||
Message::Next | Message::Init | Message::Loaded => {
|
Message::Next | Message::Init | Message::Loaded => {
|
||||||
@ -226,18 +226,7 @@ impl Player {
|
|||||||
}
|
}
|
||||||
|
|
||||||
#[cfg(feature = "mpris")]
|
#[cfg(feature = "mpris")]
|
||||||
match message {
|
self.ui.mpris.handle(&message).await?;
|
||||||
Message::ChangeVolume(_) | Message::SetVolume(_) => {
|
|
||||||
self.ui.mpris.update_volume().await?
|
|
||||||
}
|
|
||||||
Message::Play | Message::Pause | Message::PlayPause => {
|
|
||||||
self.ui.mpris.update_playback().await?
|
|
||||||
}
|
|
||||||
Message::Init | Message::Loaded | Message::Next => {
|
|
||||||
self.ui.mpris.update_metadata().await?
|
|
||||||
}
|
|
||||||
_ => (),
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
self.close().await?;
|
self.close().await?;
|
||||||
|
|||||||
@ -5,7 +5,7 @@
|
|||||||
|
|
||||||
use std::sync::LazyLock;
|
use std::sync::LazyLock;
|
||||||
|
|
||||||
use futures::{stream::FuturesOrdered, StreamExt};
|
use futures_util::{stream::FuturesOrdered, StreamExt};
|
||||||
use reqwest::Client;
|
use reqwest::Client;
|
||||||
use scraper::{Html, Selector};
|
use scraper::{Html, Selector};
|
||||||
|
|
||||||
|
|||||||
@ -1,7 +1,8 @@
|
|||||||
use eyre::eyre;
|
use eyre::eyre;
|
||||||
use futures::stream::FuturesUnordered;
|
use futures_util::stream::FuturesUnordered;
|
||||||
use futures::StreamExt;
|
use futures_util::StreamExt;
|
||||||
use indicatif::ProgressBar;
|
use indicatif::ProgressBar;
|
||||||
|
use std::future::Future;
|
||||||
use std::str::FromStr;
|
use std::str::FromStr;
|
||||||
use std::{fmt, sync::LazyLock};
|
use std::{fmt, sync::LazyLock};
|
||||||
|
|
||||||
@ -87,7 +88,7 @@ async fn scan_page(
|
|||||||
number: usize,
|
number: usize,
|
||||||
client: &Client,
|
client: &Client,
|
||||||
bar: ProgressBar,
|
bar: ProgressBar,
|
||||||
) -> eyre::Result<Vec<impl futures::Future<Output = Result<Release, ReleaseError>>>> {
|
) -> eyre::Result<Vec<impl Future<Output = Result<Release, ReleaseError>>>> {
|
||||||
let path = format!("releases/?page={number}");
|
let path = format!("releases/?page={number}");
|
||||||
let content = get(client, &path, Source::Chillhop).await?;
|
let content = get(client, &path, Source::Chillhop).await?;
|
||||||
let html = Html::parse_document(&content);
|
let html = Html::parse_document(&content);
|
||||||
|
|||||||
@ -5,7 +5,7 @@
|
|||||||
|
|
||||||
use std::sync::LazyLock;
|
use std::sync::LazyLock;
|
||||||
|
|
||||||
use futures::{stream::FuturesOrdered, StreamExt};
|
use futures_util::{stream::FuturesOrdered, StreamExt};
|
||||||
use reqwest::Client;
|
use reqwest::Client;
|
||||||
use scraper::{Html, Selector};
|
use scraper::{Html, Selector};
|
||||||
|
|
||||||
|
|||||||
@ -123,7 +123,7 @@ mod list {
|
|||||||
let text = "http://x/\npath!Display";
|
let text = "http://x/\npath!Display";
|
||||||
let list = List::new("t", text, None);
|
let list = List::new("t", text, None);
|
||||||
|
|
||||||
let (p, d) = list.random_path();
|
let (p, d) = list.random_path(&mut fastrand::Rng::new());
|
||||||
assert_eq!(p, "path");
|
assert_eq!(p, "path");
|
||||||
assert_eq!(d, Some("Display".into()));
|
assert_eq!(d, Some("Display".into()));
|
||||||
}
|
}
|
||||||
@ -133,7 +133,7 @@ mod list {
|
|||||||
let text = "http://x/\ntrackA";
|
let text = "http://x/\ntrackA";
|
||||||
let list = List::new("t", text, None);
|
let list = List::new("t", text, None);
|
||||||
|
|
||||||
let (p, d) = list.random_path();
|
let (p, d) = list.random_path(&mut fastrand::Rng::new());
|
||||||
assert_eq!(p, "trackA");
|
assert_eq!(p, "trackA");
|
||||||
assert!(d.is_none());
|
assert!(d.is_none());
|
||||||
}
|
}
|
||||||
@ -152,7 +152,7 @@ mod list {
|
|||||||
fn custom_display_with_exclamation() {
|
fn custom_display_with_exclamation() {
|
||||||
let text = "http://base/\nfile.mp3!My Custom Name";
|
let text = "http://base/\nfile.mp3!My Custom Name";
|
||||||
let list = List::new("t", text, None);
|
let list = List::new("t", text, None);
|
||||||
let (path, display) = list.random_path();
|
let (path, display) = list.random_path(&mut fastrand::Rng::new());
|
||||||
assert_eq!(path, "file.mp3");
|
assert_eq!(path, "file.mp3");
|
||||||
assert_eq!(display, Some("My Custom Name".into()));
|
assert_eq!(display, Some("My Custom Name".into()));
|
||||||
}
|
}
|
||||||
@ -161,7 +161,7 @@ mod list {
|
|||||||
fn single_track() {
|
fn single_track() {
|
||||||
let text = "base\nonly_track.mp3";
|
let text = "base\nonly_track.mp3";
|
||||||
let list = List::new("name", text, None);
|
let list = List::new("name", text, None);
|
||||||
let (path, _) = list.random_path();
|
let (path, _) = list.random_path(&mut fastrand::Rng::new());
|
||||||
assert_eq!(path, "only_track.mp3");
|
assert_eq!(path, "only_track.mp3");
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -171,7 +171,10 @@ mod list {
|
|||||||
let list = List::new("name", text, None);
|
let list = List::new("name", text, None);
|
||||||
|
|
||||||
let client = Client::new();
|
let client = Client::new();
|
||||||
let track = list.random(&client, &PROGRESS).await.unwrap();
|
let track = list
|
||||||
|
.random(&client, &PROGRESS, &mut fastrand::Rng::new())
|
||||||
|
.await
|
||||||
|
.unwrap();
|
||||||
assert_eq!(track.display, "Apple Juice");
|
assert_eq!(track.display, "Apple Juice");
|
||||||
assert_eq!(track.path, "https://stream.chillhop.com/mp3/9476");
|
assert_eq!(track.path, "https://stream.chillhop.com/mp3/9476");
|
||||||
assert_eq!(track.data.len(), 3150424);
|
assert_eq!(track.data.len(), 3150424);
|
||||||
|
|||||||
@ -7,7 +7,7 @@ use std::{
|
|||||||
};
|
};
|
||||||
|
|
||||||
use bytes::{BufMut as _, Bytes, BytesMut};
|
use bytes::{BufMut as _, Bytes, BytesMut};
|
||||||
use futures::StreamExt as _;
|
use futures_util::StreamExt as _;
|
||||||
use reqwest::Client;
|
use reqwest::Client;
|
||||||
use tokio::fs;
|
use tokio::fs;
|
||||||
|
|
||||||
@ -49,13 +49,13 @@ impl List {
|
|||||||
///
|
///
|
||||||
/// The second value in the tuple specifies whether the
|
/// The second value in the tuple specifies whether the
|
||||||
/// track has a custom display name.
|
/// track has a custom display name.
|
||||||
pub fn random_path(&self) -> (String, Option<String>) {
|
pub fn random_path(&self, rng: &mut fastrand::Rng) -> (String, Option<String>) {
|
||||||
// We're getting from 1 here, since the base is at `self.lines[0]`.
|
// 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
|
// We're also not pre-trimming `self.lines` into `base` & `tracks` due to
|
||||||
// how rust vectors work, since it is slower 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.
|
// the start, so it's faster to just keep it in & work around it.
|
||||||
let random = fastrand::usize(1..self.lines.len());
|
let random = rng.usize(1..self.lines.len());
|
||||||
let line = self.lines[random].clone();
|
let line = self.lines[random].clone();
|
||||||
|
|
||||||
if let Some((first, second)) = line.split_once('!') {
|
if let Some((first, second)) = line.split_once('!') {
|
||||||
@ -128,10 +128,15 @@ impl List {
|
|||||||
|
|
||||||
/// Fetches and downloads a random track from the [List].
|
/// Fetches and downloads a random track from the [List].
|
||||||
///
|
///
|
||||||
/// The Result's error is a bool, which is true if a timeout error occured,
|
/// The Result's error is a bool, which is true if a timeout error occurred,
|
||||||
/// and false otherwise. This tells lowfi if it shouldn't wait to try again.
|
/// and false otherwise. This tells lowfi if it shouldn't wait to try again.
|
||||||
pub async fn random(&self, client: &Client, progress: &AtomicU8) -> tracks::Result<Queued> {
|
pub async fn random(
|
||||||
let (path, display) = self.random_path();
|
&self,
|
||||||
|
client: &Client,
|
||||||
|
progress: &AtomicU8,
|
||||||
|
rng: &mut fastrand::Rng,
|
||||||
|
) -> tracks::Result<Queued> {
|
||||||
|
let (path, display) = self.random_path(rng);
|
||||||
let (data, path) = self.download(&path, client, Some(progress)).await?;
|
let (data, path) = self.download(&path, client, Some(progress)).await?;
|
||||||
|
|
||||||
Queued::new(path, data, display)
|
Queued::new(path, data, display)
|
||||||
|
|||||||
@ -3,7 +3,7 @@
|
|||||||
|
|
||||||
use crate::Message;
|
use crate::Message;
|
||||||
use crossterm::event::{self, EventStream, KeyCode, KeyEventKind, KeyModifiers};
|
use crossterm::event::{self, EventStream, KeyCode, KeyEventKind, KeyModifiers};
|
||||||
use futures::{FutureExt as _, StreamExt as _};
|
use futures_util::{FutureExt, StreamExt};
|
||||||
use tokio::sync::mpsc::Sender;
|
use tokio::sync::mpsc::Sender;
|
||||||
|
|
||||||
/// Starts the listener to receive input from the terminal for various events.
|
/// Starts the listener to receive input from the terminal for various events.
|
||||||
|
|||||||
@ -5,11 +5,23 @@ use crate::{
|
|||||||
Args,
|
Args,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
/// UI-specific parameters and options.
|
||||||
#[derive(Copy, Clone, Debug, Default)]
|
#[derive(Copy, Clone, Debug, Default)]
|
||||||
pub struct Params {
|
pub struct Params {
|
||||||
|
/// Whether to include borders.
|
||||||
pub borderless: bool,
|
pub borderless: bool,
|
||||||
|
|
||||||
|
/// Whether to include the bottom control bar.
|
||||||
pub minimalist: bool,
|
pub minimalist: bool,
|
||||||
|
|
||||||
|
/// Whether the visual part of the UI should be enabled.
|
||||||
|
/// This only applies if the MPRIS feature is enabled.
|
||||||
pub enabled: bool,
|
pub enabled: bool,
|
||||||
|
|
||||||
|
/// The total delta between frames, which takes into account
|
||||||
|
/// the time it takes to actually render each frame.
|
||||||
|
///
|
||||||
|
/// Derived from the FPS.
|
||||||
pub delta: Duration,
|
pub delta: Duration,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -268,8 +268,18 @@ pub struct Server {
|
|||||||
}
|
}
|
||||||
|
|
||||||
impl Server {
|
impl Server {
|
||||||
|
/// Handles a player message to update the state of the MPRIS player.
|
||||||
|
pub async fn handle(&mut self, message: &crate::Message) -> ui::Result<()> {
|
||||||
|
match message {
|
||||||
|
Message::ChangeVolume(_) | Message::SetVolume(_) => self.update_volume().await,
|
||||||
|
Message::Play | Message::Pause | Message::PlayPause => self.update_playback().await,
|
||||||
|
Message::Init | Message::Loaded | Message::Next => self.update_metadata().await,
|
||||||
|
_ => Ok(()),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/// Shorthand to emit a `PropertiesChanged` signal, like when pausing/unpausing.
|
/// Shorthand to emit a `PropertiesChanged` signal, like when pausing/unpausing.
|
||||||
pub async fn changed(
|
async fn changed(
|
||||||
&mut self,
|
&mut self,
|
||||||
properties: impl IntoIterator<Item = mpris_server::Property> + Send + Sync,
|
properties: impl IntoIterator<Item = mpris_server::Property> + Send + Sync,
|
||||||
) -> ui::Result<()> {
|
) -> ui::Result<()> {
|
||||||
@ -284,7 +294,7 @@ impl Server {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// Updates the volume with the latest information.
|
/// Updates the volume with the latest information.
|
||||||
pub async fn update_volume(&mut self) -> ui::Result<()> {
|
async fn update_volume(&mut self) -> ui::Result<()> {
|
||||||
self.changed(vec![Property::Volume(self.player().sink.volume().into())])
|
self.changed(vec![Property::Volume(self.player().sink.volume().into())])
|
||||||
.await?;
|
.await?;
|
||||||
|
|
||||||
@ -292,7 +302,7 @@ impl Server {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// Updates the playback with the latest information.
|
/// Updates the playback with the latest information.
|
||||||
pub async fn update_playback(&mut self) -> ui::Result<()> {
|
async fn update_playback(&mut self) -> ui::Result<()> {
|
||||||
let status = self.player().playback_status().await?;
|
let status = self.player().playback_status().await?;
|
||||||
self.changed(vec![Property::PlaybackStatus(status)]).await?;
|
self.changed(vec![Property::PlaybackStatus(status)]).await?;
|
||||||
|
|
||||||
@ -300,7 +310,7 @@ impl Server {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// Updates the current track data with the current information.
|
/// Updates the current track data with the current information.
|
||||||
pub async fn update_metadata(&mut self) -> ui::Result<()> {
|
async fn update_metadata(&mut self) -> ui::Result<()> {
|
||||||
let metadata = self.player().metadata().await?;
|
let metadata = self.player().metadata().await?;
|
||||||
self.changed(vec![Property::Metadata(metadata)]).await?;
|
self.changed(vec![Property::Metadata(metadata)]).await?;
|
||||||
|
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user