mirror of
https://github.com/talwat/lowfi
synced 2025-02-05 07:11:27 +00:00
feat: add duration to tracks when possible
This commit is contained in:
parent
681889a268
commit
2102564d04
107
Cargo.lock
generated
107
Cargo.lock
generated
@ -110,6 +110,18 @@ dependencies = [
|
|||||||
"windows-sys 0.52.0",
|
"windows-sys 0.52.0",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "arc-swap"
|
||||||
|
version = "1.7.1"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "69f7f8c3906b62b754cd5326047894316021dcfe5a194c8ea52bdd94934a3457"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "arrayvec"
|
||||||
|
version = "0.7.6"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "7c02d123df017efcdfbd739ef81735b36c5ba83ec3c59c80a9d7ecc718f92e50"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "atomic-waker"
|
name = "atomic-waker"
|
||||||
version = "1.1.2"
|
version = "1.1.2"
|
||||||
@ -179,6 +191,12 @@ version = "3.16.0"
|
|||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "79296716171880943b8470b5f8d03aa55eb2e645a4874bdbb28adb49162e012c"
|
checksum = "79296716171880943b8470b5f8d03aa55eb2e645a4874bdbb28adb49162e012c"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "bytemuck"
|
||||||
|
version = "1.18.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "94bbb0ad554ad961ddc5da507a12a29b14e4ae5bda06b19f575a3e6079d2e2ae"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "byteorder"
|
name = "byteorder"
|
||||||
version = "1.5.0"
|
version = "1.5.0"
|
||||||
@ -909,6 +927,12 @@ dependencies = [
|
|||||||
"wasm-bindgen",
|
"wasm-bindgen",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "lazy_static"
|
||||||
|
version = "1.5.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "bbd2bcb4c963f2ddae06a2efc7e9f3591312473c50c6685e1f298068316e66fe"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "libc"
|
name = "libc"
|
||||||
version = "0.2.158"
|
version = "0.2.158"
|
||||||
@ -951,6 +975,7 @@ checksum = "a7a70ba024b9dc04c27ea2f0c0548feb474ec5c54bba33a7f72f873a39d07b24"
|
|||||||
name = "lowifi"
|
name = "lowifi"
|
||||||
version = "0.1.0"
|
version = "0.1.0"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
|
"arc-swap",
|
||||||
"bytes",
|
"bytes",
|
||||||
"clap",
|
"clap",
|
||||||
"crossterm",
|
"crossterm",
|
||||||
@ -1010,26 +1035,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 = "68354c5c6bd36d73ff3feceb05efa59b6acb7626617f4962be322a825e61f79a"
|
checksum = "68354c5c6bd36d73ff3feceb05efa59b6acb7626617f4962be322a825e61f79a"
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "minimp3-sys"
|
|
||||||
version = "0.3.2"
|
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "e21c73734c69dc95696c9ed8926a2b393171d98b3f5f5935686a26a487ab9b90"
|
|
||||||
dependencies = [
|
|
||||||
"cc",
|
|
||||||
]
|
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "minimp3_fixed"
|
|
||||||
version = "0.5.4"
|
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "42b0f14e7e75da97ae396c2656b10262a3d4afa2ec98f35795630eff0c8b951b"
|
|
||||||
dependencies = [
|
|
||||||
"minimp3-sys",
|
|
||||||
"slice-ring-buffer",
|
|
||||||
"thiserror",
|
|
||||||
]
|
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "miniz_oxide"
|
name = "miniz_oxide"
|
||||||
version = "0.8.0"
|
version = "0.8.0"
|
||||||
@ -1570,7 +1575,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
|
|||||||
checksum = "6006a627c1a38d37f3d3a85c6575418cfe34a5392d60a686d0071e1c8d427acb"
|
checksum = "6006a627c1a38d37f3d3a85c6575418cfe34a5392d60a686d0071e1c8d427acb"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"cpal",
|
"cpal",
|
||||||
"minimp3_fixed",
|
"symphonia",
|
||||||
"thiserror",
|
"thiserror",
|
||||||
]
|
]
|
||||||
|
|
||||||
@ -1831,17 +1836,6 @@ dependencies = [
|
|||||||
"autocfg",
|
"autocfg",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "slice-ring-buffer"
|
|
||||||
version = "0.3.4"
|
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "84ae312bda09b2368f79f985fdb4df4a0b5cbc75546b511303972d195f8c27d6"
|
|
||||||
dependencies = [
|
|
||||||
"libc",
|
|
||||||
"mach2",
|
|
||||||
"winapi",
|
|
||||||
]
|
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "smallvec"
|
name = "smallvec"
|
||||||
version = "1.13.2"
|
version = "1.13.2"
|
||||||
@ -1908,6 +1902,55 @@ version = "2.6.1"
|
|||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "13c2bddecc57b384dee18652358fb23172facb8a2c51ccc10d74c157bdea3292"
|
checksum = "13c2bddecc57b384dee18652358fb23172facb8a2c51ccc10d74c157bdea3292"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "symphonia"
|
||||||
|
version = "0.5.4"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "815c942ae7ee74737bb00f965fa5b5a2ac2ce7b6c01c0cc169bbeaf7abd5f5a9"
|
||||||
|
dependencies = [
|
||||||
|
"lazy_static",
|
||||||
|
"symphonia-bundle-mp3",
|
||||||
|
"symphonia-core",
|
||||||
|
"symphonia-metadata",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "symphonia-bundle-mp3"
|
||||||
|
version = "0.5.4"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "c01c2aae70f0f1fb096b6f0ff112a930b1fb3626178fba3ae68b09dce71706d4"
|
||||||
|
dependencies = [
|
||||||
|
"lazy_static",
|
||||||
|
"log",
|
||||||
|
"symphonia-core",
|
||||||
|
"symphonia-metadata",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "symphonia-core"
|
||||||
|
version = "0.5.4"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "798306779e3dc7d5231bd5691f5a813496dc79d3f56bf82e25789f2094e022c3"
|
||||||
|
dependencies = [
|
||||||
|
"arrayvec",
|
||||||
|
"bitflags 1.3.2",
|
||||||
|
"bytemuck",
|
||||||
|
"lazy_static",
|
||||||
|
"log",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "symphonia-metadata"
|
||||||
|
version = "0.5.4"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "bc622b9841a10089c5b18e99eb904f4341615d5aa55bbf4eedde1be721a4023c"
|
||||||
|
dependencies = [
|
||||||
|
"encoding_rs",
|
||||||
|
"lazy_static",
|
||||||
|
"log",
|
||||||
|
"symphonia-core",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "syn"
|
name = "syn"
|
||||||
version = "2.0.77"
|
version = "2.0.77"
|
||||||
|
21
Cargo.toml
21
Cargo.toml
@ -4,13 +4,22 @@ version = "0.1.0"
|
|||||||
edition = "2021"
|
edition = "2021"
|
||||||
|
|
||||||
[dependencies]
|
[dependencies]
|
||||||
|
|
||||||
|
# Basics
|
||||||
clap = { version = "4.5.18", features = ["derive", "cargo"] }
|
clap = { version = "4.5.18", features = ["derive", "cargo"] }
|
||||||
reqwest = { version = "0.12.7", features = ["blocking"] }
|
|
||||||
tokio = { version = "1.40.0", features = ["full"] }
|
|
||||||
scraper = "0.20.0"
|
|
||||||
rodio = { version = "0.19.0", features = ["minimp3"], default-features = false }
|
|
||||||
eyre = "0.6.12"
|
eyre = "0.6.12"
|
||||||
futures = "0.3.30"
|
|
||||||
bytes = "1.7.2"
|
|
||||||
rand = "0.8.5"
|
rand = "0.8.5"
|
||||||
|
|
||||||
|
# Async
|
||||||
|
tokio = { version = "1.40.0", features = ["full"] }
|
||||||
|
futures = "0.3.30"
|
||||||
|
arc-swap = "1.7.1"
|
||||||
|
|
||||||
|
# Data
|
||||||
|
reqwest = { version = "0.12.7", features = ["blocking"] }
|
||||||
|
bytes = "1.7.2"
|
||||||
|
|
||||||
|
# Misc
|
||||||
|
scraper = "0.20.0"
|
||||||
|
rodio = { version = "0.19.0", features = ["mp3"], default-features = false }
|
||||||
crossterm = "0.28.1"
|
crossterm = "0.28.1"
|
||||||
|
@ -1,5 +1,6 @@
|
|||||||
use clap::{Parser, Subcommand};
|
use clap::{Parser, Subcommand};
|
||||||
|
|
||||||
|
mod play;
|
||||||
mod player;
|
mod player;
|
||||||
mod scrape;
|
mod scrape;
|
||||||
mod tracks;
|
mod tracks;
|
||||||
@ -26,6 +27,6 @@ async fn main() -> eyre::Result<()> {
|
|||||||
|
|
||||||
match cli.command {
|
match cli.command {
|
||||||
Commands::Scrape => scrape::scrape().await,
|
Commands::Scrape => scrape::scrape().await,
|
||||||
Commands::Play => player::play().await,
|
Commands::Play => play::play().await,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
24
src/play.rs
Normal file
24
src/play.rs
Normal file
@ -0,0 +1,24 @@
|
|||||||
|
use std::sync::Arc;
|
||||||
|
|
||||||
|
use tokio::{
|
||||||
|
sync::mpsc::{self},
|
||||||
|
task::{self},
|
||||||
|
};
|
||||||
|
|
||||||
|
use crate::player::Player;
|
||||||
|
use crate::player::{ui, Messages};
|
||||||
|
|
||||||
|
pub async fn play() -> eyre::Result<()> {
|
||||||
|
let (tx, rx) = mpsc::channel(8);
|
||||||
|
|
||||||
|
let player = Arc::new(Player::new().await?);
|
||||||
|
let audio = task::spawn(Player::play(player.clone(), rx));
|
||||||
|
tx.send(Messages::Init).await?;
|
||||||
|
|
||||||
|
ui::start(player.clone(), tx.clone()).await?;
|
||||||
|
|
||||||
|
audio.abort();
|
||||||
|
player.sink.stop();
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
184
src/player.rs
184
src/player.rs
@ -1,11 +1,8 @@
|
|||||||
use std::{collections::VecDeque, io::stderr, sync::Arc};
|
use std::{collections::VecDeque, sync::Arc};
|
||||||
|
|
||||||
use crossterm::{
|
use arc_swap::ArcSwapOption;
|
||||||
cursor::{MoveDown, MoveToColumn, MoveToNextLine},
|
|
||||||
style::Print,
|
|
||||||
};
|
|
||||||
use reqwest::Client;
|
use reqwest::Client;
|
||||||
use rodio::{Decoder, OutputStream, Sink};
|
use rodio::{Decoder, OutputStream, OutputStreamHandle, Sink};
|
||||||
use tokio::{
|
use tokio::{
|
||||||
select,
|
select,
|
||||||
sync::{
|
sync::{
|
||||||
@ -15,56 +12,69 @@ use tokio::{
|
|||||||
task,
|
task,
|
||||||
};
|
};
|
||||||
|
|
||||||
/// The amount of songs to buffer up.
|
use crate::tracks::{Track, TrackInfo};
|
||||||
const BUFFER_SIZE: usize = 5;
|
|
||||||
|
|
||||||
use crate::tracks::Track;
|
pub mod ui;
|
||||||
|
|
||||||
/// Handles communication between the frontend & audio player.
|
/// Handles communication between the frontend & audio player.
|
||||||
pub enum Messages {
|
pub enum Messages {
|
||||||
Skip,
|
Next,
|
||||||
Die,
|
Init,
|
||||||
|
Pause,
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Main struct responsible for queuing up tracks.
|
/// The amount of songs to buffer up.
|
||||||
///
|
const BUFFER_SIZE: usize = 5;
|
||||||
/// Internally tracks are stored in an [Arc],
|
|
||||||
/// so it's fine to clone this struct.
|
/// Main struct responsible for queuing up & playing tracks.
|
||||||
#[derive(Debug, Clone)]
|
pub struct Player {
|
||||||
pub struct Queue {
|
pub sink: Sink,
|
||||||
tracks: Arc<RwLock<VecDeque<Track>>>,
|
pub current: ArcSwapOption<TrackInfo>,
|
||||||
|
tracks: RwLock<VecDeque<Track>>,
|
||||||
|
client: Client,
|
||||||
|
_handle: OutputStreamHandle,
|
||||||
|
_stream: OutputStream,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Queue {
|
unsafe impl Send for Player {}
|
||||||
pub async fn new() -> Self {
|
unsafe impl Sync for Player {}
|
||||||
Self {
|
|
||||||
tracks: Arc::new(RwLock::new(VecDeque::with_capacity(5))),
|
impl Player {
|
||||||
|
/// Initializes the entire player, including audio devices & sink.
|
||||||
|
pub async fn new() -> eyre::Result<Self> {
|
||||||
|
let (_stream, handle) = OutputStream::try_default()?;
|
||||||
|
let sink = Sink::try_new(&handle)?;
|
||||||
|
|
||||||
|
Ok(Self {
|
||||||
|
tracks: RwLock::new(VecDeque::with_capacity(5)),
|
||||||
|
current: ArcSwapOption::new(None),
|
||||||
|
client: Client::builder().build()?,
|
||||||
|
sink,
|
||||||
|
_handle: handle,
|
||||||
|
_stream,
|
||||||
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async fn set_current(&self, info: TrackInfo) -> eyre::Result<()> {
|
||||||
|
self.current.store(Some(Arc::new(info)));
|
||||||
|
|
||||||
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
/// This will play the next track, as well as refilling the buffer in the background.
|
/// This will play the next track, as well as refilling the buffer in the background.
|
||||||
pub async fn next(&self, client: &Client) -> eyre::Result<Track> {
|
pub async fn next(queue: Arc<Player>) -> eyre::Result<Track> {
|
||||||
// This refills the queue in the background.
|
queue.current.store(None);
|
||||||
task::spawn({
|
|
||||||
let client = client.clone();
|
|
||||||
let tracks = self.tracks.clone();
|
|
||||||
|
|
||||||
async move {
|
let track = queue.tracks.write().await.pop_front();
|
||||||
while tracks.read().await.len() < BUFFER_SIZE {
|
|
||||||
let track = Track::random(&client).await.unwrap();
|
|
||||||
tracks.write().await.push_back(track);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
let track = self.tracks.write().await.pop_front();
|
|
||||||
let track = match track {
|
let track = match track {
|
||||||
Some(x) => x,
|
Some(x) => x,
|
||||||
// If the queue is completely empty, then fallback to simply getting a new track.
|
// If the queue is completely empty, then fallback to simply getting a new track.
|
||||||
// This is relevant particularly at the first song.
|
// This is relevant particularly at the first song.
|
||||||
None => Track::random(client).await?,
|
None => Track::random(&queue.client).await?,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
queue.set_current(track.info).await?;
|
||||||
|
|
||||||
Ok(track)
|
Ok(track)
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -72,78 +82,54 @@ impl Queue {
|
|||||||
///
|
///
|
||||||
/// `rx` is used to communicate with it, for example when to
|
/// `rx` is 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<Player>, mut rx: Receiver<Messages>) -> eyre::Result<()> {
|
||||||
self,
|
// This is an internal channel which serves pretty much only one purpose,
|
||||||
sink: Sink,
|
// which is to notify the buffer refiller to get back to work.
|
||||||
client: Client,
|
// This channel is useful to prevent needing to check with some infinite loop.
|
||||||
mut rx: Receiver<Messages>,
|
let (itx, mut irx) = mpsc::channel(8);
|
||||||
) -> eyre::Result<()> {
|
|
||||||
let sink = Arc::new(sink);
|
// This refills the queue in the background.
|
||||||
|
task::spawn({
|
||||||
|
let queue = queue.clone();
|
||||||
|
|
||||||
|
async move {
|
||||||
|
while let Some(()) = irx.recv().await {
|
||||||
|
while queue.tracks.read().await.len() < BUFFER_SIZE {
|
||||||
|
let track = Track::random(&queue.client).await.unwrap();
|
||||||
|
queue.tracks.write().await.push_back(track);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
itx.send(()).await?;
|
||||||
|
|
||||||
loop {
|
loop {
|
||||||
let clone = sink.clone();
|
let clone = Arc::clone(&queue);
|
||||||
let msg = select! {
|
let msg = select! {
|
||||||
Some(x) = rx.recv() => x,
|
Some(x) = rx.recv() => x,
|
||||||
|
|
||||||
// This future will finish only at the end of the current track.
|
// This future will finish only at the end of the current track.
|
||||||
Ok(()) = task::spawn_blocking(move || clone.sleep_until_end()) => Messages::Skip,
|
Ok(()) = task::spawn_blocking(move || clone.sink.sleep_until_end()) => Messages::Next,
|
||||||
};
|
};
|
||||||
|
|
||||||
match msg {
|
match msg {
|
||||||
Messages::Skip => {
|
Messages::Next | Messages::Init => {
|
||||||
sink.stop();
|
itx.send(()).await?;
|
||||||
|
|
||||||
let track = self.next(&client).await?;
|
queue.sink.stop();
|
||||||
sink.append(Decoder::new(track.data)?);
|
|
||||||
}
|
|
||||||
Messages::Die => break,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Ok(())
|
let track = Player::next(queue.clone()).await?;
|
||||||
|
queue.sink.append(track.data);
|
||||||
|
}
|
||||||
|
Messages::Pause => {
|
||||||
|
if queue.sink.is_paused() {
|
||||||
|
queue.sink.play();
|
||||||
|
} else {
|
||||||
|
queue.sink.pause();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub async fn gui() -> eyre::Result<()> {
|
|
||||||
crossterm::execute!(stderr(), MoveToColumn(0), Print("hello!\r\n"))?;
|
|
||||||
crossterm::execute!(stderr(), Print("next line!\r\n"))?;
|
|
||||||
|
|
||||||
Ok(())
|
|
||||||
}
|
|
||||||
|
|
||||||
pub async fn play() -> eyre::Result<()> {
|
|
||||||
let queue = Queue::new().await;
|
|
||||||
let (tx, rx) = mpsc::channel(8);
|
|
||||||
let (_stream, handle) = OutputStream::try_default()?;
|
|
||||||
let sink = Sink::try_new(&handle)?;
|
|
||||||
let client = Client::builder().build()?;
|
|
||||||
|
|
||||||
let audio = task::spawn(queue.clone().play(sink, client.clone(), rx));
|
|
||||||
tx.send(Messages::Skip).await?; // This is responsible for the initial track being played.
|
|
||||||
|
|
||||||
crossterm::terminal::enable_raw_mode()?;
|
|
||||||
|
|
||||||
gui().await?;
|
|
||||||
|
|
||||||
'a: loop {
|
|
||||||
match crossterm::event::read()? {
|
|
||||||
crossterm::event::Event::Key(event) => match event.code {
|
|
||||||
crossterm::event::KeyCode::Char(x) => {
|
|
||||||
if x == 'q' {
|
|
||||||
tx.send(Messages::Die).await?;
|
|
||||||
|
|
||||||
break 'a;
|
|
||||||
} else if x == 's' {
|
|
||||||
tx.send(Messages::Skip).await?;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
_ => (),
|
|
||||||
},
|
|
||||||
_ => (),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
audio.abort();
|
|
||||||
crossterm::terminal::disable_raw_mode()?;
|
|
||||||
Ok(())
|
|
||||||
}
|
|
||||||
|
84
src/player/ui.rs
Normal file
84
src/player/ui.rs
Normal file
@ -0,0 +1,84 @@
|
|||||||
|
use std::{io::stderr, sync::Arc, time::Duration};
|
||||||
|
|
||||||
|
use super::Player;
|
||||||
|
use crossterm::{
|
||||||
|
cursor::{MoveTo, MoveToColumn, MoveUp},
|
||||||
|
style::Print,
|
||||||
|
terminal::{Clear, ClearType, EnterAlternateScreen, LeaveAlternateScreen},
|
||||||
|
};
|
||||||
|
use tokio::{
|
||||||
|
sync::mpsc::Sender,
|
||||||
|
task::{self},
|
||||||
|
time::sleep,
|
||||||
|
};
|
||||||
|
|
||||||
|
use super::Messages;
|
||||||
|
|
||||||
|
async fn interface(queue: Arc<Player>) -> eyre::Result<()> {
|
||||||
|
const WIDTH: usize = 25;
|
||||||
|
|
||||||
|
loop {
|
||||||
|
// We can get away with only redrawing every 0.25 seconds
|
||||||
|
// since it's just an audio player.
|
||||||
|
sleep(Duration::from_secs_f32(1.0 / 60.0)).await;
|
||||||
|
crossterm::execute!(stderr(), Clear(ClearType::FromCursorDown))?;
|
||||||
|
|
||||||
|
let main = match queue.current.load().as_ref() {
|
||||||
|
Some(x) => {
|
||||||
|
if queue.sink.is_paused() {
|
||||||
|
format!("paused {}\r\n", x.format_name())
|
||||||
|
} else {
|
||||||
|
format!("playing {}\r\n", x.format_name())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
None => "loading...\r\n".to_owned(),
|
||||||
|
};
|
||||||
|
|
||||||
|
let bar = ["[s]kip", "[p]ause", "[q]uit"];
|
||||||
|
|
||||||
|
crossterm::execute!(stderr(), MoveToColumn(0), Print(main))?;
|
||||||
|
crossterm::execute!(stderr(), Print(bar.join(" ")))?;
|
||||||
|
crossterm::execute!(stderr(), MoveToColumn(0), MoveUp(1))?;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub async fn start(queue: Arc<Player>, sender: Sender<Messages>) -> eyre::Result<()> {
|
||||||
|
crossterm::terminal::enable_raw_mode()?;
|
||||||
|
crossterm::execute!(stderr(), EnterAlternateScreen, MoveTo(0, 0))?;
|
||||||
|
|
||||||
|
task::spawn(interface(queue.clone()));
|
||||||
|
|
||||||
|
loop {
|
||||||
|
let crossterm::event::Event::Key(event) = crossterm::event::read()? else {
|
||||||
|
continue;
|
||||||
|
};
|
||||||
|
|
||||||
|
let crossterm::event::KeyCode::Char(code) = event.code else {
|
||||||
|
continue;
|
||||||
|
};
|
||||||
|
|
||||||
|
match code {
|
||||||
|
'q' => {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
's' => {
|
||||||
|
if !queue.current.load().is_none() {
|
||||||
|
sender.send(Messages::Next).await?
|
||||||
|
}
|
||||||
|
}
|
||||||
|
'p' => {
|
||||||
|
sender.send(Messages::Pause).await?;
|
||||||
|
}
|
||||||
|
_ => {}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
crossterm::execute!(
|
||||||
|
stderr(),
|
||||||
|
Clear(ClearType::FromCursorDown),
|
||||||
|
LeaveAlternateScreen
|
||||||
|
)?;
|
||||||
|
crossterm::terminal::disable_raw_mode()?;
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
@ -1,17 +1,19 @@
|
|||||||
use std::io::Cursor;
|
use std::{io::Cursor, time::Duration};
|
||||||
|
|
||||||
use bytes::Bytes;
|
use bytes::Bytes;
|
||||||
use rand::Rng;
|
use rand::Rng;
|
||||||
use reqwest::Client;
|
use reqwest::Client;
|
||||||
|
use rodio::{Decoder, Source};
|
||||||
|
|
||||||
pub type Data = Cursor<Bytes>;
|
pub type Data = Decoder<Cursor<Bytes>>;
|
||||||
|
|
||||||
async fn download(track: &str, client: &Client) -> eyre::Result<Data> {
|
async fn download(track: &str, client: &Client) -> eyre::Result<Data> {
|
||||||
let url = format!("https://lofigirl.com/wp-content/uploads/{}", track);
|
let url = format!("https://lofigirl.com/wp-content/uploads/{}", track);
|
||||||
let response = client.get(url).send().await?;
|
let response = client.get(url).send().await?;
|
||||||
let file = Cursor::new(response.bytes().await?);
|
let file = Cursor::new(response.bytes().await?);
|
||||||
|
let source = Decoder::new(file)?;
|
||||||
|
|
||||||
Ok(file)
|
Ok(source)
|
||||||
}
|
}
|
||||||
|
|
||||||
async fn random() -> eyre::Result<&'static str> {
|
async fn random() -> eyre::Result<&'static str> {
|
||||||
@ -24,9 +26,20 @@ async fn random() -> eyre::Result<&'static str> {
|
|||||||
Ok(track)
|
Ok(track)
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Debug, PartialEq)]
|
#[derive(Debug, PartialEq, Clone, Copy)]
|
||||||
pub struct Track {
|
pub struct TrackInfo {
|
||||||
pub name: &'static str,
|
pub name: &'static str,
|
||||||
|
pub duration: Option<Duration>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl TrackInfo {
|
||||||
|
pub fn format_name(&self) -> &'static str {
|
||||||
|
self.name.split("/").nth(2).unwrap()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub struct Track {
|
||||||
|
pub info: TrackInfo,
|
||||||
pub data: Data,
|
pub data: Data,
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -35,6 +48,12 @@ impl Track {
|
|||||||
let name = random().await?;
|
let name = random().await?;
|
||||||
let data = download(&name, client).await?;
|
let data = download(&name, client).await?;
|
||||||
|
|
||||||
Ok(Self { name, data })
|
Ok(Self {
|
||||||
|
info: TrackInfo {
|
||||||
|
name,
|
||||||
|
duration: data.total_duration(),
|
||||||
|
},
|
||||||
|
data,
|
||||||
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
Loading…
x
Reference in New Issue
Block a user