Benutzer-Werkzeuge

Webseiten-Werkzeuge


en:scytheman:zeugs:code:scaicha

↑ up

scaicha

Note: scaicha is now hosted on GitHub.

Description

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.

Download

Requirements

  • Python (>=2.5)
    • pycha (shipped, also needs cairo bindings)
  • last.fm account with a sufficient number of submitted songs
  • convert from imagemagick (not really required, may be removed from source code)

Code

example

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)

Usage

standard mode

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

some examples

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

CGI mode

  • enable CGI at your webserver
  • copy scaicha.py, main.py and settings.py in your CGI directory
  • set CGI to True in settings.py
  • make sure your webserver is able to create new files inside the CGI directory in order to use the cache
  • place the cgi.html document in the webserver's html directory and adapt the location of main.py in the form
  • open cgi.html in your browser

Changelog

2012-04-30 0.5

  • added option to combine different tags
  • moved to pycha 0.6.0, requires no patching anymore
  • replaced default random color picking by rainbow colors
  • added configuration option to choose between rainbow colors and color gradient
  • added configuration option to choose base color and lighten factor
  • added configuration option to set minimum tag percentage for 'other tags'
  • added simple CGI example
  • minor fix in tag percentage calculation algorithm

2009-01-08 0.4

  • added score calculation algorithm
  • more object oriented code

2008-12-26 0.3

  • added tag ignoring option
  • tags converted to lowercase

2008-12-24 0.2

  • added artist and tag cache
  • better calculation algorithm
  • added CGI support
en/scytheman/zeugs/code/scaicha.txt · Zuletzt geändert: 2014/03/01 17:13 (Externe Bearbeitung)