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()