]> 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
 
 # Script starts here
 from sima.launch import main
+
 main()
 
 # VIM MODLINE
 main()
 
 # VIM MODLINE
index f3d1614ad6b6672093024feca685604c262d9fba..b8ccea522a2ee62993ee22beb42b631120e42c4a 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 "{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
 
 class PlayerClient(Player):
     """MPD Client
@@ -137,8 +160,10 @@ class PlayerClient(Player):
             self.log.info('Player: Initialising cache!')
         self._cache = {
                 'artists': None,
             self.log.info('Player: Initialising cache!')
         self._cache = {
                 'artists': None,
+                'nombid_artists': None,
                 }
         self._cache['artists'] = frozenset(self._client.list('artist'))
                 }
         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):
 
     @blacklist(track=True)
     def find_track(self, artist, title=None):
@@ -156,6 +181,62 @@ class PlayerClient(Player):
                                         'title', title))
         return list(tracks)
 
                                         '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)
     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
 """
 
 Defines some object to handle audio file metadata
 """
 
+import collections.abc  # python >= 3.3
 import logging
 import re
 
 import logging
 import re
 
@@ -68,7 +69,7 @@ class Meta:
         if 'mbid' in kwargs and kwargs.get('mbid'):
             try:
                 is_uuid4(kwargs.get('mbid'))
         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')))
             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 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):
         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')
         """
             >>> 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'):
         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)
 
             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
 # 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
 
         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.
 
 class Player(object):
     """Player interface to inherit from.
@@ -149,52 +127,17 @@ class Player(object):
         """
         raise NotImplementedError
 
         """
         raise NotImplementedError
 
-    @bl_artist
     def search_artist(self, artist):
         """
         Search artists based on a fuzzy search in the media library
     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']
 
             >>> 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
 
     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
 # 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):
 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):
         return as_art
 
     def get_recursive_similar_artist(self):
-        history = deque(self.history)
-        history.popleft()
-        depth = 0
         if not self.player.playlist:
             return
         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()
         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
         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
                 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:
                            '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):
         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')
         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:
         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
             # 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.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
             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: '
         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 - 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))
         # 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):
         return candidates
 
     def _get_album_history(self, artist=None):
index 031999fe38d2e6b1cf93f9384776f40378150138..cf55a0e34f7e01488fc1193da3db29529b9c0feb 100644 (file)
@@ -2,7 +2,7 @@
 
 import unittest
 
 
 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'
 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')
     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, 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')
 
     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)
 
         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')
     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')
                            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'
 
         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(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')
     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):
 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'),
                   '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.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
 
 # vim: ai ts=4 sw=4 sts=4 expandtab