#!/usr/bin/env python # -*- coding: utf-8 -*- # Copyright (c) 2009, 2010, 2011, 2012 Jack Kaliko # # 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 . # # # 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.* """ __version__ = u'0.9.0' __revison__ = u'$Revision$'[11:-2] __author__ = u'$Author$' __date__ = u'$Date$'[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 lib.daemon import Daemon from lib.mpd_player import (Player, PlayerError, PlayerCommandError) from lib.mpd_player import (MPDError as PlayerUnHandledError) 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 class Sima(Daemon): """ Main Object dealing with what and how to queue. """ def __init__(self, config_man, log): """ Declare default or empty attributes """ ## Conf self.config = config_man.config self.conf_obj = config_man self.log = log ## Set daemon Daemon.__init__(self, self.config.get('daemon', 'pidfile')) ## Set player self.player = Player(host=self.config.get('MPD', 'host'), port=self.config.get('MPD', 'port'), password=config_man.get_pw()) self.is_playing = True # Track objects self.current_track = None #self.current_last_track = None self.state_change = None # Track object we are looking similar art/track for self.current_searched = None self.tracks_to_add = list() ## SQLite Database self.db = SimaDB(db_path=self.conf_obj.userdb_file) ## Set queue mode self._set_queue_mode() def findtrk(self, artists): """ Find tracks to play (ie. not in history and etc.) while self.tracks_to_add is not reached. """ self.tracks_to_add = [] nbtracks_target = self.config.getint('sima', 'track_to_add') for artist in artists: self.log.debug(u'Trying to find titles to add for "%s"' % artist) found = self.player.find_artist(artist) # find tracks not in history tracks_in_hist = self.tracks_in_hist(artist=artist) not_in_hist = list(set(found) - set(tracks_in_hist)) if not not_in_hist: self.log.debug(u'All tracks already in current history for "%s"' % artist) random.shuffle(not_in_hist) unplayed_track = self._extract_playable_track(not_in_hist) 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 too large?') return False return True def find_top_tracks(self, artists): """ Find top tracks for artists in 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(artists[0:4])) for artist in 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)] # find tracks not in history tracks_in_hist = self.tracks_in_hist(artist=artist) for title in self._cross_check_titles(artist, titles_list): found = self.player.find_artist_title(artist=artist, title=title) not_in_hist = list(set(found) - set(tracks_in_hist)) if not not_in_hist: self.log.debug(u'No tracks available for "%s" (already played)' % title) unplayed_track = self._extract_playable_track(not_in_hist) if not unplayed_track: continue self.tracks_to_add.append(unplayed_track) break if not self.tracks_to_add: return False return True def 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 = set(self.player.list_album(artist=artist)) 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 album_to_queue = unicode() random.shuffle(albums_not_in_hist) for album in albums_not_in_hist: tracks = self.player.find_album(album) if self._detects_var_artists_album(album): continue if tracks and self.db.get_bl_album(tracks[0], add_not=True): self.log.debug(u'Blacklisted album: "%s"' % album) self.log.debug(u'using track: "%s"' % tracks[0]) continue # Look if one track of the album is already queued # Good heuristic, at least enough to guess if the whole album is # already queued. if self._is_inqueue(tracks[0]): self.log.debug(u'"%s" already queued, skipping!' % tracks[0].get_album()) 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.player.find_artist_album(artist=artist, album=album_to_queue): self.tracks_to_add.append(track) if nb_album_add == self.config.getint('sima', 'album_to_add'): return True if self.tracks_to_add: return True return False def 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.player.add(track.file) except PlayerCommandError as err: self.log.warning(err) return False def crop_playlist(self): """""" nb_tracks = self.config.getint('sima', 'consume') if nb_tracks == 0: return current_pos = self.player.currentsong().pos if current_pos <= nb_tracks: return while current_pos > nb_tracks: self.player.remove() current_pos = self.player.currentsong().pos 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.""" VarArt_str = ['Various Artists'] art_first_track = None for track in self.player.find_album(album=album): 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 all_tracks = self.player.find_artist(artist=artist) # Get all titles (filter missing titles set to 'None') all_artist_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_artist_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_artists = self.__cache.get('artists') for artist in artist_list: # Check against the actual string in artist list if artist.orig in all_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_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)) return matching_artists def _extract_playable_track(self, tracks): """ Extract one unplayed track from a Track object list. Check against history and file in queue. Check against black listing. """ for track in tracks: 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(u'Blacklisted track: %s' % track) 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 ' + u'but from same album: %s' % (track)) if self.config.getboolean('sima', 'single_album'): continue # Do not queue if single_album is set return track return None def _nb_track_ahead(self): """ How many tracks ahead? """ # current playing track position in the playlist & playlist length track_id = int(self.player.get_status().get('song', '0')) playlist_length = int(self.player.get_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.player.get_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.player.currentsong().pos) queue_lst = self.player.get_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 tracks_in_hist(self, artist): """Check against history for tracks already in history for a specific artist. """ duration = self.config.getint('sima', 'history_duration') tracks_from_db = self.db.get_history(encoding='utf-8', duration=duration, artist=artist) # Construct Track() objects list from database history played_tracks = [Track(**{'artist': tr[0], 'album': tr[1], 'title': tr[2], 'file': tr[3]}) for tr in tracks_from_db] return played_tracks 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.player.get_status().get('single') == str(1): self.log.info('Not queueing in "single" mode.') return False if self.player.get_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)) random.shuffle(art_not_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""" current_artist = self.current_searched.get_artist() self.log.debug(u'Looking in user db for artist similar to "%s"' % current_artist) sims = [(a.get('artist'), a.get('score')) for a in self.db.get_similar_artists(current_artist)] 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 supersedes_db_art(self, similar): udb_art = self.get_similar_artists_from_udb() udb_art_dict = dict() results = list() for a, m in udb_art: udb_art_dict.update({a:m}) for art, match in list(similar): if art in udb_art_dict.keys(): # updates similar with udb match score results.append((art, udb_art_dict.pop(art))) else: results.append((art, match)) for a, m in udb_art_dict.iteritems(): results.append((a, m)) return results def get_similar_artists_from_db(self): """ Retrieve similar artists on last.fm server. """ 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 as_art def _cross_check_wrapper(self, similarities): dynamic = self.config.getint('sima', 'dynamic') similarity = self.config.getint('sima', 'similarity') results = list() if dynamic == 0: artists_list = [art for art, match in similarities if match > similarity] results = self._cross_check_artist(artists_list) else: similarities.reverse() while len(results) < dynamic: if len(similarities) == 0: break art_pop, match = similarities.pop() if match < similarity: break results.extend(self._cross_check_artist([art_pop,])) self.log.debug(u'Dynamic similarity: %d%%' % match) return results def get_artists_from_player(self, similarities): """ Look in player library for availability of similar artists in similarities """ similarities_lst = [art + str(match) for art, match in similarities] similars = list() hash_list = md5(u''.join(similarities_lst).encode('UTF-8')).hexdigest() search_cache = self.__cache.get('search') self.log.info(u'Looking availability in music library') if hash_list in search_cache: self.log.debug(u'Already cross check music library for these artists.') similars = list(search_cache.get(hash_list)) else: similars = self._cross_check_wrapper(similarities) if len(search_cache) > 100: #limit size of search_cache self.log.debug('popitem in search_cache, reached limit') search_cache.popitem() search_cache.update({hash_list: list(similars)}) if not similars: self.log.warning(u'Got nothing from 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 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()) similars.remove(self.current_track.get_artist()) black_listed = set() for art in similars: if self.db.get_bl_artist(art, add_not=True): self.log.info(u'Blacklisted artist removed: %s' % art) black_listed.add(art) similars = list(set(similars) - black_listed) self.log.info(u'Got %d artists in library' % len(similars)) self.log.info(u' / '.join(similars)) random.shuffle(similars) # Move around similars items to get in unplayed|not recently played # artist first. similars_reorg = self._get_artists_list_reorg(similars) self.log.debug(u'Looking for these artists (got them reorganized).') self.log.debug(u' / '.join(similars_reorg)) return similars_reorg 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 = self.supersedes_db_art(similar) if not similar: self.log.debug('Damn! got nothing from databases!!!') return False similar = sorted(similar, key=lambda sim: sim[1], reverse=True) self.log.info(u'First five similar artist(s): %s...' % u' / '.join([a for a, m in similar[0:5]])) return similar def queue_similar_artist(self): """ Queue similar artist (at random) """ similar = self.get_similars() if not similar: return False artists = self.get_artists_from_player(similar) if not artists: return False if not self.findtrk(artists): return False random.shuffle(self.tracks_to_add) self.add_track() return True def queue_top_tracks(self): """ Queue Top track from similar artist (at random) """ similar = self.get_similars() if not similar: return False artists = self.get_artists_from_player(similar) if not artists: return False if not self.find_top_tracks(artists): return False random.shuffle(self.tracks_to_add) self.add_track() return True def queue_albums(self): """ Queue entire albums from similar artist (at random) """ similar = self.get_similars() if not similar: return False artists = self.get_artists_from_player(similar) if not artists: return False if not self.find_album(artists): return False self.add_track() return True def loop_detection(self): """Loop detection computes artist appearence frequency. """ # TODO: loop detection should be launch only on new track playing. moy = float(0); nt = 8 loop_mapping = dict() # this mapping is used to identify artist looped over history = list() for tr in self.db.get_history(): # Gathers nt uniq artist from history history.append(tr[0]) if len(set(history)) == nt: break recent_hist = history[:nt] # get only the last nt tracks, when looping, recent_hist ≠ history for art in set(history): # get uniq artists list moy += history.count(art) loop_mapping[art] = history.count(art) self.log.debug(u'Loop detection: %f' % (moy/len(set(recent_hist)),)) self.log.debug(u'Loop detection: %s' % loop_mapping) 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'Cannot 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.crop_playlist() 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_player() function where we look for its # presence in the 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 _flush_cache(self): """ Both flushes and instanciates __cache """ try: 'search' in self.__cache self.log.debug('flushing cache!') except: pass # Not flushing, initializing __cache self.__cache = {'artists': None, 'search': dict()} self.__cache['artists'] = frozenset(self.player.list(what='artist')) def loop(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 """ changed = None if self.current_track: # is None => first loop iteration changed = self.player.idle() # hangs here untill player state changes self.log.debug(u'Player state changed: %s' % changed) # controls if player media DB has been updated. if 'database' in changed: self._flush_cache() curr_track = self.player.currentsong() playing_state = self.player.get_state() if self.is_playing and playing_state != 'play': self.is_playing = False self.log.info(u'Player state is “%s” (check n*%is)' % (playing_state, WAIT_MPD_RESUME)) time.sleep(WAIT_MPD_RESUME) self.current_track = Track(artist='Dummy') # Set self.current_track to avoid 1st loop detection return elif not self.is_playing and playing_state == 'play': self.is_playing = True self.current_track = Track() self.log.info(u'Playing again, proceeding...') if not self.is_playing: return if (curr_track != self.current_track or changed != self.state_change or self._nb_track_ahead() == 0): if not curr_track: self.log.warning(u'Found no current track!') return if curr_track != self.current_track: self.db.add_history(curr_track) # Update current playlist state self.current_track = curr_track self.state_change = changed #self._set_last_track_inqueue() self.queue(curr_track) def run(self): self.log.info(u'About to connect to %s:%s' % (self.config.get('MPD', 'host'), self.config.get('MPD', 'port'))) try: self.player.connect() except PlayerError as connect_err: self.player.disconnect() self.log.critical('Player error: %s' % connect_err) return False self._flush_cache() # init internal cache while 42: try: self.loop() except XmlFMHTTPError as error: self.log.warning(u'last.fm http error: %s...' % error) # initialize current_track to have next loop gone through self.current_track = Track() time.sleep(WAIT_MPD_RESUME) except XmlFMError as 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) except PlayerCommandError as err: self.log.warning('Player command error: "%s"' % err) self.current_track = Track() except PlayerUnHandledError as err: #TODO: unhandled Player exceptions self.log.warning('Unhandled player exception: "%s"' % err) time.sleep(WAIT_MPD_RESUME) except (PlayerError): # initialize current_track to have next loop gone through self.current_track = Track() try: self.player.connect() except (PlayerError) as err: self.log.warning(u'Player error: %s' % err) self.log.info(u'Waiting a while to try again.') time.sleep(WAIT_MPD_RESUME) 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 keep 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: (?P[0-9.]*).*$' 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 say: %s' % sopt.options) # Create Database if sopt.options.get('create_db', None): 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('Done, bye...') sys.exit(0) # 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