Compare commits
No commits in common. "main" and "0.0.1" have entirely different histories.
68
src/audio.rs
68
src/audio.rs
|
@ -4,12 +4,12 @@ use cpal::{Device, Stream, StreamConfig, SampleFormat, SampleRate};
|
||||||
use std::sync::{Arc, Mutex};
|
use std::sync::{Arc, Mutex};
|
||||||
use std::time::{Duration, Instant};
|
use std::time::{Duration, Instant};
|
||||||
use symphonia::core::audio::Signal;
|
use symphonia::core::audio::Signal;
|
||||||
use symphonia::core::codecs::{DecoderOptions};
|
use symphonia::core::codecs::{Decoder, DecoderOptions};
|
||||||
use symphonia::core::formats::{FormatOptions};
|
use symphonia::core::formats::{FormatOptions, FormatReader};
|
||||||
use symphonia::core::io::MediaSourceStream;
|
use symphonia::core::io::MediaSourceStream;
|
||||||
use symphonia::core::meta::MetadataOptions;
|
use symphonia::core::meta::MetadataOptions;
|
||||||
use symphonia::core::probe::Hint;
|
use symphonia::core::probe::Hint;
|
||||||
use tracing::{error, warn};
|
use tracing::{info, debug, error, warn};
|
||||||
|
|
||||||
pub struct AsioPlayer {
|
pub struct AsioPlayer {
|
||||||
device: Device,
|
device: Device,
|
||||||
|
@ -22,6 +22,7 @@ pub struct AsioPlayer {
|
||||||
volume: f32,
|
volume: f32,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Shared audio buffer for streaming
|
||||||
struct StreamBuffer {
|
struct StreamBuffer {
|
||||||
samples: Vec<f32>,
|
samples: Vec<f32>,
|
||||||
position: usize,
|
position: usize,
|
||||||
|
@ -34,11 +35,11 @@ struct StreamBuffer {
|
||||||
impl AsioPlayer {
|
impl AsioPlayer {
|
||||||
pub fn new() -> Result<Self> {
|
pub fn new() -> Result<Self> {
|
||||||
let host = cpal::host_from_id(cpal::HostId::Asio)?;
|
let host = cpal::host_from_id(cpal::HostId::Asio)?;
|
||||||
let mut devices = host.output_devices()?;
|
let device = host.default_output_device()
|
||||||
let device = devices.next()
|
|
||||||
.ok_or_else(|| anyhow::anyhow!("No ASIO output device found"))?;
|
.ok_or_else(|| anyhow::anyhow!("No ASIO output device found"))?;
|
||||||
//info!("Using ASIO device: {}", device.name()?);
|
|
||||||
|
info!("Using ASIO device: {}", device.name()?);
|
||||||
|
|
||||||
Ok(Self {
|
Ok(Self {
|
||||||
device,
|
device,
|
||||||
stream: None,
|
stream: None,
|
||||||
|
@ -74,16 +75,17 @@ impl AsioPlayer {
|
||||||
}
|
}
|
||||||
|
|
||||||
self.forced_sample_rate = Some(sample_rate);
|
self.forced_sample_rate = Some(sample_rate);
|
||||||
|
info!("Forced sample rate set to {} Hz", sample_rate);
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn clear_sample_rate(&mut self) {
|
pub fn clear_sample_rate(&mut self) {
|
||||||
self.forced_sample_rate = None;
|
self.forced_sample_rate = None;
|
||||||
//info!("Sample rate forcing cleared - will use automatic selection");
|
info!("Sample rate forcing cleared - will use automatic selection");
|
||||||
}
|
}
|
||||||
|
|
||||||
pub async fn play_url(&mut self, url: &str) -> Result<()> {
|
pub async fn play_url(&mut self, url: &str) -> Result<()> {
|
||||||
//info!("Starting playback of: {}", url);
|
info!("Starting playback of: {}", url);
|
||||||
|
|
||||||
// Stop any existing playback
|
// Stop any existing playback
|
||||||
self.stop()?;
|
self.stop()?;
|
||||||
|
@ -91,8 +93,10 @@ impl AsioPlayer {
|
||||||
// Download and decode the audio
|
// Download and decode the audio
|
||||||
let (samples, original_sample_rate, channels, duration_ms) = self.download_and_decode(url).await?;
|
let (samples, original_sample_rate, channels, duration_ms) = self.download_and_decode(url).await?;
|
||||||
|
|
||||||
|
// Set up ASIO stream with proper sample rate
|
||||||
self.setup_stream(samples, original_sample_rate, channels)?;
|
self.setup_stream(samples, original_sample_rate, channels)?;
|
||||||
|
|
||||||
|
// Store duration
|
||||||
self.duration_ms = Some(duration_ms);
|
self.duration_ms = Some(duration_ms);
|
||||||
|
|
||||||
// Start playing
|
// Start playing
|
||||||
|
@ -100,7 +104,7 @@ impl AsioPlayer {
|
||||||
stream.play()?;
|
stream.play()?;
|
||||||
self.start_time = Some(Instant::now());
|
self.start_time = Some(Instant::now());
|
||||||
self.paused_position = None;
|
self.paused_position = None;
|
||||||
//info!("Playback started");
|
info!("Playback started");
|
||||||
}
|
}
|
||||||
|
|
||||||
Ok(())
|
Ok(())
|
||||||
|
@ -123,7 +127,7 @@ impl AsioPlayer {
|
||||||
buffer.paused = true;
|
buffer.paused = true;
|
||||||
}
|
}
|
||||||
|
|
||||||
//info!("Playback paused");
|
info!("Playback paused");
|
||||||
Ok(())
|
Ok(())
|
||||||
} else {
|
} else {
|
||||||
Err(anyhow::anyhow!("No active stream to pause"))
|
Err(anyhow::anyhow!("No active stream to pause"))
|
||||||
|
@ -146,7 +150,7 @@ impl AsioPlayer {
|
||||||
}
|
}
|
||||||
|
|
||||||
stream.play()?;
|
stream.play()?;
|
||||||
//info!("Playback resumed");
|
info!("Playback resumed");
|
||||||
Ok(())
|
Ok(())
|
||||||
} else {
|
} else {
|
||||||
Err(anyhow::anyhow!("No active stream to resume"))
|
Err(anyhow::anyhow!("No active stream to resume"))
|
||||||
|
@ -155,24 +159,12 @@ impl AsioPlayer {
|
||||||
|
|
||||||
pub fn stop(&mut self) -> Result<()> {
|
pub fn stop(&mut self) -> Result<()> {
|
||||||
if let Some(stream) = &self.stream {
|
if let Some(stream) = &self.stream {
|
||||||
// First, clear the buffer to prevent noise
|
|
||||||
if let Some(buffer) = &self.stream_buffer {
|
|
||||||
let mut buffer = buffer.lock().unwrap();
|
|
||||||
buffer.paused = true; // Stop audio immediately
|
|
||||||
buffer.position = 0; // Reset position
|
|
||||||
buffer.samples.clear(); // Clear the buffer completely
|
|
||||||
}
|
|
||||||
|
|
||||||
std::thread::sleep(std::time::Duration::from_millis(50));
|
|
||||||
|
|
||||||
stream.pause()?;
|
stream.pause()?;
|
||||||
|
|
||||||
self.stream = None;
|
self.stream = None;
|
||||||
self.stream_buffer = None;
|
self.stream_buffer = None;
|
||||||
self.start_time = None;
|
self.start_time = None;
|
||||||
self.paused_position = None;
|
self.paused_position = None;
|
||||||
|
info!("Playback stopped");
|
||||||
//info!("Playback stopped and buffer cleared");
|
|
||||||
Ok(())
|
Ok(())
|
||||||
} else {
|
} else {
|
||||||
// Not an error if already stopped
|
// Not an error if already stopped
|
||||||
|
@ -197,13 +189,13 @@ impl AsioPlayer {
|
||||||
self.start_time = Some(Instant::now() - Duration::from_secs_f32(elapsed_secs));
|
self.start_time = Some(Instant::now() - Duration::from_secs_f32(elapsed_secs));
|
||||||
}
|
}
|
||||||
|
|
||||||
if let Some(_stream) = &self.stream {
|
if let Some(stream) = &self.stream {
|
||||||
if buffer.paused {
|
if buffer.paused {
|
||||||
self.paused_position = Some(position);
|
self.paused_position = Some(position);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
//info!("Seeked to position: {:.1}%", position * 100.0);
|
info!("Seeked to position: {:.1}%", position * 100.0);
|
||||||
Ok(())
|
Ok(())
|
||||||
} else {
|
} else {
|
||||||
Err(anyhow::anyhow!("No active stream to seek"))
|
Err(anyhow::anyhow!("No active stream to seek"))
|
||||||
|
@ -217,7 +209,7 @@ impl AsioPlayer {
|
||||||
if let Some(buffer) = &self.stream_buffer {
|
if let Some(buffer) = &self.stream_buffer {
|
||||||
let mut buffer = buffer.lock().unwrap();
|
let mut buffer = buffer.lock().unwrap();
|
||||||
buffer.volume = volume;
|
buffer.volume = volume;
|
||||||
//info!("Volume set to: {:.0}%", volume * 100.0);
|
info!("Volume set to: {:.0}%", volume * 100.0);
|
||||||
}
|
}
|
||||||
|
|
||||||
Ok(())
|
Ok(())
|
||||||
|
@ -238,12 +230,12 @@ impl AsioPlayer {
|
||||||
}
|
}
|
||||||
|
|
||||||
async fn download_and_decode(&self, url: &str) -> Result<(Vec<f32>, u32, u16, u64)> {
|
async fn download_and_decode(&self, url: &str) -> Result<(Vec<f32>, u32, u16, u64)> {
|
||||||
//debug!("Downloading audio from: {}", url);
|
debug!("Downloading audio from: {}", url);
|
||||||
|
|
||||||
let response = reqwest::get(url).await?;
|
let response = reqwest::get(url).await?;
|
||||||
let bytes = response.bytes().await?;
|
let bytes = response.bytes().await?;
|
||||||
|
|
||||||
//debug!("Downloaded {} bytes, decoding...", bytes.len());
|
debug!("Downloaded {} bytes, decoding...", bytes.len());
|
||||||
|
|
||||||
// Create media source
|
// Create media source
|
||||||
let cursor = std::io::Cursor::new(bytes);
|
let cursor = std::io::Cursor::new(bytes);
|
||||||
|
@ -273,7 +265,7 @@ impl AsioPlayer {
|
||||||
let mut decoder = symphonia::default::get_codecs()
|
let mut decoder = symphonia::default::get_codecs()
|
||||||
.make(&track.codec_params, &decoder_opts)?;
|
.make(&track.codec_params, &decoder_opts)?;
|
||||||
|
|
||||||
//info!("Decoding: {} channels, {} Hz", channels, sample_rate);
|
info!("Decoding: {} channels, {} Hz", channels, sample_rate);
|
||||||
|
|
||||||
let mut samples = Vec::new();
|
let mut samples = Vec::new();
|
||||||
|
|
||||||
|
@ -345,12 +337,12 @@ impl AsioPlayer {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
_ => {
|
_ => {
|
||||||
//debug!("Unsupported audio format");
|
debug!("Unsupported audio format");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
Err(_e) => {
|
Err(e) => {
|
||||||
//debug!("Decode error: {}", e);
|
debug!("Decode error: {}", e);
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -363,7 +355,7 @@ impl AsioPlayer {
|
||||||
let num_frames = samples.len() / channels as usize;
|
let num_frames = samples.len() / channels as usize;
|
||||||
let duration_ms = (num_frames as f64 / sample_rate as f64 * 1000.0) as u64;
|
let duration_ms = (num_frames as f64 / sample_rate as f64 * 1000.0) as u64;
|
||||||
|
|
||||||
//info!("Decoded {} samples, duration: {}ms", samples.len(), duration_ms);
|
info!("Decoded {} samples, duration: {}ms", samples.len(), duration_ms);
|
||||||
Ok((samples, sample_rate, channels, duration_ms))
|
Ok((samples, sample_rate, channels, duration_ms))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -383,7 +375,7 @@ impl AsioPlayer {
|
||||||
config.channels() == source_channels {
|
config.channels() == source_channels {
|
||||||
selected_config = Some(config.with_sample_rate(SampleRate(forced_rate)));
|
selected_config = Some(config.with_sample_rate(SampleRate(forced_rate)));
|
||||||
target_sample_rate = forced_rate;
|
target_sample_rate = forced_rate;
|
||||||
//info!("Using forced sample rate: {} Hz", forced_rate);
|
info!("Using forced sample rate: {} Hz", forced_rate);
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -443,8 +435,8 @@ impl AsioPlayer {
|
||||||
self.device.default_output_config()?
|
self.device.default_output_config()?
|
||||||
};
|
};
|
||||||
|
|
||||||
//info!("Selected config: {} channels, {} Hz, format: {:?}",
|
info!("Selected config: {} channels, {} Hz, format: {:?}",
|
||||||
//config.channels(), config.sample_rate().0, config.sample_format());
|
config.channels(), config.sample_rate().0, config.sample_format());
|
||||||
|
|
||||||
// Resample if necessary
|
// Resample if necessary
|
||||||
let final_samples = if target_sample_rate != source_sample_rate {
|
let final_samples = if target_sample_rate != source_sample_rate {
|
||||||
|
|
|
@ -130,21 +130,23 @@ impl JellyfinClient {
|
||||||
|
|
||||||
pub fn get_stream_url(&self, item_id: &str) -> Result<String> {
|
pub fn get_stream_url(&self, item_id: &str) -> Result<String> {
|
||||||
let user_id = self.config.user_id.as_ref().ok_or_else(|| anyhow::anyhow!("Not authenticated"))?;
|
let user_id = self.config.user_id.as_ref().ok_or_else(|| anyhow::anyhow!("Not authenticated"))?;
|
||||||
let token = self.config.access_token.as_ref().ok_or_else(|| anyhow::anyhow!("Not authenticated"))?;
|
let token = self.config.access_token.as_ref().ok_or_else(|| anyhow::anyhow!("Not authenticated"))?;
|
||||||
|
|
||||||
let url = format!(
|
|
||||||
"{}/Audio/{}/stream?UserId={}&api_key={}&Static=true",
|
|
||||||
self.config.server_url, item_id, user_id, token
|
|
||||||
);
|
|
||||||
|
|
||||||
Ok(url)
|
let url = format!(
|
||||||
}
|
"{}/Audio/{}/stream?UserId={}&api_key={}&Static=true",
|
||||||
|
self.config.server_url, item_id, user_id, token
|
||||||
|
);
|
||||||
|
|
||||||
|
Ok(url)
|
||||||
|
}
|
||||||
|
|
||||||
pub fn is_authenticated(&self) -> bool {
|
pub fn is_authenticated(&self) -> bool {
|
||||||
self.config.is_authenticated()
|
self.config.is_authenticated()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub fn logout(&mut self) -> Result<()> {
|
||||||
|
self.config.clear_auth()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl LibraryItem {
|
impl LibraryItem {
|
||||||
|
|
104
src/main.rs
104
src/main.rs
|
@ -1,5 +1,6 @@
|
||||||
use anyhow::Result;
|
use anyhow::Result;
|
||||||
use tracing::info;
|
use tracing::info;
|
||||||
|
use cpal::traits::{HostTrait, DeviceTrait};
|
||||||
use std::io::Write;
|
use std::io::Write;
|
||||||
|
|
||||||
mod jellyfin;
|
mod jellyfin;
|
||||||
|
@ -14,14 +15,17 @@ async fn main() -> Result<()> {
|
||||||
|
|
||||||
info!("Starting Jellyfin ASIO Client");
|
info!("Starting Jellyfin ASIO Client");
|
||||||
|
|
||||||
|
// Load config
|
||||||
let mut config = config::Config::load()?;
|
let mut config = config::Config::load()?;
|
||||||
|
|
||||||
// Create Jellyfin client
|
// Create Jellyfin client
|
||||||
let mut jellyfin = jellyfin::JellyfinClient::new(config.clone());
|
let mut jellyfin = jellyfin::JellyfinClient::new(config.clone());
|
||||||
|
|
||||||
|
// Check if we need to authenticate
|
||||||
if !jellyfin.is_authenticated() {
|
if !jellyfin.is_authenticated() {
|
||||||
println!("No saved session found. Please log in.");
|
println!("No saved session found. Please log in.");
|
||||||
|
|
||||||
|
// Get server URL if not set
|
||||||
if config.server_url.is_empty() {
|
if config.server_url.is_empty() {
|
||||||
print!("Enter Jellyfin server URL (e.g., https://jellyfin.example.com): ");
|
print!("Enter Jellyfin server URL (e.g., https://jellyfin.example.com): ");
|
||||||
std::io::stdout().flush()?;
|
std::io::stdout().flush()?;
|
||||||
|
@ -47,10 +51,110 @@ async fn main() -> Result<()> {
|
||||||
info!("Using saved session for {}", config.username.unwrap_or_else(|| "Unknown user".to_string()));
|
info!("Using saved session for {}", config.username.unwrap_or_else(|| "Unknown user".to_string()));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// List ASIO devices
|
||||||
|
list_asio_devices()?;
|
||||||
|
|
||||||
|
// Create player
|
||||||
let player = player::Player::new(jellyfin.clone())?;
|
let player = player::Player::new(jellyfin.clone())?;
|
||||||
|
|
||||||
|
// Create and run TUI
|
||||||
let mut tui = tui::Tui::new(jellyfin, player)?;
|
let mut tui = tui::Tui::new(jellyfin, player)?;
|
||||||
tui.run().await?;
|
tui.run().await?;
|
||||||
|
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async fn test_jellyfin_connection() -> Result<()> {
|
||||||
|
info!("Testing Jellyfin connection...");
|
||||||
|
|
||||||
|
let config = config::Config {
|
||||||
|
server_url: "https://jfin.mercurio.moe".to_string(),
|
||||||
|
..config::Config::default()
|
||||||
|
};
|
||||||
|
let mut client = jellyfin::JellyfinClient::new(config);
|
||||||
|
|
||||||
|
println!("Enter your Jellyfin username:");
|
||||||
|
let mut username = String::new();
|
||||||
|
std::io::stdin().read_line(&mut username)?;
|
||||||
|
username = username.trim().to_string();
|
||||||
|
|
||||||
|
println!("Enter your Jellyfin password:");
|
||||||
|
let password = rpassword::read_password()?;
|
||||||
|
|
||||||
|
client.authenticate(&username, &password).await?;
|
||||||
|
|
||||||
|
let items = client.get_music_library().await?;
|
||||||
|
|
||||||
|
info!("Your music library (first few items):");
|
||||||
|
for item in items.iter().take(10) {
|
||||||
|
let artist = item.artists.as_ref()
|
||||||
|
.and_then(|a| a.first())
|
||||||
|
.map(|s| s.as_str())
|
||||||
|
.unwrap_or("Unknown Artist");
|
||||||
|
let album = item.album.as_ref()
|
||||||
|
.map(|s| s.as_str())
|
||||||
|
.unwrap_or("Unknown Album");
|
||||||
|
|
||||||
|
info!(" {} - {} - {}", artist, album, item.name);
|
||||||
|
|
||||||
|
if items.iter().position(|i| i.id == item.id) == Some(0) {
|
||||||
|
let stream_url = client.get_stream_url(&item.id)?;
|
||||||
|
info!(" Stream URL: {}", stream_url);
|
||||||
|
|
||||||
|
println!("\nDo you want to play this track? (y/n):");
|
||||||
|
let mut input = String::new();
|
||||||
|
std::io::stdin().read_line(&mut input)?;
|
||||||
|
|
||||||
|
if input.trim().to_lowercase() == "y" {
|
||||||
|
let mut player = audio::AsioPlayer::new()?;
|
||||||
|
|
||||||
|
println!("Do you want to set a specific sample rate? (y/n):");
|
||||||
|
let mut rate_input = String::new();
|
||||||
|
std::io::stdin().read_line(&mut rate_input)?;
|
||||||
|
|
||||||
|
if rate_input.trim().to_lowercase() == "y" {
|
||||||
|
println!("Enter sample rate (e.g., 44100, 48000, 96000, 192000):");
|
||||||
|
let mut rate_str = String::new();
|
||||||
|
std::io::stdin().read_line(&mut rate_str)?;
|
||||||
|
|
||||||
|
if let Ok(sample_rate) = rate_str.trim().parse::<u32>() {
|
||||||
|
match player.set_sample_rate(sample_rate) {
|
||||||
|
Ok(_) => println!("Sample rate set to {} Hz", sample_rate),
|
||||||
|
Err(e) => println!("Failed to set sample rate: {}", e),
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
println!("Invalid sample rate, using automatic selection");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
player.play_url(&stream_url).await?;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
fn list_asio_devices() -> Result<()> {
|
||||||
|
info!("Enumerating ASIO devices...");
|
||||||
|
|
||||||
|
let host = cpal::host_from_id(cpal::HostId::Asio)?;
|
||||||
|
|
||||||
|
let devices = host.output_devices()?;
|
||||||
|
|
||||||
|
for (i, device) in devices.enumerate() {
|
||||||
|
let name = device.name()?;
|
||||||
|
info!("ASIO Device {}: {}", i, name);
|
||||||
|
|
||||||
|
let supported_configs = device.supported_output_configs()?;
|
||||||
|
for config in supported_configs {
|
||||||
|
info!(" Channels: {}, Sample Rate: {}-{} Hz, Format: {:?}",
|
||||||
|
config.channels(),
|
||||||
|
config.min_sample_rate().0,
|
||||||
|
config.max_sample_rate().0,
|
||||||
|
config.sample_format());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
35
src/tui.rs
35
src/tui.rs
|
@ -2,7 +2,7 @@ use anyhow::Result;
|
||||||
use crossterm::event::{self, Event, KeyCode, KeyEvent, KeyModifiers};
|
use crossterm::event::{self, Event, KeyCode, KeyEvent, KeyModifiers};
|
||||||
use crossterm::terminal::{disable_raw_mode, enable_raw_mode, EnterAlternateScreen, LeaveAlternateScreen};
|
use crossterm::terminal::{disable_raw_mode, enable_raw_mode, EnterAlternateScreen, LeaveAlternateScreen};
|
||||||
use crossterm::ExecutableCommand;
|
use crossterm::ExecutableCommand;
|
||||||
use ratatui::backend::{CrosstermBackend};
|
use ratatui::backend::{Backend, CrosstermBackend};
|
||||||
use ratatui::layout::{Constraint, Direction, Layout, Rect};
|
use ratatui::layout::{Constraint, Direction, Layout, Rect};
|
||||||
use ratatui::style::{Color, Modifier, Style};
|
use ratatui::style::{Color, Modifier, Style};
|
||||||
use ratatui::text::{Line, Span, Text};
|
use ratatui::text::{Line, Span, Text};
|
||||||
|
@ -12,7 +12,6 @@ use std::io::{self, Stdout};
|
||||||
use std::sync::mpsc::{channel, Receiver, Sender};
|
use std::sync::mpsc::{channel, Receiver, Sender};
|
||||||
use std::time::{Duration, Instant};
|
use std::time::{Duration, Instant};
|
||||||
use std::thread;
|
use std::thread;
|
||||||
use::tracing::{debug};
|
|
||||||
|
|
||||||
use crate::jellyfin::{JellyfinClient, LibraryItem};
|
use crate::jellyfin::{JellyfinClient, LibraryItem};
|
||||||
use crate::player::{Player, PlayerCommand, PlaybackState};
|
use crate::player::{Player, PlayerCommand, PlaybackState};
|
||||||
|
@ -313,7 +312,7 @@ impl Tui {
|
||||||
// Player controls (only if not showing help and not in search mode)
|
// Player controls (only if not showing help and not in search mode)
|
||||||
if !self.show_help && self.active_tab != TAB_SEARCH {
|
if !self.show_help && self.active_tab != TAB_SEARCH {
|
||||||
match key.code {
|
match key.code {
|
||||||
KeyCode::Char('o') => {
|
KeyCode::Char(' ') => {
|
||||||
self.player.send_command(PlayerCommand::PlayPause).await?;
|
self.player.send_command(PlayerCommand::PlayPause).await?;
|
||||||
return Ok(false);
|
return Ok(false);
|
||||||
},
|
},
|
||||||
|
@ -449,9 +448,9 @@ impl Tui {
|
||||||
if let Some(selected) = self.songs_state.selected() {
|
if let Some(selected) = self.songs_state.selected() {
|
||||||
if selected < self.songs.len() {
|
if selected < self.songs.len() {
|
||||||
let song = &self.songs[selected];
|
let song = &self.songs[selected];
|
||||||
debug!("Song ID: {}", song.id);
|
let url = self.jellyfin.get_stream_url(&song.id)?;
|
||||||
self.player.send_command(PlayerCommand::Play(song.id.clone())).await?;
|
self.player.send_command(PlayerCommand::Play(url)).await?;
|
||||||
|
|
||||||
if !self.queue.iter().any(|item| item.id == song.id) {
|
if !self.queue.iter().any(|item| item.id == song.id) {
|
||||||
self.queue.push(song.clone());
|
self.queue.push(song.clone());
|
||||||
}
|
}
|
||||||
|
@ -493,8 +492,8 @@ impl Tui {
|
||||||
if let Some(selected) = self.queue_state.selected() {
|
if let Some(selected) = self.queue_state.selected() {
|
||||||
if selected < self.queue.len() {
|
if selected < self.queue.len() {
|
||||||
let song = &self.queue[selected];
|
let song = &self.queue[selected];
|
||||||
self.player.send_command(PlayerCommand::Play(song.id.clone())).await?;
|
let url = self.jellyfin.get_stream_url(&song.id)?;
|
||||||
|
self.player.send_command(PlayerCommand::Play(url)).await?;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -544,14 +543,16 @@ impl Tui {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
KeyCode::Enter => {
|
KeyCode::Enter => {
|
||||||
if let Some(selected) = self.songs_state.selected() {
|
if let Some(selected) = self.search_state.selected() {
|
||||||
if selected < self.songs.len() {
|
if selected < self.search_results.len() {
|
||||||
let song = &self.songs[selected];
|
let song = &self.search_results[selected];
|
||||||
debug!("Song ID: {}", song.id);
|
if song.item_type == "Audio" {
|
||||||
self.player.send_command(PlayerCommand::Play(song.id.clone())).await?;
|
let url = self.jellyfin.get_stream_url(&song.id)?;
|
||||||
|
self.player.send_command(PlayerCommand::Play(url)).await?;
|
||||||
if !self.queue.iter().any(|item| item.id == song.id) {
|
|
||||||
self.queue.push(song.clone());
|
if !self.queue.iter().any(|item| item.id == song.id) {
|
||||||
|
self.queue.push(song.clone());
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -859,7 +860,7 @@ impl Tui {
|
||||||
f.render_widget(playing_text, chunks[0]);
|
f.render_widget(playing_text, chunks[0]);
|
||||||
|
|
||||||
// Controls help
|
// Controls help
|
||||||
let controls = "o: Play/Pause | s: Stop | n/p: Next/Prev";
|
let controls = "Space: Play/Pause | s: Stop | n/p: Next/Prev";
|
||||||
let controls_text = Paragraph::new(controls)
|
let controls_text = Paragraph::new(controls)
|
||||||
.block(Block::default().borders(Borders::ALL).title("Controls"));
|
.block(Block::default().borders(Borders::ALL).title("Controls"));
|
||||||
f.render_widget(controls_text, chunks[1]);
|
f.render_widget(controls_text, chunks[1]);
|
||||||
|
|
Loading…
Reference in a new issue