#!/usr/bin/python # vim:fileencoding=utf-8 # # lastfm-rip.py # # Copyright (c) 2009, Phillip Berndt # All rights reserved. # # Changelog: # 02/05/2011 Changed filename encoding for non-utf8-filesystems # (Thanks to Christian Köstlin for pointing that out and # providing a patch ☺) # 05/14/2010 Added option to keep a list of downloaded songs # Added file-name cleanup code as suggested by Thomas # 04/19/2010 Added option to stop after downloading n songs # 07/28/2009 Enqueue songs in rythmbox if possible (by Jesper Zedlitz) # 07/23/2009 Applied patch by Jesper Zedlitz # to include GNOME keyring # 06/24/2009 Applied patch by Christian Hammers to avoid crashes due to # wrong character encoding # # Redistribution and use in source and binary forms, with or without # modification, are permitted provided that the following conditions are met: # # * Redistributions of source code must retain the above copyright notice, this # list of conditions and the following disclaimer. * Redistributions in binary # form must reproduce the above copyright notice, this list of conditions and # the following disclaimer in the documentation and/or other materials provided # with the distribution. * The names of its contributors may be used to # endorse or promote products derived from this software without specific prior # written permission. # # THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" # AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE # IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE # ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE # LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR # CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF # SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS # INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN # CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) # ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE # POSSIBILITY OF SUCH DAMAGE. # APP_NAME = 'lastfm-rip.py' # Mandatory imports try: import sys import xml.dom.minidom import urllib import getpass import hashlib import os import re import time import dbm from optparse import OptionParser except: print "A required module was not found. Make sure you have the python" print "standard library installed." sys.exit(0) # Check for eyeD3 hasEyeD3lib = False try: import eyeD3 hasEyeD3lib = True except: print "Note: If you install eyeD3 I'll write tags for you!\n" # Check for gnome keyring hasGnomeKeyring = False try: import gtk # sets app name import gnomekeyring import gconf hasGnomeKeyring = True except: pass # Check for unidecode hasUnidecode = False try: import unidecode hasUnidecode = True except: pass # Function to encode query strings def encodeUrl(data): retval = "" allowedRange = map(chr, range(ord('a'), ord('z') + 1) + range(ord('A'), ord('Z') + 1) + range(ord('0'), ord('9') + 1)) return "".join(( "%%%02x" % ord(char) if char not in allowedRange else char for char in data)) # Parse command line parser = OptionParser(usage="%prog [options] ", epilog=("lastfm-rip.py - Copyright (c) 2009, Phillip " "Berndt. This software may be redistributed under the terms of the BSD-License")) parser.add_option("-u", "--username", dest="username", help="Last.FM username") parser.add_option("-p", "--password", dest="password", help="Last.FM password") parser.add_option("-a", "--artist", dest="artist", help="Play similar artists") parser.add_option("-d", "--directory", dest="directory", help="Store files to this directory", default=".") parser.add_option("-f", "--filter", dest="filter", help="Apply argument as a regex to filter songs (artist - title)") parser.add_option("-r", "--enqueue-rythmbox", dest="enqueue", default=False, action="store_true", help="Enqueue downloaded songs in Rythmbox (Requires dbus and pygtk)") parser.add_option("-n", "--fetch-n-songs", dest="number", help="Try to fetch exactly n songs, then exit") parser.add_option("-k", "--keep-list", dest="keep", action="store_true", help="Keep a persistent list of downloaded songs so this will never download the same song twice") optlist, args = parser.parse_args() numberOfSongs = -1 if optlist.number: try: numberOfSongs = int(optlist.number) assert(numberOfSongs > 0) except: parser.error("Parameter to -n should be numeric and greater than zero") if optlist.filter: try: re.compile(optlist.filter) except: parser.error("--filter argument has a syntax error") if optlist.artist and len(args) != 0: parser.error("Supply EITHER -a oder url") if optlist.artist: args.append("lastfm://artist/%s/similarartists" % \ encodeUrl(optlist.artist)) if not optlist.username: parser.error("--username is mandatory") if len(args) != 1: print "You must supply a last-fm url. Using personal radio as default." args.append("lastfm://user/%s/personal" % optlist.username) if optlist.enqueue: hasConnectionToRhythmbox = False try: import dbus session_bus = dbus.SessionBus() proxy_obj = session_bus.get_object('org.gnome.Rhythmbox', '/org/gnome/Rhythmbox/Shell') rhythmboxShell = dbus.Interface(proxy_obj, 'org.gnome.Rhythmbox.Shell') hasConnectionToRhythmbox = True except: pass # try to read the password from GNOME'S keyring if (not optlist.password) and hasGnomeKeyring: gconf_key = "/apps/%s/%s/keyring_auth_token" % (APP_NAME, optlist.username ) keyring = gnomekeyring.get_default_keyring_sync() auth_token = gconf.client_get_default().get_int(gconf_key) if auth_token > 0: item = gnomekeyring.item_get_info_sync(keyring,auth_token) optlist.password = item.get_secret() if not optlist.password: # ask the user for a password password = getpass.getpass() if hasGnomeKeyring: # store password in GNOME's keyring gconf_key = "/apps/%s/%s/keyring_auth_token" % (APP_NAME, optlist.username ) keyring = gnomekeyring.get_default_keyring_sync() auth_token = gnomekeyring.item_create_sync( keyring, gnomekeyring.ITEM_GENERIC_SECRET, "%s@last.fm" % optlist.username, dict(appname=APP_NAME,username=optlist.username), str(password), True) gconf.client_get_default().set_int(gconf_key, auth_token) passwordmd5 = hashlib.md5(password).hexdigest() else: passwordmd5 = hashlib.md5(optlist.password).hexdigest() if optlist.directory: try: os.chdir(optlist.directory) except: print "Failed to chdir into ", optlist.directory sys.exit(1) # Login to last.fm / Perform handshake try: handshakeUrl = ( "http://ws.audioscrobbler.com/radio/handshake.php?version=1.4." + "2.58240&platform=linux&platformversion=Unix%2FLinux&username=" + optlist.username + "&passwordmd5=" + passwordmd5 + "&language=en" ) handshakeDataRaw = urllib.urlopen(handshakeUrl).read() handshakeData = dict(filter(lambda x: len(x) == 2, ( x.split("=", 1) for x in handshakeDataRaw.split() ))) except: print "Handshake failed." try: if handshakeDataRaw: print "Information:" print handshakeDataRaw except: pass sys.exit(1) if handshakeData["session"] == "FAILED": print "Login failed." sys.exit(1) # Adjust session to radio station adjustUrl = ( "http://ws.audioscrobbler.com/radio/adjust.php?session=" + handshakeData["session"] + "&url=" + args[0] ) adjustDataRaw = urllib.urlopen(adjustUrl).read() adjustData = dict(filter(lambda x: len(x) == 2, ( x.split("=", 1) for x in adjustDataRaw.split() ))) if "response" not in adjustData or adjustData["response"] != "OK": print "Failed to tune station" sys.exit(1) # Receive playlists (they contain all information and auto-update on reretrieval) def getTracks(): trackUrl = "http://ws.audioscrobbler.com/radio/xspf.php?sk=" + handshakeData["session"] + "&discovery=0&desktop=1.4.2.58240" xmlData = urllib.urlopen(trackUrl).read() try: playlist = xml.dom.minidom.parseString(xmlData) except KeyboardInterrupt: print "Caught interrupt. The latest file may be corrupted." sys.exit(0) except: print "Failed to parse XML." #print xmlData sys.exit(1) for track in playlist.getElementsByTagName("track"): try: attrs = [ "title", "location", "album", "creator" ] toyield = {} for attr in attrs: try: toyield[attr] = track.getElementsByTagName(attr)[0].firstChild.toxml() except: if attr == "title": raise Exception("Mandatory field title missing") toyield[attr] = "" yield toyield except KeyboardInterrupt: print "Caught interrupt. The latest file may be corrupted." sys.exit(0) except: print "Failed to extract data from XML node." #print track.toxml() sys.exit(1) def displayDownloadStatus(blocks, blockSize, totalSize): percent = blocks * blockSize * 100 / totalSize; megaBytes = blocks * blockSize * 1.0 / 1024**2 print " %d%% (%.2f MB)\r" % (percent, megaBytes), sys.stdout.flush() if optlist.keep: database = dbm.open("knownsongs", "c") fetchedCount = 0 while True: for track in getTracks(): # Check against filter / for existance storeAs = "%s - %s.mp3" % (track["creator"], track["title"]) storeAs = storeAs.translate("/") trackHash = hashlib.md5(storeAs).hexdigest() if sys.getfilesystemencoding() != "UTF-8": if hasUnidecode: storeAs = unidecode.unidecode(storeAs) else: storeAs = storeAs.encode(sys.getfilesystemencoding(), "replace") if optlist.filter and not re.search(optlist.filter, storeAs, re.I): print "Skipping", storeAs, "(filter failed)" try: time.sleep(3) except KeyboardInterrupt: print "Caught interrupt. The latest file may be corrupted." sys.exit(0) continue if optlist.keep and database.has_key(trackHash): print "Skipping", storeAs, "(downloaded before)" continue if os.access(storeAs, os.F_OK): print "Skipping", storeAs, "(exists)" continue # Download file if numberOfSongs > 0: fetchedCount += 1 if fetchedCount > numberOfSongs: sys.exit(0) print "Receiving", storeAs try: urllib.urlretrieve(track["location"], storeAs, displayDownloadStatus) except KeyboardInterrupt: os.unlink(storeAs) print "Caught interrupt. The latest file may be corrupted." sys.exit(0) if optlist.keep: database[trackHash] = "" if hasEyeD3lib: print " Writing ID3 tag \r", tag = eyeD3.Tag() tag.link(storeAs) tag.header.setVersion(eyeD3.ID3_V2_3) tag.setTextEncoding(eyeD3.frames.UTF_16_ENCODING) tag.setArtist(track["creator"]) tag.setTitle(track["title"]) if track["album"]: tag.setAlbum(track["album"]) tag.update() if optlist.enqueue and hasConnectionToRhythmbox: song = 'file://' + ( os.getcwd() + '/' + storeAs).replace(' ','%20') print song rhythmboxShell.loadURI(song,False) rhythmboxShell.addToQueue(song)