[shtrom] ability to get last played music from lastfm. fixes #3559
This commit is contained in:
parent
50fbbe9b7d
commit
39a03aa369
|
@ -153,6 +153,8 @@ class Config:
|
|||
'after_nickname': [ opt_str, ':', _('Characters that are printed after the nickname in conversations') ],
|
||||
'send_os_info': [ opt_bool, True ],
|
||||
'set_status_msg_from_current_music_track': [ opt_bool, False ],
|
||||
'set_status_msg_from_lastfm': [ opt_bool, False, _('If checked, Gajim can regularly poll a Last.fm account and adjust the status message to reflect recently played songs. set_status_msg_from_current_music_track option must be False.') ],
|
||||
'lastfm_username': [ opt_str, '', _('The username used to identify the Last.fm account.')],
|
||||
'notify_on_new_gmail_email': [ opt_bool, True ],
|
||||
'notify_on_new_gmail_email_extra': [ opt_bool, False ],
|
||||
'usegpg': [ opt_bool, False, '', True ],
|
||||
|
|
|
@ -0,0 +1,217 @@
|
|||
#!/bin/env python
|
||||
"""
|
||||
LastFM Python class
|
||||
Copyright (C) 2007 Olivier Mehani <shtrom@ssji.net>
|
||||
|
||||
$Id: lastfm.py 52 2007-11-03 23:19:00Z shtrom $
|
||||
Python class to handily retrieve song information from a Last.fm account.
|
||||
|
||||
This program is free software; you can redistribute it and/or modify
|
||||
it under the terms of the GNU General Public License as published by
|
||||
the Free Software Foundation; either version 2 of the License, or
|
||||
(at your option) any later version.
|
||||
|
||||
This program is distributed in the hope that it will be useful,
|
||||
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
GNU General Public License for more details.
|
||||
|
||||
You should have received a copy of the GNU General Public License
|
||||
along with this program; if not, write to the Free Software
|
||||
Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
|
||||
"""
|
||||
|
||||
__version__ = '$Revision: 64 $'
|
||||
|
||||
from urllib import urlopen
|
||||
from xml.dom import minidom
|
||||
from time import time, strftime
|
||||
|
||||
class LastFM:
|
||||
# Where to fetch the played song information
|
||||
LASTFM_FORMAT_URL = \
|
||||
'http://ws.audioscrobbler.com/1.0/user/%s/recenttracks.xml'
|
||||
# Delay in seconds after which the last song entry is considered too old tox
|
||||
# be displayed.
|
||||
MAX_DELAY = 600
|
||||
|
||||
ARTIST = 0
|
||||
NAME = 1
|
||||
ALBUM = 2
|
||||
TIME = 3
|
||||
|
||||
def __init__(self, username, proxies=None):
|
||||
"""
|
||||
Create a new LastFM object.
|
||||
|
||||
username, the Last.fm username
|
||||
proxies, the list of proxies to use to connect to the Last.fm data, as
|
||||
expected by urllib.urlopen()
|
||||
"""
|
||||
self.setUsername(username)
|
||||
self._proxies = proxies
|
||||
self.scrobbling = False
|
||||
self.updateData()
|
||||
|
||||
def __str__(self):
|
||||
return 'Last.fm song tracker for user %s.%s' % (self._username,
|
||||
self.formatSongTitle(
|
||||
' Last song was \"%(n)s\" by \"%(a)s\" in album \"%(b)s\".'))
|
||||
|
||||
def getUsername(self):
|
||||
return self._username
|
||||
|
||||
def setUsername(self, username):
|
||||
self._username = username
|
||||
self.lastSongs = []
|
||||
|
||||
def updateData(self):
|
||||
"""
|
||||
Fetch the last recent tracks list and update the object accordingly.
|
||||
|
||||
Return True if the last played time has changed, False otherwise.
|
||||
"""
|
||||
try:
|
||||
xmldocument = urlopen(self.LASTFM_FORMAT_URL % self._username,
|
||||
self._proxies)
|
||||
xmltree = minidom.parse(xmldocument)
|
||||
except:
|
||||
print 'Error parsing XML from Last.fm...'
|
||||
return False
|
||||
|
||||
if xmltree.childNodes.length != 1:
|
||||
raise Exception('XML document not formed as expected')
|
||||
|
||||
recenttracks = xmltree.childNodes[0]
|
||||
|
||||
tracklist = recenttracks.getElementsByTagName('track')
|
||||
|
||||
# do not update if nothing more has been scrobbled since last time
|
||||
if len(tracklist) > 0 and \
|
||||
int(tracklist[0].getElementsByTagName('date')[0].
|
||||
getAttribute('uts')) != self.getLastScrobbledTime():
|
||||
self.lastSongs = []
|
||||
for track in tracklist:
|
||||
artistNode = track.getElementsByTagName('artist')[0]
|
||||
if artistNode.firstChild:
|
||||
artist = artistNode.firstChild.data
|
||||
else:
|
||||
artist = None
|
||||
|
||||
nameNode = track.getElementsByTagName('name')[0]
|
||||
if nameNode.firstChild:
|
||||
name = nameNode.firstChild.data
|
||||
else:
|
||||
name = None
|
||||
|
||||
albumNode = track.getElementsByTagName('album')[0]
|
||||
if albumNode.firstChild:
|
||||
album = albumNode.firstChild.data
|
||||
else:
|
||||
album = None
|
||||
|
||||
timeNode = track.getElementsByTagName('date')[0]
|
||||
self.lastSongs.append((artist, name, album,
|
||||
int(timeNode.getAttribute('uts'))))
|
||||
self.scrobbling = True
|
||||
return True
|
||||
|
||||
# if nothing has been scrobbled for too long, an update to the
|
||||
# "currently" playing song should be made
|
||||
if self.scrobbling and not self.lastSongIsRecent():
|
||||
self.scrobbling = False
|
||||
return True
|
||||
|
||||
return False
|
||||
|
||||
def getLastSong(self):
|
||||
"""
|
||||
Return the last played song as a tuple of (ARTIST, SONG, ALBUM, TIME).
|
||||
"""
|
||||
if len(self.lastSongs) < 1:
|
||||
return None
|
||||
return self.lastSongs[0]
|
||||
|
||||
def getLastScrobbledTime(self):
|
||||
"""
|
||||
Return the Unix time the last song was played.
|
||||
"""
|
||||
if len(self.lastSongs) < 1:
|
||||
return 0
|
||||
return self.lastSongs[0][self.TIME]
|
||||
|
||||
def timeSinceLastScrobbled(self, lst=None):
|
||||
"""
|
||||
Return the time in seconds since the last song has been scrobbled.
|
||||
|
||||
lst, the Unix time at which a song has been scrobbled, defaults to that
|
||||
of the last song
|
||||
"""
|
||||
if lst == None:
|
||||
lst = self.getLastScrobbledTime()
|
||||
return int(time()) - lst
|
||||
|
||||
def lastSongIsRecent(self, delay=None):
|
||||
"""
|
||||
Return a boolean stating whether the last song has been played less
|
||||
the specified delay earlier.
|
||||
|
||||
delay, the delay to use, defaults to self.MAX_DELAY
|
||||
"""
|
||||
if delay == None:
|
||||
delay = self.MAX_DELAY
|
||||
return self.timeSinceLastScrobbled() < delay
|
||||
|
||||
def getLastRecentSong(self, delay=None):
|
||||
"""
|
||||
Return the last *recently* played song.
|
||||
|
||||
"Recently" means that the song has been played less than delay
|
||||
earlier.
|
||||
|
||||
delay, the delay to use, see lastSongIsRecent for the semantics
|
||||
"""
|
||||
self.updateData()
|
||||
if self.lastSongIsRecent(delay):
|
||||
return self.getLastSong()
|
||||
return None
|
||||
|
||||
def formatSongTitle(self, formatString='%(a)s - %(n)s', songTuple=None):
|
||||
"""
|
||||
Format a song tuple according to a format string. This makes use of the
|
||||
basic Python string formatting operations.
|
||||
|
||||
formatString, the string according to which the song should be formated:
|
||||
"%(a)s" is replaced by the artist;
|
||||
"%(n)s" is replaced by the name of the song;
|
||||
"%(b)s" is replaced by the album;
|
||||
defaults to "%s - %t".
|
||||
songTuple, the tuple representing the song, defaults to the last song
|
||||
"""
|
||||
str = ''
|
||||
if songTuple == None:
|
||||
songTuple = self.getLastRecentSong()
|
||||
|
||||
if songTuple != None:
|
||||
dict = {
|
||||
'a': songTuple[0],
|
||||
'n': songTuple[1],
|
||||
'b': songTuple[2]
|
||||
}
|
||||
str = formatString % dict
|
||||
|
||||
return str
|
||||
|
||||
# Fallback if the script is called directly
|
||||
if __name__ == '__main__':
|
||||
from sys import argv
|
||||
from time import sleep
|
||||
if len(argv) != 2:
|
||||
raise Exception('Incorrect number of arguments. Only the Last.fm username is required.')
|
||||
|
||||
lfm = LastFM(argv[1])
|
||||
print lfm
|
||||
while 1:
|
||||
if lfm.updateData():
|
||||
print lfm.formatSongTitle()
|
||||
sleep(60)
|
|
@ -0,0 +1,103 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
## lastfmtracklistener.py
|
||||
##
|
||||
## Copyright (C) 2007 Olivier Mehani <shtrom-gajim@ssji.net>
|
||||
## Heavily based on music_track_listener.py:
|
||||
## Copyright (C) 2006 Gustavo Carneiro <gjcarneiro@gmail.com>
|
||||
## Copyright (C) 2006 Nikos Kouremenos <kourem@gmail.com>
|
||||
##
|
||||
## This program is free software; you can redistribute it and/or modify
|
||||
## it under the terms of the GNU General Public License as published
|
||||
## by the Free Software Foundation; version 2 only.
|
||||
##
|
||||
## This program is distributed in the hope that it will be useful,
|
||||
## but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
## MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
## GNU General Public License for more details.
|
||||
##
|
||||
import gobject
|
||||
from lastfm import LastFM
|
||||
|
||||
class LastFMTrackInfo(object):
|
||||
__slots__ = ['title', 'album', 'artist']
|
||||
|
||||
def __eq__(self, other):
|
||||
if self.__class__ != other.__class__:
|
||||
return False
|
||||
return self.title == other.title and self.album == other.album and \
|
||||
self.artist == other.artist
|
||||
|
||||
def __ne__(self, other):
|
||||
return not self.__eq__(other)
|
||||
|
||||
class LastFMTrackListener(gobject.GObject):
|
||||
__gsignals__ = {
|
||||
'music-track-changed': (gobject.SIGNAL_RUN_LAST, None, (object,)),
|
||||
}
|
||||
|
||||
# polling period in milliseconds
|
||||
INTERVAL = 60000 #LastFM.MAX_DELAY * 250 # 1/4 of LastFM's delay (in s)
|
||||
|
||||
_instance = None
|
||||
@classmethod
|
||||
def get(cls, username):
|
||||
if cls._instance is None:
|
||||
cls._instance = cls(username)
|
||||
else:
|
||||
cls._instance._lfm.setUsername(username)
|
||||
return cls._instance
|
||||
|
||||
def __init__(self, username):
|
||||
super(LastFMTrackListener, self).__init__()
|
||||
self._lfm_user = username
|
||||
self._lfm = LastFM(self._lfm_user)
|
||||
self._last_playing_music = None
|
||||
self._lastfm_music_track_change()
|
||||
gobject.timeout_add(self.INTERVAL, self._lastfm_periodic_check)
|
||||
|
||||
def _lastfm_properties_extract(self, song_tuple):
|
||||
if song_tuple:
|
||||
info = LastFMTrackInfo()
|
||||
info.title = song_tuple[LastFM.NAME]
|
||||
info.album = song_tuple[LastFM.ALBUM]
|
||||
info.artist = song_tuple[LastFM.ARTIST]
|
||||
return info
|
||||
return None
|
||||
|
||||
def _lastfm_periodic_check(self):
|
||||
if self._lfm.updateData():
|
||||
self._lastfm_music_track_change()
|
||||
return True
|
||||
|
||||
def _lastfm_music_track_change(self):
|
||||
info = self._lastfm_properties_extract(
|
||||
self._lfm.getLastRecentSong())
|
||||
self._last_playing_music = info
|
||||
self.emit('music-track-changed', info)
|
||||
|
||||
def get_playing_track(self):
|
||||
'''Return a LastFMTrackInfo for the currently playing
|
||||
song, or None if no song is playing'''
|
||||
return self._last_playing_music
|
||||
|
||||
# here we test :)
|
||||
if __name__ == '__main__':
|
||||
from sys import argv
|
||||
if len(argv) != 2:
|
||||
raise Exception("Incorrect number of arguments. Only the Last.fm username is required.")
|
||||
|
||||
def music_track_change_cb(listener, music_track_info):
|
||||
if music_track_info is None:
|
||||
print "Stop!"
|
||||
else:
|
||||
print 'Now playing: "%s" by %s' % (
|
||||
music_track_info.title, music_track_info.artist)
|
||||
|
||||
listener = LastFMTrackListener.get(argv[1])
|
||||
listener.connect('music-track-changed', music_track_change_cb)
|
||||
track = listener.get_playing_track()
|
||||
if track is None:
|
||||
print 'Now not playing anything'
|
||||
else:
|
||||
print 'Now playing: "%s" by %s' % (track.title, track.artist)
|
||||
gobject.MainLoop().run()
|
|
@ -60,6 +60,7 @@ from common import dbus_support
|
|||
if dbus_support.supported:
|
||||
from music_track_listener import MusicTrackListener
|
||||
import dbus
|
||||
from lastfm_track_listener import LastFMTrackListener
|
||||
|
||||
import sys
|
||||
if sys.platform == 'darwin':
|
||||
|
@ -3707,6 +3708,26 @@ class RosterWindow:
|
|||
self._music_track_changed_signal = None
|
||||
self._music_track_changed(None, None)
|
||||
|
||||
## enable setting status msg from a Last.fm account
|
||||
def enable_syncing_status_msg_from_lastfm(self, enabled):
|
||||
'''if enabled is True, we start polling the Last.fm server,
|
||||
and we update our status message accordinly'''
|
||||
if enabled:
|
||||
if self._music_track_changed_signal is None:
|
||||
listener = LastFMTrackListener.get(
|
||||
gajim.config.get('lastfm_username'))
|
||||
self._music_track_changed_signal = listener.connect(
|
||||
'music-track-changed', self._music_track_changed)
|
||||
track = listener.get_playing_track()
|
||||
self._music_track_changed(listener, track)
|
||||
else:
|
||||
if self._music_track_changed_signal is not None:
|
||||
listener = LastFMTrackListener.get(
|
||||
gajim.config.get('lastfm_username'))
|
||||
listener.disconnect(self._music_track_changed_signal)
|
||||
self._music_track_changed_signal = None
|
||||
self._music_track_changed(None, None)
|
||||
|
||||
def _change_awn_icon_status(self, status):
|
||||
if not dbus_support.supported:
|
||||
# do nothing if user doesn't have D-Bus bindings
|
||||
|
@ -5423,9 +5444,15 @@ class RosterWindow:
|
|||
## Music Track notifications
|
||||
## FIXME: we use a timeout because changing status of
|
||||
## accounts has no effect until they are connected.
|
||||
gobject.timeout_add(1000,
|
||||
self.enable_syncing_status_msg_from_current_music_track,
|
||||
gajim.config.get('set_status_msg_from_current_music_track'))
|
||||
st = gajim.config.get('set_status_msg_from_current_music_track')
|
||||
if st:
|
||||
gobject.timeout_add(1000,
|
||||
self.enable_syncing_status_msg_from_current_music_track,
|
||||
st)
|
||||
else:
|
||||
gobject.timeout_add(1000,
|
||||
self.enable_syncing_status_msg_from_lastfm,
|
||||
gajim.config.get('set_status_msg_from_lastfm'))
|
||||
|
||||
if gajim.config.get('show_roster_on_startup'):
|
||||
self.window.show_all()
|
||||
|
|
Loading…
Reference in New Issue