Monday, March 8, 2010

Last.fm Wallpaper generator script "wallpaperfm" in Python

        I just came across a nice script (named wallpaperfm by the author) that makes a very nice wallpaper by watching your listening habits from your last.fm profile.. I didnt make the script, all credit goes to KOANT. Well you can of coarse play with the script to make it to your needs....:)

Here is the screenshot of my wallpaper..


To get the above wallpaper, I ran the following command..

./wallpaperfm.py -m collage --CanvasSize 1440x900 --AlbumNumber 100 --GradientSize 10 --AlbumOpacity 60 -u shadyabhi -i 1440x900
Play well with the options to get what you exactly want...
Source-code to the script...

#!/usr/bin/python
# Wallpaperfm.py is a python script that generates desktop wallpapers from your last.fm music profile.
# by Koant, http://www.last.fm/user/Koant
# ./wallpaper.py will display the instructions
#
# Requirements:
# . Python Imaging Library (probably already installed, available through synaptic for Ubuntu users)
# . a last.fm account and an active internet connection
#
# v. 13 July 2009

__author__ = 'Koant (http://www.last.fm/user/Koant)'
__version__ = '$13 July 2009$'
__date__ = '$Date: 2009/07/13  $'
__copyright__ = 'Copyright (c) 2008 Koant'
__license__ = 'GPL'


from urllib import urlopen
from xml.dom import minidom
import os
import os.path
import sys
from getopt import getopt
import random
import Image
import ImageDraw
import ImageFilter

def usage():
    print "Quick examples"
    print "--------------"
    print "./wallpaperfm.py -m tile -u your_lastfm_username    will generate an image with all your favorite albums tiled up in a random order."
    print "./wallpaperfm.py -m glass -u your_lastfm_username    will generate an image with a small random collection of albums, with a glassy effect."
    print "./wallpaperfm.py -m collage -u your_lastfm_username    will generate a random collage of your favorite albums."    
    
    print "\nGlobal switches:"
    print "-u, --Username: your last.fm username."
    print "-f, --Filename: the filename where the image will be saved. Username by default."
    print "-t, --Past: [overall] how far back should the profile go. One of 3month,6month,12month or overall."
    print "-O, --FinalOpacity: [80] darkness of the final image. from 0 to 100"
    print "-i, --ImageSize: [1280x1024] size of the final image. Format: numberxnumber"
    print "-c, --CanvasSize: size of the canvas. = image size by default."
    print "-e, --Cache: [wpcache] path to the cache."
    print "-x, --ExcludedList: ['http://cdn.last.fm/depth/catalogue/noimage/cover_med.gif'] excluded urls, comma separated." 
    print "-l, --Local: use a local copy of the charts. Ideal for using it offline or being kind to the last.fm servers."    

    print "\nSpecific switches for the 'tile' mode (-m tile):"
    print "-a, --AlbumSize: [130] size of the albums, in pixel."
    print "-s, --Interspace: [5]  space between in tile, in pixel."

    print "\nSpecific switches for the 'glass' mode (-m glass):"
    print "-n, --AlbumNumber: [7] number of albums to show."
    print "-d, --EndPoint: [75] controls when the shadow ends, in percentage of the album size."    
    print "-r, --Offset: [40] starting value of opacity for the shadow."

    print "\nSpecific switches for the 'collage' mode (-m collage):"    
    print "-a, --AlbumSize: [250] size of the albums, in pixel."
    print "-o, --AlbumOpacity: [90] maximum opacity of each album, from 0 to 100."
    print "-n, --AlbumNumber: [50] number of albums to show."
    print "-g, --GradientSize: [15] portion of the album in the gradient, from 0 to 100"
    print "-p, --Passes: [4] number of iterations of the algorithms."
    sys.exit()

def getSize(s):
    """ Turns '300x400' to (300,400) """
    return tuple([int(item) for item in s.rsplit('x')])

def getParameters():
    """ Get Parameters from the command line or display usage in case of problem """
    # Common Default Parameters
    Filename=''
    mode='tile'

    Profile=dict()
    Profile['Username']='Koant'
    Profile['Past']='overall'
    Profile['cache']='wpcache'
    Profile['ExcludedList']=['http://cdn.last.fm/depth/catalogue/noimage/cover_med.gif','http://cdn.last.fm/flatness/catalogue/noimage/2/default_album_medium.png']    
    Profile['Limit']=50    
    Profile['Local']='no'

    Common=dict();    
    Common['ImageSize']=(1280,1024)
    Common['CanvasSize']=''
    Common['FinalOpacity']=80

    ## Specific Default Parameters
    # Collage
    Collage=dict();
    Collage['Passes']=4
    Collage['AlbumOpacity']=90
    Collage['GradientSize']=15
    Collage['AlbumSize']=250

    # Tile
    Tile=dict()
    Tile['AlbumSize']=130
    Tile['Interspace']=5

    # Glass
    Glass=dict()
    Glass['AlbumNumber']=7
    Glass['Offset']=40
    Glass['EndPoint']=75

    try:
        optlist, args=getopt(sys.argv[1:], 'hu:t:n:c:f:a:o:g:O:i:m:p:s:e:d:r:x:l',["help", "Mode=", "Username=", "Past=", "Filename=","CanvasSize=", "ImageSize=", "FinalOpacity=", "AlbumSize=","AlbumOpacity=","GradientSize=", "Passes=", "AlbumNumber=", "Interspace=","Cache=","Offset=","EndPoint=","ExcludedList=","Local"])
    except Exception, err:
        print "#"*20
        print str(err)
        print "#"*20 
        usage()
    if len(optlist)==0:
        usage()
    for option, value in optlist:
        if option in ('-h','--help'):
            usage()    
        elif option in ('-m','--Mode'):                # m: mode, one of Tile,Glass or Collage
            mode=value.lower()

        elif option in('-e','--Cache'):                # e: cache
            Profile['cache']=value

        elif option in('-l','--Local'):                # l: use a local copy of the charts
            Profile['Local']='yes'

        elif option in ('-u','--Username'):            # u: username (Common)
            Profile['Username']=value

        elif option in ('-t','--Past'):                # t: how far back (Common), either 3month,6month or 12month
            Profile['Past']=value
        
        elif option in ('-x','--ExcludedList'):            # x: excluded url
            Profile['ExcludedList'].extend(value.rsplit(','))

        elif option in ('-f', '--Filename'):            # f: image filename (Common)
            Filename=value

        elif option in ('-c','--CanvasSize'):            # c: canvas size (Common)
            Common['CanvasSize']=getSize(value)

        elif option in ('-i','--ImageSize'):            # i: image size (Common)
            Common['ImageSize']=getSize(value)

        elif option in ('-O', '--FinalOpacity'):         # O: opacity of final image (Common)
            Common['FinalOpacity']=int(value)

        elif option in ('-a','--AlbumSize'):            # a: album size (Collage,Tile)
            Collage['AlbumSize']=int(value)
            Tile['AlbumSize']=int(value)

        elif option in ('-o','--AlbumOpacity'):            # o: album opacity (Collage)
            Collage['AlbumOpacity']=int(value)

        elif option in ('-g','--GradientSize'):            # g: gradient size (Collage)
            Collage['GradientSize']=int(value)

        elif option in ('-p','--Passes'):            # p: number of passes (Collage)
            Collage['Passes']=int(value)

        elif option in ('-n','--AlbumNumber'):            # n: number of albums (Glass, Collage)
            Glass['AlbumNumber']=int(value)
            Collage['AlbumNumber']=int(value)

        elif option in ('-s','--Interspace'):            # s: interspace (Tile)
            Tile['Interspace']=int(value)
        
        elif option in ('-d','--EndPoint'):            # d: EndPoint (Glass)
            Glass['EndPoint']=int(value)    
        
        elif option in ('-r','--Offset'):            # r: Offset (Glass)
            Glass['Offset']=int(value)
    

        else:
            print "I'm not using ", option 

    if Filename=='': # by default, Filename=Username
        Filename=Profile['Username']
    if Common['CanvasSize']=='':    # by default, CanvasSize=ImageName
        Common['CanvasSize']=Common['ImageSize']

    # Add the common parameters
    for k,v in Common.iteritems():
        Collage[k]=v
        Tile[k]=v
        Glass[k]=v

    return {'Filename':Filename, 'Mode':mode, 'Profile':Profile, 'Tile':Tile, 'Glass':Glass, 'Collage':Collage}

##############################
## Parse and download the files
##############################
def makeFilename(url):
    """ Turns the url into a filename by replacing possibly annoying characters by _ """
    url=url[7:] # remove 'http://'    
    for c in ['/', ':', '?', '#', '&','%']:
        url=url.replace(c,'_')
    return url

def download(url,filename):
    """ download the binary file at url """
    instream=urlopen(url)
    outfile=open(filename,'wb')
    for chunk in instream:
        outfile.write(chunk)
    instream.close()
    outfile.close()

def IsImageFile(imfile):
    """ Make sure the file is an image, and not a 404. """
    flag=True
    try:
        i=Image.open(imfile)
    except Exception,err:
        flag=False
    return flag

def getAlbumCovers(Username='Koant',Past='overall',cache='wp_cache',ExcludedList=['http://cdn.last.fm/depth/catalogue/noimage/cover_med.gif','http://cdn.last.fm/flatness/catalogue/noimage/2/default_album_medium.png'],Limit=50,Local='no'):
    """ download album covers if necessary """
    ## Preparing the file list.
    if Past in ('3month','6month','12month'):
        tpe='&type='+Past
    else:
        tpe=''

    url='http://ws.audioscrobbler.com/1.0/user/'+Username+'/topalbums.xml?limit='+str(Limit)+tpe
    
    # make cache if doesn't exist
    if not os.path.exists(cache):
        print "cache directory ("+cache+") doesn't exist. I'm creating it."    
        os.mkdir(cache)    
    
    # Make a local copy of the charts
    if Local=='no':    
        try:
            print "Downloading from ",url
            download(url,cache+os.sep+'charts_'+Username+'.xml')
        except Exception,err:
            print "#"*20
            print "I couldn't download the profile or make a local copy of it."
            print "#"*20
    else:
        print "Reading from local copy:  ",cache+os.sep+'charts_'+Username+'.xml'

    # Parse image filenames
    print "Parsing..."
    try:
        data=open(cache+os.sep+'charts_'+Username+'.xml','rb')
        xmldoc=minidom.parse(data)
        data.close()
    except Exception,err:
        print '#'*20
        print "Error while parsing your profile. Your username might be misspelt or your charts empty."
        print '#'*20        
        sys.exit()

    filelist=[imfile.firstChild.data for imfile in xmldoc.getElementsByTagName('large')]



    # Exclude covers from the ExcludedList
    filelist=[item for item in filelist if not item in ExcludedList]

    # Stop if charts are empty
    if len(filelist)==0:
        print '#'*20
        print "Your charts are empty. I can't proceed."
        print '#'*20
        sys.exit()

    # download covers only if not available in the cache
    for imfile in filelist[:]:
        url=imfile
        imfile=makeFilename(imfile)    
        if not os.path.exists(cache+os.sep+imfile):
            print "    Downloading ",url
            download(url,cache+os.sep+imfile)

    filelist=[cache+os.sep+makeFilename(imfile) for imfile in filelist] 

    filelist=[imfile for imfile in filelist if IsImageFile(imfile)] # Checks the file is indeed an image
    
    return filelist

##############################
## Tile
##############################
def Tile(Profile,ImageSize=(1280,1024),CanvasSize=(1280,1024),AlbumSize=130,FinalOpacity=30,Interspace=5):
    """ produce a tiling of albums covers """

    imagex,imagey=ImageSize
    canvasx,canvasy=CanvasSize

    offsetx=(imagex-canvasx)/2
    offsety=(imagey-canvasy)/2
    
    #number of albums on rows and columns
    nx=(canvasx-Interspace)/(AlbumSize+Interspace) 
    ny=(canvasy-Interspace)/(AlbumSize+Interspace)
    
    # number of images to download
    Profile['Limit']=ny*nx+len(Profile['ExcludedList'])+5 # some extra in case of 404 , even though there shouldn't be any really.

    # download images
    filelist=getAlbumCovers(**Profile)

    background=Image.new('RGB',(imagex,imagey),0) # background

    filelist2=list()
    posy=-AlbumSize+(canvasy-ny*(AlbumSize+Interspace)-Interspace)/2
    for j in range(0,ny):
        posx,posy=(-AlbumSize+(canvasx-nx*(AlbumSize+Interspace)-Interspace)/2,posy+Interspace+AlbumSize) # location of album in the canvas
        for i in range(0,nx):
            posx=posx+Interspace+AlbumSize
            if len(filelist2)==0: # better than random.choice() (minimises risk of doubles and goes through the whole list) 
                filelist2=list(filelist)
                random.shuffle(filelist2)
            imfile=filelist2.pop()
            try:
                im=Image.open(imfile).convert('RGB')
            except Exception,err:
                print "#"*20
                print err
                print "I couln't read that file: "+imfile
                print "You might want to exclude its corresponding URL with -x because it probably doesn't point to an image."
                print "#"*20                
                sys.exit()
            im=im.resize((AlbumSize,AlbumSize),2)        
            background.paste(im,(posx+offsetx,posy+offsety))        
        
    # darken the result
    background=background.point(lambda i: FinalOpacity*i/100)
    return background

##############################
## Glassy wallpaper
##############################
def makeGlassMask(ImageSize,Offset=50,EndPoint=75):
    """ Make mask for the glassy wallpaper """
    mask=Image.new('L',ImageSize,0)
    di=ImageDraw.Draw(mask)
    sizex,sizey=ImageSize
        
    stop=min((EndPoint*sizey)/100,sizey)
    E=EndPoint*sizey/100
    O=255*Offset/100
    for i in range(0,stop):
        color=(255*Offset/100*-100*i)/(EndPoint*sizey)+255*Offset/100 #linear gradient        
        #color=((i-E)*(i-E)*O)/(E*E) # quadratic gradient        
        #color=(O*(E*E-i*i))/(E*E)
        di.line((0,i,sizex,i),color)        
    return mask

def Glass(Profile, ImageSize=(1280,1024),CanvasSize=(1280,1024),AlbumNumber=7,FinalOpacity=100,Offset=50,EndPoint=75):
    """ Make a glassy wallpaper from album covers """     

    if AlbumNumber>Profile['Limit']:
        Profile['Limit']=AlbumNumber+len(Profile['ExcludedList'])+5
    
    filelist=getAlbumCovers(**Profile)
    imagex,imagey=ImageSize
        
    canvasx,canvasy=CanvasSize

    offsetx=(imagex-canvasx)/2
    offsety=(imagey-canvasy)/2

    background=Image.new('RGB',(imagex,imagey),0) # background

    albumsize=canvasx/AlbumNumber
    mask=makeGlassMask((albumsize,albumsize),Offset,EndPoint)    

    posx=(canvasx-AlbumNumber*albumsize)/2-albumsize
    
    for i in range(0,AlbumNumber):
        imfile=filelist.pop()    # assumes there are enough albums in the filelist
        tmpfile=Image.open(imfile).convert('RGB')
        tmpfile=tmpfile.resize((albumsize,albumsize),2) # make it square, prettier
        posx,posy=(posx+albumsize,canvasy/2-albumsize)
        background.paste(tmpfile,(posx+offsetx,posy+offsety)) # paste the album cover
        tmpfile=tmpfile.transpose(1)                #turn it upside down
        background.paste(tmpfile,(posx+offsetx,canvasy/2+offsety),mask)    # apply mask and paste
    # darken the result
    background=background.point(lambda i: FinalOpacity*i/100)
    return background

############################ 
## Collage
############################
def erfc(x):
    """ approximate erfc with a few splines """
    if x<-2:
        return 2;
    elif (-2<=x) and (x<-1):
        c=[ 0.9040,   -1.5927,   -0.7846,   -0.1305];
    elif (-1<=x) and (x<0):
        c=[1.0000, -1.1284,   -0.1438,    0.1419];
    elif (0<=x) and (x<1):
        c=[1.0000,   -1.1284 ,   0.1438,    0.1419];
    elif (1<=x) and (x<2):
        c=[1.0960,   -1.5927,    0.7846 ,  -0.1305];
    else:
        return 0;
    return c[0]+c[1]*x+c[2]*x*x+c[3]*x*x*x;    

def makeCollageMask(size,transparency,gradientsize):
    mask=Image.new('L',size,0)
    sizex,sizey=size
    l=(gradientsize*sizex)/100
    c=(255*transparency)/100.0
    c=c/4.0 # 4=normalizing constant from convolution
    s2=1/(l*1.4142)
    for i in range(sizex):
        for j in range(sizey):
            v=c*(erfc(s2*(l-i))-erfc(s2*(sizex-l-i)))*(erfc(s2*(l-j))-erfc(s2*(sizex-l-j)))
            mask.putpixel((i,j),int(v))
        
    return mask

def Collage(Profile,ImageSize=(1280,1024),CanvasSize=(1280,1024),AlbumNumber=50,AlbumSize=300,GradientSize=20,AlbumOpacity=70,Passes=4,FinalOpacity=70):
    """ make a collage of album covers """    

    Profile['Limit']=min(200,max(AlbumNumber,Profile['Limit'])) 

    filelist=getAlbumCovers(**Profile)

    imagex,imagey=ImageSize
    canvasx,canvasy=CanvasSize
    
    background=Image.new('RGB',(imagex,imagey),0) # background
    mask=makeCollageMask((AlbumSize,AlbumSize),AlbumOpacity,GradientSize)
    print "Computing the collage..."    
    for p in range(0,Passes):
        print "Pass ",p+1," of ",Passes    
        for imfile in filelist:
                tmpfile=Image.open(imfile).convert('RGB')
                tmpfile=tmpfile.resize((AlbumSize,AlbumSize),1)
                posx=random.randint(0,canvasx-AlbumSize)
                posy=random.randint(0,canvasy-AlbumSize)
                background.paste(tmpfile,(posx+(imagex-canvasx)/2,posy+(imagey-canvasy)/2),mask)

    # darken the result
    background=background.point(lambda i: FinalOpacity*i/100)
    return background

########################
## main
########################
def main():
    print ""
    print "    Wallpaperfm.py is a python script that generates desktop wallpapers from your last.fm musical profile."
    print "    by Koant, http://www.last.fm/user/Koant"
    print ""
    param=getParameters()
    
    print "Mode: "+param['Mode']
    print "    Image will be saved as "+param['Filename']+".jpg"
    if param['Mode']=='tile':
        for k,v in param['Tile'].iteritems():
            print "    "+k+": "+str(v)
        image=Tile(param['Profile'],**param['Tile'])        
    elif param['Mode']=='glass':
        for k,v in param['Glass'].iteritems():
            print "    "+k+": "+str(v)
        image=Glass(param['Profile'],**param['Glass'])
    elif param['Mode']=='collage':
        for k,v in param['Collage'].iteritems():
            print "    "+k+": "+str(v)
        image=Collage(param['Profile'],**param['Collage'])
    else:
        print " I don't know this mode: ", param['Mode']
        sys.exit()

    image.save(param['Filename']+'.jpg')
    print "Image saved as "+param['Filename']+'.jpg'
    
if __name__=="__main__":
    main()

No comments:

Post a Comment