Usage and information: https://redd.it/orzend
#Requires AutoHotkey v2.0 | |
/* | |
If placing below any hotkeys that had a | |
previous thread change use the default: | |
*/ | |
#MaxThreadsPerHotkey 1 | |
/* | |
Change keys for something meaningful | |
This example is in alphabetical order | |
*/ | |
1::Spotify.Next() | |
2::Spotify.Previous() | |
3::Spotify.Repeat() | |
4::Spotify.SeekBackward() | |
5::Spotify.SeekForward() | |
6::Spotify.Shuffle() | |
7::Spotify.TogglePlay() | |
8::Spotify.VolumeDown() | |
9::Spotify.VolumeUp() | |
; Show Spotify | |
0::Spotify.Restore() | |
; Dependency | |
#Include %A_LineFile%\..\Spotify.ahk |
#Requires AutoHotkey v1.1 | |
/* | |
If placing below any hotkeys that had a | |
previous thread change use the default: | |
*/ | |
#MaxThreadsPerHotkey 1 | |
/* | |
Change keys for something meaningful | |
This example is in alphabetical order | |
*/ | |
1::Spotify.Next() | |
2::Spotify.Previous() | |
3::Spotify.Repeat() | |
4::Spotify.SeekBackward() | |
5::Spotify.SeekForward() | |
6::Spotify.Shuffle() | |
7::Spotify.TogglePlay() | |
8::Spotify.VolumeDown() | |
9::Spotify.VolumeUp() | |
; Show Spotify | |
0::Spotify.Restore() | |
; Dependency | |
#Include %A_LineFile%\..\Spotify1.ahk |
#Requires AutoHotkey v2.0 | |
; Version: 2023.06.18.1 | |
; Usages and information: https://redd.it/orzend | |
class Spotify extends Spotify.Controls { | |
static _hWnd := 0, _OnExit := "" | |
static Restore() { | |
this._Win() | |
WinShow() | |
WinActivate() | |
Spotify._hWnd := 0 | |
if (IsObject(Spotify._OnExit)) { | |
OnExit(Spotify._OnExit, 0) | |
Spotify._OnExit := "" | |
} | |
} | |
static _Win(Stash := false) { | |
static title := "-|Spotify ahk_exe i)Spotify\.exe" | |
DetectHiddenWindows(true) | |
SetTitleMatchMode("RegEx") | |
hWnd := WinExist(title) | |
if (hWnd = 0) { | |
MsgBox("Spotify is not running...", "Error", 0x40010) | |
Exit() | |
} | |
if (Spotify._hWnd = hWnd) { | |
return | |
} | |
Spotify._hWnd := hWnd | |
visible := DllCall("IsWindowVisible", "Ptr", hWnd, "Int") | |
RunWait("Spotify:") | |
WinWait(title) | |
if (Stash && !visible) { | |
WinWaitActive() | |
WinHide() | |
if (!IsObject(Spotify._OnExit)) { | |
Spotify._OnExit := ObjBindMethod(Spotify, "Restore") | |
OnExit(Spotify._OnExit, 1) | |
} | |
} | |
} | |
class Controls { | |
static _shortcuts := Map("Next", "^{Right}", "Previous", "^{Left}", "Repeat", "^r", "SeekBackward", "+{Left}", "SeekForward", "+{Right}", "Shuffle", "^s", "TogglePlay", "{Space}", "VolumeDown", "^{Down}", "VolumeUp", "^{Up}") | |
static __Call(Action, *) { | |
static WM_APPCOMMAND := 793 | |
if (!this._shortcuts.Has(Action)) { | |
throw Error("Invalid action." Action, -1) | |
} | |
shortcut := this._shortcuts[Action] | |
hActive := WinExist("A") | |
this._Win(true) | |
ControlFocus("Chrome Legacy Window") | |
ControlSend(shortcut, "Chrome Legacy Window") | |
if (Action ~= "i)^(Next|Previous|TogglePlay)$") { | |
hWnd := DllCall("FindWindow", "Str", "NativeHWNDHost", "Ptr", 0) | |
try PostMessage(WM_APPCOMMAND, 0x000C, 0xA0000, , hWnd) | |
} | |
WinActivate(hActive) | |
} | |
} | |
} |
#Requires AutoHotkey v1.1 | |
; Version: 2023.06.18.1 | |
; Usages and information: https://redd.it/orzend | |
class Spotify extends Spotify.Controls { | |
static _hWnd := 0, _OnExit := "" | |
Restore() { | |
this._Win() | |
WinShow | |
WinActivate | |
Spotify._hWnd := 0 | |
if (IsObject(Spotify._OnExit)) { | |
OnExit(Spotify._OnExit, 0) | |
Spotify._OnExit := "" | |
} | |
} | |
_Win(Stash := false) { | |
static title := "-|Spotify ahk_exe i)Spotify\.exe" | |
DetectHiddenWindows On | |
SetTitleMatchMode RegEx | |
hWnd := WinExist(title) | |
if (hWnd = 0) { | |
MsgBox 0x40010, Error, Spotify is not running... | |
Exit | |
} | |
if (Spotify._hWnd = hWnd) { | |
return | |
} | |
Spotify._hWnd := hWnd | |
visible := DllCall("IsWindowVisible", "Ptr", hWnd, "Int") | |
RunWait Spotify: | |
WinWait % title | |
if (Stash && !visible) { | |
WinWaitActive | |
WinHide | |
if (!IsObject(Spotify._OnExit)) { | |
Spotify._OnExit := ObjBindMethod(Spotify, "Restore") | |
OnExit(Spotify._OnExit, 1) | |
} | |
} | |
} | |
class Controls { | |
static _shortcuts := { Next: "^{Right}", Previous: "^{Left}", Repeat: "^r", SeekBackward: "+{Left}", SeekForward: "+{Right}", Shuffle: "^s", TogglePlay: "{Space}", VolumeDown: "^{Down}", VolumeUp: "^{Up}" } | |
__Call(Action, _*) { | |
static WM_APPCOMMAND := 793 | |
if (!this._shortcuts.HasKey(Action)) { | |
throw Exception("Invalid action." Action, -1) | |
} | |
shortcut := this._shortcuts[Action] | |
hActive := WinExist("A") | |
this._Win(true) | |
ControlFocus Chrome Legacy Window | |
ControlSend Chrome Legacy Window, % shortcut | |
if (Action ~= "i)^(Next|Previous|TogglePlay)$") { | |
hWnd := DllCall("FindWindow", "Str", "NativeHWNDHost", "Ptr", 0) | |
try PostMessage WM_APPCOMMAND, 0x000C, 0xA0000, , % "ahk_id" hWnd | |
} | |
WinActivate % "ahk_id" hActive | |
} | |
} | |
} |
--- Spotify.ahk | |
+++ Spotify1.ahk | |
@@ -1 +1 @@ | |
-#Requires AutoHotkey v2.0 | |
+#Requires AutoHotkey v1.1 | |
@@ -10 +10 @@ | |
- static Restore() { | |
+ Restore() { | |
@@ -12,2 +12,2 @@ | |
- WinShow() | |
- WinActivate() | |
+ WinShow | |
+ WinActivate | |
@@ -21 +21 @@ | |
- static _Win(Stash := false) { | |
+ _Win(Stash := false) { | |
@@ -23,2 +23,2 @@ | |
- DetectHiddenWindows(true) | |
- SetTitleMatchMode("RegEx") | |
+ DetectHiddenWindows On | |
+ SetTitleMatchMode RegEx | |
@@ -27,2 +27,2 @@ | |
- MsgBox("Spotify is not running...", "Error", 0x40010) | |
- Exit() | |
+ MsgBox 0x40010, Error, Spotify is not running... | |
+ Exit | |
@@ -35,2 +35,2 @@ | |
- RunWait("Spotify:") | |
- WinWait(title) | |
+ RunWait Spotify: | |
+ WinWait % title | |
@@ -38,2 +38,2 @@ | |
- WinWaitActive() | |
- WinHide() | |
+ WinWaitActive | |
+ WinHide | |
@@ -49 +49 @@ | |
- static _shortcuts := Map("Next", "^{Right}", "Previous", "^{Left}", "Repeat", "^r", "SeekBackward", "+{Left}", "SeekForward", "+{Right}", "Shuffle", "^s", "TogglePlay", "{Space}", "VolumeDown", "^{Down}", "VolumeUp", "^{Up}") | |
+ static _shortcuts := { Next: "^{Right}", Previous: "^{Left}", Repeat: "^r", SeekBackward: "+{Left}", SeekForward: "+{Right}", Shuffle: "^s", TogglePlay: "{Space}", VolumeDown: "^{Down}", VolumeUp: "^{Up}" } | |
@@ -51 +51 @@ | |
- static __Call(Action, *) { | |
+ __Call(Action, _*) { | |
@@ -53,2 +53,2 @@ | |
- if (!this._shortcuts.Has(Action)) { | |
- throw Error("Invalid action." Action, -1) | |
+ if (!this._shortcuts.HasKey(Action)) { | |
+ throw Exception("Invalid action." Action, -1) | |
@@ -59,2 +59,2 @@ | |
- ControlFocus("Chrome Legacy Window") | |
- ControlSend(shortcut, "Chrome Legacy Window") | |
+ ControlFocus Chrome Legacy Window | |
+ ControlSend Chrome Legacy Window, % shortcut | |
@@ -63 +63 @@ | |
- try PostMessage(WM_APPCOMMAND, 0x000C, 0xA0000, , hWnd) | |
+ try PostMessage WM_APPCOMMAND, 0x000C, 0xA0000, , % "ahk_id" hWnd | |
@@ -65 +65 @@ | |
- WinActivate(hActive) | |
+ WinActivate % "ahk_id" hActive |
Forgot to mention, before controlsend we must use controlfocus
I replied to the first comment, and I was completely wrong, then while trying differently I knew I was overthinking it way too much. Thanks a lot for the input, I updated the script with a more simplified approach.
Great!
When win activated, hotkeys stoped working. You can use just send instead of controlsend.
Is not that, is about the janky detection of the parent handler for the control, I've got mixed results with GetWindow
and SetParent
because the Chrome_RenderWidgetHostHWND
control swaps parents depending on the window state (WTF!?). If the app is restored/maximized the proper parent is retrieved but as soon as is minimized or sent to tray the parent changes.
While the one I had first looping trough all the Chrome_WidgetWin_0
instances was working it felt wrong, so I used GetWindow
but as you pointed out is not reliable. So it comes down to check when playing always has a dash, if stopped it has the word Spotify
. Seems a more consistent approach.
And I say consistent because I haven't found what might be considered the proper way as all have their drawbacks:
- This method flashes the window, but still is one of the easiest and more complete.
- API is just for Premium and scares people getting it to work, plus is cumbersome to get seeking done.
- Talking to the application via web sockets is easy but unfortunately dispatching keyboard events doesn't trigger the actions, plus is needed to start the app with debugging parameters.
- With Accessibility there's no way of doing seeking or changing the volume and of course each update changes the layout potentially breaking the paths (unless you traverse the objects and cache the matches). Not to mention people that uses skins or like me that I use the old UI because the new one sucks big time.
All in all the four methods I've tried are incomplete and is so easy for the developers to have it working properly that frustrate me enormously.
Thanks for your invaluable time testing, as I see it I guess I need to write some JavaScript to seeks and change volume and inject it to have a complete version working with web sockets but that's also what I don't use so is just a nice to have for the moment being.
Maybe Spotify devs messed some code. But why you don't want to use send instead contrlsend? I think it is simplest and most realible method with activated window. Also i noticed that you don't use winhide when window is minimized or closed with win buttons in up right corner, that is why hotkeys don't work in minimized or trayed states for me at least, are they work for you in minimized or trayed state? I use in my script function MouseIsOverButtons for this purpose, maybe you find out better way to implement this solution.
"This method flashes the window, but still is one of the easiest and more complete." Have seen flash of the window only first run script, and time to time(very rarely) in first rewind after script reload.
"Talking to the application via web sockets is easy but unfortunately dispatching keyboard events doesn't trigger the actions, plus is needed to start the app with debugging parameters."
I don't know about this method((
Do you have some examples how to use it? Maybe we can retrieve track picture from this method?
I use Accessibility method to get song name and artists, also time of played and total time of song to show it (some kind of OSD) when rewind and i want to get picture of song to get full OSD, but for now the only way i know is to screenshot internal OSD of windows10 when pause\play {Media_Play_Pause} song. Maybe you know better method how to do this?
"(unless you traverse the objects and cache the matches)" can you explain a bit what you mean?
But why you don't want to use send instead contrlsend? I think it is simplest and most realible method with activated window
Is not needed as it works exactly the same behind curtains when the app is in the foreground.
Also i noticed that you don't use winhide when window is minimized
WinHide
should only be used when the app is in the tray (ie, the user don't want it in the taskbar). And the way it is works no matter where the app is: maximized/restored, minimized or in the tray.
Have seen flash of the window only first run script
It should only flash the first key press and after restored or script reloaded.
WebSockets
Spotify is an Electron app which meas is simply a subset of the Chromium engine, thus you can talk to the app via Chrome DevTools Protocol, and while I guess you can grab the album art from there it will be easier with the API as I demonstrated here. Regarding that you can't simply trigger the hotkeys:
https://developer.mozilla.org/en-US/docs/Web/API/KeyboardEvent
Note: Manually firing an event does not generate the default action associated with that event. For example, manually firing a key event does not cause that letter to appear in a focused text input. In the case of UI events, this is important for security reasons, as it prevents scripts from simulating user actions that interact with the browser itself.
OSD
That code to show the album art and current track information lacks proper validation and overall is not polished, however the code to talk to the API and keep the oAuth2 tokens is pretty solid. Using that as a base you to build any kind of OSD as it has all the proper information, I have a half-baked prototype for this, unfortunately I haven't had the time to finish it. You can play around with the API consumption and work on yours.
Accessibility
You can grab any information on the screen with accessibility, but is always prone to change on every update because the pats are just the parent/child relation of each element and if the devs change something in the UI layout everything can change. For example this are the paths for the latest version (1.1.66.578.gc54d0f69-a
):
base := "4.1.2.1.1.1.2.1.1.3.1"
paths := { "track":"1.2.1.1.1", "artist":"1.2.2.1.1" }
But I use 1.1.57.443.ga029a6c4
(as the latest UI is dreadful) so those won't work. With every single update (and Spotify does like one per week) there's a chance a path can change, so you need to traverse the root window looking for the right objects. Basically you need to recursively ask for the children until you find the one you're looking for (via roles/names/values).
In the getUrl() function I do this by looking for the element that has in the name the word Address
. For example for the track name you should look for an object whose value has a link in the form spotify:track:6rCGU3kEHIVa3M3kJ5MrHT
for the old version and https://xpui.app.spotify.com/album/5GrKy9mYCzEKfbhGmpxQKE
in the new one. Of course to look for all the elements you need a proper filtering function and not just one specialized... proper validation and caching is needed as doing this each time will be slow.
I did some slight modifications to the Acc core functions to speedup DLL access but statically caching the object references will be the best approach for such task.
@anonymous1184
Thank you for detailed response i appreciate this very much!
I tried to run OSD from your example.
Make options.ini with my client_id and client_secret
Make subfolder Lib with: SpotifyAPI.ahk, Socket.ahk, WinHttpRequest.ahk
I runed example, fired hotkey and see GUI with only "- - 00. - - 00."
What can i do wrong? Maybe my version of AHK is incompatible? I use v1.1.31.01. Or version of Spotify? I use 1.1.65.643.g2d707698-a
I can't see any error.
Also, when i remove file options.ini or folder Lib, i can't see any error either - realy strange.
"I did some slight modifications to the Acc core functions to speedup DLL access"
Where i can download it?
Can came thru authenticitication with this scripts https://github.com/CloakerSmoker/Spotify.ahk
But the only data i can get is thru MsgBox, % Spoofy.CurrentUser.SubscriptionLevel and it is retrieve word "open"
Default hot keys in example return error "PREMIUM_REQUIRED: Player command failed: Premium required"
Your AutoHotkey version is fine, any version 1.1 from the last couple of years should do the trick. As for Spotify, when using the API the client has nothing to do (you might as well don't have Spotify in the computer and control your phone App from AHK).
I can't see any error.
when i remove file options.ini or folder Lib, i can't see any error either
I tested that thoughtfully and some of the helpers in the Subreddit did too, it worked fine for them also.
Use the #Warn
directive (with All
argument) to help you spot any issue, still the best way to debug anything is with a proper tool; I use VSCode, here's how it looks debugging an object and here's a guide I wrote, if you don't like it other editors also have debugging capabilities.
You error (or lack thereof) seems a missing SpotifyStatus
class (is in the same gist as SpotifyAPI
). Make sure you have the following structure:
options.ini ; https://redd.it/on9g8t - Configuration
spotify.ahk ; https://redd.it/on9gb5 - Example OSD code
Lib\SpotifyAPI.ahk ; https://git.io/JWFfe - Automatic token handling
Lib\SpotifyStatus.ahk ; - Actual API currently-playing method
; Dependencies
Lib\Socket.ahk ; https://git.io/Jc9Sn - For TCP connections
Lib\WinHttpRequest.ahk ; https://git.io/JnMcX - Handle web requests
If you find that to be too much, you can put together everything, I do it like this as is the proper way of doing it but that's up to the end user.
You'll end up with something like this. The JSON coming from that call looks like this and the object should be something like this. And like I've said, there's no validations on that code and is just a dump on the currently playing information.
The small mods I made to the Acc core are in the getUrl()
gist, basically all you have to do is pre-load the library and get the entry points for the functions in the DLL beforehand. You can do the same with the rest of the functions. But again, cache the objects once you find them so you only look for them once.
Never knew another project of Spotify handling its API exists. I just skimmed through the code, seems like they used their own app for everyone to use; I went with a personal approach on this (for privacy to avoid others to track me and my usage) and the way I did it was more generic so anyone can write what actually needs instead of disclosing all my information and requiring all the scopes.
However different, both projects have the same ideas behind them... I guess is just that my brain is hardwired to code as if I were in a Corporate environment, damn force of habit :P
Good luck!
Yes, i don't had Lib\SpotifyStatus.ahk
also i add to example global spotify := SpotifyStatus("options.ini")
spotify.auth()
and have error
C:\staff\Spotify\web\Lib\SpotifyStatus.ahk (10) : ==> Unknown class. Specifically: SpotifyAPI
but i have Lib\SpotifyAPI.ahk
i don't know why it can't find it.
I tried move all libs to example script, then i have parse error i think in this
_toJSON(str)
{
return this._parser.Call(str)
}
Maybe you can archive your folder with right files and structure? And i am just fill client_id and client_secret?
My example OSD is like this now, correct?
without
global spotify := SpotifyStatus("options.ini")
spotify.auth()
it just do nothing, like i said before(
global spotify := SpotifyStatus("options.ini")
spotify.auth()
global nowPlaying := ""
, spotify := new SpotifyStatus("options.ini")
F2::nowPlaying()
nowPlaying()
{
res := spotify.currentlyPlaying()
res := spotify._toJSON(res) ; or JSON.Load(res)
info := { ""
. "album" : res.item.album.name
, "artist" : res.item.artists.0.name
, "cover" : res.item.album.images.1.url
, "track_number": res.item.track_number
, "track" : res.item.name }
UrlDownloadToFile % info.cover, % A_Temp "\img"
Gui 1:New, +AlwaysOnTop -Caption
Gui Add, Picture, w300 h-1 x0 y0, % A_Temp "\img"
Gui Add, Text, -Wrap w300 x5, % nowPlaying := info.artist " - " info.album " - " Format("{:02}", info.track_number) ". " info.track
Gui Show, w300 h325
SetTimer marquee, 300
}
marquee()
{
static position := 0
len := StrLen(nowPlaying)
position += position = len ? -len : 1
GuiControl Text, Static2, % SubStr(nowPlaying Format("{: 10}", ""), position) nowPlaying
}
GuiClose:
GuiEscape:
SetTimer marquee, Delete
Gui Destroy
return
Thank you, for now i have parse error.
Error in #include file "C:\staff\Spotify\test\Lib\SpotifyAPI.ahk":
0x80020101 -
Specifically: parse
Line#
133: RegExMatch(request, "\bcode=(?<Code>[^&]+)", match)
134: this._code := matchCode
135: this._access()
136: Send,^{F4}
137: }
138: }
141: {
---> 142: Return,this._parser.Call(str)
143: }
146: {
147: this._http := this._doc := this._parser := ""
148: }
005: {
007: Return,isObject(instance) ? instance : instance := new %A_ThisFunc%(params*)
010: }
Continue running the script?
I compared files, in Lib\SpotifyStatus.ahk i don't have #Include
and in example in
global nowPlaying := ""
, spotify := new SpotifyStatus("options.ini")
i have "new". Without "new" i have parse error, with "new" have not any error, but "- - 00. - - 00." in GUI
What can cause parse problem, how to fix it?
Can you keep your replies in a single comment?
The idea of me sending a zip file is for you to use it regardless of what you have, it works, I tested it. Literally you only need to add your client ID/Secret. If for some reason it doesn't work try debugging it (use the VSCode guide I provided, or at very least print what's in the string). Whatever it is, is on your side.
Of course, sorry.
I don't know why, but it started working)) i tried other version of ahk, and it is worked, then i tried my default version of ahk and it is worked too))
Thank you for your patience!)
I noticed that the script displays only the first artist, how can I display all the artists?
I tried like this
info := { ""
. "album" : res.item.album.name
, "artists" : (res.item.artists.0.name ", " res.item.album.artists.1.name ", " res.item.album.artists.2.name ", " res.item.album.artists.3.name)
But when there is less then 4 artists, it is show an error at missed artists((
Glad you got it working @OmTatSat!
Regarding the artists, that's something you have to find out for yourself. I've been listening exclusively to metal for the last 30 years and having multiple artists is something not seen so I never looked into that.
For the sake of simplicity I used a JSON parser from a COM object, is very restrictive as it doesn't let you iterate (I explained that in the original posts). You might want to use cocobelgica's (or any other) that loads the JSON string into a native AHK object. The screenshot I previously shared of VSCode is precisely that.
However if you stick with the COM object use try
statements:
artist0 := ""
try artist0 := res.item.artist.0.name
artist1 := ""
try artist1 := res.item.artist.1.name
artist2 := ""
try artist2 := res.item.artist.2.name
artist3 := ""
try artist3 := res.item.artist.3.name
MsgBox % "Artist 0: " artist0
. "`n Artist 1: " artist1
. "`n Artist 2: " artist2
. "`n Artist 3: " artist3
I know it looks awful but you can't do it with a loop
. If you have issues I recommend you to use r/AutoHotkey as there's lots of people that can help.
@anonymous1184
Great solution! Thank you, it is very useful for me
i feel like i'm missing something but surprisingly this is pretty much the only script i've found that'll handle spicefied version. but i've noticed when i first run a hotkey it focuses the window(well activates it in the background) but doesn't do anything until i then minimize and restore spotify.
@NeonLightning: given that I no longer use v1.1, I added the v2.0 version and updated a little the code, adding an extra step to make sure the focused window was not lost (no core functionality was changed).
Check the README.md for version information and the Reddit post for overall details.
ah shame i use it with vmr.ahk to control voicemeeter and that's a 1.0 thanks anyways
@NeonLightning like I've said, check the README.md it contains both versions.
yup finally figured it out. my apologies
workin great btw.
the other one i used that used api calls stopped working for me but your um... unique method. is working still :)
@NeonLightning Thanks for letting me know, I'll check out the API calls and revisit the code as there are improved methods I'd like to implement for that one too.
You can just winhide window of spotify and use controlsend
Like here http://forum.script-coding.com/viewtopic.php?id=16534