6
\$\begingroup\$

This is refactored and much more expanded version of the script reviewed here. The whole authorization process is added.

import os
import secrets
import string
import time
import webbrowser

import furl
import requests
import simplejson as json

import config


URL_CODE_BASE_VK = 'https://oauth.vk.com/authorize'
URL_CODE_BASE_SP = 'https://accounts.spotify.com/authorize'
URL_TOKEN_VK = 'https://oauth.vk.com/access_token'
URL_TOKEN_SP = 'https://accounts.spotify.com/api/token'
URL_TRACK = 'https://api.spotify.com/v1/me/player/currently-playing'
URL_STATUS = 'https://api.vk.com/method/status.set'
EXP_IN_TOKEN_SP = 3400
EXP_IN_TOKEN_VK = 86400
FILE_TOKEN_VK = 'vk_token.json'
FILE_TOKEN_SP = 'sp_token.json'
INP_MSG = '''Enter the full URL, that you have been redirected on after giving
the permissions: '''


def get_auth_code_vk():
    url_code_params = {
        'client_id': config.CLIENT_ID_VK,
        'response_type': 'code',
        'redirect_uri': 'https://oauth.vk.com/blank.html',
        'v': 5.92,
        'scope': 'status',
        'state': gen_state(),
        'display': 'page'
    }

    code = url_open(URL_CODE_BASE_VK, url_code_params)
    return parse_code(code)


def get_auth_code_sp():
    url_code_params = {
        'client_id': config.CLIENT_ID_SP,
        'response_type': 'code',
        'redirect_uri': 'https://www.spotify.com/',
        'scope': 'user-read-currently-playing',
        'state': gen_state()
    }

    code = url_open(URL_CODE_BASE_SP, url_code_params)
    return parse_code(code)


def gen_state():
    symbols = string.ascii_lowercase + string.digits
    return ''.join(secrets.choice(symbols) for _ in range(12))


def url_open(url_base, url_params):
    url_code_full = furl.furl(url_base).add(url_params).url
    webbrowser.open_new_tab(url_code_full)

    input_url = input(INP_MSG)
    return input_url


def parse_code(url):
    return (url.split("code=")[1]).split("&state=")[0]


def get_token_vk():
    data = {
        'grant_type': 'authorization_code',
        'code': get_auth_code_vk(),
        'redirect_uri': 'https://oauth.vk.com/blank.html',
        'client_id': 6782333,
        'client_secret': config.CLIENT_SECRET_VK
    }

    response = requests.post(url=URL_TOKEN_VK, data=data).json()
    write_file(FILE_TOKEN_VK, response)


def get_token_sp():
    data = {
        'grant_type': 'authorization_code',
        'code': get_auth_code_sp(),
        'redirect_uri': 'https://www.spotify.com/',
        'client_id': config.CLIENT_ID_SP,
        'client_secret': config.CLIENT_SECRET_SP
    }

    response = requests.post(url=URL_TOKEN_SP, data=data).json()
    write_file(FILE_TOKEN_SP, response)


def write_file(tkn_file, response):
    token = {}
    token['token'] = response["access_token"]
    token['time'] = time.time()

    with open(tkn_file, 'w') as file:
        file.write(json.dumps(token))


def load_file(tkn_file):
    with open(tkn_file) as file:
        data = json.load(file)
    return data


def set_status():
    params = {
        'user_id': 8573490,
        'v': 5.92,
        'access_token': load_file(FILE_TOKEN_VK)['token'],
        'text': current_track()
    }

    response = requests.get(url=URL_STATUS, params=params)
    error = http_error(response)
    if error:
        return error
    return response


def track_data():
    tkn_file = load_file(FILE_TOKEN_SP)['token']
    headers = {
        'Accept': 'application/json',
        'Authorization': f'Bearer {tkn_file}'
    }

    return requests.get(url=URL_TRACK, headers=headers)


def current_track():
    response = track_data()
    error = http_error(response)
    if error:
        return error

    data = response.json()
    artist = data['item']['artists'][0]['name']
    track = data['item']['name']

    return f'{artist} - {track}'


def http_error(response):
    try:
        response.raise_for_status()
        return None
    except requests.exceptions.HTTPError as error:
        return error


def check_playback():
    if track_data().status_code == 204:
        print("Not playing")
    else:
        set_status()
        print(current_track())


def token_missing(file):
    return not os.path.isfile(file)


def token_expired(file, exp_in):
    return time.time() - load_file(file)['time'] > exp_in


def token_not_valid(file, exp_in):
    return token_missing(file) or token_expired(file, exp_in)


def run_script():
    if token_not_valid(FILE_TOKEN_VK, EXP_IN_TOKEN_VK):
        get_token_vk()

    if token_not_valid(FILE_TOKEN_SP, EXP_IN_TOKEN_SP):
        get_token_sp()

    check_playback()


if __name__ == "__main__":
    run_script()
\$\endgroup\$
  • \$\begingroup\$ I rolled back your last edit. After getting an answer you are not allowed to change your code anymore. This is to ensure that answers do not get invalidated and have to hit a moving target. If you have changed your code you can either post it as an answer (if it would constitute a code review) or ask a new question with your changed code (linking back to this one as reference). Refer to this post for more information \$\endgroup\$ – Sᴀᴍ Onᴇᴌᴀ Dec 19 '18 at 15:33
6
\$\begingroup\$

Parse URLs with furl

This method looks like a hacky way to get the value of the code parameter from a query string:

def parse_code(url):
    return (url.split("code=")[1]).split("&state=")[0]

Since the script already imports furl, why not use it for this job:

def parse_code(url):
    return furl(url).args.get('code', '')

As you mentioned in a comment, the parameters in the URL are not proper query parameters following ?, but encoded in the URL fragment after #. In this case, I suggest to replace the beginning of the string until # with ?, and then apply furl:

def parse_code(url):
    # urls contain the parameters encoded in the fragment after #
    url = '?' + url[url.find('#')+1:]
    return furl(url).args.get('code', '')

Avoid magic values

It's good that you have defined some constants at the top of the file. It would be good to go a bit further and define some more, because there are still quite many magic values scattered around in the code, for example these definitely deserve some explanation (by a good name), or to come from configuration:

'redirect_uri': 'https://oauth.vk.com/blank.html',
'v': 5.92,
# ...

'client_id': 6782333,
'user_id': 8573490,
# ...

return ''.join(secrets.choice(symbols) for _ in range(12))
#                                                     ^^ why 12?

Avoid redundant local variables

There are some local variables that are set once and then immediately returned. I don't see much value brought by these variables, I would return the values directly, for example here:

input_url = input(INP_MSG)
return input_url
# ...

with open(tkn_file) as file:
    data = json.load(file)
return data
# ...
\$\endgroup\$
  • \$\begingroup\$ return furl(URL).args.get('code') this does not work in vk.com case because the code is returned like this https://oauth.vk.com/blank.html#code=. Initially, I've had different parsing lines for two services. Had something similar to your proposal for the Spotify auth code. The way in my code is really hacky, yes. \$\endgroup\$ – edvard_munch Dec 18 '18 at 6:43
  • \$\begingroup\$ @Flynn84 in that case, I guess you want to get empty string instead of None, you can pass a default value to get: furl(URL).args.get('code', '') \$\endgroup\$ – janos Dec 18 '18 at 7:16
  • \$\begingroup\$ return ''.join(secrets.choice(symbols) for _ in range(12)) it is really a random number, what should be better in this case, define a constant with that number? Or put another generator for this number? For example, generate a random length of state parameter between 8 and 20. \$\endgroup\$ – edvard_munch Dec 18 '18 at 7:18
  • \$\begingroup\$ @Flynn84 I meant that perhaps you could define a constant for 12, for example SECRET_LENGTH = 12 \$\endgroup\$ – janos Dec 18 '18 at 7:20
  • \$\begingroup\$ I meant that there is an actual code being passed but furl and other URL functions couldn't parse it because there is # instead of ?. If I understood it correctly. \$\endgroup\$ – edvard_munch Dec 18 '18 at 7:47

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy

Not the answer you're looking for? Browse other questions tagged or ask your own question.