Skip to content

Instantly share code, notes, and snippets.

What would you like to do?
lastfm scrobble exporter (from // changed the script to try more often on failure )
#!/usr/bin/env python
#-*- coding: utf-8 -*-
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# GNU General Public License for more details.
# You should have received a copy of the GNU General Public License
# along with this program. If not, see <>.
Script for exporting tracks through audioscrobbler API.
Usage: -u USER [-o OUTFILE] [-p STARTPAGE] [-s SERVER]
import urllib2, urllib, sys, time, re
import xml.etree.ElementTree as ET
from optparse import OptionParser
__version__ = '0.0.4'
def get_options(parser):
""" Define command line options."""
parser.add_option("-u", "--user", dest="username", default=None,
help="User name.")
parser.add_option("-o", "--outfile", dest="outfile", default="exported_tracks.txt",
help="Output file, default is exported_tracks.txt")
parser.add_option("-p", "--page", dest="startpage", type="int", default="1",
help="Page to start fetching tracks from, default is 1")
parser.add_option("-s", "--server", dest="server", default="",
help="Server to fetch track info from, default is")
parser.add_option("-t", "--type", dest="infotype", default="scrobbles",
help="Type of information to export, scrobbles|loved|banned, default is scrobbles")
options, args = parser.parse_args()
if not options.username:
sys.exit("User name not specified, see --help")
if options.infotype == "loved":
infotype = "lovedtracks"
elif options.infotype == "banned":
infotype = "bannedtracks"
infotype = "recenttracks"
return options.username, options.outfile, options.startpage, options.server, infotype
def connect_server(server, username, startpage, sleep_func=time.sleep, tracktype='recenttracks'):
""" Connect to server and get a XML page."""
if server == "":
baseurl = ''
urlvars = dict(method='user.get%s' % tracktype,
api_key=('' % __version__).ljust(32, '-'),
elif server == "":
baseurl = ''
urlvars = dict(method='user.get%s' % tracktype,
if server[:7] != 'http://':
server = 'http://%s' % server
baseurl = server + '/2.0/?'
urlvars = dict(method='user.get%s' % tracktype,
api_key=('' % __version__).ljust(32, '-'),
url = baseurl + urllib.urlencode(urlvars)
for interval in (1, 5, 10, 62, 240):
f = urllib2.urlopen(url)
except Exception, e:
last_exc = e
print "Exception occured, retrying in %ds: %s" % (interval, e)
print "Failed to open page %s" % urlvars['page']
raise last_exc
response =
#bad hack to fix bad xml
response = re.sub('\xef\xbf\xbe', '', response)
return response
def get_pageinfo(response, tracktype='recenttracks'):
"""Check how many pages of tracks the user have."""
xmlpage = ET.fromstring(response)
totalpages = xmlpage.find(tracktype).attrib.get('totalPages')
return int(totalpages)
def get_tracklist(response):
"""Read XML page and get a list of tracks and their info."""
xmlpage = ET.fromstring(response)
tracklist = xmlpage.getiterator('track')
return tracklist
def parse_track(trackelement):
"""Extract info from every track entry and output to list."""
if trackelement.find('artist').getchildren():
#artist info is nested in loved/banned tracks xml
artistname = trackelement.find('artist').find('name').text
artistmbid = trackelement.find('artist').find('mbid').text
artistname = trackelement.find('artist').text
artistmbid = trackelement.find('artist').get('mbid')
if trackelement.find('album') is None:
#no album info for loved/banned tracks
albumname = ''
albummbid = ''
albumname = trackelement.find('album').text
albummbid = trackelement.find('album').get('mbid')
trackname = trackelement.find('name').text
trackmbid = trackelement.find('mbid').text
date = trackelement.find('date').get('uts')
output = [date, trackname, artistname, albumname, trackmbid, artistmbid, albummbid]
for i, v in enumerate(output):
if v is None:
output[i] = ''
return output
def write_tracks(tracks, outfileobj):
"""Write tracks to an open file"""
for fields in tracks:
outfileobj.write(("\t".join(fields) + "\n").encode('utf-8'))
def get_tracks(server, username, startpage=1, sleep_func=time.sleep, tracktype='recenttracks'):
page = startpage
response = connect_server(server, username, page, sleep_func, tracktype)
totalpages = get_pageinfo(response, tracktype)
if startpage > totalpages:
raise ValueError("First page (%s) is higher than total pages (%s)." % (startpage, totalpages))
while page <= totalpages:
#Skip connect if on first page, already have that one stored.
if page > startpage:
response = connect_server(server, username, page, sleep_func, tracktype)
tracklist = get_tracklist(response)
tracks = []
for trackelement in tracklist:
# do not export the currently playing track.
if not trackelement.attrib.has_key("nowplaying") or not trackelement.attrib["nowplaying"]:
yield page, totalpages, tracks
page += 1
def main(server, username, startpage, outfile, infotype='recenttracks'):
trackdict = dict()
page = startpage # for case of exception
totalpages = -1 # ditto
n = 0
for page, totalpages, tracks in get_tracks(server, username, startpage, tracktype=infotype):
print "Got page %s of %s.." % (page, totalpages)
for track in tracks:
if infotype == 'recenttracks':
trackdict.setdefault(track[0], track)
#Can not use timestamp as key for loved/banned tracks as it's not unique
n += 1
trackdict.setdefault(n, track)
except ValueError, e:
except Exception:
with open(outfile, 'a') as outfileobj:
tracks = sorted(trackdict.values(), reverse=True)
write_tracks(tracks, outfileobj)
print "Wrote page %s-%s of %s to file %s" % (startpage, page, totalpages, outfile)
if __name__ == "__main__":
parser = OptionParser()
username, outfile, startpage, server, infotype = get_options(parser)
main(server, username, startpage, outfile, infotype)
Copy link

ademar111190 commented Feb 19, 2014


I cannot found pull requests on gist files so i wrote here a little pull request to your code, why on line 147 instead:

outfileobj.write(("\t".join(fields) + "\n").encode('utf-8'))

you not uses:

outfileobj.write(("\t".join(field.replace('\t', ' ') for field in fields) + "\n").encode('utf-8'))

it is because if the track name have any '\t' character, the result file breaks when imported to libre office so replacing the tab character by a space character you skip this problem :)

Copy link

cuppajoeman commented Aug 31, 2020

When I ran this I got the following error

>>> python 
  File "", line 87
    except Exception, e:
SyntaxError: invalid syntax

the reason was that it was using python3, you have to run it with python2 for it to work.

Copy link

artlung commented Jan 9, 2022

Following up on @cuppajoeman - with python2 installed, change the first line of the script to #!/usr/bin/env python2 from #!/usr/bin/env python

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment