-
Notifications
You must be signed in to change notification settings - Fork 5
/
SpotifyProcess.java
224 lines (193 loc) · 8.15 KB
/
SpotifyProcess.java
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
package de.labystudio.spotifyapi.platform.windows.api.spotify;
import de.labystudio.spotifyapi.platform.windows.api.WinProcess;
import de.labystudio.spotifyapi.platform.windows.api.jna.Psapi;
import de.labystudio.spotifyapi.platform.windows.api.playback.MemoryPlaybackAccessor;
import de.labystudio.spotifyapi.platform.windows.api.playback.PlaybackAccessor;
import de.labystudio.spotifyapi.platform.windows.api.playback.PseudoPlaybackAccessor;
/**
* This class represents the Spotify Windows application.
*
* @author LabyStudio
*/
public class SpotifyProcess extends WinProcess {
private static final boolean DEBUG = System.getProperty("SPOTIFY_API_DEBUG") != null;
// Spotify track id
private static final String PREFIX_SPOTIFY_TRACK = "spotify:track:";
private static final long[] OFFSETS_TRACK_ID = {
0x154A60, // 64-Bit (1.2.26.1187.g36b715a1)
0x14FA30, // 64-Bit (1.2.21.1104.g42cf0a50)
0x106198, // 32-Bit (1.2.21.1104.g42cf0a50)
0x14C9F0, // 64-Bit (Old)
0x102178, // 32-Bit (Old)
0x1499F0, // 64-Bit (Old)
0xFEFE8 // 32-Bit (Old)
};
private final long addressTrackId;
private final PlaybackAccessor playbackAccessor;
private SpotifyTitle previousTitle = SpotifyTitle.UNKNOWN;
/**
* Creates a new instance of the {@link SpotifyProcess} class.
* It will immediately try to connect to the Spotify application.
*
* @throws IllegalStateException if the Spotify process could not be found.
*/
public SpotifyProcess() {
super("Spotify.exe");
if (DEBUG) {
System.out.println("Spotify process loaded! Searching for addresses...");
}
long timeScanStart = System.currentTimeMillis();
this.addressTrackId = this.findTrackIdAddress();
this.playbackAccessor = this.findPlaybackAccessor();
if (DEBUG) {
System.out.println("Scanning took " + (System.currentTimeMillis() - timeScanStart) + "ms");
}
}
private long findTrackIdAddress() {
Psapi.ModuleInfo chromeElfModule = this.getModuleInfo("chrome_elf.dll");
if (chromeElfModule == null) {
throw new IllegalStateException("Could not find chrome_elf.dll module");
}
// Find address of track id (Located in the chrome_elf.dll module)
long chromeElfAddress = chromeElfModule.getBaseOfDll();
// Check all offsets for valid track id
long addressTrackId = -1;
long minTrackIdOffset = Long.MAX_VALUE;
long maxTrackIdOffset = Long.MIN_VALUE;
for (long trackIdOffset : OFFSETS_TRACK_ID) {
// Get min and max of hardcoded offset
minTrackIdOffset = Math.min(minTrackIdOffset, trackIdOffset);
maxTrackIdOffset = Math.max(maxTrackIdOffset, trackIdOffset);
// Check if the hardcoded offset is valid
long targetAddressTrackId = chromeElfAddress + trackIdOffset;
if (this.isTrackIdValid(this.readTrackId(targetAddressTrackId))) {
// If the offset works, exit the loop
addressTrackId = targetAddressTrackId;
break;
}
}
// If the hardcoded offsets are not valid, try to find it dynamically
if (addressTrackId == -1) {
if (DEBUG) {
System.out.println("Could not find track id with hardcoded offsets. Trying to find it dynamically...");
}
long threshold = (maxTrackIdOffset - minTrackIdOffset) * 3;
long scanAddressFrom = chromeElfAddress + minTrackIdOffset - threshold;
long scanAddressTo = chromeElfAddress + maxTrackIdOffset + threshold;
addressTrackId = this.findAddressOfText(scanAddressFrom, scanAddressTo, PREFIX_SPOTIFY_TRACK, (address, index) -> {
return this.isTrackIdValid(this.readTrackId(address));
});
}
if (addressTrackId == -1) {
throw new IllegalStateException("Could not find track id in memory");
}
if (DEBUG) {
System.out.printf(
"Found track id address: %s (+%s) [%s%s]%n",
Long.toHexString(addressTrackId),
Long.toHexString(addressTrackId - chromeElfAddress),
PREFIX_SPOTIFY_TRACK,
this.readTrackId(addressTrackId)
);
}
return addressTrackId;
}
private PlaybackAccessor findPlaybackAccessor() {
// Find addresses of playback states when playing a playlist
long addressPlayBack = this.findAddressOfText(0, 0x0FFFFFFF, "playlist", (address, index) -> {
return this.hasText(address + 408, "context", "autoplay")
&& this.hasText(address + 128, "your_library", "home")
&& new MemoryPlaybackAccessor(this, address).isValid();
});
if (addressPlayBack == -1) {
// Find addresses of playback states when playing an album
addressPlayBack = this.findAddressOfText(0, 0x0FFFFFFF, "album", (address, index) -> {
return this.hasText(address + 408, "context", "autoplay")
&& this.hasText(address + 128, "your_library", "home")
&& new MemoryPlaybackAccessor(this, address).isValid();
});
}
if (addressPlayBack == -1) {
if (DEBUG) {
System.out.println("Could not find playback address in memory");
}
return new PseudoPlaybackAccessor(this);
}
// Create the playback accessor with the found address
MemoryPlaybackAccessor playbackAccessor = new MemoryPlaybackAccessor(this, addressPlayBack);
if (!playbackAccessor.isValid()) {
if (DEBUG) {
System.out.println("Found playback address is not valid");
}
return new PseudoPlaybackAccessor(this);
}
if (DEBUG) {
System.out.println("Found playback address at: " + Long.toHexString(addressPlayBack));
}
return playbackAccessor;
}
/**
* Read the track id from the memory.
*
* @param address The address where the prefix "spotify:track:" starts
* @return the track id without the prefix "spotify:track:"
*/
private String readTrackId(long address) {
return this.readString(address + 14, 22);
}
/**
* Read the track id from the memory.
*
* @return the track id without the prefix "spotify:track:"
*/
public String getTrackId() {
return this.readTrackId(this.addressTrackId);
}
/**
* Read the playback state from the title bar.
* <p>
* If the title bar contains the delimiter " - ", the song is playing.
*
* @return true if the song is playing, false if the song is paused
*/
public boolean isPlayingUsingTitle() {
return this.getWindowTitle().contains(SpotifyTitle.DELIMITER);
}
/**
* Read the currently playing track name and artist from the title bar.
* If no song is playing it will return a cached value.
*
* @return the currently playing track name and artist
*/
public SpotifyTitle getTitle() {
SpotifyTitle title = SpotifyTitle.of(this.getWindowTitle());
if (title == null) {
return this.previousTitle;
}
return (this.previousTitle = title);
}
public PlaybackAccessor getPlaybackAccessor() {
return this.playbackAccessor;
}
public long getAddressTrackId() {
return this.addressTrackId;
}
/**
* Checks if the given track ID is valid.
* A track ID is valid if there are no characters with a value of zero.
*
* @param trackId The track ID to check.
* @return True if the track ID is valid, false otherwise.
*/
public boolean isTrackIdValid(String trackId) {
for (char c : trackId.toCharArray()) {
boolean isValidCharacter = c >= 'a' && c <= 'z'
|| c >= 'A' && c <= 'Z'
|| c >= '0' && c <= '9';
if (!isValidCharacter) {
return false;
}
}
return true;
}
}