mirror of
https://github.com/talwat/lowfi
synced 2025-01-28 03:11:27 +00:00
chore: fix some more lints and make the code more compact
This commit is contained in:
parent
8110b8418a
commit
65f7574765
12
src/main.rs
12
src/main.rs
@ -1,12 +1,3 @@
|
||||
#![warn(
|
||||
clippy::all,
|
||||
clippy::restriction,
|
||||
clippy::pedantic,
|
||||
clippy::nursery,
|
||||
clippy::cargo
|
||||
)]
|
||||
#![allow(clippy::single_call_fn)]
|
||||
|
||||
use clap::{Parser, Subcommand};
|
||||
|
||||
mod play;
|
||||
@ -18,10 +9,13 @@ mod tracks;
|
||||
#[derive(Parser)]
|
||||
#[command(about)]
|
||||
struct Args {
|
||||
/// The command that was ran.
|
||||
/// This is [None] if no command was specified.
|
||||
#[command(subcommand)]
|
||||
command: Option<Commands>,
|
||||
}
|
||||
|
||||
/// Defines all of the extra commands lowfi can run.
|
||||
#[derive(Subcommand)]
|
||||
enum Commands {
|
||||
/// Scrapes the lofi girl website file server for files.
|
||||
|
@ -16,10 +16,10 @@ 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));
|
||||
let audio = task::spawn(Player::play(Arc::clone(&player), rx));
|
||||
tx.send(Messages::Init).await?;
|
||||
|
||||
ui::start(player.clone(), tx.clone()).await?;
|
||||
ui::start(Arc::clone(&player), tx.clone()).await?;
|
||||
|
||||
audio.abort();
|
||||
player.sink.stop();
|
||||
|
@ -93,9 +93,8 @@ impl Player {
|
||||
}
|
||||
|
||||
/// This will play the next track, as well as refilling the buffer in the background.
|
||||
pub async fn next(queue: Arc<Player>) -> eyre::Result<DecodedTrack> {
|
||||
let track = queue.tracks.write().await.pop_front();
|
||||
let track = match track {
|
||||
pub async fn next(queue: Arc<Self>) -> eyre::Result<DecodedTrack> {
|
||||
let track = match queue.tracks.write().await.pop_front() {
|
||||
Some(x) => x,
|
||||
// If the queue is completely empty, then fallback to simply getting a new track.
|
||||
// This is relevant particularly at the first song.
|
||||
@ -112,7 +111,7 @@ impl Player {
|
||||
///
|
||||
/// `rx` is used to communicate with it, for example when to
|
||||
/// skip tracks or pause.
|
||||
pub async fn play(queue: Arc<Player>, mut rx: Receiver<Messages>) -> eyre::Result<()> {
|
||||
pub async fn play(queue: Arc<Self>, 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.
|
||||
@ -120,12 +119,14 @@ impl Player {
|
||||
|
||||
// This refills the queue in the background.
|
||||
task::spawn({
|
||||
let queue = queue.clone();
|
||||
let queue = Arc::clone(&queue);
|
||||
|
||||
async move {
|
||||
while let Some(()) = irx.recv().await {
|
||||
while irx.recv().await == Some(()) {
|
||||
while queue.tracks.read().await.len() < BUFFER_SIZE {
|
||||
let track = Track::random(&queue.client).await.unwrap();
|
||||
let Ok(track) = Track::random(&queue.client).await else {
|
||||
continue;
|
||||
};
|
||||
queue.tracks.write().await.push_back(track);
|
||||
}
|
||||
}
|
||||
@ -141,7 +142,7 @@ impl Player {
|
||||
Some(x) = rx.recv() => x,
|
||||
|
||||
// This future will finish only at the end of the current track.
|
||||
Ok(()) = task::spawn_blocking(move || clone.sink.sleep_until_end()) => Messages::Next,
|
||||
Ok(_) = task::spawn_blocking(move || clone.sink.sleep_until_end()) => Messages::Next,
|
||||
};
|
||||
|
||||
match msg {
|
||||
@ -155,7 +156,7 @@ impl Player {
|
||||
itx.send(()).await?;
|
||||
|
||||
queue.sink.stop();
|
||||
let track = Player::next(queue.clone()).await?;
|
||||
let track = Self::next(Arc::clone(&queue)).await?;
|
||||
queue.sink.append(track.data);
|
||||
}
|
||||
Messages::Pause => {
|
||||
|
@ -1,3 +1,5 @@
|
||||
//! The module which manages all user interface, including inputs.
|
||||
|
||||
use std::{io::stderr, sync::Arc, time::Duration};
|
||||
|
||||
use crate::tracks::TrackInfo;
|
||||
@ -5,8 +7,9 @@ use crate::tracks::TrackInfo;
|
||||
use super::Player;
|
||||
use crossterm::{
|
||||
cursor::{Hide, MoveToColumn, MoveUp, Show},
|
||||
event,
|
||||
style::{Print, Stylize},
|
||||
terminal::{Clear, ClearType},
|
||||
terminal::{self, Clear, ClearType},
|
||||
};
|
||||
use tokio::{
|
||||
sync::mpsc::Sender,
|
||||
@ -16,6 +19,7 @@ use tokio::{
|
||||
|
||||
use super::Messages;
|
||||
|
||||
/// Small helper function to format durations.
|
||||
fn format_duration(duration: &Duration) -> String {
|
||||
let seconds = duration.as_secs() % 60;
|
||||
let minutes = duration.as_secs() / 60;
|
||||
@ -31,43 +35,49 @@ enum ActionBar {
|
||||
}
|
||||
|
||||
impl ActionBar {
|
||||
/// Formats the action bar to be displayed.
|
||||
/// The second value is the character length of the result.
|
||||
fn format(&self) -> (String, usize) {
|
||||
let (word, subject) = match self {
|
||||
ActionBar::Playing(x) => ("playing", Some(x.name.clone())),
|
||||
ActionBar::Paused(x) => ("paused", Some(x.name.clone())),
|
||||
ActionBar::Loading => ("loading", None),
|
||||
Self::Playing(x) => ("playing", Some(x.name.clone())),
|
||||
Self::Paused(x) => ("paused", Some(x.name.clone())),
|
||||
Self::Loading => ("loading", None),
|
||||
};
|
||||
|
||||
if let Some(subject) = subject {
|
||||
(
|
||||
format!("{} {}", word, subject.clone().bold()),
|
||||
word.len() + 1 + subject.len(),
|
||||
)
|
||||
} else {
|
||||
(word.to_string(), word.len())
|
||||
}
|
||||
subject.map_or_else(
|
||||
|| (word.to_owned(), word.len()),
|
||||
|subject| {
|
||||
(
|
||||
format!("{} {}", word, subject.clone().bold()),
|
||||
word.len() + 1 + subject.len(),
|
||||
)
|
||||
},
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
/// The code for the interface itself.
|
||||
async fn interface(queue: Arc<Player>) -> eyre::Result<()> {
|
||||
/// The total width of the UI.
|
||||
const WIDTH: usize = 27;
|
||||
|
||||
/// The width of the progress bar, not including the borders (`[` and `]`) or padding.
|
||||
const PROGRESS_WIDTH: usize = WIDTH - 16;
|
||||
|
||||
loop {
|
||||
let (mut main, len) = match queue.current.load().as_ref() {
|
||||
Some(x) => {
|
||||
let name = (*x.clone()).clone();
|
||||
|
||||
let (mut main, len) = queue
|
||||
.current
|
||||
.load()
|
||||
.as_ref()
|
||||
.map_or(ActionBar::Loading, |x| {
|
||||
let name = (*Arc::clone(&x)).clone();
|
||||
if queue.sink.is_paused() {
|
||||
ActionBar::Paused(name)
|
||||
} else {
|
||||
ActionBar::Playing(name)
|
||||
}
|
||||
}
|
||||
None => ActionBar::Loading,
|
||||
}
|
||||
.format();
|
||||
})
|
||||
.format();
|
||||
|
||||
if len > WIDTH {
|
||||
main = format!("{}...", &main[..=WIDTH]);
|
||||
@ -122,18 +132,18 @@ async fn interface(queue: Arc<Player>) -> eyre::Result<()> {
|
||||
|
||||
/// 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::terminal::enable_raw_mode()?;
|
||||
terminal::enable_raw_mode()?;
|
||||
crossterm::execute!(stderr(), Hide)?;
|
||||
//crossterm::execute!(stderr(), EnterAlternateScreen, MoveTo(0, 0))?;
|
||||
|
||||
task::spawn(interface(queue.clone()));
|
||||
task::spawn(interface(Arc::clone(&queue)));
|
||||
|
||||
loop {
|
||||
let crossterm::event::Event::Key(event) = crossterm::event::read()? else {
|
||||
let event::Event::Key(event) = event::read()? else {
|
||||
continue;
|
||||
};
|
||||
|
||||
let crossterm::event::KeyCode::Char(code) = event.code else {
|
||||
let event::KeyCode::Char(code) = event.code else {
|
||||
continue;
|
||||
};
|
||||
|
||||
@ -155,7 +165,7 @@ pub async fn start(queue: Arc<Player>, sender: Sender<Messages>) -> eyre::Result
|
||||
|
||||
//crossterm::execute!(stderr(), LeaveAlternateScreen)?;
|
||||
crossterm::execute!(stderr(), Clear(ClearType::FromCursorDown), Show)?;
|
||||
crossterm::terminal::disable_raw_mode()?;
|
||||
terminal::disable_raw_mode()?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
@ -10,6 +10,7 @@ use rand::Rng;
|
||||
use reqwest::Client;
|
||||
use rodio::{Decoder, Source};
|
||||
|
||||
/// Downloads a raw track, but doesn't decode it.
|
||||
async fn download(track: &str, client: &Client) -> eyre::Result<Bytes> {
|
||||
let url = format!("https://lofigirl.com/wp-content/uploads/{}", track);
|
||||
let response = client.get(url).send().await?;
|
||||
@ -18,16 +19,19 @@ async fn download(track: &str, client: &Client) -> eyre::Result<Bytes> {
|
||||
Ok(data)
|
||||
}
|
||||
|
||||
async fn random() -> eyre::Result<&'static str> {
|
||||
let tracks = include_str!("../data/tracks.txt");
|
||||
let tracks: Vec<&str> = tracks.split_ascii_whitespace().collect();
|
||||
/// Gets a random track from `tracks.txt` and returns it.
|
||||
fn random() -> &'static str {
|
||||
let tracks: Vec<&str> = include_str!("../data/tracks.txt")
|
||||
.split_ascii_whitespace()
|
||||
.collect();
|
||||
|
||||
let random = rand::thread_rng().gen_range(0..tracks.len());
|
||||
let track = tracks[random];
|
||||
|
||||
Ok(track)
|
||||
track
|
||||
}
|
||||
|
||||
/// Just a shorthand for a decoded [Bytes].
|
||||
pub type DecodedData = Decoder<Cursor<Bytes>>;
|
||||
|
||||
/// The TrackInfo struct, which has the name and duration of a track.
|
||||
@ -38,10 +42,16 @@ pub type DecodedData = Decoder<Cursor<Bytes>>;
|
||||
pub struct TrackInfo {
|
||||
/// This is a formatted name, so it doesn't include the full path.
|
||||
pub name: String,
|
||||
|
||||
/// The duration of the track, this is an [Option] because there are
|
||||
/// cases where the duration of a track is unknown.
|
||||
pub duration: Option<Duration>,
|
||||
}
|
||||
|
||||
impl TrackInfo {
|
||||
/// Formats a name with [Inflector].
|
||||
/// This will also strip the first few numbers that are
|
||||
/// usually present on most lofi tracks.
|
||||
fn format_name(name: &'static str) -> String {
|
||||
let mut formatted = name
|
||||
.split("/")
|
||||
@ -52,6 +62,9 @@ impl TrackInfo {
|
||||
.to_title_case();
|
||||
|
||||
let mut skip = 0;
|
||||
|
||||
// SAFETY: All of the track names originate with the `'static` lifetime,
|
||||
// SAFETY: so basically this has already been checked.
|
||||
for character in unsafe { formatted.as_bytes_mut() } {
|
||||
if character.is_ascii_digit() {
|
||||
skip += 1;
|
||||
@ -63,6 +76,7 @@ impl TrackInfo {
|
||||
String::from(&formatted[skip..])
|
||||
}
|
||||
|
||||
/// Creates a new [`TrackInfo`] from a raw name & decoded track data.
|
||||
pub fn new(name: &'static str, decoded: &DecodedData) -> Self {
|
||||
Self {
|
||||
duration: decoded.total_duration(),
|
||||
@ -82,6 +96,8 @@ pub struct DecodedTrack {
|
||||
}
|
||||
|
||||
impl DecodedTrack {
|
||||
/// Creates a new track.
|
||||
/// This is equivalent to [Track::decode].
|
||||
pub fn new(track: Track) -> eyre::Result<Self> {
|
||||
let data = Decoder::new(Cursor::new(track.data))?;
|
||||
let info = TrackInfo::new(track.name, &data);
|
||||
@ -103,7 +119,7 @@ pub struct Track {
|
||||
impl Track {
|
||||
/// Fetches and downloads a random track from the tracklist.
|
||||
pub async fn random(client: &Client) -> eyre::Result<Self> {
|
||||
let name = random().await?;
|
||||
let name = random();
|
||||
let data = download(name, client).await?;
|
||||
|
||||
Ok(Self { data, name })
|
||||
|
Loading…
x
Reference in New Issue
Block a user