#!/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

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 is 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 is 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 is None:
			songTuple = self.getLastRecentSong()

		if songTuple is not 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) 
