From 5bf2a78a8dec4df96882ad7040379019b9347a19 Mon Sep 17 00:00:00 2001 From: Marco Date: Thu, 12 Nov 2015 13:12:09 +0100 Subject: [PATCH 1/8] add markdown to txt file --- .python-version | 1 + spotify-backup.py | 15 ++++++++------- 2 files changed, 9 insertions(+), 7 deletions(-) create mode 100644 .python-version diff --git a/.python-version b/.python-version new file mode 100644 index 0000000..6cb9d3d --- /dev/null +++ b/.python-version @@ -0,0 +1 @@ +3.4.3 diff --git a/spotify-backup.py b/spotify-backup.py index cf603de..5d0bfcc 100755 --- a/spotify-backup.py +++ b/spotify-backup.py @@ -154,16 +154,17 @@ def main(): # Tab-separated file. elif args.format == 'txt': + f.write("# Spotify Playlists Backup " + time.strftime("%d %b %Y") + "\r\n") for playlist in playlists: - f.write(playlist['name'] + '\r\n') + 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("* {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') + f.write("\r\n") log('Wrote file: ' + args.file) if __name__ == '__main__': From 79fb694de863eea47d9a721da015abdd5f91c6fc Mon Sep 17 00:00:00 2001 From: Marco Date: Sun, 15 Nov 2015 16:19:13 +0100 Subject: [PATCH 2/8] Test --- spotify-backup.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/spotify-backup.py b/spotify-backup.py index 5d0bfcc..2903ce5 100755 --- a/spotify-backup.py +++ b/spotify-backup.py @@ -152,7 +152,7 @@ def main(): if args.format == 'json': json.dump(playlists, f) - # Tab-separated file. + # Tab-separated file. Test elif args.format == 'txt': f.write("# Spotify Playlists Backup " + time.strftime("%d %b %Y") + "\r\n") for playlist in playlists: From dafc8b08c7b08e2b4ed146ccff4d0617935dbe52 Mon Sep 17 00:00:00 2001 From: Alexandre L Date: Sun, 29 Nov 2015 17:26:23 +0100 Subject: [PATCH 3/8] Separate MD/Txt, export more data, allow json indent Separate MD/Txt : new option for format Export more data : add profile, albums, tracks, collaborative playlists Allow json indent : to have pretty output Todo : txt/MD output for all data --- spotify-backup.py | 105 +++++++++++++++++++++++++++++++++------------- 1 file changed, 77 insertions(+), 28 deletions(-) diff --git a/spotify-backup.py b/spotify-backup.py index 2903ce5..deae799 100755 --- a/spotify-backup.py +++ b/spotify-backup.py @@ -20,7 +20,7 @@ class SpotifyAPI: 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 @@ -34,7 +34,10 @@ class SpotifyAPI: 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) @@ -43,12 +46,15 @@ class SpotifyAPI: # 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']: 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. @@ -110,9 +116,9 @@ class SpotifyAPI: 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('%I:%M:%S'), str).encode(sys.stdout.encoding, errors='replace')) sys.stdout.flush() def main(): @@ -120,9 +126,11 @@ def main(): 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('-f', '--format', default='json', choices=['json', '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', nargs='?') args = parser.parse_args() @@ -130,41 +138,82 @@ def main(): while not args.file: args.file = input('Enter a file name (e.g. playlists.txt): ') - # 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}) + # 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') + + # 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)) + 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) + json.dump(data, f, indent=args.indent) - # Tab-separated file. Test + # Tab-separated file. elif args.format == 'txt': - f.write("# Spotify Playlists Backup " + time.strftime("%d %b %Y") + "\r\n") - for playlist in playlists: - f.write("## " + playlist["name"] + "\r\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}`\r\n".format( + 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("\r\n") + f.write("\n") log('Wrote file: ' + args.file) if __name__ == '__main__': From e950b8bb891a7aed8069bbf9f903abf95594cd8d Mon Sep 17 00:00:00 2001 From: Alexandre L Date: Sun, 29 Nov 2015 17:28:25 +0100 Subject: [PATCH 4/8] Show progress fix Oh and it shows retreival progression. --- spotify-backup.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/spotify-backup.py b/spotify-backup.py index deae799..38d81fe 100755 --- a/spotify-backup.py +++ b/spotify-backup.py @@ -179,7 +179,7 @@ def main(): # 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)) + log('Loading playlist: {name} ({tracks[total]} songs)'.format(**playlist), end='') playlist['tracks'] = spotify.list(playlist['tracks']['href'], {'limit': 100}) # Write the file. From 10614cfc8bed15101ffc95da4aec103fe7017805 Mon Sep 17 00:00:00 2001 From: Alexandre L Date: Sun, 29 Nov 2015 17:31:23 +0100 Subject: [PATCH 5/8] Update readme with this version. --- README.md | 13 +++++-------- 1 file changed, 5 insertions(+), 8 deletions(-) diff --git a/README.md b/README.md index b3fa7df..13187fc 100644 --- a/README.md +++ b/README.md @@ -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 the `playlist-read-private` permission) and pass it with the `--token` option. From fad200f619b06f2a0c503c25f3dcac7fa34120c3 Mon Sep 17 00:00:00 2001 From: Alexandre L Date: Sun, 29 Nov 2015 17:32:20 +0100 Subject: [PATCH 6/8] Update permission needed --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 13187fc..6fb8554 100644 --- a/README.md +++ b/README.md @@ -10,4 +10,4 @@ You can also run the script from the command line: python spotify-backup.py data.json -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. +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. From dea77e0eedc57613c05e477382cbc80f907ea97f Mon Sep 17 00:00:00 2001 From: Alexandre L Date: Sun, 29 Nov 2015 17:32:32 +0100 Subject: [PATCH 7/8] Delete .python-version --- .python-version | 1 - 1 file changed, 1 deletion(-) delete mode 100644 .python-version diff --git a/.python-version b/.python-version deleted file mode 100644 index 6cb9d3d..0000000 --- a/.python-version +++ /dev/null @@ -1 +0,0 @@ -3.4.3 From 923d11311a6ae41bb77c538f30ccc8a2042fc467 Mon Sep 17 00:00:00 2001 From: Alexandre L Date: Sat, 3 Jun 2017 01:15:19 +0200 Subject: [PATCH 8/8] Add xspf support With some materials from https://github.com/jnylen/spotify-backup, and thanks @Iristyle for the notification. Assuming you saved in json, you can convert them with something like `py -3 .\spotify-backup.py -l .\spotify_2017-06-02.json -f xspf xspf_playlists_directory` --- spotify-backup.py | 134 +++++++++++------ xspf.py | 371 ++++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 459 insertions(+), 46 deletions(-) create mode 100644 xspf.py diff --git a/spotify-backup.py b/spotify-backup.py index 38d81fe..28c84fc 100755 --- a/spotify-backup.py +++ b/spotify-backup.py @@ -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: @@ -118,7 +114,7 @@ class SpotifyAPI: def log(str, end="\n"): #print('[{}] {}'.format(time.strftime('%I:%M:%S'), str).encode(sys.stdout.encoding, errors='replace')) - sys.stdout.buffer.write(('[{}] {}'+end).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(): @@ -128,15 +124,15 @@ def main(): + ' an OAuth token with the --token option.') parser.add_argument('-t', '--token', metavar='OAUTH_TOKEN', help='use a Spotify OAuth token (requires the ' + '`playlist-read-private` permission)') - parser.add_argument('-f', '--format', default='json', choices=['json', 'txt', 'md'], help='output format (default: json)') + 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', nargs='?') + 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): ') if args.load: with open(args.load, 'r', encoding='utf-8') as f: @@ -182,39 +178,85 @@ def main(): log('Loading playlist: {name} ({tracks[total]} songs)'.format(**playlist), end='') 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(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) + # 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() diff --git a/xspf.py b/xspf.py new file mode 100644 index 0000000..654623d --- /dev/null +++ b/xspf.py @@ -0,0 +1,371 @@ +#!/usr/bin/python + +import xml.etree.ElementTree as ET + +class XspfBase(object): + NS = "http://xspf.org/ns/0/" + + def _addAttributesToXml(self, parent, attrs): + for attr in attrs: + value = getattr(self, attr) + if value: + el = ET.SubElement(parent, "{{{0}}}{1}".format(self.NS, attr)) + el.text = value + + def _addDictionaryElements(self, parent, name, values): + # Sort keys so we have a stable order of items for testing. + # Alternative would be SortedDict, but is >=2.7 + for k in sorted(values.keys()): + el = ET.SubElement(parent, "{{{0}}}{1}".format(self.NS, name)) + el.set("rel", k) + el.text = values[k] + +# Avoid namespace prefixes, VLC doesn't like it +if hasattr(ET, 'register_namespace'): + ET.register_namespace('', XspfBase.NS) + +# in-place prettyprint formatter +# From http://effbot.org/zone/element-lib.htm +def indent(elem, level=0): + i = "\n" + level*" " + if len(elem): + if not elem.text or not elem.text.strip(): + elem.text = i + " " + if not elem.tail or not elem.tail.strip(): + elem.tail = i + for elem in elem: + indent(elem, level+1) + if not elem.tail or not elem.tail.strip(): + elem.tail = i + else: + if level and (not elem.tail or not elem.tail.strip()): + elem.tail = i + +class Xspf(XspfBase): + def __init__(self, obj={}, **kwargs): + self.version = "1" + + self._title = "" + self._creator = "" + self._info = "" + self._annotation = "" + self._location = "" + self._identifier = "" + self._image = "" + self._date = "" + self._license = "" + self._attributions = [] + self._link = {} + self._meta = {} + + self._trackList = [] + + if len(obj): + if "playlist" in obj: + obj = obj["playlist"] + for k, v in list(obj.items()): + setattr(self, k, v) + + if len(kwargs): + for k, v in list(kwargs.items()): + setattr(self, k, v) + + @property + def title(self): + """A human-readable title for the playlist. Optional""" + return self._title + @title.setter + def title(self, title): + self._title = title + + @property + def creator(self): + """Human-readable name of the entity (author, authors, group, company, etc) + that authored the playlist. Optional""" + return self._creator + @creator.setter + def creator(self, creator): + self._creator = creator + + @property + def annotation(self): + """A human-readable comment on the playlist. This is character data, + not HTML, and it may not contain markup. Optional""" + return self._annotation + @annotation.setter + def annotation(self, annotation): + self._annotation = annotation + + @property + def info(self): + """URI of a web page to find out more about this playlist. Optional""" + return self._info + @info.setter + def info(self, info): + self._info = info + + @property + def location(self): + """Source URI for this playlist. Optional""" + return self._location + @location.setter + def location(self, location): + self._location = location + + @property + def identifier(self): + """Canonical ID for this playlist. Likely to be a hash or other + location-independent name. Optional""" + return self._identifier + @identifier.setter + def identifier(self, identifier): + self._identifier = identifier + + @property + def image(self): + """URI of an image to display in the absence of a trackList/image + element. Optional""" + return self._image + @image.setter + def image(self, image): + self._image = image + + @property + def date(self): + """Creation date (not last-modified date) of the playlist. Optional""" + return self._date + @date.setter + def date(self, date): + self._date = date + + @property + def license(self): + """URI of a resource that describes the license under which this + playlist was released. Optional""" + return self._license + @license.setter + def license(self, license): + self._license = license + + @property + def meta(self): + return self._meta + + def add_meta(self, key, value): + """Add a meta element to the playlist.""" + self._meta[key] = value + + def del_meta(self, key): + """Remove a meta element.""" + del self._meta[key] + + def add_link(self, key, value): + """Add a link element to the playlist.""" + self._link[key] = value + + def del_link(self, key): + """Remove a link element.""" + del self._link[key] + + def add_attribution(self, location, identifier): + self.attrbutions.append((location, identifier)) + + def truncate_attributions(self, numattributions): + self.attrbutions = self.attributions[-numattributions:] + + # Todo: Attribution, Link, Meta, Extension + + def add_extension(self, application): + pass + + def make_extension_element(self, namespace, name, attributes, value): + pass + + def remove_extension(self, application): + pass + + @property + def track(self): + return self._trackList + @track.setter + def track(self, track): + self.add_track(track) + + def add_track(self, track={}, **kwargs): + if isinstance(track, list): + for t in track: + self.add_track(t) + elif isinstance(track, Track): + self._trackList.append(track) + elif isinstance(track, dict) and len(track) > 0: + self._trackList.append(Track(track)) + elif len(kwargs) > 0: + self._trackList.append(Track(kwargs)) + + def add_tracks(self, tracks): + for t in tracks: + self.add_track(t) + + def toXml(self, encoding="utf-8", pretty_print=True): + root = ET.Element("{{{0}}}playlist".format(self.NS)) + root.set("version", self.version) + + self._addAttributesToXml(root, ["title", "info", "creator", "annotation", + "location", "identifier", "image", "date", "license"]) + + self._addDictionaryElements(root, "link", self._link) + self._addDictionaryElements(root, "meta", self._meta) + + if len(self._trackList): + track_list = ET.SubElement(root, "{{{0}}}trackList".format(self.NS)) + for track in self._trackList: + track_list = track.getXmlObject(track_list) + if pretty_print: + indent(root) + return ET.tostring(root, encoding) + +class Track(XspfBase): + def __init__(self, obj={}, **kwargs): + self._location = "" + self._identifier = "" + self._title = "" + self._creator = "" + self._annotation = "" + self._info = "" + self._image = "" + self._album = "" + self._trackNum = "" + self._duration = "" + self._link = {} + self._meta = {} + + if len(obj): + for k, v in list(obj.items()): + setattr(self, k, v) + + if len(kwargs): + for k, v in list(kwargs.items()): + setattr(self, k, v) + + @property + def location(self): + """URI of resource to be rendered. Probably an audio resource, but MAY be any type of + resource with a well-known duration. Zero or more""" + return self._location + @location.setter + def location(self, location): + self._location = location + + @property + def identifier(self): + """ID for this resource. Likely to be a hash or other location-independent name, + such as a MusicBrainz identifier. MUST be a legal URI. Zero or more""" + return self._identifier + @identifier.setter + def identifier(self, identifier): + self._identifier = identifier + + @property + def title(self): + """Human-readable name of the track that authored the resource which defines the + duration of track rendering. Optional""" + return self._title + @title.setter + def title(self, title): + self._title = title + + @property + def creator(self): + """Human-readable name of the entity (author, authors, group, company, etc) that authored + the resource which defines the duration of track rendering.""" + return self._creator + @creator.setter + def creator(self, creator): + self._creator = creator + + @property + def annotation(self): + """A human-readable comment on the track. This is character data, not HTML, + and it may not contain markup.""" + return self._annotation + @annotation.setter + def annotation(self, annotation): + self._annotation = annotation + + @property + def info(self): + """URI of a place where this resource can be bought or more info can be found. Optional""" + return self._info + @info.setter + def info(self, info): + self._info = info + + @property + def image(self): + """URI of an image to display for the duration of the track. Optional""" + return self._image + @image.setter + def image(self, image): + self._image = image + + @property + def album(self): + """Human-readable name of the collection from which the resource which defines + the duration of track rendering comes. Optional""" + return self._album + @album.setter + def album(self, album): + self._album = album + + @property + def trackNum(self): + """Integer with value greater than zero giving the ordinal position of the media + on the album. Optional""" + return self._trackNum + @trackNum.setter + def trackNum(self, trackNum): + self._trackNum = trackNum + + @property + def duration(self): + """The time to render a resource, in milliseconds. Optional""" + return self._duration + @duration.setter + def duration(self, duration): + self._duration = duration + + @property + def meta(self): + return self._meta + + def add_meta(self, key, value): + """Add a meta element to the playlist.""" + self._meta[key] = value + + def del_meta(self, key): + """Remove a meta element.""" + del self._meta[key] + + def add_link(self, key, value): + """Add a link element to the playlist.""" + self._link[key] = value + + def del_link(self, key): + """Remove a link element.""" + del self._link[key] + + # Todo: Link, Meta, Extension + + def getXmlObject(self, parent): + track = ET.SubElement(parent, "{{{0}}}track".format(self.NS)) + + self._addAttributesToXml(track, ["location", "identifier", "title", "creator", + "annotation", "info", "image", "album", + "trackNum", "duration"]) + + self._addDictionaryElements(track, "link", self._link) + self._addDictionaryElements(track, "meta", self._meta) + + return parent + +Spiff = Xspf