mirror of
https://github.com/talwat/lowfi
synced 2024-12-26 03:01: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]]
|
||||
name = "lowfi"
|
||||
version = "1.1.1"
|
||||
version = "1.2.0"
|
||||
dependencies = [
|
||||
"Inflector",
|
||||
"arc-swap",
|
||||
@ -991,6 +991,7 @@ dependencies = [
|
||||
"crossterm",
|
||||
"eyre",
|
||||
"futures",
|
||||
"lazy_static",
|
||||
"rand",
|
||||
"reqwest",
|
||||
"rodio",
|
||||
|
@ -1,6 +1,6 @@
|
||||
[package]
|
||||
name = "lowfi"
|
||||
version = "1.1.1"
|
||||
version = "1.2.0"
|
||||
edition = "2021"
|
||||
description = "An extremely simple lofi player."
|
||||
license = "MIT"
|
||||
@ -31,3 +31,4 @@ scraper = "0.20.0"
|
||||
rodio = { version = "0.19.0", features = ["mp3"], default-features = false }
|
||||
crossterm = "0.28.1"
|
||||
Inflector = "0.11.4"
|
||||
lazy_static = "1.5.0"
|
||||
|
@ -1,7 +1,8 @@
|
||||
//! 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::{
|
||||
sync::mpsc::{self},
|
||||
task::{self},
|
||||
@ -13,6 +14,12 @@ use crate::player::{ui, Messages};
|
||||
/// Initializes the audio server, and then safely stops
|
||||
/// it when the frontend quits.
|
||||
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 player = Arc::new(Player::new().await?);
|
||||
|
@ -5,12 +5,13 @@
|
||||
use std::{collections::VecDeque, sync::Arc, time::Duration};
|
||||
|
||||
use arc_swap::ArcSwapOption;
|
||||
use downloader::Downloader;
|
||||
use reqwest::Client;
|
||||
use rodio::{OutputStream, OutputStreamHandle, Sink};
|
||||
use tokio::{
|
||||
select,
|
||||
sync::{
|
||||
mpsc::{self, Receiver, Sender},
|
||||
mpsc::{Receiver, Sender},
|
||||
RwLock,
|
||||
},
|
||||
task,
|
||||
@ -18,6 +19,7 @@ use tokio::{
|
||||
|
||||
use crate::tracks::{DecodedTrack, Track, TrackInfo};
|
||||
|
||||
pub mod downloader;
|
||||
pub mod ui;
|
||||
|
||||
/// Handles communication between the frontend & audio player.
|
||||
@ -122,39 +124,22 @@ impl Player {
|
||||
|
||||
/// 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.
|
||||
pub async fn play(
|
||||
queue: Arc<Self>,
|
||||
player: Arc<Self>,
|
||||
tx: Sender<Messages>,
|
||||
mut rx: Receiver<Messages>,
|
||||
) -> eyre::Result<()> {
|
||||
// This is an internal channel which serves pretty much only one purpose,
|
||||
// which is to notify the buffer refiller to get back to work.
|
||||
// This channel is useful to prevent needing to check with some infinite loop.
|
||||
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);
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
// `itx` is used to notify the `Downloader` when it needs to download new tracks.
|
||||
let (downloader, itx) = Downloader::new(player.clone());
|
||||
downloader.start().await;
|
||||
|
||||
// Start buffering tracks immediately.
|
||||
itx.send(()).await?;
|
||||
|
||||
loop {
|
||||
let clone = Arc::clone(&queue);
|
||||
let clone = Arc::clone(&player);
|
||||
let msg = select! {
|
||||
Some(x) = rx.recv() => x,
|
||||
|
||||
@ -166,17 +151,17 @@ impl Player {
|
||||
Messages::Next | Messages::Init | Messages::TryAgain => {
|
||||
// Skip as early as possible so that music doesn't play
|
||||
// while lowfi is "loading".
|
||||
queue.sink.stop();
|
||||
player.sink.stop();
|
||||
|
||||
// Serves as an indicator that the queue is "loading".
|
||||
// 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 {
|
||||
Ok(track) => {
|
||||
queue.sink.append(track.data);
|
||||
player.sink.append(track.data);
|
||||
|
||||
// Notify the background downloader that there's an empty spot
|
||||
// in the buffer.
|
||||
@ -192,10 +177,10 @@ impl Player {
|
||||
};
|
||||
}
|
||||
Messages::Pause => {
|
||||
if queue.sink.is_paused() {
|
||||
queue.sink.play();
|
||||
if player.sink.is_paused() {
|
||||
player.sink.play();
|
||||
} 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 crossterm::{
|
||||
cursor::{Hide, MoveToColumn, MoveUp, Show},
|
||||
cursor::{Hide, MoveToColumn, MoveUp, RestorePosition, Show},
|
||||
event::{self, KeyCode, KeyModifiers},
|
||||
style::{Print, Stylize},
|
||||
terminal::{self, Clear, ClearType},
|
||||
@ -19,6 +19,11 @@ use tokio::{
|
||||
|
||||
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.
|
||||
fn format_duration(duration: &Duration) -> String {
|
||||
let seconds = duration.as_secs() % 60;
|
||||
@ -126,14 +131,19 @@ async fn interface(queue: Arc<Player>) -> eyre::Result<()> {
|
||||
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.
|
||||
pub async fn start(queue: Arc<Player>, sender: Sender<Messages>) -> eyre::Result<()> {
|
||||
crossterm::execute!(
|
||||
stderr(),
|
||||
RestorePosition,
|
||||
Clear(ClearType::FromCursorDown),
|
||||
Hide
|
||||
)?;
|
||||
terminal::enable_raw_mode()?;
|
||||
crossterm::execute!(stderr(), Hide)?;
|
||||
//crossterm::execute!(stderr(), EnterAlternateScreen, MoveTo(0, 0))?;
|
||||
|
||||
task::spawn(interface(Arc::clone(&queue)));
|
||||
|
@ -1,14 +1,14 @@
|
||||
//! Has all of the functions for the `scrape` command.
|
||||
|
||||
use std::sync::LazyLock;
|
||||
|
||||
use futures::{stream::FuturesUnordered, StreamExt};
|
||||
use lazy_static::lazy_static;
|
||||
use scraper::{Html, Selector};
|
||||
|
||||
const BASE_URL: &str = "https://lofigirl.com/wp-content/uploads/";
|
||||
|
||||
static SELECTOR: LazyLock<Selector> =
|
||||
LazyLock::new(|| Selector::parse("html > body > pre > a").unwrap());
|
||||
lazy_static! {
|
||||
static ref SELECTOR: Selector = Selector::parse("html > body > pre > a").unwrap();
|
||||
}
|
||||
|
||||
async fn parse(path: &str) -> eyre::Result<Vec<String>> {
|
||||
let response = reqwest::get(format!("{}{}", BASE_URL, path)).await?;
|
||||
|
Loading…
Reference in New Issue
Block a user