Pberndt V4

Direkt zum Inhalt springen


Quellcode lastfm-rip.py

Sourcecode

#!/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 <jesper ät zedlitz düd de>
#    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] <lastfm-url>", 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)

Download

Dateiname
lastfm-rip.py
Größe
10.7kb