3

I'm using a python wrapper for the Spotify API.

https://spotipy.readthedocs.io/en/latest/#installation

As part of the authorization process, the Spotify API has the user login to Spotify in their default web browser, then sends them to a predefined (when you register the app with Spotify) REDIRECT_URI. This REDIRECT_URI is currently set to localhost:6969. Once you login, it spawns an input() (Input of Death) for you to copy paste the URI that you were redirected to into the command prompt.

I really don't want to sell random people on using a command prompt.

The goal is to open a flask server on localhost:6969 (sue me) and then double tap its /authorize page (send to oauth2 login then capture code)

http://flask.pocoo.org/docs/1.0/

So -

I keep my Flask rig and spotify catcher in jflask.py:

from flask import Flask, request, redirect, session
from requests_oauth2 import OAuth2
from spotipy import Spotify
import spotipy.util as util
import requests
import datetime

app = Flask(__name__)

spotify_auth = OAuth2(
  client_id='e33c6fa0d6a249ccafa232a9cf62a616',
  client_secret='a43b0b6a4de14b97b4e468459e0f7824',
  redirect_uri="http://localhost:6969/authorize",
  site="https://accounts.spotify.com",
  authorization_url="/authorize",
  token_url="/api/token",
  scope_sep=" "
)

# and then use this url as a link within a public login view
print(spotify_auth.authorize_url(
  scope=["user-read-recently-played", "user-top-read"],
  response_type="code"
  )
)
# and have an authorize route that can direct you to Spotify or handle you 
#being redirected back here from Spotify
@app .route('/authorize')
def authorize():

  print('triggered')

  error = request.args.get("error")
  if error:
    abort(404)

  code = request.args.get("code")
  if code:
    data = spotify_auth.get_token(
      code=code,
      grant_type="authorization_code",
    )
    access_token = data["access_token"]
    refresh_token = data["refresh_token"]
    scopes = data["scope"].split()
    self.sp = spotipy.Spotify(auth=access_token)
    print(access_token)
    return self.sp
  else:
    print('code aint defined hoe')
    return redirect(spotify_auth.authorize_url(
      scope=["user-read-recently-played", "user-top-read"],
      response_type="code",
      #state=session['state']
    ))

if __name__ == '__main__':
  app.run(host='localhost', port=6969)

I spawn it with master.py:

from subprocess import Popen, PIPE
import webbrowser

jflask=Popen(['python', 'jflask.py'])

webbrowser.open('http://localhost:6969/authorize')

I've already tried using Popen with stdout=PIPE. After further research, I'm pretty sure that was also inhibiting my success. The specification is meant to be used with subprocess.communicate()

https://docs.python.org/3/library/subprocess.html#subprocess.PIP

Thank you everyone for your help.

Gaun
  • 43
  • 6
  • `input` certainly reads from `sys.stdin`—I’m not sure what to make of the rest of this. – Davis Herring Mar 02 '19 at 01:51
  • @Davis Herring Thank you. That helped. My problem is still not fixed, but I understand what's going on a little more. – Gaun Mar 03 '19 at 14:23
  • @DavisHerring : Yes, it does, however, functions that are made for, let say, editing purposes do some other things too because they expect a tty to be on the other end of sys.stdin, not a regular file or a PIPE. In general, this should have worked with stdin=PIPE and .communicate() method. Using sys.stdin.readline() instead of input() should work. – Dalen Mar 03 '19 at 14:57
  • @Dalen: Python doesn’t, for instance, use `readline` if `stdin` is not a terminal. So I still wouldn’t expect different behavior. – Davis Herring Mar 03 '19 at 20:08

2 Answers2

2

Spotipy's prompt_for_user_token method is provided as a quick and dirty method to allow someone using the module locally to get up and running and isn't intended to be code that should be built upon directly if you want arbitrary users to be able to authenticate themselves through a website. Since you have a Flask app you should probably have it and your Spotipy code interact directly through importing rather than using pipes and standard i/o.

Some things to look into are:

  • Flask-Login if you want to manage Users logging in to a website
  • The alternative methods of passing auth to spotipy.Spotify other than just access token (client credentials manager or requests session). These other methods allow the Spotipy Client to refresh an access token if it expires mid-use, handle user revokes, etc.
  • requests-oauth2 (note that this is only one of many OAuth2 Python libs and may not be the best one) which makes all the authorization stuff a bit more convenient and allows you to do something like:
# import Flask things, requests-oauth2, 
from requests_oauth2 import OAuth2
from spotipy import Spotify
from flask import redirect

spotify_auth = OAuth2(
    client_id=os.environ["SPOTIFY_CLIENT_ID"],
    client_secret=os.environ["SPOTIFY_CLIENT_SECRET"],
    redirect_uri=f"http://{os.environ['HOST_NAME']}/authorize",
    site="https://accounts.spotify.com",
    authorization_url="/authorize",
    token_url="/api/token",
    scope_sep=" "
)

# and then use this url as a link within a public login view
spotify_auth.authorize_url(
    scope=["user-read-recently-played", "user-top-read"],
    response_type="code"
)

# and have an authorize route that can direct you to Spotify or handle you being redirected back here from Spotify
@app.route('/authorize')
def authorize():

    error = request.args.get("error")
    if error:
        abort(404)

    code = request.args.get("code")
    if code:
        data = spotify_auth.get_token(
            code=code,
            grant_type="authorization_code",
        )
        access_token = data["access_token"]
        refresh_token = data["refresh_token"]
        scopes = data["scope"].split()
        spotify_client = spotipy.Spotify(auth=access_token)
    else:
        return redirect(spotify_auth.authorize_url(
            scope=["user-read-recently-played", "user-top-read"],
            response_type="code",
            state=session['state']
        ))

On the first visit to /authorize, it will redirect the user to Spotify's Login Page. But when the user logs in successfully there, it redirects the user back to the Flask site (back to /authorize), rather than doing the copy/paste part of prompt_for_user_token().

Once they are back on /authorize, this time there is a code request parameter - so it makes the OAuth2 Client-to-Provider request to transform this code into access tokens for you.

Both this and prompt_for_user_token follow the same approach:

  1. Get the Spotify authorization URL to direct the user to login at
  2. Handle the redirect from Spotify Login - Flask does this by receiving input from the URL that Spotify redirects to, and prompt_for_user_token does this by having you copy/paste the URL from a webpage that doesn't exist.
  3. Make the call to convert the code into access and refresh tokens for a user.

I may have butchered this code a bit because it's cherry-picked from my old Flask implementation of my Spotify integration. Here's a more complete gist but I'm pretty sure a lot of the SQLAlchemy stuff, the authorize view, is quite bad, so take it with a pinch of salt.

Gaun
  • 43
  • 6
Rach Sharp
  • 2,324
  • 14
  • 31
  • 1
    Are you using prompt_for_user_token() to get them to log in? If not, how do you log them in? Furthermore, thank you for answering; that's a new package to me. – Gaun Mar 07 '19 at 00:42
  • I'm not, this approach in Flask is doing the same steps `prompt_for_user_token` does but in a different way. I'd recommend https://medium.com/@darutk/the-simplest-guide-to-oauth-2-0-8c71bd9a15bb which explains this OAuth2 process pretty well, it's not something unique to Spotify, hence Python libraries like `requests-oauth2` being useful here. – Rach Sharp Mar 07 '19 at 13:45
  • Thank you so much! I'm running your code and redirect() is not defined. Where is that function coming from? – Gaun Mar 18 '19 at 14:44
  • Ahh yep, `redirect` comes from `from flask import redirect` - if you check the imports in the gist I linked, that will show where I imported things from if there's anything else I missed – Rach Sharp Mar 18 '19 at 14:47
1

You can try modifying the util module. Replace the try-except block with response = raw_input() inside it with:

response = sys.stdin.readline().strip()

Do not forget to import sys if it is not already imported.

This should allow normal PIPE-ing.

Or you can use pexpect library instead of the subprocess module. It knows how to handle input which tweaked fcntl flags of a terminal or uses mscvrt on Windows.

Also, when PIPE-ing data, do not forget that either raw_input(), input() or sys.stdin.readline() will not return until they receive the appropriate end of line character. "\r", "\n" or "\r\n". Did you send it before with your authorization URL?

Dalen
  • 4,128
  • 1
  • 17
  • 35