Changeset 1850

Show
Ignore:
Timestamp:
31/10/08 22:36:10 (2 months ago)
Author:
duncan
svm:headrev:

cc3e1ea1-1e01-0410-8d68-8b121e83a9d5:11131
Message:

Updated for play list support
There is a nasty bug in mplayer that corrupts the URLs; to avoid this problem this version downloads the music track to a file and plays it with mplayer.

The program still fails after the first play list with an invalid ticket. Not sure quite what is causing this problem.

Getting last.fm play lists to work correctly is non-trival to say the least, thankfully we have wireshark and http as a filter ;-)

Files:
1 modified

Legend:

Unmodified
Added
Removed
  • freevo/src/audio/plugins/lastfm2.py

    r1826 r1850  
    11# -*- coding: iso-8859-1 -*- 
    22# ----------------------------------------------------------------------- 
    3 #  
     3# 
    44# ----------------------------------------------------------------------- 
    55# $Id$ 
     
    3131 
    3232import sys, os, time 
    33 import md5, urllib, urllib2, re 
     33import md5, urllib, urllib2, httplib, re 
     34from threading import Thread 
    3435 
    3536import kaa 
     
    3940import plugin 
    4041import rc 
     42from event import PLAY_END 
    4143from menu import MenuItem, Menu 
     44from gui import AlertBox 
    4245from audio.audioitem import AudioItem 
    4346from audio.player import PlayerGUI 
     
    5659 
    5760 
     61class LastFMError(Exception): 
     62    """ 
     63    An exception class for last.fm 
     64    """ 
     65    @benchmark(benchmarking, benchmarkcall) 
     66    def __init__(self, why): 
     67        Exception.__init__(self) 
     68        self.why = why 
     69 
     70    def __str__(self): 
     71        return self.why 
     72 
     73 
     74 
    5875class PluginInterface(plugin.MainMenuPlugin): 
    5976    """ 
     
    6582    | LASTFM_USER = '<last fm user name>' 
    6683    | LASTFM_PASS = '<last fm password>' 
    67     | LASTFM_MPLAYER_AF_TRACK = True 
    6884    | LASTFM_LOCATIONS = [ 
    6985    |     ('Last Fm - Neighbours', 'lastfm://user/%s/neighbours' % LASTFM_USER), 
     
    88104            return 
    89105        plugin.MainMenuPlugin.__init__(self) 
     106        self.menuitem = None 
     107        if not os.path.isdir(config.LASTFM_DIR): 
     108            os.makedirs(config.LASTFM_DIR, 0777) 
     109 
     110 
    90111    @benchmark(benchmarking, benchmarkcall) 
    91112    def config(self): 
     
    98119            ('LASTFM_PASS', None, 'Password for www.last.fm'), 
    99120            ('LASTFM_LANG', 'en', 'Language of last fm metadata (cn,de,en,es,fr,it,jp,pl,ru,sv,tr)'), 
    100             #('LASTFM_MPLAYER_AF_TRACK', False, 'Set to True if using mplayer\'s -af track'), 
     121            ('LASTFM_DIR', os.path.join(config.FREEVO_CACHEDIR, 'lastfm'), 'Directory to save lastfm files'), 
    101122            ('LASTFM_LOCATIONS', [], 'LastFM locations') 
    102123        ] 
     
    106127    def items(self, parent): 
    107128        _debug_('items(parent=%r)' % (parent,), 2) 
    108         return [ LastFMMainMenuItem(parent) ] 
    109  
    110  
    111  
    112 class LastFMError(Exception): 
    113     @benchmark(benchmarking, benchmarkcall) 
    114     def __init__(self, why): 
    115         Exception.__init__(self) 
    116         self.why = why 
     129        self.menuitem = LastFMMainMenuItem(parent) 
     130        return [ self.menuitem ] 
     131 
     132 
     133    @benchmark(benchmarking, benchmarkcall) 
     134    def shutdown(self): 
     135        print 'PluginInterface.shutdown' 
     136        if self.menuitem is not None: 
     137            self.menuitem.shutdown() 
    117138 
    118139 
     
    120141class LastFMMainMenuItem(MenuItem): 
    121142    """ 
    122     this is the item for the main menu and creates the list 
    123     of commands in a submenu. 
     143    This is the item for the main menu and creates the list of commands in a 
     144    submenu. 
    124145    """ 
    125146    @benchmark(benchmarking, benchmarkcall) 
     
    153174        menuw.pushmenu(lfm_menu) 
    154175        menuw.refresh() 
    155   
     176 
     177 
     178    @benchmark(benchmarking, benchmarkcall) 
     179    def shutdown(self): 
     180        print 'LastFMMainMenuItem.shutdown' 
     181        if self.webservices is not None: 
     182            self.webservices.shutdown() 
     183 
    156184 
    157185 
     
    162190    and for displaying stdout and stderr of last command run. 
    163191    """ 
     192    poll_interval = 4 
     193    poll_interval = 1 
    164194    @benchmark(benchmarking, benchmarkcall) 
    165195    def __init__(self, parent, name, station, webservices): 
     
    195225 
    196226 
    197     @benchmark(benchmarking, benchmarkcall) 
     227    @benchmark(benchmarking, True) #benchmarkcall) 
    198228    def eventhandler(self, event, menuw=None): 
    199229        _debug_('LastFMItem.eventhandler(event=%s, menuw=%r)' % (event, menuw), 2) 
     
    223253            self.menuw = menuw 
    224254 
    225         if self.feed is None or self.entry > len(self.feed.entries): 
     255        if self.feed is None or self.entry >= len(self.feed.entries): 
    226256            try: 
    227                 self.feed = self.xspf.parse(self.webservices.request_xspf()) 
     257                for i in range(3): 
     258                    xspf = self.webservices.request_xspf() 
     259                    if xspf != 'No recs :(': 
     260                        break 
     261                    time.sleep(2) 
     262                else: 
     263                    if menuw: 
     264                        AlertBox(text='No recs :(').show() 
     265                    rc.post_event(PLAY_END) 
     266                    return 
     267 
     268                self.feed = self.xspf.parse(xspf) 
     269                if self.feed is None: 
     270                    if menuw: 
     271                        AlertBox(text=_('Cannot get XSFP')).show() 
     272                    rc.post_event(PLAY_END) 
     273                    return 
    228274            except LastFMError, why: 
    229                 _debug_('why=%r' % (why,), DWARNING) 
     275                _debug_(why, DWARNING) 
    230276                if menuw: 
    231                     AlertBox(text=why).show() 
    232                 rc.post_event(rc.PLAY_END) 
     277                    AlertBox(text=str(why)).show() 
     278                rc.post_event(PLAY_END) 
    233279            self.entry = 0 
    234280 
     
    238284        self.title = entry.title 
    239285        self.length = entry.duration 
    240         self.image = self.webservices.download_cover(entry.image_url) 
    241         self.url = entry.location_url 
     286        basename = entry.artist + '-' + entry.album + '-' + entry.title 
     287        self.basename = basename.lower().replace(' ', '_').replace('.', '').replace('\'', '').replace(':', '') 
     288        self.url = os.path.join(config.LASTFM_DIR, self.basename + os.path.splitext(entry.location_url)[1]) 
     289        self.trackpath = os.path.join(config.LASTFM_DIR, self.basename + os.path.splitext(entry.location_url)[1]) 
     290        self.location_url = entry.location_url 
     291        self.track_downloader = self.webservices.download(self.location_url, self.trackpath) 
     292        self.stream_name = urllib.unquote_plus(self.feed.feed.title) 
     293        self.imagepath = os.path.join(config.LASTFM_DIR, self.basename + os.path.splitext(entry.image_url)[1]) 
     294        self.image_downloader = self.webservices.download(entry.image_url, self.imagepath) 
     295        #self.is_playlist = True 
     296        # Wait for a bit of the file to be downloaded 
     297        while self.track_downloader.filesize() < 1024 * 20: 
     298            if not self.track_downloader.isrunning(): 
     299                rc.post_event(PLAY_END) 
     300                return 
     301            time.sleep(0.1) 
    242302        self.player = PlayerGUI(self, menuw) 
     303        if self.timer is not None and self.timer.active(): 
     304            self.timer.stop() 
    243305        self.timer = kaa.OneShotTimer(self.timerhandler) 
    244306        self.timer.start(entry.duration) 
     
    248310            if menuw: 
    249311                AlertBox(text=error).show() 
    250             rc.post_event(rc.PLAY_END) 
     312            rc.post_event(PLAY_END) 
    251313 
    252314 
     
    257319        """ 
    258320        if self.timer is None: 
    259             print 'DJW:timer is None' 
     321            _debug_('timer is not running', DINFO) 
    260322            return 
    261         poll_interval = 3 
    262         polling_itme = 600 
    263         now_playing = self.webservices.now_playing() 
    264         if now_playing is None: 
     323        if self.track_downloader is None: 
     324            _debug_('downloader is not running', DERROR) 
     325            return 
     326        if self.track_downloader.isrunning(): 
     327            _debug_('still playing', DINFO) 
     328            self.timer.start(LastFMItem.poll_interval) 
     329        else: 
    265330            self.entry += 1 
    266             print 'DJW:', self.entry, len(self.feed.entries) 
    267331            self.play(self.arg, self.menuw) 
    268         else: 
    269             _debug_('now_playing: still playing=%r' % (now_playing,)) 
    270             #self.timer = kaa.OneShotTimer(self.timerhandler) 
    271             self.timer.start(poll_interval) 
    272332 
    273333 
     
    278338        """ 
    279339        _debug_('LastFMItem.stop(arg=%r, menuw=%r)' % (arg, menuw), 1) 
    280         if self.timer is not None: 
     340        if self.timer is not None and self.timer.active(): 
    281341            self.timer.stop() 
    282             self.timer = None 
     342        self.timer = None 
    283343 
    284344 
     
    286346    def skip(self): 
    287347        """Skip song""" 
    288         _debug_('skip()', 2) 
     348        _debug_('skip()', 1) 
    289349        self.entry += 1 
    290         if self.timer is not None: 
     350        if self.timer is not None and self.timer.active(): 
    291351            self.timer.stop() 
    292             self.timer = None 
     352        self.timer = None 
    293353        self.play(self.arg, self.menuw) 
    294354 
     
    297357    def love(self): 
    298358        """Send "Love" information to audioscrobbler""" 
    299         _debug_('love()', 2) 
     359        _debug_('love()', 1) 
    300360        self.webservices.love() 
    301361 
     
    304364    def ban(self): 
    305365        """Send "Ban" information to audioscrobbler""" 
    306         _debug_('ban()', 2) 
     366        _debug_('ban()', 1) 
    307367        self.webservices.ban() 
     368 
     369 
     370 
     371 
     372class SmartRedirectHandler(urllib2.HTTPRedirectHandler): 
     373    def http_error_301(self, req, fp, code, msg, headers): 
     374        result = urllib2.HTTPRedirectHandler.http_error_301(self, req, fp, code, msg, headers) 
     375        result.status = code 
     376        return result 
     377 
     378    def http_error_302(self, req, fp, code, msg, headers): 
     379        result = urllib2.HTTPRedirectHandler.http_error_302(self, req, fp, code, msg, headers) 
     380        result.status = code 
     381        return result 
     382 
     383 
     384 
     385class LastFMDownloader(Thread): 
     386    """ 
     387    Download the stream to a file 
     388 
     389    There is a bad bug im mplayer that corrupts the url passed, so we have to 
     390    download it to a file and then play it 
     391    """ 
     392    def __init__(self, url, filename, headers=None): 
     393        Thread.__init__(self) 
     394        self.url = url 
     395        self.filename = filename 
     396        self.headers = headers 
     397        self.running = True 
     398        self.size = 0 
     399 
     400 
     401    def run(self): 
     402        """ 
     403        Execute a download operation. Stop when finished downloading or 
     404        requested to stop. 
     405        """ 
     406        httplib.HTTPConnection.debuglevel = 1 
     407        request = urllib2.Request(self.url, headers=self.headers) 
     408        opener = urllib2.build_opener(SmartRedirectHandler()) 
     409        try: 
     410            httplib.HTTPConnection.debuglevel = 1 
     411            f = opener.open(request) 
     412            fd = open(self.filename, 'wb') 
     413            while self.running: 
     414                reply = f.read(1024 * 100) 
     415                fd.write(reply) 
     416                if len(reply) == 0: 
     417                    self.running = False 
     418                    _debug_('%s downloaded' % self.filename) 
     419                    # what we could do now is to add tags to track 
     420                    break 
     421                self.size += len(reply) 
     422            else: 
     423                _debug_('%s aborted' % self.filename) 
     424            fd.close() 
     425            f.close() 
     426        except urllib2.HTTPError, why: 
     427            _debug_('%s: %s' % (self.filename, why), DWARNING) 
     428        httplib.HTTPConnection.debuglevel = 0 
     429 
     430 
     431    def stop(self): 
     432        """ 
     433        Stop the download thead running 
     434        """ 
     435        # this does not stop the download thread 
     436        self.running = False 
     437 
     438 
     439    def filesize(self): 
     440        """ 
     441        Get the downloaded file size 
     442        """ 
     443        return self.size 
     444 
     445 
     446    def isrunning(self): 
     447        """ 
     448        See if the thread running 
     449        """ 
     450        return self.running 
    308451 
    309452 
     
    325468            self.base_url = self.cachefd.readline().strip('\n') 
    326469            self.base_path = self.cachefd.readline().strip('\n') 
     470            self.downloader = None 
    327471        except IOError, why: 
    328472            self._login() 
     473 
     474 
     475    @benchmark(benchmarking, benchmarkcall) 
     476    def shutdown(self): 
     477        """ 
     478        Shutdown the lasf.fm webservices 
     479        """ 
     480        print 'LastFMWebServices.shutdown' 
     481        if self.downloader is not None: 
     482            self.downloader.stop() 
    329483 
    330484 
     
    341495        @returns: reply from request 
    342496        """ 
    343         _debug_('url=%r, data=%r' % (url, data), 1) 
    344         print 'DJW:url=%r, data=%r' % (url, data) 
    345         if lines: 
    346             reply = [] 
    347             try: 
    348                 lines = urllib.urlopen(url, data).readlines() 
    349                 if lines is None: 
    350                     return [] 
    351                 for line in lines: 
    352                     reply.append(line.strip('\n')) 
    353             except Exception, why: 
    354                 _debug_('%s: %s' % (url, why), DWARNING) 
    355                 raise 
    356             _debug_('reply=%r' % (reply,), 1) 
    357             #DJW# 
    358             import pprint 
    359             if len(reply) == 1: 
    360                 print 'DJW:reply', reply 
     497        httplib.HTTPConnection.debuglevel = 1 
     498        try: 
     499            _debug_('url=%r, data=%r' % (url, data), 1) 
     500            request = urllib2.Request(url) 
     501            opener = urllib2.build_opener(SmartRedirectHandler()) 
     502            if lines: 
     503                reply = [] 
     504                try: 
     505                    f = opener.open(request) 
     506                    lines = f.readlines() 
     507                    if lines is None: 
     508                        return [] 
     509                    for line in lines: 
     510                        reply.append(line.strip('\n')) 
     511                except httplib.BadStatusLine, why: 
     512                    print 'BadStatusLine:', why 
     513                    reply = None 
     514                except AttributeError, why: 
     515                    reply = None 
     516                except Exception, why: 
     517                    _debug_('%s: %s' % (url, why), DWARNING) 
     518                    raise 
     519                _debug_('reply=%r' % (reply,), 1) 
     520                return reply 
    361521            else: 
    362                 print 'DJW:reply:', len(reply), 'lines' 
    363                 pprint.pprint(lines) 
    364             #DJW# 
    365             return reply 
    366         else: 
    367             reply = '' 
    368             try: 
    369                 reply = urllib.urlopen(url, data).read() 
    370             except Exception, why: 
    371                 _debug_('%s: %s' % (url, why), DWARNING) 
    372                 raise 
    373             _debug_('reply=%r' % (reply,), 1) 
    374             return reply 
     522                reply = '' 
     523                try: 
     524                    f = opener.open(request) 
     525                    reply = f.read() 
     526                except Exception, why: 
     527                    _debug_('%s: %s' % (url, why), DWARNING) 
     528                    raise 
     529                _debug_('len(reply)=%r' % (len(reply),), 1) 
     530                return reply 
     531        finally: 
     532            httplib.HTTPConnection.debuglevel = 0 
    375533 
    376534 
     
    413571        if not self.session: 
    414572            self._login() 
    415         request_url = 'http://%s%s/xspf.php?sk=%s&discovery=0&desktop=%s' % \ 
     573        #request_url = 'http://%s%s/xspf.php?sk=%s&discovery=0&desktop=%s' % \ 
     574        request_url = 'http://%s%s/xspf.php?sk=%s&discovery=1&desktop=%s' % \ 
    416575            (self.base_url, self.base_path, self.session, LastFMWebServices._version) 
    417576        return self._urlopen(request_url, lines=False) 
     
    453612 
    454613    @benchmark(benchmarking, benchmarkcall) 
    455     def download_cover(self, image_url): 
    456         """ 
    457         Download album cover to freevo cache directory 
    458         XXX don't need to save a file, can use the image directly using imlib2 
    459         """ 
    460         _debug_('download_cover(image_url=%r)' % (image_url,), 2) 
    461  
    462         os.system('rm -f %s' % os.path.join(config.FREEVO_CACHEDIR, 'lfmcover_*.jpg')) 
     614    def download(self, url, filename): 
     615        """ 
     616        Download album cover or track to last.fm directory. 
     617 
     618        Add the session as a cookie to the request 
     619 
     620        @param url: location of item to download 
     621        @param filename: path to downloaded file 
     622        """ 
     623        _debug_('download(url=%r, filename=%r)' % (url, filename), 1) 
    463624        if not self.session: 
    464625            self._login() 
    465         try: 
    466             pic_file = self._urlopen(image_url, lines=False) 
    467             filename = os.path.join(config.FREEVO_CACHEDIR, 'lfmcover_'+str(time.time())+'.jpg') 
    468             save = open(filename, 'w') 
    469             try: 
    470                 print >>save, pic_file 
    471                 return filename 
    472             finally: 
    473                 save.close() 
    474         except IOError: 
    475             return None 
     626        headers = { 
     627            'Session': self.session, 
     628        } 
     629        self.downloader = LastFMDownloader(url, filename, headers) 
     630        self.downloader.start() 
     631        return self.downloader 
    476632 
    477633 
     
    525681        * AUTH1: md5( md5(password) + Timestamp), An md5 sum of an md5 sum of the password, plus the timestamp as salt. 
    526682        * AUTH2: Second possible Password. The client uses md5( md5(toLower(password)) + Timestamp) 
    527         * PLAYER: See Appendix  
    528         """ 
    529         return True 
     683        * PLAYER: See Appendix 
     684        """ 
     685        timestamp = time.strftime('%s', time.gmtime(time.time())) 
     686        username = config.LASTFM_USER 
     687        password = config.LASTFM_PASS 
     688        auth = md5.new(md5.new(password).hexdigest()+timestamp).hexdigest() 
     689        auth2 = md5.new(md5.new(password.lower()).hexdigest()+timestamp).hexdigest() 
     690        url = 'http://ws.audioscrobbler.com//ass/pwcheck.php?' + \ 
     691            'time=%s&' % timestamp + \ 
     692            'username=%s&' % username + \ 
     693            'auth=%s&' % auth + \ 
     694            'auth2=%s&' % auth2 + \ 
     695            'defaultplayer=fvo' 
     696        return self._urlopen(url) 
    530697 
    531698 
     
    537704    XSPF is documented at U{http://www.xspf.org/quickstart/} 
    538705    """ 
    539     _LASTFM_NS = 'http://www.audioscrobbler.net/dtd/xspf-lastfm/' 
     706    _LASTFM_NS = 'http://www.audioscrobbler.net/dtd/xspf-lastfm' 
    540707 
    541708    @benchmark(benchmarking, benchmarkcall) 
     
    564731            for track_elem in tracklist.findall('track'): 
    565732                track = feedparser.FeedParserDict() 
     733                track_map = dict((c, p) for p in track_elem.getiterator() for c in p) 
    566734                title = track_elem.find('title') 
    567735                track.title = title is not None and title.text or u'' 
     
    576744                duration_ms = track_elem.find('duration') 
    577745                track.duration = duration_ms is not None and int(float(duration_ms.text)/1000.0+0.5) or 0 
     746                trackauth = track_elem.find('{%s}trackauth' % LastFMXSPF._LASTFM_NS) 
     747                track.trackauth = trackauth is not None and trackauth.text or u'' 
    578748                self.entries.append(track) 
    579749        return self 
     750 
     751 
     752 
     753if __name__ == '__main__': 
     754    """ 
     755    To run this test harness need to have defined in local_conf.py: 
     756 
     757        - LASTFM_USER 
     758        - LASTFM_PASS 
     759        - LASTFM_LANG 
     760    """ 
     761 
     762    station = 'lastfm://globaltags/jazz' 
     763    station_url = urllib.quote_plus(station) 
     764    webservices = LastFMWebServices() 
     765    print webservices.test_user_pass() 
     766    print webservices._login() 
     767    print webservices.adjust_station(station_url) 
     768    print webservices.request_xspf() 
     769    print 'sleep(10)' 
     770    time.sleep(10) 
     771    for i in range(3): 
     772        xspf = self.webservices.request_xspf() 
     773        print xspf 
     774        if xspf != 'No recs :(': 
     775            break 
     776        time.sleep(2) 
     777    else: 
     778        print 'Failed to get second playlist'