summaryrefslogtreecommitdiff
path: root/plugins
diff options
context:
space:
mode:
authorjerous <jerous@gmail.com>2008-09-16 03:28:16 +0200
committerjerous <jerous@gmail.com>2008-09-16 03:28:16 +0200
commitb3335d2ea997c5aa36fc95cef8cfae8df69a6deb (patch)
treef8ec4f866088e903588f882c8f7ca617a079c78b /plugins
parent9375e807f9baa028a1ca7ed7cf50d86567c9998b (diff)
simple scrobbler plugin
Diffstat (limited to 'plugins')
-rw-r--r--plugins/Scrobbler.py372
1 files changed, 372 insertions, 0 deletions
diff --git a/plugins/Scrobbler.py b/plugins/Scrobbler.py
new file mode 100644
index 0000000..94c0c89
--- /dev/null
+++ b/plugins/Scrobbler.py
@@ -0,0 +1,372 @@
+import time
+import datetime
+
+from clPlugin import *
+import log
+
+class pluginScrobbler(Plugin):
+ submitted=False
+ time=None
+ def __init__(self, winMain):
+ Plugin.__init__(self, winMain, 'Scrobbler')
+
+ def _load(self):
+ monty.addListener('onSongChange', self.onSongChange)
+ monty.addListener('onTimeChange', self.onTimeChange)
+ self._login()
+
+ def _login(self):
+ self.submitting=False
+ if self._username():
+ log.normal("Scrobbler: logging in %s"%(self._username()))
+ login(self._username(), self._password())
+ log.normal("Scrobbler: logged in")
+ else:
+ log.debug("Scrobbler: no username provided, not logging in")
+
+ def _username(self):
+ return settings.get('scrobbler.username', '')
+ def _password(self):
+ return settings.get('scrobbler.password', '')
+ def onTimeChange(self, params):
+ if self.submitted==False:
+ song=monty.getCurrentSong()
+ if song.getTag('time')>30:
+ if int(params['newTime'])>int(song.getTag('time'))/2 \
+ or int(params['newTime'])>240:
+ log.normal("Scrobbler: submitting song")
+ submit(song.getArtist(), song.getTitle(), self.time, 'P', '', song.getTag('time'), song.getAlbum(), song.getTrack())
+ log.debug("Scrobbler: flushing ...")
+ try:
+ flush()
+ self.submitted=True
+ except Exception, e:
+ log.important("Scrobbler: failed to sumit song - %s"%(e))
+ log.debug("Scrobbler: flushed")
+
+ def onSongChange(self, params):
+ self.time=int(time.mktime(datetime.utcnow().timetuple()))
+ self.submitted=False
+ song=monty.getCurrentSong()
+ try:
+ log.extended("Scrobbler: submitting now playing")
+ now_playing(song.getArtist(), song.getTitle(), song.getAlbum(), "", song.getTrack())
+ log.debug("Scrobbler: submitted")
+ except AuthError, e:
+ log.important("Scrobbler: failed to submit playing song - %s"%(e))
+ def _getSettings(self):
+ return [
+ ['scrobbler.username', 'Username', 'Username to submit to last.fm.', QtGui.QLineEdit(self._username())],
+ ['scrobbler.password', 'Password', 'Password to user to submit. Note that the password is stored *unencrypted* to file.', QtGui.QLineEdit(self._password())],
+ ]
+ def afterSaveSettings(self):
+ self._login()
+ def getInfo(self):
+ return "Submits tracks to last.fm"
+
+
+
+
+# Big thank you,
+# http://exhuma.wicked.lu/projects/python/scrobbler/ !"
+
+"""
+A pure-python library to assist sending data to AudioScrobbler (the LastFM
+backend)
+"""
+import urllib, urllib2
+from time import mktime
+from datetime import datetime, timedelta
+from md5 import md5
+
+SESSION_ID = None
+POST_URL = None
+NOW_URL = None
+HARD_FAILS = 0
+LAST_HS = None # Last handshake time
+HS_DELAY = 0 # wait this many seconds until next handshake
+SUBMIT_CACHE = []
+MAX_CACHE = 5 # keep only this many songs in the cache
+PROTOCOL_VERSION = '1.2'
+
+class BackendError(Exception):
+ "Raised if the AS backend does something funny"
+ pass
+class AuthError(Exception):
+ "Raised on authencitation errors"
+ pass
+class PostError(Exception):
+ "Raised if something goes wrong when posting data to AS"
+ pass
+class SessionError(Exception):
+ "Raised when problems with the session exist"
+ pass
+class ProtocolError(Exception):
+ "Raised on general Protocol errors"
+ pass
+
+def login( user, password, client=('tst', '1.0') ):
+ """Authencitate with AS (The Handshake)
+
+ @param user: The username
+ @param password: The password
+ @param client: Client information (see http://www.audioscrobbler.net/development/protocol/ for more info)
+ @type client: Tuple: (client-id, client-version)"""
+ global LAST_HS, SESSION_ID, POST_URL, NOW_URL, HARD_FAILS, HS_DELAY, PROTOCOL_VERSION
+
+ if LAST_HS is not None:
+ next_allowed_hs = LAST_HS + timedelta(seconds=HS_DELAY)
+ if datetime.now() < next_allowed_hs:
+ delta = next_allowed_hs - datetime.now()
+ raise ProtocolError("""Please wait another %d seconds until next handshake (login) attempt.""" % delta.seconds)
+
+ LAST_HS = datetime.now()
+
+ tstamp = int(mktime(datetime.now().timetuple()))
+ url = "http://post.audioscrobbler.com/"
+ pwhash = md5(password).hexdigest()
+ token = md5( "%s%d" % (pwhash, int(tstamp))).hexdigest()
+ values = {
+ 'hs': 'true',
+ 'p' : PROTOCOL_VERSION,
+ 'c': client[0],
+ 'v': client[1],
+ 'u': user,
+ 't': tstamp,
+ 'a': token
+ }
+ data = urllib.urlencode(values)
+ req = urllib2.Request("%s?%s" % (url, data) )
+ response = urllib2.urlopen(req)
+ result = response.read()
+ lines = result.split('\n')
+
+
+ if lines[0] == 'BADAUTH':
+ raise AuthError('Bad username/password')
+
+ elif lines[0] == 'BANNED':
+ raise Exception('''This client-version was banned by Audioscrobbler. Please contact the author of this module!''')
+
+ elif lines[0] == 'BADTIME':
+ raise ValueError('''Your system time is out of sync with Audioscrobbler.Consider using an NTP-client to keep you system time in sync.''')
+
+ elif lines[0].startswith('FAILED'):
+ handle_hard_error()
+ raise BackendError("Authencitation with AS failed. Reason: %s" %
+ lines[0])
+
+ elif lines[0] == 'OK':
+ # wooooooohooooooo. We made it!
+ SESSION_ID = lines[1]
+ NOW_URL = lines[2]
+ POST_URL = lines[3]
+ HARD_FAILS = 0
+
+ else:
+ # some hard error
+ handle_hard_error()
+
+def handle_hard_error():
+ "Handles hard errors."
+ global SESSION_ID, HARD_FAILS, HS_DELAY
+
+ if HS_DELAY == 0:
+ HS_DELAY = 60
+ elif HS_DELAY < 120*60:
+ HS_DELAY *= 2
+ if HS_DELAY > 120*60:
+ HS_DELAY = 120*60
+
+ HARD_FAILS += 1
+ if HARD_FAILS == 3:
+ SESSION_ID = None
+
+def now_playing( artist, track, album="", length="", trackno="", mbid="" ):
+ """Tells audioscrobbler what is currently running in your player. This won't
+ affect the user-profile on last.fm. To do submissions, use the "submit"
+ method
+
+ @param artist: The artist name
+ @param track: The track name
+ @param album: The album name
+ @param length: The song length in seconds
+ @param trackno: The track number
+ @param mbid: The MusicBrainz Track ID
+ @return: True on success, False on failure"""
+
+ global SESSION_ID, NOW_URL
+
+ if SESSION_ID is None:
+ raise AuthError("Please 'login()' first. (No session available)")
+
+ if POST_URL is None:
+ raise PostError("Unable to post data. Post URL was empty!")
+
+ if length != "" and type(length) != type(1):
+ raise TypeError("length should be of type int")
+
+ if trackno != "" and type(trackno) != type(1):
+ raise TypeError("trackno should be of type int")
+
+ values = {'s': SESSION_ID,
+ 'a': unicode(artist).encode('utf-8'),
+ 't': unicode(track).encode('utf-8'),
+ 'b': unicode(album).encode('utf-8'),
+ 'l': length,
+ 'n': trackno,
+ 'm': mbid }
+
+ data = urllib.urlencode(values)
+ req = urllib2.Request(NOW_URL, data)
+ response = urllib2.urlopen(req)
+ result = response.read()
+
+ if result.strip() == "OK":
+ return True
+ elif result.strip() == "BADSESSION" :
+ raise SessionError('Invalid session')
+ else:
+ return False
+
+def submit(artist, track, time, source='P', rating="", length="", album="",
+ trackno="", mbid="", autoflush=False):
+ """Append a song to the submission cache. Use 'flush()' to send the cache to
+ AS. You can also set "autoflush" to True.
+
+ From the Audioscrobbler protocol docs:
+ ---------------------------------------------------------------------------
+
+ The client should monitor the user's interaction with the music playing
+ service to whatever extent the service allows. In order to qualify for
+ submission all of the following criteria must be met:
+
+ 1. The track must be submitted once it has finished playing. Whether it has
+ finished playing naturally or has been manually stopped by the user is
+ irrelevant.
+ 2. The track must have been played for a duration of at least 240 seconds or
+ half the track's total length, whichever comes first. Skipping or pausing
+ the track is irrelevant as long as the appropriate amount has been played.
+ 3. The total playback time for the track must be more than 30 seconds. Do
+ not submit tracks shorter than this.
+ 4. Unless the client has been specially configured, it should not attempt to
+ interpret filename information to obtain metadata instead of tags (ID3,
+ etc).
+
+ @param artist: Artist name
+ @param track: Track name
+ @param time: Time the track *started* playing in the UTC timezone (see
+ datetime.utcnow()).
+
+ Example: int(time.mktime(datetime.utcnow()))
+ @param source: Source of the track. One of:
+ 'P': Chosen by the user
+ 'R': Non-personalised broadcast (e.g. Shoutcast, BBC Radio 1)
+ 'E': Personalised recommendation except Last.fm (e.g.
+ Pandora, Launchcast)
+ 'L': Last.fm (any mode). In this case, the 5-digit Last.fm
+ recommendation key must be appended to this source ID to
+ prove the validity of the submission (for example,
+ "L1b48a").
+ 'U': Source unknown
+ @param rating: The rating of the song. One of:
+ 'L': Love (on any mode if the user has manually loved the
+ track)
+ 'B': Ban (only if source=L)
+ 'S': Skip (only if source=L)
+ '': Not applicable
+ @param length: The song length in seconds
+ @param album: The album name
+ @param trackno:The track number
+ @param mbid: MusicBrainz Track ID
+ @param autoflush: Automatically flush the cache to AS?
+ """
+
+ global SUBMIT_CACHE, MAX_CACHE
+
+ source = source.upper()
+ rating = rating.upper()
+
+ if source == 'L' and (rating == 'B' or rating == 'S'):
+ raise ProtocolError("""You can only use rating 'B' or 'S' on source 'L'.See the docs!""")
+
+ if source == 'P' and length == '':
+ raise ProtocolError("""Song length must be specified when using 'P' as source!""")
+
+ if type(time) != type(1):
+ raise ValueError("""The time parameter must be of type int (unix timestamp). Instead it was %s""" % time)
+
+ SUBMIT_CACHE.append(
+ { 'a': unicode(artist).encode('utf-8'),
+ 't': unicode(track).encode('utf-8'),
+ 'i': time,
+ 'o': source,
+ 'r': rating,
+ 'l': length,
+ 'b': unicode(album).encode('utf-8'),
+ 'n': trackno,
+ 'm': mbid
+ }
+ )
+
+ if autoflush or len(SUBMIT_CACHE) >= MAX_CACHE:
+ flush()
+
+def flush():
+ "Sends the cached songs to AS."
+ global SUBMIT_CACHE
+
+ values = {}
+
+ for i, item in enumerate(SUBMIT_CACHE):
+ for key in item:
+ values[key + "[%d]" % i] = item[key]
+
+ values['s'] = SESSION_ID
+
+ data = urllib.urlencode(values)
+ req = urllib2.Request(POST_URL, data)
+ response = urllib2.urlopen(req)
+ result = response.read()
+ lines = result.split('\n')
+
+ if lines[0] == "OK":
+ SUBMIT_CACHE = []
+ return True
+ elif lines[0] == "BADSESSION" :
+ raise SessionError('Invalid session')
+ elif lines[0].startswith('FAILED'):
+ handle_hard_error()
+ raise BackendError("Authencitation with AS failed. Reason: %s" %
+ lines[0])
+ else:
+ # some hard error
+ handle_hard_error()
+ return False
+
+
+if __name__ == "__main__":
+ login( 'user', 'password' )
+ submit(
+ 'De/Vision',
+ 'Scars',
+ 1192374052,
+ source='P',
+ length=3*60+44
+ )
+ submit(
+ 'Spineshank',
+ 'Beginning of the End',
+ 1192374052+(5*60),
+ source='P',
+ length=3*60+32
+ )
+ submit(
+ 'Dry Cell',
+ 'Body Crumbles',
+ 1192374052+(10*60),
+ source='P',
+ length=3*60+3
+ )
+ print flush()
+