Skip to content

Commit 3553f00

Browse files
committed
support latest spotify version, support for 64 bit, version 1.1.12
1 parent eb641b3 commit 3553f00

File tree

9 files changed

+263
-118
lines changed

9 files changed

+263
-118
lines changed

README.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -23,7 +23,7 @@ repositories {
2323
}
2424
2525
dependencies {
26-
implementation 'com.github.LabyStudio:java-spotify-api:1.1.10:all'
26+
implementation 'com.github.LabyStudio:java-spotify-api:1.1.13:all'
2727
}
2828
```
2929

build.gradle

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@ plugins {
44
}
55

66
group 'de.labystudio'
7-
version '1.1.12'
7+
version '1.1.13'
88

99
compileJava {
1010
sourceCompatibility = '1.8'

src/main/java/de/labystudio/spotifyapi/platform/windows/api/WinApi.java

Lines changed: 39 additions & 36 deletions
Original file line numberDiff line numberDiff line change
@@ -77,6 +77,10 @@ default WinNT.HANDLE openProcessHandle(int processId) {
7777
}
7878

7979
default WinDef.HWND openWindow(int processId) {
80+
return this.openWindow(processId, hWnd -> true);
81+
}
82+
83+
default WinDef.HWND openWindow(int processId, WindowCondition condition) {
8084
AtomicReference<WinDef.HWND> window = new AtomicReference<>();
8185

8286
// Iterate over all windows and find the one with the given processId
@@ -87,7 +91,7 @@ default WinDef.HWND openWindow(int processId) {
8791
User32.INSTANCE.GetWindowThreadProcessId(hWnd, reference);
8892

8993
// Check if the window belongs to the process
90-
if (reference.getValue() == processId && this.isWindowVisible(hWnd)) {
94+
if (reference.getValue() == processId && this.isWindowVisible(hWnd) && condition.test(hWnd)) {
9195
window.set(hWnd);
9296
return false;
9397
}
@@ -112,75 +116,70 @@ default String getWindowTitle(WinDef.HWND window) {
112116
default Map<String, Psapi.ModuleInfo> getModules(WinNT.HANDLE handle) {
113117
Map<String, Psapi.ModuleInfo> modules = new HashMap<>();
114118

115-
Psapi psapi = Psapi.INSTANCE;
116-
117-
// Get the list of modules
118-
Pointer[] moduleHandles = new Pointer[1024];
119-
psapi.EnumProcessModulesEx(
120-
handle,
121-
moduleHandles,
122-
moduleHandles.length,
123-
null,
124-
Psapi.ModuleFilter.X32BIT
125-
);
126-
127119
// Iterate over all modules
120+
Pointer[] moduleHandles = this.getModuleHandles(handle);
128121
for (Pointer moduleHandle : moduleHandles) {
129-
if (moduleHandle == null) {
130-
break;
131-
}
132-
133122
// Get module name
134123
char[] characters = new char[1024];
135-
int length = psapi.GetModuleBaseName(handle, moduleHandle, characters, characters.length);
124+
int length = Psapi.INSTANCE.GetModuleBaseName(handle, moduleHandle, characters, characters.length);
136125
String moduleName = new String(characters, 0, length);
137126

138127
// Get module info
139128
Psapi.ModuleInfo moduleInfo = new Psapi.ModuleInfo();
140-
psapi.GetModuleInformation(handle, moduleHandle, moduleInfo, moduleInfo.size());
129+
Psapi.INSTANCE.GetModuleInformation(handle, moduleHandle, moduleInfo, moduleInfo.size());
141130
modules.put(moduleName, moduleInfo);
142131
}
143132

144133
return modules;
145134
}
146135

147136
default Psapi.ModuleInfo getModuleInfo(WinNT.HANDLE handle, String moduleName) {
148-
Psapi psapi = Psapi.INSTANCE;
149-
150-
// Get the list of modules
151-
Pointer[] moduleHandles = new Pointer[1024];
152-
psapi.EnumProcessModulesEx(
153-
handle,
154-
moduleHandles,
155-
moduleHandles.length,
156-
null,
157-
Psapi.ModuleFilter.X32BIT
158-
);
137+
Pointer[] moduleHandles = this.getModuleHandles(handle);
159138

160139
// Iterate over all modules
161140
for (Pointer moduleHandle : moduleHandles) {
162-
if (moduleHandle == null) {
163-
break;
164-
}
165-
166141
// Get module name
167142
char[] characters = new char[1024];
168-
int length = psapi.GetModuleBaseName(handle, moduleHandle, characters, characters.length);
143+
int length = Psapi.INSTANCE.GetModuleBaseName(handle, moduleHandle, characters, characters.length);
169144
String entryModuleName = new String(characters, 0, length);
170145

171146
// Compare with the name we are looking for
172147
if (entryModuleName.equals(moduleName)) {
173148

174149
// Get module info
175150
Psapi.ModuleInfo moduleInfo = new Psapi.ModuleInfo();
176-
psapi.GetModuleInformation(handle, moduleHandle, moduleInfo, moduleInfo.size());
151+
Psapi.INSTANCE.GetModuleInformation(handle, moduleHandle, moduleInfo, moduleInfo.size());
177152

178153
return moduleInfo;
179154
}
180155
}
156+
181157
return null;
182158
}
183159

160+
default Pointer[] getModuleHandles(WinNT.HANDLE handle) {
161+
IntByReference amountRef = new IntByReference();
162+
163+
// Get the list of modules
164+
Pointer[] moduleHandles = new Pointer[2048];
165+
if (!Psapi.INSTANCE.EnumProcessModulesEx(
166+
handle,
167+
moduleHandles,
168+
moduleHandles.length,
169+
amountRef,
170+
Psapi.ModuleFilter.ALL
171+
)) {
172+
throw new RuntimeException("Failed to get module list: ERROR " + Kernel32.INSTANCE.GetLastError());
173+
}
174+
175+
int amount = amountRef.getValue();
176+
if (amount == 0) {
177+
throw new RuntimeException("No modules found");
178+
}
179+
180+
return Arrays.copyOf(moduleHandles, amount);
181+
}
182+
184183
default void pressKey(int keyCode) {
185184
WinUser.INPUT input = new WinUser.INPUT();
186185

@@ -200,4 +199,8 @@ default void pressKey(int keyCode) {
200199
User32.INSTANCE.SendInput(new WinDef.DWORD(1), new WinUser.INPUT[]{input}, input.size());
201200

202201
}
202+
203+
public interface WindowCondition {
204+
boolean test(WinDef.HWND window);
205+
}
203206
}

src/main/java/de/labystudio/spotifyapi/platform/windows/api/WinProcess.java

Lines changed: 91 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,7 @@
11
package de.labystudio.spotifyapi.platform.windows.api;
22

3+
import com.google.gson.Gson;
4+
import com.google.gson.JsonObject;
35
import com.sun.jna.Memory;
46
import com.sun.jna.Pointer;
57
import com.sun.jna.platform.win32.WinDef;
@@ -18,10 +20,14 @@
1820
*/
1921
public class WinProcess implements WinApi {
2022

23+
protected final static Gson GSON = new Gson();
24+
2125
protected final int processId;
2226
protected final WinNT.HANDLE handle;
2327
protected final WinDef.HWND window;
2428

29+
protected long scanTimeout = 1000 * 10;
30+
2531
/**
2632
* Creates a new instance of the {@link WinProcess} class.
2733
*
@@ -39,7 +45,10 @@ public WinProcess(String executableName) {
3945
throw new IllegalStateException("Process handle of " + this.processId + " not found");
4046
}
4147

42-
this.window = this.openWindow(this.processId);
48+
this.window = this.openWindow(this.processId, hWnd -> {
49+
String title = this.getWindowTitle(hWnd);
50+
return !title.equals("Spotify Debug Window") && !title.equals("DevTools");
51+
});
4352
if (this.getWindowTitle().isEmpty()) {
4453
throw new IllegalStateException("Window for process " + this.processId + " not found");
4554
}
@@ -112,6 +121,7 @@ public byte[] readBytes(long address, int length) {
112121
* @return The address of the first matching bytes.
113122
*/
114123
public long findInMemory(long minAddress, long maxAddress, byte[] searchBytes) {
124+
long timeStart = System.currentTimeMillis();
115125
int chunkSize = 1024 * 64;
116126

117127
for (long cursor = minAddress; cursor < maxAddress; cursor += chunkSize) {
@@ -129,6 +139,11 @@ public long findInMemory(long minAddress, long maxAddress, byte[] searchBytes) {
129139
return cursor + i;
130140
}
131141
}
142+
143+
long timePassed = System.currentTimeMillis() - timeStart;
144+
if (timePassed > this.scanTimeout) {
145+
throw new IllegalStateException("Scan timeout of " + this.scanTimeout + "ms reached at address " + cursor);
146+
}
132147
}
133148
return -1;
134149
}
@@ -194,6 +209,23 @@ public boolean hasBytes(long address, byte[] bytes) {
194209
return true;
195210
}
196211

212+
/**
213+
* Check if one of the given bytes are at the given address.
214+
*
215+
* @param address The address to check.
216+
* @param chunksOfBytes The chunks of bytes to check.
217+
* @return True if one of the bytes are at the given address.
218+
*/
219+
public boolean hasBytes(long address, byte[]... chunksOfBytes) {
220+
for (byte[] bytes : chunksOfBytes) {
221+
if (this.hasBytes(address, bytes)) {
222+
return true;
223+
}
224+
}
225+
return false;
226+
}
227+
228+
197229
/**
198230
* Check if the given text is at the given address.
199231
*
@@ -205,6 +237,22 @@ public boolean hasText(long address, String text) {
205237
return this.hasBytes(address, text.getBytes());
206238
}
207239

240+
/**
241+
* Check if one of the given text is at the given address.
242+
*
243+
* @param address The address to check.
244+
* @param texts The texts to check.
245+
* @return True if one of the text is at the given address.
246+
*/
247+
public boolean hasText(long address, String... texts) {
248+
for (String text : texts) {
249+
if (this.hasText(address, text)) {
250+
return true;
251+
}
252+
}
253+
return false;
254+
}
255+
208256
/**
209257
* Search for the given text in the given module.
210258
*
@@ -243,7 +291,7 @@ public long findAddressOfText(long start, String text, int index) {
243291
* @return The address of the text at the given index
244292
*/
245293
public long findAddressOfText(long start, String text, SearchCondition condition) {
246-
return this.findAddressOfText(start, Integer.MAX_VALUE, text, condition);
294+
return this.findAddressOfText(start, Long.MAX_VALUE, text, condition);
247295
}
248296

249297
/**
@@ -296,6 +344,37 @@ public long findAddressUsingRules(SearchRule... rules) {
296344
return cursor;
297345
}
298346

347+
public JsonObject readJsonObject(long address) {
348+
int depth = 0;
349+
boolean inString = false;
350+
int chunkSize = 1024;
351+
352+
long offset = 0;
353+
StringBuilder json = new StringBuilder();
354+
do {
355+
String chunk = this.readString(address + offset, chunkSize);
356+
for (int i = 0; i < chunk.length(); i++) {
357+
char c = chunk.charAt(i);
358+
if (c == '"') {
359+
inString = !inString;
360+
}
361+
if (!inString) {
362+
if (c == '{' || c == '[') {
363+
depth++;
364+
} else if (c == '}' || c == ']') {
365+
depth--;
366+
}
367+
}
368+
json.append(c);
369+
if (depth == 0) {
370+
break;
371+
}
372+
}
373+
offset += chunkSize;
374+
} while (depth > 0);
375+
return GSON.fromJson(json.toString(), JsonObject.class);
376+
}
377+
299378
/**
300379
* Get the module information of the given module name.
301380
*
@@ -373,6 +452,16 @@ public WinNT.HANDLE getHandle() {
373452
return this.handle;
374453
}
375454

455+
/**
456+
* Set the timeout for the memory scan in milliseconds.
457+
* It will throw an exception if the timeout is reached.
458+
*
459+
* @param scanTimeout The timeout in milliseconds.
460+
*/
461+
public void setScanTimeout(long scanTimeout) {
462+
this.scanTimeout = scanTimeout;
463+
}
464+
376465
/**
377466
* Checks if the current handle is not null.
378467
*

src/main/java/de/labystudio/spotifyapi/platform/windows/api/jna/Kernel32.java

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,4 +24,6 @@ public interface Kernel32 extends WinNT, StdCallLibrary {
2424
boolean CloseHandle(HANDLE hObject);
2525

2626
HANDLE OpenProcess(int fdwAccess, boolean fInherit, int IDProcess);
27+
28+
int GetLastError();
2729
}

src/main/java/de/labystudio/spotifyapi/platform/windows/api/playback/PlaybackAccessor.java

Lines changed: 8 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -22,22 +22,17 @@ public class PlaybackAccessor {
2222
/**
2323
* Creates a new instance of the PlaybackAccessor.
2424
*
25-
* @param process The Spotify process to read from.
26-
* @param contextBaseAddress The base address of the context.
25+
* @param process The Spotify process to read from.
26+
* @param address The reference address of the playback section
2727
*/
28-
public PlaybackAccessor(WinProcess process, long contextBaseAddress) {
28+
public PlaybackAccessor(WinProcess process, long address) {
2929
this.process = process;
3030

3131
// Create pointer registry to calculate the absolute addresses using the relative offsets
32-
this.pointerRegistry = new PointerRegistry(0x0D3A2064, contextBaseAddress);
33-
this.pointerRegistry.register("position", 0x0D3A2178);
34-
this.pointerRegistry.register("length", 0x0D3A2188);
35-
this.pointerRegistry.register("is_playing", 0x0D3A21AC);
36-
37-
// Parity pointers to make sure that we have the correct base address
38-
this.pointerRegistry.register("parity_1", 0x0D3A2199);
39-
this.pointerRegistry.register("parity_2", 0x0D3A21A4);
40-
this.pointerRegistry.register("parity_3", 0x0D3A216E);
32+
this.pointerRegistry = new PointerRegistry(0x0CFF4498, address);
33+
this.pointerRegistry.register("position", 0x0CFF4810);
34+
this.pointerRegistry.register("length", 0x0CFF4820);
35+
this.pointerRegistry.register("is_playing", 0x0CFF4850); // 1=true, 0=false
4136

4237
this.update();
4338
}
@@ -73,10 +68,7 @@ public boolean isValid() {
7368
return this.position <= this.length
7469
&& this.position >= 0
7570
&& this.length <= MAX_TRACK_DURATION
76-
&& this.length >= MIN_TRACK_DURATION
77-
&& this.process.readBoolean(this.pointerRegistry.getAddress("parity_1")) != this.isPlaying
78-
&& this.process.readBoolean(this.pointerRegistry.getAddress("parity_2")) != this.isPlaying
79-
&& (this.process.readByte(this.pointerRegistry.getAddress("parity_3")) == 0) != this.isPlaying;
71+
&& this.length >= MIN_TRACK_DURATION;
8072
}
8173

8274
public int getLength() {

0 commit comments

Comments
 (0)