├── .gitignore ├── README.md ├── auth.py ├── main.py └── processing.py /.gitignore: -------------------------------------------------------------------------------- 1 | *.txt 2 | *.xml 3 | envVariables.py 4 | *.pyc 5 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # amToSpotify - Music to Spotify playlist converter 2 | 3 | ## Description 4 | 5 | This application converts a user's playlist from Apple Music (Itunes Music Library xml file) to his/her playlist in Spotify (using Spotify's API). The songs that couldn't be converted will be written to "failure.txt" and the rest should be added to your selected Spotify playlist. 6 | 7 | The plan is to further develop and deploy the web app in the near future! 8 | 9 | ## Dependencies 10 | 11 | - Python 2 12 | - Flask 13 | 14 | ## Startup 15 | To get started, clone this repository to your local environment. 16 | 17 | ```bash 18 | git clone git://github.com/Vumz/amToSpotify.git 19 | ``` 20 | 21 | Then locate your [Itunes Music Library XML file](https://support.apple.com/en-us/HT201610) and copy it into the repository folder. Make sure the name of the XML file and the below in main.py are the same. 22 | 23 | ```python 24 | itunesXMLFile = 'itunes Music Library.xml' 25 | ``` 26 | 27 | Type client credentials in auth.py from your own spotify app, which can be made [here](https://beta.developer.spotify.com/dashboard/login) 28 | ```python 29 | clientID = envVariables.clientID 30 | clientSecret = envVariables.clientSecret 31 | ``` 32 | 33 | Type in the Apple Music playlist in your library that you want to convert and the Spotify playlist in your library that you want to append the songs to in main.py 34 | ```python 35 | playlistApple = 'Drive' 36 | playlistSpotify = 'Drive' 37 | ``` 38 | 39 | To run the program, cd into the project directory and enter the following in terminal 40 | ```python 41 | python main.py 42 | ``` 43 | 44 | ## Reporting Issues 45 | 46 | If there are any suggestions, issues, or bugs please list them [here](https://github.com/Vumz/amToSpotify/issues). 47 | -------------------------------------------------------------------------------- /auth.py: -------------------------------------------------------------------------------- 1 | # Created by Vamsee Gangaram 2 | 3 | import envVariables 4 | import urllib 5 | from flask import request 6 | import requests 7 | import base64 8 | import json 9 | 10 | 11 | #API client keys 12 | clientID = envVariables.clientID 13 | clientSecret = envVariables.clientSecret 14 | 15 | #URLS 16 | spotifyAuthURL = 'https://accounts.spotify.com/authorize' 17 | spotifyTokenURL = 'https://accounts.spotify.com/api/token' 18 | 19 | #payload parameters 20 | redirectURI = 'http://127.0.0.1:8080/login' 21 | scope = 'playlist-modify-private playlist-modify-public' 22 | 23 | #header for requesting tokens 24 | authHeader = {'Authorization': 25 | 'Basic {}'.format(base64.b64encode('{}:{}'.format(clientID, clientSecret)))} 26 | #or {'Authorization': 'Basic {}'.format(('{}:{}'.format(clientID, clientSecret)).encode('base64', 'strict'))} 27 | 28 | 29 | #returns authentication URL 30 | def getAuthURL(): 31 | authPayload = {'client_id': clientID, 32 | 'response_type': 'code', 33 | 'redirect_uri': redirectURI, 34 | 'scope': scope} 35 | urlparams = urllib.urlencode(authPayload) 36 | return '{}/?{}'.format(spotifyAuthURL, urlparams) 37 | 38 | 39 | #returns dictionary of authentication tokens 40 | def getAuthTokens(): 41 | authCode = request.args.get('code') 42 | if not authCode: 43 | return None 44 | authPayload = {'grant_type': 'authorization_code', 45 | 'code': authCode, 46 | 'redirect_uri': redirectURI} 47 | postResponse = requests.post(spotifyTokenURL, headers=authHeader, data=authPayload) 48 | return json.loads(postResponse.text) 49 | 50 | 51 | #returns a dictionary of the access header 52 | def getAccessHeader(tokenData): 53 | accessToken = tokenData['access_token'] 54 | return {'Authorization': 'Bearer {}'.format(accessToken)} 55 | 56 | #returns a dictionary of updated authentication tokens 57 | def refreshTokens(tokenData): 58 | authPayload = {'grant_type': 'refresh_token', 59 | 'refresh_token': tokenData['refresh_token']} 60 | postResponse = requests.post(spotifyTokenURL, header=authHeader, data=authPayload) 61 | return json.loads(postResponse.text) 62 | -------------------------------------------------------------------------------- /main.py: -------------------------------------------------------------------------------- 1 | #Created by Vamsee Gangaram 2 | 3 | from flask import Flask, request, redirect, url_for 4 | import auth 5 | import envVariables as eVar 6 | import webbrowser 7 | import ast 8 | import processing 9 | 10 | 11 | 12 | itunesXMLFile = 'itunes Music Library.xml' 13 | playlistApple = 'Drive' 14 | playlistSpotify = 'Drive' 15 | 16 | app = Flask(__name__) 17 | 18 | @app.route('/') 19 | def index(): 20 | #directs the user to the spotify authentication url to request permission 21 | authURL = auth.getAuthURL() 22 | return redirect(authURL) 23 | 24 | @app.route('/login') 25 | def login(): 26 | #gets token data from the authentication callback 27 | tokenData = auth.getAuthTokens() 28 | return redirect(url_for('convert', tokenData=tokenData)) 29 | 30 | @app.route('/convert') 31 | def convert(): 32 | try: 33 | #get the tokenData from the url 34 | tokenData = ast.literal_eval(request.args.get('tokenData')) 35 | except ValueError as v: 36 | #if user cancels spotify authentication, return them to the authentication page again 37 | return redirect('http://127.0.0.1:8080/') 38 | accessHeader = auth.getAccessHeader(tokenData) 39 | itunesSongs = processing.getAppleMusic(itunesXMLFile, playlistApple) 40 | trackURIs = processing.getTrackURIs(itunesSongs, accessHeader) 41 | if processing.addToPlaylist(trackURIs, playlistSpotify, accessHeader): 42 | return "Success" 43 | return "Failed" 44 | 45 | if __name__ == '__main__': 46 | webbrowser.open('http://127.0.0.1:8080/') 47 | app.run(debug=False,port=8080) 48 | -------------------------------------------------------------------------------- /processing.py: -------------------------------------------------------------------------------- 1 | #Created by Vamsee Gangaram 2 | 3 | from __future__ import print_function 4 | import sys 5 | import plistlib 6 | from flask import request 7 | import requests 8 | import json 9 | import time 10 | 11 | 12 | 13 | spotifyAPIURL = "https://api.spotify.com/v1" 14 | 15 | 16 | #returns dictionary of songs(key) and artists(value) 17 | def getAppleMusic(xmlItunes, playlist): 18 | itunesLib = plistlib.readPlist(xmlItunes) 19 | with open('failure.txt', 'w') as fail: 20 | trackIDs = [str(key['Track ID']) 21 | for playlist in itunesLib['Playlists'] 22 | if playlist['Name'] == 'Drive' 23 | for key in playlist['Playlist Items']] 24 | songs = {} 25 | for trackID in trackIDs: 26 | try: 27 | songs[(itunesLib['Tracks'][trackID]['Name']).encode('utf-8')] = (itunesLib['Tracks'][trackID]['Artist']).encode('utf-8') 28 | except: 29 | fail.write(itunesLib['Tracks'][trackID]['Name'] + '\n') 30 | return songs 31 | 32 | #returns a list of the Spotify trackIDs from the songs dictionary passed in 33 | def getTrackURIs(songs, accessHeader): 34 | trackEndpoint = "{}/search".format(spotifyAPIURL) 35 | trackPayload = {'type': 'track', 36 | 'limit': 1, 37 | 'offset': 0} 38 | trackList = [] 39 | with open('failure.txt', 'a') as fail: 40 | for song, artist in songs.iteritems(): 41 | retries = 2 42 | while retries > 0: 43 | try: 44 | #requests for track details from Spotify API 45 | trackPayload['q'] = '{} artist:{}'.format(song, artist) 46 | getResponse = requests.get(trackEndpoint, params=trackPayload, headers=accessHeader) 47 | trackData = json.loads(getResponse.text) 48 | #checks if the track can be found by exluding the features 49 | if (trackData['tracks']['items'] == []): 50 | songF = getNoFeat(song) 51 | if songF is not None: 52 | trackPayload['q'] = '{} artist:{}'.format(songF, artist) 53 | getResponse = requests.get(trackEndpoint, params=trackPayload, headers=accessHeader) 54 | trackData = json.loads(getResponse.text) 55 | #checks if the track can be found with a single artist 56 | if (trackData['tracks']['items'] == []): 57 | artistS = getSingleArtist(artist) 58 | if artistS is not None: 59 | trackPayload['q'] = '{} artist:{}'.format(song, artistS) 60 | getResponse = requests.get(trackEndpoint, params=trackPayload, headers=accessHeader) 61 | trackData = json.loads(getResponse.text) 62 | #if getResponse.status_code == 429: 63 | #time.sleep(retry after seconds) 64 | # need to handle when the API rate limit is reached^ 65 | #adds the trackID to trackList 66 | trackList.append(trackData['tracks']['items'][0]['uri']) 67 | retries = 0 68 | except: 69 | print(trackData) 70 | retries -= 1 71 | #retry track details request in case there was a bad gateway error 72 | if retries > 0: 73 | continue 74 | #write to track to failure file if the track cannot be found 75 | fail.write(song + "-" + artist + "\n") 76 | return trackList 77 | 78 | #returns True if the the tracks were added to the user's playlist, False if not 79 | def addToPlaylist(trackIDs, playlist, accessHeader): 80 | #gets user's playlists data 81 | userPEndpoint = "{}/me/playlists".format(spotifyAPIURL) 82 | getResponse = requests.get(userPEndpoint, headers=accessHeader) 83 | playlistData = json.loads(getResponse.text) 84 | try: 85 | #gets the playlist ID (of the playlist the user entered) and Owner ID 86 | itemNum = [pos for pos in xrange(len(playlistData['items'])) 87 | if (playlist.lower() == (playlistData['items'][pos]['name']).lower())][0] 88 | playlistID = playlistData['items'][itemNum]['id'] 89 | userID = playlistData['items'][itemNum]['owner']['id'] 90 | except: 91 | print("something went wrong, make sure the playlist you entered is existing in your account") 92 | return False 93 | playlistEndpoint = "{}/users/{}/playlists/{}/tracks".format(spotifyAPIURL, userID, playlistID) 94 | tempHeader = {'Authorization': accessHeader['Authorization'], 95 | 'Content-Type': 'application/json'} 96 | lastI = 0 97 | #adds the tracks with valid IDs from the itunes xml to the user's selected playlist (in increments of a 100) 98 | for i in xrange(0, len(trackIDs), 100): 99 | playlistPayload = {'uris': trackIDs[lastI:i]} 100 | lastI = i 101 | postResponse = requests.post(playlistEndpoint, headers=tempHeader, data=json.dumps(playlistPayload)) 102 | #adds the remaining tracks to the user's selected playlist 103 | if ((len(trackIDs) - lastI) > 0): 104 | playlistPayload = {'uris': trackIDs[lastI:]} 105 | postResponse = requests.post(playlistEndpoint, headers=tempHeader, data=json.dumps(playlistPayload)) 106 | return True 107 | 108 | #returns the string of the track without the feature section 109 | def getNoFeat(song): 110 | index = song.find('(feat.') 111 | if(index > -1): 112 | return song[:index] 113 | index = song.find('[feat.') 114 | if(index > -1): 115 | return song[:index] 116 | return None 117 | 118 | #returns the string of the first artist 119 | def getSingleArtist(artist): 120 | index = artist.find(',') 121 | if(index > -1): 122 | return artist[:index] 123 | index = artist.find('&') 124 | if(index > -1): 125 | return artist[:index] 126 | return None 127 | 128 | 129 | 130 | --------------------------------------------------------------------------------