Skip to content

Commit 7f94d80

Browse files
committed
fetch cover art image locally on Windows
1 parent b267734 commit 7f94d80

File tree

16 files changed

+521
-250
lines changed

16 files changed

+521
-250
lines changed

rust/Cargo.lock

Lines changed: 7 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

rust/Cargo.toml

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,9 +5,10 @@ edition = "2021"
55

66
[dependencies]
77
jni = "0.21.1"
8-
windows = { version = "0.61.3", features = ["Media_Control", "Win32_Foundation"] }
8+
windows = { version = "0.61.3", features = ["Media_Control", "Win32_Foundation", "Storage_Streams"] }
99
futures = "0.3"
1010
lazy_static = "1.5.0"
11+
libc = "1.0.0-alpha.1"
1112

1213
[lib]
1314
crate-type = ["cdylib"]

rust/src/lib.rs

Lines changed: 158 additions & 60 deletions
Original file line numberDiff line numberDiff line change
@@ -2,107 +2,205 @@ use jni::objects::JClass;
22
use jni::sys::{jint, jlong};
33
use jni::JNIEnv;
44
use std::ffi::CString;
5+
use std::os::raw::c_char;
56

67
use futures::executor::block_on;
8+
use windows::Win32::Foundation::E_FAIL;
79
use windows::{core::*, Media::Control::*};
810

9-
use std::os::raw::c_char;
10-
1111
mod session;
1212
pub use session::get_spotify_session;
1313

14+
#[no_mangle]
15+
pub extern "system" fn isSpotifyAvailable(_env: JNIEnv, _class: JClass) -> jint {
16+
let result = std::panic::catch_unwind(|| {
17+
block_on(async {
18+
match get_spotify_session() {
19+
Ok(Some(_session)) => Ok(1),
20+
Ok(None) => Ok(0),
21+
Err(_) => Err(()),
22+
}
23+
})
24+
});
25+
26+
match result {
27+
Ok(Ok(val)) => val,
28+
_ => 0,
29+
}
30+
}
31+
1432
#[no_mangle]
1533
pub extern "system" fn getPlaybackPosition(_env: JNIEnv, _class: JClass) -> jlong {
16-
let result: Result<jlong> = block_on(async {
17-
match get_spotify_session()? {
18-
Some(session) => {
19-
let timeline = session.GetTimelineProperties()?;
20-
let position = timeline.Position()?.Duration;
21-
22-
// Convert ticks to ms
23-
Ok((position / 10_000) as jlong)
34+
let result = std::panic::catch_unwind(|| {
35+
block_on(async {
36+
match get_spotify_session() {
37+
Ok(Some(session)) => {
38+
let timeline = session.GetTimelineProperties()?;
39+
let position = timeline.Position()?;
40+
Ok((position.Duration / 10_000) as jlong)
41+
}
42+
_ => Err(Error::new(E_FAIL, "Spotify session not available")),
2443
}
25-
None => Ok(0),
26-
}
44+
})
2745
});
28-
result.unwrap_or(0)
46+
47+
match result {
48+
Ok(Ok(position)) => position,
49+
_ => -1,
50+
}
2951
}
3052

3153
#[no_mangle]
3254
pub extern "system" fn getTrackDuration(_env: JNIEnv, _class: JClass) -> jlong {
33-
let result: Result<jlong> = block_on(async {
34-
match get_spotify_session()? {
35-
Some(session) => {
36-
let timeline = session.GetTimelineProperties()?;
37-
let end = timeline.EndTime()?.Duration;
38-
39-
// Convert ticks to ms
40-
Ok((end / 10_000) as jlong)
55+
let result = std::panic::catch_unwind(|| {
56+
block_on(async {
57+
match get_spotify_session()? {
58+
Some(session) => {
59+
let timeline = session.GetTimelineProperties()?;
60+
let duration = timeline.EndTime()?.Duration;
61+
Ok((duration / 10_000) as jlong)
62+
}
63+
_ => Err(Error::new(E_FAIL, "Spotify session not found")),
4164
}
42-
None => Ok(0),
43-
}
65+
})
4466
});
45-
result.unwrap_or(0)
67+
68+
match result {
69+
Ok(Ok(duration)) => duration,
70+
_ => -1,
71+
}
4672
}
4773

4874
#[no_mangle]
4975
pub extern "system" fn getTrackTitle(_env: JNIEnv, _class: JClass) -> *const c_char {
50-
let track_title = block_on(async {
51-
match get_spotify_session() {
52-
Ok(Some(session)) => {
53-
let props = session.TryGetMediaPropertiesAsync().unwrap().get().unwrap();
54-
let title = props.Title().unwrap();
55-
title.to_string()
76+
let result = std::panic::catch_unwind(|| {
77+
block_on(async {
78+
match get_spotify_session()? {
79+
Some(session) => {
80+
let props = session.TryGetMediaPropertiesAsync()?.get()?;
81+
let title = props.Title()?.to_string();
82+
// Safely create CString, fallback to empty string if null bytes present
83+
Ok(CString::new(title).unwrap_or_default().into_raw())
84+
}
85+
_ => Err(Error::new(E_FAIL, "Spotify session not found")),
5686
}
57-
_ => "".to_string(),
58-
}
87+
})
5988
});
6089

61-
// Convert Rust String to CString
62-
let cstring = CString::new(track_title).unwrap_or_else(|_| CString::new("").unwrap());
63-
cstring.into_raw()
90+
match result {
91+
Ok(Ok(ptr)) => ptr,
92+
_ => std::ptr::null(),
93+
}
6494
}
6595

6696
#[no_mangle]
6797
pub extern "system" fn getArtistName(_env: JNIEnv, _class: JClass) -> *const c_char {
68-
let artist_name = block_on(async {
69-
match get_spotify_session() {
70-
Ok(Some(session)) => {
71-
let props = session.TryGetMediaPropertiesAsync().unwrap().get().unwrap();
72-
let artist = props.Artist().unwrap();
73-
artist.to_string()
98+
let result = std::panic::catch_unwind(|| {
99+
block_on(async {
100+
match get_spotify_session()? {
101+
Some(session) => {
102+
let props = session.TryGetMediaPropertiesAsync()?.get()?;
103+
let artist = props.Artist()?.to_string();
104+
Ok(CString::new(artist).unwrap_or_default().into_raw())
105+
}
106+
_ => Err(Error::new(E_FAIL, "Spotify session not found")),
74107
}
75-
_ => "".to_string(),
76-
}
108+
})
77109
});
78110

79-
// Convert Rust String to CString
80-
let cstring = CString::new(artist_name).unwrap_or_else(|_| CString::new("").unwrap());
81-
cstring.into_raw()
111+
match result {
112+
Ok(Ok(ptr)) => ptr,
113+
_ => std::ptr::null(),
114+
}
82115
}
83116

84117
#[no_mangle]
85118
pub extern "system" fn isPlaying(_env: JNIEnv, _class: JClass) -> jint {
86-
let result: Result<jint> = block_on(async {
87-
match get_spotify_session()? {
88-
Some(session) => {
89-
let playback_info = session.GetPlaybackInfo()?;
90-
let status = playback_info.PlaybackStatus()?;
91-
let is_playing = (status
92-
== GlobalSystemMediaTransportControlsSessionPlaybackStatus::Playing)
93-
as jint;
94-
Ok(is_playing)
119+
let result = std::panic::catch_unwind(|| {
120+
block_on(async {
121+
match get_spotify_session()? {
122+
Some(session) => {
123+
let playback_info = session.GetPlaybackInfo()?;
124+
let status = playback_info.PlaybackStatus()?;
125+
Ok(
126+
(status == GlobalSystemMediaTransportControlsSessionPlaybackStatus::Playing)
127+
as jint,
128+
)
129+
}
130+
_ => Err(Error::new(E_FAIL, "Spotify session not found")),
95131
}
96-
None => Ok(0),
97-
}
132+
})
98133
});
99-
result.unwrap_or(0)
134+
135+
match result {
136+
Ok(Ok(val)) => val,
137+
_ => -1,
138+
}
139+
}
140+
141+
#[no_mangle]
142+
pub extern "system" fn getCoverArt(out_ptr: *mut *mut u8, out_len: *mut usize) -> i32 {
143+
if out_ptr.is_null() || out_len.is_null() {
144+
return 0; // false
145+
}
146+
let result = std::panic::catch_unwind(|| {
147+
block_on(async {
148+
match get_spotify_session()? {
149+
Some(session) => {
150+
let props = session.TryGetMediaPropertiesAsync()?.get()?;
151+
let thumbnail = props.Thumbnail()?;
152+
let stream = thumbnail.OpenReadAsync()?.get()?;
153+
let size = stream.Size()?;
154+
155+
if size == 0 {
156+
return Err(Error::new(E_FAIL, "Cover art size is zero"));
157+
}
158+
159+
use windows::Storage::Streams::DataReader;
160+
let reader = DataReader::CreateDataReader(&stream)?;
161+
reader.LoadAsync(size as u32)?.get()?;
162+
163+
let mut buffer = vec![0u8; size as usize];
164+
reader.ReadBytes(&mut buffer)?;
165+
Ok(buffer)
166+
}
167+
_ => Err(Error::empty()),
168+
}
169+
})
170+
});
171+
172+
match result {
173+
Ok(Ok(buffer)) => unsafe {
174+
let len = buffer.len();
175+
let ptr = libc::malloc(len);
176+
if ptr.is_null() {
177+
return 0; // false
178+
}
179+
std::ptr::copy_nonoverlapping(buffer.as_ptr(), ptr as *mut u8, len);
180+
*out_ptr = ptr as *mut u8;
181+
*out_len = len;
182+
1 // true
183+
},
184+
_ => {
185+
0 // false
186+
}
187+
}
100188
}
101189

102190
#[no_mangle]
103191
pub extern "system" fn freeString(s: *mut c_char) {
104-
if s.is_null() { return; }
192+
if !s.is_null() {
193+
unsafe {
194+
let _ = CString::from_raw(s);
195+
}
196+
}
197+
}
198+
199+
#[no_mangle]
200+
pub extern "system" fn freeCoverArt(ptr: *mut u8) {
105201
unsafe {
106-
let _ = CString::from_raw(s);
202+
if !ptr.is_null() {
203+
libc::free(ptr as *mut _);
204+
}
107205
}
108206
}

rust/src/main.rs

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
use futures::executor::block_on;
2+
use windows::Storage::Streams::DataReader;
23
use windows::{core::*, Media::Control::*};
34

45
mod session;
@@ -41,6 +42,23 @@ fn main() -> Result<()> {
4142
timeline.EndTime()?.Duration / 10_000
4243
);
4344

45+
if let Some(media_properties) = session.TryGetMediaPropertiesAsync()?.get().ok() {
46+
println!("Current Track Name: {}", media_properties.Title()?);
47+
println!("Current Artist Name: {}", media_properties.Artist()?);
48+
49+
// Get cover art thumbnail
50+
let thumbnail = media_properties.Thumbnail()?; // No Option here
51+
let stream_ref = thumbnail.OpenReadAsync()?.get()?;
52+
let size = stream_ref.Size()?;
53+
54+
let reader = DataReader::CreateDataReader(&stream_ref)?;
55+
reader.LoadAsync(size as u32)?.get()?;
56+
let mut buffer = vec![0u8; size as usize];
57+
reader.ReadBytes(&mut buffer)?;
58+
59+
println!("Cover art thumbnail size: {} bytes", buffer.len());
60+
}
61+
4462
Ok(())
4563
})
4664
}

0 commit comments

Comments
 (0)