]> kaliko git repositories - mpd-sima.git/commitdiff
Better MusicBrainz ID integration
authorkaliko <kaliko@azylum.org>
Tue, 16 Dec 2014 16:35:06 +0000 (17:35 +0100)
committerkaliko <kaliko@azylum.org>
Tue, 16 Dec 2014 16:35:06 +0000 (17:35 +0100)
mpd-sima
sima/client.py
sima/lib/meta.py
sima/lib/player.py
sima/lib/webserv.py
tests/test_meta.py

index 5cf19d03a76588929a5c1d920d77a60107246533..33d56effd331972fbd88f89eb47fc4fbda772b7a 100755 (executable)
--- a/mpd-sima
+++ b/mpd-sima
@@ -3,6 +3,7 @@
 
 # Script starts here
 from sima.launch import main
+
 main()
 
 # VIM MODLINE
index f3d1614ad6b6672093024feca685604c262d9fba..b8ccea522a2ee62993ee22beb42b631120e42c4a 100644 (file)
@@ -36,6 +36,7 @@ except ImportError as err:
     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
@@ -50,6 +51,28 @@ class PlayerCommandError(PlayerError):
 
 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 "{0}"'.format(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
@@ -137,8 +160,10 @@ class PlayerClient(Player):
             self.log.info('Player: Initialising cache!')
         self._cache = {
                 'artists': None,
+                'nombid_artists': None,
                 }
         self._cache['artists'] = frozenset(self._client.list('artist'))
+        self._cache['nombid_artists'] = frozenset(self._client.list('artist', 'musicbrainz_artistid', ''))
 
     @blacklist(track=True)
     def find_track(self, artist, title=None):
@@ -156,6 +181,62 @@ class PlayerClient(Player):
                                         'title', title))
         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
+            [artist.add_alias(name) for name in
+                    self._client.list('artist', 'musicbrainz_artistid', artist.mbid)]
+            if artist.aliases:
+                found = True
+        else:
+            artist = Artist(name=artist.name)
+        # then complete with fuzzy search on artist with no musicbrainz_artistid
+        nombid_artists = self._cache.get('nombid_artists', [])
+        match = get_close_matches(artist.name, nombid_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: {}'.format('/'.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)
index 44369a272e8cc51f1313d9b60519e0e59ac4589f..5bf1e5fbbca9c9cf93e99cb1801c25427afee101 100644 (file)
@@ -21,6 +21,7 @@
 Defines some object to handle audio file metadata
 """
 
+import collections.abc  # python >= 3.3
 import logging
 import re
 
@@ -68,7 +69,7 @@ class Meta:
         if 'mbid' in kwargs and kwargs.get('mbid'):
             try:
                 is_uuid4(kwargs.get('mbid'))
-                self.__mbid = kwargs.pop('mbid')
+                self.__mbid = kwargs.pop('mbid').upper()
             except WrongUUID4:
                 self.log.warning('Wrong mbid {}:{}'.format(self.__name,
                                                          kwargs.get('mbid')))
@@ -88,8 +89,7 @@ class Meta:
         """
         #if hasattr(other, 'mbid'):  # better isinstance?
         if isinstance(other, Meta) and self.mbid and other.mbid:
-            if self.mbid and other.mbid:
-                return self.mbid == other.mbid
+            return self.mbid == other.mbid
         elif isinstance(other, Meta):
             return bool(self.names & other.names)
         elif getattr(other, '__str__', None):
@@ -149,7 +149,7 @@ class Artist(Meta):
             >>> artobj0 = Artist(**trk)
             >>> artobj1 = Artist(name='Tool')
         """
-        name = kwargs.get('artist', name)
+        name = kwargs.get('artist', name).split(', ')[0]
         mbid = kwargs.get('musicbrainz_artistid', mbid)
         if (kwargs.get('albumartist', False) and
                 kwargs.get('albumartist') != 'Various Artists'):
@@ -159,5 +159,29 @@ class Artist(Meta):
             mbid = kwargs.get('musicbrainz_albumartistid').split(', ')[0]
         super().__init__(name=name, mbid=mbid)
 
+class MetaContainer(collections.abc.Set):
+
+    def __init__(self, iterable):
+        self.elements = lst = []
+        for value in iterable:
+            if value not in lst:
+                lst.append(value)
+            else:
+                for inlst in lst:
+                    if value == inlst:
+                        inlst.add_alias(value)
+
+    def __iter__(self):
+        return iter(self.elements)
+
+    def __contains__(self, value):
+        return value in self.elements
+
+    def __len__(self):
+        return len(self.elements)
+
+    def __repr__(self):
+        return repr(self.elements)
+
 # VIM MODLINE
 # vim: ai ts=4 sw=4 sts=4 expandtab
index 20a4958d9e738cff968fb1b9f777ee6a22300da4..592091196d856a730d885f0435ac15d064993778 100644 (file)
@@ -60,28 +60,6 @@ def blacklist(artist=False, album=False, track=False):
         return wrapper
     return decorated
 
-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 "{0}"'.format(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 Player(object):
     """Player interface to inherit from.
@@ -149,52 +127,17 @@ class Player(object):
         """
         raise NotImplementedError
 
-    @bl_artist
     def search_artist(self, artist):
         """
         Search artists based on a fuzzy search in the media library
-            >>> bea = player.search_artist('The beatles')
+            >>> art = Artist(name='the beatles', mbid=<UUID4>) # mbid optional
+            >>> bea = player.search_artist(art)
             >>> print(bea.names)
             >>> ['The Beatles', 'Beatles', 'the beatles']
 
-        Returns a list of strings (artist names)
+        Returns an Artist object
         """
-        found = False
-        # Then proceed with fuzzy matching if got nothing
-        match = get_close_matches(artist.name, self.artists, 50, 0.73)
-        if not match:
-            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: {}'.format('/'.join(artist.names)))
-            return artist
-        return
+        raise NotImplementedError
 
     def disconnect(self):
         """Closing client connection with the Player
index 79a9b9b01c58abb342c2eb1e4668809b2cf858f6..d7fb6faa4558219d1046942328ea6e7f4882b33e 100644 (file)
@@ -32,7 +32,7 @@ from hashlib import md5
 # local import
 from .plugin import Plugin
 from .track import Track
-from .meta import Artist
+from .meta import Artist, MetaContainer
 from ..utils.utils import WSError, WSNotFound
 
 def cache(func):
@@ -189,20 +189,21 @@ class WebService(Plugin):
         return as_art
 
     def get_recursive_similar_artist(self):
-        history = deque(self.history)
-        history.popleft()
-        depth = 0
         if not self.player.playlist:
             return
-        last_trk = self.player.playlist[-1]
+        history = list(self.history)
+        history = self.player.queue + history
+        history = deque(history)
+        last_trk = history.popleft() # remove
         extra_arts = list()
         ret_extra = list()
+        depth = 0
         while depth < self.plugin_conf.getint('depth'):
             if len(history) == 0:
                 break
             trk = history.popleft()
             if (trk.Artist in extra_arts
-                or trk.Artist == last_trk.Artist):
+                    or trk.Artist == last_trk.Artist):
                 continue
             extra_arts.append(trk.Artist)
             depth += 1
@@ -213,10 +214,14 @@ class WebService(Plugin):
                            'to "{}" as well'.format(artist))
             similar = self.ws_similar_artists(artist=artist)
             if not similar:
-                return []
-            ret_extra = self.get_artists_from_player(similar)
-            if last_trk.Artist in ret_extra:
-                ret_extra.remove(last_trk.Artist)
+                continue
+            ret_extra.extend(self.get_artists_from_player(similar))
+
+        if ret_extra:
+            self.log.debug('similar artist(s) fond: {}...'.format(
+                ' / '.join(map(str, ret_extra))))
+        if last_trk.Artist in ret_extra:
+            ret_extra.remove(last_trk.Artist)
         return ret_extra
 
     def get_local_similar_artists(self):
@@ -234,7 +239,7 @@ class WebService(Plugin):
         self.log.info('First five similar artist(s): {}...'.format(
                       ' / '.join(map(str, list(similar)[:5]))))
         self.log.info('Looking availability in music library')
-        ret = set(self.get_artists_from_player(similar))
+        ret = MetaContainer(self.get_artists_from_player(similar))
         ret_extra = None
         if len(self.history) >= 2:
             if self.plugin_conf.getint('depth') > 1:
@@ -243,30 +248,30 @@ class WebService(Plugin):
             # get them reorg to pick up best element
             ret_extra = self._get_artists_list_reorg(ret_extra)
             # pickup half the number of ret artist
-            ret_extra = set(ret_extra[:len(ret)//2])
+            ret_extra = MetaContainer(ret_extra[:len(ret)//2])
             self.log.debug('Using extra: {}'.format(
                            ' / '.join(map(str, ret_extra))))
             ret = ret | ret_extra
         if not ret:
             self.log.warning('Got nothing from music library.')
-            self.log.warning('Try running in debug mode to guess why...')
             return []
         # WARNING:
         #   * operation on set will not match against aliases
         #   * composite set w/ mbid set and whitout won't match either
-        queued_artists = {trk.Artist for trk in self.player.queue}
+        queued_artists = MetaContainer([trk.Artist for trk in self.player.queue])
         if ret & queued_artists:
             self.log.debug('Removing already queued artists: '
-                           '{0}'.format(ret & queued_artists))
+                           '{0}'.format('/'.join(map(str, ret & queued_artists))))
             ret = ret - queued_artists
         if self.player.current and self.player.current.Artist in ret:
             self.log.debug('Removing current artist: {0}'.format(self.player.current.Artist))
-            ret = ret - {self.player.current.Artist}
+            ret = ret -  MetaContainer([self.player.current.Artist])
         # Move around similars items to get in unplayed|not recently played
         # artist first.
         self.log.info('Got {} artists in library'.format(len(ret)))
         candidates = self._get_artists_list_reorg(list(ret))
-        self.log.info(' / '.join(map(str, candidates)))
+        if candidates:
+            self.log.info(' / '.join(map(str, candidates)))
         return candidates
 
     def _get_album_history(self, artist=None):
index 031999fe38d2e6b1cf93f9384776f40378150138..cf55a0e34f7e01488fc1193da3db29529b9c0feb 100644 (file)
@@ -2,7 +2,7 @@
 
 import unittest
 
-from sima.lib.meta import Meta, Artist, is_uuid4
+from sima.lib.meta import Meta, Artist, MetaContainer, is_uuid4
 from sima.lib.meta import WrongUUID4, MetaException
 
 VALID = '110E8100-E29B-41D1-A716-116655250000'
@@ -31,23 +31,20 @@ class TestMetaObject(unittest.TestCase):
     def test_equality(self):
         a = Meta(mbid=VALID, name='a')
         b = Meta(mbid=VALID, name='b')
+        c = Meta(mbid=VALID.lower(), name='c')
         self.assertEqual(a, b)
+        self.assertEqual(a, c)
 
     def test_hash(self):
         a = Meta(mbid=VALID, name='a')
         b = Meta(mbid=VALID, name='b')
         c = Meta(mbid=VALID, name='c')
-        self.assertTrue(len({a,b,c}) == 1)
+        self.assertTrue(len({a, b, c}) == 1)
         self.assertTrue(a in [c, b])
         self.assertTrue(a in {c, b})
         # mbid is immutable
         self.assertRaises(AttributeError, a.__setattr__, 'mbid', VALID)
 
-    def test_identity(self):
-        a = Meta(mbid=VALID, name='a')
-        b = Meta(mbid=VALID, name='a')
-        self.assertTrue(a is not b)
-
     def test_aliases(self):
         art0 = Meta(name='Silver Mt. Zion')
         art0.add_alias('A Silver Mt. Zion')
@@ -67,6 +64,8 @@ class TestMetaObject(unittest.TestCase):
                            mbid='f22942a1-6f70-4f48-866e-238cb2308fbd')
         art02 = Meta(name='Some Other Name not even close, avoid fuzzy match',
                            mbid='f22942a1-6f70-4f48-866e-238cb2308fbd')
+        art03 = Meta(name='Aphex Twin',
+                           mbid='322942a1-6f70-4f48-866e-238cb2308fbd')
 
         self.assertTrue(len({art00, art02}) == 1)
         art00._Meta__name = art02._Meta__name = 'Aphex Twin'
@@ -84,6 +83,9 @@ class TestMetaObject(unittest.TestCase):
         self.assertTrue(len({art00, art02}) == 1,
                         'wrong: hash({!r}) != hash({!r})'.format(art00, art02))
 
+        self.assertTrue(hash(art00) != hash(art03),
+                        'wrong: hash({!r}) == hash({!r})'.format(art00, art03))
+
     def test_comparison(self):
         art00 = Meta(name='Aphex Twin',
                      mbid='f22942a1-6f70-4f48-866e-238cb2308fbd')
@@ -106,7 +108,7 @@ class TestMetaObject(unittest.TestCase):
 class TestArtistObject(unittest.TestCase):
 
     def test_init(self):
-        artist = {'artist': ['Name featuring', 'Feature'],
+        artist = {'artist': ', '.join(['Original Name', 'Featuring Nane', 'Featureā€¦']),
                   'albumartist': 'Name',
                   'musicbrainz_artistid': VALID,
                   'musicbrainz_albumartistid': VALID.replace('11', '22'),
@@ -119,5 +121,36 @@ class TestArtistObject(unittest.TestCase):
         self.assertTrue(art.mbid == VALID)
         artist.pop('albumartist')
         art = Artist(**artist)
+        self.assertTrue(art.name == 'Original Name', art.name)
+
+
+class TestMetaContainers(unittest.TestCase):
+
+    def  test_init(self):
+        a = Meta(mbid=VALID, name='a')
+        b = Meta(mbid=VALID, name='b')
+        c = Meta(mbid=VALID.replace('11', '22'), name='b')
+        # redondant with Meta test_comparison, but anyway
+        cont = MetaContainer([a, b, c])
+        self.assertTrue(len(cont) == 2)
+        self.assertTrue(a in cont)
+        self.assertTrue(b in cont)
+        self.assertTrue(Meta(name='a') in cont)
+
+    def test_intersection_difference(self):
+        # Now set works as expected with composite (name/mbid) collections of Meta
+        # cf Meta test_union
+        # >>> len(MetaContainer([Artist(name='Name'), Artist(name='Name', mbid=<UUID4>)]) == 1
+        # but
+        # >>> len({Artist(name='Name'), Artist(name='Name', mbid=<UUID4>}) == 2
+        art00 = Meta(name='Aphex Twin', mbid='f22942a1-6f70-4f48-866e-238cb2308fbd')
+        art01 = Meta(name='Aphex Twin', mbid=None)
+        self.assertTrue(MetaContainer([art00]) & MetaContainer([art01]))
+        self.assertFalse(MetaContainer([art01]) - MetaContainer([art01]))
+        art01._Meta__mbid = art00.mbid
+        self.assertTrue(MetaContainer([art00]) & MetaContainer([art01]))
+        self.assertFalse(MetaContainer([art01]) - MetaContainer([art01]))
+        art01._Meta__mbid = art00.mbid.replace('229', '330')
+        self.assertFalse(MetaContainer([art00]) & MetaContainer([art01]))
 
 # vim: ai ts=4 sw=4 sts=4 expandtab