Package elisa :: Package plugins :: Package ugly :: Package lastfm_plugin :: Module lastfm_scrobbler
[hide private]
[frames] | no frames]

Source Code for Module elisa.plugins.ugly.lastfm_plugin.lastfm_scrobbler

  1  # -*- coding: utf-8 -*- 
  2  # Elisa - Home multimedia server 
  3  # Copyright (C) 2006-2008 Fluendo Embedded S.L. (www.fluendo.com). 
  4  # All rights reserved. 
  5  # 
  6  # This file is available under one of two license agreements. 
  7  # 
  8  # This file is licensed under the GPL version 3. 
  9  # See "LICENSE.GPL" in the root of this distribution including a special 
 10  # exception to use Elisa with Fluendo's plugins. 
 11  # 
 12  # The GPL part of Elisa is also available under a commercial licensing 
 13  # agreement from Fluendo. 
 14  # See "LICENSE.Elisa" in the root directory of this distribution package 
 15  # for details on that license. 
 16   
 17  """ 
 18  AudioScrobbler music stats submission 
 19  """ 
 20   
 21  __maintainer__ = "Philippe Normand <philippe@fluendo.com>" 
 22   
 23  from elisa.base_components import service_provider 
 24  from elisa.core import log, common, component, player 
 25  from elisa.core.observers.observer import Observer 
 26  from elisa.core.bus.bus_message import PlayerModel, ComponentsLoaded 
 27  from twisted.internet import defer, threads 
 28   
 29  import gst 
 30  import urllib2, urllib, threading 
 31  import re, time, md5, os, socket 
 32  import xml.utils.iso8601 
 33  from cPickle import dump, load 
 34   
 35   
 36  UNKNOWN = 'unknown' 
 37  SAVED_TRACKS_FILE = os.path.expanduser('~/.elisa/unsubmitted_tracks.txt') 
 38   
 39  socket.setdefaulttimeout(7) 
 40   
41 -class ScrobbledTrack:
42 - def __init__(self, media, length):
43 if media.artist != 'unknown artist': 44 self.artist = media.artist 45 else: 46 self.artist = UNKNOWN 47 if media.album != 'unknown album': 48 self.album = media.album 49 else: 50 self.album = UNKNOWN 51 self.name = media.song 52 self.length = str(length) 53 54 # MusicBrainz not supported yet 55 self.mbid = None 56 57 self.date = re.sub("(\d\d\d\d-\d\d-\d\d)T(\d\d:\d\d:\d\d).*","\\1 \\2", 58 xml.utils.iso8601.tostring(time.time()))
59
60 - def isSubmitable(self):
61 return UNKNOWN not in (self.artist, self.album)
62
63 - def __repr__(self):
64 return "%r by %r from %r (%r @ %r)" % (self.name, self.artist, 65 self.album, 66 self.length, self.date)
67
68 - def urlencoded(self, num):
69 encode = "" 70 71 encode += "a["+str(num)+"]="+urllib.quote_plus(self.artist.encode('utf-8')) 72 encode += "&t["+str(num)+"]="+urllib.quote_plus(self.name.encode('utf-8')) 73 encode += "&l["+str(num)+"]="+urllib.quote_plus(self.length) 74 encode += "&i["+str(num)+"]="+(self.date) 75 if self.mbid is not None: 76 encode += "&m["+str(num)+"]="+urllib.quote_plus(self.mbid) 77 else: 78 encode += "&m["+str(num)+"]=" 79 encode += "&b["+str(num)+"]="+urllib.quote_plus(self.album.encode('utf-8')) 80 return encode
81
82 -class PlayerModelObserver(log.Loggable, Observer):
83 logCategory = "player_observer" 84
85 - def __init__(self, service):
86 Observer.__init__(self) 87 self._service = service 88 self._submitted_uris = [] 89 self._current_uri = None
90
91 - def attribute_set(self, origin, key, old_value, new_value):
92 self.log("New new_value for %r: %r", key, new_value) 93 94 if key == 'position' and new_value > 0: 95 player_model = self._observable 96 97 uri = player_model.uri 98 self._current_uri = uri 99 100 if uri not in self._submitted_uris: 101 length = player_model.duration / gst.SECOND 102 status = new_value / gst.SECOND 103 104 if status > length/2.: 105 media_manager = common.application.media_manager 106 media = media_manager.get_media_information(uri, extended=True) 107 self.debug("Found by media_manager: %r", media) 108 if media and hasattr(media, 'artist'): 109 self._submitted_uris.append(uri) 110 track = ScrobbledTrack(media, length) 111 threads.deferToThread(self._service.submit, track) 112 else: 113 self.log("Not yet ready to report") 114 else: 115 self.log("Found new track, will report it soon") 116 elif key == 'state' and new_value == player.STATES.STOPPED: 117 current = self._current_uri 118 if current is not None and current in self._submitted_uris: 119 self.debug("Removing reference to %r", current) 120 self._submitted_uris.remove(current) 121 self._current_uri = None
122
123 -class LastfmScrobbler(service_provider.ServiceProvider):
124 """ 125 DOCME 126 """ 127 128 config_doc = {'user': 'Last.FM username', 129 'password': 'Last.FM password for the user :-)' 130 } 131 132 default_config = {'user': 'fill_me', 133 'password': 'fill_me' 134 } 135
136 - def initialize(self):
137 user = self.config.get('user') 138 if user == 'fill_me': 139 msg = "Please configure your Last.FM account settings" 140 raise component.InitializeFailure(self.name, msg) 141 self.url = "http://post.audioscrobbler.com/" 142 self.user = user 143 self.password = self.config.get('password') 144 self.client = "eli" 145 self.version = "0.1" 146 self.lock = threading.Lock() 147 self.loadSavedTracks()
148
149 - def start(self):
153
154 - def stop(self):
155 self.saveTracks()
156
157 - def _register_player_model(self, msg, sender):
158 self.debug("Got player model %r", msg.player_model) 159 self._player_observer = PlayerModelObserver(self) 160 msg.player_model.add_observer(self._player_observer)
161
162 - def loadSavedTracks(self):
163 self.tracksToSubmit = [] 164 if os.path.exists(SAVED_TRACKS_FILE): 165 f = open(SAVED_TRACKS_FILE, 'r') 166 try: 167 self.tracksToSubmit = load(f) 168 finally: 169 f.close()
170
171 - def saveTracks(self):
172 if self.tracksToSubmit: 173 self.debug('Saving %s tracks' % len(self.tracksToSubmit)) 174 f = open(SAVED_TRACKS_FILE, 'w') 175 dump(self.tracksToSubmit, f) 176 f.close()
177
178 - def handshake(self):
179 self.logged = False 180 self.debug("Handshaking...") 181 url = self.url+"?"+urllib.urlencode({ 182 "hs":"true", 183 "p":"1.1", 184 "c":self.client, 185 "v":self.version, 186 "u":self.user 187 }) 188 189 try: 190 result = urllib2.urlopen(url).readlines() 191 except urllib2.URLError, ex: 192 self.debug("Could not connect to AudioScrobbler: %s", ex.reason.message) 193 except Exception, ex: 194 self.debug("Could not connect to AudioScrobbler: %s", ex) 195 else: 196 status = result[0] 197 if status.startswith("BADUSER"): 198 return self.baduser(result[1:]) 199 if status.startswith("FAILED"): 200 return self.failed(result) 201 self.logged = True 202 if status.startswith("UPTODATE") or status.startswith("UPDATE"): 203 return self.uptodate(result[1:]) 204 return True 205 return False
206
207 - def uptodate(self, lines):
208 self.md5 = re.sub("\n$","", lines[0]) 209 self.debug("MD5 = %r" % self.md5) 210 self.submiturl = re.sub("\n$","", lines[1]) 211 self.debug("submiturl = %r" % self.submiturl) 212 self.interval(lines[2]) 213 return True
214
215 - def baduser(self, lines):
216 self.debug("Bad user") 217 self.interval(lines[1]) 218 return False
219
220 - def failed(self, lines):
221 self.debug('Failed : %s' % lines[0]) 222 self.interval(lines[1]) 223 return False
224
225 - def interval(self, line):
226 match = re.match("INTERVAL (\d+)", line) 227 if match is not None: 228 secs = int(match.group(1)) 229 self.debug("Sleeping a while (%s seconds)" % secs) 230 time.sleep(secs)
231
232 - def submit(self, track):
233 if not self.logged: 234 # try to log 235 if not self.handshake(): 236 if track not in self.tracksToSubmit: 237 self.debug("Queued submission of track %s" % track) 238 self.tracksToSubmit.append(track) 239 self.debug("%s track(s) currently queued" % len(self.tracksToSubmit)) 240 self.saveTracks() 241 return 242 243 if track not in self.tracksToSubmit: 244 self.tracksToSubmit.append(track) 245 246 self.debug("Will try to submit tracks : %s" % str(self.tracksToSubmit)) 247 248 try: 249 md5response = md5.md5(md5.md5(self.password).hexdigest()+self.md5).hexdigest() 250 self.debug('md5response: %s' % md5response) 251 252 post = "u="+self.user+"&s="+md5response 253 count = 0 254 self.debug('post: %r' % post) 255 256 for track in self.tracksToSubmit: 257 l = int(track.length) 258 if not track.isSubmitable(): 259 self.debug("Missing informations from track, skipping submit") 260 self.debug(track) 261 elif l < 30 or l > (30*60): 262 self.debug("Track is too short or too long, skipping submit") 263 self.debug(track) 264 else: 265 self.debug('encoded: %r' % track.urlencoded(count)) 266 post += "&" 267 post += track.urlencoded(count) 268 count += 1 269 except Exception, ex: 270 self.debug('Exception : %s' % ex) 271 272 self.debug('count = %s' % count) 273 self.debug(post) 274 post = unicode(post) 275 if count: 276 try: 277 result = urllib2.urlopen(self.submiturl,post) 278 except Exception,ex: 279 self.debug(ex) 280 self.logged = False 281 else: 282 results = result.readlines() 283 self.debug("submit result : %r" % results) 284 if results[0].startswith("OK"): 285 self.interval(results[1]) 286 self.tracksToSubmit = [] 287 self.saveTracks() 288 elif results[0].startswith("FAILED"): 289 self.failed(results) 290 self.handshake() 291 elif results[0].startswith("BADAUTH"): 292 self.baduser(results) 293 self.handshake()
294