Example #1
0
class RequestPlugin(SongsMenuPlugin):
    PLUGIN_ID = "Request"
    PLUGIN_NAME = _("Autoqueue Request")  # noqa
    PLUGIN_DESC = _(  # noqa
        "Request songs that autoqueue will then work its way toward.")
    PLUGIN_ICON = "gtk-find-and-replace"
    PLUGIN_VERSION = "0.1"

    def __init__(self, *args):
        super(RequestPlugin, self).__init__(*args)
        self.requests = Requests()

    def plugin_songs(self, songs):
        """Add the work to the coroutine pool."""
        for song in songs:
            self.requests.add(song('~filename'))
Example #2
0
 def __init__(self, player):
     self._cache_dir = None
     self.blocking = Blocking()
     self.configuration = Configuration()
     self.context = None
     self.cache = Cache()
     bus = dbus.SessionBus()
     sim = bus.get_object('org.autoqueue',
                          '/org/autoqueue/Similarity',
                          follow_name_owner_changes=True)
     self.similarity = dbus.Interface(
         sim, dbus_interface='org.autoqueue.SimilarityInterface')
     self.has_gaia = self.similarity.has_gaia()
     self.player = player
     self.player.set_variables_from_config(self.configuration)
     self.cache.set_nearby_artist(self.configuration)
     self.requests = Requests()
Example #3
0
 def __init__(self, player):
     self._cache_dir = None
     self.blocking = Blocking()
     self.configuration = Configuration()
     self.context = None
     self.cache = Cache()
     bus = dbus.SessionBus()
     sim = bus.get_object(
         'org.autoqueue', '/org/autoqueue/Similarity',
         follow_name_owner_changes=True)
     self.similarity = dbus.Interface(
         sim, dbus_interface='org.autoqueue.SimilarityInterface')
     self.has_gaia = self.similarity.has_gaia()
     self.player = player
     self.player.set_variables_from_config(self.configuration)
     self.cache.set_nearby_artist(self.configuration)
     self.requests = Requests()
Example #4
0
 def __init__(self, *args):
     super(RequestPlugin, self).__init__(*args)
     self.requests = Requests()
Example #5
0
class AutoQueueBase(object):

    """Generic base class for autoqueue plugins."""

    def __init__(self, player):
        self._cache_dir = None
        self.blocking = Blocking()
        self.configuration = Configuration()
        self.context = None
        self.cache = Cache()
        bus = dbus.SessionBus()
        sim = bus.get_object(
            'org.autoqueue', '/org/autoqueue/Similarity',
            follow_name_owner_changes=True)
        self.similarity = dbus.Interface(
            sim, dbus_interface='org.autoqueue.SimilarityInterface')
        self.has_gaia = self.similarity.has_gaia()
        self.player = player
        self.player.set_variables_from_config(self.configuration)
        self.cache.set_nearby_artist(self.configuration)
        self.requests = Requests()

    @property
    def use_gaia(self):
        return self.configuration.use_gaia and self.has_gaia

    def error_handler(self, *args, **kwargs):
        """Log errors when calling D-Bus methods in a async way."""
        print('Error handler received: %r, %r' % (args, kwargs))

    def allowed(self, song):
        """Check whether a song is allowed to be queued."""
        filename = song.get_filename()
        for qsong in self.get_last_songs():
            if qsong.get_filename() == filename:
                return False

        if self.requests.has(filename):
            return True

        date_search = re.compile("([0-9]{4}-)?%02d-%02d" % (
            self.eoq.month, self.eoq.day))
        for tag in song.get_stripped_tags():
            if date_search.match(tag):
                return True

        for artist in song.get_artists():
            if artist in self.blocking.get_blocked_artists(
                    self.get_last_songs()):
                return False

        return True

    def on_song_ended(self, song, skipped):
        """Should be called by the plugin when a song ends or is skipped."""
        if song is None:
            return

        if skipped:
            return

        for artist_name in song.get_artists():
            self.blocking.block_artist(artist_name)

    def on_song_started(self, song):
        """Should be called by the plugin when a new song starts.

        If the right conditions apply, we start looking for new songs
        to queue.

        """
        if song is None:
            return
        self.cache.song = song
        if self.cache.running:
            return
        if self.configuration.desired_queue_length == 0 or \
                self.queue_needs_songs():
            self.queue_song()
        self.blocking.unblock_artists()
        self.pop_request(song)

    def pop_request(self, song):
        filename = song.get_filename()
        if self.requests.has(filename):
            self.cache.current_request = None
        self.requests.pop(filename)

    def on_removed(self, songs):
        if not self.use_gaia:
            return
        for song in songs:
            self.remove_missing_track(song.get_filename())

    def queue_needs_songs(self):
        """Determine whether the queue needs more songs added."""
        queue_length = self.player.get_queue_length()
        return queue_length < self.configuration.desired_queue_length

    @property
    def eoq(self):
        return datetime.now() + timedelta(0, self.player.get_queue_length())

    def construct_filenames_search(self, filenames):
        return self.player.construct_files_search(filenames)

    def construct_search(self, artist=None, title=None, tags=None,
                         filename=None):
        """Construct a search based on several criteria."""
        if filename:
            return self.player.construct_file_search(filename)
        if title:
            return self.player.construct_track_search(artist, title)
        if artist:
            return self.player.construct_artist_search(artist)
        if tags:
            return self.player.construct_tag_search(tags)

    def queue_song(self):
        """Queue a single track."""
        self.cache.running = True
        self.cache.last_songs = self.get_last_songs()
        song = self.cache.last_song = self.cache.last_songs.pop()
        self.analyze_and_callback(
            song.get_filename(), reply_handler=self.analyzed,
            empty_handler=self.gaia_reply_handler)

    def analyzed(self):
        song = self.cache.last_song
        filename = ensure_string(song.get_filename())
        if not filename:
            return
        if self.use_gaia:
            print('Get similar tracks for: %s' % filename)
            request = self.requests.get_first()
            if request:
                request = ensure_string(request)
                if request:
                    self.similarity.get_ordered_gaia_tracks_by_request(
                        filename, self.configuration.number, request,
                        reply_handler=self.gaia_reply_handler,
                        error_handler=self.error_handler, timeout=TIMEOUT)
                    return
            self.similarity.get_ordered_gaia_tracks(
                filename, self.configuration.number,
                reply_handler=self.gaia_reply_handler,
                error_handler=self.error_handler, timeout=TIMEOUT)
        else:
            self.gaia_reply_handler([])

    def gaia_reply_handler(self, results):
        """Handler for (gaia) similar tracks returned from dbus."""
        self.player.execute_async(self._gaia_reply_handler, results=results)

    def continue_queueing(self):
        if not self.queue_needs_songs():
            self.done()
        else:
            self.queue_song()

    def _gaia_reply_handler(self, results=None):
        """Exexute processing asynchronous."""
        self.cache.found = False
        if results:
            for _ in self.process_filename_results([{'score': match,
                                                     'filename': filename}
                                                    for match, filename
                                                    in results]):
                yield
        if self.cache.found:
            self.continue_queueing()
            return
        self.get_similar_tracks()

    def get_similar_tracks(self):
        if not self.configuration.use_lastfm:
            self.similar_artists_handler([])
            return

        last_song = self.cache.last_song
        artist_name = last_song.get_artist()
        title = last_song.get_title()
        if artist_name and title:
            print('Get similar tracks for: %s - %s' % (artist_name, title))
            self.similarity.get_ordered_similar_tracks(
                artist_name, title,
                reply_handler=self.similar_tracks_handler,
                error_handler=self.error_handler, timeout=TIMEOUT)
        else:
            self.similar_tracks_handler([])

    def done(self):
        """Analyze the last song and stop."""
        song = self.get_last_songs()[-1]
        self.analyze_and_callback(song.get_filename())
        self.cache.running = False

    def similar_tracks_handler(self, results):
        """Handler for similar tracks returned from dbus."""
        self.player.execute_async(
            self._similar_tracks_handler, results=results)

    def _similar_tracks_handler(self, results=None):
        """Exexute processing asynchronous."""
        self.cache.found = False
        for _ in self.process_results([{'score': match, 'artist': artist,
                                        'title': title} for match, artist,
                                       title in results], invert_scores=True):
            yield
        if self.cache.found:
            self.continue_queueing()
            return
        self.get_similar_artists()

    def get_similar_artists(self):
        artists = [
            a.encode('utf-8') for a in self.cache.last_song.get_artists()]
        print('Get similar artists for %s' % artists)
        self.similarity.get_ordered_similar_artists(
            artists, reply_handler=self.similar_artists_handler,
            error_handler=self.error_handler, timeout=TIMEOUT)

    def similar_artists_handler(self, results):
        """Handler for similar artists returned from dbus."""
        self.player.execute_async(
            self._similar_artists_handler, results=results)

    def _similar_artists_handler(self, results=None):
        """Exexute processing asynchronous."""
        self.cache.found = False
        if results:
            for _ in self.process_results([{'score': match, 'artist': artist}
                                           for match, artist in results],
                                          invert_scores=True):
                yield

        if self.cache.found:
            self.continue_queueing()
            return

        if self.configuration.use_groupings:
            for _ in self.process_results(
                    self.get_ordered_similar_by_tag(self.cache.last_song),
                    invert_scores=True):
                yield

            if self.cache.found:
                self.continue_queueing()
                return

        if not self.cache.last_songs:
            self.cache.running = False
            return
        song = self.cache.last_song = self.cache.last_songs.pop()
        self.analyze_and_callback(
            song.get_filename(), reply_handler=self.analyzed,
            empty_handler=self.gaia_reply_handler)

    def analyze_and_callback(self, filename, reply_handler=no_op,
                             empty_handler=no_op):
        filename = ensure_string(filename)
        if not filename:
            return
        if self.use_gaia:
            print('Analyzing: %s' % filename)
            self.similarity.analyze_track(
                filename, reply_handler=reply_handler,
                error_handler=self.error_handler, timeout=TIMEOUT)
        else:
            empty_handler([])

    @staticmethod
    def satisfies(song, criteria):
        """Check whether the song satisfies any of the criteria."""
        filename = criteria.get('filename')
        if filename:
            return filename == song.get_filename()
        title = criteria.get('title')
        artist = criteria.get('artist')
        if title:
            return (
                song.get_title().lower() == title.lower() and
                song.get_artist().lower() == artist.lower())
        if artist:
            artist_lower = artist.lower()
            for song_artist in song.get_artists():
                if song_artist.lower() == artist_lower:
                    return True
            return False
        tags = criteria.get('tags')
        song_tags = song.get_tags()
        for tag in tags:
            if (tag in song_tags or 'artist:%s' % (tag,) in song_tags or
                    'album:%s' % (tag,) in song_tags):
                return True
        return False

    def search_filenames(self, results):
        filenames = [r['filename'] for r in results]
        search = self.construct_filenames_search(filenames)
        self.perform_search(search, results)

    def search_database(self, results):
        """Search for songs in results."""
        for result in results:
            search = self.construct_search(
                artist=result.get('artist'), title=result.get('title'),
                filename=result.get('filename'), tags=result.get('tags'))
            self.perform_search(search, [result])
            yield

    def get_current_request(self):
        if not self.cache.current_request:
            filename = self.requests.get_first()
            if not filename:
                return

            search = self.construct_filenames_search([filename])
            songs = self.player.search(search)
            if not songs:
                return

            self.cache.current_request = songs[0]
        return self.cache.current_request

    def perform_search(self, search, results):
        songs = set(
            self.player.search(
                search, restrictions=self.configuration.restrictions))
        found = set()
        for result in results:
            for song in songs - found:
                if self.satisfies(song, result):
                    result['song'] = song
                    found.add(song)
                    break
            else:
                if not self.configuration.restrictions:
                    filename = result.get('filename')
                    if filename:
                        self.remove_missing_track(filename)

    def remove_missing_track(self, filename):
        print('Remove similarity for %s' % filename)
        if not isinstance(filename, str):
            filename = str(filename, 'utf-8')
        self.similarity.remove_track_by_filename(
            filename, reply_handler=no_op,
            error_handler=self.error_handler, timeout=TIMEOUT)

    def adjust_scores(self, results, invert_scores):
        """Adjust scores based on similarity with previous song and context."""
        if self.configuration.contextualize:
            self.get_current_request()
            self.context = Context(
                context_date=self.eoq, configuration=self.configuration,
                cache=self.cache)
            maximum_score = max(result['score'] for result in results) + 1
            for result in results[:]:
                if 'song' not in result:
                    results.remove(result)
                    continue
                if invert_scores:
                    result['score'] = maximum_score - result['score']
                self.context.adjust_score(result)
                yield

    def process_results(self, results, invert_scores=False):
        """Process results and queue best one(s)."""
        if not results:
            return
        for _ in self.search_database(results):
            yield
        for _ in self.adjust_scores(results, invert_scores):
            yield
        if not results:
            return
        self.pick_result(results)

    def pick_result(self, results):
        for number, result in enumerate(sorted(results,
                                               key=lambda x: x['score'])):
            song = result.get('song')
            if not song:
                print("'song' not found in %s" % result)
                continue
            self.log_lookup(number, result)
            frequency = song.get_play_frequency()
            if frequency is NotImplemented:
                frequency = 1
            rating = song.get_rating()
            if rating is NotImplemented:
                rating = THRESHOLD
            for reason in result.get('reasons', []):
                print("  %s" % (reason,))
            print("score: %.5f, play frequency %.5f" % (rating, frequency))
            comparison = rating
            if self.configuration.favor_new:
                comparison -= frequency
            if (frequency > 0 or not self.configuration.favor_new) and \
                    random.random() > comparison:
                continue

            if self.maybe_enqueue_album(song):
                self.cache.found = True
                return

            if self.allowed(song):
                self.enqueue_song(song)
                self.cache.found = True
                return

    def process_filename_results(self, results):
        if not results:
            return
        self.search_filenames(results)
        for _ in self.adjust_scores(results, invert_scores=False):
            yield
        if not results:
            return
        self.pick_result(results)

    @staticmethod
    def log_lookup(number, result):
        look_for = str(result.get('artist', ''))
        if look_for:
            title = str(result.get('title', ''))
            if title:
                look_for += ' - ' + title
        elif 'filename' in result:
            look_for = str(result['filename'])
        elif 'tags' in result:
            look_for = result['tags']
        else:
            print(repr(result))
            look_for = str(result)
        print('%03d: %06d %s' % (number + 1, result.get('score', 0), look_for))

    def maybe_enqueue_album(self, song):
        """Determine if a whole album should be queued, and do so."""
        if (self.configuration.whole_albums and song.get_tracknumber() == 1 and
                random.random() > .5):
            album = song.get_album()
            album_artist = song.get_album_artist()
            album_id = song.get_musicbrainz_albumid()
            if album and album.lower() not in BANNED_ALBUMS:
                return self.enqueue_album(album, album_artist, album_id)

        return False

    def enqueue_song(self, song):
        self.cache.add_to_previous_terms(song)
        self.player.enqueue(song)

    def enqueue_album(self, album, album_artist, album_id):
        """Try to enqueue whole album."""
        search = self.player.construct_album_search(
            album=album, album_artist=album_artist, album_id=album_id)
        songs = sorted([
            (song.get_discnumber(), song.get_tracknumber(), song)
            for song in self.player.search(search)])
        if songs and all(self.allowed(song[2]) for song in songs):
            for _, _, song in songs:
                self.enqueue_song(song)
            return True
        return False

    def get_last_songs(self):
        """Return the currently playing song plus the songs in the queue."""
        queue = self.player.get_songs_in_queue() or []
        return [self.cache.song] + queue

    def get_ordered_similar_by_tag(self, last_song):
        """Get similar tracks by tag."""
        tag_set = set(last_song.get_non_geo_tags())
        if not tag_set:
            return []
        search = self.construct_search(tags=list(tag_set))
        songs = sorted(
            [(tag_score(song, tag_set), song)
             for song in self.player.search(search)],
            reverse=True)
        return [
            {'score': score, 'filename': song.get_filename()}
            for score, song in songs]
Example #6
0
class AutoQueueBase(object):
    """Generic base class for autoqueue plugins."""
    def __init__(self, player):
        self._cache_dir = None
        self.blocking = Blocking()
        self.configuration = Configuration()
        self.context = None
        self.cache = Cache()
        bus = dbus.SessionBus()
        sim = bus.get_object('org.autoqueue',
                             '/org/autoqueue/Similarity',
                             follow_name_owner_changes=True)
        self.similarity = dbus.Interface(
            sim, dbus_interface='org.autoqueue.SimilarityInterface')
        self.has_gaia = self.similarity.has_gaia()
        self.player = player
        self.player.set_variables_from_config(self.configuration)
        self.cache.set_nearby_artist(self.configuration)
        self.requests = Requests()

    @property
    def use_gaia(self):
        return self.configuration.use_gaia and self.has_gaia

    def error_handler(self, *args, **kwargs):
        """Log errors when calling D-Bus methods in a async way."""
        print('Error handler received: %r, %r' % (args, kwargs))

    def allowed(self, song):
        """Check whether a song is allowed to be queued."""
        filename = song.get_filename()
        for qsong in self.get_last_songs():
            if qsong.get_filename() == filename:
                return False

        if self.requests.has(filename):
            return True

        date_search = re.compile("([0-9]{4}-)?%02d-%02d" %
                                 (self.eoq.month, self.eoq.day))
        for tag in song.get_stripped_tags():
            if date_search.match(tag):
                return True

        for artist in song.get_artists():
            if artist in self.blocking.get_blocked_artists(
                    self.get_last_songs()):
                return False

        return True

    def on_song_ended(self, song, skipped):
        """Should be called by the plugin when a song ends or is skipped."""
        if song is None:
            return

        if skipped:
            return

        for artist_name in song.get_artists():
            self.blocking.block_artist(artist_name)

    def on_song_started(self, song):
        """Should be called by the plugin when a new song starts.

        If the right conditions apply, we start looking for new songs
        to queue.

        """
        if song is None:
            return
        self.cache.song = song
        if self.cache.running:
            return
        if self.configuration.desired_queue_length == 0 or \
                self.queue_needs_songs():
            self.queue_song()
        self.blocking.unblock_artists()
        self.pop_request(song)

    def pop_request(self, song):
        filename = song.get_filename()
        if self.requests.has(filename):
            self.cache.current_request = None
        self.requests.pop(filename)

    def on_removed(self, songs):
        if not self.use_gaia:
            return
        for song in songs:
            self.remove_missing_track(song.get_filename())

    def queue_needs_songs(self):
        """Determine whether the queue needs more songs added."""
        queue_length = self.player.get_queue_length()
        return queue_length < self.configuration.desired_queue_length

    @property
    def eoq(self):
        return datetime.now() + timedelta(0, self.player.get_queue_length())

    def construct_filenames_search(self, filenames):
        return self.player.construct_files_search(filenames)

    def construct_search(self,
                         artist=None,
                         title=None,
                         tags=None,
                         filename=None):
        """Construct a search based on several criteria."""
        if filename:
            return self.player.construct_file_search(filename)
        if title:
            return self.player.construct_track_search(artist, title)
        if artist:
            return self.player.construct_artist_search(artist)
        if tags:
            return self.player.construct_tag_search(tags)

    def queue_song(self):
        """Queue a single track."""
        self.cache.running = True
        self.cache.last_songs = self.get_last_songs()
        song = self.cache.last_song = self.cache.last_songs.pop()
        self.analyze_and_callback(song.get_filename(),
                                  reply_handler=self.analyzed,
                                  empty_handler=self.gaia_reply_handler)

    def analyzed(self):
        song = self.cache.last_song
        filename = ensure_string(song.get_filename())
        if not filename:
            return
        if self.use_gaia:
            print('Get similar tracks for: %s' % filename)
            request = self.requests.get_first()
            if request:
                request = ensure_string(request)
                if request:
                    self.similarity.get_ordered_gaia_tracks_by_request(
                        filename,
                        self.configuration.number,
                        request,
                        reply_handler=self.gaia_reply_handler,
                        error_handler=self.error_handler,
                        timeout=TIMEOUT)
                    return
            self.similarity.get_ordered_gaia_tracks(
                filename,
                self.configuration.number,
                reply_handler=self.gaia_reply_handler,
                error_handler=self.error_handler,
                timeout=TIMEOUT)
        else:
            self.gaia_reply_handler([])

    def gaia_reply_handler(self, results):
        """Handler for (gaia) similar tracks returned from dbus."""
        self.player.execute_async(self._gaia_reply_handler, results=results)

    def continue_queueing(self):
        if not self.queue_needs_songs():
            self.done()
        else:
            self.queue_song()

    def _gaia_reply_handler(self, results=None):
        """Exexute processing asynchronous."""
        self.cache.found = False
        if results:
            for _ in self.process_filename_results([{
                    'score': match,
                    'filename': filename
            } for match, filename in results]):
                yield
        if self.cache.found:
            self.continue_queueing()
            return
        self.get_similar_tracks()

    def get_similar_tracks(self):
        if not self.configuration.use_lastfm:
            self.similar_artists_handler([])
            return

        last_song = self.cache.last_song
        artist_name = last_song.get_artist()
        title = last_song.get_title()
        if artist_name and title:
            print('Get similar tracks for: %s - %s' % (artist_name, title))
            self.similarity.get_ordered_similar_tracks(
                artist_name,
                title,
                reply_handler=self.similar_tracks_handler,
                error_handler=self.error_handler,
                timeout=TIMEOUT)
        else:
            self.similar_tracks_handler([])

    def done(self):
        """Analyze the last song and stop."""
        song = self.get_last_songs()[-1]
        self.analyze_and_callback(song.get_filename())
        self.cache.running = False

    def similar_tracks_handler(self, results):
        """Handler for similar tracks returned from dbus."""
        self.player.execute_async(self._similar_tracks_handler,
                                  results=results)

    def _similar_tracks_handler(self, results=None):
        """Exexute processing asynchronous."""
        self.cache.found = False
        for _ in self.process_results([{
                'score': match,
                'artist': artist,
                'title': title
        } for match, artist, title in results],
                                      invert_scores=True):
            yield
        if self.cache.found:
            self.continue_queueing()
            return
        self.get_similar_artists()

    def get_similar_artists(self):
        artists = [
            a.encode('utf-8') for a in self.cache.last_song.get_artists()
        ]
        print('Get similar artists for %s' % artists)
        self.similarity.get_ordered_similar_artists(
            artists,
            reply_handler=self.similar_artists_handler,
            error_handler=self.error_handler,
            timeout=TIMEOUT)

    def similar_artists_handler(self, results):
        """Handler for similar artists returned from dbus."""
        self.player.execute_async(self._similar_artists_handler,
                                  results=results)

    def _similar_artists_handler(self, results=None):
        """Exexute processing asynchronous."""
        self.cache.found = False
        if results:
            for _ in self.process_results([{
                    'score': match,
                    'artist': artist
            } for match, artist in results],
                                          invert_scores=True):
                yield

        if self.cache.found:
            self.continue_queueing()
            return

        if self.configuration.use_groupings:
            for _ in self.process_results(self.get_ordered_similar_by_tag(
                    self.cache.last_song),
                                          invert_scores=True):
                yield

            if self.cache.found:
                self.continue_queueing()
                return

        if not self.cache.last_songs:
            self.cache.running = False
            return
        song = self.cache.last_song = self.cache.last_songs.pop()
        self.analyze_and_callback(song.get_filename(),
                                  reply_handler=self.analyzed,
                                  empty_handler=self.gaia_reply_handler)

    def analyze_and_callback(self,
                             filename,
                             reply_handler=no_op,
                             empty_handler=no_op):
        filename = ensure_string(filename)
        if not filename:
            return
        if self.use_gaia:
            print('Analyzing: %s' % filename)
            self.similarity.analyze_track(filename,
                                          reply_handler=reply_handler,
                                          error_handler=self.error_handler,
                                          timeout=TIMEOUT)
        else:
            empty_handler([])

    @staticmethod
    def satisfies(song, criteria):
        """Check whether the song satisfies any of the criteria."""
        filename = criteria.get('filename')
        if filename:
            return filename == song.get_filename()
        title = criteria.get('title')
        artist = criteria.get('artist')
        if title:
            return (song.get_title().lower() == title.lower()
                    and song.get_artist().lower() == artist.lower())
        if artist:
            artist_lower = artist.lower()
            for song_artist in song.get_artists():
                if song_artist.lower() == artist_lower:
                    return True
            return False
        tags = criteria.get('tags')
        song_tags = song.get_tags()
        for tag in tags:
            if (tag in song_tags or 'artist:%s' % (tag, ) in song_tags
                    or 'album:%s' % (tag, ) in song_tags):
                return True
        return False

    def search_filenames(self, results):
        filenames = [r['filename'] for r in results]
        search = self.construct_filenames_search(filenames)
        self.perform_search(search, results)

    def search_database(self, results):
        """Search for songs in results."""
        for result in results:
            search = self.construct_search(artist=result.get('artist'),
                                           title=result.get('title'),
                                           filename=result.get('filename'),
                                           tags=result.get('tags'))
            self.perform_search(search, [result])
            yield

    def get_current_request(self):
        if not self.cache.current_request:
            filename = self.requests.get_first()
            if not filename:
                return

            search = self.construct_filenames_search([filename])
            songs = self.player.search(search)
            if not songs:
                return

            self.cache.current_request = songs[0]
        return self.cache.current_request

    def perform_search(self, search, results):
        songs = set(
            self.player.search(search,
                               restrictions=self.configuration.restrictions))
        found = set()
        for result in results:
            for song in songs - found:
                if self.satisfies(song, result):
                    result['song'] = song
                    found.add(song)
                    break
            else:
                if not self.configuration.restrictions:
                    filename = result.get('filename')
                    if filename:
                        self.remove_missing_track(filename)

    def remove_missing_track(self, filename):
        print('Remove similarity for %s' % filename)
        if not isinstance(filename, str):
            filename = str(filename, 'utf-8')
        self.similarity.remove_track_by_filename(
            filename,
            reply_handler=no_op,
            error_handler=self.error_handler,
            timeout=TIMEOUT)

    def adjust_scores(self, results, invert_scores):
        """Adjust scores based on similarity with previous song and context."""
        if self.configuration.contextualize:
            self.get_current_request()
            self.context = Context(context_date=self.eoq,
                                   configuration=self.configuration,
                                   cache=self.cache)
            maximum_score = max(result['score'] for result in results) + 1
            for result in results[:]:
                if 'song' not in result:
                    results.remove(result)
                    continue
                if invert_scores:
                    result['score'] = maximum_score - result['score']
                self.context.adjust_score(result)
                yield

    def process_results(self, results, invert_scores=False):
        """Process results and queue best one(s)."""
        if not results:
            return
        for _ in self.search_database(results):
            yield
        for _ in self.adjust_scores(results, invert_scores):
            yield
        if not results:
            return
        self.pick_result(results)

    def pick_result(self, results):
        for number, result in enumerate(
                sorted(results, key=lambda x: x['score'])):
            song = result.get('song')
            if not song:
                print("'song' not found in %s" % result)
                continue
            self.log_lookup(number, result)
            frequency = song.get_play_frequency()
            if frequency is NotImplemented:
                frequency = 1
            rating = song.get_rating()
            if rating is NotImplemented:
                rating = THRESHOLD
            for reason in result.get('reasons', []):
                print("  %s" % (reason, ))
            print("score: %.5f, play frequency %.5f" % (rating, frequency))
            comparison = rating
            if self.configuration.favor_new:
                comparison -= frequency
            if (frequency > 0 or not self.configuration.favor_new) and \
                    random.random() > comparison:
                continue

            if self.maybe_enqueue_album(song):
                self.cache.found = True
                return

            if self.allowed(song):
                self.enqueue_song(song)
                self.cache.found = True
                return

    def process_filename_results(self, results):
        if not results:
            return
        self.search_filenames(results)
        for _ in self.adjust_scores(results, invert_scores=False):
            yield
        if not results:
            return
        self.pick_result(results)

    @staticmethod
    def log_lookup(number, result):
        look_for = str(result.get('artist', ''))
        if look_for:
            title = str(result.get('title', ''))
            if title:
                look_for += ' - ' + title
        elif 'filename' in result:
            look_for = str(result['filename'])
        elif 'tags' in result:
            look_for = result['tags']
        else:
            print(repr(result))
            look_for = str(result)
        print('%03d: %06d %s' % (number + 1, result.get('score', 0), look_for))

    def maybe_enqueue_album(self, song):
        """Determine if a whole album should be queued, and do so."""
        if (self.configuration.whole_albums and song.get_tracknumber() == 1
                and random.random() > .5):
            album = song.get_album()
            album_artist = song.get_album_artist()
            album_id = song.get_musicbrainz_albumid()
            if album and album.lower() not in BANNED_ALBUMS:
                return self.enqueue_album(album, album_artist, album_id)

        return False

    def enqueue_song(self, song):
        self.cache.add_to_previous_terms(song)
        self.player.enqueue(song)

    def enqueue_album(self, album, album_artist, album_id):
        """Try to enqueue whole album."""
        search = self.player.construct_album_search(album=album,
                                                    album_artist=album_artist,
                                                    album_id=album_id)
        songs = sorted([(song.get_discnumber(), song.get_tracknumber(), song)
                        for song in self.player.search(search)])
        if songs and all(self.allowed(song[2]) for song in songs):
            for _, _, song in songs:
                self.enqueue_song(song)
            return True
        return False

    def get_last_songs(self):
        """Return the currently playing song plus the songs in the queue."""
        queue = self.player.get_songs_in_queue() or []
        return [self.cache.song] + queue

    def get_ordered_similar_by_tag(self, last_song):
        """Get similar tracks by tag."""
        tag_set = set(last_song.get_non_geo_tags())
        if not tag_set:
            return []
        search = self.construct_search(tags=list(tag_set))
        songs = sorted([(tag_score(song, tag_set), song)
                        for song in self.player.search(search)],
                       reverse=True)
        return [{
            'score': score,
            'filename': song.get_filename()
        } for score, song in songs]