#! /usr/bin/python
# -*- coding: iso-8859-15 -*-

##
## itsgotthevibez.py
##  - It's Got The Vibez, an offline last.fm scrobbler for Trekstor's
##    "Vibez" portable music player.
## See http://www.ohrner.net/ for latest news and updates, please.
## 
## Copyright (C) 2007-2008 Gunter Ohrner "gunter _(@)_ ohrner.net"
##
## 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., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301  USA
##

import os
import socket
import sys
import time

try:
	import cPickle as pickle
except ImportError:
	import pickle

import ConfigParser

import lastfmsubmitter
import itsgotthevibezlib
import utilitylib


## Import listening data import modules.
## This could be made more dynamic in the future
## if additional players are supported.

import importmodule_vibez_privatedb
import importmodule_vibez_musiclog


VERSION='0.2-alpha1'


debug = True
#debug = False



def restoreState(statefile_name):
	'''restore remembered scrobbler state from state file'''
	statefile = None
	try:
		statefile = open(statefile_name, 'r')
		scrobbler_state = pickle.load(statefile)
		return scrobbler_state
	finally:
		utilitylib.safeClose(statefile)



def persistState(statefile_name, scrobbler_state):
	'''store scrobbler state, including date required by currently used
	import modules and the last.fm submit queue, to the state file'''
	statefile = None
	try:
		try:
			statefile = open(statefile_name, 'w')
			pickle.dump(scrobbler_state, statefile)
		finally:
			utilitylib.safeClose(statefile)
	except Exception, e:
		print 'Severe: Could not store state: %s' % (e,)



def submitTracks(scrobbler_state, username, password):
	'''Actually submit the pending queue.'''

	lfms = lastfmsubmitter.LastFmSubmitter(username, password)

	## last.fm is rather slow to respond at times...
	socket.setdefaulttimeout(25.0)

	#tmp = scrobbler_state.agenda[:15]
	#scrobbler_state.agenda = lfms.submitTracks(tmp) + scrobbler_state.agenda[15:]
	try:
		lfms.submitTracks(scrobbler_state.agenda)
		## If we got no exception, all pending sumbissions have
		## been transmitted successfully. :-)
		scrobbler_state.agenda = []

	except lastfmsubmitter.LastFmSubmissionException, e:
		## A problem occured! Rescue submit log.
		scrobbler_state.agenda = e.getPendingSubmissions()

		print ('Error while submitting tracks (%d plays still unsubmitted): '
					 '"%s"') % (len(scrobbler_state.agenda), e.getCause())

		raise itsgotthevibezlib.ItsGotTheVibezLastfmSubmissionException(
			'Multiple failures occured while submitting '
			'data to last.fm.', e)

	except lastfmsubmitter.LastFmException, e:
		raise itsgotthevibezlib.ItsGotTheVibezLastfmSubmissionException(
			'A problem occured while talking to last.fm', e)



def printAgenda(agenda):
	'''Output a list of TrackSubmitRecords to standard output.'''
	for submit_record in agenda:
		print '%s: %s by %s (Album: %s)' \
					% (time.ctime(submit_record.play_timestamp),
						 utilitylib.emptyIfNone(submit_record.title, 'unknown track'),
						 utilitylib.emptyIfNone(submit_record.artist, 'an unknown artist'),
						 utilitylib.emptyIfNone(submit_record.album, 'unknown'))
	print '%d entries in submission queue.' % len(agenda)



def migrateScrobblerStateIfNeccessary(app_object):
	'''This is function is responsible to migrate a stored application
	state object to a new version if neccessary.
	
	It may require slightly fragile code dependant which at least for
	the state object version 1 -> 2 migration depends on internals of
	the privatedb import module. This is caused by a design deficiency
	in the original scrobbler state file format (v1) and should not
	happen in the future.

	For future conversions from state file format v2 onwards, this
	method will only have to migrate changes to the scrobbler\'s
	state data, while changes to the import module\'s state objects
	are completely independant from this and will have to be
	performed by the affected import module itself. This separation
	of concerns should keep possible future migration code as clean
	and localized as possible.'''

	state_object = app_object.scrobbler_state

	if 'version' not in state_object.__dict__:
		print 'Migrating original format state file to version 2 format...'
		## We encountered a version 1 (original format) state file!
		## Migrate to version 2
		state_object.version = 2
		## v1 only knows the privatedb import module and keeps
		## its state information in the main state object.
		## Separate it.
		## This code performs surgery in the guts of an import module's
		## state object. :-(
		## Fortunately something like that will never be neccessary for
		## migration from v2 to future versions! :-)
		privatedb_state = importmodule_vibez_privatedb.ImPersistentStateImpl()
		privatedb_state.known_tracks = state_object.known_tracks
		state_object.known_tracks = None
		state_object.module_states = {
			importmodule_vibez_privatedb.IMPORT_MODULE_NAME: privatedb_state }

		## Finished migration. Now we have a valid v2 state file carrying
		## a valid v1 private db import module state object. :-)
		print 'File format migration successful.'



def getCommandLineParser(default_config_file, default_config):
	'''Initializes and returns the command line parser.'''
	from optparse import OptionParser
	
	parser = OptionParser(usage = '%prog [options]',
												version = '%prog' + (' %s' % VERSION))
	
	parser.add_option(
		'-c', '--config-file', dest='config_file',
		help='Path/filename of an alternate configuration file. '
		'Default is "%s"' % default_config_file,
		metavar='CONFIGFILE', default=default_config_file)
	parser.add_option(
		'-s', '--state-file', dest='state_file',
		help='Path/filename of the file that It\'sGotTheVibez uses '
		'to remember its internal track submission state. '
		'The default is "%s".' % default_config['state_file'],
		metavar='STATEFILE')
	parser.add_option(
		'-u', '--username', dest='username',
		help='Your last.fm user name',
		metavar='USERNAME')
	parser.add_option(
		'-p', '--password', dest='password',
		help='Your last.fm password. YOU SHOULD NOT SPECIFY THIS '
		'ON THE COMMAND LINE on a multi-user computer, as other '
		'users can read your password eg. using the system '
		'utility "ps" while It\'sGotTheVibez runs!',
		metavar='PASSWORD')
	parser.add_option(
		'-d', '--dry-run', dest='dry_run',
		help='Test run: Do everything as normal, except transmitting '
		'anything to last.fm or updating your state.',
		action='store_true')
	parser.add_option(
		'-l', '--dont-submit', dest='dont_submit',
		help='Do not try to submit anything to last.fm ("local" mode). '
		'This flag '
		'is intended to be used if you want to import new track '
		'listens from your Vibez into the submit queue but currently '
		'have not internet connection.',
		action='store_true')
	parser.add_option(
		'-n', '--dont-read-events', dest='dont_read_events',
		help='Do not read the Vibez\' db, you can use this flag if '
		'you want to submit pending tracks from your current submit '
		'queue only, but do not want to attach your Vibez to your '
		'computer.',
		action='store_true')
	parser.add_option(
		'-r', '--remove-pending-events', dest='remove_pending_events',
		help='POTENTIALLY DANGEROUS OPTION! '
		'Asks the selected import module to forget all pending '
		'listeing events. This will delete pending listening '
		'events from your music player without submitting anything '
		'to last.fm, ie. the listening event will be LOST. '
		'It\'sGotTheVibez\'s pending submissions queue is NOT affected '
		'by this command.',
		action='store_true')
	parser.add_option(
		'--list-pending-submissions', dest='list_pending_submissions',
		help='List all pending submissions before actually transmitting '
		'them to last.fm. This option is probably most useful in '
		'conjunction with "-l".',
		action='store_true')

	return parser



class ItsGotTheVibez( object ):
	"""Class representing It'sGotTheVibez."""
	
	default_config = { 'state_file':  '~/.itsgotthevibez/scrobblerstate.bin',
										 'username': '',
										 'password': '' }

	import_modules = { importmodule_vibez_privatedb.IMPORT_MODULE_NAME:
										 importmodule_vibez_privatedb,
										 importmodule_vibez_musiclog.IMPORT_MODULE_NAME:
										 importmodule_vibez_musiclog }

	def __init__(self):
		## Set application state to undefined values
		## to make sure attributes exist.
		self.options = None

		try:
			self.scrobbler_state = None
			try:
				self.readConfiguration()

				self.checkAndPrepareRunEnvironment()

				self.scrobbler_state = self.restoreScrobblerState()

				migrateScrobblerStateIfNeccessary(self)

				self.volatile_module_states = {}

				selected_module_id = self.options.import_module
				
				if not self.options.dont_read_events:
					## process options for selected import module
					import_module = self.import_modules[selected_module_id]
					module_states = self.scrobbler_state.module_states

					## New module or virgin scrobbler state object?
					if selected_module_id not in module_states:
						module_states[selected_module_id] = None

					(im_volatile_state, im_scrobbler_state) \
					= import_module.initImportModule(
						self.options,
						module_states[selected_module_id])

					self.scrobbler_state.module_states[selected_module_id] \
						= im_scrobbler_state
					self.volatile_module_states[selected_module_id] \
						= im_volatile_state

				self.performRequestedScrobblerAction(selected_module_id)

			finally:
				if self.scrobbler_state != None:
					self.persistScrobblerState(self.scrobbler_state)

			print 'Finished successfully, exiting.'

		except itsgotthevibezlib.ItsGotTheVibezException, e:
			print 'Ups, an error occured!'
			print str(e)
			sys.exit(e.getErrorCode())



	def readConfiguration(self):
		self.config_dir = os.path.expanduser('~/.itsgotthevibez')
		self.config_file = os.path.expanduser('~/.itsgotthevibez/config')

		parser = getCommandLineParser(self.config_file, self.default_config)

		self.default_config['import_module'] = self.import_modules.keys()[0]
		parser.add_option(
			'-m', '--import-module', dest='import_module',
			help='Listen event import module to use. '
			'This basically specifies from where you want '
			'to get the data which should be submitted '
			'to last.fm. The following modules are '
			'available, the first of which is the '
			'default module if none is specified: '
			+ ', '.join(self.import_modules.keys()),
			metavar='MODULE_ID',
			default=self.default_config['import_module'])
		
		## add options from import modules
		for import_module in self.import_modules.values():
			import_module.publishOptions(parser, self.default_config)

		(options, args) = parser.parse_args()

		config = ConfigParser.ConfigParser(self.default_config)
		config.read([options.config_file])

		## Iterate over all option names, fetching values from
		## config file if not specified on the command line.
		## Values are userpath expanded, ie. a leading '~' is replaced
		## by the current user's home directory and a leading '~NAME'
		## is expanded by user NAME's home directory.
		for field in options.__dict__:
			config_value = options.__dict__[field]
			## fetch value from config file if neccessary and available
			if config_value == None \
				 and config.has_option('DEFAULT', field):
				config_value = config.get('DEFAULT', field)

			if config_value != None \
					 and not isinstance(config_value, bool):
				options.__dict__[field] = os.path.expanduser(config_value)

		options.submit_tracks = not options.dont_submit

		self.options = options


	
	def checkAndPrepareRunEnvironment(self):
		options = self.options

		error_messages = []
		
		if options.submit_tracks \
				 and (options.username == ''
							or options.password == ''):
			error_messages.append(
				'Username or password are missing, '
				'both MUST be defined to be able'
				'to submit anything to last.fm.')
		
		if not self.options.dont_read_events \
				 and options.import_module not in self.import_modules:
			error_messages.append(
				'The specified import module %s does not exist.'
				% options.import_module)
		
		if options.dont_read_events and options.remove_pending_events:
			error_messages.append(
				'Removing listening events without reading them is '
				'not possible. You cannot specify both -n and -r.')

		## Configuration errors where found, inform the user.
		if len(error_messages) != 0:
			raise itsgotthevibezlib.ItsGotTheVibezConfigurationException(
				'\n'.join(error_messages))



	def performRequestedScrobblerAction(self, import_module_id):
		"""Dispatch action requested by the user."""
		options = self.options

		if not self.options.dont_read_events:
			import_module = self.import_modules[import_module_id]
			im_volatile_state = self.volatile_module_states[import_module_id]
			im_module_state = self.scrobbler_state.module_states[import_module_id]
			new_submit_records = import_module.importListeningEvents(
				options,
				im_volatile_state, im_module_state)

			print 'Import module %s returned %d new listening events.' \
						% (import_module_id, len(new_submit_records))

			self.scrobbler_state.agenda += new_submit_records

		if self.options.list_pending_submissions:
			printAgenda(self.scrobbler_state.agenda)
		
		if self.options.submit_tracks and not self.options.dry_run:
			submitTracks(self.scrobbler_state,
									 options.username, options.password)



	def restoreScrobblerState(self):
		if not os.path.exists(self.options.state_file):
			## Statefile could not be found, so simulate reading
			## an empty one.
			print 'No statefile found, starting with a fresh one.'
			return itsgotthevibezlib.ScrobblerState()

		## Statefile exists...

		try:
			return restoreState(self.options.state_file)
		except StandardError, e:
			raise itsgotthevibezlib.ItsGotTheVibezRuntimeException(
				'Problem while loading state from file "%s": %s'
				% (self.options.state_file, e))



	def persistScrobblerState(self, scrobbler_state):
		## try to persist the current scrobbler state
		if self.options.dry_run:
			print 'Dry-Run: Not saving state.'
		else:
			print 'Saving state...'
			persistState(self.options.state_file, scrobbler_state)
			print 'Done.'





####################################################################
### Compatibility Cruft
####################################################################

class ScrobblerState( itsgotthevibezlib.ScrobblerState ):
	"""This class existst for compatibility with older It'sGotTheVibez
	versions only. It allows pickle to unserialize an "version 1"
	scrobbler state file."""
	pass

class TrackPlayState( importmodule_vibez_privatedb.TrackPlayState ):
	"""This class existst for compatibility with older It'sGotTheVibez
	versions only. It allows pickle to unserialize an "version 1"
	scrobbler state file."""
	pass

####################################################################
### Compatibility Cruft
####################################################################





if __name__ == '__main__':
	#state = restoreState('/home/gunter/.itsgotthevibez/testuser.bin')
	#print dir(state)
	#print 'version' in state.__dict__
	#state.version = 2
	#print 'version' in state.__dict__
	ItsGotTheVibez()
