Trying to use Spotipy [sic] in EventGhost

Do you have questions about writing plugins or scripts in Python? Meet the coders here.
Post Reply
Septik
Posts: 33
Joined: Sun Feb 15, 2015 1:29 pm

Trying to use Spotipy [sic] in EventGhost

Post by Septik » Tue Oct 10, 2017 9:17 pm

For a long time I've been wanting a keyboard shortcut for adding the currently playing song to a specific playlist in Spotify. Today I played around with the Spotify API and a Python script, and was actually able to achieve the desired result.

The problem is authorization. While testing, I simply hard-coded the auth key after manually generating one in the Web API section of the Spotify site (used for testing purposes).

Now that the main portion of my script was functional, I wanted to implement automatic authorization. If you look at the API docs, there are a few ways to do this, but the ones that allow you to modify playlists and such typically require user interaction (logging in and okaying an "app").

After messing around with requests (mainly post) and getting nowhere, I gave up and installed the Spotipy package which is suppose to simplipy the process. However, I soon discovered that it's authorization method opens a webpage where you okay the app as described above, and then requires you to paste a redirect URL containing an auth token (see Spotify API docs). In EventGhost, the example Spotipy scripts simply continue at this point and gives an error without asking me to paste that URL.

I'm sure there's a way to do this even without Spotipy, but I'm not skilled enough in Python to get past the authorization issue. If anyone can help me with that, I can guarantee that I'll return soon with some very useful scripts/macros in return!

Thanks for reading.

PS: I would include some links, but I'm on my phone now. May update the post from my PC later.

yokel22
Experienced User
Posts: 153
Joined: Thu Feb 05, 2015 5:56 pm
Location: U.S. - Kansas city

Re: Trying to use Spotipy [sic] in EventGhost

Post by yokel22 » Wed Oct 11, 2017 5:08 am

Have you gotten to the point where you've got your first token & refresh token? This could all be done within EG but would require module that isn't stock to EG. Unless you plan on writing a plugin for it, it's kinda overkill. If it's just for personal use, then you can get it working pretty easily once you've obtained your first tokens. I find it's helpful to get the environment setup in postman https://www.getpostman.com/. Once you have your tokens you can setup a script similar to this to refresh your token every hour or so.

Code: Select all

<?xml version="1.0" encoding="UTF-8" ?>
<EventGhost Version="0.5.0-rc4">
    <Action Name="Refresh Fitbit token" XML_Guid="{3F25B5F3-321A-4A5E-A526-E9440A1AF457}">
        EventGhost.PythonScript(u'import requests\nimport json\ncode = \'your key\'\nrefreshtoken = eg.plugins.Webserver.GetPersistentValue(u\'fitbit_refreshtoken\', False)\nurl = \'https://api.fitbit.com/oauth2/token\'\npayload = { "grant_type": \'refresh_token\', "refresh_token": refreshtoken}\nheaders = {"Authorization": "Basic " + code, \'content-type\': \'application/x-www-form-urlencoded\'}\nr = requests.post(url, payload, headers=headers)\njson_string = r.content\nparsed_json = json.loads(json_string)\nnewtoken = parsed_json[\'access_token\']\nnewrefreshtoken = parsed_json[\'refresh_token\']\nprint "Success: New token = " + newtoken\neg.plugins.Webserver.SetPersistentValue(u\'fitbit_refreshtoken\', newrefreshtoken, False, False, 1)\neg.plugins.Webserver.SetPersistentValue(u\'fitbit_token\', newtoken, False, False, 1)')
    </Action>
</EventGhost>

Septik
Posts: 33
Joined: Sun Feb 15, 2015 1:29 pm

Re: Trying to use Spotipy [sic] in EventGhost

Post by Septik » Wed Oct 11, 2017 8:19 am

yokel22 wrote:
Wed Oct 11, 2017 5:08 am
Have you gotten to the point where you've got your first token & refresh token? This could all be done within EG but would require module that isn't stock to EG. Unless you plan on writing a plugin for it, it's kinda overkill. If it's just for personal use, then you can get it working pretty easily once you've obtained your first tokens. I find it's helpful to get the environment setup in postman https://www.getpostman.com/. Once you have your tokens you can setup a script similar to this to refresh your token every hour or so.

[...]
I did aquire those tokens, yeah. But it was kind of messy and manual. (Does that matter?) Now that you mention it, I would really love to see a plugin for doing what I want and other things, but I'm not sure I'd be able to write it. So for now I'm happy with a solution that works for personal use, and when I end up with something useful I'll share it in script format for now.

What would happen if more than one hour passed before refreshing the token? For occasions when my PC is off, I mean. Would it still successfully refresh next time? Also, how do you suggest I set up a periodic script like that?

Thanks for your help.

yokel22
Experienced User
Posts: 153
Joined: Thu Feb 05, 2015 5:56 pm
Location: U.S. - Kansas city

Re: Trying to use Spotipy [sic] in EventGhost

Post by yokel22 » Wed Oct 11, 2017 8:34 am

No, it doesn't matter. Once you have your tokens you should be good to go. I can't say positively here as I don't use Spotify. But in past token systems I've used. The current token will expire after a few hrs. The renew token doesn't expire, so the script should be able to update the tokens no matter how long the period of time has past. You just want to make sure you're saving the tokens as persistent variables(dont use eg.globals). Easiest way to do that is with the webserver plugin like I posted in the example before. Or you could write/read the tokens from a text file. I'm getting ready to crash, I'll update in the morning with an example script to periodically update the tokens.

Okay here's an example of how to set it up to refresh the token every hour:

Code: Select all

<?xml version="1.0" encoding="UTF-8" ?>
<EventGhost Version="0.5.0-rc2">
    <Folder Name="SpotifyToken" XML_Guid="{725B4044-0CA5-4711-9513-C7E0D65E8B53}" Expanded="True">
        <Macro Name="Start Spotify Token Timer" XML_Guid="{55189A2D-A4B5-4D5A-A148-611D60BF43CB}">
            <Event Name="Main.OnInit" XML_Guid="{3856D2CF-75F2-41BA-B219-FC85BAEEC204}" />
            <Action Name="Timer: Start Spotify Token Timer" XML_Guid="{0B733894-7472-45B1-86EF-0D331820847E}">
                Timer.TimerAction(u'updateSpotToken', 0, 0, 3600.0, u'Update.SpotifyTokken', False, False, 1, u'00:00:00')
            </Action>
        </Macro>
        <Macro Name="Renew Token" XML_Guid="{7F00576F-59C0-4D21-A2DB-DE95C6EFCDDB}">
            <Event Name="Timer.Update.SpotifyToken" XML_Guid="{2CAB5AE3-EB74-4528-8781-5BD2B3ABBB35}" />
            <Action Name="Refresh  token" XML_Guid="{3F25B5F3-321A-4A5E-A526-E9440A1AF457}">
                EventGhost.PythonScript(u'import requests\nimport json\n\ncode = \'your key\'\nrefreshtoken = eg.plugins.Webserver.GetPersistentValue(u\'spotify_refreshtoken\', False)\nurl = \'https://api.spotify.com/oauth2/token\'\npayload = { "grant_type": \'refresh_token\', "refresh_token": refreshtoken}\nheaders = {"Authorization": "Basic " + code, \'content-type\': \'application/x-www-form-urlencoded\'}\n\nr = requests.post(url, payload, headers=headers)\njson_string = r.content\nparsed_json = json.loads(json_string)\n\nnewtoken = parsed_json[\'access_token\']\nnewrefreshtoken = parsed_json[\'refresh_token\']\nprint "Success: New token = " + newtoken\n\neg.plugins.Webserver.SetPersistentValue(u\'spotify_refreshtoken\', newrefreshtoken, False, False, 1)\neg.plugins.Webserver.SetPersistentValue(u\'spotify_token\', newtoken, False, False, 1)')
            </Action>
        </Macro>
    </Folder>
</EventGhost>
Last edited by yokel22 on Wed Oct 11, 2017 4:42 pm, edited 1 time in total.

Septik
Posts: 33
Joined: Sun Feb 15, 2015 1:29 pm

Re: Trying to use Spotipy [sic] in EventGhost

Post by Septik » Wed Oct 11, 2017 2:24 pm

Great! Looking forward to try it. :)

Septik
Posts: 33
Joined: Sun Feb 15, 2015 1:29 pm

Re: Trying to use Spotipy [sic] in EventGhost

Post by Septik » Thu Oct 12, 2017 5:13 pm

yokel22 wrote:
Wed Oct 11, 2017 8:34 am
No, it doesn't matter. Once you have your tokens you should be good to go. I can't say positively here as I don't use Spotify. But in past token systems I've used. The current token will expire after a few hrs. The renew token doesn't expire, so the script should be able to update the tokens no matter how long the period of time has past. You just want to make sure you're saving the tokens as persistent variables(dont use eg.globals). Easiest way to do that is with the webserver plugin like I posted in the example before. Or you could write/read the tokens from a text file. I'm getting ready to crash, I'll update in the morning with an example script to periodically update the tokens.

Okay here's an example of how to set it up to refresh the token every hour:

Code: Select all

<?xml version="1.0" encoding="UTF-8" ?>
<EventGhost Version="0.5.0-rc2">
    <Folder Name="SpotifyToken" XML_Guid="{725B4044-0CA5-4711-9513-C7E0D65E8B53}" Expanded="True">
        <Macro Name="Start Spotify Token Timer" XML_Guid="{55189A2D-A4B5-4D5A-A148-611D60BF43CB}">
            <Event Name="Main.OnInit" XML_Guid="{3856D2CF-75F2-41BA-B219-FC85BAEEC204}" />
            <Action Name="Timer: Start Spotify Token Timer" XML_Guid="{0B733894-7472-45B1-86EF-0D331820847E}">
                Timer.TimerAction(u'updateSpotToken', 0, 0, 3600.0, u'Update.SpotifyTokken', False, False, 1, u'00:00:00')
            </Action>
        </Macro>
        <Macro Name="Renew Token" XML_Guid="{7F00576F-59C0-4D21-A2DB-DE95C6EFCDDB}">
            <Event Name="Timer.Update.SpotifyToken" XML_Guid="{2CAB5AE3-EB74-4528-8781-5BD2B3ABBB35}" />
            <Action Name="Refresh  token" XML_Guid="{3F25B5F3-321A-4A5E-A526-E9440A1AF457}">
                EventGhost.PythonScript(u'import requests\nimport json\n\ncode = \'your key\'\nrefreshtoken = eg.plugins.Webserver.GetPersistentValue(u\'spotify_refreshtoken\', False)\nurl = \'https://api.spotify.com/oauth2/token\'\npayload = { "grant_type": \'refresh_token\', "refresh_token": refreshtoken}\nheaders = {"Authorization": "Basic " + code, \'content-type\': \'application/x-www-form-urlencoded\'}\n\nr = requests.post(url, payload, headers=headers)\njson_string = r.content\nparsed_json = json.loads(json_string)\n\nnewtoken = parsed_json[\'access_token\']\nnewrefreshtoken = parsed_json[\'refresh_token\']\nprint "Success: New token = " + newtoken\n\neg.plugins.Webserver.SetPersistentValue(u\'spotify_refreshtoken\', newrefreshtoken, False, False, 1)\neg.plugins.Webserver.SetPersistentValue(u\'spotify_token\', newtoken, False, False, 1)')
            </Action>
        </Macro>
    </Folder>
</EventGhost>
In the Python script, what am I supposed to paste in the 'your code' variable? I tried both a valid access token and a valid refresh token, and both gave the following response when I ran the script: {u'error': {u'status': 400, u'message': u'Only valid bearer authentication supported'}}


Edit: After trying out the official example (using Node.js) I was able to write the below code, which works for obtaining a new access token. I obtained a valid refresh token using the Node.js tool, but had to modify its html code to have it display the entire code.

Code: Select all

import json
import base64
import requests


refresh_token = "MyRefreshToken"
client_id = "MyClientID"
client_secret = "MyClientSecret"

url = 'https://accounts.spotify.com/api/token'
payload = {'grant_type': 'refresh_token', 'refresh_token': refresh_token}
headers = {'Authorization': 'Basic ' + base64.standard_b64encode(client_id + ':' + client_secret)}

r = requests.post(url, payload, headers=headers)
json_string = r.content
parsed_json = json.loads(json_string)

newtoken = parsed_json['access_token']
eg.plugins.Webserver.SetPersistentValue(u'spotify_token', str(newtoken), False, False)
Now, would it be an idea to programatically run this based on whether or not whatever request I'm making is successful? For now I pretty much just wanted that one shortcut for adding a track to a playlist, so I don't really need a fresh access token every hour. Besides, what happens when my PC is in sleep mode and comes back on? Would it immediately refresh? Would the PC wake up for refreshing? My idea is basically (pseudocode):

Code: Select all

#getting the info I want
r = requests.get(url, payload, headers=headers)

if r.statuscode != 200 {
<get new auth key>
}
else {
<do some other stuff>
}
Appreciate your help so far, and hope you have some feedback here as well!
Last edited by Septik on Thu Oct 12, 2017 5:59 pm, edited 1 time in total.

yokel22
Experienced User
Posts: 153
Joined: Thu Feb 05, 2015 5:56 pm
Location: U.S. - Kansas city

Re: Trying to use Spotipy [sic] in EventGhost

Post by yokel22 » Thu Oct 12, 2017 5:58 pm

The code i posted won't likely be exactly right, it was a script i use for fitbit(it's very similar). It will need to be modified slightly. This is an example script from spotify's api doc. page here: https://developer.spotify.com/web-api/a ... ion-guide/. I believe it's a base64encoded clientID:clientsecret that you use for the 'code' variable in that script. It should look something like this 'MjI3V0dHOkcxMZkwVTIxNzM3TTk1M2ZhNDA4M2MyNDnwNWQcMjc2'.

Code: Select all

curl -H "Authorization: Basic ZjM4Zj...Y0MzE=" -d grant_type=refresh_token -d refresh_token=NgAagA...NUm_SHo https://accounts.spotify.com/api/token
{
   "access_token": "NgA6ZcYI...ixn8bUQ",
   "token_type": "Bearer",
   "scope": "user-read-private user-read-email",
   "expires_in": 3600
}
The lower portion is the returned result. With the newtoken, scope, and expiration time(1hr). It doesn't look like spotify ever changes your refresh token, so i'm going to comment out the lines that update the refreshtoken. A modified version that hopefully works for you will look like this. You just need to change to code variable.

Code: Select all

import requests
import json

# Base 64 encoded string that contains the client ID
# and client secret key. similar to 'ZjM4Zj...Y0MzE'
code = 'base64 encoded "clientID:client secret"' 

refreshtoken = eg.plugins.Webserver.GetPersistentValue(u'spotify_refreshtoken', False)
url = 'https://accounts.spotify.com/api/token'
payload = { "grant_type": 'refresh_token', "refresh_token": refreshtoken}
headers = {"Authorization": "Basic " + code, 'content-type': 'application/x-www-form-urlencoded'}

r = requests.post(url, payload, headers=headers)
json_string = r.content
parsed_json = json.loads(json_string)

newtoken = parsed_json['access_token']
#newrefreshtoken = parsed_json['refresh_token']
print "Success: New token = " + newtoken

#eg.plugins.Webserver.SetPersistentValue(u'spotify_refreshtoken', newrefreshtoken, False, False, 1)
eg.plugins.Webserver.SetPersistentValue(u'spotify_token', newtoken, False, False, 1)

yokel22
Experienced User
Posts: 153
Joined: Thu Feb 05, 2015 5:56 pm
Location: U.S. - Kansas city

Re: Trying to use Spotipy [sic] in EventGhost

Post by yokel22 » Thu Oct 12, 2017 6:10 pm

Yep, refreshing the token just upon error should work fine for your needs. As long as your network connection is active when your pc wakes up, i don't foresee any problems. you'll want to format your logic statements like this though.

Code: Select all

if r.statuscode != 200:
    'do something'
else:
    'do something else'

yokel22
Experienced User
Posts: 153
Joined: Thu Feb 05, 2015 5:56 pm
Location: U.S. - Kansas city

Re: Trying to use Spotipy [sic] in EventGhost

Post by yokel22 » Thu Oct 12, 2017 6:23 pm

on second thought you may want to set it up as an eg.globals function, that way you can call it from any other script. You'll want to run this script either on the 'Main.OnInit' event or put it in your tree's autostart.

Code: Select all

import json
import base64
import requests

def refreshSpot():
    
    refresh_token = "MyRefreshToken"
    client_id = "MyClientID"
    client_secret = "MyClientSecret"

    url = 'https://accounts.spotify.com/api/token'
    payload = {'grant_type': 'refresh_token', 'refresh_token': refresh_token}
    headers = {'Authorization': 'Basic ' + base64.standard_b64encode(client_id + ':' + client_secret)}

    r = requests.post(url, payload, headers=headers)
    json_string = r.content
    parsed_json = json.loads(json_string)

    newtoken = parsed_json['access_token']
    eg.plugins.Webserver.SetPersistentValue(u'spotify_token', str(newtoken), False, False)

eg.globals.refreshSpotifyToken = refreshSpot
then from other scripts call it like this.

Code: Select all

apiCall = 'your request to api' 
if apiCall.status.code != 200:
   eg.globals.refreshSpotifyToken()
   retry 'your request to api'

Septik
Posts: 33
Joined: Sun Feb 15, 2015 1:29 pm

Re: Trying to use Spotipy [sic] in EventGhost

Post by Septik » Thu Oct 12, 2017 8:35 pm

Alright, so I finally ended up with something that works as intended. Just a reminder: I'm using a keyboard shortcut to trigger this script while listening to, say, a Spotify radio playlist. The script checks if the playing song is already in a specific playlist, and if not, adds the song to the playlist. Sharing the full script for others who might need it and for feedback. Would be cool if we were able to shorten it or make it more efficient somehow. Haven't gotten much experience with Python, so I'm sure there's room for improvement.

Thanks you very much for help and feedback, yokel22!

Hastebin

Text:

Code: Select all

import json
import base64
import requests
import sys

def getAccessToken(test):
    #USER VARIABLES
    refresh_token = "<your refresh token>"
    client_id = "<your client id>"
    client_secret = "<your client secret>"
    ##
    
    url = 'https://accounts.spotify.com/api/token'
    payload = {'grant_type': 'refresh_token', 'refresh_token': refresh_token}
    headers = {'Authorization': 'Basic ' + base64.standard_b64encode(client_id + ':' + client_secret)}

    r = requests.post(url, payload, headers=headers)
    json_string = r.content
    parsed_json = json.loads(json_string)

    newtoken = parsed_json['access_token']
    eg.plugins.Webserver.SetPersistentValue(u'spotify_token', str(newtoken), False, False)
    
    return

#Create OSD object
osd = eg.plugins.EventGhost.actions["ShowOSD"]()

#USER VARIABLES
username = "<your username>"
playlistID = "<ID of playlist you wish to use>"
##

accessToken = eg.plugins.Webserver.GetPersistentValue(u'spotify_token', False)

trackInfo = requests.get("https://api.spotify.com/v1/me/player/currently-playing", headers={"Authorization": "Bearer " + accessToken})

if trackInfo.status_code == 401:
    test = 5
    getAccessToken(test)
    accessToken = eg.plugins.Webserver.GetPersistentValue(u'spotify_token', False)
    trackInfo = requests.get("https://api.spotify.com/v1/me/player/currently-playing", headers={"Authorization": "Bearer " + accessToken})

if trackInfo.status_code == 204:
    print "No track info found!"
    osd("No track info found!", u'0;-16;0;0;0;700;0;0;0;0;3;2;1;34;Arial', (255, 255, 255), None, 3, (5, 37), 0, 3.0, True)
    sys.exit()

#Parse track info
i=trackInfo.json()
trackID=i['item']['id']
trackName=i['item']['name']
trackArtist=i['item']['album']['artists'][0]['name']
##

#Get playlist name
playlist = requests.get("https://api.spotify.com/v1/users/" + username + "/playlists/" + playlistID + "?fields=name", headers={"Authorization": "Bearer " + accessToken})
p = playlist.json()
pname = p['name']

#Get playlist contents
tracklist = requests.get("https://api.spotify.com/v1/users/" + username + "/playlists/" + playlistID + "/tracks?fields=items(track.id),total", headers={"Accept": "application/json", "Authorization": "Bearer " + accessToken + "\"" })
t = tracklist.json()
total = t['total']

osd("Checking for duplicates...", u'0;-16;0;0;0;700;0;0;0;0;3;2;1;34;Arial', (255, 255, 255), None, 3, (5, 37), 0, 3.0, True)
print "Checking for duplicates..."

#Check for duplicates
offset = 100
while (offset < total):
    tracklist = requests.get("https://api.spotify.com/v1/users/" + username + "/playlists/" + playlistID + "/tracks?fields=items(track.id),total&offset=" + str(offset), headers={"Accept": "application/json", "Authorization": "Bearer " + accessToken + "\"" })
    t = tracklist.json()
    for i, song in enumerate(t['items']):
        if song['track']['id'] == trackID:
            print "Duplicate found!"
            osd("Error: Duplicate found!", u'0;-16;0;0;0;700;0;0;0;0;3;2;1;34;Arial', (255, 255, 255), None, 3, (5, 37), 0, 5.0, True)
            sys.exit()
    offset +=100

print "No duplicates found."

#Add track to playlist
add = requests.post("https://api.spotify.com/v1/users/" + username + "/playlists/" + playlistID + "/tracks?uris=spotify%3Atrack%3A" + trackID, headers={"Accept": "application/json", "Authorization": "Bearer " + accessToken + "\"" })
##



#Exit with error if song couldn't be added to playlist
if add.status_code != 201:
    print ("POST ERROR: " + str(add.status_code) + " " + add.reason)
    osd("Error adding song to playlist! See log for more details.")
    sys.exit()
##

#Show OSD if everything went well
osd("\"" + trackArtist + " - " + trackName + "\" added to playlist \"" + pname + "\".", u'0;-16;0;0;0;700;0;0;0;0;3;2;1;34;Arial', (255, 255, 255), None, 3, (5, 37), 0, 5.0, True)
print "\"" + trackArtist + " - " + trackName + "\" added to playlist \"" + pname + "\"."

yokel22
Experienced User
Posts: 153
Joined: Thu Feb 05, 2015 5:56 pm
Location: U.S. - Kansas city

Re: Trying to use Spotipy [sic] in EventGhost

Post by yokel22 » Fri Oct 13, 2017 2:29 am

Man, it looks pretty concise to me. Well done & thanks for sharing. If you ever feel like setting this up into a plugin with the grant flow authorization. I'd be willing to work on it with you. As i have a bunch of fitbit scripts sitting around that just need to be formulated into a plugin. The last thing i need to do is setup the grant flow.

Septik
Posts: 33
Joined: Sun Feb 15, 2015 1:29 pm

Re: Trying to use Spotipy [sic] in EventGhost

Post by Septik » Fri Oct 13, 2017 1:15 pm

yokel22 wrote:
Fri Oct 13, 2017 2:29 am
If you ever feel like setting this up into a plugin with the grant flow authorization. I'd be willing to work on it with you.
I'd be happy to collaborate on that. Just know that I've never written a plugin for EventGhost and wouldn't quite know where to begin. Still, get in touch if/when you wanna get started on it! :)

yokel22
Experienced User
Posts: 153
Joined: Thu Feb 05, 2015 5:56 pm
Location: U.S. - Kansas city

Re: Trying to use Spotipy [sic] in EventGhost

Post by yokel22 » Fri Oct 13, 2017 3:19 pm

That's not a problem. If it seems like i know what im doing 100% of the time. Its mearly an illusion. You pretty much have several actions done now. I'll get the base plugin setup with some of your code. Since you already know what it does, you'll be able to understand the plugin structure pretty quickly. Im gonna be busy with some outdoor projects this next week. I should have some free time after that. Ill pm ya about it.

Post Reply