mirror of
https://github.com/talwat/lowfi
synced 2025-02-06 15:51: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};
|
use clap::{Parser, Subcommand};
|
||||||
|
|
||||||
mod play;
|
mod play;
|
||||||
@ -18,10 +9,13 @@ mod tracks;
|
|||||||
#[derive(Parser)]
|
#[derive(Parser)]
|
||||||
#[command(about)]
|
#[command(about)]
|
||||||
struct Args {
|
struct Args {
|
||||||
|
/// The command that was ran.
|
||||||
|
/// This is [None] if no command was specified.
|
||||||
#[command(subcommand)]
|
#[command(subcommand)]
|
||||||
command: Option<Commands>,
|
command: Option<Commands>,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Defines all of the extra commands lowfi can run.
|
||||||
#[derive(Subcommand)]
|
#[derive(Subcommand)]
|
||||||
enum Commands {
|
enum Commands {
|
||||||
/// Scrapes the lofi girl website file server for files.
|
/// 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 (tx, rx) = mpsc::channel(8);
|
||||||
|
|
||||||
let player = Arc::new(Player::new().await?);
|
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?;
|
tx.send(Messages::Init).await?;
|
||||||
|
|
||||||
ui::start(player.clone(), tx.clone()).await?;
|
ui::start(Arc::clone(&player), tx.clone()).await?;
|
||||||
|
|
||||||
audio.abort();
|
audio.abort();
|
||||||
player.sink.stop();
|
player.sink.stop();
|
||||||
|
@ -93,9 +93,8 @@ impl Player {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// 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(queue: Arc<Player>) -> eyre::Result<DecodedTrack> {
|
pub async fn next(queue: Arc<Self>) -> eyre::Result<DecodedTrack> {
|
||||||
let track = queue.tracks.write().await.pop_front();
|
let track = match queue.tracks.write().await.pop_front() {
|
||||||
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.
|
||||||
@ -112,7 +111,7 @@ impl Player {
|
|||||||
///
|
///
|
||||||
/// `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(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,
|
// This is an internal channel which serves pretty much only one purpose,
|
||||||
// which is to notify the buffer refiller to get back to work.
|
// 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.
|
// 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.
|
// This refills the queue in the background.
|
||||||
task::spawn({
|
task::spawn({
|
||||||
let queue = queue.clone();
|
let queue = Arc::clone(&queue);
|
||||||
|
|
||||||
async move {
|
async move {
|
||||||
while let Some(()) = irx.recv().await {
|
while irx.recv().await == Some(()) {
|
||||||
while queue.tracks.read().await.len() < BUFFER_SIZE {
|
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);
|
queue.tracks.write().await.push_back(track);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -141,7 +142,7 @@ impl Player {
|
|||||||
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.sink.sleep_until_end()) => Messages::Next,
|
Ok(_) = task::spawn_blocking(move || clone.sink.sleep_until_end()) => Messages::Next,
|
||||||
};
|
};
|
||||||
|
|
||||||
match msg {
|
match msg {
|
||||||
@ -155,7 +156,7 @@ impl Player {
|
|||||||
itx.send(()).await?;
|
itx.send(()).await?;
|
||||||
|
|
||||||
queue.sink.stop();
|
queue.sink.stop();
|
||||||
let track = Player::next(queue.clone()).await?;
|
let track = Self::next(Arc::clone(&queue)).await?;
|
||||||
queue.sink.append(track.data);
|
queue.sink.append(track.data);
|
||||||
}
|
}
|
||||||
Messages::Pause => {
|
Messages::Pause => {
|
||||||
|
@ -1,3 +1,5 @@
|
|||||||
|
//! The module which manages all user interface, including inputs.
|
||||||
|
|
||||||
use std::{io::stderr, sync::Arc, time::Duration};
|
use std::{io::stderr, sync::Arc, time::Duration};
|
||||||
|
|
||||||
use crate::tracks::TrackInfo;
|
use crate::tracks::TrackInfo;
|
||||||
@ -5,8 +7,9 @@ use crate::tracks::TrackInfo;
|
|||||||
use super::Player;
|
use super::Player;
|
||||||
use crossterm::{
|
use crossterm::{
|
||||||
cursor::{Hide, MoveToColumn, MoveUp, Show},
|
cursor::{Hide, MoveToColumn, MoveUp, Show},
|
||||||
|
event,
|
||||||
style::{Print, Stylize},
|
style::{Print, Stylize},
|
||||||
terminal::{Clear, ClearType},
|
terminal::{self, Clear, ClearType},
|
||||||
};
|
};
|
||||||
use tokio::{
|
use tokio::{
|
||||||
sync::mpsc::Sender,
|
sync::mpsc::Sender,
|
||||||
@ -16,6 +19,7 @@ use tokio::{
|
|||||||
|
|
||||||
use super::Messages;
|
use super::Messages;
|
||||||
|
|
||||||
|
/// 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;
|
||||||
let minutes = duration.as_secs() / 60;
|
let minutes = duration.as_secs() / 60;
|
||||||
@ -31,43 +35,49 @@ enum ActionBar {
|
|||||||
}
|
}
|
||||||
|
|
||||||
impl 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) {
|
fn format(&self) -> (String, usize) {
|
||||||
let (word, subject) = match self {
|
let (word, subject) = match self {
|
||||||
ActionBar::Playing(x) => ("playing", Some(x.name.clone())),
|
Self::Playing(x) => ("playing", Some(x.name.clone())),
|
||||||
ActionBar::Paused(x) => ("paused", Some(x.name.clone())),
|
Self::Paused(x) => ("paused", Some(x.name.clone())),
|
||||||
ActionBar::Loading => ("loading", None),
|
Self::Loading => ("loading", None),
|
||||||
};
|
};
|
||||||
|
|
||||||
if let Some(subject) = subject {
|
subject.map_or_else(
|
||||||
(
|
|| (word.to_owned(), word.len()),
|
||||||
format!("{} {}", word, subject.clone().bold()),
|
|subject| {
|
||||||
word.len() + 1 + subject.len(),
|
(
|
||||||
)
|
format!("{} {}", word, subject.clone().bold()),
|
||||||
} else {
|
word.len() + 1 + subject.len(),
|
||||||
(word.to_string(), word.len())
|
)
|
||||||
}
|
},
|
||||||
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// The code for the interface itself.
|
/// The code for the interface itself.
|
||||||
async fn interface(queue: Arc<Player>) -> eyre::Result<()> {
|
async fn interface(queue: Arc<Player>) -> eyre::Result<()> {
|
||||||
|
/// The total width of the UI.
|
||||||
const WIDTH: usize = 27;
|
const WIDTH: usize = 27;
|
||||||
|
|
||||||
|
/// The width of the progress bar, not including the borders (`[` and `]`) or padding.
|
||||||
const PROGRESS_WIDTH: usize = WIDTH - 16;
|
const PROGRESS_WIDTH: usize = WIDTH - 16;
|
||||||
|
|
||||||
loop {
|
loop {
|
||||||
let (mut main, len) = match queue.current.load().as_ref() {
|
let (mut main, len) = queue
|
||||||
Some(x) => {
|
.current
|
||||||
let name = (*x.clone()).clone();
|
.load()
|
||||||
|
.as_ref()
|
||||||
|
.map_or(ActionBar::Loading, |x| {
|
||||||
|
let name = (*Arc::clone(&x)).clone();
|
||||||
if queue.sink.is_paused() {
|
if queue.sink.is_paused() {
|
||||||
ActionBar::Paused(name)
|
ActionBar::Paused(name)
|
||||||
} else {
|
} else {
|
||||||
ActionBar::Playing(name)
|
ActionBar::Playing(name)
|
||||||
}
|
}
|
||||||
}
|
})
|
||||||
None => ActionBar::Loading,
|
.format();
|
||||||
}
|
|
||||||
.format();
|
|
||||||
|
|
||||||
if len > WIDTH {
|
if len > WIDTH {
|
||||||
main = format!("{}...", &main[..=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.
|
/// 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::terminal::enable_raw_mode()?;
|
terminal::enable_raw_mode()?;
|
||||||
crossterm::execute!(stderr(), Hide)?;
|
crossterm::execute!(stderr(), Hide)?;
|
||||||
//crossterm::execute!(stderr(), EnterAlternateScreen, MoveTo(0, 0))?;
|
//crossterm::execute!(stderr(), EnterAlternateScreen, MoveTo(0, 0))?;
|
||||||
|
|
||||||
task::spawn(interface(queue.clone()));
|
task::spawn(interface(Arc::clone(&queue)));
|
||||||
|
|
||||||
loop {
|
loop {
|
||||||
let crossterm::event::Event::Key(event) = crossterm::event::read()? else {
|
let event::Event::Key(event) = event::read()? else {
|
||||||
continue;
|
continue;
|
||||||
};
|
};
|
||||||
|
|
||||||
let crossterm::event::KeyCode::Char(code) = event.code else {
|
let event::KeyCode::Char(code) = event.code else {
|
||||||
continue;
|
continue;
|
||||||
};
|
};
|
||||||
|
|
||||||
@ -155,7 +165,7 @@ pub async fn start(queue: Arc<Player>, sender: Sender<Messages>) -> eyre::Result
|
|||||||
|
|
||||||
//crossterm::execute!(stderr(), LeaveAlternateScreen)?;
|
//crossterm::execute!(stderr(), LeaveAlternateScreen)?;
|
||||||
crossterm::execute!(stderr(), Clear(ClearType::FromCursorDown), Show)?;
|
crossterm::execute!(stderr(), Clear(ClearType::FromCursorDown), Show)?;
|
||||||
crossterm::terminal::disable_raw_mode()?;
|
terminal::disable_raw_mode()?;
|
||||||
|
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
@ -10,6 +10,7 @@ use rand::Rng;
|
|||||||
use reqwest::Client;
|
use reqwest::Client;
|
||||||
use rodio::{Decoder, Source};
|
use rodio::{Decoder, Source};
|
||||||
|
|
||||||
|
/// Downloads a raw track, but doesn't decode it.
|
||||||
async fn download(track: &str, client: &Client) -> eyre::Result<Bytes> {
|
async fn download(track: &str, client: &Client) -> eyre::Result<Bytes> {
|
||||||
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?;
|
||||||
@ -18,16 +19,19 @@ async fn download(track: &str, client: &Client) -> eyre::Result<Bytes> {
|
|||||||
Ok(data)
|
Ok(data)
|
||||||
}
|
}
|
||||||
|
|
||||||
async fn random() -> eyre::Result<&'static str> {
|
/// Gets a random track from `tracks.txt` and returns it.
|
||||||
let tracks = include_str!("../data/tracks.txt");
|
fn random() -> &'static str {
|
||||||
let tracks: Vec<&str> = tracks.split_ascii_whitespace().collect();
|
let tracks: Vec<&str> = include_str!("../data/tracks.txt")
|
||||||
|
.split_ascii_whitespace()
|
||||||
|
.collect();
|
||||||
|
|
||||||
let random = rand::thread_rng().gen_range(0..tracks.len());
|
let random = rand::thread_rng().gen_range(0..tracks.len());
|
||||||
let track = tracks[random];
|
let track = tracks[random];
|
||||||
|
|
||||||
Ok(track)
|
track
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Just a shorthand for a decoded [Bytes].
|
||||||
pub type DecodedData = Decoder<Cursor<Bytes>>;
|
pub type DecodedData = Decoder<Cursor<Bytes>>;
|
||||||
|
|
||||||
/// The TrackInfo struct, which has the name and duration of a track.
|
/// 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 {
|
pub struct TrackInfo {
|
||||||
/// This is a formatted name, so it doesn't include the full path.
|
/// This is a formatted name, so it doesn't include the full path.
|
||||||
pub name: String,
|
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>,
|
pub duration: Option<Duration>,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl TrackInfo {
|
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 {
|
fn format_name(name: &'static str) -> String {
|
||||||
let mut formatted = name
|
let mut formatted = name
|
||||||
.split("/")
|
.split("/")
|
||||||
@ -52,6 +62,9 @@ impl TrackInfo {
|
|||||||
.to_title_case();
|
.to_title_case();
|
||||||
|
|
||||||
let mut skip = 0;
|
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() } {
|
for character in unsafe { formatted.as_bytes_mut() } {
|
||||||
if character.is_ascii_digit() {
|
if character.is_ascii_digit() {
|
||||||
skip += 1;
|
skip += 1;
|
||||||
@ -63,6 +76,7 @@ impl TrackInfo {
|
|||||||
String::from(&formatted[skip..])
|
String::from(&formatted[skip..])
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Creates a new [`TrackInfo`] from a raw name & decoded track data.
|
||||||
pub fn new(name: &'static str, decoded: &DecodedData) -> Self {
|
pub fn new(name: &'static str, decoded: &DecodedData) -> Self {
|
||||||
Self {
|
Self {
|
||||||
duration: decoded.total_duration(),
|
duration: decoded.total_duration(),
|
||||||
@ -82,6 +96,8 @@ pub struct DecodedTrack {
|
|||||||
}
|
}
|
||||||
|
|
||||||
impl DecodedTrack {
|
impl DecodedTrack {
|
||||||
|
/// Creates a new track.
|
||||||
|
/// This is equivalent to [Track::decode].
|
||||||
pub fn new(track: Track) -> eyre::Result<Self> {
|
pub fn new(track: Track) -> eyre::Result<Self> {
|
||||||
let data = Decoder::new(Cursor::new(track.data))?;
|
let data = Decoder::new(Cursor::new(track.data))?;
|
||||||
let info = TrackInfo::new(track.name, &data);
|
let info = TrackInfo::new(track.name, &data);
|
||||||
@ -103,7 +119,7 @@ pub struct Track {
|
|||||||
impl Track {
|
impl Track {
|
||||||
/// Fetches and downloads a random track from the tracklist.
|
/// Fetches and downloads a random track from the tracklist.
|
||||||
pub async fn random(client: &Client) -> eyre::Result<Self> {
|
pub async fn random(client: &Client) -> eyre::Result<Self> {
|
||||||
let name = random().await?;
|
let name = random();
|
||||||
let data = download(name, client).await?;
|
let data = download(name, client).await?;
|
||||||
|
|
||||||
Ok(Self { data, name })
|
Ok(Self { data, name })
|
||||||
|
Loading…
x
Reference in New Issue
Block a user