From c7434c84f7a1ab7e5bc547f00979058d449415f7 Mon Sep 17 00:00:00 2001 From: Rob Rotaru Date: Sat, 19 Jul 2025 02:02:23 -0400 Subject: [PATCH 01/16] feat: add playlist edit command for adding/removing tracks MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Implements `playlist edit --id ` command that allows users to add or remove tracks from Spotify playlists via CLI. Changes: - Add EditAction enum with Add/Delete variants - Add Edit variant to PlaylistCommand enum - Implement CLI argument parsing for edit subcommand - Add Spotify API integration for playlist_add_items and playlist_remove_all_occurrences_of_items - Add proper type conversion from TrackId to PlayableId::Track πŸ€– Generated with [Claude Code](https://claude.ai/code) Co-Authored-By: Claude --- spotify_player/src/cli/client.rs | 38 +++++++++++++++++++++++++++++- spotify_player/src/cli/commands.rs | 15 ++++++++++++ spotify_player/src/cli/handlers.rs | 34 ++++++++++++++++++++++++-- spotify_player/src/cli/mod.rs | 11 +++++++++ 4 files changed, 95 insertions(+), 3 deletions(-) diff --git a/spotify_player/src/cli/client.rs b/spotify_player/src/cli/client.rs index fa91bb92..cddb0555 100644 --- a/spotify_player/src/cli/client.rs +++ b/spotify_player/src/cli/client.rs @@ -23,7 +23,7 @@ use crate::{ use rspotify::prelude::{BaseClient, OAuthClient}; use super::{ - Command, Deserialize, GetRequest, IdOrName, ItemId, ItemType, Key, PlaylistCommand, Response, + Command, Deserialize, EditAction, GetRequest, IdOrName, ItemId, ItemType, Key, PlaylistCommand, Response, Serialize, MAX_REQUEST_SIZE, }; @@ -594,6 +594,42 @@ async fn handle_playlist_request(client: &Client, command: PlaylistCommand) -> R Ok(result) } + PlaylistCommand::Edit { + playlist_id, + action, + track_id, + } => { + match action { + EditAction::Add => { + client + .playlist_add_items( + playlist_id.clone(), + [rspotify::model::PlayableId::Track(track_id.as_ref())], + None, + ) + .await?; + Ok(format!( + "Track '{}' added to playlist '{}'", + track_id.id(), + playlist_id.id() + )) + } + EditAction::Delete => { + client + .playlist_remove_all_occurrences_of_items( + playlist_id.clone(), + [rspotify::model::PlayableId::Track(track_id.as_ref())], + None, + ) + .await?; + Ok(format!( + "Track '{}' removed from playlist '{}'", + track_id.id(), + playlist_id.id() + )) + } + } + } } } diff --git a/spotify_player/src/cli/commands.rs b/spotify_player/src/cli/commands.rs index 2e5e6b4e..4e130c4a 100644 --- a/spotify_player/src/cli/commands.rs +++ b/spotify_player/src/cli/commands.rs @@ -207,4 +207,19 @@ pub fn init_playlist_subcommand() -> Command { .long("delete") .action(clap::ArgAction::SetTrue) .help("Deletes any previously imported tracks that are no longer in an imported playlist since last import."))) + .subcommand(Command::new("edit").about("Add or remove tracks from a playlist.") + .arg(Arg::new("playlist_id") + .help("Playlist ID") + .required(true) + .value_parser(clap::builder::NonEmptyStringValueParser::new())) + .arg(Arg::new("action") + .help("Action to perform") + .required(true) + .value_parser(["add", "delete"])) + .arg(Arg::new("track_id") + .long("id") + .short('i') + .help("Track ID to add or remove") + .required(true) + .value_parser(clap::builder::NonEmptyStringValueParser::new()))) } diff --git a/spotify_player/src/cli/handlers.rs b/spotify_player/src/cli/handlers.rs index 11ec3480..22307834 100644 --- a/spotify_player/src/cli/handlers.rs +++ b/spotify_player/src/cli/handlers.rs @@ -1,8 +1,8 @@ use crate::{auth::AuthConfig, client}; use super::{ - config, init_cli, start_socket, Command, ContextType, GetRequest, IdOrName, ItemType, Key, - PlaylistCommand, PlaylistId, Request, Response, MAX_REQUEST_SIZE, + config, init_cli, start_socket, Command, ContextType, EditAction, GetRequest, IdOrName, ItemType, Key, + PlaylistCommand, PlaylistId, Request, Response, TrackId, MAX_REQUEST_SIZE, }; use anyhow::{Context, Result}; use clap::{ArgMatches, Id}; @@ -314,6 +314,36 @@ fn handle_playlist_subcommand(args: &ArgMatches) -> Result { PlaylistCommand::Sync { id: pid, delete } } + "edit" => { + let playlist_id_str = args + .get_one::("playlist_id") + .expect("playlist_id arg is required") + .to_owned(); + + let action_str = args + .get_one::("action") + .expect("action arg is required"); + + let track_id_str = args + .get_one::("track_id") + .expect("track_id arg is required") + .to_owned(); + + let playlist_id = PlaylistId::from_id(playlist_id_str)?; + let track_id = TrackId::from_id(track_id_str)?; + + let action = match action_str.as_str() { + "add" => EditAction::Add, + "delete" => EditAction::Delete, + _ => unreachable!(), + }; + + PlaylistCommand::Edit { + playlist_id, + action, + track_id, + } + } _ => unreachable!(), }; diff --git a/spotify_player/src/cli/mod.rs b/spotify_player/src/cli/mod.rs index 3bee6516..f3f1b398 100644 --- a/spotify_player/src/cli/mod.rs +++ b/spotify_player/src/cli/mod.rs @@ -58,6 +58,12 @@ pub enum IdOrName { Name(String), } +#[derive(Debug, Serialize, Deserialize, clap::ValueEnum, Clone)] +pub enum EditAction { + Add, + Delete, +} + #[derive(Debug, Serialize, Deserialize)] pub enum PlaylistCommand { New { @@ -82,6 +88,11 @@ pub enum PlaylistCommand { id: Option>, delete: bool, }, + Edit { + playlist_id: PlaylistId<'static>, + action: EditAction, + track_id: TrackId<'static>, + }, } #[derive(Debug, Serialize, Deserialize)] From a6f05f1b0010202783930bc693f901305ec0c353 Mon Sep 17 00:00:00 2001 From: tojamrok <16987182+tojamrok@users.noreply.github.com> Date: Thu, 31 Jul 2025 16:20:33 +0200 Subject: [PATCH 02/16] Implement playlist edit --album-id CLI command On top of PR #776, which implements --id CLI command, allow adding and removing whole albums. Modified track manipulation - it is now done with --track-id instead of --id parameter. --- spotify_player/src/cli/client.rs | 105 ++++++++++++++++++++++------- spotify_player/src/cli/commands.rs | 19 ++++-- spotify_player/src/cli/handlers.rs | 19 ++++-- spotify_player/src/cli/mod.rs | 3 +- 4 files changed, 109 insertions(+), 37 deletions(-) diff --git a/spotify_player/src/cli/client.rs b/spotify_player/src/cli/client.rs index cddb0555..b7c24d51 100644 --- a/spotify_player/src/cli/client.rs +++ b/spotify_player/src/cli/client.rs @@ -598,35 +598,92 @@ async fn handle_playlist_request(client: &Client, command: PlaylistCommand) -> R playlist_id, action, track_id, + album_id, } => { match action { EditAction::Add => { - client - .playlist_add_items( - playlist_id.clone(), - [rspotify::model::PlayableId::Track(track_id.as_ref())], - None, - ) - .await?; - Ok(format!( - "Track '{}' added to playlist '{}'", - track_id.id(), - playlist_id.id() - )) + if let Some(track_id) = track_id { + client + .playlist_add_items( + playlist_id.clone(), + [rspotify::model::PlayableId::Track(track_id.as_ref())], + None, + ) + .await?; + Ok(format!( + "Track '{}' added to playlist '{}'", + track_id.id(), + playlist_id.id() + )) + } else if let Some(album_id) = album_id { + let album = client.album(album_id.clone(), None).await?; + let track_count = album.tracks.items.len(); + let track_ids: Vec = album + .tracks + .items + .into_iter() + .filter_map(|t| t.id.map(|id| rspotify::model::PlayableId::Track(id))) + .collect(); + + if !track_ids.is_empty() { + client + .playlist_add_items(playlist_id.clone(), track_ids, None) + .await?; + } + + Ok(format!( + "Album '{}' ({} tracks) added to playlist '{}'", + album.name, + track_count, + playlist_id.id() + )) + } else { + anyhow::bail!("Either track_id or album_id must be provided") + } } EditAction::Delete => { - client - .playlist_remove_all_occurrences_of_items( - playlist_id.clone(), - [rspotify::model::PlayableId::Track(track_id.as_ref())], - None, - ) - .await?; - Ok(format!( - "Track '{}' removed from playlist '{}'", - track_id.id(), - playlist_id.id() - )) + if let Some(track_id) = track_id { + client + .playlist_remove_all_occurrences_of_items( + playlist_id.clone(), + [rspotify::model::PlayableId::Track(track_id.as_ref())], + None, + ) + .await?; + Ok(format!( + "Track '{}' removed from playlist '{}'", + track_id.id(), + playlist_id.id() + )) + } else if let Some(album_id) = album_id { + let album = client.album(album_id.clone(), None).await?; + let track_count = album.tracks.items.len(); + let track_ids: Vec = album + .tracks + .items + .into_iter() + .filter_map(|t| t.id.map(|id| rspotify::model::PlayableId::Track(id))) + .collect(); + + if !track_ids.is_empty() { + client + .playlist_remove_all_occurrences_of_items( + playlist_id.clone(), + track_ids, + None + ) + .await?; + } + + Ok(format!( + "Album '{}' ({} tracks) removed from playlist '{}'", + album.name, + track_count, + playlist_id.id() + )) + } else { + anyhow::bail!("Either track_id or album_id must be provided") + } } } } diff --git a/spotify_player/src/cli/commands.rs b/spotify_player/src/cli/commands.rs index 4e130c4a..1727490d 100644 --- a/spotify_player/src/cli/commands.rs +++ b/spotify_player/src/cli/commands.rs @@ -207,7 +207,7 @@ pub fn init_playlist_subcommand() -> Command { .long("delete") .action(clap::ArgAction::SetTrue) .help("Deletes any previously imported tracks that are no longer in an imported playlist since last import."))) - .subcommand(Command::new("edit").about("Add or remove tracks from a playlist.") + .subcommand(Command::new("edit").about("Add or remove tracks or albums from a playlist.") .arg(Arg::new("playlist_id") .help("Playlist ID") .required(true) @@ -217,9 +217,18 @@ pub fn init_playlist_subcommand() -> Command { .required(true) .value_parser(["add", "delete"])) .arg(Arg::new("track_id") - .long("id") - .short('i') + .long("track-id") + .short('t') .help("Track ID to add or remove") - .required(true) - .value_parser(clap::builder::NonEmptyStringValueParser::new()))) + .value_parser(clap::builder::NonEmptyStringValueParser::new())) + .arg(Arg::new("album_id") + .long("album-id") + .short('a') + .help("Album ID to add or remove") + .value_parser(clap::builder::NonEmptyStringValueParser::new())) + .group( + ArgGroup::new("content_id") + .args(["track_id", "album_id"]) + .required(true) + )) } diff --git a/spotify_player/src/cli/handlers.rs b/spotify_player/src/cli/handlers.rs index 22307834..d6daee48 100644 --- a/spotify_player/src/cli/handlers.rs +++ b/spotify_player/src/cli/handlers.rs @@ -2,7 +2,7 @@ use crate::{auth::AuthConfig, client}; use super::{ config, init_cli, start_socket, Command, ContextType, EditAction, GetRequest, IdOrName, ItemType, Key, - PlaylistCommand, PlaylistId, Request, Response, TrackId, MAX_REQUEST_SIZE, + PlaylistCommand, PlaylistId, Request, Response, TrackId, AlbumId, MAX_REQUEST_SIZE, }; use anyhow::{Context, Result}; use clap::{ArgMatches, Id}; @@ -324,13 +324,7 @@ fn handle_playlist_subcommand(args: &ArgMatches) -> Result { .get_one::("action") .expect("action arg is required"); - let track_id_str = args - .get_one::("track_id") - .expect("track_id arg is required") - .to_owned(); - let playlist_id = PlaylistId::from_id(playlist_id_str)?; - let track_id = TrackId::from_id(track_id_str)?; let action = match action_str.as_str() { "add" => EditAction::Add, @@ -338,10 +332,21 @@ fn handle_playlist_subcommand(args: &ArgMatches) -> Result { _ => unreachable!(), }; + let track_id = args + .get_one::("track_id") + .map(|s| TrackId::from_id(s.to_owned())) + .transpose()?; + + let album_id = args + .get_one::("album_id") + .map(|s| AlbumId::from_id(s.to_owned())) + .transpose()?; + PlaylistCommand::Edit { playlist_id, action, track_id, + album_id, } } _ => unreachable!(), diff --git a/spotify_player/src/cli/mod.rs b/spotify_player/src/cli/mod.rs index f3f1b398..6b8ca210 100644 --- a/spotify_player/src/cli/mod.rs +++ b/spotify_player/src/cli/mod.rs @@ -91,7 +91,8 @@ pub enum PlaylistCommand { Edit { playlist_id: PlaylistId<'static>, action: EditAction, - track_id: TrackId<'static>, + track_id: Option>, + album_id: Option>, }, } From 6fd3dd16fa3c0173a614756409cc0d52692bb5fd Mon Sep 17 00:00:00 2001 From: tojamrok <16987182+tojamrok@users.noreply.github.com> Date: Thu, 31 Jul 2025 17:45:43 +0200 Subject: [PATCH 03/16] feat: add name-based support to playlist edit command MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Extends the playlist edit command to support name-based arguments alongside IDs: - Add --playlist-name option as alternative to --playlist-id - Add --track-name option as alternative to --track-id - Add --album-name option as alternative to --album-id - Automatically resolve names to IDs via Spotify search API - Maintain backward compatibility with existing ID-based commands Examples: - spotify_player playlist edit --playlist-name "Melancholic Midnight Musings" add --track-name "Bohemian Rhapsody" - spotify_player playlist edit --playlist-id "123" add --album-name "The Dark Side of the Moon" - spotify_player playlist edit --playlist-name "Post-Apocalyptic Soundscapes" delete --track-name "Mad World" πŸ€– Generated with [Claude Code](https://claude.ai/code) Co-Authored-By: Claude --- spotify_player/src/cli/client.rs | 65 ++++++++++++++++++++++++++---- spotify_player/src/cli/commands.rs | 29 ++++++++----- spotify_player/src/cli/handlers.rs | 39 ++++++++++++++---- spotify_player/src/cli/mod.rs | 4 +- 4 files changed, 112 insertions(+), 25 deletions(-) diff --git a/spotify_player/src/cli/client.rs b/spotify_player/src/cli/client.rs index b7c24d51..0823150f 100644 --- a/spotify_player/src/cli/client.rs +++ b/spotify_player/src/cli/client.rs @@ -595,14 +595,32 @@ async fn handle_playlist_request(client: &Client, command: PlaylistCommand) -> R Ok(result) } PlaylistCommand::Edit { - playlist_id, + playlist_id_or_name, action, track_id, + track_name, album_id, + album_name, } => { + let playlist_id = match get_spotify_id(client, ItemType::Playlist, playlist_id_or_name).await? { + ItemId::Playlist(id) => id, + _ => unreachable!(), + }; + match action { EditAction::Add => { - if let Some(track_id) = track_id { + if track_id.is_some() || track_name.is_some() { + let track_id = if let Some(track_id) = track_id { + track_id + } else if let Some(track_name) = track_name { + match get_spotify_id(client, ItemType::Track, IdOrName::Name(track_name)).await? { + ItemId::Track(id) => id, + _ => unreachable!(), + } + } else { + unreachable!() + }; + client .playlist_add_items( playlist_id.clone(), @@ -615,7 +633,18 @@ async fn handle_playlist_request(client: &Client, command: PlaylistCommand) -> R track_id.id(), playlist_id.id() )) - } else if let Some(album_id) = album_id { + } else if album_id.is_some() || album_name.is_some() { + let album_id = if let Some(album_id) = album_id { + album_id + } else if let Some(album_name) = album_name { + match get_spotify_id(client, ItemType::Album, IdOrName::Name(album_name)).await? { + ItemId::Album(id) => id, + _ => unreachable!(), + } + } else { + unreachable!() + }; + let album = client.album(album_id.clone(), None).await?; let track_count = album.tracks.items.len(); let track_ids: Vec = album @@ -638,11 +667,22 @@ async fn handle_playlist_request(client: &Client, command: PlaylistCommand) -> R playlist_id.id() )) } else { - anyhow::bail!("Either track_id or album_id must be provided") + anyhow::bail!("Either track or album must be provided") } } EditAction::Delete => { - if let Some(track_id) = track_id { + if track_id.is_some() || track_name.is_some() { + let track_id = if let Some(track_id) = track_id { + track_id + } else if let Some(track_name) = track_name { + match get_spotify_id(client, ItemType::Track, IdOrName::Name(track_name)).await? { + ItemId::Track(id) => id, + _ => unreachable!(), + } + } else { + unreachable!() + }; + client .playlist_remove_all_occurrences_of_items( playlist_id.clone(), @@ -655,7 +695,18 @@ async fn handle_playlist_request(client: &Client, command: PlaylistCommand) -> R track_id.id(), playlist_id.id() )) - } else if let Some(album_id) = album_id { + } else if album_id.is_some() || album_name.is_some() { + let album_id = if let Some(album_id) = album_id { + album_id + } else if let Some(album_name) = album_name { + match get_spotify_id(client, ItemType::Album, IdOrName::Name(album_name)).await? { + ItemId::Album(id) => id, + _ => unreachable!(), + } + } else { + unreachable!() + }; + let album = client.album(album_id.clone(), None).await?; let track_count = album.tracks.items.len(); let track_ids: Vec = album @@ -682,7 +733,7 @@ async fn handle_playlist_request(client: &Client, command: PlaylistCommand) -> R playlist_id.id() )) } else { - anyhow::bail!("Either track_id or album_id must be provided") + anyhow::bail!("Either track or album must be provided") } } } diff --git a/spotify_player/src/cli/commands.rs b/spotify_player/src/cli/commands.rs index 1727490d..b0af6d41 100644 --- a/spotify_player/src/cli/commands.rs +++ b/spotify_player/src/cli/commands.rs @@ -88,6 +88,16 @@ fn add_id_or_name_group(cmd: Command) -> Command { ) } +fn add_playlist_id_or_name_group(cmd: Command) -> Command { + cmd.arg(Arg::new("playlist_id").long("playlist-id").help("Playlist ID")) + .arg(Arg::new("playlist_name").long("playlist-name").help("Playlist name")) + .group( + ArgGroup::new("playlist_id_or_name") + .args(["playlist_id", "playlist_name"]) + .required(true), + ) +} + pub fn init_playback_subcommand() -> Command { Command::new("playback") .about("Interact with the playback") @@ -207,11 +217,7 @@ pub fn init_playlist_subcommand() -> Command { .long("delete") .action(clap::ArgAction::SetTrue) .help("Deletes any previously imported tracks that are no longer in an imported playlist since last import."))) - .subcommand(Command::new("edit").about("Add or remove tracks or albums from a playlist.") - .arg(Arg::new("playlist_id") - .help("Playlist ID") - .required(true) - .value_parser(clap::builder::NonEmptyStringValueParser::new())) + .subcommand(add_playlist_id_or_name_group(Command::new("edit").about("Add or remove tracks or albums from a playlist.") .arg(Arg::new("action") .help("Action to perform") .required(true) @@ -221,14 +227,17 @@ pub fn init_playlist_subcommand() -> Command { .short('t') .help("Track ID to add or remove") .value_parser(clap::builder::NonEmptyStringValueParser::new())) + .arg(Arg::new("track_name") + .long("track-name") + .help("Track name to add or remove") + .value_parser(clap::builder::NonEmptyStringValueParser::new())) .arg(Arg::new("album_id") .long("album-id") .short('a') .help("Album ID to add or remove") .value_parser(clap::builder::NonEmptyStringValueParser::new())) - .group( - ArgGroup::new("content_id") - .args(["track_id", "album_id"]) - .required(true) - )) + .arg(Arg::new("album_name") + .long("album-name") + .help("Album name to add or remove") + .value_parser(clap::builder::NonEmptyStringValueParser::new())))) } diff --git a/spotify_player/src/cli/handlers.rs b/spotify_player/src/cli/handlers.rs index d6daee48..66f56b76 100644 --- a/spotify_player/src/cli/handlers.rs +++ b/spotify_player/src/cli/handlers.rs @@ -46,6 +46,26 @@ fn get_id_or_name(args: &ArgMatches) -> IdOrName { } } +fn get_playlist_id_or_name(args: &ArgMatches) -> IdOrName { + match args + .get_one::("playlist_id_or_name") + .expect("playlist_id_or_name group is required") + .as_str() + { + "playlist_name" => IdOrName::Name( + args.get_one::("playlist_name") + .expect("playlist_name should be specified") + .to_owned(), + ), + "playlist_id" => IdOrName::Id( + args.get_one::("playlist_id") + .expect("playlist_id should be specified") + .to_owned(), + ), + id => panic!("unknown playlist id: {id}"), + } +} + fn handle_get_subcommand(args: &ArgMatches) -> Request { let (cmd, args) = args.subcommand().expect("playback subcommand is required"); @@ -315,16 +335,11 @@ fn handle_playlist_subcommand(args: &ArgMatches) -> Result { PlaylistCommand::Sync { id: pid, delete } } "edit" => { - let playlist_id_str = args - .get_one::("playlist_id") - .expect("playlist_id arg is required") - .to_owned(); + let playlist_id_or_name = get_playlist_id_or_name(args); let action_str = args .get_one::("action") .expect("action arg is required"); - - let playlist_id = PlaylistId::from_id(playlist_id_str)?; let action = match action_str.as_str() { "add" => EditAction::Add, @@ -337,16 +352,26 @@ fn handle_playlist_subcommand(args: &ArgMatches) -> Result { .map(|s| TrackId::from_id(s.to_owned())) .transpose()?; + let track_name = args + .get_one::("track_name") + .map(|s| s.to_owned()); + let album_id = args .get_one::("album_id") .map(|s| AlbumId::from_id(s.to_owned())) .transpose()?; + let album_name = args + .get_one::("album_name") + .map(|s| s.to_owned()); + PlaylistCommand::Edit { - playlist_id, + playlist_id_or_name, action, track_id, + track_name, album_id, + album_name, } } _ => unreachable!(), diff --git a/spotify_player/src/cli/mod.rs b/spotify_player/src/cli/mod.rs index 6b8ca210..5f1b49db 100644 --- a/spotify_player/src/cli/mod.rs +++ b/spotify_player/src/cli/mod.rs @@ -89,10 +89,12 @@ pub enum PlaylistCommand { delete: bool, }, Edit { - playlist_id: PlaylistId<'static>, + playlist_id_or_name: IdOrName, action: EditAction, track_id: Option>, + track_name: Option, album_id: Option>, + album_name: Option, }, } From 33ad96d4898d92295a2346e6512d4180487520fe Mon Sep 17 00:00:00 2001 From: Mark Gandolfo Date: Sun, 27 Jul 2025 06:52:30 +1000 Subject: [PATCH 04/16] only update count prefix after command/action is handled Fixes #753 Fixes #764 This PR updates the logic to only update `count_prefix` if the pressed digit key is not handled as bind command or action --------- Co-authored-by: Thang Pham --- spotify_player/src/event/mod.rs | 44 +++++++++++++---------------- spotify_player/src/media_control.rs | 12 ++++---- 2 files changed, 26 insertions(+), 30 deletions(-) diff --git a/spotify_player/src/event/mod.rs b/spotify_player/src/event/mod.rs index 117cc896..4914c6fd 100644 --- a/spotify_player/src/event/mod.rs +++ b/spotify_player/src/event/mod.rs @@ -100,26 +100,6 @@ fn handle_key_event( let key: Key = event.into(); let mut ui = state.ui.lock(); - // Check if the key is a digit and handle count prefix - if let Key::None(KeyCode::Char(c)) = key { - if c.is_ascii_digit() { - let digit = c.to_digit(10).unwrap() as usize; - // If we have an existing count prefix, append the digit - // Otherwise, start a new count (but ignore leading zeros) - ui.count_prefix = match ui.count_prefix { - Some(count) => Some(count * 10 + digit), - None => { - if digit > 0 { - Some(digit) - } else { - None - } - } - }; - return Ok(()); - } - } - let mut key_sequence = ui.input_key_sequence.clone(); key_sequence.keys.push(key); @@ -163,10 +143,26 @@ fn handle_key_event( ui.input_key_sequence.keys = vec![]; ui.count_prefix = None; } else { - ui.input_key_sequence = key_sequence; - // If we didn't handle the key and it wasn't a digit, clear the count prefix - if !matches!(key, Key::None(KeyCode::Char(c)) if c.is_ascii_digit()) { - ui.count_prefix = None; + // update the count prefix if the key is a digit + match key { + Key::None(KeyCode::Char(c)) if c.is_ascii_digit() => { + let digit = c.to_digit(10).unwrap() as usize; + ui.input_key_sequence.keys = vec![]; + ui.count_prefix = match ui.count_prefix { + Some(count) => Some(count * 10 + digit), + None => { + if digit > 0 { + Some(digit) + } else { + None + } + } + }; + } + _ => { + ui.input_key_sequence = key_sequence; + ui.count_prefix = None; + } } } Ok(()) diff --git a/spotify_player/src/media_control.rs b/spotify_player/src/media_control.rs index f084b859..07fca455 100644 --- a/spotify_player/src/media_control.rs +++ b/spotify_player/src/media_control.rs @@ -192,7 +192,7 @@ mod windows { ..Default::default() }; - if RegisterClassExW(&wnd_class) == 0 { + if RegisterClassExW(&raw const wnd_class) == 0 { return Err(format!( "Registering class failed: {}", Error::last_os_error() @@ -246,14 +246,14 @@ mod windows { pub fn pump_event_queue() -> bool { unsafe { let mut msg: MSG = std::mem::zeroed(); - let mut has_message = PeekMessageW(&mut msg, None, 0, 0, PM_REMOVE).as_bool(); + let mut has_message = PeekMessageW(&raw mut msg, None, 0, 0, PM_REMOVE).as_bool(); while msg.message != WM_QUIT && has_message { - if !IsDialogMessageW(GetAncestor(msg.hwnd, GA_ROOT), &msg).as_bool() { - let _ = TranslateMessage(&msg); - let _ = DispatchMessageW(&msg); + if !IsDialogMessageW(GetAncestor(msg.hwnd, GA_ROOT), &raw const msg).as_bool() { + let _ = TranslateMessage(&raw const msg); + let _ = DispatchMessageW(&raw const msg); } - has_message = PeekMessageW(&mut msg, None, 0, 0, PM_REMOVE).as_bool(); + has_message = PeekMessageW(&raw mut msg, None, 0, 0, PM_REMOVE).as_bool(); } msg.message == WM_QUIT From f820228e6115acb85b9a1ca9a28a1981889438be Mon Sep 17 00:00:00 2001 From: John Mitchell Date: Sat, 26 Jul 2025 22:10:03 +0100 Subject: [PATCH 05/16] Feature: Add ability to jump to search result (#748) --- README.md | 1 + spotify_player/src/command.rs | 2 ++ spotify_player/src/config/keymap.rs | 4 ++++ spotify_player/src/event/window.rs | 18 +++++++++++++++++- 4 files changed, 24 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index 1bd69608..12e22781 100644 --- a/README.md +++ b/README.md @@ -386,6 +386,7 @@ List of supported commands: | `MovePlaylistItemDown` | move playlist item down one position | `C-j` | | `CreatePlaylist` | create a new playlist | `N` | | `JumpToCurrentTrackInContext` | jump to the current track in the context | `g c` | +| `JumpToHighlightTrackInContext`| jump to the currently highlighted search result in the context | `C-g` | To add new shortcuts or modify the default shortcuts, please refer to the [keymaps section](docs/config.md#keymaps) in the configuration documentation. diff --git a/spotify_player/src/command.rs b/spotify_player/src/command.rs index 38633441..1c365cd3 100644 --- a/spotify_player/src/command.rs +++ b/spotify_player/src/command.rs @@ -53,6 +53,7 @@ pub enum Command { ShowActionsOnSelectedItem, ShowActionsOnCurrentTrack, AddSelectedItemToQueue, + JumpToHighlightTrackInContext, BrowseUserPlaylists, BrowseUserFollowedArtists, @@ -330,6 +331,7 @@ impl Command { Self::ShowActionsOnSelectedItem => "open a popup showing actions on a selected item", Self::ShowActionsOnCurrentTrack => "open a popup showing actions on the current track", Self::AddSelectedItemToQueue => "add the selected item to queue", + Self::JumpToHighlightTrackInContext => "jump to the currently highlighted search result in the context", Self::FocusNextWindow => "focus the next focusable window (if any)", Self::FocusPreviousWindow => "focus the previous focusable window (if any)", Self::SwitchTheme => "open a popup for switching theme", diff --git a/spotify_player/src/config/keymap.rs b/spotify_player/src/config/keymap.rs index ac408967..4d8429af 100644 --- a/spotify_player/src/config/keymap.rs +++ b/spotify_player/src/config/keymap.rs @@ -107,6 +107,10 @@ impl Default for KeymapConfig { key_sequence: "Z".into(), command: Command::AddSelectedItemToQueue, }, + Keymap { + key_sequence: "C-g".into(), + command: Command::JumpToHighlightTrackInContext, + }, Keymap { key_sequence: "C-space".into(), command: Command::ShowActionsOnSelectedItem, diff --git a/spotify_player/src/event/window.rs b/spotify_player/src/event/window.rs index db7ade28..e9c5ad03 100644 --- a/spotify_player/src/event/window.rs +++ b/spotify_player/src/event/window.rs @@ -5,7 +5,7 @@ use crate::{ construct_album_actions, construct_artist_actions, construct_playlist_actions, construct_show_actions, }, - state::{Episode, Show, UIStateGuard}, + state::{Episode, MutableWindowState, Show, UIStateGuard}, }; use command::Action; use rand::Rng; @@ -337,6 +337,22 @@ fn handle_command_for_track_table_window( filtered_tracks[id].id.clone().into(), ))?; } + Command::JumpToHighlightTrackInContext => { + ui.popup = None; + let selected_track = filtered_tracks[id]; + let location = tracks + .iter() + .enumerate() + .find(|(_, track)| track.id == selected_track.id) + .unwrap(); + + // Move selection and change the offset so selection is at the top + ui.current_page_mut().select(location.0); + match ui.current_page_mut().focus_window_state_mut().unwrap() { + MutableWindowState::Table(table) => *table.offset_mut() = location.0, + _ => unreachable!("playlist context should be a table"), + } + } _ => return Ok(false), } Ok(true) From e7ef79ae0d69e230b9c02a6c57c6e7108a64443d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jos=C3=A9=20Antonio=20Moreno?= <166874887+joan-morera@users.noreply.github.com> Date: Sun, 27 Jul 2025 22:44:21 +0200 Subject: [PATCH 06/16] ArchLinux install process update (#761) Updated archlinux install guide, as official package is now supporting all functionalities, built with pulseaudio-backend. --- README.md | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index 12e22781..20eb673d 100644 --- a/README.md +++ b/README.md @@ -101,7 +101,8 @@ Run `cargo install spotify_player --locked` to install the application from [cra Run `pacman -S spotify-player` to install the application. -Alternatively, run `yay -S spotify-player-full` to install an AUR package compiled with full feature support and Pulseaudio/Pipewire instead of rodio. +**Note**: Defaults to PulseAudio / Pipewire audio backend. For a different one, please consider modifying the [official PKGBUILD](https://gitlab.archlinux.org/archlinux/packaging/packages/spotify-player) and rebuilding it manually. See [Audio Backends](#audio-backend) for a list of options. + ### Void Linux From cf1237208781c8db6978f50b31b7f842f6ca68cf Mon Sep 17 00:00:00 2001 From: VimDie5el Date: Tue, 24 Jun 2025 15:49:43 +0300 Subject: [PATCH 07/16] feat: make playback metadata fields configurable (#756) - Add `playback_metadata_fields` option to config, allowing users to control which metadata fields (repeat, shuffle, volume, device) are shown in the playback UI. - Update example config and documentation to reflect the new option. - Users can now specify any combination and order of metadata fields to display in the `{metadata}` section. --- docs/config.md | 67 ++++++++++++++++--------------- examples/app.toml | 1 + spotify_player/src/config/mod.rs | 7 ++++ spotify_player/src/ui/playback.rs | 46 ++++++++++++--------- 4 files changed, 70 insertions(+), 51 deletions(-) diff --git a/docs/config.md b/docs/config.md index e034f433..49342600 100644 --- a/docs/config.md +++ b/docs/config.md @@ -23,39 +23,40 @@ All configuration files should be placed inside the application's configuration `spotify_player` uses `app.toml` to configure general application configurations: -| Option | Description | Default | -| --------------------------------- | ---------------------------------------------------------------------------------------- | ----------------------------------------------------------- | -| `client_id` | the Spotify client's ID | `65b708073fc0480ea92a077233ca87bd` | -| `client_id_command` | a shell command that prints the Spotify client ID to stdout (overrides `client_id`) | `None` | -| `login_redirect_uri` | the redirect URI for authenticating the application | `http://127.0.0.1:8989/login` | -| `client_port` | the port that the application's client is running on to handle CLI commands | `8080` | -| `tracks_playback_limit` | the limit for the number of tracks played in a **tracks** playback | `50` | -| `playback_format` | the format of the text in the playback's window | `{status} {track} β€’ {artists} {liked}\n{album}\n{metadata}` | -| `notify_format` | the format of a notification (`notify` feature only) | `{ summary = "{track} β€’ {artists}", body = "{album}" }` | -| `notify_timeout_in_secs` | the timeout (in seconds) of a notification (`notify` feature only) | `0` (no timeout) | -| `player_event_hook_command` | the hook command executed when there is a new player event | `None` | -| `ap_port` | the application's Spotify session connection port | `None` | -| `proxy` | the application's Spotify session connection proxy | `None` | -| `theme` | the application's theme | `default` | -| `app_refresh_duration_in_ms` | the duration (in ms) between two consecutive application refreshes | `32` | -| `playback_refresh_duration_in_ms` | the duration (in ms) between two consecutive playback refreshes | `0` | -| `page_size_in_rows` | a page's size expressed as a number of rows (for page-navigation commands) | `20` | -| `enable_media_control` | enable application media control support (`media-control` feature only) | `true` (Linux), `false` (Windows and MacOS) | -| `enable_streaming` | enable streaming (`streaming` feature only) | `Always` | -| `enable_notify` | enable notification (`notify` feature only) | `true` | -| `enable_cover_image_cache` | store album's cover images in the cache folder | `true` | -| `notify_streaming_only` | only send notification when streaming is enabled (`streaming` and `notify` feature only) | `false` | -| `default_device` | the default device to connect to on startup if no playing device found | `spotify-player` | -| `play_icon` | the icon to indicate playing state of a Spotify item | `β–Ά` | -| `pause_icon` | the icon to indicate pause state of a Spotify item | `β–Œβ–Œ` | -| `liked_icon` | the icon to indicate the liked state of a song | `β™₯` | -| `border_type` | the type of the application's borders | `Plain` | -| `progress_bar_type` | the type of the playback progress bar | `Rectangle` | -| `cover_img_width` | the width of the cover image (`image` feature only) | `5` | -| `cover_img_length` | the length of the cover image (`image` feature only) | `9` | -| `cover_img_scale` | the scale of the cover image (`image` feature only) | `1.0` | -| `seek_duration_secs` | the duration (in seconds) to seek when using `SeekForward` and `SeekBackward` commands | `5` | -| `sort_artist_albums_by_type` | sort albums on artist's pages by type, i.e. album or single | `false` | +| Option | Description | Default | +| --------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------ | ----------------------------------------------------------- | +| `client_id` | the Spotify client's ID | `65b708073fc0480ea92a077233ca87bd` | +| `client_id_command` | a shell command that prints the Spotify client ID to stdout (overrides `client_id`) | `None` | +| `login_redirect_uri` | the redirect URI for authenticating the application | `http://127.0.0.1:8989/login` | +| `client_port` | the port that the application's client is running on to handle CLI commands | `8080` | +| `tracks_playback_limit` | the limit for the number of tracks played in a **tracks** playback | `50` | +| `playback_format` | the format of the text in the playback's window | `{status} {track} β€’ {artists} {liked}\n{album}\n{metadata}` | +| `playback_metadata_fields` | list of ordered metadata fields to display in the playback UI's `{metadata}` section. Possible values: `"repeat"`, `"shuffle"`, `"volume"`, `"device"` | `["repeat", "shuffle", "volume", "device"]` | +| `notify_format` | the format of a notification (`notify` feature only) | `{ summary = "{track} β€’ {artists}", body = "{album}" }` | +| `notify_timeout_in_secs` | the timeout (in seconds) of a notification (`notify` feature only) | `0` (no timeout) | +| `player_event_hook_command` | the hook command executed when there is a new player event | `None` | +| `ap_port` | the application's Spotify session connection port | `None` | +| `proxy` | the application's Spotify session connection proxy | `None` | +| `theme` | the application's theme | `default` | +| `app_refresh_duration_in_ms` | the duration (in ms) between two consecutive application refreshes | `32` | +| `playback_refresh_duration_in_ms` | the duration (in ms) between two consecutive playback refreshes | `0` | +| `page_size_in_rows` | a page's size expressed as a number of rows (for page-navigation commands) | `20` | +| `enable_media_control` | enable application media control support (`media-control` feature only) | `true` (Linux), `false` (Windows and MacOS) | +| `enable_streaming` | enable streaming (`streaming` feature only) | `Always` | +| `enable_notify` | enable notification (`notify` feature only) | `true` | +| `enable_cover_image_cache` | store album's cover images in the cache folder | `true` | +| `notify_streaming_only` | only send notification when streaming is enabled (`streaming` and `notify` feature only) | `false` | +| `default_device` | the default device to connect to on startup if no playing device found | `spotify-player` | +| `play_icon` | the icon to indicate playing state of a Spotify item | `β–Ά` | +| `pause_icon` | the icon to indicate pause state of a Spotify item | `β–Œβ–Œ` | +| `liked_icon` | the icon to indicate the liked state of a song | `β™₯` | +| `border_type` | the type of the application's borders | `Plain` | +| `progress_bar_type` | the type of the playback progress bar | `Rectangle` | +| `cover_img_width` | the width of the cover image (`image` feature only) | `5` | +| `cover_img_length` | the length of the cover image (`image` feature only) | `9` | +| `cover_img_scale` | the scale of the cover image (`image` feature only) | `1.0` | +| `seek_duration_secs` | the duration (in seconds) to seek when using `SeekForward` and `SeekBackward` commands | `5` | +| `sort_artist_albums_by_type` | sort albums on artist's pages by type, i.e. album or single | `false` | ### Notes diff --git a/examples/app.toml b/examples/app.toml index 2818b38f..38e561fc 100644 --- a/examples/app.toml +++ b/examples/app.toml @@ -4,6 +4,7 @@ login_redirect_uri = "http://127.0.0.1:8989/login" client_port = 8080 tracks_playback_limit = 50 playback_format = "{status} {track} β€’ {artists}\n{album}\n{metadata}" +playback_metadata_fields = ["repeat", "shuffle", "volume", "device"] notify_format = { summary = "{track} β€’ {artists}", body = "{album}" } notify_timeout_in_secs = 0 app_refresh_duration_in_ms = 32 diff --git a/spotify_player/src/config/mod.rs b/spotify_player/src/config/mod.rs index 3602ebb1..d0ea458a 100644 --- a/spotify_player/src/config/mod.rs +++ b/spotify_player/src/config/mod.rs @@ -60,6 +60,7 @@ pub struct AppConfig { pub player_event_hook_command: Option, pub playback_format: String, + pub playback_metadata_fields: Vec, #[cfg(feature = "notify")] pub notify_format: NotifyFormat, #[cfg(feature = "notify")] @@ -268,6 +269,12 @@ impl Default for AppConfig { playback_format: String::from( "{status} {track} β€’ {artists} {liked}\n{album}\n{metadata}", ), + playback_metadata_fields: vec![ + "repeat".to_string(), + "shuffle".to_string(), + "volume".to_string(), + "device".to_string(), + ], #[cfg(feature = "notify")] notify_format: NotifyFormat { summary: String::from("{track} β€’ {artists}"), diff --git a/spotify_player/src/ui/playback.rs b/spotify_player/src/ui/playback.rs index 55bbf9ba..4069033f 100644 --- a/spotify_player/src/ui/playback.rs +++ b/spotify_player/src/ui/playback.rs @@ -279,24 +279,34 @@ fn construct_playback_text( (episode.show.name.clone(), ui.theme.playback_album()) } }, - "{metadata}" => ( - format!( - "repeat: {} | shuffle: {} | volume: {} | device: {}", - if playback.fake_track_repeat_state { - "track (fake)" - } else { - <&'static str>::from(playback.repeat_state) - }, - playback.shuffle_state, - if let Some(volume) = playback.mute_state { - format!("{volume}% (muted)") - } else { - format!("{}%", playback.volume.unwrap_or_default()) - }, - playback.device_name, - ), - ui.theme.playback_metadata(), - ), + "{metadata}" => { + let repeat_value = if playback.fake_track_repeat_state { + "track (fake)".to_string() + } else { + <&'static str>::from(playback.repeat_state).to_string() + }; + + let volume_value = if let Some(volume) = playback.mute_state { + format!("{volume}% (muted)") + } else { + format!("{}%", playback.volume.unwrap_or_default()) + }; + + let mut parts = vec![]; + + for field in &configs.app_config.playback_metadata_fields { + match field.as_str() { + "repeat" => parts.push(format!("repeat: {repeat_value}")), + "shuffle" => parts.push(format!("shuffle: {}", playback.shuffle_state)), + "volume" => parts.push(format!("volume: {volume_value}")), + "device" => parts.push(format!("device: {}", playback.device_name)), + _ => {} + } + } + + let metadata_str = parts.join(" | "); + (metadata_str, ui.theme.playback_metadata()) + } _ => continue, }; From 7f3c518c5116e77b7bd212a5ad8c3a5bf5da3ac0 Mon Sep 17 00:00:00 2001 From: damageboy <125730+damageboy@users.noreply.github.com> Date: Mon, 28 Jul 2025 00:23:53 +0300 Subject: [PATCH 08/16] Add Support for RTL text in most UI elements (#759) (#760) Resolves #759 - Search results - Playback Metadata/Rect - Main View - Playlist View Before: ![image](https://github.com/user-attachments/assets/1a1dc3f5-b1b9-474d-99bc-f0bf3f777221) After: ![image](https://github.com/user-attachments/assets/a6afa467-ce4c-4b88-9d48-61019faecc7d) --- spotify_player/src/state/model.rs | 40 ++++++++++++++++----------- spotify_player/src/ui/mod.rs | 2 +- spotify_player/src/ui/page.rs | 33 +++++++++++++---------- spotify_player/src/ui/playback.rs | 45 ++++++++++++++++++------------- spotify_player/src/ui/utils.rs | 17 ++++++++++++ 5 files changed, 88 insertions(+), 49 deletions(-) diff --git a/spotify_player/src/state/model.rs b/spotify_player/src/state/model.rs index 73e38ff4..0ef75d80 100644 --- a/spotify_player/src/state/model.rs +++ b/spotify_player/src/state/model.rs @@ -1,3 +1,4 @@ +use crate::ui::utils::to_bidi_string; use crate::utils::map_join; use html_escape::decode_html_entities; pub use rspotify::model::{ @@ -5,8 +6,15 @@ pub use rspotify::model::{ }; use serde::{Deserialize, Serialize}; use std::borrow::Cow; -use std::fmt::Write; -use unicode_bidi::BidiInfo; +use std::fmt::{Display, Write}; + +/// A trait similar to Display but with bidirectional text support +pub trait BidiDisplay: Display { + fn to_bidi_string(&self) -> String { + let disp_str = self.to_string(); + to_bidi_string(&disp_str) + } +} #[derive(Serialize, Clone, Debug)] #[serde(untagged)] @@ -421,6 +429,8 @@ impl std::fmt::Display for Track { } } +impl BidiDisplay for Track {} + impl Album { /// tries to convert from a `rspotify::model::SimplifiedAlbum` into `Album` pub fn try_from_simplified_album(album: rspotify::model::SimplifiedAlbum) -> Option { @@ -493,6 +503,8 @@ impl std::fmt::Display for Album { } } +impl BidiDisplay for Album {} + impl Artist { /// tries to convert from a `rspotify::model::SimplifiedArtist` into `Artist` pub fn try_from_simplified_artist(artist: rspotify::model::SimplifiedArtist) -> Option { @@ -529,6 +541,8 @@ fn from_simplified_artists_to_artists( .collect() } +impl BidiDisplay for Artist {} + impl From for Playlist { fn from(playlist: rspotify::model::SimplifiedPlaylist) -> Self { Self { @@ -574,6 +588,8 @@ impl std::fmt::Display for Playlist { } } +impl BidiDisplay for Playlist {} + impl From for Show { fn from(show: rspotify::model::SimplifiedShow) -> Self { Self { @@ -598,6 +614,8 @@ impl std::fmt::Display for Show { } } +impl BidiDisplay for Show {} + impl From for Episode { fn from(episode: rspotify::model::SimplifiedEpisode) -> Self { Self { @@ -640,6 +658,8 @@ impl std::fmt::Display for PlaylistFolder { } } +impl BidiDisplay for PlaylistFolder {} + impl std::fmt::Display for PlaylistFolderItem { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { match self { @@ -649,6 +669,8 @@ impl std::fmt::Display for PlaylistFolderItem { } } +impl BidiDisplay for PlaylistFolderItem {} + impl From for Category { fn from(c: rspotify::model::category::Category) -> Self { Self { @@ -739,19 +761,7 @@ impl From for Lyrics { l.start_time_ms.parse::().expect("invalid number"), ); - // Some songs use multiple languages and may contain a mix of rtl and ltr text. - // Therefore rtl formatting needs to be done on a per-line basis. - let bidi_info = BidiInfo::new(&l.words, None); - - let words = if bidi_info.has_rtl() && !bidi_info.paragraphs.is_empty() { - bidi_info - .reorder_line(&bidi_info.paragraphs[0], 0..l.words.len()) - .into_owned() - } else { - l.words - }; - - (t, words) + (t, to_bidi_string(&l.words)) }) .collect::>(); lines.sort_by_key(|l| l.0); diff --git a/spotify_player/src/ui/mod.rs b/spotify_player/src/ui/mod.rs index 94e1aa99..ba1b2c49 100644 --- a/spotify_player/src/ui/mod.rs +++ b/spotify_player/src/ui/mod.rs @@ -28,7 +28,7 @@ mod page; mod playback; mod popup; pub mod single_line_input; -mod utils; +pub mod utils; /// Run the application UI pub fn run(state: &SharedState) -> Result<()> { diff --git a/spotify_player/src/ui/page.rs b/spotify_player/src/ui/page.rs index 14e80b9c..9df5722c 100644 --- a/spotify_player/src/ui/page.rs +++ b/spotify_player/src/ui/page.rs @@ -3,10 +3,6 @@ use std::{ fmt::Display, }; -use ratatui::text::Line; - -use crate::{state::Episode, utils::format_duration}; - use super::{ config, utils, utils::construct_and_render_block, Album, Artist, ArtistFocusState, Borders, BrowsePageUIState, Cell, Constraint, Context, ContextPageUIState, DataReadGuard, Frame, Id, @@ -14,6 +10,10 @@ use super::{ PlaylistFolderItem, Rect, Row, SearchFocusState, SharedState, Style, Table, Track, UIStateGuard, }; +use crate::state::BidiDisplay; +use crate::ui::utils::to_bidi_string; +use crate::{state::Episode, utils::format_duration}; +use ratatui::text::Line; const COMMAND_TABLE_CONSTRAINTS: [Constraint; 3] = [ Constraint::Percentage(25), @@ -36,7 +36,10 @@ pub fn render_search_page( rect: Rect, ) { fn search_items(items: &[T]) -> Vec<(String, bool)> { - items.iter().map(|i| (i.to_string(), false)).collect() + items + .iter() + .map(|i| (to_bidi_string(&i.to_string()), false)) + .collect() } // 1. Get data @@ -445,9 +448,9 @@ pub fn render_library_page( .into_iter() .map(|item| match item { PlaylistFolderItem::Playlist(p) => { - (p.to_string(), curr_context_uri == Some(p.id.uri())) + (p.to_bidi_string(), curr_context_uri == Some(p.id.uri())) } - PlaylistFolderItem::Folder(f) => (f.to_string(), false), + PlaylistFolderItem::Folder(f) => (f.to_bidi_string(), false), }) .collect::>(); @@ -463,7 +466,7 @@ pub fn render_library_page( &ui.theme, ui.search_filtered_items(&data.user_data.saved_albums) .into_iter() - .map(|a| (a.to_string(), curr_context_uri == Some(a.id.uri()))) + .map(|a| (a.to_bidi_string(), curr_context_uri == Some(a.id.uri()))) .collect(), is_active && focus_state == LibraryFocusState::SavedAlbums, ); @@ -472,7 +475,7 @@ pub fn render_library_page( &ui.theme, ui.search_filtered_items(&data.user_data.followed_artists) .into_iter() - .map(|a| (a.to_string(), curr_context_uri == Some(a.id.uri()))) + .map(|a| (a.to_bidi_string(), curr_context_uri == Some(a.id.uri()))) .collect(), is_active && focus_state == LibraryFocusState::FollowedArtists, ); @@ -606,8 +609,10 @@ pub fn render_lyrics_page( // 4. Render the page's widgets // render lyric page description text + let bidi_track = to_bidi_string(track); + let bidi_artists = to_bidi_string(artists); frame.render_widget( - Paragraph::new(format!("{track} by {artists}")).style(ui.theme.page_desc()), + Paragraph::new(format!("{bidi_track} by {bidi_artists}")).style(ui.theme.page_desc()), chunks[0], ); @@ -975,9 +980,9 @@ fn render_track_table( Cell::from("") }, Cell::from(id), - Cell::from(t.display_name()), - Cell::from(t.artists_info()), - Cell::from(t.album_info()), + Cell::from(to_bidi_string(&t.display_name())), + Cell::from(to_bidi_string(&t.artists_info())), + Cell::from(to_bidi_string(&t.album_info())), Cell::from(format!( "{}:{:02}", t.duration.as_secs() / 60, @@ -1067,7 +1072,7 @@ fn render_episode_table( }; Row::new(vec![ Cell::from(id), - Cell::from(e.name.clone()), + Cell::from(to_bidi_string(&e.name)), Cell::from(e.release_date.clone()), Cell::from(format!( "{}:{:02}", diff --git a/spotify_player/src/ui/playback.rs b/spotify_player/src/ui/playback.rs index 4069033f..9f63d2a7 100644 --- a/spotify_player/src/ui/playback.rs +++ b/spotify_player/src/ui/playback.rs @@ -1,14 +1,14 @@ -#[cfg(feature = "image")] -use crate::state::ImageRenderInfo; -#[cfg(feature = "image")] -use anyhow::{Context, Result}; -use rspotify::model::Id; - use super::{ config, utils::construct_and_render_block, Borders, Constraint, Frame, Gauge, Layout, Line, LineGauge, Modifier, Paragraph, PlaybackMetadata, Rect, SharedState, Span, Style, Text, UIStateGuard, Wrap, }; +#[cfg(feature = "image")] +use crate::state::ImageRenderInfo; +use crate::ui::utils::to_bidi_string; +#[cfg(feature = "image")] +use anyhow::{Context, Result}; +use rspotify::model::Id; /// Render a playback window showing information about the current playback, which includes /// - track title, artists, album @@ -246,25 +246,31 @@ fn construct_playback_text( }, "{track}" => match playable { rspotify::model::PlayableItem::Track(track) => ( - if track.explicit { - format!("{} (E)", track.name) - } else { - track.name.clone() + { + let bidi_string = to_bidi_string(&track.name); + if track.explicit { + format!("{bidi_string} (E)") + } else { + bidi_string + } }, ui.theme.playback_track(), ), rspotify::model::PlayableItem::Episode(episode) => ( - if episode.explicit { - format!("{} (E)", episode.name) - } else { - episode.name.clone() + { + let bidi_string = to_bidi_string(&episode.name); + if episode.explicit { + format!("{bidi_string} (E)") + } else { + bidi_string + } }, ui.theme.playback_track(), ), }, "{artists}" => match playable { rspotify::model::PlayableItem::Track(track) => ( - crate::utils::map_join(&track.artists, |a| &a.name, ", "), + to_bidi_string(&crate::utils::map_join(&track.artists, |a| &a.name, ", ")), ui.theme.playback_artists(), ), rspotify::model::PlayableItem::Episode(episode) => { @@ -273,11 +279,12 @@ fn construct_playback_text( }, "{album}" => match playable { rspotify::model::PlayableItem::Track(track) => { - (track.album.name.clone(), ui.theme.playback_album()) - } - rspotify::model::PlayableItem::Episode(episode) => { - (episode.show.name.clone(), ui.theme.playback_album()) + (to_bidi_string(&track.album.name), ui.theme.playback_album()) } + rspotify::model::PlayableItem::Episode(episode) => ( + to_bidi_string(&episode.show.name), + ui.theme.playback_album(), + ), }, "{metadata}" => { let repeat_value = if playback.fake_track_repeat_state { diff --git a/spotify_player/src/ui/utils.rs b/spotify_player/src/ui/utils.rs index f04d4182..99ac2d07 100644 --- a/spotify_player/src/ui/utils.rs +++ b/spotify_player/src/ui/utils.rs @@ -2,6 +2,7 @@ use super::{ config, Block, BorderType, Borders, Frame, List, ListItem, ListState, Rect, Span, Style, Table, TableState, }; +use unicode_bidi::BidiInfo; /// Construct and render a block. /// @@ -118,3 +119,19 @@ pub fn render_table_window( adjust_table_state(state, len); frame.render_stateful_widget(widget, rect, state); } + +/// Convert a string to a bidirectional string. +/// Used to handle RTL text properly in the UI. +pub fn to_bidi_string(s: &str) -> String { + let bidi_info = BidiInfo::new(s, None); + + let bidi_string = if bidi_info.has_rtl() && !bidi_info.paragraphs.is_empty() { + bidi_info + .reorder_line(&bidi_info.paragraphs[0], 0..s.len()) + .into_owned() + } else { + s.to_string() + }; + + bidi_string +} From 2dd4c16441dc1fcef9d07eed98d2536c9e941aee Mon Sep 17 00:00:00 2001 From: Thang Pham Date: Sun, 27 Jul 2025 18:37:50 -0400 Subject: [PATCH 09/16] Pre-release `v0.20.7` (#780) * update dependencies * update logic to generate description for track table --- Cargo.lock | 407 ++++++++++++++++++------------ lyric_finder/Cargo.toml | 4 +- spotify_player/Cargo.toml | 30 +-- spotify_player/src/event/popup.rs | 6 +- spotify_player/src/state/model.rs | 38 ++- spotify_player/src/ui/page.rs | 26 +- 6 files changed, 295 insertions(+), 216 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 04dbb18b..75156e9c 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -4,9 +4,9 @@ version = 4 [[package]] name = "ab_glyph" -version = "0.2.29" +version = "0.2.31" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ec3672c180e71eeaaac3a541fbbc5f5ad4def8b747c595ad30d674e43049f7b0" +checksum = "e074464580a518d16a7126262fffaaa47af89d4099d4cb403f8ed938ba12ee7d" dependencies = [ "ab_glyph_rasterizer", "owned_ttf_parser", @@ -14,9 +14,9 @@ dependencies = [ [[package]] name = "ab_glyph_rasterizer" -version = "0.1.8" +version = "0.1.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c71b1793ee61086797f5c80b6efa2b8ffa6d5dd703f118545808a7f2e27f7046" +checksum = "b2187590a23ab1e3df8681afdf0987c48504d80291f002fcdb651f0ef5e25169" [[package]] name = "addr2line" @@ -312,18 +312,18 @@ dependencies = [ [[package]] name = "avif-serialize" -version = "0.8.3" +version = "0.8.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "98922d6a4cfbcb08820c69d8eeccc05bb1f29bfa06b4f5b1dbfe9a868bd7608e" +checksum = "2ea8ef51aced2b9191c08197f55450d830876d9933f8f48a429b354f1d496b42" dependencies = [ "arrayvec", ] [[package]] name = "aws-lc-rs" -version = "1.13.1" +version = "1.13.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "93fcc8f365936c834db5514fc45aee5b1202d677e6b40e48468aaaa8183ca8c7" +checksum = "5c953fe1ba023e6b7730c0d4b031d06f267f23a46167dcbd40316644b10a17ba" dependencies = [ "aws-lc-sys", "zeroize", @@ -331,9 +331,9 @@ dependencies = [ [[package]] name = "aws-lc-sys" -version = "0.29.0" +version = "0.30.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "61b1d86e7705efe1be1b569bab41d4fa1e14e220b60a160f78de2db687add079" +checksum = "dbfd150b5dbdb988bcc8fb1fe787eb6b7ee6180ca24da683b61ea5405f3d43ff" dependencies = [ "bindgen 0.69.5", "cc", @@ -487,9 +487,9 @@ checksum = "56ed6191a7e78c36abdb16ab65341eefd73d64d303fffccdbb00d51e4205967b" [[package]] name = "bumpalo" -version = "3.18.1" +version = "3.19.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "793db76d6187cd04dff33004d8e6c9cc4e05cd330500379d2394209271b4aeee" +checksum = "46c5e41b57b8bba42a04676d81cb89e9ee8e859a1a66f80a5a72e1cb76b34d43" [[package]] name = "bytemuck" @@ -549,18 +549,18 @@ checksum = "df8670b8c7b9dae1793364eafadf7239c40d669904660c5960d74cfd80b46a53" [[package]] name = "castaway" -version = "0.2.3" +version = "0.2.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0abae9be0aaf9ea96a3b1b8b1b55c602ca751eba1b1500220cea4ecbafe7c0d5" +checksum = "dec551ab6e7578819132c713a93c022a05d60159dc86e7a7050223577484c55a" dependencies = [ "rustversion", ] [[package]] name = "cc" -version = "1.2.27" +version = "1.2.30" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d487aa071b5f64da6f19a3e848e3578944b726ee5a4854b82172f02aa876bfdc" +checksum = "deec109607ca693028562ed836a5f1c4b8bd77755c4e132fc5ce11b0b6211ae7" dependencies = [ "jobserver", "libc", @@ -594,9 +594,9 @@ dependencies = [ [[package]] name = "cfg-expr" -version = "0.20.0" +version = "0.20.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e34e221e91c7eb5e8315b5c9cf1a61670938c0626451f954a51693ed44b37f45" +checksum = "0d0390889d58f934f01cd49736275b4c2da15bcfc328c78ff2349907e6cabf22" dependencies = [ "smallvec", "target-lexicon 0.13.2", @@ -652,9 +652,9 @@ dependencies = [ [[package]] name = "clap" -version = "4.5.40" +version = "4.5.41" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "40b6887a1d8685cebccf115538db5c0efe625ccac9696ad45c409d96566e910f" +checksum = "be92d32e80243a54711e5d7ce823c35c41c9d929dc4ab58e1276f625841aadf9" dependencies = [ "clap_builder", "clap_derive", @@ -662,9 +662,9 @@ dependencies = [ [[package]] name = "clap_builder" -version = "4.5.40" +version = "4.5.41" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e0c66c08ce9f0c698cbce5c0279d0bb6ac936d8674174fe48f736533b964f59e" +checksum = "707eab41e9622f9139419d573eca0900137718000c517d47da73045f54331c3d" dependencies = [ "anstream", "anstyle", @@ -674,18 +674,18 @@ dependencies = [ [[package]] name = "clap_complete" -version = "4.5.54" +version = "4.5.55" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "aad5b1b4de04fead402672b48897030eec1f3bfe1550776322f59f6d6e6a5677" +checksum = "a5abde44486daf70c5be8b8f8f1b66c49f86236edf6fa2abadb4d961c4c6229a" dependencies = [ "clap", ] [[package]] name = "clap_derive" -version = "4.5.40" +version = "4.5.41" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d2c7947ae4cc3d851207c1adb5b5e260ff0cca11446b1d6d1423788e442257ce" +checksum = "ef4f52386a59ca4c860f7393bcf8abd8dfd91ecccc0f774635ff68e92eeef491" dependencies = [ "heck", "proc-macro2", @@ -701,9 +701,9 @@ checksum = "b94f61472cee1439c0b966b47e3aca9ae07e45d070759512cd390ea2bebc6675" [[package]] name = "clipboard-win" -version = "5.4.0" +version = "5.4.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "15efe7a882b08f34e38556b14f2fb3daa98769d06c7f0c1b076dfd0d983bc892" +checksum = "bde03770d3df201d4fb868f2c9c59e66a3e4e2bd06692a0fe701e7103c7e84d4" dependencies = [ "error-code", ] @@ -794,14 +794,14 @@ dependencies = [ [[package]] name = "config_parser2" -version = "0.1.5" +version = "0.1.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "19860ead0ab4f28e269696d51d517b8d8560334f60aa2a4bcfaf96fc50b16327" +checksum = "82ee1270e89fd879f8ad252cd8c7e8a0b13193b7c417b4267d20f57a30bb2b0f" dependencies = [ "anyhow", "config_parser_derive", "serde", - "toml", + "toml 0.9.2", ] [[package]] @@ -960,9 +960,9 @@ dependencies = [ [[package]] name = "crc32fast" -version = "1.4.2" +version = "1.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a97769d94ddab943e4510d138150169a2758b5ef3eb191a9ee688de3e23ef7b3" +checksum = "9481c1c90cbf2ac953f07c8d4a58aa3945c425b7185c9154d67a65e4230da511" dependencies = [ "cfg-if", ] @@ -1020,7 +1020,7 @@ dependencies = [ "document-features", "mio", "parking_lot", - "rustix 1.0.7", + "rustix 1.0.8", "signal-hook", "signal-hook-mio", "winapi", @@ -1037,9 +1037,9 @@ dependencies = [ [[package]] name = "crunchy" -version = "0.2.3" +version = "0.2.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "43da5946c66ffcc7745f48db692ffbb10a83bfe0afd96235c5c2a4fb23994929" +checksum = "460fbee9c2c2f33933d720630a6a0bac33ba7053db5344fac858d4b8952d77d5" [[package]] name = "crypto-common" @@ -1280,7 +1280,7 @@ version = "0.5.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "330c60081dcc4c72131f8eb70510f1ac07223e5d4163db481a04a0befcffa412" dependencies = [ - "libloading 0.7.4", + "libloading 0.8.8", ] [[package]] @@ -1713,9 +1713,9 @@ dependencies = [ [[package]] name = "gif" -version = "0.13.2" +version = "0.13.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fcc37f9a2bfe731e69f1e08d29d91d30604b9ce24bcb2880a961e82d89c6ed89" +checksum = "4ae047235e33e2829703574b54fdec96bfbad892062d97fed2f76022287de61b" dependencies = [ "color_quant", "weezl", @@ -1943,9 +1943,9 @@ dependencies = [ [[package]] name = "h2" -version = "0.3.26" +version = "0.3.27" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "81fe527a889e1532da5c525686d96d4c2e74cdd345badf8dfef9f6b39dd5f5e8" +checksum = "0beca50380b1fc32983fc1cb4587bfa4bb9e78fc259aad4a0032d2080309222d" dependencies = [ "bytes", "fnv", @@ -1962,9 +1962,9 @@ dependencies = [ [[package]] name = "h2" -version = "0.4.10" +version = "0.4.11" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a9421a676d1b147b16b82c9225157dc629087ef8ec4d5e2960f9437a90dac0a5" +checksum = "17da50a276f1e01e0ba6c029e47b7100754904ee8a278f886546e98575380785" dependencies = [ "atomic-waker", "bytes", @@ -2155,14 +2155,14 @@ dependencies = [ "futures-channel", "futures-core", "futures-util", - "h2 0.3.26", + "h2 0.3.27", "http 0.2.12", "http-body 0.4.6", "httparse", "httpdate", "itoa", "pin-project-lite", - "socket2", + "socket2 0.5.10", "tokio", "tower-service", "tracing", @@ -2178,7 +2178,7 @@ dependencies = [ "bytes", "futures-channel", "futures-util", - "h2 0.4.10", + "h2 0.4.11", "http 1.3.1", "http-body 1.0.1", "httparse", @@ -2253,13 +2253,13 @@ dependencies = [ "hyper 1.6.0", "hyper-util", "log", - "rustls 0.23.28", + "rustls 0.23.30", "rustls-native-certs 0.8.1", "rustls-pki-types", "tokio", "tokio-rustls 0.26.2", "tower-service", - "webpki-roots 1.0.1", + "webpki-roots 1.0.2", ] [[package]] @@ -2280,9 +2280,9 @@ dependencies = [ [[package]] name = "hyper-util" -version = "0.1.14" +version = "0.1.16" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "dc2fdfdbff08affe55bb779f33b053aa1fe5dd5b54c257343c17edfa55711bdb" +checksum = "8d9b05277c7e8da2c93a568989bb6207bef0112e8d17df7a6eda4a3cf143bc5e" dependencies = [ "base64 0.22.1", "bytes", @@ -2296,7 +2296,7 @@ dependencies = [ "libc", "percent-encoding", "pin-project-lite", - "socket2", + "socket2 0.6.0", "system-configuration 0.6.1", "tokio", "tower-service", @@ -2482,9 +2482,9 @@ checksum = "d0263a3d970d5c054ed9312c0057b4f3bde9c0b33836d3637361d4a9e6e7a408" [[package]] name = "indexmap" -version = "2.9.0" +version = "2.10.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cea70ddb795996207ad57735b50c5982d8844f38ba9ee5f1aedcfb708a2aa11e" +checksum = "fe4cd85333e22411419a0bcae1297d25e58c9443848b11dc6a86fefe8c78a661" dependencies = [ "equivalent", "hashbrown", @@ -2507,9 +2507,9 @@ dependencies = [ [[package]] name = "instability" -version = "0.3.7" +version = "0.3.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0bf9fed6d91cfb734e7476a06bde8300a1b94e217e1b523b6f0cd1a01998c71d" +checksum = "435d80800b936787d62688c927b6490e887c7ef5ff9ce922c6c6050fca75eb9a" dependencies = [ "darling", "indoc", @@ -2529,6 +2529,17 @@ dependencies = [ "syn", ] +[[package]] +name = "io-uring" +version = "0.7.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d93587f37623a1a17d94ef2bc9ada592f5465fe7732084ab7beefabe5c77c0c4" +dependencies = [ + "bitflags 2.9.1", + "cfg-if", + "libc", +] + [[package]] name = "ipnet" version = "2.11.0" @@ -2697,9 +2708,9 @@ dependencies = [ [[package]] name = "libfuzzer-sys" -version = "0.4.9" +version = "0.4.10" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cf78f52d400cf2d84a3a973a78a592b4adc535739e0a5597a0da6f0c357adc75" +checksum = "5037190e1f70cbeef565bd267599242926f724d3b8a9f510fd7e0b540cfa4404" dependencies = [ "arbitrary", "cc", @@ -2781,13 +2792,13 @@ dependencies = [ [[package]] name = "libredox" -version = "0.1.3" +version = "0.1.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c0ff37bd590ca25063e35af745c343cb7a0271906fb7b37e4813e8f79f00268d" +checksum = "4488594b9328dee448adb906d8b126d9b7deb7cf5c22161ee591610bb1be83c0" dependencies = [ "bitflags 2.9.1", "libc", - "redox_syscall 0.5.13", + "redox_syscall 0.5.16", ] [[package]] @@ -2986,9 +2997,9 @@ checksum = "241eaef5fd12c88705a01fc1066c48c4b36e0dd4377dcdc7ec3942cea7a69956" [[package]] name = "litrs" -version = "0.4.1" +version = "0.4.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b4ce301924b7887e9d637144fdade93f9dfff9b60981d4ac161db09720d39aa5" +checksum = "f5e54036fe321fd421e10d732f155734c4e4afd610dd556d9a82833ab3ee0bed" [[package]] name = "lock_api" @@ -3039,7 +3050,7 @@ dependencies = [ "html5ever", "log", "markup5ever_rcdom", - "reqwest 0.12.20", + "reqwest 0.12.22", "serde", "tokio", ] @@ -3052,9 +3063,9 @@ checksum = "c41e0c4fef86961ac6d6f8a82609f55f31b05e4fce149ac5710e439df7619ba4" [[package]] name = "mac-notification-sys" -version = "0.6.4" +version = "0.6.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0b95dfb34071d1592b45622bf93e315e3a72d414b6782aca9a015c12bec367ef" +checksum = "119c8490084af61b44c9eda9d626475847a186737c0378c85e32d77c33a01cd4" dependencies = [ "cc", "objc2 0.6.1", @@ -3150,9 +3161,9 @@ checksum = "32a282da65faaf38286cf3be983213fcf1d2e2a58700e808f83f4ea9a4804bc0" [[package]] name = "memmap2" -version = "0.9.5" +version = "0.9.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fd3f7eed9d3848f8b98834af67102b720745c4ec028fcd0aa0239277e7de374f" +checksum = "483758ad303d734cec05e5c12b41d7e93e6a6390c5e9dae6bdeb7c1259012d28" dependencies = [ "libc", ] @@ -3838,9 +3849,9 @@ checksum = "b15813163c1d831bf4a13c3610c05c0d03b39feb07f7e09fa234dac9b15aaf39" [[package]] name = "owned_ttf_parser" -version = "0.25.0" +version = "0.25.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "22ec719bbf3b2a81c109a4e20b1f129b5566b7dce654bc3872f6a05abf82b2c4" +checksum = "36820e9051aca1014ddc75770aab4d68bc1e9e632f0f5627c4086bc216fb583b" dependencies = [ "ttf-parser", ] @@ -3871,7 +3882,7 @@ dependencies = [ "cfg-if", "libc", "petgraph", - "redox_syscall 0.5.13", + "redox_syscall 0.5.16", "smallvec", "thread-id", "windows-targets 0.52.6", @@ -4030,17 +4041,16 @@ dependencies = [ [[package]] name = "polling" -version = "3.8.0" +version = "3.9.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b53a684391ad002dd6a596ceb6c74fd004fdce75f4be2e3f615068abbea5fd50" +checksum = "8ee9b2fa7a4517d2c91ff5bc6c297a427a96749d15f98fcdbb22c05571a4d4b7" dependencies = [ "cfg-if", "concurrent-queue", "hermit-abi", "pin-project-lite", - "rustix 1.0.7", - "tracing", - "windows-sys 0.59.0", + "rustix 1.0.8", + "windows-sys 0.60.2", ] [[package]] @@ -4102,9 +4112,9 @@ checksum = "925383efa346730478fb4838dbe9137d2a47675ad789c546d150a6e1dd4ab31c" [[package]] name = "prettyplease" -version = "0.2.35" +version = "0.2.36" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "061c1221631e079b26479d25bbf2275bfe5917ae8419cd7e34f13bfc2aa7539a" +checksum = "ff24dfcda44452b9816fff4cd4227e1bb73ff5a2f1bc1105aa92fb8565ce44d2" dependencies = [ "proc-macro2", "syn", @@ -4255,8 +4265,8 @@ dependencies = [ "quinn-proto", "quinn-udp", "rustc-hash 2.1.1", - "rustls 0.23.28", - "socket2", + "rustls 0.23.30", + "socket2 0.5.10", "thiserror 2.0.12", "tokio", "tracing", @@ -4272,10 +4282,10 @@ dependencies = [ "bytes", "getrandom 0.3.3", "lru-slab", - "rand 0.9.1", + "rand 0.9.2", "ring", "rustc-hash 2.1.1", - "rustls 0.23.28", + "rustls 0.23.30", "rustls-pki-types", "slab", "thiserror 2.0.12", @@ -4293,7 +4303,7 @@ dependencies = [ "cfg_aliases", "libc", "once_cell", - "socket2", + "socket2 0.5.10", "tracing", "windows-sys 0.59.0", ] @@ -4326,9 +4336,9 @@ dependencies = [ [[package]] name = "rand" -version = "0.9.1" +version = "0.9.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9fbfd9d094a40bf3ae768db9361049ace4c0e04a4fd6b359518bd7b73a73dd97" +checksum = "6db2770f06117d490610c7488547d543617b21bfa07796d7a12f6f1bd53850d1" dependencies = [ "rand_chacha 0.9.0", "rand_core 0.9.3", @@ -4397,7 +4407,7 @@ dependencies = [ "itertools 0.13.0", "lru", "paste", - "strum", + "strum 0.26.3", "unicode-segmentation", "unicode-truncate", "unicode-width 0.2.0", @@ -4490,9 +4500,9 @@ dependencies = [ [[package]] name = "redox_syscall" -version = "0.5.13" +version = "0.5.16" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0d04b7d0ee6b4a0207a0a7adb104d23ecb0b47d6beae7152d0fa34b692b29fd6" +checksum = "7251471db004e509f4e75a62cca9435365b5ec7bcdff530d612ac7c87c44a792" dependencies = [ "bitflags 2.9.1", ] @@ -4563,7 +4573,7 @@ dependencies = [ "encoding_rs", "futures-core", "futures-util", - "h2 0.3.26", + "h2 0.3.27", "http 0.2.12", "http-body 0.4.6", "hyper 0.14.32", @@ -4595,15 +4605,15 @@ dependencies = [ [[package]] name = "reqwest" -version = "0.12.20" +version = "0.12.22" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "eabf4c97d9130e2bf606614eb937e86edac8292eaa6f422f995d7e8de1eb1813" +checksum = "cbc931937e6ca3a06e3b6c0aa7841849b160a90351d6ab467a8b9b9959767531" dependencies = [ "base64 0.22.1", "bytes", "encoding_rs", "futures-core", - "h2 0.4.10", + "h2 0.4.11", "http 1.3.1", "http-body 1.0.1", "http-body-util", @@ -4618,7 +4628,7 @@ dependencies = [ "percent-encoding", "pin-project-lite", "quinn", - "rustls 0.23.28", + "rustls 0.23.30", "rustls-pki-types", "serde", "serde_json", @@ -4634,14 +4644,14 @@ dependencies = [ "wasm-bindgen", "wasm-bindgen-futures", "web-sys", - "webpki-roots 1.0.1", + "webpki-roots 1.0.2", ] [[package]] name = "rgb" -version = "0.8.50" +version = "0.8.52" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "57397d16646700483b67d2dd6511d79318f9d057fdbd21a4066aeac8b41d310a" +checksum = "0c6a884d2998352bb4daf0183589aec883f16a6da1f4dde84d8e2e9a5409a1ce" dependencies = [ "bytemuck", ] @@ -4692,9 +4702,9 @@ dependencies = [ [[package]] name = "rspotify" -version = "0.14.0" +version = "0.15.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "97e4f3254b449534ab3891331881d922d50ec36bd07c155147253a747fa5d475" +checksum = "77beedc33ecff4c39e8ef0e6f7ebc8d849f3ffebbeb786f9997d96f0d9cf4017" dependencies = [ "async-stream", "async-trait", @@ -4716,43 +4726,43 @@ dependencies = [ [[package]] name = "rspotify-http" -version = "0.14.0" +version = "0.15.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ed8a6b6d3cfea3040a2436e02366e5cb8d84f7658667be7075f6ed1cc64360da" +checksum = "fde1ea9e2a49698cffbc994a83f5f909b37736c31cccb202f9577e8a32df3a63" dependencies = [ "async-trait", "log", "maybe-async", - "reqwest 0.12.20", + "reqwest 0.12.22", "serde_json", "thiserror 2.0.12", ] [[package]] name = "rspotify-macros" -version = "0.14.0" +version = "0.15.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "559fad82b639297c093c5cc8ef406001dd0cb55cd9f5125a8fb40310e38b95d9" +checksum = "ee3dfb51ee54bd754ad76e96ad60a3b64bc70ae33a89261d9dbabc4c148a496f" [[package]] name = "rspotify-model" -version = "0.14.0" +version = "0.15.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8cd00345ab89d2dd8059f0d0c168a6b0f858099795d7e318554411303b827d95" +checksum = "018f29a6a8c47cfe7923c48140ed546a395f660c7af05b73e6001d4505f89c8d" dependencies = [ "chrono", "enum_dispatch", "serde", "serde_json", - "strum", + "strum 0.27.2", "thiserror 2.0.12", ] [[package]] name = "rustc-demangle" -version = "0.1.25" +version = "0.1.26" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "989e6739f80c4ad5b13e0fd7fe89531180375b18520cc8c82080e4dc4035b84f" +checksum = "56f7d92ca342cea22a06f2121d944b4fd82af56988c270852495420f961d4ace" [[package]] name = "rustc-hash" @@ -4781,15 +4791,15 @@ dependencies = [ [[package]] name = "rustix" -version = "1.0.7" +version = "1.0.8" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c71e83d6afe7ff64890ec6b71d6a69bb8a610ab78ce364b3352876bb4c801266" +checksum = "11181fbabf243db407ef8df94a6ce0b2f9a733bd8be4ad02b4eda9602296cac8" dependencies = [ "bitflags 2.9.1", "errno", "libc", "linux-raw-sys 0.9.4", - "windows-sys 0.59.0", + "windows-sys 0.60.2", ] [[package]] @@ -4820,16 +4830,16 @@ dependencies = [ [[package]] name = "rustls" -version = "0.23.28" +version = "0.23.30" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7160e3e10bf4535308537f3c4e1641468cd0e485175d6163087c0393c7d46643" +checksum = "069a8df149a16b1a12dcc31497c3396a173844be3cac4bd40c9e7671fef96671" dependencies = [ "aws-lc-rs", "log", "once_cell", "ring", "rustls-pki-types", - "rustls-webpki 0.103.3", + "rustls-webpki 0.103.4", "subtle", "zeroize", ] @@ -4910,9 +4920,9 @@ dependencies = [ [[package]] name = "rustls-webpki" -version = "0.103.3" +version = "0.103.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e4a72fe2bcf7a6ac6fd7d0b9e5cb68aeb7d4c0a0271730218b3e92d43b4eb435" +checksum = "0a17884ae0c1b773f1ccd2bd4a8c72f16da897310a98b0e84bf349ad5ead92fc" dependencies = [ "aws-lc-rs", "ring", @@ -5066,9 +5076,9 @@ dependencies = [ [[package]] name = "serde_json" -version = "1.0.140" +version = "1.0.141" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "20068b6e96dc6c9bd23e01df8827e6c7e1f2fddd43c21810382803c136b99373" +checksum = "30b9eff21ebe718216c6ec64e1d9ac57087aad11efc64e32002bce4a0d4c03d3" dependencies = [ "itoa", "memchr", @@ -5095,6 +5105,15 @@ dependencies = [ "serde", ] +[[package]] +name = "serde_spanned" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "40734c41988f7306bb04f0ecf60ec0f3f1caa34290e4e8ea471dcd3346483b83" +dependencies = [ + "serde", +] + [[package]] name = "serde_urlencoded" version = "0.7.1" @@ -5294,11 +5313,21 @@ dependencies = [ "windows-sys 0.52.0", ] +[[package]] +name = "socket2" +version = "0.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "233504af464074f9d066d7b5416c5f9b894a5862a6506e306f7b816cdd6f1807" +dependencies = [ + "libc", + "windows-sys 0.59.0", +] + [[package]] name = "souvlaki" -version = "0.8.2" +version = "0.8.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ff1bb90fced3a4a752db0ab08065ca919f37880fa1c7c136258bdd4c520e5c3e" +checksum = "5855c8f31521af07d896b852eaa9eca974ddd3211fc2ae292e58dda8eb129bc8" dependencies = [ "base64 0.22.1", "block", @@ -5342,7 +5371,7 @@ dependencies = [ [[package]] name = "spotify_player" -version = "0.20.6" +version = "0.20.7" dependencies = [ "anyhow", "async-trait", @@ -5368,23 +5397,23 @@ dependencies = [ "maybe-async", "notify-rust", "parking_lot", - "rand 0.9.1", + "rand 0.9.2", "ratatui", "regex", - "reqwest 0.12.20", + "reqwest 0.12.22", "rspotify", - "rustls 0.23.28", + "rustls 0.23.30", "serde", "serde_json", "souvlaki", "tokio", - "toml", + "toml 0.9.2", "tracing", "tracing-subscriber", "ttl_cache", "unicode-bidi", "viuer", - "which 7.0.3", + "which 8.0.0", "windows 0.58.0", "winit", ] @@ -5444,7 +5473,16 @@ version = "0.26.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8fec0f0aef304996cf250b31b5a10dee7980c85da9d759361292b8bca5a18f06" dependencies = [ - "strum_macros", + "strum_macros 0.26.4", +] + +[[package]] +name = "strum" +version = "0.27.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "af23d6f6c1a224baef9d3f61e287d2761385a5b88fdab4eb4c6f11aeb54c4bcf" +dependencies = [ + "strum_macros 0.27.2", ] [[package]] @@ -5460,6 +5498,18 @@ dependencies = [ "syn", ] +[[package]] +name = "strum_macros" +version = "0.27.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7695ce3845ea4b33927c055a39dc438a45b059f7c1b3d91d38d10355fb8cbca7" +dependencies = [ + "heck", + "proc-macro2", + "quote", + "syn", +] + [[package]] name = "subtle" version = "2.6.1" @@ -5651,7 +5701,7 @@ dependencies = [ "cfg-expr 0.15.8", "heck", "pkg-config", - "toml", + "toml 0.8.23", "version-compare 0.2.0", ] @@ -5661,10 +5711,10 @@ version = "7.0.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e4be53aa0cba896d2dc615bd42bbc130acdcffa239e0a2d965ea5b3b2a86ffdb" dependencies = [ - "cfg-expr 0.20.0", + "cfg-expr 0.20.1", "heck", "pkg-config", - "toml", + "toml 0.8.23", "version-compare 0.2.0", ] @@ -5701,7 +5751,7 @@ dependencies = [ "fastrand", "getrandom 0.3.3", "once_cell", - "rustix 1.0.7", + "rustix 1.0.8", "windows-sys 0.59.0", ] @@ -5880,19 +5930,21 @@ checksum = "1f3ccbac311fea05f86f61904b462b55fb3df8837a366dfc601a0161d0532f20" [[package]] name = "tokio" -version = "1.45.1" +version = "1.47.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "75ef51a33ef1da925cea3e4eb122833cb377c61439ca401b770f54902b806779" +checksum = "43864ed400b6043a4757a25c7a64a8efde741aed79a056a2fb348a406701bb35" dependencies = [ "backtrace", "bytes", + "io-uring", "libc", "mio", "parking_lot", "pin-project-lite", - "socket2", + "slab", + "socket2 0.6.0", "tokio-macros", - "windows-sys 0.52.0", + "windows-sys 0.59.0", ] [[package]] @@ -5943,7 +5995,7 @@ version = "0.26.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8e727b36a1a0e8b74c376ac2211e40c2c8af09fb4013c60d910495810f008e9b" dependencies = [ - "rustls 0.23.28", + "rustls 0.23.30", "tokio", ] @@ -5966,7 +6018,7 @@ checksum = "edc5f74e248dc973e0dbb7b74c7e0d6fcc301c694ff50049504004ef4d0cdcd9" dependencies = [ "futures-util", "log", - "rustls 0.23.28", + "rustls 0.23.30", "rustls-native-certs 0.8.1", "rustls-pki-types", "tokio", @@ -5994,11 +6046,26 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "dc1beb996b9d83529a9e75c17a1686767d148d70663143c7854d8b4a09ced362" dependencies = [ "serde", - "serde_spanned", - "toml_datetime", + "serde_spanned 0.6.9", + "toml_datetime 0.6.11", "toml_edit", ] +[[package]] +name = "toml" +version = "0.9.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ed0aee96c12fa71097902e0bb061a5e1ebd766a6636bb605ba401c45c1650eac" +dependencies = [ + "indexmap", + "serde", + "serde_spanned 1.0.0", + "toml_datetime 0.7.0", + "toml_parser", + "toml_writer", + "winnow", +] + [[package]] name = "toml_datetime" version = "0.6.11" @@ -6008,6 +6075,15 @@ dependencies = [ "serde", ] +[[package]] +name = "toml_datetime" +version = "0.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bade1c3e902f58d73d3f294cd7f20391c1cb2fbcb643b73566bc773971df91e3" +dependencies = [ + "serde", +] + [[package]] name = "toml_edit" version = "0.22.27" @@ -6016,17 +6092,25 @@ checksum = "41fe8c660ae4257887cf66394862d21dbca4a6ddd26f04a3560410406a2f819a" dependencies = [ "indexmap", "serde", - "serde_spanned", - "toml_datetime", - "toml_write", + "serde_spanned 0.6.9", + "toml_datetime 0.6.11", "winnow", ] [[package]] -name = "toml_write" -version = "0.1.2" +name = "toml_parser" +version = "1.0.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5d99f8c9a7727884afe522e9bd5edbfc91a3312b36a77b5fb8926e4c31a41801" +checksum = "97200572db069e74c512a14117b296ba0a80a30123fbbb5aa1f4a348f639ca30" +dependencies = [ + "winnow", +] + +[[package]] +name = "toml_writer" +version = "1.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fcc842091f2def52017664b53082ecbbeb5c7731092bad69d2c63050401dfd64" [[package]] name = "tower" @@ -6168,7 +6252,7 @@ dependencies = [ "httparse", "log", "rand 0.8.5", - "rustls 0.23.28", + "rustls 0.23.30", "rustls-pki-types", "sha1", "thiserror 1.0.69", @@ -6271,7 +6355,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3cf4199d1e5d15ddd86a694e4d0dffa9c323ce759fea589f00fef9d81cc1931d" dependencies = [ "getrandom 0.3.3", - "rand 0.9.1", + "rand 0.9.2", ] [[package]] @@ -6622,9 +6706,9 @@ checksum = "5f20c57d8d7db6d3b86154206ae5d8fba62dd39573114de97c2cb0578251f8e1" [[package]] name = "webpki-roots" -version = "1.0.1" +version = "1.0.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8782dd5a41a24eed3a4f40b606249b3e236ca61adf1f25ea4d45c73de122b502" +checksum = "7e8983c3ab33d6fb807cfcdad2491c4ea8cbc8ed839181c7dfd9c67c83e261b2" dependencies = [ "rustls-pki-types", ] @@ -6649,13 +6733,12 @@ dependencies = [ [[package]] name = "which" -version = "7.0.3" +version = "8.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "24d643ce3fd3e5b54854602a080f34fb10ab75e0b813ee32d00ca2b44fa74762" +checksum = "d3fabb953106c3c8eea8306e4393700d7657561cb43122571b172bbfb7c7ba1d" dependencies = [ - "either", "env_home", - "rustix 1.0.7", + "rustix 1.0.8", "winsafe", ] @@ -6894,9 +6977,9 @@ dependencies = [ [[package]] name = "windows-registry" -version = "0.5.2" +version = "0.5.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b3bab093bdd303a1240bb99b8aba8ea8a69ee19d34c9e2ef9594e708a4878820" +checksum = "5b8a9ed28765efc97bbc954883f4e6796c33a06546ebafacbabee9696967499e" dependencies = [ "windows-link", "windows-result 0.3.4", @@ -7256,9 +7339,9 @@ checksum = "271414315aff87387382ec3d271b52d7ae78726f5d44ac98b4f4030c91880486" [[package]] name = "winit" -version = "0.30.11" +version = "0.30.12" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a4409c10174df8779dc29a4788cac85ed84024ccbc1743b776b21a520ee1aaf4" +checksum = "c66d4b9ed69c4009f6321f762d6e61ad8a2389cd431b97cb1e146812e9e6c732" dependencies = [ "ahash", "android-activity", @@ -7308,9 +7391,9 @@ dependencies = [ [[package]] name = "winnow" -version = "0.7.11" +version = "0.7.12" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "74c7b26e3480b707944fc872477815d29a8e429d2f93a1ce000f5fa84a15cbcd" +checksum = "f3edebf492c8125044983378ecb5766203ad3b4c2f7a922bd7dd207f6d443e95" dependencies = [ "memchr", ] @@ -7380,9 +7463,9 @@ checksum = "ec107c4503ea0b4a98ef47356329af139c0a4f7750e621cf2973cd3385ebcb3d" [[package]] name = "xcursor" -version = "0.3.9" +version = "0.3.10" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "635887f4315a33cb714eb059bdbd7c1c92bfa71bc5b9d5115460502f788c2ab5" +checksum = "bec9e4a500ca8864c5b47b8b482a73d62e4237670e5b5f1d6b9e3cae50f28f2b" [[package]] name = "xkbcommon-dl" @@ -7556,9 +7639,9 @@ dependencies = [ [[package]] name = "zune-jpeg" -version = "0.4.18" +version = "0.4.19" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7384255a918371b5af158218d131530f694de9ad3815ebdd0453a940485cb0fa" +checksum = "2c9e525af0a6a658e031e95f14b7f889976b74a11ba0eca5a5fc9ac8a1c43a6a" dependencies = [ "zune-core", ] diff --git a/lyric_finder/Cargo.toml b/lyric_finder/Cargo.toml index 8105a400..bf47c037 100644 --- a/lyric_finder/Cargo.toml +++ b/lyric_finder/Cargo.toml @@ -14,7 +14,7 @@ rustls-tls = ["reqwest/rustls-tls"] native-tls = ["reqwest/native-tls"] [dependencies] -reqwest = { version = "0.12.20", features = ["json", "native-tls-alpn", "http2"], default-features = false } +reqwest = { version = "0.12.22", features = ["json", "native-tls-alpn", "http2"], default-features = false } anyhow = "1.0.98" serde = { version = "1.0.219", features = ["derive"] } html5ever = "=0.27.0" @@ -22,7 +22,7 @@ markup5ever_rcdom = "0.3.0" log = "0.4.27" [dev-dependencies] -tokio = { version = "1.45.1", features = ["rt", "rt-multi-thread", "macros"] } +tokio = { version = "1.47.0", features = ["rt", "rt-multi-thread", "macros"] } env_logger = { version = "0.11.8", default-features = false } [[example]] diff --git a/spotify_player/Cargo.toml b/spotify_player/Cargo.toml index 443ebfaf..8d0cba15 100644 --- a/spotify_player/Cargo.toml +++ b/spotify_player/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "spotify_player" -version = "0.20.6" +version = "0.20.7" authors = ["Thang Pham "] edition = "2021" license = "MIT" @@ -11,8 +11,8 @@ readme = "../README.md" [dependencies] anyhow = "1.0.98" -clap = { version = "4.5.40", features = ["derive", "string"] } -config_parser2 = "0.1.5" +clap = { version = "4.5.41", features = ["derive", "string"] } +config_parser2 = "0.1.6" crossterm = "0.29.0" dirs-next = "2.0.0" librespot-connect = { version = "0.6.0", optional = true } @@ -22,48 +22,48 @@ librespot-playback = { version = "0.6.0", optional = true } librespot-metadata = "0.6.0" log = "0.4.27" chrono = "0.4.41" -reqwest = { version = "0.12.20", features = ["json"] } -rspotify = "0.14.0" +reqwest = { version = "0.12.22", features = ["json"] } +rspotify = "0.15.0" serde = { version = "1.0.219", features = ["derive"] } -tokio = { version = "1.45.1", features = [ +tokio = { version = "1.47.0", features = [ "rt", "rt-multi-thread", "macros", "time", ] } -toml = "0.8.23" +toml = "0.9.2" ratatui = { version = "0.29.0" } -rand = "0.9.1" +rand = "0.9.2" maybe-async = "0.2.10" async-trait = "0.1.88" parking_lot = "0.12.4" tracing = "0.1.41" tracing-subscriber = { version = "0.3.19", features = ["env-filter"] } backtrace = "0.3.75" -souvlaki = { version = "0.8.2", optional = true } +souvlaki = { version = "0.8.3", optional = true } viuer = { version = "0.9.2", optional = true } image = { version = "0.25.6", optional = true } notify-rust = { version = "4.11.7", optional = true, default-features = false, features = [ "d", ] } flume = "0.11.1" -serde_json = "1.0.140" +serde_json = "1.0.141" regex = "1.11.1" daemonize = { version = "0.5.0", optional = true } ttl_cache = "0.5.1" -clap_complete = "4.5.54" -which = "7.0.3" +clap_complete = "4.5.55" +which = "8.0.0" fuzzy-matcher = { version = "0.3.7", optional = true } html-escape = "0.2.13" -rustls = { version = "0.23.28", default-features = false, features = ["ring"] } +rustls = { version = "0.23.30", default-features = false, features = ["ring"] } unicode-bidi = "0.3.18" [target.'cfg(any(target_os = "windows", target_os = "macos"))'.dependencies.winit] -version = "0.30.11" +version = "0.30.12" optional = true [target.'cfg(target_os = "windows")'.dependencies] -clipboard-win = "5.4.0" +clipboard-win = "5.4.1" [target.'cfg(target_os = "windows")'.dependencies.windows] version = "0.58.0" diff --git a/spotify_player/src/event/popup.rs b/spotify_player/src/event/popup.rs index 656afd9d..008d8dce 100644 --- a/spotify_player/src/event/popup.rs +++ b/spotify_player/src/event/popup.rs @@ -214,7 +214,7 @@ pub fn handle_key_sequence_for_popup( command, ui, &artist_uris, - rspotify::model::Type::Artist, + &rspotify::model::Type::Artist, ) } PopupState::UserSavedAlbumList(_) => { @@ -231,7 +231,7 @@ pub fn handle_key_sequence_for_popup( command, ui, &album_uris, - rspotify::model::Type::Album, + &rspotify::model::Type::Album, ) } PopupState::ThemeList(themes, _) => { @@ -379,7 +379,7 @@ fn handle_command_for_context_browsing_list_popup( command: Command, ui: &mut UIStateGuard, uris: &[String], - context_type: rspotify::model::Type, + context_type: &rspotify::model::Type, ) -> Result { handle_command_for_list_popup( command, diff --git a/spotify_player/src/state/model.rs b/spotify_player/src/state/model.rs index 0ef75d80..4f30c5be 100644 --- a/spotify_player/src/state/model.rs +++ b/spotify_player/src/state/model.rs @@ -242,31 +242,27 @@ impl Context { Context::Album { ref album, ref tracks, - } => { - let album_length = play_time(tracks); - format!( - "{} | {} | {} songs | {}", - album.name, - album.release_date, - tracks.len(), - album_length, - ) - } + } => format!( + "{} | {} | {} songs | {}", + album.name, + album.release_date, + tracks.len(), + play_time(tracks), + ), Context::Playlist { ref playlist, tracks, - } => { - let playlist_length = play_time(tracks); - format!( - "{} | {} | {} songs | {}", - playlist.name, - playlist.owner.0, - tracks.len(), - playlist_length, - ) - } + } => format!( + "{} | {} | {} songs | {}", + playlist.name, + playlist.owner.0, + tracks.len(), + play_time(tracks), + ), Context::Artist { ref artist, .. } => artist.name.to_string(), - Context::Tracks { desc, tracks } => format!("{} | {} songs", desc, tracks.len()), + Context::Tracks { desc, tracks } => { + format!("{} | {} songs | {}", desc, tracks.len(), play_time(tracks)) + } Context::Show { ref show, ref episodes, diff --git a/spotify_player/src/ui/page.rs b/spotify_player/src/ui/page.rs index 9df5722c..6b1b3831 100644 --- a/spotify_player/src/ui/page.rs +++ b/spotify_player/src/ui/page.rs @@ -292,23 +292,23 @@ pub fn render_context_page( Some(context) => { // render context description let chunks = Layout::vertical([Constraint::Length(1), Constraint::Fill(0)]).split(rect); - let is_followed_string = if let Context::Playlist { playlist, .. } = context { - if data.user_data.is_followed_playlist(playlist) { - "Followed" - } else { - "Not Followed" - } + + let description = if let Context::Playlist { playlist, .. } = context { + format!( + "{} | {}", + context.description(), + if data.user_data.is_followed_playlist(playlist) { + "Followed" + } else { + "Not Followed" + } + ) } else { - "" + context.description() }; frame.render_widget( - Paragraph::new(format!( - "{} | {}", - context.description(), - is_followed_string - )) - .style(ui.theme.page_desc()), + Paragraph::new(description).style(ui.theme.page_desc()), chunks[0], ); let rect = chunks[1]; From 279ce89ef410390988c985c1bde232b3a1fd64cb Mon Sep 17 00:00:00 2001 From: KUHLwasStolen <149091393+KUHLwasStolen@users.noreply.github.com> Date: Wed, 30 Jul 2025 19:57:35 +0200 Subject: [PATCH 10/16] update version from yanked version 0.23.30 to latest version 0.23.31 (#788) --- Cargo.lock | 20 ++++++++++---------- spotify_player/Cargo.toml | 2 +- 2 files changed, 11 insertions(+), 11 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 75156e9c..8c2aec12 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2253,7 +2253,7 @@ dependencies = [ "hyper 1.6.0", "hyper-util", "log", - "rustls 0.23.30", + "rustls 0.23.31", "rustls-native-certs 0.8.1", "rustls-pki-types", "tokio", @@ -4265,7 +4265,7 @@ dependencies = [ "quinn-proto", "quinn-udp", "rustc-hash 2.1.1", - "rustls 0.23.30", + "rustls 0.23.31", "socket2 0.5.10", "thiserror 2.0.12", "tokio", @@ -4285,7 +4285,7 @@ dependencies = [ "rand 0.9.2", "ring", "rustc-hash 2.1.1", - "rustls 0.23.30", + "rustls 0.23.31", "rustls-pki-types", "slab", "thiserror 2.0.12", @@ -4628,7 +4628,7 @@ dependencies = [ "percent-encoding", "pin-project-lite", "quinn", - "rustls 0.23.30", + "rustls 0.23.31", "rustls-pki-types", "serde", "serde_json", @@ -4830,9 +4830,9 @@ dependencies = [ [[package]] name = "rustls" -version = "0.23.30" +version = "0.23.31" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "069a8df149a16b1a12dcc31497c3396a173844be3cac4bd40c9e7671fef96671" +checksum = "c0ebcbd2f03de0fc1122ad9bb24b127a5a6cd51d72604a3f3c50ac459762b6cc" dependencies = [ "aws-lc-rs", "log", @@ -5402,7 +5402,7 @@ dependencies = [ "regex", "reqwest 0.12.22", "rspotify", - "rustls 0.23.30", + "rustls 0.23.31", "serde", "serde_json", "souvlaki", @@ -5995,7 +5995,7 @@ version = "0.26.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8e727b36a1a0e8b74c376ac2211e40c2c8af09fb4013c60d910495810f008e9b" dependencies = [ - "rustls 0.23.30", + "rustls 0.23.31", "tokio", ] @@ -6018,7 +6018,7 @@ checksum = "edc5f74e248dc973e0dbb7b74c7e0d6fcc301c694ff50049504004ef4d0cdcd9" dependencies = [ "futures-util", "log", - "rustls 0.23.30", + "rustls 0.23.31", "rustls-native-certs 0.8.1", "rustls-pki-types", "tokio", @@ -6252,7 +6252,7 @@ dependencies = [ "httparse", "log", "rand 0.8.5", - "rustls 0.23.30", + "rustls 0.23.31", "rustls-pki-types", "sha1", "thiserror 1.0.69", diff --git a/spotify_player/Cargo.toml b/spotify_player/Cargo.toml index 8d0cba15..47972500 100644 --- a/spotify_player/Cargo.toml +++ b/spotify_player/Cargo.toml @@ -55,7 +55,7 @@ clap_complete = "4.5.55" which = "8.0.0" fuzzy-matcher = { version = "0.3.7", optional = true } html-escape = "0.2.13" -rustls = { version = "0.23.30", default-features = false, features = ["ring"] } +rustls = { version = "0.23.31", default-features = false, features = ["ring"] } unicode-bidi = "0.3.18" [target.'cfg(any(target_os = "windows", target_os = "macos"))'.dependencies.winit] From fd7cb1f6d5cfecd79a1533ad1bf374a000fd0ff1 Mon Sep 17 00:00:00 2001 From: KUHLwasStolen <149091393+KUHLwasStolen@users.noreply.github.com> Date: Tue, 5 Aug 2025 16:02:05 +0200 Subject: [PATCH 11/16] add optional feature to pixelate album art to match terminal aesthetic (#793) --- README.md | 16 ++++++++++++++++ docs/config.md | 1 + examples/app.toml | 1 + spotify_player/Cargo.toml | 1 + spotify_player/src/client/mod.rs | 22 ++++++++++++++++++++++ spotify_player/src/config/mod.rs | 4 ++++ 6 files changed, 45 insertions(+) diff --git a/README.md b/README.md index 20eb673d..62a62dd1 100644 --- a/README.md +++ b/README.md @@ -247,6 +247,22 @@ Examples of image rendering: ![others](https://user-images.githubusercontent.com/40011582/172967325-d2098037-e19e-440a-a38a-5b076253ecb1.png) +#### Pixelate + +If your terminal supports high-res images, but you like the pixelated look you can enable the `pixelate` feature, which also enables the `image` feature: + +```shell +cargo install spotify_player --features pixelate +``` + +The amount of pixels can be tweaked via the `cover_img_pixels` config option. + +| `cover_img_pixels` | `8` | `16` | `32` | `64` | +|--------------------|-----|------|------|------| +| example | 8x8 | 16x16 | 32x32 | 64x64 | + +To temporarily disable the `pixelate` feature just set `cover_img_pixels` to a high value like `512`. + ### Notify To enable desktop notification support, `spotify_player` needs to be built/installed with `notify` feature (**disabled** by default). To install the application with `notify` feature included, run: diff --git a/docs/config.md b/docs/config.md index 49342600..4ea45d9a 100644 --- a/docs/config.md +++ b/docs/config.md @@ -55,6 +55,7 @@ All configuration files should be placed inside the application's configuration | `cover_img_width` | the width of the cover image (`image` feature only) | `5` | | `cover_img_length` | the length of the cover image (`image` feature only) | `9` | | `cover_img_scale` | the scale of the cover image (`image` feature only) | `1.0` | +| `cover_img_pixels` | the amount of pixels per side of the cover image (`image` and `pixelate` feature only) | `16` | | `seek_duration_secs` | the duration (in seconds) to seek when using `SeekForward` and `SeekBackward` commands | `5` | | `sort_artist_albums_by_type` | sort albums on artist's pages by type, i.e. album or single | `false` | diff --git a/examples/app.toml b/examples/app.toml index 38e561fc..c4611a4c 100644 --- a/examples/app.toml +++ b/examples/app.toml @@ -21,6 +21,7 @@ pause_icon = "β–Œβ–Œ" liked_icon = "β™₯" cover_img_length = 9 cover_img_width = 5 +cover_img_pixels = 16 seek_duration_secs = 5 [device] diff --git a/spotify_player/Cargo.toml b/spotify_player/Cargo.toml index 47972500..a5c812d2 100644 --- a/spotify_player/Cargo.toml +++ b/spotify_player/Cargo.toml @@ -88,6 +88,7 @@ streaming = ["librespot-playback", "librespot-connect"] media-control = ["souvlaki", "winit", "windows"] image = ["viuer", "dep:image"] sixel = ["image", "viuer/sixel"] +pixelate = ["image"] notify = ["notify-rust"] daemon = ["daemonize", "streaming"] fzf = ["fuzzy-matcher"] diff --git a/spotify_player/src/client/mod.rs b/spotify_player/src/client/mod.rs index ac0a752d..de71a663 100644 --- a/spotify_player/src/client/mod.rs +++ b/spotify_player/src/client/mod.rs @@ -1663,8 +1663,19 @@ impl Client { #[cfg(feature = "image")] if !state.data.read().caches.images.contains_key(url) { let bytes = self.retrieve_image(url, &path, false).await?; + + #[cfg(not(feature = "pixelate"))] let image = image::load_from_memory(&bytes).context("Failed to load image from memory")?; + #[cfg(feature = "pixelate")] + let mut image = + image::load_from_memory(&bytes).context("Failed to load image from memory")?; + + #[cfg(feature = "pixelate")] + { + Self::pixelate_image(&mut image); + } + state .data .write() @@ -1833,6 +1844,17 @@ impl Client { Ok(bytes.to_vec()) } + #[cfg(feature = "pixelate")] + fn pixelate_image(image: &mut image::DynamicImage) { + let pixels = config::get_config().app_config.cover_img_pixels; + let pixelated_image = image.resize(pixels, pixels, image::imageops::FilterType::Nearest); + *image = pixelated_image.resize( + image.width(), + image.height(), + image::imageops::FilterType::Nearest, + ); + } + /// Process a list of albums, which includes /// - sort albums by the release date /// - sort albums by the type if `sort_artist_albums_by_type` config is enabled diff --git a/spotify_player/src/config/mod.rs b/spotify_player/src/config/mod.rs index d0ea458a..58ec49aa 100644 --- a/spotify_player/src/config/mod.rs +++ b/spotify_player/src/config/mod.rs @@ -95,6 +95,8 @@ pub struct AppConfig { pub cover_img_width: usize, #[cfg(feature = "image")] pub cover_img_scale: f32, + #[cfg(feature = "pixelate")] + pub cover_img_pixels: u32, #[cfg(feature = "media-control")] pub enable_media_control: bool, @@ -307,6 +309,8 @@ impl Default for AppConfig { cover_img_width: 5, #[cfg(feature = "image")] cover_img_scale: 1.0, + #[cfg(feature = "pixelate")] + cover_img_pixels: 16, // Because of the "creating new window and stealing focus" behaviour // when running the media control event loop on startup, From 963c36a35a166b6c90a06d73a2d97a581e8d1ea6 Mon Sep 17 00:00:00 2001 From: KUHLwasStolen <149091393+KUHLwasStolen@users.noreply.github.com> Date: Tue, 5 Aug 2025 16:13:16 +0200 Subject: [PATCH 12/16] remove unnecessary auth scopes email and birthdate (#792) --- spotify_player/src/auth.rs | 2 -- 1 file changed, 2 deletions(-) diff --git a/spotify_player/src/auth.rs b/spotify_player/src/auth.rs index e5b2d797..f3b7646e 100644 --- a/spotify_player/src/auth.rs +++ b/spotify_player/src/auth.rs @@ -24,9 +24,7 @@ const OAUTH_SCOPES: &[&str] = &[ "user-modify-playback-state", "user-modify-private", "user-personalized", - "user-read-birthdate", "user-read-currently-playing", - "user-read-email", "user-read-play-history", "user-read-playback-position", "user-read-playback-state", From db5dab97a603620c2da51705b449dc80d2e50210 Mon Sep 17 00:00:00 2001 From: Thang Pham Date: Sun, 24 Aug 2025 15:48:05 -0400 Subject: [PATCH 13/16] Upgrade `librespot` to `0.7.0`, fixing audio, authentication, DNS lookup issues (#808) Resolves #802 Resolves #799 Resolves #796 Resolves #787 This PR was based on #807 with a few updates --------- Co-authored-by: Bradley Zylstra --- Cargo.lock | 1523 +++++++++---------------- spotify_player/Cargo.toml | 10 +- spotify_player/src/auth.rs | 15 +- spotify_player/src/client/mod.rs | 2 +- spotify_player/src/config/mod.rs | 2 - spotify_player/src/event/clipboard.rs | 2 +- spotify_player/src/state/ui/page.rs | 2 +- spotify_player/src/streaming.rs | 11 +- spotify_player/src/token.rs | 41 +- spotify_player/src/ui/page.rs | 15 +- spotify_player/src/utils.rs | 2 +- 11 files changed, 548 insertions(+), 1077 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 8c2aec12..a44f5097 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -14,9 +14,9 @@ dependencies = [ [[package]] name = "ab_glyph_rasterizer" -version = "0.1.9" +version = "0.1.10" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b2187590a23ab1e3df8681afdf0987c48504d80291f002fcdb651f0ef5e25169" +checksum = "366ffbaa4442f4684d91e2cd7c5ea7c4ed8add41959a31447066e279e432b618" [[package]] name = "addr2line" @@ -54,7 +54,7 @@ dependencies = [ "getrandom 0.3.3", "once_cell", "version_check", - "zerocopy 0.8.26", + "zerocopy", ] [[package]] @@ -88,7 +88,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ed7572b7ba83a31e20d1b48970ee402d2e3e0537dcfe0a3ff4d6eb7508617d43" dependencies = [ "alsa-sys", - "bitflags 2.9.1", + "bitflags 2.9.3", "cfg-if", "libc", ] @@ -110,16 +110,16 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ef6978589202a00cd7e118380c448a08b6ed394c3a8df3a430d0898e3a42d046" dependencies = [ "android-properties", - "bitflags 2.9.1", + "bitflags 2.9.3", "cc", "cesu8", "jni", "jni-sys", "libc", "log", - "ndk 0.9.0", + "ndk", "ndk-context", - "ndk-sys 0.6.0+11769913", + "ndk-sys", "num_enum", "thiserror 1.0.69", ] @@ -156,9 +156,9 @@ dependencies = [ [[package]] name = "anstream" -version = "0.6.19" +version = "0.6.20" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "301af1932e46185686725e0fad2f8f2aa7da69dd70bf6ecc44d6b703844a3933" +checksum = "3ae563653d1938f79b1ab1b5e668c87c76a9930414574a6583a7b7e11a8e6192" dependencies = [ "anstyle", "anstyle-parse", @@ -186,35 +186,35 @@ dependencies = [ [[package]] name = "anstyle-query" -version = "1.1.3" +version = "1.1.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6c8bdeb6047d8983be085bab0ba1472e6dc604e7041dbf6fcd5e71523014fae9" +checksum = "9e231f6134f61b71076a3eab506c379d4f36122f2af15a9ff04415ea4c3339e2" dependencies = [ - "windows-sys 0.59.0", + "windows-sys 0.60.2", ] [[package]] name = "anstyle-wincon" -version = "3.0.9" +version = "3.0.10" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "403f75924867bb1033c59fbf0797484329750cfbe3c4325cd33127941fabc882" +checksum = "3e0633414522a32ffaac8ac6cc8f748e090c5717661fddeea04219e2344f5f2a" dependencies = [ "anstyle", "once_cell_polyfill", - "windows-sys 0.59.0", + "windows-sys 0.60.2", ] [[package]] name = "anyhow" -version = "1.0.98" +version = "1.0.99" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e16d2d3311acee920a9eb8d33b8cbc1787ce4a264e85f964c2404b969bdcd487" +checksum = "b0674a1ddeecb70197781e945de4b3b8ffb61fa939a5597bcf48503737663100" [[package]] name = "arbitrary" -version = "1.4.1" +version = "1.4.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "dde20b3d026af13f561bdd0f15edf01fc734f0dafcedbaf42bba506a9517f223" +checksum = "c3d036a3c4ab069c7b410a2ce876bd74808d2d0888a82667669f8e783a898bf1" [[package]] name = "arg_enum_proc_macro" @@ -269,9 +269,9 @@ dependencies = [ [[package]] name = "async-trait" -version = "0.1.88" +version = "0.1.89" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e539d3fca749fcee5236ab05e93a52867dd549cc157c8cb7f99595f3cedffdb5" +checksum = "9035ad2d096bed7955a320ee7e2230574d28fd3c3a0f186cbea1ff3c7eed5dbb" dependencies = [ "proc-macro2", "quote", @@ -312,36 +312,13 @@ dependencies = [ [[package]] name = "avif-serialize" -version = "0.8.5" +version = "0.8.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2ea8ef51aced2b9191c08197f55450d830876d9933f8f48a429b354f1d496b42" +checksum = "47c8fbc0f831f4519fe8b810b6a7a91410ec83031b8233f730a0480029f6a23f" dependencies = [ "arrayvec", ] -[[package]] -name = "aws-lc-rs" -version = "1.13.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5c953fe1ba023e6b7730c0d4b031d06f267f23a46167dcbd40316644b10a17ba" -dependencies = [ - "aws-lc-sys", - "zeroize", -] - -[[package]] -name = "aws-lc-sys" -version = "0.30.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "dbfd150b5dbdb988bcc8fb1fe787eb6b7ee6180ca24da683b61ea5405f3d43ff" -dependencies = [ - "bindgen 0.69.5", - "cc", - "cmake", - "dunce", - "fs_extra", -] - [[package]] name = "backtrace" version = "0.3.75" @@ -357,18 +334,6 @@ dependencies = [ "windows-targets 0.52.6", ] -[[package]] -name = "base64" -version = "0.13.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9e1b586273c5702936fe7b7d6896644d8be71e6314cfe09d3167c95f712589e8" - -[[package]] -name = "base64" -version = "0.21.7" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9d297deb1925b89f2ccc13d7635fa0714f12c87adce1c75356b39ca9b7178567" - [[package]] name = "base64" version = "0.22.1" @@ -381,47 +346,6 @@ version = "1.8.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "55248b47b0caf0546f7988906588779981c43bb1bc9d0c44087278f80cdb44ba" -[[package]] -name = "bindgen" -version = "0.69.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "271383c67ccabffb7381723dea0672a673f292304fcb45c01cc648c7a8d58088" -dependencies = [ - "bitflags 2.9.1", - "cexpr", - "clang-sys", - "itertools 0.12.1", - "lazy_static", - "lazycell", - "log", - "prettyplease", - "proc-macro2", - "quote", - "regex", - "rustc-hash 1.1.0", - "shlex", - "syn", - "which 4.4.2", -] - -[[package]] -name = "bindgen" -version = "0.72.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4f72209734318d0b619a5e0f5129918b848c416e122a3c4ce054e03cb87b726f" -dependencies = [ - "bitflags 2.9.1", - "cexpr", - "clang-sys", - "itertools 0.13.0", - "proc-macro2", - "quote", - "regex", - "rustc-hash 2.1.1", - "shlex", - "syn", -] - [[package]] name = "bit_field" version = "0.10.2" @@ -436,9 +360,9 @@ checksum = "bef38d45163c2f1dde094a7dfd33ccf595c92905c8f8f4fdc18d06fb1037718a" [[package]] name = "bitflags" -version = "2.9.1" +version = "2.9.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1b8e56985ec62d17e9c1001dc89c88ecd7dc08e47eba5ec7c29c7b5eeecde967" +checksum = "34efbcccd345379ca2868b2b2c9d3782e9cc58ba87bc7d79d5b53d9c9ae6f25d" [[package]] name = "bitstream-io" @@ -476,7 +400,7 @@ version = "0.6.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "340d2f0bdb2a43c1d3cd40513185b2bd7def0aa1052f956455114bc98f82dcf2" dependencies = [ - "objc2 0.6.1", + "objc2 0.6.2", ] [[package]] @@ -493,9 +417,9 @@ checksum = "46c5e41b57b8bba42a04676d81cb89e9ee8e859a1a66f80a5a72e1cb76b34d43" [[package]] name = "bytemuck" -version = "1.23.1" +version = "1.23.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5c76a5792e44e4abe34d3abf15636779261d45a7450612059293d1d2cfc63422" +checksum = "3995eaeebcdf32f91f980d360f78732ddc061097ab4e39991ae7a6ace9194677" [[package]] name = "byteorder" @@ -521,7 +445,7 @@ version = "0.13.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b99da2f8558ca23c71f4fd15dc57c906239752dd27ff3c00a1d56b685b7cbfec" dependencies = [ - "bitflags 2.9.1", + "bitflags 2.9.3", "log", "polling", "rustix 0.38.44", @@ -558,9 +482,9 @@ dependencies = [ [[package]] name = "cc" -version = "1.2.30" +version = "1.2.34" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "deec109607ca693028562ed836a5f1c4b8bd77755c4e132fc5ce11b0b6211ae7" +checksum = "42bc4aea80032b7bf409b0bc7ccad88853858911b7713a8062fdc0623867bedc" dependencies = [ "jobserver", "libc", @@ -573,15 +497,6 @@ version = "1.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6d43a04d8753f35258c91f8ec639f792891f748a1edbd759cf1dcea3382ad83c" -[[package]] -name = "cexpr" -version = "0.6.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6fac387a98bb7c37292057cffc56d62ecb629900026402633ae9160df93a8766" -dependencies = [ - "nom", -] - [[package]] name = "cfg-expr" version = "0.15.8" @@ -594,9 +509,9 @@ dependencies = [ [[package]] name = "cfg-expr" -version = "0.20.1" +version = "0.20.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0d0390889d58f934f01cd49736275b4c2da15bcfc328c78ff2349907e6cabf22" +checksum = "c8d458d63f0f0f482c8da9b7c8b76c21bd885a02056cc94c6404d861ca2b8206" dependencies = [ "smallvec", "target-lexicon 0.13.2", @@ -604,9 +519,9 @@ dependencies = [ [[package]] name = "cfg-if" -version = "1.0.1" +version = "1.0.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9555578bc9e57714c812a1f84e4fc5b4d21fcb063490c624de019f7464c91268" +checksum = "2fd1289c04a9ea8cb22300a459a72a385d7c73d3259e2ed7dcb2af674838cfa9" [[package]] name = "cfg_aliases" @@ -639,22 +554,11 @@ dependencies = [ "inout", ] -[[package]] -name = "clang-sys" -version = "1.8.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0b023947811758c97c59bf9d1c188fd619ad4718dcaa767947df1cadb14f39f4" -dependencies = [ - "glob", - "libc", - "libloading 0.8.8", -] - [[package]] name = "clap" -version = "4.5.41" +version = "4.5.45" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "be92d32e80243a54711e5d7ce823c35c41c9d929dc4ab58e1276f625841aadf9" +checksum = "1fc0e74a703892159f5ae7d3aac52c8e6c392f5ae5f359c70b5881d60aaac318" dependencies = [ "clap_builder", "clap_derive", @@ -662,9 +566,9 @@ dependencies = [ [[package]] name = "clap_builder" -version = "4.5.41" +version = "4.5.44" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "707eab41e9622f9139419d573eca0900137718000c517d47da73045f54331c3d" +checksum = "b3e7f4214277f3c7aa526a59dd3fbe306a370daee1f8b7b8c987069cd8e888a8" dependencies = [ "anstream", "anstyle", @@ -674,18 +578,18 @@ dependencies = [ [[package]] name = "clap_complete" -version = "4.5.55" +version = "4.5.57" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a5abde44486daf70c5be8b8f8f1b66c49f86236edf6fa2abadb4d961c4c6229a" +checksum = "4d9501bd3f5f09f7bbee01da9a511073ed30a80cd7a509f1214bb74eadea71ad" dependencies = [ "clap", ] [[package]] name = "clap_derive" -version = "4.5.41" +version = "4.5.45" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ef4f52386a59ca4c860f7393bcf8abd8dfd91ecccc0f774635ff68e92eeef491" +checksum = "14cb31bb0a7d536caef2639baa7fad459e15c3144efefa6dbd1c84562c4739f6" dependencies = [ "heck", "proc-macro2", @@ -708,15 +612,6 @@ dependencies = [ "error-code", ] -[[package]] -name = "cmake" -version = "0.1.54" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e7caa3f9de89ddbe2c607f4101924c5abec803763ae9534e4f4d7d8f84aa81f0" -dependencies = [ - "cc", -] - [[package]] name = "cocoa" version = "0.24.1" @@ -726,7 +621,7 @@ dependencies = [ "bitflags 1.3.2", "block", "cocoa-foundation", - "core-foundation 0.9.4", + "core-foundation", "core-graphics 0.22.3", "foreign-types 0.3.2", "libc", @@ -741,7 +636,7 @@ checksum = "8c6234cbb2e4c785b456c0644748b1ac416dd045799740356f8363dfe00c93f7" dependencies = [ "bitflags 1.3.2", "block", - "core-foundation 0.9.4", + "core-foundation", "core-graphics-types", "libc", "objc", @@ -801,7 +696,7 @@ dependencies = [ "anyhow", "config_parser_derive", "serde", - "toml 0.9.2", + "toml 0.9.5", ] [[package]] @@ -852,16 +747,6 @@ dependencies = [ "libc", ] -[[package]] -name = "core-foundation" -version = "0.10.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b2a6cd9ae233e7f62ba4e9353e81a88df7fc8a5987b8d445b4d90c879bd156f6" -dependencies = [ - "core-foundation-sys", - "libc", -] - [[package]] name = "core-foundation-sys" version = "0.8.7" @@ -875,7 +760,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2581bbab3b8ffc6fcbd550bf46c355135d16e9ff2a6ea032ad6b9bf1d7efe4fb" dependencies = [ "bitflags 1.3.2", - "core-foundation 0.9.4", + "core-foundation", "core-graphics-types", "foreign-types 0.3.2", "libc", @@ -888,7 +773,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c07782be35f9e1140080c6b96f0d44b739e2278479f64e02fdab4e32dfd8b081" dependencies = [ "bitflags 1.3.2", - "core-foundation 0.9.4", + "core-foundation", "core-graphics-types", "foreign-types 0.5.0", "libc", @@ -901,38 +786,31 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "45390e6114f68f718cc7a830514a96f903cccd70d02a8f6d9f643ac4ba45afaf" dependencies = [ "bitflags 1.3.2", - "core-foundation 0.9.4", + "core-foundation", "libc", ] [[package]] name = "coreaudio-rs" -version = "0.11.3" +version = "0.13.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "321077172d79c662f64f5071a03120748d5bb652f5231570141be24cfcd2bace" +checksum = "1aae284fbaf7d27aa0e292f7677dfbe26503b0d555026f702940805a630eac17" dependencies = [ "bitflags 1.3.2", - "core-foundation-sys", - "coreaudio-sys", -] - -[[package]] -name = "coreaudio-sys" -version = "0.2.17" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ceec7a6067e62d6f931a2baf6f3a751f4a892595bcec1461a3c94ef9949864b6" -dependencies = [ - "bindgen 0.72.0", + "libc", + "objc2-audio-toolbox", + "objc2-core-audio", + "objc2-core-audio-types", + "objc2-core-foundation", ] [[package]] name = "cpal" -version = "0.15.3" +version = "0.16.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "873dab07c8f743075e57f524c583985fbaf745602acbe916a01539364369a779" +checksum = "cbd307f43cc2a697e2d1f8bc7a1d824b5269e052209e28883e5bc04d095aaa3f" dependencies = [ "alsa", - "core-foundation-sys", "coreaudio-rs", "dasp_sample", "jack", @@ -940,9 +818,13 @@ dependencies = [ "js-sys", "libc", "mach2", - "ndk 0.8.0", + "ndk", "ndk-context", - "oboe", + "num-derive", + "num-traits", + "objc2-audio-toolbox", + "objc2-core-audio", + "objc2-core-audio-types", "wasm-bindgen", "wasm-bindgen-futures", "web-sys", @@ -998,7 +880,7 @@ version = "0.28.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "829d955a0bb380ef178a640b91779e3987da38c9aea133b20614cfed8cdea9c6" dependencies = [ - "bitflags 2.9.1", + "bitflags 2.9.3", "crossterm_winapi", "mio", "parking_lot", @@ -1014,7 +896,7 @@ version = "0.29.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d8b9f2e4c67f833b660cdb0a3523065869fb35570177239812ed4c905aeff87b" dependencies = [ - "bitflags 2.9.1", + "bitflags 2.9.3", "crossterm_winapi", "derive_more", "document-features", @@ -1124,13 +1006,13 @@ checksum = "2a2330da5de22e8a3cb63252ce2abb30116bf5265e89c0e01bc17015ce30a476" [[package]] name = "dbus" -version = "0.9.7" +version = "0.9.8" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1bb21987b9fb1613058ba3843121dd18b163b254d8a6e797e144cbac14d96d1b" +checksum = "6e8d0767bcb66eb101d5ab87b9f38542691185af14fa8a7026c2490e62b45cfc" dependencies = [ "libc", "libdbus-sys", - "winapi", + "windows-sys 0.59.0", ] [[package]] @@ -1259,8 +1141,8 @@ version = "0.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "89a09f22a6c6069a18470eb92d2298acf25463f14256d24778e1230d789a2aec" dependencies = [ - "bitflags 2.9.1", - "objc2 0.6.1", + "bitflags 2.9.3", + "objc2 0.6.2", ] [[package]] @@ -1304,12 +1186,6 @@ version = "0.1.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d8b14ccef22fc6f5a8f4d7d768562a182c04ce9a3b3157b91390b52ddfdf1a76" -[[package]] -name = "dunce" -version = "1.0.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "92773504d58c093f6de2459af4af33faa518c13451eb8f2b5698ed3d36e7c813" - [[package]] name = "either" version = "1.15.0" @@ -1524,19 +1400,13 @@ checksum = "aa9a19cbb55df58761df49b23516a86d432839add4af60fc256da840f66ed35b" [[package]] name = "form_urlencoded" -version = "1.2.1" +version = "1.2.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e13624c2627564efccf4934284bdd98cbaa14e79b0b5a141218e507b3a823456" +checksum = "cb4cb245038516f5f85277875cdaa4f7d2c9a0fa0468de06ed190163b1581fcf" dependencies = [ "percent-encoding", ] -[[package]] -name = "fs_extra" -version = "1.3.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "42703706b716c37f96a77aea830392ad231f44c9e9a67872fa5548707e11b11c" - [[package]] name = "futf" version = "0.1.5" @@ -1597,9 +1467,9 @@ checksum = "9e5c1b78ca4aae1ac06c48a526a655760685149f0d465d21f37abfe57ce075c6" [[package]] name = "futures-lite" -version = "2.6.0" +version = "2.6.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f5edaec856126859abb19ed65f39e90fea3a9574b9707f13539acf4abf7eb532" +checksum = "f78e10609fe0e0b3f4157ffab1876319b5b0db102a2c60dc4626306dc46b44ad" dependencies = [ "fastrand", "futures-core", @@ -1729,24 +1599,24 @@ checksum = "07e28edb80900c19c28f1072f2e8aeca7fa06b23cd4169cefe1af5aa3260783f" [[package]] name = "gio-sys" -version = "0.20.10" +version = "0.21.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "521e93a7e56fc89e84aea9a52cfc9436816a4b363b030260b699950ff1336c83" +checksum = "a03f2234671e5a588cfe1f59c2b22c103f5772ea351be9cc824a9ce0d06d99fd" dependencies = [ "glib-sys", "gobject-sys", "libc", "system-deps 7.0.5", - "windows-sys 0.59.0", + "windows-sys 0.60.2", ] [[package]] name = "glib" -version = "0.20.12" +version = "0.21.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ffc4b6e352d4716d84d7dde562dd9aee2a7d48beb872dd9ece7f2d1515b2d683" +checksum = "60bdc26493257b5794ba9301f7cbaf7ab0d69a570bfbefa4d7d360e781cb5205" dependencies = [ - "bitflags 2.9.1", + "bitflags 2.9.3", "futures-channel", "futures-core", "futures-executor", @@ -1763,9 +1633,9 @@ dependencies = [ [[package]] name = "glib-macros" -version = "0.20.12" +version = "0.21.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e8084af62f09475a3f529b1629c10c429d7600ee1398ae12dd3bf175d74e7145" +checksum = "e772291ebea14c28eb11bb75741f62f4a4894f25e60ce80100797b6b010ef0f9" dependencies = [ "heck", "proc-macro-crate", @@ -1776,25 +1646,19 @@ dependencies = [ [[package]] name = "glib-sys" -version = "0.20.10" +version = "0.21.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8ab79e1ed126803a8fb827e3de0e2ff95191912b8db65cee467edb56fc4cc215" +checksum = "dc7c43cff6a7dc43821e45ebf172399437acd6716fa2186b6852d2b397bf622d" dependencies = [ "libc", "system-deps 7.0.5", ] -[[package]] -name = "glob" -version = "0.3.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a8d1add55171497b4705a648c6b583acafb01d58050a51727785f0b2c8e0a2b2" - [[package]] name = "gobject-sys" -version = "0.20.10" +version = "0.21.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ec9aca94bb73989e3cfdbf8f2e0f1f6da04db4d291c431f444838925c4c63eda" +checksum = "3e9a190eef2bce144a6aa8434e306974c6062c398e0a33a146d60238f9062d5c" dependencies = [ "glib-sys", "libc", @@ -1803,27 +1667,30 @@ dependencies = [ [[package]] name = "governor" -version = "0.6.3" +version = "0.10.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "68a7f542ee6b35af73b06abc0dad1c1bae89964e4e253bc4b587b91c9637867b" +checksum = "444405bbb1a762387aa22dd569429533b54a1d8759d35d3b64cb39b0293eaa19" dependencies = [ "cfg-if", - "futures", + "futures-sink", "futures-timer", - "no-std-compat", + "futures-util", + "getrandom 0.3.3", + "hashbrown", "nonzero_ext", "parking_lot", "portable-atomic", - "rand 0.8.5", + "rand 0.9.2", "smallvec", "spinning_top", + "web-time", ] [[package]] name = "gstreamer" -version = "0.23.7" +version = "0.24.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8757a87f3706560037a01a9f06a59fcc7bdb0864744dcf73546606e60c4316e1" +checksum = "32f5db514ad5ccf70ad35485058aa8b894bb81cfcf76bb994af135d9789427c6" dependencies = [ "cfg-if", "futures-channel", @@ -1832,23 +1699,23 @@ dependencies = [ "glib", "gstreamer-sys", "itertools 0.14.0", + "kstring", "libc", "muldiv", "num-integer", "num-rational", - "once_cell", "option-operations", "paste", "pin-project-lite", "smallvec", - "thiserror 2.0.12", + "thiserror 2.0.16", ] [[package]] name = "gstreamer-app" -version = "0.23.5" +version = "0.24.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2e9a883eb21aebcf1289158225c05f7aea5da6ecf71fa7f0ff1ce4d25baf004e" +checksum = "fad8ae64a7af6d1aa04e96db085a0cbd64a6b838d85c115c99fa053ab8902d98" dependencies = [ "futures-core", "futures-sink", @@ -1861,9 +1728,9 @@ dependencies = [ [[package]] name = "gstreamer-app-sys" -version = "0.23.5" +version = "0.24.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "94f7ef838306fe51852d503a14dc79ac42de005a59008a05098de3ecdaf05455" +checksum = "aaf1a3af017f9493c34ccc8439cbce5c48f6ddff6ec0514c23996b374ff25f9a" dependencies = [ "glib-sys", "gstreamer-base-sys", @@ -1874,9 +1741,9 @@ dependencies = [ [[package]] name = "gstreamer-audio" -version = "0.23.6" +version = "0.24.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2e7ec7e0374298897e669db7c79544bc44df12011985e7dd5f38644edaf2caf4" +checksum = "7404c5d0cbb2189e6a10d05801e93f47fe60b195e4d73dd1c540d055f7b340b8" dependencies = [ "cfg-if", "glib", @@ -1884,15 +1751,14 @@ dependencies = [ "gstreamer-audio-sys", "gstreamer-base", "libc", - "once_cell", "smallvec", ] [[package]] name = "gstreamer-audio-sys" -version = "0.23.6" +version = "0.24.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2b5f3e09e7c04ec91d78c2a6ca78d50b574b9ed49fdf5e72f3693adca4306a87" +checksum = "626cd3130bc155a8b6d4ac48cfddc15774b5a6cc76fcb191aab09a2655bad8f5" dependencies = [ "glib-sys", "gobject-sys", @@ -1904,9 +1770,9 @@ dependencies = [ [[package]] name = "gstreamer-base" -version = "0.23.6" +version = "0.24.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f19a74fd04ffdcb847dd322640f2cf520897129d00a7bcb92fd62a63f3e27404" +checksum = "34745d3726a080e0d57e402a314e37073d0b341f3a5754258550311ca45e4754" dependencies = [ "atomic_refcell", "cfg-if", @@ -1918,9 +1784,9 @@ dependencies = [ [[package]] name = "gstreamer-base-sys" -version = "0.23.6" +version = "0.24.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "87f2fb0037b6d3c5b51f60dea11e667910f33be222308ca5a101450018a09840" +checksum = "dfad00fa63ddd8132306feef9d5095a3636192f09d925adfd0a9be0d82b9ea91" dependencies = [ "glib-sys", "gobject-sys", @@ -1931,10 +1797,11 @@ dependencies = [ [[package]] name = "gstreamer-sys" -version = "0.23.6" +version = "0.24.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "feea73b4d92dbf9c24a203c9cd0bcc740d584f6b5960d5faf359febf288919b2" +checksum = "36f46b35f9dc4b5a0dca3f19d2118bb5355c3112f228a99a84ed555f48ce5cf9" dependencies = [ + "cfg-if", "glib-sys", "gobject-sys", "libc", @@ -1943,35 +1810,16 @@ dependencies = [ [[package]] name = "h2" -version = "0.3.27" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0beca50380b1fc32983fc1cb4587bfa4bb9e78fc259aad4a0032d2080309222d" -dependencies = [ - "bytes", - "fnv", - "futures-core", - "futures-sink", - "futures-util", - "http 0.2.12", - "indexmap", - "slab", - "tokio", - "tokio-util", - "tracing", -] - -[[package]] -name = "h2" -version = "0.4.11" +version = "0.4.12" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "17da50a276f1e01e0ba6c029e47b7100754904ee8a278f886546e98575380785" +checksum = "f3c0b69cfcb4e1b9f1bf2f53f95f766e4661169728ec61cd3fe5a0166f2d1386" dependencies = [ "atomic-waker", "bytes", "fnv", "futures-core", "futures-sink", - "http 1.3.1", + "http", "indexmap", "slab", "tokio", @@ -1991,9 +1839,9 @@ dependencies = [ [[package]] name = "hashbrown" -version = "0.15.4" +version = "0.15.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5971ac85611da7067dbfcabef3c70ebb5606018acd9e2a3903a0da507521e0d5" +checksum = "9229cfe53dfd69f0609a49f65461bd93001ea1ef889cd5529dd176593f5338a1" dependencies = [ "allocator-api2", "equivalent", @@ -2006,10 +1854,10 @@ version = "0.4.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b3314d5adb5d94bcdf56771f2e50dbbc80bb4bdf88967526706205ac9eff24eb" dependencies = [ - "base64 0.22.1", + "base64", "bytes", "headers-core", - "http 1.3.1", + "http", "httpdate", "mime", "sha1", @@ -2021,7 +1869,7 @@ version = "0.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "54b4a22553d4242c49fddb9ba998a99962b5cc6f22cb5a3482bec22522403ce4" dependencies = [ - "http 1.3.1", + "http", ] [[package]] @@ -2077,17 +1925,6 @@ dependencies = [ "syn", ] -[[package]] -name = "http" -version = "0.2.12" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "601cbb57e577e2f5ef5be8e7b83f0f63994f25aa94d673e54a92d5c516d101f1" -dependencies = [ - "bytes", - "fnv", - "itoa", -] - [[package]] name = "http" version = "1.3.1" @@ -2099,17 +1936,6 @@ dependencies = [ "itoa", ] -[[package]] -name = "http-body" -version = "0.4.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7ceab25649e9960c0311ea418d17bee82c0dcec1bd053b5f9a66e265a693bed2" -dependencies = [ - "bytes", - "http 0.2.12", - "pin-project-lite", -] - [[package]] name = "http-body" version = "1.0.1" @@ -2117,7 +1943,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1efedce1fb8e6913f23e0c92de8e62cd5b772a67e7b3946df930a62566c93184" dependencies = [ "bytes", - "http 1.3.1", + "http", ] [[package]] @@ -2128,8 +1954,8 @@ checksum = "b021d93e26becf5dc7e1b75b1bed1fd93124b374ceb73f43d4d4eafec896a64a" dependencies = [ "bytes", "futures-core", - "http 1.3.1", - "http-body 1.0.1", + "http", + "http-body", "pin-project-lite", ] @@ -2147,43 +1973,21 @@ checksum = "df3b46402a9d5adb4c86a0cf463f42e19994e3ee891101b1841f30a545cb49a9" [[package]] name = "hyper" -version = "0.14.32" +version = "1.7.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "41dfc780fdec9373c01bae43289ea34c972e40ee3c9f6b3c8801a35f35586ce7" +checksum = "eb3aa54a13a0dfe7fbe3a59e0c76093041720fdc77b110cc0fc260fafb4dc51e" dependencies = [ + "atomic-waker", "bytes", "futures-channel", "futures-core", - "futures-util", - "h2 0.3.27", - "http 0.2.12", - "http-body 0.4.6", - "httparse", - "httpdate", - "itoa", - "pin-project-lite", - "socket2 0.5.10", - "tokio", - "tower-service", - "tracing", - "want", -] - -[[package]] -name = "hyper" -version = "1.6.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cc2b571658e38e0c01b1fdca3bbbe93c00d3d71693ff2770043f8c29bc7d6f80" -dependencies = [ - "bytes", - "futures-channel", - "futures-util", - "h2 0.4.11", - "http 1.3.1", - "http-body 1.0.1", + "h2", + "http", + "http-body", "httparse", "itoa", "pin-project-lite", + "pin-utils", "smallvec", "tokio", "want", @@ -2198,48 +2002,14 @@ dependencies = [ "bytes", "futures-util", "headers", - "http 1.3.1", - "hyper 1.6.0", - "hyper-rustls 0.26.0", + "http", + "hyper", + "hyper-tls", "hyper-util", + "native-tls", "pin-project-lite", - "rustls-native-certs 0.7.3", - "tokio", - "tokio-rustls 0.25.0", - "tower-service", - "webpki", -] - -[[package]] -name = "hyper-rustls" -version = "0.24.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ec3efd23720e2049821a693cbc7e65ea87c72f1c58ff2f9522ff332b1491e590" -dependencies = [ - "futures-util", - "http 0.2.12", - "hyper 0.14.32", - "rustls 0.21.12", - "tokio", - "tokio-rustls 0.24.1", -] - -[[package]] -name = "hyper-rustls" -version = "0.26.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a0bea761b46ae2b24eb4aef630d8d1c398157b6fc29e6350ecf090a0b70c952c" -dependencies = [ - "futures-util", - "http 1.3.1", - "hyper 1.6.0", - "hyper-util", - "log", - "rustls 0.22.4", - "rustls-native-certs 0.7.3", - "rustls-pki-types", "tokio", - "tokio-rustls 0.25.0", + "tokio-native-tls", "tower-service", ] @@ -2249,17 +2019,15 @@ version = "0.27.7" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e3c93eb611681b207e1fe55d5a71ecf91572ec8a6705cdb6857f7d8d5242cf58" dependencies = [ - "http 1.3.1", - "hyper 1.6.0", + "http", + "hyper", "hyper-util", - "log", - "rustls 0.23.31", - "rustls-native-certs 0.8.1", + "rustls", "rustls-pki-types", "tokio", - "tokio-rustls 0.26.2", + "tokio-rustls", "tower-service", - "webpki-roots 1.0.2", + "webpki-roots", ] [[package]] @@ -2270,7 +2038,7 @@ checksum = "70206fc6890eaca9fde8a0bf71caa2ddfc9fe045ac9e5c70df101a7dbde866e0" dependencies = [ "bytes", "http-body-util", - "hyper 1.6.0", + "hyper", "hyper-util", "native-tls", "tokio", @@ -2284,20 +2052,20 @@ version = "0.1.16" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8d9b05277c7e8da2c93a568989bb6207bef0112e8d17df7a6eda4a3cf143bc5e" dependencies = [ - "base64 0.22.1", + "base64", "bytes", "futures-channel", "futures-core", "futures-util", - "http 1.3.1", - "http-body 1.0.1", - "hyper 1.6.0", + "http", + "http-body", + "hyper", "ipnet", "libc", "percent-encoding", "pin-project-lite", "socket2 0.6.0", - "system-configuration 0.6.1", + "system-configuration", "tokio", "tower-service", "tracing", @@ -2422,9 +2190,9 @@ checksum = "b9e0384b61958566e926dc50660321d12159025e767c18e043daf26b70104c39" [[package]] name = "idna" -version = "1.0.3" +version = "1.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "686f825264d630750a544639377bae737628043f20d38bbc029e8f29ea968a7e" +checksum = "3b0875f23caa03898994f6ddc501886a45c7d3d62d04d2d90788d47be1b1e4de" dependencies = [ "idna_adapter", "smallvec", @@ -2482,9 +2250,9 @@ checksum = "d0263a3d970d5c054ed9312c0057b4f3bde9c0b33836d3637361d4a9e6e7a408" [[package]] name = "indexmap" -version = "2.10.0" +version = "2.11.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fe4cd85333e22411419a0bcae1297d25e58c9443848b11dc6a86fefe8c78a661" +checksum = "f2481980430f9f78649238835720ddccc57e52df14ffce1c6f37391d61b563e9" dependencies = [ "equivalent", "hashbrown", @@ -2531,11 +2299,11 @@ dependencies = [ [[package]] name = "io-uring" -version = "0.7.9" +version = "0.7.10" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d93587f37623a1a17d94ef2bc9ada592f5465fe7732084ab7beefabe5c77c0c4" +checksum = "046fa2d4d00aea763528b4950358d0ead425372445dc8ff86312b3c69ff7727b" dependencies = [ - "bitflags 2.9.1", + "bitflags 2.9.3", "cfg-if", "libc", ] @@ -2556,6 +2324,25 @@ dependencies = [ "serde", ] +[[package]] +name = "is-docker" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "928bae27f42bc99b60d9ac7334e3a21d10ad8f1835a4e12ec3ec0464765ed1b3" +dependencies = [ + "once_cell", +] + +[[package]] +name = "is-wsl" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "173609498df190136aa7dea1a91db051746d339e18476eed5ca40521f02d7aa5" +dependencies = [ + "is-docker", + "once_cell", +] + [[package]] name = "is_terminal_polyfill" version = "1.70.1" @@ -2597,11 +2384,11 @@ checksum = "4a5f13b858c8d314ee3e8f639011f7ccefe71f97f96e50151fb991f267928e2c" [[package]] name = "jack" -version = "0.11.4" +version = "0.13.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0e5a18a3c2aefb354fb77111ade228b20267bdc779de84e7a4ccf7ea96b9a6cd" +checksum = "f70ca699f44c04a32d419fc9ed699aaea89657fc09014bf3fa238e91d13041b9" dependencies = [ - "bitflags 1.3.2", + "bitflags 2.9.3", "jack-sys", "lazy_static", "libc", @@ -2670,6 +2457,15 @@ dependencies = [ "wasm-bindgen", ] +[[package]] +name = "kstring" +version = "2.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "558bf9508a558512042d3095138b1f7b8fe90c5467d94f9f1da28b3731c5dbd1" +dependencies = [ + "static_assertions", +] + [[package]] name = "lazy_static" version = "1.5.0" @@ -2679,12 +2475,6 @@ dependencies = [ "spin", ] -[[package]] -name = "lazycell" -version = "1.3.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "830d08ce1d1d941e6b30645f1a0eb5643013d835ce3779a5fc208261dbe10f55" - [[package]] name = "lebe" version = "0.5.2" @@ -2693,15 +2483,15 @@ checksum = "03087c2bad5e1034e8cace5926dec053fb3790248370865f5117a7d0213354c8" [[package]] name = "libc" -version = "0.2.174" +version = "0.2.175" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1171693293099992e19cddea4e8b849964e9846f4acee11b3948bcc337be8776" +checksum = "6a82ae493e598baaea5209805c49bbf2ea7de956d50d7da0da1164f9c6d28543" [[package]] name = "libdbus-sys" -version = "0.2.5" +version = "0.2.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "06085512b750d640299b79be4bad3d2fa90a9c00b1fd9e1b46364f66f0485c72" +checksum = "5cbe856efeb50e4681f010e9aaa2bf0a644e10139e54cde10fc83a307c23bd9f" dependencies = [ "pkg-config", ] @@ -2733,7 +2523,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "07033963ba89ebaf1584d767badaa2e8fcec21aedea6b8c0346d487d49c28667" dependencies = [ "cfg-if", - "windows-targets 0.53.2", + "windows-targets 0.53.3", ] [[package]] @@ -2748,7 +2538,7 @@ version = "2.30.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "909eb3049e16e373680fe65afe6e2a722ace06b671250cc4849557bc57d6a397" dependencies = [ - "bitflags 2.9.1", + "bitflags 2.9.3", "libc", "libpulse-sys", "num-derive", @@ -2792,79 +2582,79 @@ dependencies = [ [[package]] name = "libredox" -version = "0.1.6" +version = "0.1.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4488594b9328dee448adb906d8b126d9b7deb7cf5c22161ee591610bb1be83c0" +checksum = "391290121bad3d37fbddad76d8f5d1c1c314cfc646d143d7e07a3086ddff0ce3" dependencies = [ - "bitflags 2.9.1", + "bitflags 2.9.3", "libc", - "redox_syscall 0.5.16", + "redox_syscall 0.5.17", ] [[package]] name = "librespot-audio" -version = "0.6.0" +version = "0.7.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7e07566fe7553042936c61bbdd9bedb524114a904aa7f9738e266c641468bab8" +checksum = "1ccbd884e99ad67528a34b7ce54f4ad872bee9f57b232441edb1befb8c60c88b" dependencies = [ "aes", "bytes", "ctr", "futures-util", "http-body-util", - "hyper 1.6.0", + "hyper", "hyper-util", "librespot-core", "log", "parking_lot", "tempfile", - "thiserror 1.0.69", + "thiserror 2.0.16", "tokio", ] [[package]] name = "librespot-connect" -version = "0.6.0" +version = "0.7.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "67ce3a5634669ce051a425cff5437a474831111df045fd5a2bb37fabe316fa60" +checksum = "9ee65534c5bb1e2179a8b4aefe2ec21021f17ea88917be7dfd264491725e663b" dependencies = [ - "form_urlencoded", "futures-util", "librespot-core", "librespot-playback", "librespot-protocol", "log", "protobuf", - "rand 0.8.5", - "serde", + "rand 0.9.2", "serde_json", - "thiserror 1.0.69", + "thiserror 2.0.16", "tokio", "tokio-stream", + "uuid", ] [[package]] name = "librespot-core" -version = "0.6.0" +version = "0.7.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c4c76303efcf949a59f9380220ca420c4d72fa32dbb3641a0079f429cc5e44e7" +checksum = "44bda6a0a394643182ced9bc4cdd0f6ef040852199419fd98a7b30bfb382a79a" dependencies = [ "aes", - "base64 0.22.1", + "base64", "byteorder", "bytes", "data-encoding", + "flate2", "form_urlencoded", "futures-core", "futures-util", "governor", "hmac", - "http 1.3.1", + "http", "http-body-util", "httparse", - "hyper 1.6.0", + "hyper", "hyper-proxy2", - "hyper-rustls 0.27.7", + "hyper-tls", "hyper-util", "librespot-oauth", "librespot-protocol", @@ -2874,21 +2664,22 @@ dependencies = [ "num-derive", "num-integer", "num-traits", - "once_cell", "parking_lot", "pbkdf2", "pin-project-lite", "priority-queue", "protobuf", - "quick-xml 0.36.2", - "rand 0.8.5", + "protobuf-json-mapping", + "quick-xml 0.38.2", + "rand 0.9.2", + "rand_distr", "rsa", "serde", "serde_json", "sha1", "shannon", "sysinfo", - "thiserror 1.0.69", + "thiserror 2.0.16", "time", "tokio", "tokio-stream", @@ -2901,9 +2692,9 @@ dependencies = [ [[package]] name = "librespot-metadata" -version = "0.6.0" +version = "0.7.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7cf6d5c46a401b1dd3e062ebdce959aa694bbae1039841756545d2e9c4f8be5f" +checksum = "d2f186eef62b0b3a156e9c204717e88c404396b36cf8f5c0e298f627bd4d2dca" dependencies = [ "async-trait", "bytes", @@ -2913,27 +2704,29 @@ dependencies = [ "protobuf", "serde", "serde_json", - "thiserror 1.0.69", + "thiserror 2.0.16", "uuid", ] [[package]] name = "librespot-oauth" -version = "0.6.0" +version = "0.7.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "00dcad779aa6c3b442e493e2a40ca83a5c5fcf38a65c05b026c3587bd4f8d14f" +checksum = "5abefd211d7ab9a850f951258c07a346391f235e512adcd478c069ef7916f8bb" dependencies = [ "log", "oauth2", - "thiserror 1.0.69", + "open", + "reqwest", + "thiserror 2.0.16", "url", ] [[package]] name = "librespot-playback" -version = "0.6.0" +version = "0.7.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4ed1f776a04c8c9275407f8d4df034f2615ea729ec6c6d0fa69f423fc571df64" +checksum = "20ef81a3ccdff4c31f94b80efd89bfac737daddd94574e97026d86e6e1d072e0" dependencies = [ "alsa", "cpal", @@ -2949,23 +2742,24 @@ dependencies = [ "librespot-metadata", "log", "parking_lot", + "portable-atomic", "portaudio-rs", - "rand 0.8.5", + "rand 0.9.2", "rand_distr", "rodio", "sdl2", "shell-words", "symphonia", - "thiserror 1.0.69", + "thiserror 2.0.16", "tokio", - "zerocopy 0.7.35", + "zerocopy", ] [[package]] name = "librespot-protocol" -version = "0.6.0" +version = "0.7.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "80802f52b5a1b3a2157454e6aca483a627fbf7b942e0a5fea037ebf3ed8b0546" +checksum = "c647bea4e2c8d19215aabdb93c9c2243f09d1500e68cbd0c894d7a2ed822bb43" dependencies = [ "protobuf", "protobuf-codegen", @@ -3050,7 +2844,7 @@ dependencies = [ "html5ever", "log", "markup5ever_rcdom", - "reqwest 0.12.22", + "reqwest", "serde", "tokio", ] @@ -3068,7 +2862,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "119c8490084af61b44c9eda9d626475847a186737c0378c85e32d77c33a01cd4" dependencies = [ "cc", - "objc2 0.6.1", + "objc2 0.6.2", "objc2-foundation 0.3.1", "time", ] @@ -3161,9 +2955,9 @@ checksum = "32a282da65faaf38286cf3be983213fcf1d2e2a58700e808f83f4ea9a4804bc0" [[package]] name = "memmap2" -version = "0.9.7" +version = "0.9.8" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "483758ad303d734cec05e5c12b41d7e93e6a6390c5e9dae6bdeb7c1259012d28" +checksum = "843a98750cd611cc2965a8213b53b43e715f13c37a9e096c6408e69990961db7" dependencies = [ "libc", ] @@ -3229,35 +3023,21 @@ dependencies = [ "openssl-probe", "openssl-sys", "schannel", - "security-framework 2.11.1", + "security-framework", "security-framework-sys", "tempfile", ] -[[package]] -name = "ndk" -version = "0.8.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2076a31b7010b17a38c01907c45b945e8f11495ee4dd588309718901b1f7a5b7" -dependencies = [ - "bitflags 2.9.1", - "jni-sys", - "log", - "ndk-sys 0.5.0+25.2.9519653", - "num_enum", - "thiserror 1.0.69", -] - [[package]] name = "ndk" version = "0.9.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c3f42e7bbe13d351b6bead8286a43aac9534b82bd3cc43e47037f012ebfd62d4" dependencies = [ - "bitflags 2.9.1", + "bitflags 2.9.3", "jni-sys", "log", - "ndk-sys 0.6.0+11769913", + "ndk-sys", "num_enum", "raw-window-handle", "thiserror 1.0.69", @@ -3269,15 +3049,6 @@ version = "0.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "27b02d87554356db9e9a873add8782d4ea6e3e58ea071a9adb9a2e8ddb884a8b" -[[package]] -name = "ndk-sys" -version = "0.5.0+25.2.9519653" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8c196769dd60fd4f363e11d948139556a344e79d451aeb2fa2fd040738ef7691" -dependencies = [ - "jni-sys", -] - [[package]] name = "ndk-sys" version = "0.6.0+11769913" @@ -3293,12 +3064,6 @@ version = "1.0.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "650eef8c711430f1a879fdd01d4745a7deea475becfb90269c06775983bbf086" -[[package]] -name = "no-std-compat" -version = "0.4.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b93853da6d84c2e3c7d730d6473e8817692dd89be387eb01b94d7f108ecb5b8c" - [[package]] name = "nom" version = "7.1.3" @@ -3361,7 +3126,6 @@ checksum = "a5e44f723f1133c9deac646763579fdb3ac745e418f2a7af9cd0c431da1f20b9" dependencies = [ "num-integer", "num-traits", - "rand 0.8.5", ] [[package]] @@ -3472,16 +3236,16 @@ dependencies = [ [[package]] name = "oauth2" -version = "4.4.2" +version = "5.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c38841cdd844847e3e7c8d29cef9dcfed8877f8f56f9071f77843ecf3baf937f" +checksum = "51e219e79014df21a225b1860a479e2dcd7cbd9130f4defd4bd0e191ea31d67d" dependencies = [ - "base64 0.13.1", + "base64", "chrono", "getrandom 0.2.16", - "http 0.2.12", + "http", "rand 0.8.5", - "reqwest 0.11.27", + "reqwest", "serde", "serde_json", "serde_path_to_error", @@ -3517,9 +3281,9 @@ dependencies = [ [[package]] name = "objc2" -version = "0.6.1" +version = "0.6.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "88c6597e14493ab2e44ce58f2fdecf095a51f12ca57bec060a11c57332520551" +checksum = "561f357ba7f3a2a61563a186a163d0a3a5247e1089524a3981d49adb775078bc" dependencies = [ "objc2-encode", ] @@ -3530,7 +3294,7 @@ version = "0.2.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e4e89ad9e3d7d297152b17d39ed92cd50ca8063a89a9fa569046d41568891eff" dependencies = [ - "bitflags 2.9.1", + "bitflags 2.9.3", "block2 0.5.1", "libc", "objc2 0.5.2", @@ -3540,13 +3304,28 @@ dependencies = [ "objc2-quartz-core", ] +[[package]] +name = "objc2-audio-toolbox" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "10cbe18d879e20a4aea544f8befe38bcf52255eb63d3f23eca2842f3319e4c07" +dependencies = [ + "bitflags 2.9.3", + "libc", + "objc2 0.6.2", + "objc2-core-audio", + "objc2-core-audio-types", + "objc2-core-foundation", + "objc2-foundation 0.3.1", +] + [[package]] name = "objc2-cloud-kit" version = "0.2.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "74dd3b56391c7a0596a295029734d3c1c5e7e510a4cb30245f8221ccea96b009" dependencies = [ - "bitflags 2.9.1", + "bitflags 2.9.3", "block2 0.5.1", "objc2 0.5.2", "objc2-core-location", @@ -3564,13 +3343,35 @@ dependencies = [ "objc2-foundation 0.2.2", ] +[[package]] +name = "objc2-core-audio" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ca44961e888e19313b808f23497073e3f6b3c22bb485056674c8b49f3b025c82" +dependencies = [ + "dispatch2", + "objc2 0.6.2", + "objc2-core-audio-types", + "objc2-core-foundation", +] + +[[package]] +name = "objc2-core-audio-types" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c0f1cc99bb07ad2ddb6527ddf83db6a15271bb036b3eb94b801cd44fdc666ee1" +dependencies = [ + "bitflags 2.9.3", + "objc2 0.6.2", +] + [[package]] name = "objc2-core-data" version = "0.2.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "617fbf49e071c178c0b24c080767db52958f716d9eabdf0890523aeae54773ef" dependencies = [ - "bitflags 2.9.1", + "bitflags 2.9.3", "block2 0.5.1", "objc2 0.5.2", "objc2-foundation 0.2.2", @@ -3582,9 +3383,9 @@ version = "0.3.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1c10c2894a6fed806ade6027bcd50662746363a9589d3ec9d9bef30a4e4bc166" dependencies = [ - "bitflags 2.9.1", + "bitflags 2.9.3", "dispatch2", - "objc2 0.6.1", + "objc2 0.6.2", ] [[package]] @@ -3623,7 +3424,7 @@ version = "0.2.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0ee638a5da3799329310ad4cfa62fbf045d5f56e3ef5ba4149e7452dcf89d5a8" dependencies = [ - "bitflags 2.9.1", + "bitflags 2.9.3", "block2 0.5.1", "dispatch", "libc", @@ -3636,10 +3437,20 @@ version = "0.3.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "900831247d2fe1a09a683278e5384cfb8c80c79fe6b166f9d14bfdde0ea1b03c" dependencies = [ - "bitflags 2.9.1", + "bitflags 2.9.3", "block2 0.6.1", "libc", - "objc2 0.6.1", + "objc2 0.6.2", + "objc2-core-foundation", +] + +[[package]] +name = "objc2-io-kit" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "71c1c64d6120e51cd86033f67176b1cb66780c2efe34dec55176f77befd93c0a" +dependencies = [ + "libc", "objc2-core-foundation", ] @@ -3661,7 +3472,7 @@ version = "0.2.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "dd0cba1276f6023976a406a14ffa85e1fdd19df6b0f737b063b95f6c8c7aadd6" dependencies = [ - "bitflags 2.9.1", + "bitflags 2.9.3", "block2 0.5.1", "objc2 0.5.2", "objc2-foundation 0.2.2", @@ -3673,7 +3484,7 @@ version = "0.2.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e42bee7bff906b14b167da2bac5efe6b6a07e6f7c0a21a7308d40c960242dc7a" dependencies = [ - "bitflags 2.9.1", + "bitflags 2.9.3", "block2 0.5.1", "objc2 0.5.2", "objc2-foundation 0.2.2", @@ -3696,7 +3507,7 @@ version = "0.2.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b8bb46798b20cd6b91cbd113524c490f1686f4c4e8f49502431415f3512e2b6f" dependencies = [ - "bitflags 2.9.1", + "bitflags 2.9.3", "block2 0.5.1", "objc2 0.5.2", "objc2-cloud-kit", @@ -3728,7 +3539,7 @@ version = "0.2.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "76cfcbf642358e8689af64cee815d139339f3ed8ad05103ed5eaf73db8d84cb3" dependencies = [ - "bitflags 2.9.1", + "bitflags 2.9.3", "block2 0.5.1", "objc2 0.5.2", "objc2-core-location", @@ -3744,29 +3555,6 @@ dependencies = [ "memchr", ] -[[package]] -name = "oboe" -version = "0.6.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e8b61bebd49e5d43f5f8cc7ee2891c16e0f41ec7954d36bcb6c14c5e0de867fb" -dependencies = [ - "jni", - "ndk 0.8.0", - "ndk-context", - "num-derive", - "num-traits", - "oboe-sys", -] - -[[package]] -name = "oboe-sys" -version = "0.6.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6c8bb09a4a2b1d668170cfe0a7d5bc103f8999fb316c98099b6a9939c9f2e79d" -dependencies = [ - "cc", -] - [[package]] name = "once_cell" version = "1.21.3" @@ -3779,13 +3567,24 @@ version = "1.70.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a4895175b425cb1f87721b59f0f286c2092bd4af812243672510e1ac53e2e0ad" +[[package]] +name = "open" +version = "5.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e2483562e62ea94312f3576a7aca397306df7990b8d89033e18766744377ef95" +dependencies = [ + "is-wsl", + "libc", + "pathdiff", +] + [[package]] name = "openssl" version = "0.10.73" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8505734d46c8ab1e19a1dce3aef597ad87dcb4c37e7188231769bd6bd51cebf8" dependencies = [ - "bitflags 2.9.1", + "bitflags 2.9.3", "cfg-if", "foreign-types 0.3.2", "libc", @@ -3882,7 +3681,7 @@ dependencies = [ "cfg-if", "libc", "petgraph", - "redox_syscall 0.5.16", + "redox_syscall 0.5.17", "smallvec", "thread-id", "windows-targets 0.52.6", @@ -3894,6 +3693,12 @@ version = "1.0.15" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "57c0d7b74b563b49d38dae00a0c37d4d6de9b432382b2892f0574ddcae73fd0a" +[[package]] +name = "pathdiff" +version = "0.2.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "df94ce210e5bc13cb6651479fa48d14f601d9858cfe0467f43ae157023b938d3" + [[package]] name = "pbkdf2" version = "0.12.2" @@ -3915,9 +3720,9 @@ dependencies = [ [[package]] name = "percent-encoding" -version = "2.3.1" +version = "2.3.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e3148f5046208a5d56bcfc03053e3ca6334e51da8dfb19b6cdc8b306fae3283e" +checksum = "9b4f627cb1b25917193a259e49bdad08f671f8d9708acfd5fe0a8c1455d87220" [[package]] name = "petgraph" @@ -4041,9 +3846,9 @@ dependencies = [ [[package]] name = "polling" -version = "3.9.0" +version = "3.10.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8ee9b2fa7a4517d2c91ff5bc6c297a427a96749d15f98fcdbb22c05571a4d4b7" +checksum = "b5bd19146350fe804f7cb2669c851c03d69da628803dab0d98018142aaa5d829" dependencies = [ "cfg-if", "concurrent-queue", @@ -4101,7 +3906,7 @@ version = "0.2.21" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "85eae3c4ed2f50dcfe72643da4befc30deadb458a9b590d720cde2f2b1e97da9" dependencies = [ - "zerocopy 0.8.26", + "zerocopy", ] [[package]] @@ -4110,16 +3915,6 @@ version = "0.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "925383efa346730478fb4838dbe9137d2a47675ad789c546d150a6e1dd4ab31c" -[[package]] -name = "prettyplease" -version = "0.2.36" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ff24dfcda44452b9816fff4cd4227e1bb73ff5a2f1bc1105aa92fb8565ce44d2" -dependencies = [ - "proc-macro2", - "syn", -] - [[package]] name = "priority-queue" version = "2.5.0" @@ -4142,9 +3937,9 @@ dependencies = [ [[package]] name = "proc-macro2" -version = "1.0.95" +version = "1.0.101" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "02b3e5e68a3a1a02aad3ec490a98007cbc13c37cbe84a3cd7b8e406d76e7f778" +checksum = "89ae43fd86e4158d6db51ad8e2b80f313af9cc74f5c0e03ccb87de09998732de" dependencies = [ "unicode-ident", ] @@ -4194,6 +3989,17 @@ dependencies = [ "thiserror 1.0.69", ] +[[package]] +name = "protobuf-json-mapping" +version = "3.7.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e0d6e4be637b310d8a5c02fa195243328e2d97fa7df1127a27281ef1187fcb1d" +dependencies = [ + "protobuf", + "protobuf-support", + "thiserror 1.0.69", +] + [[package]] name = "protobuf-parse" version = "3.7.2" @@ -4236,21 +4042,21 @@ checksum = "a993555f31e5a609f617c12db6250dedcac1b0a85076912c436e6fc9b2c8e6a3" [[package]] name = "quick-xml" -version = "0.36.2" +version = "0.37.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f7649a7b4df05aed9ea7ec6f628c67c9953a43869b8bc50929569b2999d443fe" +checksum = "331e97a1af0bf59823e6eadffe373d7b27f485be8748f71471c662c1f269b7fb" dependencies = [ "memchr", - "serde", ] [[package]] name = "quick-xml" -version = "0.37.5" +version = "0.38.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "331e97a1af0bf59823e6eadffe373d7b27f485be8748f71471c662c1f269b7fb" +checksum = "d200a41a7797e6461bd04e4e95c3347053a731c32c87f066f2f0dda22dbdbba8" dependencies = [ "memchr", + "serde", ] [[package]] @@ -4264,10 +4070,10 @@ dependencies = [ "pin-project-lite", "quinn-proto", "quinn-udp", - "rustc-hash 2.1.1", - "rustls 0.23.31", + "rustc-hash", + "rustls", "socket2 0.5.10", - "thiserror 2.0.12", + "thiserror 2.0.16", "tokio", "tracing", "web-time", @@ -4284,11 +4090,11 @@ dependencies = [ "lru-slab", "rand 0.9.2", "ring", - "rustc-hash 2.1.1", - "rustls 0.23.31", + "rustc-hash", + "rustls", "rustls-pki-types", "slab", - "thiserror 2.0.12", + "thiserror 2.0.16", "tinyvec", "tracing", "web-time", @@ -4384,12 +4190,12 @@ dependencies = [ [[package]] name = "rand_distr" -version = "0.4.3" +version = "0.5.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "32cb0b9bc82b0a0876c2dd994a7e7a2683d3e7390ca40e6886785ef0c7e3ee31" +checksum = "6a8615d50dcf34fa31f7ab52692afec947c4dd0ab803cc87cb3b0b4570ff7463" dependencies = [ "num-traits", - "rand 0.8.5", + "rand 0.9.2", ] [[package]] @@ -4398,7 +4204,7 @@ version = "0.29.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "eabd94c2f37801c20583fc49dd5cd6b0ba68c716787c2dd6ed18571e1e63117b" dependencies = [ - "bitflags 2.9.1", + "bitflags 2.9.3", "cassowary", "compact_str", "crossterm 0.28.1", @@ -4471,9 +4277,9 @@ checksum = "20675572f6f24e9e76ef639bc5552774ed45f1c30e2951e1e99c59888861c539" [[package]] name = "rayon" -version = "1.10.0" +version = "1.11.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b418a60154510ca1a002a752ca9714984e21e4241e804d32555251faf8b78ffa" +checksum = "368f01d005bf8fd9b1206fb6fa653e6c4a81ceb1466406b81792d87c5677a58f" dependencies = [ "either", "rayon-core", @@ -4481,9 +4287,9 @@ dependencies = [ [[package]] name = "rayon-core" -version = "1.12.1" +version = "1.13.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1465873a3dfdaa8ae7cb14b4383657caab0b3e8a0aa9ae8e04b044854c8dfce2" +checksum = "22e18b0f0062d30d4230b2e85ff77fdfe4326feb054b9783a3460d8435c8ab91" dependencies = [ "crossbeam-deque", "crossbeam-utils", @@ -4500,11 +4306,11 @@ dependencies = [ [[package]] name = "redox_syscall" -version = "0.5.16" +version = "0.5.17" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7251471db004e509f4e75a62cca9435365b5ec7bcdff530d612ac7c87c44a792" +checksum = "5407465600fb0548f1442edf71dd20683c6ed326200ace4b1ef0763521bb3b77" dependencies = [ - "bitflags 2.9.1", + "bitflags 2.9.3", ] [[package]] @@ -4556,69 +4362,30 @@ version = "0.6.29" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f162c6dd7b008981e4d40210aca20b4bd0f9b60ca9271061b07f78537722f2e1" -[[package]] -name = "regex-syntax" -version = "0.8.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2b15c43186be67a4fd63bee50d0303afffcef381492ebe2c5d87f324e1b8815c" - -[[package]] -name = "reqwest" -version = "0.11.27" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "dd67538700a17451e7cba03ac727fb961abb7607553461627b97de0b89cf4a62" -dependencies = [ - "base64 0.21.7", - "bytes", - "encoding_rs", - "futures-core", - "futures-util", - "h2 0.3.27", - "http 0.2.12", - "http-body 0.4.6", - "hyper 0.14.32", - "hyper-rustls 0.24.2", - "ipnet", - "js-sys", - "log", - "mime", - "once_cell", - "percent-encoding", - "pin-project-lite", - "rustls 0.21.12", - "rustls-pemfile 1.0.4", - "serde", - "serde_json", - "serde_urlencoded", - "sync_wrapper 0.1.2", - "system-configuration 0.5.1", - "tokio", - "tokio-rustls 0.24.1", - "tower-service", - "url", - "wasm-bindgen", - "wasm-bindgen-futures", - "web-sys", - "webpki-roots 0.25.4", - "winreg", -] +[[package]] +name = "regex-syntax" +version = "0.8.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2b15c43186be67a4fd63bee50d0303afffcef381492ebe2c5d87f324e1b8815c" [[package]] name = "reqwest" -version = "0.12.22" +version = "0.12.23" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cbc931937e6ca3a06e3b6c0aa7841849b160a90351d6ab467a8b9b9959767531" +checksum = "d429f34c8092b2d42c7c93cec323bb4adeb7c67698f70839adec842ec10c7ceb" dependencies = [ - "base64 0.22.1", + "base64", "bytes", "encoding_rs", + "futures-channel", "futures-core", - "h2 0.4.11", - "http 1.3.1", - "http-body 1.0.1", + "futures-util", + "h2", + "http", + "http-body", "http-body-util", - "hyper 1.6.0", - "hyper-rustls 0.27.7", + "hyper", + "hyper-rustls", "hyper-tls", "hyper-util", "js-sys", @@ -4628,15 +4395,15 @@ dependencies = [ "percent-encoding", "pin-project-lite", "quinn", - "rustls 0.23.31", + "rustls", "rustls-pki-types", "serde", "serde_json", "serde_urlencoded", - "sync_wrapper 1.0.2", + "sync_wrapper", "tokio", "tokio-native-tls", - "tokio-rustls 0.26.2", + "tokio-rustls", "tower", "tower-http", "tower-service", @@ -4644,7 +4411,7 @@ dependencies = [ "wasm-bindgen", "wasm-bindgen-futures", "web-sys", - "webpki-roots 1.0.2", + "webpki-roots", ] [[package]] @@ -4672,12 +4439,13 @@ dependencies = [ [[package]] name = "rodio" -version = "0.19.0" +version = "0.21.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6006a627c1a38d37f3d3a85c6575418cfe34a5392d60a686d0071e1c8d427acb" +checksum = "e40ecf59e742e03336be6a3d53755e789fd05a059fa22dfa0ed624722319e183" dependencies = [ "cpal", - "thiserror 1.0.69", + "dasp_sample", + "num-rational", ] [[package]] @@ -4708,7 +4476,7 @@ checksum = "77beedc33ecff4c39e8ef0e6f7ebc8d849f3ffebbeb786f9997d96f0d9cf4017" dependencies = [ "async-stream", "async-trait", - "base64 0.22.1", + "base64", "chrono", "futures", "getrandom 0.2.16", @@ -4720,7 +4488,7 @@ dependencies = [ "serde", "serde_json", "sha2", - "thiserror 2.0.12", + "thiserror 2.0.16", "url", ] @@ -4733,9 +4501,9 @@ dependencies = [ "async-trait", "log", "maybe-async", - "reqwest 0.12.22", + "reqwest", "serde_json", - "thiserror 2.0.12", + "thiserror 2.0.16", ] [[package]] @@ -4755,7 +4523,7 @@ dependencies = [ "serde", "serde_json", "strum 0.27.2", - "thiserror 2.0.12", + "thiserror 2.0.16", ] [[package]] @@ -4764,12 +4532,6 @@ version = "0.1.26" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "56f7d92ca342cea22a06f2121d944b4fd82af56988c270852495420f961d4ace" -[[package]] -name = "rustc-hash" -version = "1.1.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "08d43f7aa6b08d49f382cde6a7982047c3426db949b1424bc4b7ec9ae12c6ce2" - [[package]] name = "rustc-hash" version = "2.1.1" @@ -4782,7 +4544,7 @@ version = "0.38.44" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "fdb5bc1ae2baa591800df16c9ca78619bf65c0488b41b96ccec5d11220d8c154" dependencies = [ - "bitflags 2.9.1", + "bitflags 2.9.3", "errno", "libc", "linux-raw-sys 0.4.15", @@ -4795,98 +4557,27 @@ version = "1.0.8" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "11181fbabf243db407ef8df94a6ce0b2f9a733bd8be4ad02b4eda9602296cac8" dependencies = [ - "bitflags 2.9.1", + "bitflags 2.9.3", "errno", "libc", "linux-raw-sys 0.9.4", "windows-sys 0.60.2", ] -[[package]] -name = "rustls" -version = "0.21.12" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3f56a14d1f48b391359b22f731fd4bd7e43c97f3c50eee276f3aa09c94784d3e" -dependencies = [ - "log", - "ring", - "rustls-webpki 0.101.7", - "sct", -] - -[[package]] -name = "rustls" -version = "0.22.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bf4ef73721ac7bcd79b2b315da7779d8fc09718c6b3d2d1b2d94850eb8c18432" -dependencies = [ - "log", - "ring", - "rustls-pki-types", - "rustls-webpki 0.102.8", - "subtle", - "zeroize", -] - [[package]] name = "rustls" version = "0.23.31" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c0ebcbd2f03de0fc1122ad9bb24b127a5a6cd51d72604a3f3c50ac459762b6cc" dependencies = [ - "aws-lc-rs", - "log", "once_cell", "ring", "rustls-pki-types", - "rustls-webpki 0.103.4", + "rustls-webpki", "subtle", "zeroize", ] -[[package]] -name = "rustls-native-certs" -version = "0.7.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e5bfb394eeed242e909609f56089eecfe5fda225042e8b171791b9c95f5931e5" -dependencies = [ - "openssl-probe", - "rustls-pemfile 2.2.0", - "rustls-pki-types", - "schannel", - "security-framework 2.11.1", -] - -[[package]] -name = "rustls-native-certs" -version = "0.8.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7fcff2dd52b58a8d98a70243663a0d234c4e2b79235637849d15913394a247d3" -dependencies = [ - "openssl-probe", - "rustls-pki-types", - "schannel", - "security-framework 3.2.0", -] - -[[package]] -name = "rustls-pemfile" -version = "1.0.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1c74cae0a4cf6ccbbf5f359f08efdf8ee7e1dc532573bf0db71968cb56b1448c" -dependencies = [ - "base64 0.21.7", -] - -[[package]] -name = "rustls-pemfile" -version = "2.2.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "dce314e5fee3f39953d46bb63bb8a46d40c2f8fb7cc5a3b6cab2bde9721d6e50" -dependencies = [ - "rustls-pki-types", -] - [[package]] name = "rustls-pki-types" version = "1.12.0" @@ -4897,34 +4588,12 @@ dependencies = [ "zeroize", ] -[[package]] -name = "rustls-webpki" -version = "0.101.7" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8b6275d1ee7a1cd780b64aca7726599a1dbc893b1e64144529e55c3c2f745765" -dependencies = [ - "ring", - "untrusted", -] - -[[package]] -name = "rustls-webpki" -version = "0.102.8" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "64ca1bc8749bd4cf37b5ce386cc146580777b4e8572c7b97baf22c83f444bee9" -dependencies = [ - "ring", - "rustls-pki-types", - "untrusted", -] - [[package]] name = "rustls-webpki" version = "0.103.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0a17884ae0c1b773f1ccd2bd4a8c72f16da897310a98b0e84bf349ad5ead92fc" dependencies = [ - "aws-lc-rs", "ring", "rustls-pki-types", "untrusted", @@ -4932,9 +4601,9 @@ dependencies = [ [[package]] name = "rustversion" -version = "1.0.21" +version = "1.0.22" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8a0d197bd2c9dc6e53b84da9556a69ba4cdfab8619eb41a8bd1cc2027a0f6b1d" +checksum = "b39cdef0fa800fc44525c84ccb54a029961a8215f9619753635a9c0d2538d46d" [[package]] name = "ryu" @@ -4972,16 +4641,6 @@ version = "1.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "94143f37725109f92c262ed2cf5e59bce7498c01bcc1502d7b9afe439a4e9f49" -[[package]] -name = "sct" -version = "0.7.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "da046153aa2352493d6cb7da4b6e5c0c057d8a1d0a9aa8560baffdd945acd414" -dependencies = [ - "ring", - "untrusted", -] - [[package]] name = "sctk-adwaita" version = "0.10.1" @@ -4997,9 +4656,9 @@ dependencies = [ [[package]] name = "sdl2" -version = "0.37.0" +version = "0.38.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3b498da7d14d1ad6c839729bd4ad6fc11d90a57583605f3b4df2cd709a9cd380" +checksum = "2d42407afc6a8ab67e36f92e80b8ba34cbdc55aaeed05249efe9a2e8d0e9feef" dependencies = [ "bitflags 1.3.2", "lazy_static", @@ -5009,9 +4668,9 @@ dependencies = [ [[package]] name = "sdl2-sys" -version = "0.37.0" +version = "0.38.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "951deab27af08ed9c6068b7b0d05a93c91f0a8eb16b6b816a5e73452a43521d3" +checksum = "3ff61407fc75d4b0bbc93dc7e4d6c196439965fbef8e4a4f003a36095823eac0" dependencies = [ "cfg-if", "libc", @@ -5024,21 +4683,8 @@ version = "2.11.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "897b2245f0b511c87893af39b033e5ca9cce68824c4d7e7630b5a1d339658d02" dependencies = [ - "bitflags 2.9.1", - "core-foundation 0.9.4", - "core-foundation-sys", - "libc", - "security-framework-sys", -] - -[[package]] -name = "security-framework" -version = "3.2.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "271720403f46ca04f7ba6f55d438f8bd878d6b8ca0a1046e8228c4145bcbb316" -dependencies = [ - "bitflags 2.9.1", - "core-foundation 0.10.1", + "bitflags 2.9.3", + "core-foundation", "core-foundation-sys", "libc", "security-framework-sys", @@ -5076,9 +4722,9 @@ dependencies = [ [[package]] name = "serde_json" -version = "1.0.141" +version = "1.0.143" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "30b9eff21ebe718216c6ec64e1d9ac57087aad11efc64e32002bce4a0d4c03d3" +checksum = "d401abef1d108fbd9cbaebc3e46611f4b1021f714a0597a71f41ee463f5f4a5a" dependencies = [ "itoa", "memchr", @@ -5201,9 +4847,9 @@ dependencies = [ [[package]] name = "signal-hook-registry" -version = "1.4.5" +version = "1.4.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9203b8055f63a2a00e2f593bb0510367fe707d7ff1e5c872de2f537b339e5410" +checksum = "b2a4719bff48cee6b39d12c020eeb490953ad2443b7055bd0b21fca26bd8c28b" dependencies = [ "libc", ] @@ -5259,9 +4905,9 @@ dependencies = [ [[package]] name = "slab" -version = "0.4.10" +version = "0.4.11" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "04dc19736151f35336d325007ac991178d504a119863a2fcb3758cdb5e52c50d" +checksum = "7a2ae44ef20feb57a68b23d846850f861394c2e02dc425a50098ae8c90267589" [[package]] name = "smallvec" @@ -5275,7 +4921,7 @@ version = "0.19.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3457dea1f0eb631b4034d61d4d8c32074caa6cd1ab2d59f2327bd8461e2c0016" dependencies = [ - "bitflags 2.9.1", + "bitflags 2.9.3", "calloop", "calloop-wayland-source", "cursor-icon", @@ -5329,7 +4975,7 @@ version = "0.8.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5855c8f31521af07d896b852eaa9eca974ddd3211fc2ae292e58dda8eb129bc8" dependencies = [ - "base64 0.22.1", + "base64", "block", "cocoa", "core-graphics 0.22.3", @@ -5400,14 +5046,14 @@ dependencies = [ "rand 0.9.2", "ratatui", "regex", - "reqwest 0.12.22", + "reqwest", "rspotify", - "rustls 0.23.31", + "rustls", "serde", "serde_json", "souvlaki", "tokio", - "toml 0.9.2", + "toml 0.9.5", "tracing", "tracing-subscriber", "ttl_cache", @@ -5602,21 +5248,15 @@ dependencies = [ [[package]] name = "syn" -version = "2.0.104" +version = "2.0.106" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "17b6f705963418cdb9927482fa304bc562ece2fdd4f616084c50b7023b435a40" +checksum = "ede7c438028d4436d71104916910f5bb611972c5cfd7f89b8300a8186e6fada6" dependencies = [ "proc-macro2", "quote", "unicode-ident", ] -[[package]] -name = "sync_wrapper" -version = "0.1.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2047c6ded9c721764247e62cd3b03c09ffc529b2ba5b10ec482ae507a4a70160" - [[package]] name = "sync_wrapper" version = "1.0.2" @@ -5639,26 +5279,16 @@ dependencies = [ [[package]] name = "sysinfo" -version = "0.31.4" +version = "0.36.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "355dbe4f8799b304b05e1b0f05fc59b2a18d36645cf169607da45bde2f69a1be" +checksum = "252800745060e7b9ffb7b2badbd8b31cfa4aa2e61af879d0a3bf2a317c20217d" dependencies = [ - "core-foundation-sys", "libc", "memchr", "ntapi", - "windows 0.57.0", -] - -[[package]] -name = "system-configuration" -version = "0.5.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ba3a3adc5c275d719af8cb4272ea1c4a6d668a777f37e115f6d11ddbc1c8e0e7" -dependencies = [ - "bitflags 1.3.2", - "core-foundation 0.9.4", - "system-configuration-sys 0.5.0", + "objc2-core-foundation", + "objc2-io-kit", + "windows 0.61.3", ] [[package]] @@ -5667,19 +5297,9 @@ version = "0.6.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3c879d448e9d986b661742763247d3693ed13609438cf3d006f51f5368a5ba6b" dependencies = [ - "bitflags 2.9.1", - "core-foundation 0.9.4", - "system-configuration-sys 0.6.0", -] - -[[package]] -name = "system-configuration-sys" -version = "0.5.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a75fb188eb626b924683e3b95e3a48e63551fcfb51949de2f06a9d91dbee93c9" -dependencies = [ - "core-foundation-sys", - "libc", + "bitflags 2.9.3", + "core-foundation", + "system-configuration-sys", ] [[package]] @@ -5711,7 +5331,7 @@ version = "7.0.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e4be53aa0cba896d2dc615bd42bbc130acdcffa239e0a2d965ea5b3b2a86ffdb" dependencies = [ - "cfg-expr 0.20.1", + "cfg-expr 0.20.2", "heck", "pkg-config", "toml 0.8.23", @@ -5737,22 +5357,22 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0b1e66e07de489fe43a46678dd0b8df65e0c973909df1b60ba33874e297ba9b9" dependencies = [ "quick-xml 0.37.5", - "thiserror 2.0.12", + "thiserror 2.0.16", "windows 0.61.3", "windows-version", ] [[package]] name = "tempfile" -version = "3.20.0" +version = "3.21.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e8a64e3985349f2441a1a9ef0b853f869006c3855f2cda6862a94d26ebb9d6a1" +checksum = "15b61f8f20e3a6f7e0649d825294eaf317edce30f82cf6026e7e4cb9222a7d1e" dependencies = [ "fastrand", "getrandom 0.3.3", "once_cell", "rustix 1.0.8", - "windows-sys 0.59.0", + "windows-sys 0.60.2", ] [[package]] @@ -5786,11 +5406,11 @@ dependencies = [ [[package]] name = "thiserror" -version = "2.0.12" +version = "2.0.16" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "567b8a2dae586314f7be2a752ec7474332959c6460e02bde30d702a66d488708" +checksum = "3467d614147380f2e4e374161426ff399c91084acd2363eaf549172b3d5e60c0" dependencies = [ - "thiserror-impl 2.0.12", + "thiserror-impl 2.0.16", ] [[package]] @@ -5806,9 +5426,9 @@ dependencies = [ [[package]] name = "thiserror-impl" -version = "2.0.12" +version = "2.0.16" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7f7cf42b4507d8ea322120659672cf1b9dbb93f8f2d4ecfd6e51350ff5b17a1d" +checksum = "6c5e1be1c48b9172ee610da68fd9cd2770e7a4056cb3fc98710ee6906f0c7960" dependencies = [ "proc-macro2", "quote", @@ -5915,9 +5535,9 @@ dependencies = [ [[package]] name = "tinyvec" -version = "1.9.0" +version = "1.10.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "09b3661f17e86524eccd4371ab0429194e0d7c008abb45f7a7495b1719463c71" +checksum = "bfa5fdc3bce6191a1dbc8c02d5c8bffcf557bafa17c124c5264a458f1b0613fa" dependencies = [ "tinyvec_macros", ] @@ -5930,9 +5550,9 @@ checksum = "1f3ccbac311fea05f86f61904b462b55fb3df8837a366dfc601a0161d0532f20" [[package]] name = "tokio" -version = "1.47.0" +version = "1.47.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "43864ed400b6043a4757a25c7a64a8efde741aed79a056a2fb348a406701bb35" +checksum = "89e49afdadebb872d3145a5638b59eb0691ea23e46ca484037cfab3b76b95038" dependencies = [ "backtrace", "bytes", @@ -5968,34 +5588,13 @@ dependencies = [ "tokio", ] -[[package]] -name = "tokio-rustls" -version = "0.24.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c28327cf380ac148141087fbfb9de9d7bd4e84ab5d2c28fbc911d753de8a7081" -dependencies = [ - "rustls 0.21.12", - "tokio", -] - -[[package]] -name = "tokio-rustls" -version = "0.25.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "775e0c0f0adb3a2f22a00c4745d728b479985fc15ee7ca6a2608388c5569860f" -dependencies = [ - "rustls 0.22.4", - "rustls-pki-types", - "tokio", -] - [[package]] name = "tokio-rustls" version = "0.26.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8e727b36a1a0e8b74c376ac2211e40c2c8af09fb4013c60d910495810f008e9b" dependencies = [ - "rustls 0.23.31", + "rustls", "tokio", ] @@ -6012,25 +5611,23 @@ dependencies = [ [[package]] name = "tokio-tungstenite" -version = "0.24.0" +version = "0.27.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "edc5f74e248dc973e0dbb7b74c7e0d6fcc301c694ff50049504004ef4d0cdcd9" +checksum = "489a59b6730eda1b0171fcfda8b121f4bee2b35cba8645ca35c5f7ba3eb736c1" dependencies = [ "futures-util", "log", - "rustls 0.23.31", - "rustls-native-certs 0.8.1", - "rustls-pki-types", + "native-tls", "tokio", - "tokio-rustls 0.26.2", + "tokio-native-tls", "tungstenite", ] [[package]] name = "tokio-util" -version = "0.7.15" +version = "0.7.16" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "66a539a9ad6d5d281510d5bd368c973d636c02dbf8a67300bfb6b950696ad7df" +checksum = "14307c986784f72ef81c89db7d9e28d6ac26d16213b109ea501696195e6e3ce5" dependencies = [ "bytes", "futures-core", @@ -6053,9 +5650,9 @@ dependencies = [ [[package]] name = "toml" -version = "0.9.2" +version = "0.9.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ed0aee96c12fa71097902e0bb061a5e1ebd766a6636bb605ba401c45c1650eac" +checksum = "75129e1dc5000bfbaa9fee9d1b21f974f9fbad9daec557a521ee6e080825f6e8" dependencies = [ "indexmap", "serde", @@ -6099,9 +5696,9 @@ dependencies = [ [[package]] name = "toml_parser" -version = "1.0.1" +version = "1.0.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "97200572db069e74c512a14117b296ba0a80a30123fbbb5aa1f4a348f639ca30" +checksum = "b551886f449aa90d4fe2bdaa9f4a2577ad2dde302c61ecf262d80b116db95c10" dependencies = [ "winnow", ] @@ -6121,7 +5718,7 @@ dependencies = [ "futures-core", "futures-util", "pin-project-lite", - "sync_wrapper 1.0.2", + "sync_wrapper", "tokio", "tower-layer", "tower-service", @@ -6133,11 +5730,11 @@ version = "0.6.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "adc82fd73de2a9722ac5da747f12383d2bfdb93591ee6c58486e0097890f05f2" dependencies = [ - "bitflags 2.9.1", + "bitflags 2.9.3", "bytes", "futures-util", - "http 1.3.1", - "http-body 1.0.1", + "http", + "http-body", "iri-string", "pin-project-lite", "tower", @@ -6241,21 +5838,19 @@ dependencies = [ [[package]] name = "tungstenite" -version = "0.24.0" +version = "0.27.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "18e5b8366ee7a95b16d32197d0b2604b43a0be89dc5fac9f8e96ccafbaedda8a" +checksum = "eadc29d668c91fcc564941132e17b28a7ceb2f3ebf0b9dae3e03fd7a6748eb0d" dependencies = [ - "byteorder", "bytes", "data-encoding", - "http 1.3.1", + "http", "httparse", "log", - "rand 0.8.5", - "rustls 0.23.31", - "rustls-pki-types", + "native-tls", + "rand 0.9.2", "sha1", - "thiserror 1.0.69", + "thiserror 2.0.16", "utf-8", ] @@ -6314,9 +5909,9 @@ checksum = "8ecb6da28b8a351d773b68d5825ac39017e680750f980f3a1a85cd8dd28a47c1" [[package]] name = "url" -version = "2.5.4" +version = "2.5.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "32f8b686cadd1473f4bd0117a5d28d36b1ade384ea9b5069a1c40aefed7fda60" +checksum = "08bc136a29a3d1758e07a9cca267be308aeebf5cfd5a10f3f67ab2097683ef5b" dependencies = [ "form_urlencoded", "idna", @@ -6350,12 +5945,13 @@ checksum = "06abde3611657adf66d383f00b093d7faecc7fa57071cce2578660c9f1010821" [[package]] name = "uuid" -version = "1.17.0" +version = "1.18.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3cf4199d1e5d15ddd86a694e4d0dffa9c323ce759fea589f00fef9d81cc1931d" +checksum = "f33196643e165781c20a5ead5582283a7dacbb87855d867fbc2df3f81eddc1be" dependencies = [ "getrandom 0.3.3", - "rand 0.9.2", + "js-sys", + "wasm-bindgen", ] [[package]] @@ -6444,7 +6040,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0ae7c6870b98c838123f22cac9a594cbe2d74ea48d79271c08f8c9e680b40fac" dependencies = [ "ansi_colours", - "base64 0.22.1", + "base64", "console", "crossterm 0.28.1", "image", @@ -6561,13 +6157,13 @@ dependencies = [ [[package]] name = "wayland-backend" -version = "0.3.10" +version = "0.3.11" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fe770181423e5fc79d3e2a7f4410b7799d5aab1de4372853de3c6aa13ca24121" +checksum = "673a33c33048a5ade91a6b139580fa174e19fb0d23f396dca9fa15f2e1e49b35" dependencies = [ "cc", "downcast-rs", - "rustix 0.38.44", + "rustix 1.0.8", "scoped-tls", "smallvec", "wayland-sys", @@ -6575,12 +6171,12 @@ dependencies = [ [[package]] name = "wayland-client" -version = "0.31.10" +version = "0.31.11" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "978fa7c67b0847dbd6a9f350ca2569174974cd4082737054dbb7fbb79d7d9a61" +checksum = "c66a47e840dc20793f2264eb4b3e4ecb4b75d91c0dd4af04b456128e0bdd449d" dependencies = [ - "bitflags 2.9.1", - "rustix 0.38.44", + "bitflags 2.9.3", + "rustix 1.0.8", "wayland-backend", "wayland-scanner", ] @@ -6591,29 +6187,29 @@ version = "0.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "625c5029dbd43d25e6aa9615e88b829a5cad13b2819c4ae129fdbb7c31ab4c7e" dependencies = [ - "bitflags 2.9.1", + "bitflags 2.9.3", "cursor-icon", "wayland-backend", ] [[package]] name = "wayland-cursor" -version = "0.31.10" +version = "0.31.11" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a65317158dec28d00416cb16705934070aef4f8393353d41126c54264ae0f182" +checksum = "447ccc440a881271b19e9989f75726d60faa09b95b0200a9b7eb5cc47c3eeb29" dependencies = [ - "rustix 0.38.44", + "rustix 1.0.8", "wayland-client", "xcursor", ] [[package]] name = "wayland-protocols" -version = "0.32.8" +version = "0.32.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "779075454e1e9a521794fed15886323ea0feda3f8b0fc1390f5398141310422a" +checksum = "efa790ed75fbfd71283bd2521a1cfdc022aabcc28bdcff00851f9e4ae88d9901" dependencies = [ - "bitflags 2.9.1", + "bitflags 2.9.3", "wayland-backend", "wayland-client", "wayland-scanner", @@ -6621,11 +6217,11 @@ dependencies = [ [[package]] name = "wayland-protocols-plasma" -version = "0.3.8" +version = "0.3.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4fd38cdad69b56ace413c6bcc1fbf5acc5e2ef4af9d5f8f1f9570c0c83eae175" +checksum = "a07a14257c077ab3279987c4f8bb987851bf57081b93710381daea94f2c2c032" dependencies = [ - "bitflags 2.9.1", + "bitflags 2.9.3", "wayland-backend", "wayland-client", "wayland-protocols", @@ -6634,11 +6230,11 @@ dependencies = [ [[package]] name = "wayland-protocols-wlr" -version = "0.3.8" +version = "0.3.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1cb6cdc73399c0e06504c437fe3cf886f25568dd5454473d565085b36d6a8bbf" +checksum = "efd94963ed43cf9938a090ca4f7da58eb55325ec8200c3848963e98dc25b78ec" dependencies = [ - "bitflags 2.9.1", + "bitflags 2.9.3", "wayland-backend", "wayland-client", "wayland-protocols", @@ -6647,9 +6243,9 @@ dependencies = [ [[package]] name = "wayland-scanner" -version = "0.31.6" +version = "0.31.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "896fdafd5d28145fce7958917d69f2fd44469b1d4e861cb5961bcbeebc6d1484" +checksum = "54cb1e9dc49da91950bdfd8b848c49330536d9d1fb03d4bfec8cae50caa50ae3" dependencies = [ "proc-macro2", "quick-xml 0.37.5", @@ -6658,9 +6254,9 @@ dependencies = [ [[package]] name = "wayland-sys" -version = "0.31.6" +version = "0.31.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "dbcebb399c77d5aa9fa5db874806ee7b4eba4e73650948e8f93963f128896615" +checksum = "34949b42822155826b41db8e5d0c1be3a2bd296c747577a43a3e6daefc296142" dependencies = [ "dlib", "log", @@ -6688,22 +6284,6 @@ dependencies = [ "wasm-bindgen", ] -[[package]] -name = "webpki" -version = "0.22.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ed63aea5ce73d0ff405984102c42de94fc55a6b75765d621c65262469b3c9b53" -dependencies = [ - "ring", - "untrusted", -] - -[[package]] -name = "webpki-roots" -version = "0.25.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5f20c57d8d7db6d3b86154206ae5d8fba62dd39573114de97c2cb0578251f8e1" - [[package]] name = "webpki-roots" version = "1.0.2" @@ -6760,11 +6340,11 @@ checksum = "ac3b87c63620426dd9b991e5ce0329eff545bccbbb34f3be09ff6fb6ab51b7b6" [[package]] name = "winapi-util" -version = "0.1.9" +version = "0.1.10" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cf221c93e13a30d793f7645a0e7762c55d169dbb0a49671918a2319d289b10bb" +checksum = "0978bf7171b3d90bac376700cb56d606feb40f251a475a5d6634613564460b22" dependencies = [ - "windows-sys 0.59.0", + "windows-sys 0.60.2", ] [[package]] @@ -6792,16 +6372,6 @@ dependencies = [ "windows-targets 0.52.6", ] -[[package]] -name = "windows" -version = "0.57.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "12342cb4d8e3b046f3d80effd474a7a02447231330ef77d71daa6fbc40681143" -dependencies = [ - "windows-core 0.57.0", - "windows-targets 0.52.6", -] - [[package]] name = "windows" version = "0.58.0" @@ -6844,18 +6414,6 @@ dependencies = [ "windows-targets 0.52.6", ] -[[package]] -name = "windows-core" -version = "0.57.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d2ed2439a290666cd67ecce2b0ffaad89c2a56b976b736e6ece670297897832d" -dependencies = [ - "windows-implement 0.57.0", - "windows-interface 0.57.0", - "windows-result 0.1.2", - "windows-targets 0.52.6", -] - [[package]] name = "windows-core" version = "0.58.0" @@ -6893,17 +6451,6 @@ dependencies = [ "windows-threading", ] -[[package]] -name = "windows-implement" -version = "0.57.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9107ddc059d5b6fbfbffdfa7a7fe3e22a226def0b2608f72e9d552763d3e1ad7" -dependencies = [ - "proc-macro2", - "quote", - "syn", -] - [[package]] name = "windows-implement" version = "0.58.0" @@ -6926,17 +6473,6 @@ dependencies = [ "syn", ] -[[package]] -name = "windows-interface" -version = "0.57.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "29bee4b38ea3cde66011baa44dba677c432a78593e202392d1e9070cf2a7fca7" -dependencies = [ - "proc-macro2", - "quote", - "syn", -] - [[package]] name = "windows-interface" version = "0.58.0" @@ -7041,15 +6577,6 @@ dependencies = [ "windows-targets 0.42.2", ] -[[package]] -name = "windows-sys" -version = "0.48.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "677d2418bec65e3338edb076e806bc1ec15693c5d0104683f2efe857f61056a9" -dependencies = [ - "windows-targets 0.48.5", -] - [[package]] name = "windows-sys" version = "0.52.0" @@ -7074,7 +6601,7 @@ version = "0.60.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f2f500e4d28234f72040990ec9d39e3a6b950f9f22d3dba18416c35882612bcb" dependencies = [ - "windows-targets 0.53.2", + "windows-targets 0.53.3", ] [[package]] @@ -7125,10 +6652,11 @@ dependencies = [ [[package]] name = "windows-targets" -version = "0.53.2" +version = "0.53.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c66f69fcc9ce11da9966ddb31a40968cad001c5bedeb5c2b82ede4253ab48aef" +checksum = "d5fe6031c4041849d7c496a8ded650796e7b6ecc19df1a431c1a363342e5dc91" dependencies = [ + "windows-link", "windows_aarch64_gnullvm 0.53.0", "windows_aarch64_msvc 0.53.0", "windows_i686_gnu 0.53.0", @@ -7346,20 +6874,20 @@ dependencies = [ "ahash", "android-activity", "atomic-waker", - "bitflags 2.9.1", + "bitflags 2.9.3", "block2 0.5.1", "bytemuck", "calloop", "cfg_aliases", "concurrent-queue", - "core-foundation 0.9.4", + "core-foundation", "core-graphics 0.23.2", "cursor-icon", "dpi", "js-sys", "libc", "memmap2", - "ndk 0.9.0", + "ndk", "objc2 0.5.2", "objc2-app-kit", "objc2-foundation 0.2.2", @@ -7391,23 +6919,13 @@ dependencies = [ [[package]] name = "winnow" -version = "0.7.12" +version = "0.7.13" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f3edebf492c8125044983378ecb5766203ad3b4c2f7a922bd7dd207f6d443e95" +checksum = "21a0236b59786fed61e2a80582dd500fe61f18b5dca67a4a067d0bc9039339cf" dependencies = [ "memchr", ] -[[package]] -name = "winreg" -version = "0.50.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "524e57b2c537c0f9b1e69f1965311ec12182b4122e45035b1508cd24d2adadb1" -dependencies = [ - "cfg-if", - "windows-sys 0.48.0", -] - [[package]] name = "winsafe" version = "0.0.19" @@ -7420,7 +6938,7 @@ version = "0.39.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6f42320e61fe2cfd34354ecb597f86f413484a798ba44a8ca1165c58d42da6c1" dependencies = [ - "bitflags 2.9.1", + "bitflags 2.9.3", ] [[package]] @@ -7473,7 +6991,7 @@ version = "0.4.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d039de8032a9a8856a6be89cea3e5d12fdd82306ab7c94d74e6deab2460651c5" dependencies = [ - "bitflags 2.9.1", + "bitflags 2.9.3", "dlib", "log", "once_cell", @@ -7521,34 +7039,13 @@ dependencies = [ "synstructure", ] -[[package]] -name = "zerocopy" -version = "0.7.35" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1b9b4fd18abc82b8136838da5d50bae7bdea537c574d8dc1a34ed098d6c166f0" -dependencies = [ - "byteorder", - "zerocopy-derive 0.7.35", -] - [[package]] name = "zerocopy" version = "0.8.26" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1039dd0d3c310cf05de012d8a39ff557cb0d23087fd44cad61df08fc31907a2f" dependencies = [ - "zerocopy-derive 0.8.26", -] - -[[package]] -name = "zerocopy-derive" -version = "0.7.35" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fa4f8080344d4671fb4e831a13ad1e68092748387dfc4f55e356242fae12ce3e" -dependencies = [ - "proc-macro2", - "quote", - "syn", + "zerocopy-derive", ] [[package]] @@ -7602,9 +7099,9 @@ dependencies = [ [[package]] name = "zerovec" -version = "0.11.2" +version = "0.11.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4a05eb080e015ba39cc9e23bbe5e7fb04d5fb040350f99f34e338d5fdd294428" +checksum = "e7aa2bd55086f1ab526693ecbe444205da57e25f4489879da80635a46d90e73b" dependencies = [ "yoke", "zerofrom", @@ -7639,9 +7136,9 @@ dependencies = [ [[package]] name = "zune-jpeg" -version = "0.4.19" +version = "0.4.20" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2c9e525af0a6a658e031e95f14b7f889976b74a11ba0eca5a5fc9ac8a1c43a6a" +checksum = "fc1f7e205ce79eb2da3cd71c5f55f3589785cb7c79f6a03d1c8d1491bda5d089" dependencies = [ "zune-core", ] diff --git a/spotify_player/Cargo.toml b/spotify_player/Cargo.toml index a5c812d2..b9542ccb 100644 --- a/spotify_player/Cargo.toml +++ b/spotify_player/Cargo.toml @@ -15,11 +15,11 @@ clap = { version = "4.5.41", features = ["derive", "string"] } config_parser2 = "0.1.6" crossterm = "0.29.0" dirs-next = "2.0.0" -librespot-connect = { version = "0.6.0", optional = true } -librespot-core = "0.6.0" -librespot-oauth = "0.6.0" -librespot-playback = { version = "0.6.0", optional = true } -librespot-metadata = "0.6.0" +librespot-connect = { version = "0.7.0", optional = true } +librespot-core = "0.7.0" +librespot-oauth = "0.7.0" +librespot-playback = { version = "0.7.0", optional = true } +librespot-metadata = "0.7.0" log = "0.4.27" chrono = "0.4.41" reqwest = { version = "0.12.22", features = ["json"] } diff --git a/spotify_player/src/auth.rs b/spotify_player/src/auth.rs index f3b7646e..8464876b 100644 --- a/spotify_player/src/auth.rs +++ b/spotify_player/src/auth.rs @@ -1,8 +1,7 @@ +use crate::config; use anyhow::Result; use librespot_core::{authentication::Credentials, cache::Cache, config::SessionConfig, Session}; -use librespot_oauth::get_access_token; - -use crate::config; +use librespot_oauth::OAuthClientBuilder; pub const SPOTIFY_CLIENT_ID: &str = "65b708073fc0480ea92a077233ca87bd"; // based on https://github.com/librespot-org/librespot/blob/f96f36c064795011f9fee912291eecb1aa46fff6/src/main.rs#L173 @@ -96,12 +95,16 @@ pub fn get_creds(auth_config: &AuthConfig, reauth: bool, use_cached: bool) -> Re let msg = "No cached credentials found, please authenticate the application first."; if reauth { eprintln!("{msg}"); - get_access_token( + + let client_builder = OAuthClientBuilder::new( SPOTIFY_CLIENT_ID, &auth_config.login_redirect_uri, OAUTH_SCOPES.to_vec(), - ) - .map(|t| Credentials::with_access_token(t.access_token))? + ); + let oauth_client = client_builder.build()?; + oauth_client + .get_access_token() + .map(|t| Credentials::with_access_token(t.access_token))? } else { anyhow::bail!(msg); } diff --git a/spotify_player/src/client/mod.rs b/spotify_player/src/client/mod.rs index de71a663..51cb7bee 100644 --- a/spotify_player/src/client/mod.rs +++ b/spotify_player/src/client/mod.rs @@ -46,7 +46,7 @@ pub struct Client { spotify: Arc, auth_config: AuthConfig, #[cfg(feature = "streaming")] - stream_conn: Arc>>, + stream_conn: Arc>>, } impl Deref for Client { diff --git a/spotify_player/src/config/mod.rs b/spotify_player/src/config/mod.rs index 58ec49aa..63f5d5ef 100644 --- a/spotify_player/src/config/mod.rs +++ b/spotify_player/src/config/mod.rs @@ -101,7 +101,6 @@ pub struct AppConfig { #[cfg(feature = "media-control")] pub enable_media_control: bool, - #[cfg(feature = "streaming")] pub enable_streaming: StreamingType, #[cfg(feature = "notify")] @@ -323,7 +322,6 @@ impl Default for AppConfig { #[cfg(all(unix, not(target_os = "macos")))] enable_media_control: true, - #[cfg(feature = "streaming")] enable_streaming: StreamingType::Always, #[cfg(feature = "notify")] diff --git a/spotify_player/src/event/clipboard.rs b/spotify_player/src/event/clipboard.rs index fb06cfa3..665caa82 100644 --- a/spotify_player/src/event/clipboard.rs +++ b/spotify_player/src/event/clipboard.rs @@ -109,7 +109,7 @@ pub fn get_clipboard_provider() -> Box { #[cfg(not(target_os = "windows"))] { tracing::warn!("No clipboard provider found! Fallback to a NOP clipboard provider."); - return Box::new(NopProvider {}); + Box::new(NopProvider {}) } } } diff --git a/spotify_player/src/state/ui/page.rs b/spotify_player/src/state/ui/page.rs index 8451ed31..cd8a401c 100644 --- a/spotify_player/src/state/ui/page.rs +++ b/spotify_player/src/state/ui/page.rs @@ -164,7 +164,7 @@ impl PageState { } /// The currently focused window state of the page. - pub fn focus_window_state_mut(&mut self) -> Option { + pub fn focus_window_state_mut(&mut self) -> Option> { match self { Self::Library { state: diff --git a/spotify_player/src/streaming.rs b/spotify_player/src/streaming.rs index 48b79ed0..74dd7e31 100644 --- a/spotify_player/src/streaming.rs +++ b/spotify_player/src/streaming.rs @@ -1,6 +1,6 @@ use crate::{client::Client, config, state::SharedState}; use anyhow::Context; -use librespot_connect::{config::ConnectConfig, spirc::Spirc}; +use librespot_connect::{ConnectConfig, Spirc}; use librespot_core::authentication::Credentials; use librespot_core::Session; use librespot_core::{config::DeviceType, spotify_id}; @@ -156,17 +156,20 @@ pub async fn new_connection( let connect_config = ConnectConfig { name: device.name.clone(), device_type: device.device_type.parse::().unwrap_or_default(), - initial_volume: Some(volume), + initial_volume: volume, // non-configurable fields, use default values. // We may allow users to configure these fields in a future release - has_volume_ctrl: true, is_group: false, + disable_volume: false, + volume_steps: 64, }; tracing::info!("Application's connect configurations: {:?}", connect_config); - let mixer = Arc::new(mixer::softmixer::SoftMixer::open(MixerConfig::default())); + let mixer = Arc::new( + mixer::softmixer::SoftMixer::open(MixerConfig::default()).context("opening softmixer")?, + ); mixer.set_volume(volume); let backend = audio_backend::find(None).expect("should be able to find an audio backend"); diff --git a/spotify_player/src/token.rs b/spotify_player/src/token.rs index e5b0fe44..cbaec371 100644 --- a/spotify_player/src/token.rs +++ b/spotify_player/src/token.rs @@ -6,43 +6,16 @@ use librespot_core::session::Session; const TIMEOUT_IN_SECS: u64 = 5; -/// The application authentication token's permission scopes -const SCOPES: [&str; 15] = [ - "user-read-recently-played", - "user-top-read", - "user-read-playback-position", - "user-read-playback-state", - "user-modify-playback-state", - "user-read-currently-playing", - "streaming", - "playlist-read-private", - "playlist-modify-private", - "playlist-modify-public", - "playlist-read-collaborative", - "user-follow-read", - "user-follow-modify", - "user-library-read", - "user-library-modify", -]; - pub async fn get_token_librespot( session: &Session, - client_id: &str, + _client_id: &str, ) -> Result { - let query_uri = format!( - "hm://keymaster/token/authenticated?scope={}&client_id={}&device_id={}", - SCOPES.join(","), - client_id, - session.device_id(), - ); - let request = session.mercury().get(query_uri)?; - let response = request.await?; - let data = response - .payload - .first() - .ok_or(librespot_core::token::TokenError::Empty)? - .clone(); - let token = librespot_core::token::Token::from_json(String::from_utf8(data)?)?; + // TODO: figure out how to support custom client_id for Spotify Connect + let auth_data = session.auth_data(); + if auth_data.is_empty() { + anyhow::bail!("Session has no stored credentials for login5 token acquisition"); + } + let token = session.login5().auth_token().await.unwrap(); Ok(token) } diff --git a/spotify_player/src/ui/page.rs b/spotify_player/src/ui/page.rs index 6b1b3831..fdbf52a3 100644 --- a/spotify_player/src/ui/page.rs +++ b/spotify_player/src/ui/page.rs @@ -276,15 +276,12 @@ pub fn render_context_page( ); // 3+4. Construct and render the page's widgets - let id = match id { - None => { - frame.render_widget( - Paragraph::new("Cannot determine the current page's context"), - rect, - ); - return; - } - Some(id) => id, + let Some(id) = id else { + frame.render_widget( + Paragraph::new("Cannot determine the current page's context"), + rect, + ); + return; }; let data = state.data.read(); diff --git a/spotify_player/src/utils.rs b/spotify_player/src/utils.rs index a8d905d1..95147423 100644 --- a/spotify_player/src/utils.rs +++ b/spotify_player/src/utils.rs @@ -37,7 +37,7 @@ pub fn get_episode_show_image_url(episode: &rspotify::model::FullEpisode) -> Opt } } -pub fn parse_uri(uri: &str) -> Cow { +pub fn parse_uri(uri: &str) -> Cow<'_, str> { let parts = uri.split(':').collect::>(); // The below URI probably has a format of `spotify:user:{user_id}:{type}:{id}`, // but `rspotify` library expects to receive an URI of format `spotify:{type}:{id}`. From e619615474d6731958201e36e651f7e44bab0530 Mon Sep 17 00:00:00 2001 From: Thang Pham Date: Mon, 25 Aug 2025 00:25:13 -0400 Subject: [PATCH 14/16] Make Spotify Connect integration work with new authentication changes (#809) Resolves #804 - define a custom client to get user's available devices for Spotify Connect integration - update `client_id` general config to be optional + setting one will enable "user-provided client integration" which is requred for Spotify Connect feature - cleanup authentication codes - update documentation --- Cargo.lock | 45 +++++++-- README.md | 138 +++++++++++++------------- docs/config.md | 6 +- examples/app.toml | 1 - spotify_player/Cargo.toml | 2 +- spotify_player/src/auth.rs | 37 +++---- spotify_player/src/cli/client.rs | 22 ++-- spotify_player/src/cli/handlers.rs | 5 +- spotify_player/src/client/handlers.rs | 2 +- spotify_player/src/client/mod.rs | 95 ++++++++++-------- spotify_player/src/client/spotify.rs | 22 +--- spotify_player/src/config/mod.rs | 9 +- spotify_player/src/main.rs | 7 +- spotify_player/src/streaming.rs | 4 +- spotify_player/src/token.rs | 47 ++++----- 15 files changed, 229 insertions(+), 213 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index a44f5097..3494c68b 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -621,7 +621,7 @@ dependencies = [ "bitflags 1.3.2", "block", "cocoa-foundation", - "core-foundation", + "core-foundation 0.9.4", "core-graphics 0.22.3", "foreign-types 0.3.2", "libc", @@ -636,7 +636,7 @@ checksum = "8c6234cbb2e4c785b456c0644748b1ac416dd045799740356f8363dfe00c93f7" dependencies = [ "bitflags 1.3.2", "block", - "core-foundation", + "core-foundation 0.9.4", "core-graphics-types", "libc", "objc", @@ -747,6 +747,16 @@ dependencies = [ "libc", ] +[[package]] +name = "core-foundation" +version = "0.10.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b2a6cd9ae233e7f62ba4e9353e81a88df7fc8a5987b8d445b4d90c879bd156f6" +dependencies = [ + "core-foundation-sys", + "libc", +] + [[package]] name = "core-foundation-sys" version = "0.8.7" @@ -760,7 +770,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2581bbab3b8ffc6fcbd550bf46c355135d16e9ff2a6ea032ad6b9bf1d7efe4fb" dependencies = [ "bitflags 1.3.2", - "core-foundation", + "core-foundation 0.9.4", "core-graphics-types", "foreign-types 0.3.2", "libc", @@ -773,7 +783,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c07782be35f9e1140080c6b96f0d44b739e2278479f64e02fdab4e32dfd8b081" dependencies = [ "bitflags 1.3.2", - "core-foundation", + "core-foundation 0.9.4", "core-graphics-types", "foreign-types 0.5.0", "libc", @@ -786,7 +796,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "45390e6114f68f718cc7a830514a96f903cccd70d02a8f6d9f643ac4ba45afaf" dependencies = [ "bitflags 1.3.2", - "core-foundation", + "core-foundation 0.9.4", "libc", ] @@ -4490,6 +4500,7 @@ dependencies = [ "sha2", "thiserror 2.0.16", "url", + "webbrowser", ] [[package]] @@ -4684,7 +4695,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "897b2245f0b511c87893af39b033e5ca9cce68824c4d7e7630b5a1d339658d02" dependencies = [ "bitflags 2.9.3", - "core-foundation", + "core-foundation 0.9.4", "core-foundation-sys", "libc", "security-framework-sys", @@ -5298,7 +5309,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3c879d448e9d986b661742763247d3693ed13609438cf3d006f51f5368a5ba6b" dependencies = [ "bitflags 2.9.3", - "core-foundation", + "core-foundation 0.9.4", "system-configuration-sys", ] @@ -6284,6 +6295,24 @@ dependencies = [ "wasm-bindgen", ] +[[package]] +name = "webbrowser" +version = "1.0.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ea9fe1ebb156110ff855242c1101df158b822487e4957b0556d9ffce9db0f535" +dependencies = [ + "block2 0.5.1", + "core-foundation 0.10.1", + "home", + "jni", + "log", + "ndk-context", + "objc2 0.5.2", + "objc2-foundation 0.2.2", + "url", + "web-sys", +] + [[package]] name = "webpki-roots" version = "1.0.2" @@ -6880,7 +6909,7 @@ dependencies = [ "calloop", "cfg_aliases", "concurrent-queue", - "core-foundation", + "core-foundation 0.9.4", "core-graphics 0.23.2", "cursor-icon", "dpi", diff --git a/README.md b/README.md index 62a62dd1..9cf16daf 100644 --- a/README.md +++ b/README.md @@ -69,12 +69,15 @@ A Spotify Premium account is **required**. ```shell sudo apt install libssl-dev libasound2-dev libdbus-1-dev ``` + - On RHEL/Fedora based systems, run the below command to install application's dependencies : ```shell sudo dnf install openssl-devel alsa-lib-devel dbus-devel ``` + or if you're using `yum`: + ```shell sudo yum install openssl-devel alsa-lib-devel dbus-devel ``` @@ -103,7 +106,6 @@ Run `pacman -S spotify-player` to install the application. **Note**: Defaults to PulseAudio / Pipewire audio backend. For a different one, please consider modifying the [official PKGBUILD](https://gitlab.archlinux.org/archlinux/packaging/packages/spotify-player) and rebuilding it manually. See [Audio Backends](#audio-backend) for a list of options. - ### Void Linux Run `xbps-install -S spotify-player` to install the application. @@ -160,11 +162,13 @@ docker run --rm \ ### Spotify Connect -To enable a full [Spotify connect](https://www.spotify.com/us/connect/) support, user will need to register a Spotify application and specify the application's `client_id` in the general configuration file as described in the [configuration documentation](docs/config.md#general). +To enable a full [Spotify connect](https://www.spotify.com/us/connect/) support, users will need to enable a _"user-provided client integration"_. + +This integration can be done by following [this documentation](https://developer.spotify.com/documentation/general/guides/authorization/app-settings/) to register a Spotify app and then specifying the app's `client_id` in the [general configuration file](docs/config.md#general). -More details about registering a Spotify application can be found in the [official Spotify documentation](https://developer.spotify.com/documentation/general/guides/authorization/app-settings/). +Upon running `spotify_player` with a user-provided `client_id`, user will be prompted to authenticate the app described earlier. **NOTE** that this prompt is different from the prompt to authenticate `spotify_player`. Upon accepting the authentication request, `spotify_player` will retrieve an access token of the app to finish setting up the integration. -When `spotify_player` runs with your own `client_id`, press **D** (default shortcut for `SwitchDevice` command) to get the list of available devices, then press **enter** (default shortcut for `ChooseSelected` command) to connect to the selected device. +After the user-provided client is successfully integrated, press **D** (default shortcut for `SwitchDevice` command) to get the list of available devices, then press **enter** (default shortcut for `ChooseSelected` command) to connect to the selected device. ### Streaming @@ -257,9 +261,9 @@ cargo install spotify_player --features pixelate The amount of pixels can be tweaked via the `cover_img_pixels` config option. -| `cover_img_pixels` | `8` | `16` | `32` | `64` | -|--------------------|-----|------|------|------| -| example | 8x8 | 16x16 | 32x32 | 64x64 | +| `cover_img_pixels` | `8` | `16` | `32` | `64` | +| ------------------ | ------------------------------------------------------------------------------------------------------------------- | --------------------------------------------------------------------------------------------------------------------- | --------------------------------------------------------------------------------------------------------------------- | --------------------------------------------------------------------------------------------------------------------- | +| example | 8x8 | 16x16 | 32x32 | 64x64 | To temporarily disable the `pixelate` feature just set `cover_img_pixels` to a high value like `512`. @@ -344,66 +348,66 @@ To go to the shortcut help page, press `?` or `C-h` (default shortcuts for `Open List of supported commands: -| Command | Description | Default shortcuts | -| ------------------------------ | -------------------------------------------------------------------------------------------------- | ------------------ | -| `NextTrack` | next track | `n` | -| `PreviousTrack` | previous track | `p` | -| `ResumePause` | resume/pause based on the current playback | `space` | -| `PlayRandom` | play a random track in the current context | `.` | -| `Repeat` | cycle the repeat mode | `C-r` | -| `ToggleFakeTrackRepeatMode` | toggle fake track repeat mode | `M-r` | -| `Shuffle` | toggle the shuffle mode | `C-s` | -| `VolumeChange` | change playback volume by an offset (default shortcuts use 5%) | `+`, `-` | -| `Mute` | toggle playback volume between 0% and previous level | `_` | -| `SeekForward` | seek forward by 5s | `>` | -| `SeekBackward` | seek backward by 5s | `<` | -| `Quit` | quit the application | `C-c`, `q` | -| `ClosePopup` | close a popup | `esc` | -| `SelectNextOrScrollDown` | select the next item in a list/table or scroll down (supports vim-style count: 5j) | `j`, `C-n`, `down` | -| `SelectPreviousOrScrollUp` | select the previous item in a list/table or scroll up (supports vim-style count: 10k) | `k`, `C-p`, `up` | -| `PageSelectNextOrScrollDown` | select the next page item in a list/table or scroll a page down (supports vim-style count: 3C-f) | `page_down`, `C-f` | -| `PageSelectPreviousOrScrollUp` | select the previous page item in a list/table or scroll a page up (supports vim-style count: 2C-b) | `page_up`, `C-b` | -| `SelectFirstOrScrollToTop` | select the first item in a list/table or scroll to the top | `g g`, `home` | -| `SelectLastOrScrollToBottom` | select the last item in a list/table or scroll to the bottom | `G`, `end` | -| `ChooseSelected` | choose the selected item | `enter` | -| `RefreshPlayback` | manually refresh the current playback | `r` | -| `RestartIntegratedClient` | restart the integrated client (`streaming` feature only) | `R` | -| `ShowActionsOnSelectedItem` | open a popup showing actions on a selected item | `g a`, `C-space` | -| `ShowActionsOnCurrentTrack` | open a popup showing actions on the current track | `a` | -| `AddSelectedItemToQueue` | add the selected item to queue | `Z`, `C-z` | -| `FocusNextWindow` | focus the next focusable window (if any) | `tab` | -| `FocusPreviousWindow` | focus the previous focusable window (if any) | `backtab` | -| `SwitchTheme` | open a popup for switching theme | `T` | -| `SwitchDevice` | open a popup for switching device | `D` | -| `Search` | open a popup for searching in the current page | `/` | -| `BrowseUserPlaylists` | open a popup for browsing user's playlists | `u p` | -| `BrowseUserFollowedArtists` | open a popup for browsing user's followed artists | `u a` | -| `BrowseUserSavedAlbums` | open a popup for browsing user's saved albums | `u A` | -| `CurrentlyPlayingContextPage` | go to the currently playing context page | `g space` | -| `TopTrackPage` | go to the user top track page | `g t` | -| `RecentlyPlayedTrackPage` | go to the user recently played track page | `g r` | -| `LikedTrackPage` | go to the user liked track page | `g y` | -| `LyricsPage` | go to the lyrics page of the current track | `g L`, `l` | -| `LibraryPage` | go to the user library page | `g l` | -| `SearchPage` | go to the search page | `g s` | -| `BrowsePage` | go to the browse page | `g b` | -| `Queue` | go to the queue page | `z` | -| `OpenCommandHelp` | go to the command help page | `?`, `C-h` | -| `PreviousPage` | go to the previous page | `backspace`, `C-q` | -| `OpenSpotifyLinkFromClipboard` | open a Spotify link from clipboard | `O` | -| `SortTrackByTitle` | sort the track table (if any) by track's title | `s t` | -| `SortTrackByArtists` | sort the track table (if any) by track's artists | `s a` | -| `SortTrackByAlbum` | sort the track table (if any) by track's album | `s A` | -| `SortTrackByAddedDate` | sort the track table (if any) by track's added date | `s D` | -| `SortTrackByDuration` | sort the track table (if any) by track's duration | `s d` | -| `SortLibraryAlphabetically` | sort the library alphabetically | `s l a` | -| `SortLibraryByRecent` | sort the library (playlists and albums) by recently added items | `s l r` | -| `ReverseOrder` | reverse the order of the track table (if any) | `s r` | -| `MovePlaylistItemUp` | move playlist item up one position | `C-k` | -| `MovePlaylistItemDown` | move playlist item down one position | `C-j` | -| `CreatePlaylist` | create a new playlist | `N` | -| `JumpToCurrentTrackInContext` | jump to the current track in the context | `g c` | -| `JumpToHighlightTrackInContext`| jump to the currently highlighted search result in the context | `C-g` | +| Command | Description | Default shortcuts | +| ------------------------------- | -------------------------------------------------------------------------------------------------- | ------------------ | +| `NextTrack` | next track | `n` | +| `PreviousTrack` | previous track | `p` | +| `ResumePause` | resume/pause based on the current playback | `space` | +| `PlayRandom` | play a random track in the current context | `.` | +| `Repeat` | cycle the repeat mode | `C-r` | +| `ToggleFakeTrackRepeatMode` | toggle fake track repeat mode | `M-r` | +| `Shuffle` | toggle the shuffle mode | `C-s` | +| `VolumeChange` | change playback volume by an offset (default shortcuts use 5%) | `+`, `-` | +| `Mute` | toggle playback volume between 0% and previous level | `_` | +| `SeekForward` | seek forward by 5s | `>` | +| `SeekBackward` | seek backward by 5s | `<` | +| `Quit` | quit the application | `C-c`, `q` | +| `ClosePopup` | close a popup | `esc` | +| `SelectNextOrScrollDown` | select the next item in a list/table or scroll down (supports vim-style count: 5j) | `j`, `C-n`, `down` | +| `SelectPreviousOrScrollUp` | select the previous item in a list/table or scroll up (supports vim-style count: 10k) | `k`, `C-p`, `up` | +| `PageSelectNextOrScrollDown` | select the next page item in a list/table or scroll a page down (supports vim-style count: 3C-f) | `page_down`, `C-f` | +| `PageSelectPreviousOrScrollUp` | select the previous page item in a list/table or scroll a page up (supports vim-style count: 2C-b) | `page_up`, `C-b` | +| `SelectFirstOrScrollToTop` | select the first item in a list/table or scroll to the top | `g g`, `home` | +| `SelectLastOrScrollToBottom` | select the last item in a list/table or scroll to the bottom | `G`, `end` | +| `ChooseSelected` | choose the selected item | `enter` | +| `RefreshPlayback` | manually refresh the current playback | `r` | +| `RestartIntegratedClient` | restart the integrated client (`streaming` feature only) | `R` | +| `ShowActionsOnSelectedItem` | open a popup showing actions on a selected item | `g a`, `C-space` | +| `ShowActionsOnCurrentTrack` | open a popup showing actions on the current track | `a` | +| `AddSelectedItemToQueue` | add the selected item to queue | `Z`, `C-z` | +| `FocusNextWindow` | focus the next focusable window (if any) | `tab` | +| `FocusPreviousWindow` | focus the previous focusable window (if any) | `backtab` | +| `SwitchTheme` | open a popup for switching theme | `T` | +| `SwitchDevice` | open a popup for switching device | `D` | +| `Search` | open a popup for searching in the current page | `/` | +| `BrowseUserPlaylists` | open a popup for browsing user's playlists | `u p` | +| `BrowseUserFollowedArtists` | open a popup for browsing user's followed artists | `u a` | +| `BrowseUserSavedAlbums` | open a popup for browsing user's saved albums | `u A` | +| `CurrentlyPlayingContextPage` | go to the currently playing context page | `g space` | +| `TopTrackPage` | go to the user top track page | `g t` | +| `RecentlyPlayedTrackPage` | go to the user recently played track page | `g r` | +| `LikedTrackPage` | go to the user liked track page | `g y` | +| `LyricsPage` | go to the lyrics page of the current track | `g L`, `l` | +| `LibraryPage` | go to the user library page | `g l` | +| `SearchPage` | go to the search page | `g s` | +| `BrowsePage` | go to the browse page | `g b` | +| `Queue` | go to the queue page | `z` | +| `OpenCommandHelp` | go to the command help page | `?`, `C-h` | +| `PreviousPage` | go to the previous page | `backspace`, `C-q` | +| `OpenSpotifyLinkFromClipboard` | open a Spotify link from clipboard | `O` | +| `SortTrackByTitle` | sort the track table (if any) by track's title | `s t` | +| `SortTrackByArtists` | sort the track table (if any) by track's artists | `s a` | +| `SortTrackByAlbum` | sort the track table (if any) by track's album | `s A` | +| `SortTrackByAddedDate` | sort the track table (if any) by track's added date | `s D` | +| `SortTrackByDuration` | sort the track table (if any) by track's duration | `s d` | +| `SortLibraryAlphabetically` | sort the library alphabetically | `s l a` | +| `SortLibraryByRecent` | sort the library (playlists and albums) by recently added items | `s l r` | +| `ReverseOrder` | reverse the order of the track table (if any) | `s r` | +| `MovePlaylistItemUp` | move playlist item up one position | `C-k` | +| `MovePlaylistItemDown` | move playlist item down one position | `C-j` | +| `CreatePlaylist` | create a new playlist | `N` | +| `JumpToCurrentTrackInContext` | jump to the current track in the context | `g c` | +| `JumpToHighlightTrackInContext` | jump to the currently highlighted search result in the context | `C-g` | To add new shortcuts or modify the default shortcuts, please refer to the [keymaps section](docs/config.md#keymaps) in the configuration documentation. diff --git a/docs/config.md b/docs/config.md index 4ea45d9a..a22da603 100644 --- a/docs/config.md +++ b/docs/config.md @@ -25,8 +25,8 @@ All configuration files should be placed inside the application's configuration | Option | Description | Default | | --------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------ | ----------------------------------------------------------- | -| `client_id` | the Spotify client's ID | `65b708073fc0480ea92a077233ca87bd` | -| `client_id_command` | a shell command that prints the Spotify client ID to stdout (overrides `client_id`) | `None` | +| `client_id` | user-provided client's ID (required for [Spotify Connect feature](https://github.com/aome510/spotify-player#spotify-connect)) | `None` | +| `client_id_command` | a shell command that prints user client ID to stdout (overrides `client_id`) | `None` | | `login_redirect_uri` | the redirect URI for authenticating the application | `http://127.0.0.1:8989/login` | | `client_port` | the port that the application's client is running on to handle CLI commands | `8080` | | `tracks_playback_limit` | the limit for the number of tracks played in a **tracks** playback | `50` | @@ -61,8 +61,6 @@ All configuration files should be placed inside the application's configuration ### Notes -- By default, `spotify_player` uses the official Spotify Web app's client (`client_id = 65b708073fc0480ea92a077233ca87bd`) -- It's recommended to specify [your own Client ID](https://developer.spotify.com/documentation/web-api/concepts/apps) to avoid possible rate limits and to allow a full [Spotify connect](https://www.spotify.com/us/connect/) support. An error such as `Failed to initialize the Spotify data` can appear if the `client_id` is invalid. - `ap_port` and `proxy` are [Librespot's session configurations](https://github.com/librespot-org/librespot/wiki/Behind-web-proxy). By default, `spotify_player` doesn't set those values, which means the Librespot library will fallback to use its default options. - Positive-value `app_refresh_duration_in_ms` is used to refresh the playback periodically. This can result in hitting a Spotify rate limit if the application is running for a long time. - To prevent the rate limit, `spotify_player` sets `playback_refresh_duration_in_ms=0` by default and makes additional API calls when there is an event or a command triggering a playback update. diff --git a/examples/app.toml b/examples/app.toml index c4611a4c..ecde1858 100644 --- a/examples/app.toml +++ b/examples/app.toml @@ -1,5 +1,4 @@ theme = "default" -client_id = "65b708073fc0480ea92a077233ca87bd" login_redirect_uri = "http://127.0.0.1:8989/login" client_port = 8080 tracks_playback_limit = 50 diff --git a/spotify_player/Cargo.toml b/spotify_player/Cargo.toml index b9542ccb..ca6b5545 100644 --- a/spotify_player/Cargo.toml +++ b/spotify_player/Cargo.toml @@ -23,7 +23,7 @@ librespot-metadata = "0.7.0" log = "0.4.27" chrono = "0.4.41" reqwest = { version = "0.12.22", features = ["json"] } -rspotify = "0.15.0" +rspotify = {version = "0.15.0", features = ["cli"] } serde = { version = "1.0.219", features = ["derive"] } tokio = { version = "1.47.0", features = [ "rt", diff --git a/spotify_player/src/auth.rs b/spotify_player/src/auth.rs index 8464876b..6ae9ab88 100644 --- a/spotify_player/src/auth.rs +++ b/spotify_player/src/auth.rs @@ -4,32 +4,32 @@ use librespot_core::{authentication::Credentials, cache::Cache, config::SessionC use librespot_oauth::OAuthClientBuilder; pub const SPOTIFY_CLIENT_ID: &str = "65b708073fc0480ea92a077233ca87bd"; -// based on https://github.com/librespot-org/librespot/blob/f96f36c064795011f9fee912291eecb1aa46fff6/src/main.rs#L173 +// based on https://developer.spotify.com/documentation/web-api/concepts/scopes#list-of-scopes const OAUTH_SCOPES: &[&str] = &[ + // Spotify Connect + "user-read-playback-state", + "user-modify-playback-state", + "user-read-currently-playing", + // Playback "app-remote-control", - "playlist-modify", + "streaming", + // Playlists + "playlist-read-private", + "playlist-read-collaborative", "playlist-modify-private", "playlist-modify-public", - "playlist-read", - "playlist-read-collaborative", - "playlist-read-private", - "streaming", - "ugc-image-upload", + // Follow "user-follow-modify", "user-follow-read", + // Listening History + "user-read-playback-position", + "user-top-read", + "user-read-recently-played", + // Library "user-library-modify", "user-library-read", - "user-modify", - "user-modify-playback-state", - "user-modify-private", + // Users "user-personalized", - "user-read-currently-playing", - "user-read-play-history", - "user-read-playback-position", - "user-read-playback-state", - "user-read-private", - "user-read-recently-played", - "user-top-read", ]; #[derive(Clone)] @@ -100,7 +100,8 @@ pub fn get_creds(auth_config: &AuthConfig, reauth: bool, use_cached: bool) -> Re SPOTIFY_CLIENT_ID, &auth_config.login_redirect_uri, OAUTH_SCOPES.to_vec(), - ); + ) + .open_in_browser(); let oauth_client = client_builder.build()?; oauth_client .get_access_token() diff --git a/spotify_player/src/cli/client.rs b/spotify_player/src/cli/client.rs index 0823150f..6373e049 100644 --- a/spotify_player/src/cli/client.rs +++ b/spotify_player/src/cli/client.rs @@ -13,7 +13,7 @@ use tracing::Instrument; use crate::{ cli::Request, - client::{Client, PlayerRequest}, + client::{AppClient, PlayerRequest}, config::get_cache_folder_path, state::{ AlbumId, ArtistId, Context, ContextId, Id, PlayableId, Playback, PlaybackMetadata, @@ -27,7 +27,7 @@ use super::{ Serialize, MAX_REQUEST_SIZE, }; -pub async fn start_socket(client: Client, socket: UdpSocket, state: Option) { +pub async fn start_socket(client: AppClient, socket: UdpSocket, state: Option) { let mut buf = [0; MAX_REQUEST_SIZE]; loop { @@ -92,7 +92,7 @@ async fn send_response( } async fn current_playback( - client: &Client, + client: &AppClient, state: Option<&SharedState>, ) -> Result> { // get current playback from the application's state, if exists, or by making an API request @@ -106,7 +106,7 @@ async fn current_playback( } async fn handle_socket_request( - client: &Client, + client: &AppClient, state: Option<&SharedState>, request: super::Request, ) -> Result> { @@ -178,7 +178,7 @@ async fn handle_socket_request( } async fn handle_get_key_request( - client: &Client, + client: &AppClient, state: Option<&SharedState>, key: Key, ) -> Result> { @@ -219,7 +219,7 @@ async fn handle_get_key_request( } /// Get a Spotify item's ID from its `IdOrName` representation -async fn get_spotify_id(client: &Client, typ: ItemType, id_or_name: IdOrName) -> Result { +async fn get_spotify_id(client: &AppClient, typ: ItemType, id_or_name: IdOrName) -> Result { // For `IdOrName::Name`, we search for the first item matching the name and return its Spotify id. // The item's id is then used to retrieve the item's data. @@ -304,7 +304,7 @@ async fn get_spotify_id(client: &Client, typ: ItemType, id_or_name: IdOrName) -> } async fn handle_get_item_request( - client: &Client, + client: &AppClient, item_type: ItemType, id_or_name: IdOrName, ) -> Result> { @@ -317,14 +317,14 @@ async fn handle_get_item_request( }) } -async fn handle_search_request(client: &Client, query: String) -> Result> { +async fn handle_search_request(client: &AppClient, query: String) -> Result> { let search_result = client.search(&query).await?; Ok(serde_json::to_vec(&search_result)?) } async fn handle_playback_request( - client: &Client, + client: &AppClient, state: Option<&SharedState>, command: Command, ) -> Result<()> { @@ -465,7 +465,7 @@ async fn handle_playback_request( Ok(()) } -async fn handle_playlist_request(client: &Client, command: PlaylistCommand) -> Result { +async fn handle_playlist_request(client: &AppClient, command: PlaylistCommand) -> Result { let uid = client.current_user().await?.id; match command { @@ -750,7 +750,7 @@ const TRACK_BUFFER_CAP: usize = 100; /// The state of `import_from` playlist is stored into a cache file to add/delete the differed tracks between /// subsequent imports of the same two playlists. async fn playlist_import( - client: &Client, + client: &AppClient, import_from: PlaylistId<'static>, import_to: PlaylistId<'static>, delete: bool, diff --git a/spotify_player/src/cli/handlers.rs b/spotify_player/src/cli/handlers.rs index 66f56b76..cf71a48d 100644 --- a/spotify_player/src/cli/handlers.rs +++ b/spotify_player/src/cli/handlers.rs @@ -173,11 +173,12 @@ fn try_connect_to_client(socket: &UdpSocket, configs: &config::Configs) -> Resul // no running `spotify_player` instance found, // initialize a new client to handle the current CLI command - let auth_config = AuthConfig::new(configs)?; let rt = tokio::runtime::Runtime::new()?; // create a Spotify API client - let client = client::Client::new(auth_config); + let client = rt + .block_on(client::AppClient::new()) + .context("construct app client")?; rt.block_on(client.new_session(None, false)) .context("new session")?; diff --git a/spotify_player/src/client/handlers.rs b/spotify_player/src/client/handlers.rs index 9dcdbe71..12423d50 100644 --- a/spotify_player/src/client/handlers.rs +++ b/spotify_player/src/client/handlers.rs @@ -19,7 +19,7 @@ struct PlayerEventHandlerState { /// starts the client's request handler pub async fn start_client_handler( state: SharedState, - client: super::Client, + client: super::AppClient, client_sub: flume::Receiver, ) { while let Ok(request) = client_sub.recv_async().await { diff --git a/spotify_player/src/client/mod.rs b/spotify_player/src/client/mod.rs index 51cb7bee..c6c058e1 100644 --- a/spotify_player/src/client/mod.rs +++ b/spotify_player/src/client/mod.rs @@ -41,15 +41,16 @@ const PLAYBACK_TYPES: [&rspotify::model::AdditionalType; 2] = [ /// The application's Spotify client #[derive(Clone)] -pub struct Client { +pub struct AppClient { http: reqwest::Client, spotify: Arc, auth_config: AuthConfig, + user_client: Option, #[cfg(feature = "streaming")] stream_conn: Arc>>, } -impl Deref for Client { +impl Deref for AppClient { type Target = spotify::Spotify; fn deref(&self) -> &Self::Target { self.spotify.as_ref() @@ -60,17 +61,49 @@ fn market_query() -> Query<'static> { Query::from([("market", "from_token")]) } -impl Client { +impl AppClient { /// Construct a new client - pub fn new(auth_config: AuthConfig) -> Self { - Self { + pub async fn new() -> Result { + let configs = config::get_config(); + let auth_config = AuthConfig::new(configs)?; + + // Construct user-provided client. + // This custom client is needed for Spotify Connect integration because the Spotify client (`AppConfig::spotify`), + // which `spotify-player` uses to retrieve Spotify data, doesn't have access to user available devices + let mut user_client = configs.app_config.get_user_client_id()?.clone().map(|id| { + let creds = rspotify::Credentials { id, secret: None }; + let oauth = rspotify::OAuth { + scopes: rspotify::scopes!("user-read-playback-state"), + redirect_uri: configs.app_config.login_redirect_uri.clone(), + ..Default::default() + }; + let config = rspotify::Config { + token_cached: true, + cache_path: configs.cache_folder.join("user_client_token.json"), + ..Default::default() + }; + rspotify::AuthCodePkceSpotify::with_config(creds, oauth, config) + }); + + if let Some(client) = &mut user_client { + let url = client + .get_authorize_url(None) + .context("get authorize URL for user-provided client")?; + client + .prompt_for_token(&url) + .await + .context("get token for user-provided client")?; + } + + Ok(Self { spotify: Arc::new(spotify::Spotify::new()), http: reqwest::Client::new(), auth_config, + user_client, #[cfg(feature = "streaming")] stream_conn: Arc::new(Mutex::new(None)), - } + }) } /// Initialize the application's playback upon creating a new session or during startup @@ -619,16 +652,15 @@ impl Client { } /// Get user available devices - // This is a custom API to replace `rspotify::device` API to support Spotify Connect feature pub async fn available_devices(&self) -> Result> { - Ok(self - .http_get::( - &format!("{SPOTIFY_API_ENDPOINT}/me/player/devices"), - &Query::new(), - true, - ) - .await? - .devices) + match &self.user_client { + None => { + tracing::warn!("User-provided client integration is not enabled, no device found."); + tracing::warn!("Please make sure you setup Spotify Connect as described in https://github.com/aome510/spotify-player#spotify-connect."); + Ok(vec![]) + } + Some(client) => Ok(client.device().await?), + } } pub fn update_playback(&self, state: &SharedState) { @@ -674,13 +706,7 @@ impl Client { /// Find an available device. If found, return the device's ID. async fn find_available_device(&self) -> Result> { let devices = self.available_devices().await?; - - if devices.is_empty() { - tracing::warn!("No device found. Please make sure you already setup Spotify Connect \ - support as described in https://github.com/aome510/spotify-player#spotify-connect."); - } else { - tracing::info!("Available devices: {devices:?}"); - } + tracing::info!("Available devices: {devices:?}"); // if there is an active device, return it if let Some(d) = devices.iter().find(|d| d.is_active) { @@ -781,7 +807,6 @@ impl Client { .http_get::>( &format!("{SPOTIFY_API_ENDPOINT}/me/playlists"), &Query::from([("limit", "50")]), - false, ) .await?; // let first_page = self @@ -810,7 +835,7 @@ impl Client { let mut maybe_next = first_page.next; while let Some(url) = maybe_next { let mut next_page = self - .http_get::(&url, &Query::new(), false) + .http_get::(&url, &Query::new()) .await? .artists; artists.append(&mut next_page.items); @@ -879,7 +904,7 @@ impl Client { .into_iter() .filter_map(Album::try_from_simplified_album) .collect(); - Ok(Client::process_artist_albums(albums)) + Ok(AppClient::process_artist_albums(albums)) } /// Start a playback @@ -1309,7 +1334,6 @@ impl Client { .http_get::( &format!("{SPOTIFY_API_ENDPOINT}/playlists/{}", playlist_id.id()), &market_query(), - false, ) .await?; @@ -1441,12 +1465,7 @@ impl Client { } /// Make a GET HTTP request to the Spotify server - async fn http_get( - &self, - url: &str, - payload: &Query<'_>, - use_user_client_id: bool, - ) -> Result + async fn http_get(&self, url: &str, payload: &Query<'_>) -> Result where T: serde::de::DeserializeOwned, { @@ -1462,13 +1481,7 @@ impl Client { .replace("\"name\":null", "\"name\":\"\"") } - let access_token = if use_user_client_id { - self.access_token_from_user_client_id().await - } else { - self.access_token().await - } - .context("get access token")?; - + let access_token = self.access_token().await?; tracing::debug!("{access_token} {url}"); let response = self @@ -1507,7 +1520,7 @@ impl Client { while let Some(url) = maybe_next { let mut next_page = self - .http_get::>(&url, payload, false) + .http_get::>(&url, payload) .await?; if next_page.items.is_empty() { break; @@ -1530,7 +1543,7 @@ impl Client { let mut maybe_next = first_page.next; while let Some(url) = maybe_next { let mut next_page = self - .http_get::>(&url, &Query::new(), false) + .http_get::>(&url, &Query::new()) .await?; items.append(&mut next_page.items); maybe_next = next_page.next; diff --git a/spotify_player/src/client/spotify.rs b/spotify_player/src/client/spotify.rs index 978be1e4..84276c11 100644 --- a/spotify_player/src/client/spotify.rs +++ b/spotify_player/src/client/spotify.rs @@ -9,7 +9,7 @@ use rspotify::{ }; use std::{fmt, sync::Arc}; -use crate::{auth::SPOTIFY_CLIENT_ID, config, token}; +use crate::token; #[derive(Clone, Default)] /// A Spotify client to interact with Spotify API server @@ -19,12 +19,6 @@ pub struct Spotify { config: Config, token: Arc>>, http: HttpClient, - /// User-provided client ID - /// - /// This client ID is mainly used to support Spotify Connect feature - /// because Spotify client ID doesn't have access to user available devices - /// () - user_client_id: String, pub(crate) session: Arc>>, } @@ -52,10 +46,6 @@ impl Spotify { }, token: Arc::new(Mutex::new(None)), http: HttpClient::default(), - user_client_id: config::get_config() - .app_config - .get_client_id() - .expect("get client_id"), session: Arc::new(tokio::sync::Mutex::new(None)), } } @@ -87,14 +77,6 @@ impl Spotify { )), } } - - /// Get a Spotify access token based on a user-provided client ID - // TODO: implement caching - pub async fn access_token_from_user_client_id(&self) -> Result { - let session = self.session().await; - let token = token::get_token_librespot(&session, &self.user_client_id).await?; - Ok(token.access_token) - } } // TODO: remove the below uses of `maybe_async` crate once @@ -127,7 +109,7 @@ impl BaseClient for Spotify { return Ok(old_token); } - match token::get_token_rspotify(&session, SPOTIFY_CLIENT_ID).await { + match token::get_token_rspotify(&session).await { Ok(token) => Ok(Some(token)), Err(err) => { tracing::error!("Failed to get a new token: {err:#}"); diff --git a/spotify_player/src/config/mod.rs b/spotify_player/src/config/mod.rs index 63f5d5ef..428f5e68 100644 --- a/spotify_player/src/config/mod.rs +++ b/spotify_player/src/config/mod.rs @@ -50,7 +50,7 @@ impl Configs { /// Application configurations pub struct AppConfig { pub theme: String, - pub client_id: String, + pub client_id: Option, pub client_id_command: Option, pub client_port: u16, @@ -257,8 +257,7 @@ impl Default for AppConfig { fn default() -> Self { Self { theme: "dracula".to_owned(), - // official Spotify web app's client id - client_id: "65b708073fc0480ea92a077233ca87bd".to_string(), + client_id: None, client_id_command: None, client_port: 8080, @@ -433,9 +432,9 @@ impl AppConfig { } /// Returns stdout of `client_id_command` if set, otherwise it returns the the value of `client_id` - pub fn get_client_id(&self) -> Result { + pub fn get_user_client_id(&self) -> Result> { match self.client_id_command { - Some(ref cmd) => cmd.execute(None).map(|out| out.trim().into()), + Some(ref cmd) => cmd.execute(None).map(|out| Some(out.trim().to_string())), None => Ok(self.client_id.clone()), } } diff --git a/spotify_player/src/main.rs b/spotify_player/src/main.rs index 3f65c5eb..b437990d 100644 --- a/spotify_player/src/main.rs +++ b/spotify_player/src/main.rs @@ -20,7 +20,7 @@ use std::io::Write; fn init_spotify( client_pub: &flume::Sender, - client: &client::Client, + client: &client::AppClient, state: &state::SharedState, ) -> Result<()> { client.initialize_playback(state); @@ -115,8 +115,9 @@ async fn start_app(state: &state::SharedState) -> Result<()> { } // create a Spotify API client - let auth_config = auth::AuthConfig::new(configs)?; - let client = client::Client::new(auth_config); + let client = client::AppClient::new() + .await + .context("construct app client")?; client .new_session(Some(state), true) .await diff --git a/spotify_player/src/streaming.rs b/spotify_player/src/streaming.rs index 74dd7e31..d2cb6133 100644 --- a/spotify_player/src/streaming.rs +++ b/spotify_player/src/streaming.rs @@ -1,4 +1,4 @@ -use crate::{client::Client, config, state::SharedState}; +use crate::{client::AppClient, config, state::SharedState}; use anyhow::Context; use librespot_connect::{ConnectConfig, Spirc}; use librespot_core::authentication::Credentials; @@ -140,7 +140,7 @@ fn execute_player_event_hook_command( /// Create a new streaming connection pub async fn new_connection( - client: Client, + client: AppClient, state: SharedState, session: Session, creds: Credentials, diff --git a/spotify_player/src/token.rs b/spotify_player/src/token.rs index cbaec371..9b038b22 100644 --- a/spotify_player/src/token.rs +++ b/spotify_player/src/token.rs @@ -1,47 +1,36 @@ use std::collections::HashSet; use anyhow::Result; -use chrono::{Duration, Utc}; use librespot_core::session::Session; -const TIMEOUT_IN_SECS: u64 = 5; +const TIMEOUT: std::time::Duration = std::time::Duration::from_secs(5); + +pub async fn get_token_rspotify(session: &Session) -> Result { + tracing::info!("Getting a new authentication token..."); -pub async fn get_token_librespot( - session: &Session, - _client_id: &str, -) -> Result { - // TODO: figure out how to support custom client_id for Spotify Connect let auth_data = session.auth_data(); if auth_data.is_empty() { anyhow::bail!("Session has no stored credentials for login5 token acquisition"); } - let token = session.login5().auth_token().await.unwrap(); - Ok(token) -} - -pub async fn get_token_rspotify(session: &Session, client_id: &str) -> Result { - tracing::info!("Getting a new authentication token..."); - - let fut = get_token_librespot(session, client_id); - let token = - match tokio::time::timeout(std::time::Duration::from_secs(TIMEOUT_IN_SECS), fut).await { - Ok(Ok(token)) => token, - Ok(Err(err)) => anyhow::bail!("failed to get the token: {:?}", err), - Err(_) => { - // The timeout likely happens because of the "corrupted" session, - // shutdown it to force re-initializing. - if !session.is_invalid() { - session.shutdown(); - } - anyhow::bail!("timeout when getting the token"); + let fut = session.login5().auth_token(); + let token = match tokio::time::timeout(TIMEOUT, fut).await { + Ok(Ok(token)) => token, + Ok(Err(err)) => anyhow::bail!("failed to get the token: {:?}", err), + Err(_) => { + // The timeout likely happens because of the "corrupted" session, + // shutdown it to force re-initializing. + if !session.is_invalid() { + session.shutdown(); } - }; + anyhow::bail!("timeout when getting the token"); + } + }; // converts the token returned by librespot `get_token` function to a `rspotify::Token` - let expires_in = Duration::from_std(token.expires_in)?; + let expires_in = chrono::Duration::from_std(token.expires_in)?; // let expires_in = Duration::from_std(std::time::Duration::from_secs(5))?; - let expires_at = Utc::now() + expires_in; + let expires_at = chrono::Utc::now() + expires_in; let token = rspotify::Token { access_token: token.access_token, From de8c625f9d8e9ac3099a9b9809e520ae95fba80f Mon Sep 17 00:00:00 2001 From: tojamrok <16987182+tojamrok@users.noreply.github.com> Date: Sat, 6 Sep 2025 11:55:26 +0200 Subject: [PATCH 15/16] Add weekly Afera.com.pl playlist automation MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Add playlist_scraper.tcl: scrapes weekly albums and tracks from afera.com.pl - Add playlist_generator.tcl: creates Spotify playlists from scraped content - Add GitHub Action workflow for weekly automated playlist updates - Add test_local.sh: local testing script before deployment πŸ€– Generated with [Claude Code](https://claude.ai/code) Co-Authored-By: Claude --- .github/workflows/weekly-playlist-update.yml | 88 ++++++++++++ playlist_generator.tcl | 80 +++++++++++ playlist_scraper.tcl | 77 ++++++++++ test_local.sh | 141 +++++++++++++++++++ 4 files changed, 386 insertions(+) create mode 100644 .github/workflows/weekly-playlist-update.yml create mode 100755 playlist_generator.tcl create mode 100755 playlist_scraper.tcl create mode 100755 test_local.sh diff --git a/.github/workflows/weekly-playlist-update.yml b/.github/workflows/weekly-playlist-update.yml new file mode 100644 index 00000000..74c02e81 --- /dev/null +++ b/.github/workflows/weekly-playlist-update.yml @@ -0,0 +1,88 @@ +name: Weekly Afera Playlist Update + +on: + schedule: + # Run every Monday at 10:00 AM UTC + - cron: '0 10 * * 1' + workflow_dispatch: # Allow manual triggering + +env: + CARGO_TERM_COLOR: always + RUST_FEATURES: "rodio-backend,media-control,image,notify" + +jobs: + update-playlist: + runs-on: ubuntu-latest + + steps: + - name: Checkout repository + uses: actions/checkout@v4 + + - name: Install dependencies + run: | + sudo apt-get update + sudo apt-get install -y libssl-dev libasound2-dev libdbus-1-dev libxcb-shape0-dev libxcb-xfixes0-dev tcl tcl-tls + + - name: Install Rust toolchain + uses: dtolnay/rust-toolchain@stable + + - name: Cache cargo deps + uses: actions/cache@v3 + with: + path: | + ~/.cargo/registry/index/ + ~/.cargo/registry/cache/ + ~/.cargo/git/db/ + target/ + key: ${{ runner.os }}-cargo-${{ hashFiles('**/Cargo.lock') }} + + - name: Build spotify_player + run: cargo build --release --no-default-features --features ${{ env.RUST_FEATURES }} + + - name: Set up Spotify credentials + env: + SPOTIFY_CLIENT_ID: ${{ secrets.SPOTIFY_CLIENT_ID }} + SPOTIFY_CLIENT_SECRET: ${{ secrets.SPOTIFY_CLIENT_SECRET }} + SPOTIFY_REFRESH_TOKEN: ${{ secrets.SPOTIFY_REFRESH_TOKEN }} + run: | + # Create config directory if it doesn't exist + mkdir -p ~/.config/spotify-player + + # Create config file with credentials + cat > ~/.config/spotify-player/app.toml << EOF + [application] + client_id = "$SPOTIFY_CLIENT_ID" + + [authentication] + client_secret = "$SPOTIFY_CLIENT_SECRET" + refresh_token = "$SPOTIFY_REFRESH_TOKEN" + EOF + + - name: Make scripts executable + run: | + chmod +x playlist_scraper.tcl + chmod +x playlist_generator.tcl + + - name: Scrape and create playlist + run: | + echo "Scraping Afera website..." + ./playlist_scraper.tcl https://www.afera.com.pl/muzyka > scraped_content.txt + + echo "Content scraped:" + cat scraped_content.txt + + echo "Creating playlist..." + cat scraped_content.txt | ./playlist_generator.tcl > playlist_commands.txt + + echo "Generated commands:" + cat playlist_commands.txt + + - name: Upload artifacts + uses: actions/upload-artifact@v4 + if: always() + with: + name: playlist-update-logs + path: | + scraped_content.txt + playlist_commands.txt + retention-days: 30 \ No newline at end of file diff --git a/playlist_generator.tcl b/playlist_generator.tcl new file mode 100755 index 00000000..e563889b --- /dev/null +++ b/playlist_generator.tcl @@ -0,0 +1,80 @@ +#!/usr/bin/env tclsh + +proc parse_music_input {input_text} { + set lines [split [string trim $input_text] "\n"] + + set tracks {} + set albums {} + + foreach line $lines { + if {[string match "πŸ’Ώ*" $line]} { + # Remove πŸ’Ώ and clean up - this is an album + regsub {^πŸ’Ώ\s*} $line {} content + set content [string trim $content] + lappend albums "\"$content\"" + } elseif {[string match "🎢*" $line]} { + # Remove 🎢 and clean up - this is a track + regsub {^🎢\s*} $line {} content + set content [string trim $content] + lappend tracks "\"$content\"" + } + } + + return [list "" $tracks $albums] +} + +proc create_playlist_and_generate_commands {playlist_name tracks albums} { + set today [clock format [clock seconds] -format "%Y-%m-%d"] + set safe_playlist_name "test-afera-$today" + + # Execute playlist creation command + set create_cmd "target/release/spotify_player playlist new \"$safe_playlist_name\"" + puts "Executing: $create_cmd" + + if {[catch {exec {*}[split $create_cmd]} result]} { + puts "Error creating playlist: $result" + return + } + + puts $result + + # Extract playlist ID from output + if {[regexp {'spotify:playlist:([^']+)'} $result -> playlist_id]} { + puts "" + puts "# Using playlist ID: $playlist_id" + puts "" + + # Generate album commands first (πŸ’Ώ lines) + if {[llength $albums] > 0} { + foreach album $albums { + puts "target/release/spotify_player playlist edit --playlist-id \"$playlist_id\" --album-name $album add" + } + } + + # Generate track commands (🎢 lines) + if {[llength $tracks] > 0} { + foreach track $tracks { + puts "target/release/spotify_player playlist edit --playlist-id \"$playlist_id\" --track-name $track add" + } + } + } else { + puts "Could not extract playlist ID from output: $result" + } +} + +proc main {} { + global argc argv + + if {$argc > 0} { + # Read from command line argument + set input_text [lindex $argv 0] + } else { + # Read from stdin + set input_text [read stdin] + } + + lassign [parse_music_input $input_text] playlist_name tracks albums + create_playlist_and_generate_commands $playlist_name $tracks $albums +} + +main \ No newline at end of file diff --git a/playlist_scraper.tcl b/playlist_scraper.tcl new file mode 100755 index 00000000..30a813b4 --- /dev/null +++ b/playlist_scraper.tcl @@ -0,0 +1,77 @@ +#!/usr/bin/env tclsh + +package require http +package require tls + +# Configure TLS for HTTPS requests +http::register https 443 ::tls::socket + +proc scrape_afera_playlists {url} { + # Fetch the webpage + set token [http::geturl $url -timeout 10000] + set status [http::status $token] + set ncode [http::ncode $token] + + if {$status ne "ok" || $ncode != 200} { + puts stderr "Error fetching webpage: $status (HTTP $ncode)" + return + } + + set html [http::data $token] + http::cleanup $token + + # Extract albums and tracks using CSS class selectors + extract_albums $html + extract_tracks $html +} + +proc extract_albums {html} { + # Look for

with PŁYTA TYGODNIA + # and extract artist - album from inside + set pattern {

\s*PŁYTA TYGODNIA\s*([^<]+)} + + set matches [regexp -all -inline $pattern $html] + + for {set i 0} {$i < [llength $matches]} {incr i 2} { + set album_info [lindex $matches [expr $i + 1]] + set album_info [string trim $album_info] + + # Clean up any HTML entities + set album_info [regsub -all {"} $album_info "\""] + set album_info [regsub -all {&} $album_info "&"] + set album_info [regsub -all {'} $album_info "'"] + + puts "πŸ’Ώ $album_info" + } +} + +proc extract_tracks {html} { + # Look for

and extract artist - track + set pattern {

([^<]+)

} + + set matches [regexp -all -inline $pattern $html] + + for {set i 0} {$i < [llength $matches]} {incr i 2} { + set track_info [lindex $matches [expr $i + 1]] + set track_info [string trim $track_info] + + # Clean up any HTML entities + set track_info [regsub -all {"} $track_info "\""] + set track_info [regsub -all {&} $track_info "&"] + set track_info [regsub -all {'} $track_info "'"] + + puts "🎢 $track_info" + } +} + +# Main execution +if {$argc < 1} { + puts "Usage: $argv0 " + puts "Example: $argv0 https://www.afera.com.pl/muzyka" + exit 1 +} + +set url [lindex $argv 0] + +# Run the scraper +scrape_afera_playlists $url \ No newline at end of file diff --git a/test_local.sh b/test_local.sh new file mode 100755 index 00000000..cf48b183 --- /dev/null +++ b/test_local.sh @@ -0,0 +1,141 @@ +#!/bin/bash + +# Local test script for Afera playlist automation +# This script simulates the GitHub Actions workflow locally + +set -e + +echo "πŸ§ͺ Testing Afera Playlist Automation Locally" +echo "==============================================" + +# Colors for output +RED='\033[0;31m' +GREEN='\033[0;32m' +YELLOW='\033[1;33m' +NC='\033[0m' # No Color + +# Check if scripts exist +echo -e "${YELLOW}Checking required files...${NC}" +if [[ ! -f "playlist_scraper.tcl" ]]; then + echo -e "${RED}❌ playlist_scraper.tcl not found${NC}" + exit 1 +fi + +if [[ ! -f "playlist_generator.tcl" ]]; then + echo -e "${RED}❌ playlist_generator.tcl not found${NC}" + exit 1 +fi + +echo -e "${GREEN}βœ… Required scripts found${NC}" + +# Check if TCL is installed +echo -e "${YELLOW}Checking TCL installation...${NC}" +if ! command -v tclsh &> /dev/null; then + echo -e "${RED}❌ TCL is not installed. Install with: brew install tcl-tk (macOS) or apt-get install tcl (Ubuntu)${NC}" + exit 1 +fi + +# Check TCL TLS package +echo -e "${YELLOW}Checking TCL TLS package...${NC}" +if ! echo 'package require tls; puts "TLS OK"' | tclsh 2>/dev/null; then + echo -e "${RED}❌ TCL TLS package not found. Install with: brew install tcl-tk (macOS) or apt-get install tcl-tls (Ubuntu)${NC}" + exit 1 +fi + +echo -e "${GREEN}βœ… TCL and TLS package available${NC}" + +# Make scripts executable +echo -e "${YELLOW}Making scripts executable...${NC}" +chmod +x playlist_scraper.tcl +chmod +x playlist_generator.tcl +echo -e "${GREEN}βœ… Scripts are executable${NC}" + +# Test scraper +echo -e "${YELLOW}Testing playlist scraper...${NC}" +echo "Scraping Afera website (this may take a few seconds)..." + +if ./playlist_scraper.tcl https://www.afera.com.pl/muzyka > test_scraped_content.txt 2>/dev/null; then + echo -e "${GREEN}βœ… Scraper completed successfully${NC}" + + # Show scraped content + echo -e "${YELLOW}Scraped content:${NC}" + cat test_scraped_content.txt + + # Count items + albums=$(grep -c "^πŸ’Ώ" test_scraped_content.txt || true) + tracks=$(grep -c "^🎢" test_scraped_content.txt || true) + + echo "" + echo -e "${GREEN}πŸ“Š Found: $albums albums, $tracks tracks${NC}" + + if [[ $albums -eq 0 && $tracks -eq 0 ]]; then + echo -e "${RED}❌ No content found - check website structure${NC}" + exit 1 + fi +else + echo -e "${RED}❌ Scraper failed${NC}" + exit 1 +fi + +# Test playlist generator (dry run) +echo -e "${YELLOW}Testing playlist generator...${NC}" + +# Check if spotify_player binary exists +if [[ ! -f "target/release/spotify_player" ]]; then + echo -e "${YELLOW}⚠️ spotify_player binary not found. Building...${NC}" + if command -v cargo &> /dev/null; then + echo "Building spotify_player..." + if cargo build --release --no-default-features --features "rodio-backend,media-control,image,notify" 2>/dev/null; then + echo -e "${GREEN}βœ… Build successful${NC}" + else + echo -e "${YELLOW}⚠️ Build failed, but will test generator logic anyway${NC}" + fi + else + echo -e "${YELLOW}⚠️ Cargo not found, skipping build${NC}" + fi +fi + +# Test generator with scraped content +echo "Testing playlist generator with scraped content..." +if cat test_scraped_content.txt | ./playlist_generator.tcl > test_playlist_commands.txt 2>/dev/null; then + echo -e "${GREEN}βœ… Generator completed successfully${NC}" + + echo -e "${YELLOW}Generated commands:${NC}" + head -20 test_playlist_commands.txt + + # Count generated commands + command_count=$(grep -c "target/release/spotify_player" test_playlist_commands.txt || true) + echo "" + echo -e "${GREEN}πŸ“Š Generated $command_count spotify_player commands${NC}" +else + echo -e "${RED}❌ Generator failed${NC}" + exit 1 +fi + +# Test complete pipeline +echo -e "${YELLOW}Testing complete pipeline...${NC}" +if ./playlist_scraper.tcl https://www.afera.com.pl/muzyka | ./playlist_generator.tcl > test_complete_pipeline.txt 2>/dev/null; then + echo -e "${GREEN}βœ… Complete pipeline test successful${NC}" + + # Show summary + lines=$(wc -l < test_complete_pipeline.txt) + echo -e "${GREEN}πŸ“Š Pipeline generated $lines lines of output${NC}" +else + echo -e "${RED}❌ Complete pipeline test failed${NC}" + exit 1 +fi + +# Cleanup +echo -e "${YELLOW}Cleaning up test files...${NC}" +rm -f test_scraped_content.txt test_playlist_commands.txt test_complete_pipeline.txt +echo -e "${GREEN}βœ… Cleanup complete${NC}" + +echo "" +echo -e "${GREEN}πŸŽ‰ ALL TESTS PASSED!${NC}" +echo -e "${GREEN}The automation is ready for GitHub Actions deployment.${NC}" +echo "" +echo -e "${YELLOW}Next steps:${NC}" +echo "1. Set up GitHub secrets: SPOTIFY_CLIENT_ID, SPOTIFY_CLIENT_SECRET, SPOTIFY_REFRESH_TOKEN" +echo "2. Push the code to GitHub" +echo "3. The workflow will run every Monday at 10:00 AM UTC" +echo "4. You can also trigger it manually from GitHub Actions tab" \ No newline at end of file From af323b49790067dcf820fad0e5628d5328c40de1 Mon Sep 17 00:00:00 2001 From: tojamrok <16987182+tojamrok@users.noreply.github.com> Date: Sat, 6 Sep 2025 12:26:40 +0200 Subject: [PATCH 16/16] Create weekly-playlist-update-2.yml --- .../workflows/weekly-playlist-update-2.yml | 88 +++++++++++++++++++ 1 file changed, 88 insertions(+) create mode 100644 .github/workflows/weekly-playlist-update-2.yml diff --git a/.github/workflows/weekly-playlist-update-2.yml b/.github/workflows/weekly-playlist-update-2.yml new file mode 100644 index 00000000..ab368b07 --- /dev/null +++ b/.github/workflows/weekly-playlist-update-2.yml @@ -0,0 +1,88 @@ +name: Weekly Afera Playlist Update + +on: + schedule: + # Run every Monday at 10:00 AM UTC + - cron: '0 10 * * 1' + workflow_dispatch: # Allow manual triggering + +env: + CARGO_TERM_COLOR: always + RUST_FEATURES: "rodio-backend,media-control,image,notify" + +jobs: + update-playlist: + runs-on: ubuntu-latest + + steps: + - name: Checkout repository + uses: actions/checkout@v4 + + - name: Install dependencies + run: | + sudo apt-get update + sudo apt-get install -y libssl-dev libasound2-dev libdbus-1-dev libxcb-shape0-dev libxcb-xfixes0-dev tcl tcl-tls + + - name: Install Rust toolchain + uses: dtolnay/rust-toolchain@stable + + - name: Cache cargo deps + uses: actions/cache@v3 + with: + path: | + ~/.cargo/registry/index/ + ~/.cargo/registry/cache/ + ~/.cargo/git/db/ + target/ + key: ${{ runner.os }}-cargo-${{ hashFiles('**/Cargo.lock') }} + + - name: Build spotify_player + run: cargo build --release --no-default-features --features ${{ env.RUST_FEATURES }} + + - name: Set up Spotify credentials + env: + SPOTIFY_CLIENT_ID: ${{ secrets.SPOTIFY_CLIENT_ID }} + SPOTIFY_CLIENT_SECRET: ${{ secrets.SPOTIFY_CLIENT_SECRET }} + SPOTIFY_REFRESH_TOKEN: ${{ secrets.SPOTIFY_REFRESH_TOKEN }} + run: | + # Create config directory if it doesn't exist + mkdir -p ~/.config/spotify-player + + # Create config file with credentials + cat > ~/.config/spotify-player/app.toml << EOF + [application] + client_id = "$SPOTIFY_CLIENT_ID" + + [authentication] + client_secret = "$SPOTIFY_CLIENT_SECRET" + refresh_token = "$SPOTIFY_REFRESH_TOKEN" + EOF + + - name: Make scripts executable + run: | + chmod +x playlist_scraper.tcl + chmod +x playlist_generator.tcl + + - name: Scrape and create playlist + run: | + echo "Scraping Afera website..." + ./playlist_scraper.tcl https://www.afera.com.pl/muzyka > scraped_content.txt + + echo "Content scraped:" + cat scraped_content.txt + + echo "Creating playlist..." + cat scraped_content.txt | ./playlist_generator.tcl > playlist_commands.txt + + echo "Generated commands:" + cat playlist_commands.txt + + - name: Upload artifacts + uses: actions/upload-artifact@v4 + if: always() + with: + name: playlist-update-logs + path: | + scraped_content.txt + playlist_commands.txt + retention-days: 30