Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Updated script with more data #7

Open
wants to merge 8 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
13 changes: 5 additions & 8 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,16 +1,13 @@
spotify-backup
==============

A Python 3* script that exports all of your Spotify playlists, useful for paranoid Spotify users like me, afraid that one day Spotify will go under and take all of our playlists with it!
A Python 3 script that exports all of your Spotify playlists, useful for paranoid Spotify users like me, afraid that one day Spotify will go under and take all of our playlists with it!

To run the script, [save it from here](https://raw.githubusercontent.com/bitsofpancake/spotify-backup/master/spotify-backup.py) and double-click it. It'll ask you for a filename and then pop open a web page so you can authorize access to the Spotify API. Then the script will load your playlists and save a tab-separated file with your playlists that you can open in Excel. You can even copy-paste the rows from Excel into a Spotify playlist.
Run the script, and double-click it. It'll ask you for a filename and then pop open a web page so you can authorize access to the Spotify API. Then the script will load your datas.
You can have a tab-separated file with your playlists that you can open in Excel using `--format txt`, so you can even copy-paste the rows from Excel into a Spotify playlist.

You can also run the script from the command line:

python spotify-backup.py playlists.txt
python spotify-backup.py data.json

Adding `--format=json` will give you a JSON dump with everything that the script gets from the Spotify API. If for some reason the browser-based authorization flow doesn't work, you can also [generate an OAuth token](https://developer.spotify.com/web-api/console/get-playlists/) on the developer site (with the `playlist-read-private` permission) and pass it with the `--token` option.

Collaborative playlists and playlist folders don't show up in the API, sadly.

*The [last version compatible with Python 2.7](https://raw.githubusercontent.com/bitsofpancake/spotify-backup/1f7e76a230e10910aa2cfa5d83ced4c271377af4/spotify-backup.py) probably still works.
If for some reason the browser-based authorization flow doesn't work, you can also [generate an OAuth token](https://developer.spotify.com/web-api/console/get-playlists/) on the developer site (with `user-follow-read user-library-read playlist-read-private playlist-read-collaborative` permission) and pass it with the `--token` option.
194 changes: 143 additions & 51 deletions spotify-backup.py
Original file line number Diff line number Diff line change
@@ -1,17 +1,13 @@
#!/usr/bin/env python3

import argparse
import sys, os, re, time
import argparse
import codecs
import http.client
import urllib.parse, urllib.request, urllib.error
import http.server
import json
import re
import sys
import time
import urllib.error
import urllib.parse
import urllib.request
import webbrowser
import json
import xspf

class SpotifyAPI:

Expand All @@ -20,7 +16,7 @@ def __init__(self, auth):
self._auth = auth

# Gets a resource from the Spotify API and returns the object.
def get(self, url, params={}, tries=3):
def get(self, url, params={}, tries=3, root=''):
# Construct the correct URL.
if not url.startswith('https://api.spotify.com/v1/'):
url = 'https://api.spotify.com/v1/' + url
Expand All @@ -34,7 +30,10 @@ def get(self, url, params={}, tries=3):
req.add_header('Authorization', 'Bearer ' + self._auth)
res = urllib.request.urlopen(req)
reader = codecs.getreader('utf-8')
return json.load(reader(res))
response = json.load(reader(res))
if root:
response = response[root]
return response
except Exception as err:
log('Couldn\'t load URL: {} ({})'.format(url, err))
time.sleep(2)
Expand All @@ -43,12 +42,15 @@ def get(self, url, params={}, tries=3):

# The Spotify API breaks long lists into multiple pages. This method automatically
# fetches all pages and joins them, returning in a single list of objects.
def list(self, url, params={}):
response = self.get(url, params)
def list(self, url, params={}, root=''):
response = self.get(url, params, root=root)
items = response['items']
while response['next']:

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If we can tell how many pages we have in total, using tqdm might be nice (I don't think it'll work on a while loop though.

response = self.get(response['next'])
items += response['items']
print('.', end='')
sys.stdout.flush()
print()
return items

# Pops open a browser window for a user to log in and authorize API access.
Expand Down Expand Up @@ -110,61 +112,151 @@ class _Authorization(Exception):
def __init__(self, access_token):
self.access_token = access_token

def log(str):
def log(str, end="\n"):
#print('[{}] {}'.format(time.strftime('%I:%M:%S'), str).encode(sys.stdout.encoding, errors='replace'))
sys.stdout.buffer.write('[{}] {}\n'.format(time.strftime('%I:%M:%S'), str).encode(sys.stdout.encoding, errors='replace'))
sys.stdout.buffer.write(('[{}] {}'+end).format(time.strftime('%H:%M:%S'), str).encode(sys.stdout.encoding, errors='replace'))
sys.stdout.flush()

def main():
# Parse arguments.
parser = argparse.ArgumentParser(description='Exports your Spotify playlists. By default, opens a browser window '
+ 'to authorize the Spotify Web API, but you can also manually specify'
+ ' an OAuth token with the --token option.')
parser.add_argument('--token', metavar='OAUTH_TOKEN', help='use a Spotify OAuth token (requires the '
parser.add_argument('-t', '--token', metavar='OAUTH_TOKEN', help='use a Spotify OAuth token (requires the '
+ '`playlist-read-private` permission)')
parser.add_argument('--format', default='txt', choices=['json', 'txt'], help='output format (default: txt)')
parser.add_argument('file', help='output filename', nargs='?')
parser.add_argument('-f', '--format', default='json', choices=['json', 'xspf', 'txt', 'md'], help='output format (default: json)')
parser.add_argument('-l', '--load', metavar='JSON_FILE', help='load an existing json file to create txt or markdown output (playlists only currently)')
parser.add_argument('-i', '--indent', metavar='INDENT_STR', default=None, help='indent JSON output')
parser.add_argument('file', help='output filename (or directory for xspf)', nargs='?')
args = parser.parse_args()

# If they didn't give a filename, then just prompt them. (They probably just double-clicked.)
while not args.file:
args.file = input('Enter a file name (e.g. playlists.txt): ')
args.file = input('Enter a file name (e.g. playlists.txt) or directory (xspf format): ')

# Log into the Spotify API.
if args.token:
spotify = SpotifyAPI(args.token)
if args.load:
with open(args.load, 'r', encoding='utf-8') as f:
data = json.load(f)
else:
spotify = SpotifyAPI.authorize(client_id='5c098bcc800e45d49e476265bc9b6934', scope='playlist-read-private')

# Get the ID of the logged in user.
me = spotify.get('me')
log('Logged in as {display_name} ({id})'.format(**me))
# Log into the Spotify API.
if args.token:
spotify = SpotifyAPI(args.token)
else:
spotify = SpotifyAPI.authorize(client_id='5c098bcc800e45d49e476265bc9b6934', scope='user-follow-read user-library-read playlist-read-private playlist-read-collaborative')

# me https://developer.spotify.com/web-api/get-current-users-profile/
# follow['artists] https://developer.spotify.com/web-api/get-followed-artists/
# albums https://developer.spotify.com/web-api/get-users-saved-albums/
# tracks https://developer.spotify.com/web-api/get-users-saved-tracks/
# playlists https://developer.spotify.com/web-api/console/get-playlists/?user_id=wizzler
data = {}

# Get the ID of the logged in user.
data['me'] = spotify.get('me')
log('Logged in as {display_name} ({id})'.format(**data['me']))

# List all playlists and all track in each playlist.
playlists = spotify.list('users/{user_id}/playlists'.format(user_id=me['id']), {'limit': 50})
for playlist in playlists:
log('Loading playlist: {name} ({tracks[total]} songs)'.format(**playlist))
playlist['tracks'] = spotify.list(playlist['tracks']['href'], {'limit': 100})

# Write the file.
with open(args.file, 'w', encoding='utf-8') as f:
# JSON file.
if args.format == 'json':
json.dump(playlists, f)
# Get follows - scope user-follow-read
# "root" workaround for non-consistent API ..
data['following'] = {}
following = spotify.get('me/following', {'type': 'artist', 'limit': 1}, root='artists')
log('Loading followed artists: {total} artists'.format(**following), end='')
data['following']['artists'] = spotify.list('me/following', {'type': 'artist', 'limit': 50}, root='artists')

# Tab-separated file.
elif args.format == 'txt':
for playlist in playlists:
f.write(playlist['name'] + '\r\n')
for track in playlist['tracks']:
f.write('{name}\t{artists}\t{album}\t{uri}\r\n'.format(
uri=track['track']['uri'],
name=track['track']['name'],
artists=', '.join([artist['name'] for artist in track['track']['artists']]),
album=track['track']['album']['name']
))
f.write('\r\n')
log('Wrote file: ' + args.file)
# List saved albums - scope user-library-read
albums = spotify.get('me/albums', {'limit': 1})
log('Loading saved albums: {total} albums'.format(**albums), end='')
data['albums'] = spotify.list('me/albums', {'limit': 50})

# List saved tracks - scope user-library-read
tracks = spotify.get('me/tracks', {'limit': 1})
log('Loading tracks: {total} songs'.format(**tracks), end='')
data['tracks'] = spotify.list('me/tracks', {'limit': 50})

# List all playlists and all track in each playlist - scope playlist-read-private, playlist-read-collaborative
data['playlists'] = spotify.list('users/{user_id}/playlists'.format(user_id=data['me']['id']), {'limit': 50})
for playlist in data['playlists']:
log('Loading playlist: {name} ({tracks[total]} songs)'.format(**playlist), end='')
playlist['tracks'] = spotify.list(playlist['tracks']['href'], {'limit': 100})

# Write the file(s).
if args.format == 'xspf':
# Create the specified directory
if not os.path.exists(args.file):
os.makedirs(args.file)
mkvalid_filename = re.compile(r'[/\\:*?"<>|]')
# Fake the special tracks playlist as regular playlist
data['playlists'].append({'id': 'saved-tracks', 'name': 'Saved tracks', 'tracks': data['tracks']})
# Playlists
for playlist in data['playlists']:
valid_filename = mkvalid_filename.sub('', playlist['name'])
with open('{}{}{}___{}.xspf'.format(args.file, os.sep, valid_filename, playlist['id']), 'w', encoding='utf-8') as f: # Avoid conflicts using id
try:
x = xspf.Xspf(title=playlist['name'])
for track in playlist['tracks']:
x.add_track(
title=track['track']['name'],
album=track['track']['album']['name'],
creator=', '.join([artist['name'] for artist in track['track']['artists']])
)
f.write(x.toXml().decode('utf-8'))
except Exception as e:
log('Failed in playlist {} ({}) : {}'.format(playlist['id'], playlist['name'], e))
# Saved albums -- different format & more informations
for album in data['albums']:
artist = ', '.join(a['name'] for a in album['album']['artists'])
filename = 'Saved album - '+artist+' - '+album['album']['name']
valid_filename = mkvalid_filename.sub('', filename)
with open('{}{}{}___{}.xspf'.format(args.file, os.sep, valid_filename, album['album']['id']), 'w', encoding='utf-8') as f: # Avoid conflicts using id
try:
x = xspf.Xspf(
date=album['album']['release_date'],
creator=artist,
title=album['album']['name']
)
for track in album['album']['tracks']['items']:
x.add_track(
title=track['name'],
album=album['album']['name'],
creator=', '.join([artist['name'] for artist in track['artists']]),
duration=str(track['duration_ms']),
trackNum=str(track['track_number']),
)
f.write(x.toXml().decode('utf-8'))
except Exception as e:
log('Failed in playlist {} ({}) : {}'.format(album['album']['id'], filename, e))
else:
with open(args.file, 'w', encoding='utf-8') as f:
# JSON file.
if args.format == 'json':
json.dump(data, f, indent=args.indent)

# Tab-separated file.
elif args.format == 'txt':
for playlist in data['playlists']:
f.write(playlist['name'] + "\n")
for track in playlist['tracks']:
f.write('{name}\t{artists}\t{album}\t{uri}\n'.format(
uri=track['track']['uri'],
name=track['track']['name'],
artists=', '.join([artist['name'] for artist in track['track']['artists']]),
album=track['track']['album']['name']
))
f.write('\n')

# Markdown
elif args.format == 'md':
f.write("# Spotify Playlists Backup " + time.strftime("%d %b %Y") + "\n")
for playlist in data['playlists']:
f.write("## " + playlist["name"] + "\n")
for track in playlist['tracks']:
f.write("* {name}\t{artists}\t{album}\t`{uri}`\n".format(
uri=track["track"]["uri"],
name=track["track"]["name"],
artists=", ".join([artist["name"] for artist in track["track"]["artists"]]),
album=track["track"]["album"]["name"]
))
f.write("\n")
log('Wrote file: ' + args.file)

if __name__ == '__main__':
main()
Loading