Skip to content

Commit

Permalink
support latest spotify version, support for 64 bit, version 1.1.12
Browse files Browse the repository at this point in the history
  • Loading branch information
LabyStudio committed Jul 5, 2023
1 parent eb641b3 commit 3553f00
Show file tree
Hide file tree
Showing 9 changed files with 263 additions and 118 deletions.
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,7 @@ repositories {
}
dependencies {
implementation 'com.github.LabyStudio:java-spotify-api:1.1.10:all'
implementation 'com.github.LabyStudio:java-spotify-api:1.1.13:all'
}
```

Expand Down
2 changes: 1 addition & 1 deletion build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ plugins {
}

group 'de.labystudio'
version '1.1.12'
version '1.1.13'

compileJava {
sourceCompatibility = '1.8'
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -77,6 +77,10 @@ default WinNT.HANDLE openProcessHandle(int processId) {
}

default WinDef.HWND openWindow(int processId) {
return this.openWindow(processId, hWnd -> true);
}

default WinDef.HWND openWindow(int processId, WindowCondition condition) {
AtomicReference<WinDef.HWND> window = new AtomicReference<>();

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

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

Psapi psapi = Psapi.INSTANCE;

// Get the list of modules
Pointer[] moduleHandles = new Pointer[1024];
psapi.EnumProcessModulesEx(
handle,
moduleHandles,
moduleHandles.length,
null,
Psapi.ModuleFilter.X32BIT
);

// Iterate over all modules
Pointer[] moduleHandles = this.getModuleHandles(handle);
for (Pointer moduleHandle : moduleHandles) {
if (moduleHandle == null) {
break;
}

// Get module name
char[] characters = new char[1024];
int length = psapi.GetModuleBaseName(handle, moduleHandle, characters, characters.length);
int length = Psapi.INSTANCE.GetModuleBaseName(handle, moduleHandle, characters, characters.length);
String moduleName = new String(characters, 0, length);

// Get module info
Psapi.ModuleInfo moduleInfo = new Psapi.ModuleInfo();
psapi.GetModuleInformation(handle, moduleHandle, moduleInfo, moduleInfo.size());
Psapi.INSTANCE.GetModuleInformation(handle, moduleHandle, moduleInfo, moduleInfo.size());
modules.put(moduleName, moduleInfo);
}

return modules;
}

default Psapi.ModuleInfo getModuleInfo(WinNT.HANDLE handle, String moduleName) {
Psapi psapi = Psapi.INSTANCE;

// Get the list of modules
Pointer[] moduleHandles = new Pointer[1024];
psapi.EnumProcessModulesEx(
handle,
moduleHandles,
moduleHandles.length,
null,
Psapi.ModuleFilter.X32BIT
);
Pointer[] moduleHandles = this.getModuleHandles(handle);

// Iterate over all modules
for (Pointer moduleHandle : moduleHandles) {
if (moduleHandle == null) {
break;
}

// Get module name
char[] characters = new char[1024];
int length = psapi.GetModuleBaseName(handle, moduleHandle, characters, characters.length);
int length = Psapi.INSTANCE.GetModuleBaseName(handle, moduleHandle, characters, characters.length);
String entryModuleName = new String(characters, 0, length);

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

// Get module info
Psapi.ModuleInfo moduleInfo = new Psapi.ModuleInfo();
psapi.GetModuleInformation(handle, moduleHandle, moduleInfo, moduleInfo.size());
Psapi.INSTANCE.GetModuleInformation(handle, moduleHandle, moduleInfo, moduleInfo.size());

return moduleInfo;
}
}

return null;
}

default Pointer[] getModuleHandles(WinNT.HANDLE handle) {
IntByReference amountRef = new IntByReference();

// Get the list of modules
Pointer[] moduleHandles = new Pointer[2048];
if (!Psapi.INSTANCE.EnumProcessModulesEx(
handle,
moduleHandles,
moduleHandles.length,
amountRef,
Psapi.ModuleFilter.ALL
)) {
throw new RuntimeException("Failed to get module list: ERROR " + Kernel32.INSTANCE.GetLastError());
}

int amount = amountRef.getValue();
if (amount == 0) {
throw new RuntimeException("No modules found");
}

return Arrays.copyOf(moduleHandles, amount);
}

default void pressKey(int keyCode) {
WinUser.INPUT input = new WinUser.INPUT();

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

}

public interface WindowCondition {
boolean test(WinDef.HWND window);
}
}
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
package de.labystudio.spotifyapi.platform.windows.api;

import com.google.gson.Gson;
import com.google.gson.JsonObject;
import com.sun.jna.Memory;
import com.sun.jna.Pointer;
import com.sun.jna.platform.win32.WinDef;
Expand All @@ -18,10 +20,14 @@
*/
public class WinProcess implements WinApi {

protected final static Gson GSON = new Gson();

protected final int processId;
protected final WinNT.HANDLE handle;
protected final WinDef.HWND window;

protected long scanTimeout = 1000 * 10;

/**
* Creates a new instance of the {@link WinProcess} class.
*
Expand All @@ -39,7 +45,10 @@ public WinProcess(String executableName) {
throw new IllegalStateException("Process handle of " + this.processId + " not found");
}

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

for (long cursor = minAddress; cursor < maxAddress; cursor += chunkSize) {
Expand All @@ -129,6 +139,11 @@ public long findInMemory(long minAddress, long maxAddress, byte[] searchBytes) {
return cursor + i;
}
}

long timePassed = System.currentTimeMillis() - timeStart;
if (timePassed > this.scanTimeout) {
throw new IllegalStateException("Scan timeout of " + this.scanTimeout + "ms reached at address " + cursor);
}
}
return -1;
}
Expand Down Expand Up @@ -194,6 +209,23 @@ public boolean hasBytes(long address, byte[] bytes) {
return true;
}

/**
* Check if one of the given bytes are at the given address.
*
* @param address The address to check.
* @param chunksOfBytes The chunks of bytes to check.
* @return True if one of the bytes are at the given address.
*/
public boolean hasBytes(long address, byte[]... chunksOfBytes) {
for (byte[] bytes : chunksOfBytes) {
if (this.hasBytes(address, bytes)) {
return true;
}
}
return false;
}


/**
* Check if the given text is at the given address.
*
Expand All @@ -205,6 +237,22 @@ public boolean hasText(long address, String text) {
return this.hasBytes(address, text.getBytes());
}

/**
* Check if one of the given text is at the given address.
*
* @param address The address to check.
* @param texts The texts to check.
* @return True if one of the text is at the given address.
*/
public boolean hasText(long address, String... texts) {
for (String text : texts) {
if (this.hasText(address, text)) {
return true;
}
}
return false;
}

/**
* Search for the given text in the given module.
*
Expand Down Expand Up @@ -243,7 +291,7 @@ public long findAddressOfText(long start, String text, int index) {
* @return The address of the text at the given index
*/
public long findAddressOfText(long start, String text, SearchCondition condition) {
return this.findAddressOfText(start, Integer.MAX_VALUE, text, condition);
return this.findAddressOfText(start, Long.MAX_VALUE, text, condition);
}

/**
Expand Down Expand Up @@ -296,6 +344,37 @@ public long findAddressUsingRules(SearchRule... rules) {
return cursor;
}

public JsonObject readJsonObject(long address) {
int depth = 0;
boolean inString = false;
int chunkSize = 1024;

long offset = 0;
StringBuilder json = new StringBuilder();
do {
String chunk = this.readString(address + offset, chunkSize);
for (int i = 0; i < chunk.length(); i++) {
char c = chunk.charAt(i);
if (c == '"') {
inString = !inString;
}
if (!inString) {
if (c == '{' || c == '[') {
depth++;
} else if (c == '}' || c == ']') {
depth--;
}
}
json.append(c);
if (depth == 0) {
break;
}
}
offset += chunkSize;
} while (depth > 0);
return GSON.fromJson(json.toString(), JsonObject.class);
}

/**
* Get the module information of the given module name.
*
Expand Down Expand Up @@ -373,6 +452,16 @@ public WinNT.HANDLE getHandle() {
return this.handle;
}

/**
* Set the timeout for the memory scan in milliseconds.
* It will throw an exception if the timeout is reached.
*
* @param scanTimeout The timeout in milliseconds.
*/
public void setScanTimeout(long scanTimeout) {
this.scanTimeout = scanTimeout;
}

/**
* Checks if the current handle is not null.
*
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -24,4 +24,6 @@ public interface Kernel32 extends WinNT, StdCallLibrary {
boolean CloseHandle(HANDLE hObject);

HANDLE OpenProcess(int fdwAccess, boolean fInherit, int IDProcess);

int GetLastError();
}
Original file line number Diff line number Diff line change
Expand Up @@ -22,22 +22,17 @@ public class PlaybackAccessor {
/**
* Creates a new instance of the PlaybackAccessor.
*
* @param process The Spotify process to read from.
* @param contextBaseAddress The base address of the context.
* @param process The Spotify process to read from.
* @param address The reference address of the playback section
*/
public PlaybackAccessor(WinProcess process, long contextBaseAddress) {
public PlaybackAccessor(WinProcess process, long address) {
this.process = process;

// Create pointer registry to calculate the absolute addresses using the relative offsets
this.pointerRegistry = new PointerRegistry(0x0D3A2064, contextBaseAddress);
this.pointerRegistry.register("position", 0x0D3A2178);
this.pointerRegistry.register("length", 0x0D3A2188);
this.pointerRegistry.register("is_playing", 0x0D3A21AC);

// Parity pointers to make sure that we have the correct base address
this.pointerRegistry.register("parity_1", 0x0D3A2199);
this.pointerRegistry.register("parity_2", 0x0D3A21A4);
this.pointerRegistry.register("parity_3", 0x0D3A216E);
this.pointerRegistry = new PointerRegistry(0x0CFF4498, address);
this.pointerRegistry.register("position", 0x0CFF4810);
this.pointerRegistry.register("length", 0x0CFF4820);
this.pointerRegistry.register("is_playing", 0x0CFF4850); // 1=true, 0=false

this.update();
}
Expand Down Expand Up @@ -73,10 +68,7 @@ public boolean isValid() {
return this.position <= this.length
&& this.position >= 0
&& this.length <= MAX_TRACK_DURATION
&& this.length >= MIN_TRACK_DURATION
&& this.process.readBoolean(this.pointerRegistry.getAddress("parity_1")) != this.isPlaying
&& this.process.readBoolean(this.pointerRegistry.getAddress("parity_2")) != this.isPlaying
&& (this.process.readByte(this.pointerRegistry.getAddress("parity_3")) == 0) != this.isPlaying;
&& this.length >= MIN_TRACK_DURATION;
}

public int getLength() {
Expand Down
Loading

0 comments on commit 3553f00

Please sign in to comment.