Reworked Queue widget to use external state
Update plyaer inner working and added agc Updated rodio to use git version
This commit is contained in:
35
Cargo.lock
generated
35
Cargo.lock
generated
@@ -1355,6 +1355,16 @@ dependencies = [
|
||||
"winapi",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "num-bigint"
|
||||
version = "0.4.6"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "a5e44f723f1133c9deac646763579fdb3ac745e418f2a7af9cd0c431da1f20b9"
|
||||
dependencies = [
|
||||
"num-integer",
|
||||
"num-traits",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "num-conv"
|
||||
version = "0.1.0"
|
||||
@@ -1372,6 +1382,26 @@ dependencies = [
|
||||
"syn 2.0.100",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "num-integer"
|
||||
version = "0.1.46"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "7969661fd2958a5cb096e56c8e1ad0444ac2bbcd0061bd28660485a44879858f"
|
||||
dependencies = [
|
||||
"num-traits",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "num-rational"
|
||||
version = "0.4.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "f83d14da390562dca69fc84082e73e548e1ad308d24accdedd2720017cb37824"
|
||||
dependencies = [
|
||||
"num-bigint",
|
||||
"num-integer",
|
||||
"num-traits",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "num-traits"
|
||||
version = "0.2.19"
|
||||
@@ -1810,13 +1840,14 @@ dependencies = [
|
||||
[[package]]
|
||||
name = "rodio"
|
||||
version = "0.20.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "e7ceb6607dd738c99bc8cb28eff249b7cd5c8ec88b9db96c0608c1480d140fb1"
|
||||
source = "git+https://github.com/RustAudio/rodio#0d352f5f2678226e843aa9c0ddea080f1e6d80ae"
|
||||
dependencies = [
|
||||
"claxon",
|
||||
"cpal",
|
||||
"dasp_sample",
|
||||
"hound",
|
||||
"lewton",
|
||||
"num-rational",
|
||||
"symphonia",
|
||||
]
|
||||
|
||||
|
||||
@@ -35,5 +35,5 @@ subsonic-types = "0.2.0"
|
||||
rand = "0.9"
|
||||
md5 = "0.7"
|
||||
|
||||
rodio = "0.20"
|
||||
rodio = { git = "https://github.com/RustAudio/rodio" }
|
||||
cpal = { version = "0.15", features = ["jack"] }
|
||||
|
||||
@@ -5,18 +5,20 @@ pub enum Action {
|
||||
Tick,
|
||||
Render,
|
||||
Resize(u16, u16),
|
||||
TryPlaySong(String),
|
||||
AddToSink(String),
|
||||
TryPlaySong(usize),
|
||||
AddToSink(usize),
|
||||
ScrollUp,
|
||||
ScrollDown,
|
||||
Quit,
|
||||
Init,
|
||||
RandomQueue,
|
||||
ClosePopUp,
|
||||
ForcePlaySong(String),
|
||||
ForcePlaySong(usize),
|
||||
VolumeUp,
|
||||
VolumeDown,
|
||||
PlayPause,
|
||||
UpdateQueue,
|
||||
Next,
|
||||
TrackEnded,
|
||||
Previous,
|
||||
}
|
||||
|
||||
45
src/app.rs
45
src/app.rs
@@ -1,11 +1,12 @@
|
||||
use color_eyre::eyre::Result;
|
||||
use crossterm::event::{Event as CrosstermEvent, KeyCode, KeyEvent};
|
||||
use rand::seq::index;
|
||||
use ratatui::{
|
||||
Terminal,
|
||||
buffer::Buffer,
|
||||
layout::{Constraint, Flex, Layout, Rect},
|
||||
prelude::CrosstermBackend,
|
||||
widgets::{Block, Clear, Paragraph, Widget, Wrap},
|
||||
widgets::{Block, Clear, Paragraph, StatefulWidget, TableState, Widget, Wrap},
|
||||
};
|
||||
use reqwest::Client;
|
||||
use serde::{Deserialize, Serialize};
|
||||
@@ -16,7 +17,7 @@ use tracing::{debug, info};
|
||||
use crate::{
|
||||
action::Action,
|
||||
event::{Event, Events},
|
||||
player::Player,
|
||||
player::{self, Player},
|
||||
subsonic_helper,
|
||||
widgets::Queue,
|
||||
};
|
||||
@@ -47,7 +48,7 @@ pub struct App {
|
||||
|
||||
client: Client,
|
||||
player: Player,
|
||||
queue: Queue,
|
||||
queue_state: TableState,
|
||||
}
|
||||
|
||||
impl Default for App {
|
||||
@@ -60,11 +61,10 @@ impl App {
|
||||
pub fn new() -> Self {
|
||||
let (tx, rx) = mpsc::unbounded_channel();
|
||||
let player = Player::new(tx.clone()).expect("Could not create rodio player");
|
||||
let songs_list = player.songs_list.clone();
|
||||
Self {
|
||||
player,
|
||||
client: Client::default(),
|
||||
queue: Queue::new(songs_list),
|
||||
queue_state: TableState::new(),
|
||||
rx,
|
||||
tx,
|
||||
mode: Mode::default(),
|
||||
@@ -120,15 +120,13 @@ impl App {
|
||||
match key.code {
|
||||
KeyCode::Esc if self.popup.is_some() => Some(Action::ClosePopUp),
|
||||
KeyCode::Enter => match self.mode {
|
||||
Mode::Queue => self
|
||||
.queue
|
||||
.get_selected()
|
||||
.map(|song| Action::ForcePlaySong(song.id)),
|
||||
Mode::Queue => self.queue_state.selected().map(Action::ForcePlaySong),
|
||||
_ => None,
|
||||
},
|
||||
KeyCode::Up => Some(Action::ScrollUp),
|
||||
KeyCode::Down => Some(Action::ScrollDown),
|
||||
KeyCode::Right => Some(Action::Next),
|
||||
KeyCode::Left => Some(Action::Previous),
|
||||
KeyCode::Char(char) => match char {
|
||||
'q' | 'Q' => Some(Action::Quit),
|
||||
'r' | 'R' if self.mode == Mode::Queue => Some(Action::RandomQueue),
|
||||
@@ -166,12 +164,12 @@ impl App {
|
||||
}
|
||||
Action::ScrollUp => {
|
||||
if self.mode == Mode::Queue {
|
||||
self.queue.scroll_up()
|
||||
self.queue_state.select_previous();
|
||||
}
|
||||
}
|
||||
Action::ScrollDown => {
|
||||
if self.mode == Mode::Queue {
|
||||
self.queue.scroll_down()
|
||||
self.queue_state.select_next();
|
||||
}
|
||||
}
|
||||
Action::RandomQueue => subsonic_helper::request_random(
|
||||
@@ -193,8 +191,22 @@ impl App {
|
||||
self.player.pause();
|
||||
}
|
||||
}
|
||||
Action::UpdateQueue => self.queue.update(),
|
||||
Action::Next => self.player.next(),
|
||||
Action::UpdateQueue => self.queue_state.select_first(),
|
||||
Action::Next => self.player.skip_next(),
|
||||
Action::Previous => {
|
||||
let index = self.player.current_playing;
|
||||
if index > 0 {
|
||||
self.tx.send(Action::ForcePlaySong(index - 1))?;
|
||||
self.player.current_playing -= 1;
|
||||
}
|
||||
}
|
||||
Action::TrackEnded => {
|
||||
let index = self.player.current_playing + 1;
|
||||
if index < self.player.songs_list.lock().unwrap().len() {
|
||||
self.tx.send(Action::TryPlaySong(index))?;
|
||||
self.player.current_playing += 1;
|
||||
}
|
||||
}
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
@@ -217,7 +229,12 @@ impl Widget for &mut App {
|
||||
Mode::Help => {}
|
||||
Mode::Popup => {}
|
||||
Mode::Quit => {}
|
||||
Mode::Queue => self.queue.render(area, buf),
|
||||
Mode::Queue => StatefulWidget::render(
|
||||
&Queue::new(self.player.songs_list.clone(), self.player.current_playing),
|
||||
area,
|
||||
buf,
|
||||
&mut self.queue_state,
|
||||
),
|
||||
};
|
||||
|
||||
if let Some((title, message)) = &mut self.popup {
|
||||
|
||||
@@ -2,7 +2,7 @@ use color_eyre::eyre::Result;
|
||||
use cpal::traits::HostTrait;
|
||||
use futures::StreamExt;
|
||||
use reqwest::Client;
|
||||
use rodio::{OutputStream, OutputStreamHandle, Sink};
|
||||
use rodio::{OutputStream, Sink, Source};
|
||||
use std::fs::{self, File};
|
||||
use std::io::BufReader;
|
||||
use std::sync::{Arc, Mutex};
|
||||
@@ -18,8 +18,8 @@ use crate::subsonic_helper::make_request_url;
|
||||
pub struct Player {
|
||||
tx: UnboundedSender<Action>,
|
||||
pub songs_list: Arc<Mutex<Vec<Child>>>,
|
||||
pub current_playing: usize,
|
||||
sink: Sink,
|
||||
_handle: OutputStreamHandle,
|
||||
_stream: OutputStream,
|
||||
}
|
||||
|
||||
@@ -32,20 +32,20 @@ impl Player {
|
||||
"make sure --features jack is specified. only works on OSes where jack is available",
|
||||
)).expect("jack host unavailable").default_output_device().expect("Could not find output jack device");
|
||||
|
||||
let (_stream, _handle) = rodio::OutputStream::try_from_device(&device)?;
|
||||
let sink = rodio::Sink::try_new(&_handle)?;
|
||||
sink.pause();
|
||||
let _stream = rodio::OutputStreamBuilder::from_device(device)?.open_stream()?;
|
||||
let sink = rodio::Sink::connect_new(_stream.mixer());
|
||||
|
||||
Ok(Self {
|
||||
tx,
|
||||
_stream,
|
||||
_handle,
|
||||
sink,
|
||||
songs_list: Default::default(),
|
||||
current_playing: 0,
|
||||
})
|
||||
}
|
||||
|
||||
pub fn try_play(&self, id: String, client: Client) -> Result<()> {
|
||||
pub fn try_play(&self, index: usize, client: Client) -> Result<()> {
|
||||
let id = self.songs_list.lock().unwrap()[index].id.clone();
|
||||
let mut path = config::temp_dir();
|
||||
path.push(format!("{id}.mp3"));
|
||||
|
||||
@@ -70,18 +70,28 @@ impl Player {
|
||||
}
|
||||
tokio::time::sleep(Duration::from_millis(100)).await;
|
||||
}
|
||||
tx.send(Action::AddToSink(id))
|
||||
tx.send(Action::AddToSink(index))
|
||||
});
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub fn add_to_sink(&self, id: String) -> Result<()> {
|
||||
pub fn add_to_sink(&mut self, index: usize) -> Result<()> {
|
||||
let id = self.songs_list.lock().unwrap()[index].id.clone();
|
||||
let mut path = config::temp_dir();
|
||||
path.push(format!("{id}.mp3"));
|
||||
let file = std::fs::File::open(&path)?;
|
||||
let decoder = rodio::Decoder::new(BufReader::new(file))?;
|
||||
self.sink.append(decoder);
|
||||
let agc_source = decoder.automatic_gain_control(1.0, 4.0, 0.005, 5.0);
|
||||
if self.sink.empty() {
|
||||
self.current_playing = index;
|
||||
}
|
||||
self.sink.append(agc_source);
|
||||
let tx = self.tx.clone();
|
||||
self.sink
|
||||
.append(rodio::source::EmptyCallback::new(Box::new(move || {
|
||||
let _ = tx.send(Action::TrackEnded);
|
||||
})));
|
||||
self.sink.play();
|
||||
Ok(())
|
||||
}
|
||||
@@ -104,13 +114,16 @@ impl Player {
|
||||
|
||||
pub fn volume_down(&self) {
|
||||
self.sink.set_volume(self.sink.volume() - 0.2);
|
||||
if self.sink.volume() <= 0. {
|
||||
self.sink.set_volume(0.);
|
||||
}
|
||||
}
|
||||
|
||||
pub fn volume_up(&self) {
|
||||
self.sink.set_volume(self.sink.volume() + 0.5);
|
||||
self.sink.set_volume(self.sink.volume() + 0.2);
|
||||
}
|
||||
|
||||
pub fn next(&self) {
|
||||
pub fn skip_next(&self) {
|
||||
self.sink.skip_one();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -5,46 +5,29 @@ use ratatui::{
|
||||
widgets::{StatefulWidget, Table, TableState},
|
||||
};
|
||||
use std::sync::{Arc, Mutex};
|
||||
use std::usize;
|
||||
use style::Styled;
|
||||
|
||||
use subsonic_types::response::Child;
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct Queue {
|
||||
table_state: TableState,
|
||||
songs_list: Arc<Mutex<Vec<Child>>>,
|
||||
current_playing: usize,
|
||||
}
|
||||
|
||||
impl Queue {
|
||||
pub fn new(songs_list: Arc<Mutex<Vec<Child>>>) -> Self {
|
||||
pub fn new(songs_list: Arc<Mutex<Vec<Child>>>, current_playing: usize) -> Self {
|
||||
Self {
|
||||
table_state: Default::default(),
|
||||
current_playing,
|
||||
songs_list,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn scroll_down(&mut self) {
|
||||
self.table_state.select_next();
|
||||
}
|
||||
pub fn scroll_up(&mut self) {
|
||||
self.table_state.select_previous();
|
||||
}
|
||||
|
||||
pub fn get_selected(&self) -> Option<Child> {
|
||||
if self.songs_list.lock().unwrap().is_empty() {
|
||||
return None;
|
||||
}
|
||||
self.table_state
|
||||
.selected()
|
||||
.map(|selected| self.songs_list.lock().unwrap()[selected].clone())
|
||||
}
|
||||
|
||||
pub fn update(&mut self) {
|
||||
self.table_state.select_first();
|
||||
}
|
||||
}
|
||||
|
||||
impl Widget for &mut Queue {
|
||||
fn render(self, area: Rect, buf: &mut Buffer) {
|
||||
impl StatefulWidget for &Queue {
|
||||
type State = TableState;
|
||||
fn render(self, area: Rect, buf: &mut Buffer, state: &mut Self::State) {
|
||||
if !self.songs_list.lock().unwrap().is_empty() {
|
||||
let vertical_pad = |line| Text::from(vec!["".into(), line, "".into()]);
|
||||
let header_cells = ["#", "Title", "Artist", "Duration"]
|
||||
@@ -58,7 +41,13 @@ impl Widget for &mut Queue {
|
||||
let rows = list
|
||||
.iter()
|
||||
.enumerate()
|
||||
.map(|(index, song)| row_from_song(index, song))
|
||||
.map(|(index, song)| {
|
||||
let mut row = row_from_song(index, song);
|
||||
if self.current_playing == index {
|
||||
row = row.set_style(Style::new().fg(style::Color::Red));
|
||||
}
|
||||
row
|
||||
})
|
||||
.collect_vec();
|
||||
|
||||
let table = Table::new(rows, column_widths)
|
||||
@@ -70,7 +59,7 @@ impl Widget for &mut Queue {
|
||||
.cell_highlight_style(Style::new().blue())
|
||||
.highlight_symbol(">>");
|
||||
|
||||
StatefulWidget::render(table, area, buf, &mut self.table_state);
|
||||
StatefulWidget::render(table, area, buf, state);
|
||||
} else {
|
||||
Text::render(Text::from("Empty queue").centered(), area, buf);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user