Skip to content
Closed
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
5 changes: 5 additions & 0 deletions docs/config.md
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,11 @@ Configuration files are located in the application's configuration directory, wh
A sample `app.toml` is available at [examples/app.toml](../examples/app.toml).

`spotify_player` uses `app.toml` for application settings. Available options:
`spotify_player` also supports cli config overriding using -o / --config-override flag. Example:

```bash
spotify_player -o device.volume=80 -o theme=dracula
```

| Option | Description | Default |
| --------------------------------- | ---------------------------------------------------------------------------------------------- | ---------------------------------------------------------------------- |
Expand Down
51 changes: 51 additions & 0 deletions spotify_player/src/cli/client.rs
Original file line number Diff line number Diff line change
Expand Up @@ -195,6 +195,7 @@ async fn handle_socket_request(
let resp = handle_search_request(client, query).await?;
Ok(resp)
}
Request::Lyrics { id_or_name } => handle_lyrics_request(client, state, id_or_name).await,
}
}

Expand Down Expand Up @@ -863,3 +864,53 @@ async fn playlist_import(

Ok(result)
}

async fn handle_lyrics_request(
client: &AppClient,
state: Option<&SharedState>,
id_or_name: Option<IdOrName>,
) -> Result<Vec<u8>> {
let track_id = if let Some(id_or_name) = id_or_name {
let ItemId::Track(id) = get_spotify_id(client, ItemType::Track, id_or_name).await? else {
anyhow::bail!("Unable to get track ID")
};
id
} else {
let playback = current_playback(client, state).await?;
match playback {
Some(ref p) => match p.item {
Some(rspotify::model::PlayableItem::Track(ref t)) => {
t.id.as_ref().context("Track has no ID")?.clone_static()
}
_ => anyhow::bail!("No track currently playing"),
},
None => anyhow::bail!("No active playback"),
}
};

let track = client.track(track_id.clone()).await?;
let lyrics = client.lyrics(track_id).await?;

let mut output = format!(
"{} - {}\n\n",
track.name,
track
.artists
.iter()
.map(|a| a.name.as_str())
.collect::<Vec<_>>()
.join(", ")
);

match lyrics {
Some(lyrics) => {
for (_, line) in &lyrics.lines {
output.push_str(line);
output.push('\n');
}
}
None => output.push_str("Lyrics not found"),
}

Ok(output.into_bytes())
}
9 changes: 9 additions & 0 deletions spotify_player/src/cli/commands.rs
Original file line number Diff line number Diff line change
Expand Up @@ -238,3 +238,12 @@ pub fn init_playlist_subcommand() -> Command {
pub fn init_print_features_command() -> Command {
Command::new("features").about("Print compiled in features")
}

pub fn init_lyrics_command() -> Command {
Command::new("lyrics")
.about(
"Print provided track`s name lyrics or current playing track, if no argument specified",
)
.arg(Arg::new("id").long("id").short('i').help("Track ID"))
.arg(Arg::new("name").long("name").short('n').help("Track Name"))
}
11 changes: 11 additions & 0 deletions spotify_player/src/cli/handlers.rs
Original file line number Diff line number Diff line change
Expand Up @@ -224,6 +224,17 @@ pub fn handle_cli_subcommand(cmd: &str, args: &ArgMatches) -> Result<()> {
.expect("query is required")
.to_owned(),
},
"lyrics" => {
let id_or_name = args
.get_one::<String>("id")
.map(|id| IdOrName::Id(id.to_owned()))
.or_else(|| {
args.get_one::<String>("name")
.map(|name| IdOrName::Name(name.to_owned()))
});

Request::Lyrics { id_or_name }
}
_ => unreachable!(),
};

Expand Down
10 changes: 10 additions & 0 deletions spotify_player/src/cli/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -131,6 +131,7 @@ pub enum Request {
Like { unlike: bool },
Playlist(PlaylistCommand),
Search { query: String },
Lyrics { id_or_name: Option<IdOrName> },
}

#[derive(Debug, Serialize, Deserialize)]
Expand Down Expand Up @@ -177,6 +178,7 @@ pub fn init_cli() -> anyhow::Result<clap::Command> {
.subcommand(commands::init_generate_command())
.subcommand(commands::init_search_command())
.subcommand(commands::init_print_features_command())
.subcommand(commands::init_lyrics_command())
.arg(
clap::Arg::new("theme")
.short('t')
Expand All @@ -199,6 +201,14 @@ pub fn init_cli() -> anyhow::Result<clap::Command> {
.value_name("FOLDER")
.default_value(default_cache_folder.into_os_string())
.help("Path to the application's cache folder"),
)
.arg(
clap::Arg::new("config-override")
.short('o')
.long("config-override")
.value_name("KEY=VALUE")
.action(clap::ArgAction::Append)
.help("Override a config option (e.g. -o device.volume=80 -o theme=dracula)"),
);

#[cfg(feature = "daemon")]
Expand Down
34 changes: 34 additions & 0 deletions spotify_player/src/config/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ use std::{
sync::OnceLock,
};

use anyhow::Context;
use keymap::KeymapConfig;
use theme::ThemeConfig;

Expand Down Expand Up @@ -510,3 +511,36 @@ pub fn set_config(configs: Configs) {
.set(configs)
.expect("configs should be initialized only once");
}

// Apply a CLI config override to the application config.
// Serializes the config to TOML, navigates to the key via dot-notation,
// overrides the value, and deserializes back into AppConfig.
// Returns an error if the key path is invalid or the value type mismatches.
pub fn apply_config_override(config: &mut AppConfig, key: &str, value: &str) -> anyhow::Result<()> {
let mut config_value = toml::Value::try_from(&*config)?;

let parts: Vec<&str> = key.split('.').collect();
let mut current = &mut config_value;

for (i, part) in parts.iter().enumerate() {
if i == parts.len() - 1 {
let table = current
.as_table_mut()
.context(format!("'{key}' is not a valid config path"))?;

let parsed_value: toml::Value = value
.parse()
.unwrap_or_else(|_| toml::Value::String(value.to_string()));

table.insert(part.to_string(), parsed_value);
} else {
current = current
.get_mut(part)
.context(format!("Config key '{part}' not found in path '{key}'"))?;
}
}

*config = config_value.try_into()?;

Ok(())
}
13 changes: 10 additions & 3 deletions spotify_player/src/main.rs
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,8 @@ mod utils;
use anyhow::{Context, Result};
use std::io::Write;

use crate::config::apply_config_override;

fn init_spotify(
client_pub: &flume::Sender<client::ClientRequest>,
client: &client::AppClient,
Expand Down Expand Up @@ -256,9 +258,14 @@ fn main() -> Result<()> {
// set the log folder to be the cache folder if it is not set
configs.app_config.log_folder = Some(cache_folder);
}
if let Some(theme) = args.get_one::<String>("theme") {
// override the theme config if user specifies a `theme` cli argument
theme.clone_into(&mut configs.app_config.theme);
if let Some(overrides) = args.get_many::<String>("config-override") {
for override_str in overrides {
let (key, value) = override_str.split_once('=').context(format!(
"Invalid override format: '{override_str}'. Expected KEY=VALUE"
))?;

apply_config_override(&mut configs.app_config, key, value)?;
}
}
config::set_config(configs);
}
Expand Down
Loading