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

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@

### Added

- **SMTC Integration**: System Media Transport Controls is now integrated with the app for Windows users. Users can now control playback state using media keys and check playback state in media flyouts ([#229](https://github.com/LargeModGames/spotatui/issues/229)).
- **Click and drag to seek on the playbar**: The progress bar is now interactive. Click anywhere on the gauge to jump to that position, or click and drag to scrub. Control buttons keep priority, the time label stays non-clickable, and seeks reuse the existing native and throttled-API paths ([#157](https://github.com/LargeModGames/spotatui/issues/157)).

### Fixed
Expand Down
12 changes: 12 additions & 0 deletions Cargo.lock

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

4 changes: 3 additions & 1 deletion Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -85,6 +85,7 @@ arboard = "3.4"
# Without this, librespot-playback falls back to the `pipe` sink which writes raw audio bytes
# to stdout (breaking the TUI and producing no audible output).
librespot-playback = { version = "0.8", optional = true, default-features = false, features = ["rodio-backend"] }
smtc-tokio = { version = "0.1.0", optional = true }

# MPRIS D-Bus support (Linux only)
[target.'cfg(target_os = "linux")'.dependencies]
Expand All @@ -104,7 +105,7 @@ objc2 = { version = "0.6", optional = true }
block2 = { version = "0.6", optional = true }

[features]
default = ["telemetry", "streaming", "audio-viz-cpal", "macos-media", "discord-rpc", "mpris", "self-update"]
default = ["telemetry", "streaming", "audio-viz-cpal", "macos-media", "discord-rpc", "mpris", "self-update", "windows-media"]
telemetry = []
self-update = ["dep:self_update", "dep:sha2", "dep:hex"]
streaming = ["librespot-core", "librespot-playback", "librespot-connect", "librespot-oauth", "librespot-metadata", "librespot-protocol", "protobuf"]
Expand All @@ -121,6 +122,7 @@ audio-viz = ["realfft", "pipewire"]
audio-viz-cpal = ["realfft", "cpal"] # Alternative for Windows/macOS or if pipewire issues
mpris = ["mpris-server"] # MPRIS D-Bus integration (Linux only)
macos-media = ["objc2-media-player", "objc2-foundation", "objc2-app-kit", "objc2", "block2", "streaming"] # macOS Now Playing integration
windows-media = ["smtc-tokio", "streaming"] # windows SMTC integration
discord-rpc = ["discord-rich-presence"]
cover-art = ["ratatui-image", "image"]

Expand Down
3 changes: 2 additions & 1 deletion src/infra/media_metadata.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,8 @@
not(any(
feature = "discord-rpc",
all(feature = "mpris", target_os = "linux"),
all(feature = "macos-media", target_os = "macos")
all(feature = "macos-media", target_os = "macos"),
all(feature = "windows-media", target_os = "windows")
)),
allow(dead_code)
)]
Expand Down
59 changes: 59 additions & 0 deletions src/infra/player/events.rs
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,8 @@ pub struct PlayerEventContext {
pub mpris_manager: Option<Arc<mpris::MprisManager>>,
#[cfg(all(feature = "macos-media", target_os = "macos"))]
pub macos_media_manager: Option<Arc<macos_media::MacMediaManager>>,
#[cfg(all(feature = "windows-media", target_os = "windows"))]
pub windows_media_manager: Option<Arc<smtc_tokio::WindowsMediaManager>>,
}

pub struct StreamingRecoveryContext {
Expand All @@ -44,6 +46,8 @@ pub struct StreamingRecoveryContext {
pub mpris_manager: Option<Arc<mpris::MprisManager>>,
#[cfg(all(feature = "macos-media", target_os = "macos"))]
pub macos_media_manager: Option<Arc<macos_media::MacMediaManager>>,
#[cfg(all(feature = "windows-media", target_os = "windows"))]
pub windows_media_manager: Option<Arc<smtc_tokio::WindowsMediaManager>>,
}

pub fn spawn_streaming_recovery_handler(ctx: StreamingRecoveryContext) {
Expand Down Expand Up @@ -108,6 +112,8 @@ async fn handle_streaming_recovery(mut ctx: StreamingRecoveryContext) {
mpris_manager: ctx.mpris_manager.clone(),
#[cfg(all(feature = "macos-media", target_os = "macos"))]
macos_media_manager: ctx.macos_media_manager.clone(),
#[cfg(all(feature = "windows-media", target_os = "windows"))]
windows_media_manager: ctx.windows_media_manager.clone(),
});
}
Err(e) => {
Expand Down Expand Up @@ -138,6 +144,8 @@ pub fn spawn_player_event_handler(ctx: PlayerEventContext) {
let mpris_manager = ctx.mpris_manager.clone();
#[cfg(all(feature = "macos-media", target_os = "macos"))]
let macos_media_manager = ctx.macos_media_manager.clone();
#[cfg(all(feature = "windows-media", target_os = "windows"))]
let windows_media_manager = ctx.windows_media_manager.clone();

tokio::spawn(async move {
handle_player_events(
Expand All @@ -151,6 +159,8 @@ pub fn spawn_player_event_handler(ctx: PlayerEventContext) {
mpris_manager,
#[cfg(all(feature = "macos-media", target_os = "macos"))]
macos_media_manager,
#[cfg(all(feature = "windows-media", target_os = "windows"))]
windows_media_manager,
)
.await;
});
Expand All @@ -171,6 +181,9 @@ async fn handle_player_events(
#[cfg(all(feature = "macos-media", target_os = "macos"))] macos_media_manager: Option<
Arc<macos_media::MacMediaManager>,
>,
#[cfg(all(feature = "windows-media", target_os = "windows"))] windows_media_manager: Option<
Arc<smtc_tokio::WindowsMediaManager>,
>,
) {
use chrono::TimeDelta;

Expand Down Expand Up @@ -206,6 +219,11 @@ async fn handle_player_events(
macos_media.set_playback_status(true);
}

#[cfg(all(feature = "windows-media", target_os = "windows"))]
if let Some(ref windows_media) = windows_media_manager {
windows_media.set_playback_status(true);
}

{
let mut app_lock = app.lock().await;
app_lock.native_is_playing = Some(true);
Expand Down Expand Up @@ -252,6 +270,11 @@ async fn handle_player_events(
macos_media.set_playback_status(false);
}

#[cfg(all(feature = "windows-media", target_os = "windows"))]
if let Some(ref windows_media) = windows_media_manager {
windows_media.set_playback_status(false);
}

{
let mut app_lock = app.lock().await;
app_lock.native_is_playing = Some(false);
Expand All @@ -277,6 +300,11 @@ async fn handle_player_events(
macos_media.set_position(position_ms as u64);
}

#[cfg(all(feature = "windows-media", target_os = "windows"))]
if let Some(ref windows_media) = windows_media_manager {
windows_media.set_position(position_ms as u64);
}

if let Ok(mut app) = app.try_lock() {
app.song_progress_ms = position_ms as u128;
app.seek_ms = None;
Expand Down Expand Up @@ -332,6 +360,17 @@ async fn handle_player_events(
);
}

#[cfg(all(feature = "windows-media", target_os = "windows"))]
if let Some(ref windows_media) = windows_media_manager {
windows_media.set_metadata(
&audio_item.name,
&artists,
&album,
audio_item.duration_ms as u64,
None,
);
}

let mut app = app.lock().await;
app.native_track_info = Some(app::NativeTrackInfo {
name: audio_item.name.clone(),
Expand All @@ -357,6 +396,11 @@ async fn handle_player_events(
macos_media.set_stopped();
}

#[cfg(all(feature = "windows-media", target_os = "windows"))]
if let Some(ref windows_media) = windows_media_manager {
windows_media.set_stopped();
}

if let Ok(mut app) = app.try_lock() {
if let Some(ref mut ctx) = app.current_playback_context {
ctx.is_playing = false;
Expand All @@ -383,6 +427,11 @@ async fn handle_player_events(
macos_media.set_stopped();
}

#[cfg(all(feature = "windows-media", target_os = "windows"))]
if let Some(ref windows_media) = windows_media_manager {
windows_media.set_stopped();
}

if let Ok(mut app) = app.try_lock() {
if let Some(ref mut ctx) = app.current_playback_context {
ctx.is_playing = false;
Expand Down Expand Up @@ -445,6 +494,11 @@ async fn handle_player_events(
if let Some(ref macos_media) = macos_media_manager {
macos_media.set_position(position_ms as u64);
}

#[cfg(all(feature = "windows-media", target_os = "windows"))]
if let Some(ref windows_media) = windows_media_manager {
windows_media.set_position(position_ms as u64);
}
}
PlayerEvent::SessionDisconnected { .. } => {
#[cfg(all(feature = "mpris", target_os = "linux"))]
Expand All @@ -457,6 +511,11 @@ async fn handle_player_events(
macos_media.set_stopped();
}

#[cfg(all(feature = "windows-media", target_os = "windows"))]
if let Some(ref windows_media) = windows_media_manager {
windows_media.set_stopped();
}

if let Some(request) = disconnect_streaming_player(
&app,
&player,
Expand Down
Loading
Loading