#!/usr/bin/env python
# -*- coding: utf-8 -*-

# Copyright (c) 2009, 2010, 2011 Jack Kaliko <efrim@azylum.org> {{{
#
#  This file is part of MPD_sima
#
#  MPD_sima 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 3 of the License, or
#  (at your option) any later version.
#
#  MPD_sima 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 MPD_sima.  If not, see <http://www.gnu.org/licenses/>.
#
#
#  }}}

# DOC
"""
This code is dealing with your MPD server.
It will add automagicaly track to the playlist.
Simply run:
    python mpd_sima

See "python mpd_sima --help" for command line options.

For user instructions please refer to doc/README.*


 Unicode issue.
 --------------
    N.B. : MPD only deals with UTF-8
"""


__version__ = u'0.8.0'
__revison__ = u'$Revision: 543 $'[11:-2]
__author__ = u'$Author: kaliko $'
__date__ = u'$Date: 2011-05-08 13:16:23 +0200 (dim. 08 mai 2011) $'[7:26]
__url__ = u'http://codingteam.net/project/sima'

WAIT_MPD_RESUME = 9

# IMPORTS {{{
import re
import random
import signal
import sys
import time
import traceback

from collections import deque
from difflib import get_close_matches
from hashlib import md5
from urllib import urlopen
from socket import error as SocketError

from lib.daemon import Daemon

from lib.simadb import (SimaDB, SimaDBNoFile, SimaDBUpgradeError)
from lib.simafm import (SimaFM, XmlFMHTTPError, XmlFMNotFound, XmlFMError)
from lib.simastr import SimaStr
from lib.track import Track
from utils.config import ConfMan
from utils.leven import levenshtein_ratio
from utils.logutil import (logger, LEVELS)
from utils.startopt import StartOpt

try:
    from mpd import (MPDClient, ConnectionError, CommandError)
except ImportError, err:
    print 'ERROR: "%s"\n\nPlease install python-mpd module.\n' % err
    sys.exit(1)


class Sima(Daemon):
    """
    Main Object dealing with what and how to queue.
    """

    def __init__(self, config_man, log):
        """
        Declare default or empty attributes
        """
        # set daemon
        Daemon.__init__(self, config_man.config.get('daemon', 'pidfile'))
        # Track objects
        self.current_track = None
        #self.current_last_track = None
        self.current_queue = None
        # Track object we are looking similar art/track for
        self.current_searched = None

        self.tracks_to_add = list()

        ## Conf
        self.config = config_man.config
        self.conf_obj = config_man
        self.log = log
        ## MPD
        self.client = MPDClient()
        self.mpd_host = self.config.get('MPD', 'host')
        self.mpd_port = self.config.getint('MPD', 'port')
        self.is_playing = True
        self.db_update = None
        self.cache_mpd = dict()
        # TODO: Add method to get host/port/password
        # with password as boolean if not set?
        ## SQLite Database
        self.db = SimaDB(db_path=self.conf_obj.userdb_file)
        ## Set queue mode
        self._set_queue_mode()

    def mpd_connect(self):
        """
        Connection.
        """
        try:
            self.client.connect(self.mpd_host, self.mpd_port)
            return True
        except SocketError:
            self.log.error(u'Unable to connect to MPD on %s!' %
                           self.config.get('MPD', 'host'))
            return False
        except ConnectionError, err:
            self.log.error(err)
            return False

    def mpd_disconnect(self):
        """
        Wrapper around MPDClient().disconnect, intercepting errors.
        """
        try:
            self.client.disconnect()
            return True
        except (SocketError, ConnectionError):
            return False

    def mpd_auth(self):
        """
        Password auth.
        """
        try:
            self.config.getboolean('MPD', 'password')
            self.log.info(u'No password set, proceeding without ' +
                          u'authentication...')
        except ValueError:
            # ValueError if password not a boolean, hence an actual password.
            # pretty ugly? TODO: should I change this?
            try:
                self.client.password(self.config.get('MPD', 'password'))
                self.log.debug(u'Auth pass, proceeding...')
            except CommandError, err:
                self.log.error(u'Auth failed with "%s", wrong password?' % err)
                sys.exit(2)

    def mpd_current_song(self):
        """
        Currently played|paused|stopped track infos.
        """
        return Track(**self.client.currentsong())

    def mpd_findtrk(self, mpd_artists):
        """
        Find tracks to play (ie. not in history and etc.) while
        self.tracks_to_add is not reached.
        """
        self.log.debug(u'Looking for these artists (got them reorganized).')
        self.log.debug(u' / '.join(mpd_artists))
        self.tracks_to_add = []
        nbtracks_target = self.config.getint('sima', 'track_to_add')
        for artist in mpd_artists:
            artist_utf8 = artist.encode('utf-8')
            mpd_find = [Track(**track) for track in self.client.find('artist', artist_utf8)]
            self.log.debug(u'Trying to find titles to add for "%s"' %
                           artist)
            random.shuffle(mpd_find)
            unplayed_track = self._extract_unplayed(mpd_find)
            if not unplayed_track:
                self.log.debug(u'Unable to find title to add' +
                              u' for "%s".' % artist)
            else:
                self.tracks_to_add.append(unplayed_track)
            if len(self.tracks_to_add) == nbtracks_target:
                break
        if not self.tracks_to_add:
            self.log.debug(u'Found no unplayed tracks, is your ' +
                             u'history getting large?')
            return False
        return True

    def mpd_find_top_tracks(self, mpd_artists):
        """
        Find top tracks for artists in mpd_artists list.
        N.B.:
            titles_list in UNICODE list
        """
        self.tracks_to_add = list()
        nbtracks_target = self.config.getint('sima', 'track_to_add')
        ## DEBUG
        self.log.info(u'Looking for top tracks: "%s"...' %
                      u' / '.join(mpd_artists[0:4]))
        for artist in mpd_artists:
            if len(self.tracks_to_add) == nbtracks_target:
                return True
            self.log.debug(u'Artist: "%s"' % artist)
            titles_list = [t for t, r in self.get_top_tracks_from_db(artist)]

            for title in self._cross_check_titles(artist, titles_list):
                art_uncd = artist.encode('utf-8')
                tit_uncd = title.encode('utf-8')
                mpd_find = [Track(**t) for t in self.client.find('artist', art_uncd, 'title', tit_uncd)]
                unplayed_track = self._extract_unplayed(mpd_find)
                if not unplayed_track:
                    continue
                self.tracks_to_add.append(unplayed_track)
                break

        if not self.tracks_to_add:
            return False
        return True

    def mpd_find_album(self, artists):
        """Find albums to queue.
        """
        self.tracks_to_add = list()
        nb_album_add = 0
        for artist in artists:
            self.log.info(u'Looking for an album to add for "%s"...' % artist)
            albums_list_utf8 = self.client.list('album', 'artist',
                    artist.encode('utf-8'))
            albums = set([unicode(a, 'UTF-8') for a in albums_list_utf8])  # get unicode
            albums_yet_in_hist = albums & self._get_album_history(artist=artist)  # albums yet in history for this artist
            albums_not_in_hist = list(albums - albums_yet_in_hist)
            # Get to next artist if there are no unplayed albums
            if not albums_not_in_hist:
                self.log.info(u'No album found for "%s"' % artist)
                continue
            #self.log.debug(albums);self.log.debug(albums_yet_in_hist);self.log.debug(albums_not_in_hist) # TODO:DEBUG line to remove
            album_to_queue = list()
            random.shuffle(albums_not_in_hist)
            for album in albums_not_in_hist:
                #self.log.debug(u'Album: "%s"' % album)
                tracks = self.client.find('album', album.encode('UTF-8'))
                if self._detects_var_artists_album(album):
                    continue
                if tracks and self.db.get_bl_album(Track(**tracks[0]), add_not=True):
                    self.log.debug(u'Blacklisted album: "%s"' % album)
                    self.log.debug(u'using track: "%s"' % Track(**tracks[0]))
                    continue
                album_to_queue = album
            if not album_to_queue:
                self.log.info(u'No album found for "%s"' % artist)
                continue
            self.log.info(u'# Add to playlist (album): %s - %s' %
                    (artist, album_to_queue))
            nb_album_add += 1
            for track in self.client.find('artist', artist.encode('UTF-8'),
                    'album', album_to_queue.encode('UTF-8')):
                self.tracks_to_add.append(Track(**track))
            if nb_album_add == self.config.getint('sima', 'album_to_add'):
                return True
        if self.tracks_to_add:
            return True
        return False

    def mpd_add_track(self):
        """
        Add track to MPD.
        """
        mode = self.config.get('sima', 'queue_mode')
        tracks = self.tracks_to_add
        for track in tracks:
            if mode in ['top', 'track']:
                self.log.info(u'# Add to playlist: %s / %s' %
                              (track.get_artist(), track.get_title()))
            try:  # DEV##ADD#
                self.client.add(track.file)
            except CommandError, err:
                self.log.warning(u'Cannot add track. (%s)' % err)
                msg = '[51@0] {add} playlist is at the max size'
                if str(err) in msg:
                    self.log.warning(u'MPD_sima hit playlist max size, '
                                     u'use consume mode or remove tracks.')
                return False

    def mpd_crop(self):
        """"""
        nb_tracks = self.config.getint('sima', 'consume')
        if nb_tracks == 0:
            return
        current_pos = int(self.client.currentsong().get('pos', 0))
        if current_pos <= nb_tracks:
            return
        while current_pos > nb_tracks:
            self.client.delete(0)
            current_pos = int(self.client.currentsong().get('pos', 0))

    def _set_queue_mode(self):
        """Set queue mode"""
        mode = self.config.get('sima', 'queue_mode')
        if mode == 'top':
            self.queue_mode = self.queue_top_tracks
        elif mode == 'album':
            self.queue_mode = self.queue_albums
        else:
            self.queue_mode = self.queue_similar_artist
        return False

    def _detects_var_artists_album(self, album):
        """Detects either an album is a "Various Artists" or a
        single artist release."""
        # TODO: Allow to set VarArt_str in config
        VarArt_str = ['Various Artists']
        art_first_track = None
        for mtrack in self.client.find('album', album.encode('UTF-8')):
            track = Track(**mtrack)
            if not art_first_track:  # set artist for the first track
                art_first_track = track.get_artist()
            alb_art = track.get_albumartist()
            if (alb_art and
                alb_art in VarArt_str):  # controls AlbumArtist Tag
                self.log.debug(track)
                # First check
                self.log.debug('Various artists in "%s", not queueing this album!' %
                        album)
                return True
            # TODO: Should add a new advanced user setting for this heuristic
            #art = track.get_artist()
            #if (art != art_first_track):
            #    # Second check
            #    #self.log.debug(track)
            #    self.log.debug(u"%s - %s" % (art,art_first_track))
            #    self.log.debug('Smells like "%s" album contains various artist, not queueing!' %
            #            album)
            #    return True
        return False

    def _cross_check_titles(self, artist, titles):
        """
        cross check titles
            * titles is UNICODE list
            * artist is UNICODE string
        """
        # Retrieve all tracks from artist
        mpd_search = self.client.find('artist', artist.encode('utf-8'))
        all_tracks = [Track(**t) for t in mpd_search]
        # Get all utf8-ed titles (filter missing titles set to 'None')
        all_mpd_titles = frozenset([tr.get_title() for tr in all_tracks
            if tr.title is not None])
        for title in titles:
            # DEBUG
            #self.log.debug(u'looking for "%s" in MPD library.' % title)
            match = get_close_matches(title, all_mpd_titles, 50, 0.78)
            if not match:
                continue
            #self.log.debug(u'found close match for "%s": %s' % (title, match))
            for title_ in match:
                leven = levenshtein_ratio(title.lower(), title_.lower())
                if leven == 1:
                    yield title_
                    self.log.debug(u'"%s" matches "%s".' % (title_, title))
                elif leven >= 0.79:  # PARAM
                    yield title_
                    self.log.debug(u'FZZZ: "%s" should match "%s" (lr=%1.3f)' %
                                   (title_, title, leven))
                else:
                    self.log.debug(u'FZZZ: "%s" does not match "%s" (lr=%1.3f)' %
                                   (title_, title, leven))
                    continue
        self.log.debug('Found no top tracks for "%s"' % artist)

    def _cross_check_artist(self, liste):
        """
        Controls presence of artists in liste in MPD library.
        Crosschecking artist names with SimaStr objects / difflib / levenshtein

        Actually this method is not calling MPDClient() to search because MPD
        search engine narrow too much the results (sic.). For instance :
            client.search('artist', 'The Doors')
        would not return tracks tagged "Doors".
        The method is then searching through complete artist list.

        N.B.: Cannot use a generator here because we need the complete artist
              list to process it with self._get_artists_list_reorg()

        TODO: proceed crosschecking even when an artist matched !!!
              Not because we found "The Doors" as "The Doors" that there is no
              remaining entries as "Doors" :/
              not straight forward, need probably heavy refactoring.
        """
        matching_artists = list()
        artist_list = [SimaStr(art) for art in liste]
        all_mpd_artists = frozenset([unicode(art, 'utf-8') for art in self.client.list('artist')])
        for artist in artist_list:
            # Check against the actual string in artist list
            if artist.orig in all_mpd_artists:
                matching_artists.append(unicode(artist))
                self.log.debug(u'found exact match for "%s"' % artist)
                continue
            # Then proceed with fuzzy matching if got nothing
            match = get_close_matches(artist.orig, all_mpd_artists, 50, 0.73)
            if not match:
                continue
            self.log.debug(u'found close match for "%s": %s' %
                           (artist, '/'.join(match)))
            # Does not perform fuzzy matching on short and single word strings
            # Only lowercased comparison
            if ' ' not in artist.orig and len(artist) < 8:
                for fuzz_art in match:
                    # Regular string comparison SimaStr().lower is regular string
                    if artist.lower() == fuzz_art.lower():
                        matching_artists.append(fuzz_art)
                        self.log.debug(u'"%s" matches "%s".' % (fuzz_art, artist))
                continue
            for fuzz_art in match:
                # Regular string comparison SimaStr().lower is regular string
                if artist.lower() == fuzz_art.lower():
                    matching_artists.append(fuzz_art)
                    self.log.debug(u'"%s" matches "%s".' % (fuzz_art, artist))
                    continue
                # Proceed with levenshtein and SimaStr
                leven = levenshtein_ratio(artist.stripped.lower(),
                        SimaStr(fuzz_art).stripped.lower())
                # SimaStr string __eq__, not regular string comparison here
                if artist == fuzz_art:
                    matching_artists.append(fuzz_art)
                    self.log.info(u'"%s" quite probably matches "%s" (SimaStr)' %
                                  (fuzz_art, artist))
                elif leven >= 0.82:  # PARAM
                    matching_artists.append(fuzz_art)
                    self.log.debug(u'FZZZ: "%s" should match "%s" (lr=%1.3f)' %
                                   (fuzz_art, artist, leven))
                else:
                    self.log.debug(u'FZZZ: "%s" does not match "%s" (lr=%1.3f)' %
                                   (fuzz_art, artist, leven))
        #else:
            #self.log.debug(u'Not found in MPD library: "%s"' % artist)
        return matching_artists

    def _extract_unplayed(self, tracks):
        """
        Extract one unplayed track from a Track object list.
        Check against history and file in queue.
        Check against black listing.
        """
        # TODO: rename _extract_playable_track()
        for track in tracks:
            track_uncd = unicode(track.__repr__(), 'UTF-8')
            if self.db.get_bl_album(track, add_not=True):
                self.log.debug('Blacklisted album: %s' % track.get_album())
                continue
            if self.db.get_bl_track(track, add_not=True):
                self.log.debug('Blacklisted track: %s' % track_uncd)
                continue
            if track in self.tracks_to_add:
                continue  # track already to be queued
            if self._is_inqueue(track):
                continue  # track already queued
            #if (track.album == self.current_track.album and
            #        track.albumartist == self.current_track.albumartist):
            # TODO: should control albumartist as well
            if (track.album == self.current_track.album):
                # the track is from the same album (OST / Compilation)
                self.log.debug(u'Found unplayed track ' +
                        'but from same album: %s' % (track_uncd))
                if self.config.getboolean('sima', 'single_album'):
                    continue  # Do not queue if single_album is set
            if not self._is_inhist(track):
                self.log.debug(u'track not yet in history: %s - %s' %
                        (track.get_artist(), track.get_title()))
                return track
            else:
                self.log.debug(u'track already in history: %s - %s' %
                        (track.get_artist(), track.get_title()))

    def _nb_track_ahead(self):
        """
        How many tracks ahead?
        """
        # current playing track position in the playlist & playlist length
        track_id = int(self.client.status().get('song', '0'))
        playlist_length = int(self.client.status().get('playlistlength', 0))
        return playlist_length - track_id - 1

    def _set_last_track_inqueue(self):
        """
        TODO: find an alternate/elegant way to do so
        """
        last_track_pos = int(self.client.status().get('playlistlength', 0)) - 1
        self.current_last_track = Track(**self.client.playlistinfo(last_track_pos)[0])

    def _is_inqueue(self, track):
        """
        Check if track is in the queue.
        """
        cursonpos = int(self.client.currentsong().get('pos', 0))
        playlist = self.client.playlistinfo()
        # create a Track() list from the playlist queue
        queue_lst = [Track(**qtrack) for qtrack in playlist[cursonpos:]]
        if track in queue_lst:
            self.log.debug(u'"%s/%s/%s" already in the queue' %
                           (track.get_artist(), track.get_album(),
                            track.get_title()))
            return True
        return False

    def _is_inhist(self, track):
        """Check against history.
        """
        duration = self.config.getint('sima', 'history_duration')
        # TODO: add artist in get_history() call
        for tr in self.db.get_history(encoding='utf-8', duration=duration):
            hist_track = Track(**{'artist': tr[0], 'album': tr[1],
                'title': tr[2], 'file': tr[3]})
            # TODO: add a new comparaison in Track object in order to compare
            #       artist/title couples instead of filenames
            if track == hist_track:
                return True
        return False

    def _get_album_history(self, artist=None):
        """Retrieve album history"""
        duration = self.config.getint('sima', 'history_duration')
        albums_list = set()
        for tr in self.db.get_history(artist=artist, duration=duration):
            albums_list.add(tr[1])
        return albums_list

    def _need_tracks(self):
        """whether or not playlist needs tracks"""
        # Does not queue if in single or repeat mode
        if self.client.status().get('single') == str(1):
            self.log.info('Not queueing in "single" mode.')
            return False
        if self.client.status().get('repeat') == str(1):
            self.log.info('Not queueing in "repeat" mode.')
            return False
        queue_trigger = self.config.getint('sima', 'queue_length')
        nb_track_ahead = self._nb_track_ahead()
        self.log.debug(u'Currently %i track(s) ahead. (target %s)' %
                       (nb_track_ahead, queue_trigger))
        if nb_track_ahead < queue_trigger:
            return True
        return False

    def _got_nothing(self):
        """log in case the script got nothing to add"""
        self.log.warning('Got nothing even with previous artists in playlist!')
        self.log.warning(u'...purge history?! rip more music?!')
        self.log.warning(u'Try running with debug verbosity to get more info.')

    def _get_artists_list_reorg(self, artists_list):
        """
        Move around items in artists_list in order to play first not recently played
        artists
        """
        duration = self.config.getint('sima', 'history_duration')
        art_in_hist = list()
        for tr in self.db.get_history(duration=duration):
            if tr[0] not in art_in_hist \
                and tr[0] in artists_list:
                art_in_hist.append(tr[0])
        art_in_hist.reverse()
        art_not_in_hist = list(set(artists_list) - set(art_in_hist))
        art_not_in_hist.extend(art_in_hist)
        return art_not_in_hist

    def get_top_tracks_from_db(self, artist=None):
        """
        Retrieve top tracks, ie. most popular song, from an artist.
        get_top_tracks_from_db function returns a list
        """
        tops = deque()
        simafm = SimaFM()
        req = simafm.get_toptracks(artist=artist)
        try:
            tops = [(song, rank) for song, rank in req]
        except XmlFMNotFound, err:
            self.log.warning("last.fm: %s" % err)
        return tops

    def get_similar_artists_from_udb(self):
        """retrieve similar artists form user DB sqlite"""
        similarity = self.config.getint('sima', 'similarity')
        current_search = self.current_searched
        self.log.debug(u'Looking in user db for artist similar to "%s"' %
                      current_search.get_artist())
        sims = [a.get('artist')
            for a in self.db.get_similar_artists(current_search.get_artist())
            if a.get('score') > similarity]
        if not sims:
            self.log.debug('Got nothing from user db')
        if sims:
            self.log.debug('Got something from user db: %s' % sims)
        return sims

    def get_similar_artists_from_db(self):
        """
        Retrieve similar artists on last.fm server.
          N.B.
            <current_search> is a Track object:
                self.get_artist()   is UNICODE
                self.artist         is the plain UTF-8 from MPD.
        """
        similarity = self.config.getint('sima', 'similarity')
        current_search = self.current_searched
        self.log.info(u'Looking for artist similar to "%s"' %
                      current_search.get_artist())
        simafm = SimaFM()
        # initialize artists deque list to construct from DB
        as_art = deque()
        as_artists = simafm.get_similar(artist=current_search.get_artist())
        self.log.debug(u'Requesting last.fm for "%s"' %
                       current_search.get_artist())
        try:
            [as_art.append((a, m)) for a, m in as_artists]
        except XmlFMHTTPError, err:
            self.log.warning(u'last.fm http error: %s' % err)
        except XmlFMNotFound, err:
            self.log.warning("last.fm: %s" % err)
        if not as_art:
            self.log.info(u'Got nothing from last.fm!')
        else:
            self.log.debug('Fetched %d artist(s) from last.fm' % len(as_art))
        return  [a for a, m in as_art if m > similarity]

    def get_artists_from_mpd(self, artists_lst):
        """
        Look in MPD library for availability of similar artists in artists_lst
        <mpd_similars> list of UNICODE strings
        """
        similarity = self.config.getint('sima', 'similarity')
        mpd_similars = list()
        hash_list = md5(u''.join(artists_lst).encode('UTF-8')).hexdigest()
        self.log.info(u'Looking availability in MPD library')
        if hash_list in self.cache_mpd:
            self.log.debug(u'Already cross check MPD library for these artists.')
            mpd_similars = list(self.cache_mpd.get(hash_list))
        else:
            mpd_similars = self._cross_check_artist(artists_lst)
            if len(self.cache_mpd) > 100:
                #limit size of self.cache_mpd
                self.log.debug('popitem in cache_mpd, reached limit')
                self.cache_mpd.popitem()
            self.cache_mpd.update({hash_list: list(mpd_similars)})
        if not mpd_similars:
            #self._got_nothing()
            self.log.warning(u'Got nothing from MPD music library.')
            self.log.warning(u'Try running in debug mode to guess why...')
            return None
        ##DEBUG
        # Remove current artist in order to avoid loop. When the script is going
        # back in the playlist, because last searched does not return any track,
        # similar artist from DB does suggest the current artist which we
        # started similarity search with.
        if self.current_track.get_artist() in mpd_similars:
            self.log.debug(u'Current searched "%s"' % self.current_searched.get_artist())
            self.log.debug(u'Removing "%s" from artist list' %
                           self.current_track.get_artist())
            mpd_similars.remove(self.current_track.get_artist())
        black_listed = set()
        for art in mpd_similars:
            if self.db.get_bl_artist(art, add_not=True):
                self.log.info(u'Blacklisted artist removed: %s' % art)
                black_listed.add(art)
        mpd_similars = list(set(mpd_similars) - black_listed)
        self.log.info(u'Got %d artists in library (at least %d%% similar).' %
                      (len(mpd_similars), similarity))
        self.log.info(u' / '.join(mpd_similars))
        random.shuffle(mpd_similars)
        # Move around mpd_similars items to get in unplayed|nor recently played
        # artist first. Sort of…
        return self._get_artists_list_reorg(mpd_similars)

    def get_similars(self):
        """Retrive similar artists from last.fm and user DB"""
        similar = self.get_similar_artists_from_db()
        if self.config.getboolean('sima', 'user_db'):
            similar.extend(self.get_similar_artists_from_udb())
        if not similar:
            self.log.debug('Damn! got nothing from databases!!!')
            return False
        self.log.info(u'First five similar artist(s): %s...' %
                       u' / '.join(similar[0:5]))
        # use a set to avoid dupes (ie. same artist from udb & last.fm)
        return list(set(similar))

    def queue_similar_artist(self):
        """
        Queue similar artist (at random)
        """
        similar = self.get_similars()
        if not similar:
            return False
        mpd_artists = self.get_artists_from_mpd(similar)
        if not mpd_artists:
            return False
        if not self.mpd_findtrk(mpd_artists):
            return False
        self.mpd_add_track()
        self.current_queue = self._nb_track_ahead()
        return True

    def queue_top_tracks(self):
        """
        Queue Top track from similar artist (at random)
        """
        similar = self.get_similars()
        if not similar:
            return False
        mpd_artists = self.get_artists_from_mpd(similar)
        if not mpd_artists:
            return False
        if not self.mpd_find_top_tracks(mpd_artists):
            return False
        self.mpd_add_track()
        self.current_queue = self._nb_track_ahead()
        return True

    def queue_albums(self):
        """
        Queue entire albums from similar artist (at random)
        """
        similar = self.get_similars()
        if not similar:
            return False
        mpd_artists = self.get_artists_from_mpd(similar)
        if not mpd_artists:
            return False
        if not self.mpd_find_album(mpd_artists):
            return False
        self.mpd_add_track()
        self.current_queue = self._nb_track_ahead()
        return True

    def queue(self, track):
        """
        On new track playing:
            add track in history
            Check either playlist needs more tracks or not.
            Find tracks to add.
        """
        if not track.artist:
            self.log.warning(u'## No artist tag set for %s' %
                             track.get_filename())
            self.log.warning(u'Cannont look for similar artist.')
            return False
        if not track.title:
            self.log.warning(u'## MISSING TITLE TAG for %s' %
                            track.get_filename())
        self.log.info(u'Playing: %s - %s' % (track.get_artist(),
                                           track.get_title()))
        if track.collapse_tags_bool:
            self.log.info(u'This file contains multiple tags: %s' %
                          track.get_filename())
            self.log.debug('Multiple tags: ' + u'/'.join(track.collapsed_tags))
        # crop playlist if necessary
        self.mpd_crop()
        if not self._need_tracks():
            return False
        self.log.info(u'The playlist needs tracks.')

        # Artist we want similar track from:
        #   currently played or last song in queue?
        # WARNING: changing from current to lastest leads to update the
        # current_* in get_artists_from_mpd() function where we look for its
        # presence in the mpd_artists list
        self.current_searched = self.current_track
        #self.current_searched = self.current_last_track

        # Already searched artists list (used when getting backward in play
        # history if nothing got queued)
        artist = self.current_searched.artist
        artists_searched = list([artist])

        history_copy = deque()
        for tr in self.db.get_history(encoding='utf-8'):
            # Back in history 'till SimaDB.__HIST_DURATION__
            history_copy.appendleft(Track(**{'artist': tr[0]}))
        while 42:
            if not self.queue_mode():
                # In case nothing got queued
                # get through play history backward until another artist got
                # something to queue
                self.log.debug('Looking for another artist in play history.')
                arthist = Track(artist=artist)
                while arthist.artist in artists_searched:
                    try:
                        arthist = history_copy.pop()
                        if not arthist.artist:
                            continue
                    except IndexError:
                        self._got_nothing()
                        return False
                # update the current_searched with new artist
                self.current_searched = arthist
                artists_searched.append(arthist.artist)
                self.log.warning(u'Trying with previous artist: %s' %
                                self.current_searched.get_artist())
            else:
                break

    def connect(self):
        """
        MPD:
        Connection to mpd server.
        And controls available commands.
        """
        needed_cmds = ['status', 'stats', 'add', 'find', \
                       'search', 'currentsong', 'ping']

        if not self.mpd_connect():
            return False
        self.log.info(u'Connected to MPD server, proceeding...')
        self.mpd_auth()
        available_cmd = self.client.commands()
        for nddcmd in needed_cmds:
            if nddcmd not in available_cmd:
                self.log.error(u'command “%s” not available, '
                               u'control permissions… Need password?' %
                               nddcmd)
                sys.exit(2)
        # Run if db_update has been previously set
        if self.db_update: self.controls_mpd_db_last_update()
        else:
            # Otherwise set up db_update
            self.db_update = int(self.client.stats().get('db_update'))
        return True

    def controls_mpd_db_last_update(self):
        """Controls last time MPD DB has been updated in order to purge
        cache_mpd."""
        db_update = int(self.client.stats().get('db_update'))
        if db_update > self.db_update:
            # MPD DB has been updated
            self.log.debug('MPD database has been updated, flushing cache!')
            self.db_update = db_update
            self.cache_mpd = dict()

    def loop(self):
        curr_track = self.mpd_current_song()
        # controls if mpd has been updated.
        self.controls_mpd_db_last_update()
        # following used to detect deleted|moved tracks
        curr_queue = self._nb_track_ahead()
        mpd_state = str(self.client.status().get('state'))
        if self.is_playing and mpd_state != 'play':
            self.is_playing = False
            self.log.info(u'MPD state is “%s” (check n*%is)' %
                          (mpd_state, WAIT_MPD_RESUME))
            time.sleep(WAIT_MPD_RESUME)
            return
        elif not self.is_playing and mpd_state == 'play':
            self.is_playing = True
            self.curr_track = Track(**{'artist': 'play again'})
            self.log.info(u'Playing again, proceeding...')
        if not self.is_playing:
            return
        if (curr_track != self.current_track or
           curr_queue != self.current_queue):
            if not curr_track:
                self.log.warning(u'Found no current track in MPD')
                return
            if curr_track != self.current_track:
                SimaDB(db_path=self.conf_obj.userdb_file).add_history(curr_track)
            # Update current playlist state
            self.current_track = curr_track
            self.current_queue = curr_queue
            #self._set_last_track_inqueue()
            ## DEBUG
            #self.log.debug(curr_track)
            self.queue(curr_track)

    def run(self):
        """
        Main Loop.
        Two events may trigger the queue process
            0) new track playing
            1) playing track has been moved or number of queued tracks has
               changed
        """
        self.log.info(u'About to connect to %s:%d' %
                     (self.mpd_host, self.mpd_port))
        self.log.debug(u'using  password "%s"' %
                unicode(self.config.get('MPD', 'password'), 'utf-8'))
        mpd_conn = self.connect()
        while 42:
            if mpd_conn:
                try:
                    self.loop()
                except XmlFMHTTPError, as_error:
                    self.log.warning(u'last.fm http error: %s...' %
                                        as_error)
                    # initialize current_track to have next loop gone through
                    self.current_track = Track()
                    time.sleep(WAIT_MPD_RESUME)
                except (ConnectionError, SocketError), err:
                    self.mpd_disconnect()
                    mpd_conn = False
                    self.log.warning(u'MPD connection lost.' +
                                     u'Trying to reconnect')
                except XmlFMError, err:
                    self.log.warning('Last.fm module error: "%s"' % err)
                    # initialize current_track to have next loop gone through
                    self.current_track = Track()
                    time.sleep(WAIT_MPD_RESUME)
            elif not mpd_conn:
                if self.connect():
                    mpd_conn = True
                else:
                    self.log.warning(u'FAILED. Waiting %is to try again.' %
                                     (WAIT_MPD_RESUME +
                                     self.config.getint('sima', 'main_loop_time')))
                    time.sleep(WAIT_MPD_RESUME)
            time.sleep(self.config.getint('sima', 'main_loop_time'))
        self.mpd_disconnect()

    def shutdown(self):
        """
        """
        self.log.warning(u'Starting shutdown.')
        self.log.info(u'Cleaning database')
        db = SimaDB(db_path=self.conf_obj.userdb_file)
        db.purge_history()
        db.clean_database()
        self.log.info(u'The way is shut, ' +
                 u'it was made be those who are dead. ' +
                 u'And the dead keet it…')
        self.log.info(u'bye...')
        sys.exit(0)


# FUNCTIONS

def sig_term_handler(signum, frame):
    """Catch sig term"""
    raise KeyboardInterrupt(u'Caught a %d\' SIG TERM signal' % signum)


def new_version_available():
    def version_convert(version):
        """Convert version string to float"""
        float_version = float()
        vsplit = version.split('.')
        for i in range(len(vsplit)):
            if not vsplit[i].isdigit():
                # get rid of the non digit like beta, rc, etc.
                continue
            float_version = float_version + (float(vsplit[i]) / pow(10, int(i)))
        return float_version

    pattern = '.*Latest stable version: <a href=".*?"><strong>(?P<version>[0-9.]*)</strong>.*$'
    pat = re.compile(pattern)
    try:
        fd = urlopen(__url__)
    except IOError, urllib_err:
        return False
    for line in fd:
        me = pat.match(line)
        if me and version_convert(me.group('version')) > version_convert(__version__):
            return True
    return False


def exception_log(log):
    """Log unknown exceptions"""
    log.error('Exception caught!!!')
    log.error(''.join(traceback.format_exc()))
    log.info(u'Please report the previous message along with some log entries right before the crash.')
    log.info(u'thanks for your help :)')
    log.info(u'Quiting now!')
    sys.exit(1)


def main():  # BOOT SEQUENCE
    """
    Main function.
    """
    info = dict({'version': __version__, 'revision': __revison__,
                 'date': __date__})
    # StartOpt gathers options from command line call (in StartOpt().options)
    sopt = StartOpt(info, log=logger(log_level='info', name='boot'))

    # Logging facility, default log level is INFO
    log_file = sopt.options.get('logfile', None)
    log = logger(log_level='info', log_file=log_file)

    log.info(u'')
    log.info(u'Starting MPD_sima version %s (revision %s - %s)' %
             (__version__, __revison__, __date__))

    # Configuration manager Object
    conf_manager = ConfMan(log, sopt.options)
    config = conf_manager.config

    # Controls new version
    check_new = config.getboolean('sima', 'check_new_version')
    if check_new and new_version_available():
        log.warning(u'New stable version available at %s' % __url__)

    # Logging settings
    # Define the logger following user conf
    #  default log level is INFO.
    log.setLevel(LEVELS.get(config.get('log', 'verbosity')))
    log.debug('Command line/env. var. say: %s' % sopt.options)

    # Upgrading User DB if necessary, create one if not existing
    try:
        SimaDB(db_path=conf_manager.userdb_file).upgrade()
    except SimaDBUpgradeError, err:
        log.warning('Error upgrading database: %s' % err)
    except SimaDBNoFile:
        log.info('Creating database in "%s"' % conf_manager.userdb_file)
        open(conf_manager.userdb_file, 'a').close()
        SimaDB(db_path=conf_manager.userdb_file).create_db()
    log.info('Using database "%s"' % conf_manager.userdb_file)

    # Run as a daemon
    if config.getboolean('daemon', 'daemon'):
        sima = Sima(conf_manager, log)
        try:
            sima.start()
        except Exception:
            exception_log(log)
        return

    # Interactive run
    # Sima Object init
    sima = Sima(conf_manager, log)
    # In order to catch "kill 15" as KeyboardInterrupt when run in background
    signal.signal(signal.SIGTERM, sig_term_handler)
    try:
        sima.foreground()
    except KeyboardInterrupt, err:
        sys.exit(0)
    except Exception:
        exception_log(log)
# END FUNCTIONS

# Script starts here
if __name__ == '__main__':
    main()

# VIM MODLINE
# vim: ai ts=4 sw=4 sts=4 expandtab
