]> kaliko git repositories - mpd-sima.git/blobdiff - sima/client.py
Enhanced queuing behavior in random mode (Closes #16)
[mpd-sima.git] / sima / client.py
index 2893d9b52cf52bb5a53266ec2bdedc7f0e05ebcd..c04f560aece84c749a839e0aade0823f47493af4 100644 (file)
@@ -36,6 +36,7 @@ except ImportError as err:
     sexit(1)
 
 # local import
     sexit(1)
 
 # local import
+from .lib.simastr import SimaStr
 from .lib.player import Player, blacklist
 from .lib.track import Track
 from .lib.meta import Album, Artist
 from .lib.player import Player, blacklist
 from .lib.track import Track
 from .lib.meta import Album, Artist
@@ -50,6 +51,28 @@ class PlayerCommandError(PlayerError):
 
 PlayerUnHandledError = MPDError  # pylint: disable=C0103
 
 
 PlayerUnHandledError = MPDError  # pylint: disable=C0103
 
+def bl_artist(func):
+    def wrapper(*args, **kwargs):
+        cls = args[0]
+        if not args[0].database:
+            return func(*args, **kwargs)
+        result = func(*args, **kwargs)
+        if not result:
+            return
+        names = list()
+        for art in result.names:
+            if cls.database.get_bl_artist(art, add_not=True):
+                cls.log.debug('Blacklisted "%s"', art)
+                continue
+            names.append(art)
+        if not names:
+            return
+        resp = Artist(name=names.pop(), mbid=result.mbid)
+        for name in names:
+            resp.add_alias(name)
+        return resp
+    return wrapper
+
 
 class PlayerClient(Player):
     """MPD Client
 
 class PlayerClient(Player):
     """MPD Client
@@ -109,11 +132,10 @@ class PlayerClient(Player):
         """
         # TODO: ain't working for "sticker find" and "sticker list"
         tracks_listing = ["playlistfind", "playlistid", "playlistinfo",
         """
         # TODO: ain't working for "sticker find" and "sticker list"
         tracks_listing = ["playlistfind", "playlistid", "playlistinfo",
-                "playlistsearch", "plchanges", "listplaylistinfo", "find",
-                "search", "sticker find",]
+                          "playlistsearch", "plchanges", "listplaylistinfo", "find",
+                          "search", "sticker find",]
         track_obj = ['currentsong']
         if self._comm in tracks_listing + track_obj:
         track_obj = ['currentsong']
         if self._comm in tracks_listing + track_obj:
-            #  pylint: disable=w0142
             if isinstance(ans, list):
                 return [Track(**track) for track in ans]
             elif isinstance(ans, dict):
             if isinstance(ans, list):
                 return [Track(**track) for track in ans]
             elif isinstance(ans, dict):
@@ -122,8 +144,8 @@ class PlayerClient(Player):
 
     def __skipped_track(self, old_curr):
         if (self.state == 'stop'
 
     def __skipped_track(self, old_curr):
         if (self.state == 'stop'
-            or not hasattr(old_curr, 'id')
-            or not hasattr(self.current, 'id')):
+                or not hasattr(old_curr, 'id')
+                or not hasattr(self.current, 'id')):
             return False
         return self.current.id != old_curr.id  # pylint: disable=no-member
 
             return False
         return self.current.id != old_curr.id  # pylint: disable=no-member
 
@@ -135,27 +157,89 @@ class PlayerClient(Player):
             self.log.info('Player: Flushing cache!')
         else:
             self.log.info('Player: Initialising cache!')
             self.log.info('Player: Flushing cache!')
         else:
             self.log.info('Player: Initialising cache!')
-        self._cache = {
-                'artists': None,
-                }
-        self._cache['artists'] = frozenset(self._client.list('artist'))
+        self._cache = {'artists': frozenset(),
+                       'nombid_artists': frozenset(),}
+        self._cache['artists'] = frozenset(filter(None, self._execute('list', ['artist'])))
+        if Artist.use_mbid:
+            self._cache['nombid_artists'] = frozenset(filter(None, self._execute('list', ['artist', 'musicbrainz_artistid', ''])))
 
     @blacklist(track=True)
     def find_track(self, artist, title=None):
         tracks = set()
 
     @blacklist(track=True)
     def find_track(self, artist, title=None):
         tracks = set()
-        for name in artist.names:
-            if title:
-                tracks |= set(self.find('artist', name, 'title', title))
-            else:
-                tracks |= set(self.find('artist', name))
         if artist.mbid:
             if title:
         if artist.mbid:
             if title:
-                tracks |= set(self.find('musicbrainz_artistid', artist.mbid))
-            else:
                 tracks |= set(self.find('musicbrainz_artistid', artist.mbid,
                                         'title', title))
                 tracks |= set(self.find('musicbrainz_artistid', artist.mbid,
                                         'title', title))
+            else:
+                tracks |= set(self.find('musicbrainz_artistid', artist.mbid))
+        else:
+            for name in artist.names:
+                if title:
+                    tracks |= set(self.find('artist', name, 'title', title))
+                else:
+                    tracks |= set(self.find('artist', name))
         return list(tracks)
 
         return list(tracks)
 
+    @bl_artist
+    def search_artist(self, artist):
+        """
+        Search artists based on a fuzzy search in the media library
+            >>> art = Artist(name='the beatles', mbid=<UUID4>) # mbid optional
+            >>> bea = player.search_artist(art)
+            >>> print(bea.names)
+            >>> ['The Beatles', 'Beatles', 'the beatles']
+
+        Returns an Artist object
+        """
+        found = False
+        if artist.mbid:
+            # look for exact search w/ musicbrainz_artistid
+            exact_m = self._execute('list', ['artist', 'musicbrainz_artistid', artist.mbid])
+            if exact_m:
+                _ = [artist.add_alias(name) for name in exact_m]
+                found = True
+        else:
+            artist = Artist(name=artist.name)
+        # then complete with fuzzy search on artist with no musicbrainz_artistid
+        if artist.mbid:
+            # we already performed a lookup on artists with mbid set
+            # search through remaining artists
+            artists = self._cache.get('nombid_artists')
+        else:
+            artists = self._cache.get('artists')
+        match = get_close_matches(artist.name, artists, 50, 0.73)
+        if not match and not found:
+            return
+        if len(match) > 1:
+            self.log.debug('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.name and len(artist.name) < 8:
+            for fuzz_art in match:
+                # Regular lowered string comparison
+                if artist.name.lower() == fuzz_art.lower():
+                    artist.add_alias(fuzz_art)
+                    return artist
+        fzartist = SimaStr(artist.name)
+        for fuzz_art in match:
+            # Regular lowered string comparison
+            if artist.name.lower() == fuzz_art.lower():
+                found = True
+                artist.add_alias(fuzz_art)
+                if artist.name != fuzz_art:
+                    self.log.debug('"%s" matches "%s".', fuzz_art, artist)
+                continue
+            # SimaStr string __eq__ (not regular string comparison here)
+            if fzartist == fuzz_art:
+                found = True
+                artist.add_alias(fuzz_art)
+                self.log.info('"%s" quite probably matches "%s" (SimaStr)',
+                              fuzz_art, artist)
+        if found:
+            if artist.aliases:
+                self.log.debug('Found: %s', '/'.join(list(artist.names)[:4]))
+            return artist
+
     def fuzzy_find_track(self, artist, title):
         # Retrieve all tracks from artist
         all_tracks = self.find_track(artist, title)
     def fuzzy_find_track(self, artist, title):
         # Retrieve all tracks from artist
         all_tracks = self.find_track(artist, title)
@@ -170,11 +254,11 @@ class PlayerClient(Player):
             if leven == 1:
                 pass
             elif leven >= 0.79:  # PARAM
             if leven == 1:
                 pass
             elif leven >= 0.79:  # PARAM
-                self.log.debug('title: "%s" should match "%s" (lr=%1.3f)' %
-                               (title_, title, leven))
+                self.log.debug('title: "%s" should match "%s" (lr=%1.3f)',
+                               title_, title, leven)
             else:
             else:
-                self.log.debug('title: "%s" does not match "%s" (lr=%1.3f)' %
-                               (title_, title, leven))
+                self.log.debug('title: "%s" does not match "%s" (lr=%1.3f)',
+                               title_, title, leven)
                 return []
             return self.find('artist', artist, 'title', title_)
 
                 return []
             return self.find('artist', artist, 'title', title_)
 
@@ -196,8 +280,9 @@ class PlayerClient(Player):
                album containing at least a single track for artist
         """
         albums = []
                album containing at least a single track for artist
         """
         albums = []
-        for name in artist.aliases:
-            self.log.debug('Searching album for {}'.format(name))
+        for name in artist.names:
+            if len(artist.names) > 1:
+                self.log.debug('Searching album for aliase: "%s"', name)
             kwalbart = {'albumartist':name, 'artist':name}
             for album in self.list('album', 'albumartist', artist):
                 if album and album not in albums:
             kwalbart = {'albumartist':name, 'artist':name}
             for album in self.list('album', 'albumartist', artist):
                 if album and album not in albums:
@@ -205,15 +290,15 @@ class PlayerClient(Player):
             for album in self.list('album', 'artist', artist):
                 album_trks = [trk for trk in self.find('album', album)]
                 if 'Various Artists' in [tr.albumartist for tr in album_trks]:
             for album in self.list('album', 'artist', artist):
                 album_trks = [trk for trk in self.find('album', album)]
                 if 'Various Artists' in [tr.albumartist for tr in album_trks]:
-                    self.log.debug('Discarding {0} ("Various Artists" set)'.format(album))
+                    self.log.debug('Discarding %s ("Various Artists" set)', album)
                     continue
                 arts = set([trk.artist for trk in album_trks])
                 if len(set(arts)) < 2:  # TODO: better heuristic, use a ratio instead
                     if album not in albums:
                     continue
                 arts = set([trk.artist for trk in album_trks])
                 if len(set(arts)) < 2:  # TODO: better heuristic, use a ratio instead
                     if album not in albums:
-                        albums.append(Album(name=album, albumartist=artist))
+                        albums.append(Album(name=album, **kwalbart))
                 elif album and album not in albums:
                     self.log.debug('"{0}" probably not an album of "{1}"'.format(
                 elif album and album not in albums:
                     self.log.debug('"{0}" probably not an album of "{1}"'.format(
-                                   album, artist) + '({0})'.format('/'.join(arts)))
+                        album, artist) + '({0})'.format('/'.join(arts)))
         return albums
 
     def monitor(self):
         return albums
 
     def monitor(self):
@@ -236,7 +321,7 @@ class PlayerClient(Player):
         if 'idle' in self._client._pending:
             self._client.noidle()
         elif self._client._pending:
         if 'idle' in self._client._pending:
             self._client.noidle()
         elif self._client._pending:
-            self.log.warning('pending commands: {}'.format(self._client._pending))
+            self.log.warning('pending commands: %s', self._client._pending)
 
     def remove(self, position=0):
         self.delete(position)
 
     def remove(self, position=0):
         self.delete(position)
@@ -244,7 +329,7 @@ class PlayerClient(Player):
     def add(self, track):
         """Overriding MPD's add method to accept add signature with a Track
         object"""
     def add(self, track):
         """Overriding MPD's add method to accept add signature with a Track
         object"""
-        self._client.add(track.file)
+        self._execute('add', [track.file])
 
     @property
     def artists(self):
 
     @property
     def artists(self):
@@ -254,6 +339,18 @@ class PlayerClient(Player):
     def state(self):
         return str(self.status().get('state'))
 
     def state(self):
         return str(self.status().get('state'))
 
+    @property
+    def playmode(self):
+        plm = {'repeat': None,
+               'single': None,
+               'random': None,
+               'consume': None,
+              }
+        for key, val in self.status().items():
+            if key in plm.keys():
+                plm.update({key:bool(int(val))})
+        return plm
+
     @property
     def current(self):
         return self.currentsong()
     @property
     def current(self):
         return self.currentsong()
@@ -293,18 +390,8 @@ class PlayerClient(Player):
         if password:
             try:
                 self._client.password(password)
         if password:
             try:
                 self._client.password(password)
-
-            # Catch errors with the password command (e.g., wrong password)
-            except CommandError as err:
-                raise PlayerError("Could not connect to '%s': "
-                                  "password command failed: %s" %
-                                  (host, err))
-
-            # Catch all other possible errors
             except (MPDError, IOError) as err:
             except (MPDError, IOError) as err:
-                raise PlayerError("Could not connect to '%s': "
-                                  "error with password command: %s" %
-                                  (host, err))
+                raise PlayerError("Could not connect to '%s': %s", (host, err))
         # Controls we have sufficient rights
         needed_cmds = ['status', 'stats', 'add', 'find', \
                        'search', 'currentsong', 'ping']
         # Controls we have sufficient rights
         needed_cmds = ['status', 'stats', 'add', 'find', \
                        'search', 'currentsong', 'ping']
@@ -321,10 +408,12 @@ class PlayerClient(Player):
         if Artist.use_mbid:
             if 'MUSICBRAINZ_ARTISTID' not in self._client.tagtypes():
                 self.log.warning('Use of MusicBrainzIdentifier is set but MPD is '
         if Artist.use_mbid:
             if 'MUSICBRAINZ_ARTISTID' not in self._client.tagtypes():
                 self.log.warning('Use of MusicBrainzIdentifier is set but MPD is '
-                        'not providing related metadata')
+                                 'not providing related metadata')
                 self.log.info(self._client.tagtypes())
                 self.log.warning('Disabling MusicBrainzIdentifier')
                 Artist.use_mbid = False
                 self.log.info(self._client.tagtypes())
                 self.log.warning('Disabling MusicBrainzIdentifier')
                 Artist.use_mbid = False
+            else:
+                self.log.trace('Available metadata: %s', self._client.tagtypes())  # pylint: disable=no-member
         else:
             self.log.warning('Use of MusicBrainzIdentifier disabled!')
             self.log.info('Consider using MusicBrainzIdentifier for your music library')
         else:
             self.log.warning('Use of MusicBrainzIdentifier disabled!')
             self.log.info('Consider using MusicBrainzIdentifier for your music library')