Note: scaicha is now hosted on GitHub.
scaicha is a python script for pie chart generation. It generates a music tag chart for an arbitrary last.fm user.
The Data is gathered with the help of the last.fm API and charts are drawn using pycha.
scaicha_0.5.tar.bz2 (30 KB)
scaicha.py:
# scaicha.py: last.fm music tag pie chart generator # # Copyright (C) 2008-2009,2012 Alexander Heinlein <alexander.heinlein@web.de> # Copyright (C) 2008-2009 Daemon Hell # # 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 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # 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, write to the Free Software # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, # USA # VER = '0.5' from settings import * # xml reading: import urllib # newer python? #from lxml import etree # python 2.5: import xml.etree.cElementTree as etree # pycha: import sys import cairo import pycha.pie import pycha.bar import os import time import math from operator import itemgetter basicColors = dict( yellow='#ffff00', fuchsia='#ff00ff', red='#ff0000', silver='#c0c0c0', gray='#808080', olive='#808000', purple='#800080', maroon='#800000', aqua='#00ffff', lime='#00ff00', teal='#008080', green='#008000', blue='#0000ff', navy='#000080', black='#000000') class scaicha: """ scaicha main class """ def __init__(self): self.username = '' self.period = 'overall' self.min_tag_perc = 1.0 self.color_scheme = 'rainbow' self.base_color = '#c80000' self.color_lighten_fac = 0.0 self.ignored_tags = [] self.combined_tags = [] self.score = False self.filename = '' self.size = 0 self.tag_count = 0.0 self.total_play_count = 0 self.other_tags = 0 # in percent # create cache directory if set and not existing if not os.path.exists(cache_dir): os.mkdir(cache_dir) def set_username(self, name): self.username = name def set_period(self, period): if period in ('3', '6', '12'): self.period = period + 'month' elif period == 'overall': self.period = 'overall' else: raise RuntimeError, 'invalid period specified' def set_ignore_tags(self, tags): self.ignored_tags = tags def set_combine_tags(self, tags): self.combined_tags = tags def set_min_tag_perc(self, perc): if perc >= 0.0 and perc < 100.0: self.min_tag_perc = perc else: raise RuntimeError, "minimum tag percentage must be >=0.0 and <100.0" def set_color_scheme(self, scheme): if scheme in ('rainbow', 'gradient'): self.color_scheme = scheme else: raise RuntimeError, 'invalid color scheme specified' def set_base_color(self, color): if color[0] == "#": # hex color given self.base_color = color elif color.lower() in basicColors: # no hex color, check for known name self.base_color = basicColors[color.lower()] else: raise RuntimeError, 'unknown color specified' def set_color_lighten_fac(self, fac): if fac >= 0.0 and fac <= 1.0: self.color_lighten_fac = fac else: raise RuntimeError, 'color lighten factor must be >= 0.0 and <=1.0' def set_size(self, size): self.size = size def set_score(self): self.score = True def get_filename(self): if not self.filename: self.filename = '%s_%s_pie.png' % (self.username, self.period) self.filename_score = '%s_%s_score.png' % (self.username, self.period) return self.filename def get_artists(self): # file name of user cache, no slashes allowed cache_file = ('%s%s_%s.user.cache' % (cache_dir, self.username.replace('/', '_').encode("utf-8"), self.period)) if os.path.exists(cache_file) == False \ or time.time() - os.path.getmtime(cache_file ) > cache_time \ or os.path.getsize(cache_file) == 0: if not CGI: print "downloading top artists for", self.username tree = etree.parse(urllib.urlopen(ART_URL % (self.username, self.period))) cache = open (cache_file,'w') for st in urllib.urlopen(ART_URL % (self.username,self.period)): cache.write(st) cache.close() elif not CGI: print 'using top artists from cache' cache = open(cache_file,'r') tree = etree.parse(cache) cache.close() artists = list() iter = tree.getiterator() for element in iter: if (element.tag == 'name'): name = element.text if (element.tag == 'playcount'): artists.append((name, element.text)) self.total_play_count += int(element.text) return artists def get_tags(self, artists): tags = dict() for entry in artists: # get artist name and play count art_name = entry[0].replace(' ', '+').replace('/','+') art_play_count = int(entry[1]) # file name of artist cache, no slashes allowed cache_file = ('%s%s.artist.cache' % (cache_dir, art_name.encode("utf-8"))) if os.path.exists(cache_file) == False \ or time.time() - os.path.getmtime(cache_file) > cache_time \ or os.path.getsize(cache_file) == 0: if not CGI: print "downloading tag data for", entry[0].encode("utf-8") cache = open (cache_file,'w') # get artist xml document for st in urllib.urlopen((TAG_URL % art_name).encode("utf-8")): cache.write(st) cache.close() elif not CGI: print 'using tag data for', entry[0].encode("utf-8"), 'from cache' cache = open (cache_file,'r') tree = etree.parse(cache) cache.close() # as the tag numbers from last.fm are a rather stupid value (neither an absolute value nor a real ratio) # we need to sum up all tag values per artist first in order to calculate the tag percentage iter = tree.getiterator() total_tag_values = 0 for element in iter: if ((element.tag == 'count') and (element.text != '0')): total_tag_values += int(element.text) # now calculate the tag percentage and generate the tag list name = None for element in iter: if (element.tag == 'name'): name = element.text.replace('-','').lower() if ((element.tag == 'count') and (element.text != '0')): art_tag_perc = int(element.text) / float(total_tag_values) * 100.0 art_play_perc = (art_play_count / float(self.total_play_count)) * 100.0 tag_value = art_tag_perc * float(art_play_perc) / 100.0 if name in tags: tags[name] += tag_value else: tags[name] = tag_value self.tag_count += tag_value name = None return tags def draw_pie_chart(self, tags): if not CGI: print 'drawing chart' surface = cairo.ImageSurface(cairo.FORMAT_ARGB32, 440, 1500) # sort tags by their count tagList = sorted(tags.iteritems(), key = itemgetter(1)) # reverse to draw most occuring tag first tagList.reverse() dataSet = [("{0} ({1:03.1f}%)".format(tag, count), [[0, count]]) for tag, count in tagList if tag != "other tags"] # insert 'other tags' at the end dataSet.append(("{0} ({1:03.1f}%)".format('other tags', self.other_tags), [[0, self.other_tags]])) # lighten base color if requested init_color = self.base_color if self.color_lighten_fac != 0.0: r, g, b = pycha.color.hex2rgb(self.base_color) init_color = pycha.color.lighten(r, g, b, self.color_lighten_fac) options = { 'axis': { 'x': { 'ticks': [dict(v=i, label=d[0]) for i, d in enumerate(tagList)], 'hide' : True, }, 'y': { 'hide' : True, }, }, 'background': {'hide': True}, 'padding': { 'left': 0, 'right': 0, 'top': 0, 'bottom': 1000, }, 'colorScheme': { 'name': self.color_scheme, 'args': { 'initialColor': init_color, }, }, 'legend': { 'hide': False, 'opacity' : 0.5, 'borderColor' : '#ffffff', 'position': { 'top': 390, 'left' : 140 } }, } chart = pycha.pie.PieChart(surface, options) chart.addDataset(dataSet) chart.render() surface.write_to_png(self.get_filename()) def calculate_score(self, tags): prescore1 = 0 words = dict() for tag in tags.keys(): prescore1 += tags[tag]**2 for word in tag.split(' '): if word in words: words[word] += 1 else: words[word] = 1 prescore1 = math.sqrt(prescore1) / (sum(tags.values())) prescore = math.sqrt(sum(words.values()) / float(len(words))) return 150 * math.exp(-3 * (prescore * prescore1)**2) def draw_score(self, score): if not CGI: print 'drawing score' score = int(score) surface = cairo.ImageSurface(cairo.FORMAT_ARGB32, 200, 50) ctx = cairo.Context(surface) ctx.set_source_rgb(0, 0, 0) for x in range(200): ctx.rectangle(0.25 + x, 0.25, 1, 10) ctx.set_source_rgb(x * 0.005, x * 0.005, x * 0.005) ctx.fill() ctx.set_source_rgb(0, 0, 0) ctx.move_to(27, 30) ctx.set_font_size(15) ctx.show_text('your scaicha score is') ctx.set_source_rgb(1, 0, 0) if score > 99: ctx.move_to(88, 45) else: ctx.move_to(95, 45) ctx.show_text(str(score)) ctx.set_source_rgb(1, 0, 0) ctx.rectangle(0.25 + score * 1.33, 0.25, 2, 15) ctx.fill() surface.write_to_png(self.filename_score) def combine_tags(self, tags): for combination in self.combined_tags: group = combination[0] for tag in combination[1:]: if tag not in tags: continue if group in tags: tags[group] += tags[tag] else: tags[group] = tags[tag] del tags[tag] return tags def trim_tags(self, tags): """ removes tags to be ignored and adds tags with less than min_tag_perc to other tags """ for tag in tags.keys(): if tag in self.ignored_tags: self.tag_count -= tags[tag] del tags[tag] for tag in tags.keys(): if tags[tag] < self.min_tag_perc: self.other_tags += tags[tag] del tags[tag] return tags def run(self): if not self.username: raise RuntimeError, 'no username specified' arts = self.get_artists() if len(arts) == 0: if not CGI: print "error: no artists found" return tags = self.get_tags(arts) tags = self.combine_tags(tags) tags = self.trim_tags(tags) filename = self.get_filename() self.draw_pie_chart(tags) # crop image border os.popen('convert -trim -page +0+0 ./%s ./%s' % (filename, filename)) # draw username and date to image os.popen('DATE=$(date "+%%F"); convert -pointsize 11 -rotate 90 -draw "gravity SouthWest text 0,0 \\"%s %s $DATE by scaicha\\"" -rotate -90 ./%s ./%s' % (self.username, self.period, filename, filename)) if self.score: score = self.calculate_score(tags) self.draw_score(score) os.popen('montage -tile 1x -background none -geometry +0+0 ./%s ./%s ./%s' % (self.filename_score, filename, filename)) if self.size > 0: os.popen('convert -resize %s ./%s ./%s' %(self.size, filename, filename))
main.py:
#!/usr/bin/python # # main.py: configuring and executing scaicha # # Copyright (C) 2008-2009,2012 Alexander Heinlein <alexander.heinlein@web.de> # Copyright (C) 2008-2009 Daemon Hell # # 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 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # 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, write to the Free Software # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, # USA # from scaicha import * from settings import * import getopt import sys if CGI: import cgitb cgitb.enable() import cgi def usage(name): if CGI: return print 'usage:', name, '-u [OPTIONS]' print ' -u <arg>, --user <arg> last.fm user name (required)' print ' -p <arg>, --period <arg> period of top artists (3, 6, 12; default: overall)' print ' -i <arg>, --ignore <arg> comma separated list of tags to ignore, e.g. "hip hop,rap"' print ' -j <arg>, --join <arg> combines a list of tag groups. groups are separated by commas, tags by colon' print ' -m <arg>, --minTagPerc <arg> minimum tag percentage (default: 1.0), less occuring tags will be merged into other tags' print ' -c <arg>, --colorScheme <arg> color scheme to use (rainbow (default) or gradient)' print ' -b <arg>, --baseColor <arg> base color to use (hex string or a HTML 4.0 color name)' print ' -l <arg>, --lighten <arg> lighten base color by given factor (between 0.0 and 1.0)' print ' -r <arg>, --resize <arg> resize image' print ' -s , --score draw score' print ' -h , --help print this help and exit' print def split_ignore_tags(tags): """ splits a comma separated string of tags to ignore """ ignore_list = [] for tag in tags.split(","): ignore_list.append(tag.strip().lower()) ignore_list.sort() return ignore_list def split_combine_tags(tags): """ splits a comma separated string of colon separated tags to combine """ combine_list = [] for list in tags.split(","): group_list = [] for tag in list.split(":"): group_list.append(tag.strip().lower()) combine_list.append(group_list) return combine_list def run_standalone(s): print 'scaicha version', VER try: # get command line arguments opts, args = getopt.getopt(sys.argv[1:], 'u:p:i:j:m:c:b:l:r:sh', ['user=', 'period=', 'ignore=', 'join=', 'minTagPerc=', 'colorScheme=', 'baseColor=', 'lighten=', 'resize=', 'score', 'help']) except getopt.GetoptError: usage(sys.argv[0]) raise RuntimeError, 'invalid argument specified' username = False for opt, arg in opts: if opt in ('-u', '--user'): s.set_username(arg) username = True elif opt in ('-p', '--period'): s.set_period(arg) elif opt in ('-i', '--ignore'): s.set_ignore_tags(split_ignore_tags(arg)) elif opt in ('-j', '--join'): s.set_combine_tags(split_combine_tags(arg)) elif opt in ('-m', '--minTagPerc'): s.set_min_tag_perc(float(arg)) elif opt in ('-c', '--colorScheme'): s.set_color_scheme(arg) elif opt in ('-b', '--baseColor'): s.set_base_color(arg) elif opt in ('-l', '--lighten'): s.set_color_lighten_fac(float(arg)) elif opt in ('-r', '--resize'): if not arg.isdigit(): raise RuntimeError, 'invalid number for size specified' else: s.set_size(arg) elif opt in ('-s', '--score'): s.set_score() elif opt in ('-h', '--help'): usage(sys.argv[0]) sys.exit(1) if not username: usage(sys.argv[0]) raise RuntimeError, 'no username specified' sys.exit(1) if DEV \ or os.path.exists(s.get_filename()) == False \ or (time.time() - os.path.getmtime(s.get_filename())) > cache_time \ or os.path.getsize(s.get_filename()) == 0: s.run() print 'chart written to', s.get_filename() def run_CGI(s): args = cgi.parse() username = args['name'][0] if not username: raise RuntimeError, 'no username specified' else: s.set_username(username) if 'period' in args: s.set_period(args['period'][0]) if 'ignore' in args: s.set_ignore_tags(split_ignore_tags(args['ignore'][0])) if 'join' in args: s.set_combine_tags(split_combine_tags(args['join'][0])) if 'minTagPerc' in args: s.set_min_tag_perc(float(args['minTagPerc'][0])) if 'colorScheme' in args: s.set_color_scheme(args['colorScheme'][0]) if 'baseColor' in args: s.set_base_color(args['baseColor'][0]) if 'lighten' in args: s.set_color_lighten_fac(float(args['lighten'][0])) if 'size' in args: s.set_size(args['size'][0]) if 'score' in args: s.set_score() if DEV \ or os.path.exists(s.get_filename()) == False \ or (time.time() - os.path.getmtime(s.get_filename())) > cache_time \ or os.path.getsize(s.get_filename()) == 0: s.run() image = open(s.get_filename(), 'r') print 'Content-Type: image/png\r\n' print image.read() image.close() if __name__ == '__main__': s = scaicha() if not CGI: run_standalone(s) else: run_CGI(s)
generate a pie chart using the default options:
$ python main.py -u username
there are several options to configure the behaviour of scaicha:
-u <arg>, --user <arg> last.fm user name (required) -p <arg>, --period <arg> period of top artists (3, 6, 12; default: overall) -i <arg>, --ignores <arg> comma separated list of tags to ignore, e.g. "hip hop,rap" -j <arg>, --join <arg> combines a list of tag groups. groups are separated by commas, tags by colon -m <arg>, --minTagPerc <arg> minimum tag percentage (default: 1.0), less occuring tags will be merged into other tags -c <arg>, --colorScheme <arg> color scheme to use (rainbow (default) or gradient) -b <arg>, --baseColor <arg> base color to use (hex string or a HTML 4.0 color name) -l <arg>, --lighten <arg> lighten base color by given factor (between 0.0 and 1.0) -r <arg>, --resize <arg> resize image -s , --score draw score
use only the top artists from the past three months:
python main.py -u KarlKartoffel -p 3
define some tags to ignore:
python main.py -u WilliamKidd -i "hip hop,french"
combine some tags, here: combine 'classic rock' and 'hard rock' into 'rock':
python main.py -u CaptainFarell -j "rock:classic rock:hard rock"
combine even more tags (note the usage of ':' and ','):
python main.py -u CaptainFarell -j "rock:classic rock:hard rock,metal:heavy metal:power metal"
you can even create new tags or rename existing ones if you like:
python main.py -u CaptainFarell -j "good stuff:rock:metal,bad stuff:hiphop:rap,worst crap ever:indie"
draw a color gradient instead of the default rainbow colors:
python main.py -u Pferdinand -c gradient
use another base color:
python main.py -u AverageJoe -b "#7d00ff"
python main.py -u JohnSmith -b maroon
lighten base color slightly:
python main.py -u AaronAronsen -l 0.3
note: all parameters can be combined
scaicha.py, main.py and settings.py in your CGI directoryCGI to True in settings.pycgi.html document in the webserver's html directory and adapt the location of main.py in the formcgi.html in your browser2012-04-30 0.5
2009-01-08 0.4
2008-12-26 0.3
2008-12-24 0.2