#!/usr/bin/python

import sys
import os
import time
import getopt
import signal

import mpdclient2
import lastfm
import lastfm.config
import lastfm.marshaller
from lastfm.config import NoConfError

USAGE = """usage: lastmp [--debug] [--no-daemon] [--help]"""

# If the system load is high, we may not wake again until slightly
# longer than we slept. This should really be like 0.1, but
# unfortunately MPD only reports times with a resolution of 1 second.

FUZZ = 1

class NoHostError(Exception): pass
class MPDPermissionError(Exception): pass

class Song:
    def __init__(self, sobj):
        self.artist = getattr(sobj, 'artist', '')
        self.title = getattr(sobj, 'title', '')
        self.album = getattr(sobj, 'album', '')
        self.length = int(getattr(sobj, 'time', 0))
        self.file = getattr(sobj, 'file', '')

    def __eq__(self, other):
        if other == None:
            return False
        else:
            return self.file == other.file or \
                (self.artist == other.artist and self.title == other.title and
                self.album == other.album and self.length == other.length)

    # O RLY?
    def __ne__(self, other):
        # YA RLY!
        return not self == other

    def __str__(self):
        time = '%d:%02d' % divmod(self.length, 60)
        if self.artist and self.title:
            return '%s - %s [%s]' % (self.artist, self.title, time)
        else:
            return '%s [%s]' % (self.file, time)

    def dict(self):
        d = {
            'time': time.gmtime(),
            'artist': lastfm.marshaller.guess_enc(self.artist, 'utf-8'),
            'title': lastfm.marshaller.guess_enc(self.title, 'utf-8'),
            'length': self.length,
            }
        if self.album:
            d['album'] = lastfm.marshaller.guess_enc(self.album, 'utf-8')
        for reqd in ('artist', 'title', 'length'):
            if not d[reqd]:
                raise ValueError('%s is missing %s' % (self.file, reqd))
        else:
            return d

class MPDMonitor:
    def __init__(self, log, sleep, host, port, password):
        self.log = log
        self.sleep = sleep
        self.mpd_args = {'host': host, 'port': port, 'password': password}
        self.mpd = None

    def wake(self):
        status = self.mpd.do.status()
        song = Song(self.mpd.do.currentsong())

        if not hasattr(status, 'state'):
            raise MPDPermissionError

        if not self.prevstatus or status.state != self.prevstatus.state:
            self.log.debug('Changed state: %s' % status.state)

        if status.state in ('play', 'pause'):
            pos, length = map(float, status.time.split(':'))
            if status.state == 'play':
                if song != self.prevsong or \
                        self.prevstatus.state == 'stop':
                    self.log.info('New song: %s', song)
                    if (self.prevsong and pos > self.sleep +
                            FUZZ + int(status.xfade)) or \
                            (self.prevsong is None and
                            pos/length > lastfm.SUB_PERCENT or
                            pos > lastfm.SUB_SECONDS):
                        self.log.warning(
                            'Started at %d, will not submit' % pos)
                        self.skipped = True
                    else:
                        self.skipped = False
                        self.submitted = False
                        self.played_enough = False
                else:
                    if self.prevpos and pos < self.sleep + FUZZ and \
                            (self.prevpos/length >= lastfm.SUB_PERCENT or \
                            self.prevpos >= lastfm.SUB_SECONDS):
                        self.log.info('Restarted song')
                        self.skipped = False
                        self.submitted = False
                        self.played_enough = False
                    if self.prevpos and \
                            pos > self.prevpos + self.sleep + FUZZ:
                        self.log.warning(
                            'Skipped from %d to %d' %
                            (self.prevpos, pos))
                        self.skipped = True
                    if not self.skipped and not self.played_enough and \
                            length >= lastfm.MIN_LEN and \
                            length <= lastfm.MAX_LEN and \
                            (pos >= lastfm.SUB_SECONDS or \
                            pos/length >= lastfm.SUB_PERCENT):
                        self.log.debug('OK, %d/%d played' % (pos, length))
                        self.played_enough = True
                    if not self.submitted and not self.skipped and \
                            self.played_enough:
                        self.submit(song)
                        self.submitted = True
        else:
            pos = None

        self.prevsong = song
        self.prevstatus = status
        self.prevpos = pos

    def observe(self):
        """Loop forever, periodically checking MPD's state and
        submitting songs when necessary."""
        while True:
            try:
                if not self.mpd:
                    self.mpd = mpdclient2.connect(**self.mpd_args)
                    self.log.info('Connected to MPD')
                    self.prevstatus = None
                    self.prevsong = None
                    self.prevpos = None
                    self.skipped = False
                    self.submitted = False
                    self.played_enough = False
                if self.mpd:
                    self.wake()
            except (EOFError, mpdclient2.socket.error):
                self.log.error("Can't connect or lost connection to MPD")
                self.mpd = None
            except MPDPermissionError:
                self.log.error("Can't read info from MPD (bad password?)")

            time.sleep(self.sleep)

    def submit(self, song):
        try:
            sub = song.dict()
            self.log.info('Sending to daemon')
            lastfm.submit([sub])
        except ValueError, e:
            self.log.error('Missing tags: %s' % e)
        except IOError:
            log.error("Can't write sub: %s" % e)

class MPDConfig(lastfm.config.Config):
    def __init__(self, name):
        lastfm.config.Config.__init__(self, name)
        try:
            self.host = self.cp.get('mpd', 'host')
        except:
            raise NoHostError
        try:
            self.port = self.cp.getint('mpd', 'port')
        except:
            self.port = 6600
        try:
            self.password = self.cp.get('mpd', 'password')
        except:
            self.password = None

def daemon(log, conf):
    mo = MPDMonitor(log, conf.sleep_time, conf.host, conf.port, conf.password)

    def shutdown(signum, frame):
        conf.remove_pidfile()
        sys.exit(0)

    signal.signal(signal.SIGTERM, shutdown)
    signal.signal(signal.SIGINT, shutdown)

    # Even if we couldn't connect, start the observe loop anyway.
    # Perhaps MPD failed at boot and the admin will fix it later.
    mo.observe()

if __name__ == '__main__':
    shortopts = 'dnh'
    longopts = ['debug', 'no-daemon', 'help']

    try:
        opts, args = getopt.getopt(sys.argv[1:], shortopts, longopts)
    except getopt.GetoptError, e:
        print >>sys.stderr, 'lastmp: %s' % e
        print >>sys.stderr, USAGE
        sys.exit(1)

    debug = False
    fork = True
    stderr = False

    for opt, arg in opts:
        if opt in ('--debug', '-d'):
            debug = True
        elif opt in ('--no-daemon', '-n'):
            fork = False
            stderr = True
        elif opt in ('--help', '-h'):
            print USAGE
            sys.exit(0)

    try:
        conf = MPDConfig('lastmp')
    except NoConfError:
        print >>sys.stderr, 'lastmp: could not find config'
        sys.exit(1)
    except NoHostError:
        print >>sys.stderr, 'lastmp: no MPD host specified'
        sys.exit(1)

    if conf.debug:
        debug = True

    if fork:
        try:
            pid = os.fork()
            if pid:
                sys.exit(0)
            else:
                conf.write_pidfile()
        except OSError, e:
            print >>sys.stderr, "lastmp: can't fork: %s" % e
            sys.exit(1)

    log = lastfm.logger('lastmp', conf.log_path, debug, stderr)

    try:
        log.info('Starting')
        daemon(log, conf)
    except SystemExit, e:
        log.info('Exiting')
        sys.exit(e.args[0])
    except:
        import traceback
        einfo = traceback.format_exception(*sys.exc_info())
        log.error('Aborting: %s' % ''.join(einfo))
        sys.exit(1)
