[shtrom] ability to get last played music from lastfm. fixes #3559

This commit is contained in:
Yann Leboulanger 2007-11-19 09:22:26 +00:00
parent 50fbbe9b7d
commit 39a03aa369
4 changed files with 352 additions and 3 deletions

View File

@ -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 ],

217
src/lastfm.py Normal file
View File

@ -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)

View File

@ -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()

View File

@ -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()