# -*- coding: utf-8 -*-
# Elisa - Home multimedia server
# Copyright (C) 2006-2008 Fluendo Embedded S.L. (www.fluendo.com).
# All rights reserved.
#
# This file is available under one of two license agreements.
#
# This file is licensed under the GPL version 3.
# See "LICENSE.GPL" in the root of this distribution including a special
# exception to use Elisa with Fluendo's plugins.
#
# The GPL part of Elisa is also available under a commercial licensing
# agreement from Fluendo.
# See "LICENSE.Elisa" in the root directory of this distribution package
# for details on that license.

"""
AudioScrobbler music stats submission
"""

__maintainer__ = "Philippe Normand <philippe@fluendo.com>"

from elisa.base_components import service_provider
from elisa.core import log, common, component, player
from elisa.core.observers.observer import Observer
from elisa.core.application import ComponentsLoaded
from twisted.internet import defer, threads

import gst
import urllib2, urllib, threading
import re, time, md5, os, socket
from cPickle import dump, load


plugin_registry = common.application.plugin_registry
PlayerModel = plugin_registry.get_component_class('base:player_model_message')

UNKNOWN = 'unknown'
SAVED_TRACKS_FILE = os.path.expanduser('~/.elisa/unsubmitted_tracks.txt')

socket.setdefaulttimeout(7)

class ScrobbledTrack:
    def __init__(self, media, length):
        if media.artist != 'unknown artist':
            self.artist = media.artist
        else:
            self.artist = UNKNOWN
        if media.album != 'unknown album':
            self.album = media.album
        else:
            self.album = UNKNOWN
        self.name = media.song
        self.length = str(length)

        # MusicBrainz not supported yet
        self.mbid = None
        
        self.date = re.sub("(\d\d\d\d-\d\d-\d\d)T(\d\d:\d\d:\d\d).*","\\1 \\2",
                           time.strftime("%Y-%m-%dT%H:%M:%SZ",time.gmtime()))

    def isSubmitable(self):
        return UNKNOWN not in (self.artist, self.album)

    def __repr__(self):
        return "%r by %r from %r (%r @ %r)" % (self.name, self.artist,
                                               self.album,
                                               self.length, self.date)

    def urlencoded(self, num):
        encode = ""

        encode += "a["+str(num)+"]="+urllib.quote_plus(self.artist.encode('utf-8'))
        encode += "&t["+str(num)+"]="+urllib.quote_plus(self.name.encode('utf-8'))
        encode += "&l["+str(num)+"]="+urllib.quote_plus(self.length)
        encode += "&i["+str(num)+"]="+(self.date)
        if self.mbid is not None:
            encode += "&m["+str(num)+"]="+urllib.quote_plus(self.mbid)
        else:
            encode += "&m["+str(num)+"]="
        encode += "&b["+str(num)+"]="+urllib.quote_plus(self.album.encode('utf-8'))
        return encode

class PlayerModelObserver(log.Loggable, Observer):
    logCategory = "player_observer"
    
    def __init__(self, service):
        Observer.__init__(self)
        self._service = service
        self._submitted_uris = []
        self._current_uri = None
        
    def attribute_set(self, origin, key, old_value, new_value):
        self.log("New new_value for %r: %r", key, new_value)

        if key == 'position' and new_value > 0:
            player_model = self._observable

            uri = player_model.uri
            self._current_uri = uri
            
            if uri not in self._submitted_uris:
                length = player_model.duration / gst.SECOND
                status = new_value / gst.SECOND

                if status > length/2.:
                    media_manager = common.application.media_manager
                    media = media_manager.get_media_information(uri, extended=True)
                    self.debug("Found by media_manager: %r", media)
                    if media and hasattr(media, 'artist'):
                        self._submitted_uris.append(uri)
                        track = ScrobbledTrack(media, length)
                        threads.deferToThread(self._service.submit, track)
                else:
                    self.log("Not yet ready to report")
            else:
                self.log("Found new track, will report it soon")
        elif key == 'state' and new_value == player.STATES.STOPPED:
            current = self._current_uri
            if current is not None and current in self._submitted_uris:
                self.debug("Removing reference to %r", current)
                self._submitted_uris.remove(current)
            self._current_uri = None

class LastfmScrobbler(service_provider.ServiceProvider):
    """
    DOCME
    """

    config_doc = {'user': 'Last.FM username',
                  'password': 'Last.FM password for the user :-)'
                  }

    default_config = {'user': 'fill_me',
                      'password': 'fill_me'
                      }

    def initialize(self):
        user = self.config.get('user')
        if user == 'fill_me':
            msg = "Please configure your Last.FM account settings"
            raise component.InitializeFailure(self.name, msg)
        self.url = "http://post.audioscrobbler.com/"
        self.user = user
        self.password = self.config.get('password')
        self.client = "eli"
        self.version = "0.1"
        self.lock = threading.Lock()
        self.loadSavedTracks()

    def start(self):
        threads.deferToThread(self.handshake)
        common.application.bus.register(self._register_player_model,
                                        PlayerModel)

    def stop(self):
        self.saveTracks()

    def _register_player_model(self, msg, sender):
        self.debug("Got player model %r", msg.player_model)
        self._player_observer = PlayerModelObserver(self)
        msg.player_model.add_observer(self._player_observer)

    def loadSavedTracks(self):
        self.tracksToSubmit = []
        if os.path.exists(SAVED_TRACKS_FILE):
            f = open(SAVED_TRACKS_FILE, 'r')
            try:
                self.tracksToSubmit = load(f)
            finally:
                f.close()

    def saveTracks(self):
        if self.tracksToSubmit:
            self.debug('Saving %s tracks' % len(self.tracksToSubmit))
        f = open(SAVED_TRACKS_FILE, 'w')
        dump(self.tracksToSubmit, f)
        f.close()

    def handshake(self):
        self.logged = False
        self.debug("Handshaking...")
        url = self.url+"?"+urllib.urlencode({
            "hs":"true",
            "p":"1.1",
            "c":self.client,
            "v":self.version,
            "u":self.user
            })

        try:
            result = urllib2.urlopen(url).readlines()
        except urllib2.URLError, ex:
            self.debug("Could not connect to AudioScrobbler: %s", ex.reason.message)
        except Exception, ex:
            self.debug("Could not connect to AudioScrobbler: %s", ex)
        else:
            status = result[0]
            if status.startswith("BADUSER"):
                return self.baduser(result[1:])
            if status.startswith("FAILED"):
                return self.failed(result)
            self.logged = True
            if status.startswith("UPTODATE") or status.startswith("UPDATE"):
                return self.uptodate(result[1:])
            return True
        return False

    def uptodate(self, lines):
        self.md5 = re.sub("\n$","", lines[0])
        self.debug("MD5 = %r" % self.md5)
        self.submiturl = re.sub("\n$","", lines[1])
        self.debug("submiturl = %r" % self.submiturl)
        self.interval(lines[2])
        return True

    def baduser(self, lines):
        self.debug("Bad user")
        self.interval(lines[1])
        return False

    def failed(self, lines):
        self.debug('Failed : %s' % lines[0])
        self.interval(lines[1])
        return False

    def interval(self, line):
        match = re.match("INTERVAL (\d+)", line)
        if match is not None:
            secs = int(match.group(1))
            self.debug("Sleeping a while (%s seconds)" % secs)
            time.sleep(secs)

    def submit(self, track):
        if not self.logged:
            # try to log
            if not self.handshake():
                if track not in self.tracksToSubmit:
                    self.debug("Queued submission of track %s" % track)
                    self.tracksToSubmit.append(track)
                self.debug("%s track(s) currently queued" % len(self.tracksToSubmit))
                self.saveTracks()
                return

        if track not in self.tracksToSubmit:
            self.tracksToSubmit.append(track)

        self.debug("Will try to submit tracks : %s" % str(self.tracksToSubmit))

        try:
            md5response = md5.md5(md5.md5(self.password).hexdigest()+self.md5).hexdigest()
            self.debug('md5response: %s' % md5response)
            
            post = "u="+self.user+"&s="+md5response
            count = 0
            self.debug('post: %r' % post)
            
            for track in self.tracksToSubmit:
                l = int(track.length)
                if not track.isSubmitable():
                    self.debug("Missing informations from track, skipping submit")
                    self.debug(track)
                elif l < 30 or l > (30*60):
                    self.debug("Track is too short or too long, skipping submit")
                    self.debug(track)
                else:
                    self.debug('encoded: %r' % track.urlencoded(count))
                    post += "&"
                    post += track.urlencoded(count)
                    count += 1
        except Exception, ex:
            self.debug('Exception : %s' % ex)
        
        self.debug('count = %s' % count)
        self.debug(post)
        post = unicode(post)
        if count:
            try:
                result = urllib2.urlopen(self.submiturl,post)
            except Exception,ex:
                self.debug(ex)
                self.logged = False
            else:
                results = result.readlines()
                self.debug("submit result : %r" % results)
                if results[0].startswith("OK"):
                    self.interval(results[1])
                    self.tracksToSubmit = []
                    self.saveTracks()
                elif results[0].startswith("FAILED"):
                    self.failed(results)
                    self.handshake()
                elif results[0].startswith("BADAUTH"):
                    self.baduser(results)
                    self.handshake()
