# Script starts here
from sima.launch import main
+
main()
# VIM MODLINE
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
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
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):
'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)
Defines some object to handle audio file metadata
"""
+import collections.abc # python >= 3.3
import logging
import re
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')))
"""
#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):
>>> 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_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
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.
"""
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
# 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):
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
'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):
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:
# 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):
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'
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')
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,
'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')
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'),
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