What is wallpaperfm.py ?
Wallpaperfm.py is a Python script that generates desktop wallpapers from your last.fm music profile. It runs on linux but should run just as well on Mac and Windows.
Here are examples of what it does with my profile, using the default parameters:-
Inspiration: Gijsco's Last.fm Desktop Generator for the use of last.fm data, and Jamie Zawinski's webcollage, for the collage mode.
File
The script can be found here. Download it, make it executable (chmod a+x wallpaperfm.py) and run it from the command line. With no argument, wallpaperfm.py shows some examples and instructions.
[Show source]
#!/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. 02 Aug 2010
# Update on 02 Aug 2010: added filelist.reverse() # changed on 02Aug2010 on l.285
__author__ = 'Koant (http://www.last.fm/user/Koant)'
__version__ = '$02 Aug 2010$'
__date__ = '$Date: 2008/07/17 $'
__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','http://userserve-ak.last.fm/serve/174s/32868291.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
filelist.reverse() # changed on 02Aug2010
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()
Requirements
Apart from a last.fm account and an active internet connection, you only need Python and the Python Imaging Library (PIL) to run the script. Python, a powerful and beautiful scripting language, and the PIL are usually installed by default on linux systems. If not, Ubuntu users will find both of them on synaptic. Mac and Windows can also run Python and I'd love to hear from their users and know how well the script works on these systems.
Installation
Here is a quick way to install and try out wallpaperfm.py (linux and mac):
cd
mkdir wallpaperfm
cd wallpaperfm
wget http://ledazibao.free.fr/wallpaperfm/wallpaperfm.py
chmod a+x wallpaperfm.py
./wallpaperfm.py -u YOURLASTFMUSERNAME
Developpers! I'm always happy when people build on my own version (see two examples in the 'TIPS' section). The code is on github at http://github.com/Koantig/wallpaperfm as well if you want to use git.
git clone git@github.com:Koantig/wallpaperfm.git
Instructions
By default, a 1280x1024 image is produced, using the Tile mode and the list of your overall 50 favorite albums. Running ./wallpaperfm.py will show you all the parameters available. The long list of switches can be a bit daunting at first but don't be overwhelmed by it: most defaults are ok and you'll probably just want to change the mode ('tile','glass' or 'collage') and the size of the image.
Here are the parameters common to all modes. Specific parameters are described in the sections Tile, Glass and Collage
Mode
The script can generate 3 different wallpapers. Select the one you want between 'tile', 'glass' and 'collage' with '-m' or '--Mode'. The default mode is 'tile'. E.g. ./wallpaperfm.py -m glass
Image size
The size of the final image is controlled by '-i' or '--ImageSize'. Use the format numberxnumber to specify the size you want. For example:
./wallpaperfm.py -i 800x600 will produce a 800x600 pixel image.
Canvas size
The canvas is the area where the drawing is actually taking place and is controlled by '-c' or '--CanvasSize' By default, it has the same size as the image but you can change that to obtain different effects, like restricting the drawing to a small portion of the screen, or drawing outside the limit of the image.
For example:
- A canvas smaller than the image:./wallpaperfm.py -i 1280x1024 -c 800x600:[Show]
- A canvas larger than the image:./wallpaperfm.py -i 1280x1024 -c 1500x1300:[Show]
- A canvas smaller than the image on the y-axis and larger on the x-axis: ./wallpaperfm.py -i 1280x1024 -c 1500x600:[Show]
Username
Specify the username with '-u' or '--Username'. Ex.:./wallpaperfm.py -u Koant
Filename
The default filename is username.jpg. You can change to whatever you want with '-f' or '--Filename'. Ex.: ./wallpaperfm.py -f mywallpaper. The .jpg extension will be added automatically.
Profile period
By default, the program fetches your overall favorite albums. You can change that to either 3, 6 or 12 months with the '-t' or '--Past' switch. Ex.: ./wallpaperfm.py -t 3month. Use one of '3month', '6month', '12month' and 'overall'.
Final Opacity
Use this option to darken the image a bit so that it's not too distracting as a desktop wallpaper. Use a value between 0 and 100, 0 meaning that the image is completely black. The default is 80 and you control it with '-O' or '--FinalOpacity'. Ex.: ./wallpaperfm.py -O 30
Cache
The albums are stored in a cache directory and are not downloaded again if there are available. The default cache is called wpcache. You can change that with the '-e' or '--Cache' switch. Ex.:./wallpaper -e tmp
Excluded albums
You can exclude albums from the process by using the '-x' or '--ExcludedList'. There is one URL that is excluded by default: http://cdn.last.fm/depth/catalogue/noimage/cover_med.gif, which is the last.fm generic cover.
Local copy
By using '-l' or '--Local', you are using a local copy of the charts, from the last time you used the script. Doing so is faster than having to go and fetch the profile on the web, and allows you to use the script offline.