├── db_fns ├── __init__.py └── db.py ├── misc ├── __init__.py ├── util.py └── auth.py ├── spotify_fns ├── __init__.py └── spotify.py ├── text_fns ├── __init__.py └── handlers.py ├── Procfile ├── authenticate.py ├── templates └── index.html ├── requirements.txt ├── .gitignore ├── main.py └── README.md /db_fns/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /misc/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /spotify_fns/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /text_fns/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /Procfile: -------------------------------------------------------------------------------- 1 | web: gunicorn main:app 2 | -------------------------------------------------------------------------------- /authenticate.py: -------------------------------------------------------------------------------- 1 | from misc.auth import authenticate 2 | 3 | authenticate() -------------------------------------------------------------------------------- /templates/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | {% for item in sorted_array%} 4 |

{{ item }}

5 | {% endfor %} 6 | 7 | -------------------------------------------------------------------------------- /requirements.txt: -------------------------------------------------------------------------------- 1 | Flask==0.10.1 2 | Jinja2==2.7.3 3 | MarkupSafe==0.23 4 | Werkzeug==0.10.1 5 | gunicorn==19.2.1 6 | httplib2==0.9 7 | itsdangerous==0.24 8 | pymongo==2.8 9 | requests==2.5.1 10 | six==1.9.0 11 | spotipy==2.3.0 12 | twilio==3.6.15 13 | wsgiref==0.1.2 14 | -------------------------------------------------------------------------------- /misc/util.py: -------------------------------------------------------------------------------- 1 | """Misc functions needed for text_dj""" 2 | 3 | def stringify_results(results): 4 | if len(results) == 0: 5 | return "Sorry, no results matched. Please try another search" 6 | else: 7 | text_response = "Text back the number of your preffered song or search again for another song: \n\n" 8 | 9 | for i in range(0,len(results)): 10 | text_response = text_response + str(i) + ": '" + results[i]['name'] + "' by " + results[i]['artists'] + "\n\n" 11 | return text_response -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .Python 2 | env/ 3 | build/ 4 | develop-eggs/ 5 | dist/ 6 | downloads/ 7 | eggs/ 8 | .eggs/ 9 | lib/ 10 | lib64/ 11 | parts/ 12 | sdist/ 13 | var/ 14 | *.egg-info/ 15 | .installed.cfg 16 | *.egg 17 | *.pyc 18 | .DS_Store 19 | 20 | # PyInstaller 21 | # Usually these files are written by a python script from a template 22 | # before PyInstaller builds the exe, so as to inject date/other infos into it. 23 | *.manifest 24 | *.spec 25 | 26 | # Installer logs 27 | pip-log.txt 28 | pip-delete-this-directory.txt 29 | 30 | # Unit test / coverage reports 31 | htmlcov/ 32 | .tox/ 33 | .coverage 34 | .cache 35 | nosetests.xml 36 | coverage.xml 37 | 38 | # Translations 39 | *.mo 40 | *.pot 41 | 42 | # Django stuff: 43 | *.log 44 | 45 | # Sphinx documentation 46 | docs/_build/ 47 | 48 | # PyBuilder 49 | target/ 50 | 51 | # venv 52 | venv/ 53 | 54 | # temp file saving auth token 55 | -------------------------------------------------------------------------------- /text_fns/handlers.py: -------------------------------------------------------------------------------- 1 | from flask import request, session, Flask 2 | from spotify_fns.spotify import search_for_song, update_playlist 3 | from misc.util import stringify_results 4 | 5 | 6 | # The session object makes use of a secret key. 7 | SECRET_KEY = 'a secret key' 8 | app = Flask(__name__) 9 | app.config.from_object(__name__) 10 | 11 | def message_handler(req): 12 | body = req.values.get('Body', None) 13 | 14 | # this is a response 15 | try: 16 | float(body) 17 | return handle_song_response(body) 18 | 19 | # this is a song name 20 | except ValueError: 21 | return handle_song_name(body) 22 | 23 | 24 | def handle_song_response(body): 25 | index = int(str(body)) 26 | song_history = session.get('song_history', None) 27 | if index not in range(0,len(song_history[-1])): 28 | return "Sorry, that's not a valid list number. Please try again" 29 | if song_history: 30 | return update_playlist(song_history[-1][index]) 31 | else: 32 | return "You must first search for a song, please respond with a song name" 33 | 34 | def handle_song_name(body): 35 | # get the results of this query in JSON 36 | search_result = search_for_song(body) 37 | text_response = stringify_results(search_result) 38 | 39 | # update this user's cookies to store these results 40 | song_history = session.get('song_history', []) 41 | song_history.append(search_result) 42 | session['song_history'] = song_history 43 | 44 | return text_response -------------------------------------------------------------------------------- /db_fns/db.py: -------------------------------------------------------------------------------- 1 | import os 2 | import sys 3 | import pymongo 4 | from pymongo import MongoClient 5 | from datetime import datetime 6 | 7 | def get_auth_token(): 8 | """Reads in auth token from mongo_db""" 9 | collection = get_mongo_collection() 10 | return collection.find_one({'key_type' : 'access_token'})['key'] 11 | 12 | def get_refresh_token(): 13 | """Reads in refresh token from mongo db""" 14 | collection = get_mongo_collection() 15 | return collection.find_one({'key_type' : 'refresh_token'})['key'] 16 | 17 | def get_expiration_time(): 18 | """Return the expiration time for access tokens""" 19 | collection = get_mongo_collection() 20 | return collection.find_one({'key_type' : 'expiration_time'})['key'] 21 | 22 | def check_token_exp(): 23 | """Returns if the current token has expired """ 24 | return datetime.now() > get_expiration_time() 25 | 26 | def get_mongo_collection(): 27 | """Returns mongo collection where our auth tokens are stored""" 28 | 29 | MONGO_URL = os.environ.get('MONGOHQ_URL') 30 | DB_NAME = os.environ.get('MONGO_DB_NAME') 31 | DB_COLLECTION = os.environ.get('MONGO_COLLECTION_NAME') 32 | return MongoClient(MONGO_URL)[DB_NAME][DB_COLLECTION] 33 | 34 | def write_to_mongo(object_type, val): 35 | """Updates the object_type if it exists with val, otherwise makes 36 | a new entry into db""" 37 | 38 | # get the collection 39 | collection = get_mongo_collection() 40 | 41 | # update/insert relevant documents 42 | return collection.update( 43 | { 'key_type': object_type}, 44 | { 45 | 'key_type': object_type, 46 | 'key': val 47 | }, 48 | upsert=True 49 | ) -------------------------------------------------------------------------------- /main.py: -------------------------------------------------------------------------------- 1 | # web protocol 2 | from flask import Flask, request, redirect, g, render_template, session 3 | import urllib 4 | 5 | # app dependencies 6 | import twilio.twiml 7 | 8 | # internal 9 | import os 10 | from text_fns.handlers import message_handler 11 | from misc.auth import authenticate 12 | 13 | 14 | # The session object makes use of a secret key. 15 | SECRET_KEY = 'a secret key' 16 | app = Flask(__name__) 17 | app.config.from_object(__name__) 18 | 19 | @app.route('/') 20 | def index(): 21 | """Start authentication process by having user log in to Spotify""" 22 | # login to spotify 23 | root_url = 'https://accounts.spotify.com/authorize/?' 24 | u = urllib.urlencode({ 25 | 'client_id': os.environ.get('SPOTIPY_CLIENT_ID'), 26 | 'response_type': 'code', 27 | 'redirect_uri':os.environ.get('SPOTIPY_REDIRECT_URI'), 28 | 'scope' : 'playlist-modify-public' 29 | }) 30 | 31 | return redirect(root_url + u) 32 | 33 | @app.route("/callback/q") 34 | def callback(): 35 | """Finish the authentication process via a callback""" 36 | 37 | # parse the token and authenticate with it 38 | access_token = str(request.args['code']) 39 | authenticate(access_token) 40 | return "Authentication was successful!" 41 | 42 | 43 | @app.route("/twilio", methods=['GET', 'POST']) 44 | def respont_to_text(): 45 | """Respond to incoming text messages""" 46 | 47 | # generate response to incoming message and reply to user 48 | resp_text = message_handler(request) 49 | resp = twilio.twiml.Response() 50 | resp.message(resp_text) 51 | return str(resp) 52 | 53 | if __name__ == "__main__": 54 | app.run(debug=True,port=8080) 55 | -------------------------------------------------------------------------------- /misc/auth.py: -------------------------------------------------------------------------------- 1 | # url navigation 2 | import requests 3 | import base64 4 | import json 5 | import urllib 6 | import os 7 | 8 | # mongo fns 9 | from db_fns.db import write_to_mongo, get_refresh_token 10 | 11 | # date calculations 12 | from datetime import datetime 13 | from datetime import timedelta 14 | 15 | def compute_expiration_time(seconds): 16 | current_time = datetime.now() 17 | expiration_time = current_time + timedelta(seconds=seconds-30) 18 | return expiration_time 19 | 20 | def get_code_paylod(access_token): 21 | if not access_token: 22 | refresh_token = get_refresh_token() 23 | code_payload = { 24 | "grant_type":"refresh_token", 25 | "refresh_token":refresh_token, 26 | } 27 | else: 28 | code_payload = { 29 | "grant_type":"authorization_code", 30 | "code":access_token, 31 | "redirect_uri":os.environ.get('SPOTIPY_REDIRECT_URI') 32 | } 33 | return code_payload 34 | 35 | def authenticate(access_token=None): 36 | """Method which gets new API credentials to Spotify and updates Mongo DB 37 | with the newest keys""" 38 | 39 | code_payload = get_code_paylod(access_token) 40 | base64encoded = base64.b64encode( 41 | os.environ.get('SPOTIPY_CLIENT_ID') + 42 | ":" + 43 | os.environ.get('SPOTIPY_CLIENT_SECRET') 44 | ) 45 | 46 | headers = {"Authorization":"Basic %s" % base64encoded} 47 | post_request = requests.post( 48 | "https://accounts.spotify.com/api/token", 49 | data=code_payload, 50 | headers=headers 51 | ) 52 | 53 | json_response = json.loads(post_request.text) 54 | print json.dumps(json_response, indent=1) 55 | 56 | # write the new access token to mongo db 57 | write_to_mongo('access_token', json_response[u'access_token']) 58 | 59 | # write the expiration time 60 | expiration_time = compute_expiration_time(json_response[u'expires_in']) 61 | write_to_mongo('expiration_time', expiration_time) 62 | 63 | # if a refresh token is passed, write that as well 64 | try: 65 | write_to_mongo('refresh_token', json_response[u'refresh_token']) 66 | except: 67 | pass 68 | 69 | return True -------------------------------------------------------------------------------- /spotify_fns/spotify.py: -------------------------------------------------------------------------------- 1 | import spotipy 2 | import pprint 3 | import json 4 | import os 5 | from misc.auth import authenticate 6 | 7 | from db_fns.db import get_auth_token, check_token_exp 8 | 9 | 10 | def get_spotipy(): 11 | """Function which returns an authenticated spotipy instance""" 12 | 13 | token = get_auth_token() 14 | sp = spotipy.Spotify(auth=token) 15 | return sp 16 | 17 | 18 | def search_for_song(song_name): 19 | 20 | def get_artists(artists): 21 | """Method to stringify artists from JSON""" 22 | output = "" 23 | for artist in artists: 24 | output += artist['name'].encode('utf-8') + ", " 25 | 26 | #delete the trailing comma/extra space 27 | output = output[:-2] 28 | return output 29 | 30 | def parse_results(results): 31 | top_results = results['tracks']['items'] 32 | response = [] 33 | 34 | max_range = min(5,len(top_results)) 35 | 36 | # store the top 5 results 37 | for i in range (0,max_range): 38 | list_item = {} 39 | list_item['name'] = top_results[i]['name'] 40 | list_item['artists'] = get_artists(top_results[i]['artists']) 41 | list_item['track_id'] = top_results[i]['id'] 42 | response.append(list_item) 43 | 44 | return response 45 | 46 | sp = get_spotipy() 47 | results = sp.search(song_name) 48 | return parse_results(results) 49 | 50 | 51 | def update_playlist(json_song): 52 | """Updates spotify playlist from JSON data that is passed in""" 53 | 54 | sp = get_spotipy() 55 | 56 | #user params 57 | track_id = json_song['track_id'] 58 | playlist_id = os.environ.get('SPOTIFY_PLAYLIST_ID') 59 | user_id = os.environ.get('SPOTIFY_USER_ID') 60 | 61 | #update playlist 62 | results = sp.user_playlist_add_tracks(user_id, playlist_id, [track_id]) 63 | 64 | #playlist URL 65 | playlist_url = ( 66 | 'http://open.spotify.com/user/' + 67 | os.environ.get('SPOTIFY_USER_ID') + 68 | '/playlist/' + 69 | os.environ.get('SPOTIFY_PLAYLIST_ID') 70 | ) 71 | 72 | 73 | #generate response 74 | if results['snapshot_id']: 75 | return "'%s' by %s was successfully added to the playlist, you can view it here %s"%( 76 | json_song['name'].encode('utf-8'), 77 | json_song['artists'], 78 | playlist_url 79 | ) 80 | else: 81 | return "Sorry, there was an error in search.update_playlist" -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | #Text to DJ# 2 | 3 | Text to DJ is an application that lets users text a song name to a phone number and update a playlist with that song. Check out the screenshots below or try it yourself by texting a song name to 781-916-8742 and update [this playlist][4]. 4 | 5 | ![][1]

6 | ![][2]

7 | ![][3] 8 | 9 | ##How does it work?## 10 | Text messages are recieved and sent via [Twilio's][5] API and song/playlist functionality is handled via [Spotify's][6] API. 11 | 12 | ##Key technologies## 13 | 1. Gunicorn to manage a web server 14 | 2. Flask to handle routing on the server 15 | 3. MongoDB to store/update Spotify auth credentials 16 | 4. Twilio API (Twilio) 17 | 5. Spotify API (Spotipy) 18 | 19 | ## Required accounts/setup ## 20 | 1. Register for a [Twilio][7] account (Free trial) 21 | 2. Register a [Spotify application][8] 22 | 3. Create a [Heroku account][11] and [install toolbelt][9] 23 | 4. Add [MongoDB][10] as an addon to your heroku account 24 | 25 | ## Set up ## 26 | 27 | Each of the four steps above will produce some sort of credentials that you'll need to store for your program to work correctly. Create a new [config variable][12] in your Heroku app fo reach of these variables. **Note: this is not secure** but will suffice for the time being. Create the following config variables: 28 |

29 | export SPOTIPY_CLIENT_ID='Client ID from Spotify'
30 | export SPOTIPY_CLIENT_SECRET='Client secret from Spotify'
31 | export SPOTIPY_REDIRECT_URI='your_heroku_app.herokuapp.com/callback/q'
32 | export MONGOHQ_URL='mongodb://user:user@99999.mongolab.com:99999/your_app' #update this URL with your app's info
33 | export MONGO_DB_NAME='this is the suffix to your MongoHQURL, usually starts with 'heroku_app'>
34 | export MONGO_COLLECTION_NAME='auth_properties' #can leave the same
35 | export SPOTIFY_PLAYLIST_ID='spotify_playlist_id'
36 | export SPOTIFY_USER_ID='your_spotify_account_name'
37 | 
38 | 39 | Update your Twilio app's messaging URL to ensure that Twilio actually hits your server: 40 | ![][13] 41 | 42 | ## Notes ## 43 | If you have trouble getting this app set up, please feel free to send me a message. This app was my first foray into server side development and working with authentication/callbacks. As a result, I'm fairly positive I went against some best practicies - your feedback is welcome! 44 | 45 | As an aside, a feature I'd like to build (time permitting) is an admin option which texts the owner of the playlist for permission each time someone makes a request to update the playlist - feel free to beat me to it! 46 | 47 | [1]:http://i.imgur.com/y6daUDV.png?1 48 | [2]:http://i.imgur.com/yaxqYBc.png?1 49 | [3]:http://i.imgur.com/FNaYzeI.png?1 50 | [4]:http://open.spotify.com/user/jayshahtx/playlist/2OPixCtmCxev1tnBTEAzGd 51 | [5]:https://www.twilio.com/api 52 | [6]:https://developer.spotify.com/web-api/ 53 | [7]:https://developer.spotify.com/web-api/ 54 | [8]:https://developer.spotify.com/my-applications/#!/applications/create 55 | [9]:https://devcenter.heroku.com/articles/getting-started-with-python#set-up 56 | [10]:https://addons.heroku.com/mongolab 57 | [11]:https://heroku.com 58 | [12]:https://devcenter.heroku.com/articles/config-vars 59 | [13]:http://i.imgur.com/e8lA14T.png?1 60 | --------------------------------------------------------------------------------