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