mirror of
https://github.com/talwat/lowfi
synced 2024-12-27 03:31:55 +00:00
fix: fix issue #1 as well as several others
fix: split downloader into a seperate struct for readability fix: use `lazy_static` to reduce MSRV fix: reduce frame delta
This commit is contained in:
parent
1278dc534c
commit
fd2d37d635
3
Cargo.lock
generated
3
Cargo.lock
generated
@ -982,7 +982,7 @@ checksum = "a7a70ba024b9dc04c27ea2f0c0548feb474ec5c54bba33a7f72f873a39d07b24"
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "lowfi"
|
name = "lowfi"
|
||||||
version = "1.1.1"
|
version = "1.2.0"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"Inflector",
|
"Inflector",
|
||||||
"arc-swap",
|
"arc-swap",
|
||||||
@ -991,6 +991,7 @@ dependencies = [
|
|||||||
"crossterm",
|
"crossterm",
|
||||||
"eyre",
|
"eyre",
|
||||||
"futures",
|
"futures",
|
||||||
|
"lazy_static",
|
||||||
"rand",
|
"rand",
|
||||||
"reqwest",
|
"reqwest",
|
||||||
"rodio",
|
"rodio",
|
||||||
|
@ -1,6 +1,6 @@
|
|||||||
[package]
|
[package]
|
||||||
name = "lowfi"
|
name = "lowfi"
|
||||||
version = "1.1.1"
|
version = "1.2.0"
|
||||||
edition = "2021"
|
edition = "2021"
|
||||||
description = "An extremely simple lofi player."
|
description = "An extremely simple lofi player."
|
||||||
license = "MIT"
|
license = "MIT"
|
||||||
@ -31,3 +31,4 @@ scraper = "0.20.0"
|
|||||||
rodio = { version = "0.19.0", features = ["mp3"], default-features = false }
|
rodio = { version = "0.19.0", features = ["mp3"], default-features = false }
|
||||||
crossterm = "0.28.1"
|
crossterm = "0.28.1"
|
||||||
Inflector = "0.11.4"
|
Inflector = "0.11.4"
|
||||||
|
lazy_static = "1.5.0"
|
||||||
|
@ -1,7 +1,8 @@
|
|||||||
//! Responsible for the basic initialization & shutdown of the audio server & frontend.
|
//! Responsible for the basic initialization & shutdown of the audio server & frontend.
|
||||||
|
|
||||||
use std::sync::Arc;
|
use std::{io::stderr, sync::Arc};
|
||||||
|
|
||||||
|
use crossterm::cursor::SavePosition;
|
||||||
use tokio::{
|
use tokio::{
|
||||||
sync::mpsc::{self},
|
sync::mpsc::{self},
|
||||||
task::{self},
|
task::{self},
|
||||||
@ -13,6 +14,12 @@ use crate::player::{ui, Messages};
|
|||||||
/// Initializes the audio server, and then safely stops
|
/// Initializes the audio server, and then safely stops
|
||||||
/// it when the frontend quits.
|
/// it when the frontend quits.
|
||||||
pub async fn play() -> eyre::Result<()> {
|
pub async fn play() -> eyre::Result<()> {
|
||||||
|
// Save the position. This is important since later on we can revert to this position
|
||||||
|
// and clear any potential error messages that may have showed up.
|
||||||
|
// TODO: Figure how to set some sort of flag to hide error messages within rodio,
|
||||||
|
// TODO: Instead of just ignoring & clearing them after.
|
||||||
|
crossterm::execute!(stderr(), SavePosition)?;
|
||||||
|
|
||||||
let (tx, rx) = mpsc::channel(8);
|
let (tx, rx) = mpsc::channel(8);
|
||||||
|
|
||||||
let player = Arc::new(Player::new().await?);
|
let player = Arc::new(Player::new().await?);
|
||||||
|
@ -5,12 +5,13 @@
|
|||||||
use std::{collections::VecDeque, sync::Arc, time::Duration};
|
use std::{collections::VecDeque, sync::Arc, time::Duration};
|
||||||
|
|
||||||
use arc_swap::ArcSwapOption;
|
use arc_swap::ArcSwapOption;
|
||||||
|
use downloader::Downloader;
|
||||||
use reqwest::Client;
|
use reqwest::Client;
|
||||||
use rodio::{OutputStream, OutputStreamHandle, Sink};
|
use rodio::{OutputStream, OutputStreamHandle, Sink};
|
||||||
use tokio::{
|
use tokio::{
|
||||||
select,
|
select,
|
||||||
sync::{
|
sync::{
|
||||||
mpsc::{self, Receiver, Sender},
|
mpsc::{Receiver, Sender},
|
||||||
RwLock,
|
RwLock,
|
||||||
},
|
},
|
||||||
task,
|
task,
|
||||||
@ -18,6 +19,7 @@ use tokio::{
|
|||||||
|
|
||||||
use crate::tracks::{DecodedTrack, Track, TrackInfo};
|
use crate::tracks::{DecodedTrack, Track, TrackInfo};
|
||||||
|
|
||||||
|
pub mod downloader;
|
||||||
pub mod ui;
|
pub mod ui;
|
||||||
|
|
||||||
/// Handles communication between the frontend & audio player.
|
/// Handles communication between the frontend & audio player.
|
||||||
@ -122,39 +124,22 @@ impl Player {
|
|||||||
|
|
||||||
/// This is the main "audio server".
|
/// This is the main "audio server".
|
||||||
///
|
///
|
||||||
/// `rx` is used to communicate with it, for example when to
|
/// `rx` & `ts` are used to communicate with it, for example when to
|
||||||
/// skip tracks or pause.
|
/// skip tracks or pause.
|
||||||
pub async fn play(
|
pub async fn play(
|
||||||
queue: Arc<Self>,
|
player: Arc<Self>,
|
||||||
tx: Sender<Messages>,
|
tx: Sender<Messages>,
|
||||||
mut rx: Receiver<Messages>,
|
mut rx: Receiver<Messages>,
|
||||||
) -> eyre::Result<()> {
|
) -> eyre::Result<()> {
|
||||||
// This is an internal channel which serves pretty much only one purpose,
|
// `itx` is used to notify the `Downloader` when it needs to download new tracks.
|
||||||
// which is to notify the buffer refiller to get back to work.
|
let (downloader, itx) = Downloader::new(player.clone());
|
||||||
// This channel is useful to prevent needing to check with some infinite loop.
|
downloader.start().await;
|
||||||
let (itx, mut irx) = mpsc::channel(8);
|
|
||||||
|
|
||||||
// This refills the queue in the background.
|
|
||||||
task::spawn({
|
|
||||||
let queue = Arc::clone(&queue);
|
|
||||||
|
|
||||||
async move {
|
|
||||||
while irx.recv().await == Some(()) {
|
|
||||||
while queue.tracks.read().await.len() < BUFFER_SIZE {
|
|
||||||
let Ok(track) = Track::random(&queue.client).await else {
|
|
||||||
continue;
|
|
||||||
};
|
|
||||||
queue.tracks.write().await.push_back(track);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
// Start buffering tracks immediately.
|
// Start buffering tracks immediately.
|
||||||
itx.send(()).await?;
|
itx.send(()).await?;
|
||||||
|
|
||||||
loop {
|
loop {
|
||||||
let clone = Arc::clone(&queue);
|
let clone = Arc::clone(&player);
|
||||||
let msg = select! {
|
let msg = select! {
|
||||||
Some(x) = rx.recv() => x,
|
Some(x) = rx.recv() => x,
|
||||||
|
|
||||||
@ -166,17 +151,17 @@ impl Player {
|
|||||||
Messages::Next | Messages::Init | Messages::TryAgain => {
|
Messages::Next | Messages::Init | Messages::TryAgain => {
|
||||||
// Skip as early as possible so that music doesn't play
|
// Skip as early as possible so that music doesn't play
|
||||||
// while lowfi is "loading".
|
// while lowfi is "loading".
|
||||||
queue.sink.stop();
|
player.sink.stop();
|
||||||
|
|
||||||
// Serves as an indicator that the queue is "loading".
|
// Serves as an indicator that the queue is "loading".
|
||||||
// This is also set by Player::next.
|
// This is also set by Player::next.
|
||||||
queue.current.store(None);
|
player.current.store(None);
|
||||||
|
|
||||||
let track = Self::next(Arc::clone(&queue)).await;
|
let track = Self::next(Arc::clone(&player)).await;
|
||||||
|
|
||||||
match track {
|
match track {
|
||||||
Ok(track) => {
|
Ok(track) => {
|
||||||
queue.sink.append(track.data);
|
player.sink.append(track.data);
|
||||||
|
|
||||||
// Notify the background downloader that there's an empty spot
|
// Notify the background downloader that there's an empty spot
|
||||||
// in the buffer.
|
// in the buffer.
|
||||||
@ -192,10 +177,10 @@ impl Player {
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
Messages::Pause => {
|
Messages::Pause => {
|
||||||
if queue.sink.is_paused() {
|
if player.sink.is_paused() {
|
||||||
queue.sink.play();
|
player.sink.play();
|
||||||
} else {
|
} else {
|
||||||
queue.sink.pause();
|
player.sink.pause();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
52
src/player/downloader.rs
Normal file
52
src/player/downloader.rs
Normal file
@ -0,0 +1,52 @@
|
|||||||
|
//! Contains the [`Downloader`] struct.
|
||||||
|
|
||||||
|
use std::sync::Arc;
|
||||||
|
|
||||||
|
use tokio::{
|
||||||
|
sync::mpsc::{self, Receiver, Sender},
|
||||||
|
task,
|
||||||
|
};
|
||||||
|
|
||||||
|
use crate::tracks::Track;
|
||||||
|
|
||||||
|
use super::{Player, BUFFER_SIZE};
|
||||||
|
|
||||||
|
/// This struct is responsible for downloading tracks in the background.
|
||||||
|
///
|
||||||
|
/// This is not used for the first track or a track when the buffer is currently empty.
|
||||||
|
pub struct Downloader {
|
||||||
|
/// The player for the downloader to download to & with.
|
||||||
|
player: Arc<Player>,
|
||||||
|
|
||||||
|
/// The internal reciever, which is used by the downloader to know
|
||||||
|
/// when to begin downloading more tracks.
|
||||||
|
rx: Receiver<()>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Downloader {
|
||||||
|
/// Initializes the [Downloader].
|
||||||
|
///
|
||||||
|
/// This also sends a [`Sender`] which can be used to notify
|
||||||
|
/// when the downloader needs to begin downloading more tracks.
|
||||||
|
pub fn new(player: Arc<Player>) -> (Self, Sender<()>) {
|
||||||
|
let (tx, rx) = mpsc::channel(8);
|
||||||
|
(Self { player, rx }, tx)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Actually starts & consumes the [Downloader].
|
||||||
|
pub async fn start(mut self) {
|
||||||
|
task::spawn(async move {
|
||||||
|
// Loop through each update notification.
|
||||||
|
while self.rx.recv().await == Some(()) {
|
||||||
|
// For each update notification, we'll push tracks until the buffer is completely full.
|
||||||
|
while self.player.tracks.read().await.len() < BUFFER_SIZE {
|
||||||
|
let Ok(track) = Track::random(&self.player.client).await else {
|
||||||
|
continue;
|
||||||
|
};
|
||||||
|
|
||||||
|
self.player.tracks.write().await.push_back(track);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
@ -6,7 +6,7 @@ use crate::tracks::TrackInfo;
|
|||||||
|
|
||||||
use super::Player;
|
use super::Player;
|
||||||
use crossterm::{
|
use crossterm::{
|
||||||
cursor::{Hide, MoveToColumn, MoveUp, Show},
|
cursor::{Hide, MoveToColumn, MoveUp, RestorePosition, Show},
|
||||||
event::{self, KeyCode, KeyModifiers},
|
event::{self, KeyCode, KeyModifiers},
|
||||||
style::{Print, Stylize},
|
style::{Print, Stylize},
|
||||||
terminal::{self, Clear, ClearType},
|
terminal::{self, Clear, ClearType},
|
||||||
@ -19,6 +19,11 @@ use tokio::{
|
|||||||
|
|
||||||
use super::Messages;
|
use super::Messages;
|
||||||
|
|
||||||
|
/// How long to wait in between frames.
|
||||||
|
/// This is fairly arbitrary, but an ideal value should be enough to feel
|
||||||
|
/// snappy but not require too many resources.
|
||||||
|
const FRAME_DELTA: f32 = 5.0 / 60.0;
|
||||||
|
|
||||||
/// Small helper function to format durations.
|
/// Small helper function to format durations.
|
||||||
fn format_duration(duration: &Duration) -> String {
|
fn format_duration(duration: &Duration) -> String {
|
||||||
let seconds = duration.as_secs() % 60;
|
let seconds = duration.as_secs() % 60;
|
||||||
@ -126,14 +131,19 @@ async fn interface(queue: Arc<Player>) -> eyre::Result<()> {
|
|||||||
MoveUp(4)
|
MoveUp(4)
|
||||||
)?;
|
)?;
|
||||||
|
|
||||||
sleep(Duration::from_secs_f32(10.0 / 60.0)).await;
|
sleep(Duration::from_secs_f32(FRAME_DELTA)).await;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Initializes the UI, this will also start taking input from the user.
|
/// Initializes the UI, this will also start taking input from the user.
|
||||||
pub async fn start(queue: Arc<Player>, sender: Sender<Messages>) -> eyre::Result<()> {
|
pub async fn start(queue: Arc<Player>, sender: Sender<Messages>) -> eyre::Result<()> {
|
||||||
|
crossterm::execute!(
|
||||||
|
stderr(),
|
||||||
|
RestorePosition,
|
||||||
|
Clear(ClearType::FromCursorDown),
|
||||||
|
Hide
|
||||||
|
)?;
|
||||||
terminal::enable_raw_mode()?;
|
terminal::enable_raw_mode()?;
|
||||||
crossterm::execute!(stderr(), Hide)?;
|
|
||||||
//crossterm::execute!(stderr(), EnterAlternateScreen, MoveTo(0, 0))?;
|
//crossterm::execute!(stderr(), EnterAlternateScreen, MoveTo(0, 0))?;
|
||||||
|
|
||||||
task::spawn(interface(Arc::clone(&queue)));
|
task::spawn(interface(Arc::clone(&queue)));
|
||||||
|
@ -1,14 +1,14 @@
|
|||||||
//! Has all of the functions for the `scrape` command.
|
//! Has all of the functions for the `scrape` command.
|
||||||
|
|
||||||
use std::sync::LazyLock;
|
|
||||||
|
|
||||||
use futures::{stream::FuturesUnordered, StreamExt};
|
use futures::{stream::FuturesUnordered, StreamExt};
|
||||||
|
use lazy_static::lazy_static;
|
||||||
use scraper::{Html, Selector};
|
use scraper::{Html, Selector};
|
||||||
|
|
||||||
const BASE_URL: &str = "https://lofigirl.com/wp-content/uploads/";
|
const BASE_URL: &str = "https://lofigirl.com/wp-content/uploads/";
|
||||||
|
|
||||||
static SELECTOR: LazyLock<Selector> =
|
lazy_static! {
|
||||||
LazyLock::new(|| Selector::parse("html > body > pre > a").unwrap());
|
static ref SELECTOR: Selector = Selector::parse("html > body > pre > a").unwrap();
|
||||||
|
}
|
||||||
|
|
||||||
async fn parse(path: &str) -> eyre::Result<Vec<String>> {
|
async fn parse(path: &str) -> eyre::Result<Vec<String>> {
|
||||||
let response = reqwest::get(format!("{}{}", BASE_URL, path)).await?;
|
let response = reqwest::get(format!("{}{}", BASE_URL, path)).await?;
|
||||||
|
Loading…
Reference in New Issue
Block a user