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.py
cgi.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