Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion spotify_player/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,7 @@ rspotify = "0.12.0"
serde = { version = "1.0.188", features = ["derive"] }
tokio = { version = "1.32.0", features = ["rt", "rt-multi-thread", "macros", "time"] }
toml = "0.8.1"
ratatui = "0.23.0"
tui = { package = "ratatui", version = "0.23.0" }
rand = "0.8.5"
maybe-async = "0.2.7"
async-trait = "0.1.73"
Expand Down
12 changes: 6 additions & 6 deletions spotify_player/src/auth.rs
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ use librespot_core::{
session::{Session, SessionError},
};

use crate::state::SharedState;
use crate::state;

#[derive(Clone)]
pub struct AuthConfig {
Expand All @@ -26,23 +26,23 @@ impl Default for AuthConfig {
}

impl AuthConfig {
pub fn new(state: &SharedState) -> Result<AuthConfig> {
let audio_cache_folder = if state.app_config.device.audio_cache {
Some(state.cache_folder.join("audio"))
pub fn new(configs: &state::Configs) -> Result<AuthConfig> {
let audio_cache_folder = if configs.app_config.device.audio_cache {
Some(configs.cache_folder.join("audio"))
} else {
None
};

let cache = Cache::new(
Some(state.cache_folder.clone()),
Some(configs.cache_folder.clone()),
None,
audio_cache_folder,
None,
)?;

Ok(AuthConfig {
cache,
session_config: state.app_config.session_config(),
session_config: configs.app_config.session_config(),
})
}
}
Expand Down
42 changes: 28 additions & 14 deletions spotify_player/src/cli/client.rs
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ use std::{
use anyhow::{Context as _, Result};
use rand::seq::SliceRandom;
use tokio::net::UdpSocket;
use tracing::Instrument;

use crate::{
cli::Request,
Expand All @@ -24,17 +25,23 @@ use rspotify::{
use super::*;

pub async fn start_socket(client: Client, state: SharedState) -> Result<()> {
let port = state.app_config.client_port;
let port = state.configs.app_config.client_port;
tracing::info!("Starting a client socket at 127.0.0.1:{port}");

let socket = UdpSocket::bind(("127.0.0.1", port)).await?;

// initialize the receive buffer to be 4096 bytes
let mut buf = [0; 4096];
let mut buf = [0; MAX_REQUEST_SIZE];
loop {
match socket.recv_from(&mut buf).await {
Err(err) => tracing::warn!("Failed to receive from the socket: {err:#}"),
Ok((n_bytes, dest_addr)) => {
if n_bytes == 0 {
// received a connection request from the destination address
socket.send_to(&[], dest_addr).await.unwrap_or_default();
continue;
}

let req_buf = &buf[0..n_bytes];
let request: Request = match serde_json::from_slice(req_buf) {
Ok(v) => v,
Expand All @@ -44,18 +51,25 @@ pub async fn start_socket(client: Client, state: SharedState) -> Result<()> {
}
};

tracing::info!("Handling socket request: {request:?}...");
let response = match handle_socket_request(&client, &state, request).await {
Err(err) => {
tracing::error!("Failed to handle socket request: {err:#}");
let msg = format!("Bad request: {err:#}");
Response::Err(msg.into_bytes())
}
Ok(data) => Response::Ok(data),
};
send_response(response, &socket, dest_addr)
.await
.unwrap_or_default();
let span = tracing::info_span!("socket_request", request = ?request, dest_addr = ?dest_addr);

async {
let response = match handle_socket_request(&client, &state, request).await {
Err(err) => {
tracing::error!("Failed to handle socket request: {err:#}");
let msg = format!("Bad request: {err:#}");
Response::Err(msg.into_bytes())
}
Ok(data) => Response::Ok(data),
};
send_response(response, &socket, dest_addr)
.await
.unwrap_or_default();

tracing::info!("Successfully handled the socket request.",);
}
.instrument(span)
.await;
}
}
}
Expand Down
99 changes: 45 additions & 54 deletions spotify_player/src/cli/handlers.rs
Original file line number Diff line number Diff line change
Expand Up @@ -25,27 +25,27 @@ fn receive_response(socket: &UdpSocket) -> Result<Response> {
Ok(serde_json::from_slice(&data)?)
}

fn get_id_or_name(args: &ArgMatches) -> Result<IdOrName> {
fn get_id_or_name(args: &ArgMatches) -> IdOrName {
match args
.get_one::<Id>("id_or_name")
.expect("id_or_name group is required")
.as_str()
{
"name" => Ok(IdOrName::Name(
"name" => IdOrName::Name(
args.get_one::<String>("name")
.expect("name should be specified")
.to_owned(),
)),
"id" => Ok(IdOrName::Id(
),
"id" => IdOrName::Id(
args.get_one::<String>("id")
.expect("id should be specified")
.to_owned(),
)),
id => anyhow::bail!("unknown id: {id}"),
),
id => panic!("unknown id: {id}"),
}
}

fn handle_get_subcommand(args: &ArgMatches, socket: &UdpSocket) -> Result<()> {
fn handle_get_subcommand(args: &ArgMatches) -> Result<Request> {
let (cmd, args) = args.subcommand().expect("playback subcommand is required");

let request = match cmd {
Expand All @@ -61,17 +61,16 @@ fn handle_get_subcommand(args: &ArgMatches, socket: &UdpSocket) -> Result<()> {
.get_one::<ItemType>("item_type")
.expect("context_type is required")
.to_owned();
let id_or_name = get_id_or_name(args)?;
let id_or_name = get_id_or_name(args);
Request::Get(GetRequest::Item(item_type, id_or_name))
}
_ => unreachable!(),
};

socket.send(&serde_json::to_vec(&request)?)?;
Ok(())
Ok(request)
}

fn handle_playback_subcommand(args: &ArgMatches, socket: &UdpSocket) -> Result<()> {
fn handle_playback_subcommand(args: &ArgMatches) -> Result<Request> {
let (cmd, args) = args.subcommand().expect("playback subcommand is required");
let command = match cmd {
"start" => match args.subcommand() {
Expand All @@ -82,7 +81,7 @@ fn handle_playback_subcommand(args: &ArgMatches, socket: &UdpSocket) -> Result<(
.to_owned();
let shuffle = args.get_flag("shuffle");

let id_or_name = get_id_or_name(args)?;
let id_or_name = get_id_or_name(args);
Command::StartContext {
context_type,
id_or_name,
Expand All @@ -101,7 +100,7 @@ fn handle_playback_subcommand(args: &ArgMatches, socket: &UdpSocket) -> Result<(
.get_one::<ItemType>("item_type")
.expect("item_type is required")
.to_owned();
let id_or_name = get_id_or_name(args)?;
let id_or_name = get_id_or_name(args);
Command::StartRadio(item_type, id_or_name)
}
_ => {
Expand Down Expand Up @@ -132,53 +131,48 @@ fn handle_playback_subcommand(args: &ArgMatches, socket: &UdpSocket) -> Result<(
_ => unreachable!(),
};

let request = Request::Playback(command);
socket.send(&serde_json::to_vec(&request)?)?;

Ok(())
}

fn handle_connect_subcommand(args: &ArgMatches, socket: &UdpSocket) -> Result<()> {
let id_or_name = get_id_or_name(args)?;

let request = Request::Connect(id_or_name);
socket.send(&serde_json::to_vec(&request)?)?;

Ok(())
}

fn handle_like_subcommand(args: &ArgMatches, socket: &UdpSocket) -> Result<()> {
let unlike = args.get_flag("unlike");

let request = Request::Like { unlike };
socket.send(&serde_json::to_vec(&request)?)?;

Ok(())
Ok(Request::Playback(command))
}

pub fn handle_cli_subcommand(
cmd: &str,
args: &ArgMatches,
state: &state::SharedState,
) -> Result<()> {
pub fn handle_cli_subcommand(cmd: &str, args: &ArgMatches, configs: &state::Configs) -> Result<()> {
let socket = UdpSocket::bind("127.0.0.1:0")?;
socket.connect(("127.0.0.1", state.app_config.client_port))?;
socket.connect(("127.0.0.1", configs.app_config.client_port))?;

// send an empty buffer as a connection request to the client
socket.send(&[])?;
if let Err(err) = socket.recv(&mut [0; 1]) {
if let std::io::ErrorKind::ConnectionRefused = err.kind() {
eprintln!("Error: {err}\nPlease make sure that there is a running \
`spotify_player` instance with a client socket running on port {}.", configs.app_config.client_port);
std::process::exit(1)
}
return Err(err.into());
}

match cmd {
"get" => handle_get_subcommand(args, &socket)?,
"playback" => handle_playback_subcommand(args, &socket)?,
"playlist" => handle_playlist_subcommand(args, &socket)?,
"connect" => handle_connect_subcommand(args, &socket)?,
"like" => handle_like_subcommand(args, &socket)?,
// construct a socket request based on the CLI command and its arguments
let request = match cmd {
"get" => handle_get_subcommand(args)?,
"playback" => handle_playback_subcommand(args)?,
"playlist" => handle_playlist_subcommand(args)?,
"connect" => Request::Connect(get_id_or_name(args)),
"like" => Request::Like {
unlike: args.get_flag("unlike"),
},
"authenticate" => {
let auth_config = AuthConfig::new(state)?;
let auth_config = AuthConfig::new(configs)?;
let rt = tokio::runtime::Runtime::new()?;
rt.block_on(new_session_with_new_creds(&auth_config))?;
std::process::exit(0);
}
_ => unreachable!(),
}
};

// send the request to the client's socket
let request_buf = serde_json::to_vec(&request)?;
assert!(request_buf.len() <= MAX_REQUEST_SIZE);
socket.send(&request_buf)?;

// receive and handle a response from the client's socket
match receive_response(&socket)? {
Response::Err(err) => {
eprintln!("{}", String::from_utf8_lossy(&err));
Expand All @@ -191,7 +185,7 @@ pub fn handle_cli_subcommand(
}
}

fn handle_playlist_subcommand(args: &ArgMatches, socket: &UdpSocket) -> Result<()> {
fn handle_playlist_subcommand(args: &ArgMatches) -> Result<Request> {
let (cmd, args) = args.subcommand().expect("playlist subcommand is required");
let command = match cmd {
"new" => {
Expand Down Expand Up @@ -276,8 +270,5 @@ fn handle_playlist_subcommand(args: &ArgMatches, socket: &UdpSocket) -> Result<(
_ => unreachable!(),
};

let request = Request::Playlist(command);
socket.send(&serde_json::to_vec(&request)?)?;

Ok(())
Ok(Request::Playlist(command))
}
2 changes: 2 additions & 0 deletions spotify_player/src/cli/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,8 @@ mod handlers;
use rspotify::model::*;
use serde::{Deserialize, Serialize};

const MAX_REQUEST_SIZE: usize = 4096;

pub use client::start_socket;
pub use commands::*;
pub use handlers::handle_cli_subcommand;
Expand Down
7 changes: 4 additions & 3 deletions spotify_player/src/client/handlers.rs
Original file line number Diff line number Diff line change
Expand Up @@ -98,11 +98,12 @@ pub async fn start_player_event_watchers(
) {
// Start a watcher task that updates the playback every `playback_refresh_duration_in_ms` ms.
// A positive value of `playback_refresh_duration_in_ms` is required to start the watcher.
if state.app_config.playback_refresh_duration_in_ms > 0 {
if state.configs.app_config.playback_refresh_duration_in_ms > 0 {
tokio::task::spawn({
let client_pub = client_pub.clone();
let playback_refresh_duration =
std::time::Duration::from_millis(state.app_config.playback_refresh_duration_in_ms);
let playback_refresh_duration = std::time::Duration::from_millis(
state.configs.app_config.playback_refresh_duration_in_ms,
);
async move {
loop {
client_pub
Expand Down
16 changes: 8 additions & 8 deletions spotify_player/src/client/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -80,9 +80,9 @@ impl Client {
let device_id = session.device_id().to_string();
let new_conn = streaming::new_connection(
session,
state.app_config.device.clone(),
state.configs.app_config.device.clone(),
self.client_pub.clone(),
state.app_config.player_event_hook_command.clone(),
state.configs.app_config.player_event_hook_command.clone(),
);

let mut stream_conn = self.stream_conn.lock();
Expand Down Expand Up @@ -547,7 +547,7 @@ impl Client {
{
let session = self.spotify.session().await;
devices.push((
state.app_config.device.name.clone(),
state.configs.app_config.device.name.clone(),
session.device_id().to_string(),
));
}
Expand All @@ -559,7 +559,7 @@ impl Client {
// Prioritize the `default_device` specified in the application's configurations
let id = if let Some(id) = devices
.iter()
.position(|d| d.0 == state.app_config.default_device)
.position(|d| d.0 == state.configs.app_config.default_device)
{
// prioritize the default device (specified in the app configs) if available
id
Expand Down Expand Up @@ -1317,15 +1317,15 @@ impl Client {
None => return Ok(()),
};

let path = state.cache_folder.join("image").join(format!(
let path = state.configs.cache_folder.join("image").join(format!(
"{}-{}-cover.jpg",
track.album.name,
crate::utils::map_join(&track.album.artists, |a| &a.name, ", ")
));

// Retrieve and save the new track's cover image into the cache folder.
// The notify feature still requires the cover images to be stored inside the cache folder.
if state.app_config.enable_cover_image_cache || cfg!(feature = "notify") {
if state.configs.app_config.enable_cover_image_cache || cfg!(feature = "notify") {
self.retrieve_image(url, &path, true).await?;
}

Expand Down Expand Up @@ -1394,10 +1394,10 @@ impl Client {
n.appname("spotify_player")
.icon(path.to_str().unwrap())
.summary(&get_text_from_format_str(
&state.app_config.notify_format.summary,
&state.configs.app_config.notify_format.summary,
))
.body(&get_text_from_format_str(
&state.app_config.notify_format.body,
&state.configs.app_config.notify_format.body,
));

n.show()?;
Expand Down
Loading