diff --git a/README.md b/README.md index 2359a33..ce6c59b 100644 --- a/README.md +++ b/README.md @@ -3,23 +3,20 @@ ### Please note that this won't work without Spotify Premium because Spotify limits web API access to non-free accounts. ### This also requires you to have set up [mopidy-spotify](https://github.com/mopidy/mopidy-spotify) in order to play spotify tracks. -* Run `pip install --user .` in this repo. +* Run `pip3 install --user .` in this repo. * Go to [https://developer.spotify.com/dashboard/applications](https://developer.spotify.com/dashboard/applications) and create an application. Call it whatever you want. -* Create a `.env` or `.envrc` file with contents like this, put it wherever you - want. +* Go to "Edit Settings" in your newly created app, then add + `http://localhost:8080` + as a redirect URI and hit "Save" (You may choose a different one by setting + `SPOTIPY_REDIRECT_URI` in your environment) -``` -export SPOTIPY_CLIENT_ID='YOUR_CLIENT_ID' -export SPOTIPY_CLIENT_SECRET='YOUR_CLIENT_SECRET' -export SPOTIPY_REDIRECT_URI='http://localhost' -export SPOTIFY_USERNAME='YOUR_SPOTIFY_USERNAME' -``` +* Find your spotify username [here](https://www.spotify.com/us/account/overview/) -* You can find the values for `SPOTIPY_CLIENT_ID` and `SPOTIPY_CLIENT_SECRET` - on the page for your application that you just created. +* Now you can run `spotsync --host=my_mpd_host` where `my_mpd_host` is the host you + are running MPD on (defaults to `localhost:6600` if you do not pass it) -* You can find your spotify username [here](https://www.spotify.com/us/account/overview/) - -* Now you can run `spotsync` with your `.env` file sourced (e.g. `source .env`) +* You will be prompted to grant permission to the app, once that's done, it + will cache the credentials locally and you should be able to just run + spotsync. diff --git a/requirements.txt b/requirements.txt index 65e4d8a..d944804 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,9 +1,14 @@ +bottle==0.12.16 certifi==2019.3.9 cffi==1.12.3 chardet==3.0.4 +gevent==1.4.0 +greenlet==0.4.15 +httplib2==0.12.3 idna==2.8 pycparser==2.19 python-mpd2==1.0.0 requests==2.21.0 +spotify-mpd-sync-weskerfoot==0.0.1 spotipy==2.4.4 urllib3==1.24.2 diff --git a/setup.py b/setup.py index 37a198e..eb5c236 100644 --- a/setup.py +++ b/setup.py @@ -5,7 +5,7 @@ with open("README.md", "r") as fh: setuptools.setup( name="spotify-mpd-sync-weskerfoot", - version="0.0.1", + version="0.0.2", author="Wesley Kerfoot", author_email="wes@wesk.tech", description="Synchronize Spotify Playlist to MPD", @@ -15,7 +15,9 @@ setuptools.setup( packages=setuptools.find_packages(), install_requires = [ "spotipy>=2.4.4", - "python-mpd2>=1.0.0" + "python-mpd2>=1.0.0", + "bottle>=0.12.16", + "gevent>=1.4.0" ], classifiers= [ "Programming Language :: Python :: 3", diff --git a/spotify_mpd_sync/msplaylist/authenticate.py b/spotify_mpd_sync/msplaylist/authenticate.py new file mode 100644 index 0000000..732bf69 --- /dev/null +++ b/spotify_mpd_sync/msplaylist/authenticate.py @@ -0,0 +1,104 @@ +# shows a user's playlists (need to be authenticated via oauth) + +import threading +import spotipy.oauth2 as oauth2 +import spotipy +import queue +import os +from bottle import route, run, response, request + +auth_token_queue = queue.Queue() +event_queue = queue.Queue() + +@route('/') +def index(): + auth_code = request.query.code + if auth_code: + auth_token_queue.put(auth_code) + return "It worked! You may close this tab now" + + return "Oops! Something went wrong. Please file a bug report" + +def wait_for_done(task): + server = threading.Thread(target=task) + server.daemon = True + server.start() + while True: + event = event_queue.get() + if event == "done": + break + +def run_server(): + threading.Thread(target= lambda: wait_for_done(lambda: run(quiet=True, + host='localhost', + port=8080))).start() + +def prompt_for_user_token(username, scope=None, client_id = None, + client_secret = None, redirect_uri = None, cache_path = None): + """ + prompts the user to login if necessary and returns + the user token suitable for use with the spotipy.Spotify + constructor + + Parameters: + + - username - the Spotify username + - scope - the desired scope of the request + - client_id - the client id of your app + - client_secret - the client secret of your app + - redirect_uri - the redirect URI of your app + - cache_path - path to location to save tokens + """ + + if not client_id: + client_id = os.getenv("SPOTIPY_CLIENT_ID") + + if not client_secret: + client_secret = os.getenv("SPOTIPY_CLIENT_SECRET") + + if not redirect_uri: + redirect_uri = os.getenv("SPOTIPY_REDIRECT_URI", "http://localhost:8080") + + if not client_id: + print(''' + You need to set your Spotify API credentials. You can do this by + setting environment variables like so: + + export SPOTIPY_CLIENT_ID='your-spotify-client-id' + export SPOTIPY_CLIENT_SECRET='your-spotify-client-secret' + export SPOTIPY_REDIRECT_URI='your-app-redirect-url' + + Get your credentials at + https://developer.spotify.com/my-applications + ''') + raise spotipy.SpotifyException(550, -1, 'no credentials set') + + cache_path = cache_path or ".cache-" + username + sp_oauth = oauth2.SpotifyOAuth(client_id, client_secret, redirect_uri, + scope=scope, cache_path=cache_path) + + # try to get a valid token for this user, from the cache, + # if not in the cache, the create a new (this will send + # the user to a web page where they can authorize this app) + + token_info = sp_oauth.get_cached_token() + + if not token_info: + run_server() + auth_url = sp_oauth.get_authorize_url() + try: + import webbrowser + webbrowser.open(auth_url) + except: + print("Please navigate here: %s" % auth_url) + + response = "%s?code=%s" % (redirect_uri, auth_token_queue.get()) + event_queue.put("done") + + code = sp_oauth.parse_response_code(response) + token_info = sp_oauth.get_access_token(code) + # Auth'ed API request + if token_info: + return token_info['access_token'] + else: + return None diff --git a/spotify_mpd_sync/msplaylist/spotify.py b/spotify_mpd_sync/msplaylist/spotify.py index 732dec9..d405394 100644 --- a/spotify_mpd_sync/msplaylist/spotify.py +++ b/spotify_mpd_sync/msplaylist/spotify.py @@ -1,23 +1,32 @@ #! /usr/bin/env python +import gevent.monkey +gevent.monkey.patch_all() import spotipy from spotipy.oauth2 import SpotifyClientCredentials +from spotipy.util import prompt_for_user_token from mpd import MPDClient from mpd.base import CommandError from collections import defaultdict from re import sub from os import environ +import spotipy.util as util +import argparse + +from spotify_mpd_sync.msplaylist.authenticate import prompt_for_user_token class Spotify(): - def __init__(self): + def __init__(self, host="localhost", port=6600): self.username = environ.get("SPOTIFY_USERNAME") - self.client_credentials_manager = SpotifyClientCredentials() - self.sp = spotipy.Spotify( - client_credentials_manager=self.client_credentials_manager - ) + + scope = "playlist-read-private" + + token = prompt_for_user_token(self.username, scope) + if token: + self.sp = spotipy.Spotify(auth=token) self.mpd_client = MPDClient() - self.mpd_client.connect("127.0.0.1", 6600) + self.mpd_client.connect(host, port) self._playlists = defaultdict(lambda: []) @@ -81,5 +90,21 @@ class Spotify(): def run_sync(): - spotify = Spotify() + parser = argparse.ArgumentParser() + parser.add_argument("-H", + "--host", + default="localhost", + help="The MPD server you would like spotsync to use, defaults localhost") + + parser.add_argument("-P", + "--port", + type=int, + default=6600, + help="The MPD port you would like spotsync to use, defaults to 6600") + + args = parser.parse_args() + + spotify = Spotify(host=args.host, + port=args.port) + spotify.persist_playlists()