Merge 923d11311a
into db47818741
commit
ee031f9404
13
README.md
13
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 `user-follow-read user-library-read playlist-read-private playlist-read-collaborative` permission) and pass it with the `--token` option.
|
||||
|
|
|
@ -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:
|
||||
|
||||
|
@ -20,7 +16,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 +30,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 +42,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 +112,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('%H:%M:%S'), str).encode(sys.stdout.encoding, errors='replace'))
|
||||
sys.stdout.flush()
|
||||
|
||||
def main():
|
||||
|
@ -120,51 +122,141 @@ 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('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))
|
||||
|
||||
# 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)
|
||||
# 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')
|
||||
|
||||
# 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)
|
||||
# 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']))
|
||||
|
||||
# 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), 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()
|
||||
|
|
|
@ -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
|
Loading…
Reference in New Issue