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
10 changes: 10 additions & 0 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 2 additions & 0 deletions spotify_player/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,7 @@ flume = "0.10.14"
serde_json = "1.0.96"
once_cell = "1.17.1"
regex = "1.8.1"
daemonize = { version = "0.5.0", optional = true }

[features]
alsa-backend = ["streaming", "librespot-playback/alsa-backend"]
Expand All @@ -62,5 +63,6 @@ media-control = ["souvlaki", "winit"]
image = ["viuer", "dep:image"]
sixel = ["image", "viuer/sixel"]
notify = ["notify-rust"]
daemon = ["daemonize"]

default = ["rodio-backend", "media-control"]
19 changes: 15 additions & 4 deletions spotify_player/src/cli/socket.rs
Original file line number Diff line number Diff line change
Expand Up @@ -23,11 +23,22 @@ pub async fn start_socket(client: Client, state: SharedState) -> Result<()> {
let mut buf = [0; 4096];
loop {
match socket.recv_from(&mut buf).await {
Err(err) => tracing::warn!("failed to receive from the socket: {err}"),
Err(err) => tracing::warn!("Failed to receive from the socket: {err}"),
Ok((n_bytes, dest_addr)) => {
let request: Request = serde_json::from_slice(&buf[0..n_bytes])?;
tracing::info!("Handle socket request: {request:?}");
handle_socket_request(&client, &state, request, &socket, dest_addr).await?;
let req_buf = &buf[0..n_bytes];
let request: Request = match serde_json::from_slice(req_buf) {
Ok(v) => v,
Err(err) => {
tracing::error!("Cannot deserialize the socket request: {err}");
continue;
}
};
tracing::info!("Handling socket request: {request:?}...");
if let Err(err) =
handle_socket_request(&client, &state, request, &socket, dest_addr).await
{
tracing::error!("Failed to handle socket request: {err}");
}
}
}
}
Expand Down
5 changes: 1 addition & 4 deletions spotify_player/src/client/handlers.rs
Original file line number Diff line number Diff line change
Expand Up @@ -21,10 +21,7 @@ pub async fn start_client_handler(
ClientRequest::NewStreamingConnection => {
// send a notification to current streaming subcriber channels to shutdown all running connections
streaming_pub.send(()).unwrap_or_default();
match client
.new_streaming_connection(streaming_sub.clone(), client_pub.clone())
.await
{
match client.new_streaming_connection(streaming_sub.clone(), client_pub.clone()) {
Err(err) => tracing::error!(
"Encountered an error during creating a new streaming connection: {err:#}",
),
Expand Down
2 changes: 1 addition & 1 deletion spotify_player/src/client/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -47,7 +47,7 @@ impl Client {

/// creates a new streaming connection
#[cfg(feature = "streaming")]
pub async fn new_streaming_connection(
pub fn new_streaming_connection(
&self,
streaming_sub: flume::Receiver<()>,
client_pub: flume::Sender<ClientRequest>,
Expand Down
135 changes: 87 additions & 48 deletions spotify_player/src/main.rs
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@ use anyhow::{Context, Result};
use std::io::Write;

fn init_app_cli_arguments() -> clap::ArgMatches {
clap::Command::new("spotify_player")
let cmd = clap::Command::new("spotify_player")
.version("0.13.1")
.about("A command driven spotify player")
.author("Thang Pham <phamducthang1234@gmail>")
Expand Down Expand Up @@ -46,8 +46,18 @@ fn init_app_cli_arguments() -> clap::ArgMatches {
.value_name("FOLDER")
.help("Path to the application's cache folder (default: $HOME/.cache/spotify-player)")
.next_line_help(true)
)
.get_matches()
);

#[cfg(feature = "daemon")]
let cmd = cmd.arg(
clap::Arg::new("daemon")
.short('d')
.long("daemon")
.action(clap::ArgAction::SetTrue)
.help("Running the application as a daemon"),
);

cmd.get_matches()
}

async fn init_spotify(
Expand All @@ -61,7 +71,6 @@ async fn init_spotify(
if state.app_config.enable_streaming {
client
.new_streaming_connection(streaming_sub.clone(), client_pub.clone())
.await
.context("failed to create a new streaming connection")?;
}

Expand Down Expand Up @@ -117,10 +126,8 @@ fn init_logging(cache_folder: &std::path::Path) -> Result<()> {
Ok(())
}

async fn start_app(state: state::SharedState, cache_folder: std::path::PathBuf) -> Result<()> {
// initialize the application's log
init_logging(&cache_folder).context("failed to initialize application's logging")?;

#[tokio::main]
async fn start_app(state: state::SharedState, is_daemon: bool) -> Result<()> {
// client channels
let (client_pub, client_sub) = flume::unbounded::<event::ClientRequest>();
// streaming channels, which are used to notify a shutdown to running streaming connections
Expand All @@ -144,33 +151,33 @@ async fn start_app(state: state::SharedState, cache_folder: std::path::PathBuf)
client.init_token().await?;

// initialize Spotify-related stuff
init_spotify(&client_pub, &streaming_sub, &client, &state)
.await
.context("failed to initialize the spotify client")?;

#[cfg(feature = "image")]
{
// initialize viuer supports for kitty and iterm2
viuer::get_kitty_support();
viuer::is_iterm_supported();
#[cfg(feature = "sixel")]
viuer::is_sixel_supported();
if is_daemon {
#[cfg(feature = "streaming")]
client
.new_streaming_connection(streaming_sub.clone(), client_pub.clone())
.context("failed to create a new streaming connection")?;
} else {
init_spotify(&client_pub, &streaming_sub, &client, &state)
.await
.context("failed to initialize the spotify client")?;
}

// Spawn application's tasks
let mut tasks = Vec::new();

tokio::task::spawn({
// client socket task (for handling CLI commands)
tasks.push(tokio::task::spawn({
let client = client.clone();
let state = state.clone();
async move {
if let Err(err) = cli::start_socket(client, state).await {
tracing::warn!("Failed to run client socket for CLI: {err}");
}
}
});
}));

// client event handler task
tokio::task::spawn({
tasks.push(tokio::task::spawn({
let state = state.clone();
let client_pub = client_pub.clone();
async move {
Expand All @@ -184,32 +191,35 @@ async fn start_app(state: state::SharedState, cache_folder: std::path::PathBuf)
)
.await;
}
});

// terminal event handler task
tokio::task::spawn_blocking({
let client_pub = client_pub.clone();
let state = state.clone();
move || {
event::start_event_handler(state, client_pub);
}
});
}));

// player event watcher task
tokio::task::spawn({
tasks.push(tokio::task::spawn({
let state = state.clone();
let client_pub = client_pub.clone();
async move {
client::start_player_event_watchers(state, client_pub).await;
}
});
}));

// application UI task
#[allow(unused_variables)]
let ui_task = tokio::task::spawn_blocking({
let state = state.clone();
move || ui::run(state)
});
if !is_daemon {
// spawn tasks needed for running the application UI

// terminal event handler task
tokio::task::spawn_blocking({
let client_pub = client_pub.clone();
let state = state.clone();
move || {
event::start_event_handler(state, client_pub);
}
});

// application UI task
tokio::task::spawn_blocking({
let state = state.clone();
move || ui::run(state)
});
}

#[cfg(feature = "media-control")]
if state.app_config.enable_media_control {
Expand All @@ -225,6 +235,7 @@ async fn start_app(state: state::SharedState, cache_folder: std::path::PathBuf)
}
});

// the winit's event loop must be run in the main thread
#[cfg(any(target_os = "macos", target_os = "windows"))]
{
// Start an event loop that listens to OS window events.
Expand All @@ -238,15 +249,15 @@ async fn start_app(state: state::SharedState, cache_folder: std::path::PathBuf)
});
}
}
#[allow(unreachable_code)]
{
ui_task.await??;
Ok(())

for task in tasks {
task.await?;
}

Ok(())
}

#[tokio::main]
async fn main() -> Result<()> {
fn main() -> Result<()> {
// parse command line arguments
let args = init_app_cli_arguments();

Expand All @@ -272,10 +283,13 @@ async fn main() -> Result<()> {
std::fs::create_dir_all(&cache_image_folder)?;
}

// initialize the application's log
init_logging(&cache_folder).context("failed to initialize application's logging")?;

// initialize the application state
let state = {
let mut state = state::State {
cache_folder: cache_folder.clone(),
cache_folder,
..state::State::default()
};
// parse config options from the config files into application's state
Expand All @@ -284,7 +298,32 @@ async fn main() -> Result<()> {
};

match args.subcommand() {
None => start_app(state, cache_folder).await,
None => {
#[cfg(feature = "daemon")]
{
let is_daemon = args.get_flag("daemon");
if is_daemon {
if cfg!(any(target_os = "macos", target_os = "windows"))
&& cfg!(feature = "media-control")
{
eprintln!("Running the application as a daemon on windows/macos with `media-control` feature enabled is not supported!");
std::process::exit(1);
}
if cfg!(not(feature = "streaming")) {
eprintln!("`streaming` feature must be enabled to run the application as a daemon!");
std::process::exit(1);
}

tracing::info!("Starting the application as a daemon...");
let daemonize = daemonize::Daemonize::new();
daemonize.start()?;
}
start_app(state, is_daemon)
}

#[cfg(not(feature = "daemon"))]
start_app(state, false)
}
Some((cmd, args)) => cli::handle_cli_subcommand(cmd, args, state.app_config.client_port),
}
}
9 changes: 9 additions & 0 deletions spotify_player/src/ui/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,15 @@ mod utils;

/// run the application UI
pub fn run(state: SharedState) -> Result<()> {
#[cfg(feature = "image")]
{
// initialize viuer supports for kitty and iterm2
viuer::get_kitty_support();
viuer::is_iterm_supported();
#[cfg(feature = "sixel")]
viuer::is_sixel_supported();
}

let mut terminal = init_ui().context("failed to initialize the application's UI")?;

let ui_refresh_duration =
Expand Down