Example #1
0
File: library.py Project: pscn/ads
class Library(Worker):
  _config = None
  _library_filename= None

  # 5 seconds
  _queue_timeout = 5
  _queue = None
  _running = False
  _update_queued = False

  _db_version = 1

  # general purpose dict
  _client_data = {}

  _history= deque()

  _lastplayed_track= None
  _playing_track= None

  _sync= Condition()

  _factor_ranking = {
        'relation'     : 0.98,
        'rating'       : 0.01,
        'random'       : 0.01,
      }

  _relation_decay = 0.5

  _ranking_updated = False

  # default rating of unplayed tracks.  set > 0.5 to favor unplayed < 0.5 to
  # inhibit unplayed
  _unplayed_rating = 0.5

  _relation_resetted = False

  _queue_update_factor = 0.5

  # thresholds in seconds
  _thres_track_lastplayed= 60 * 60 * 24 * 7 * 4 # 4weeks
  _thres_track_laststarted= 60 * 60 * 24 # 1day
  _thres_track_lastqueued= 10 * 60 # 10min

  _thres_artist_laststarted = 10 * 60 # 10min
  _thres_artist_lastplayed = 10 * 60 # 10min
  _thres_artist_lastqueued= 60

  # 6 months
  _thres_lastfm_lookup = 60 * 60 * 24 * 7 * 4 * 6
  _thres_echonest_lookup = 60 * 60 * 24 * 7 * 4 * 6

  # apply relations to maxhistory entries:
  # def fac(x, m=maxhistory): return -sqrt(1.0*x/m) + 1.0
  # e.g. 5: 1.00 0.55 0.36 0.22 0.10
  # e.g. 10: 1.00 0.68 0.55 0.45 0.37 0.29 0.23 0.16 0.11 0.05
  maxhistory= 5

  _title= 'library'

  _ranking_base = 1000

  _lastfm = None
  _echonest = None
  _queue_lookup_results = True

  _seed_tracks = set()
  _seed_artists = set()

  def __init__(self, config):
    Log.__init__(self, self._title)
    self._logger.info(u"init")
    Worker.__init__(self, lifo=False)
    self._config = config
    self._library_filename = self._config.get('Library', 'path',
        join(self._config.configdir(), 'library.pkl'))
    Factories.clear()
    Logger.set_logger(self._logger)

    if self._config.get('TrackRelation', 'use_db', True):
      TrackRelationFactory.use_db()
      TrackRelationFactory.set_path(self._config.get('TrackRelation',
          'path', join(self._config.configdir(), '')))
    else:
      TrackRelationFactory.use_fs()
      TrackRelationFactory.set_path(self._config.get('TrackRelation',
          'path', join(self._config.configdir(), 'track')))
    TrackRelationFactory.set_maxentries(
        self._config.get('TrackRelation', 'maxentries', 500))

    if self._config.get('ArtistRelation', 'use_db', True):
      ArtistRelationFactory.use_db()
      ArtistRelationFactory.set_path(self._config.get('ArtistRelation',
          'path', join(self._config.configdir(), '')))
    else:
      ArtistRelationFactory.use_fs()
      ArtistRelationFactory.set_path(self._config.get('ArtistRelation',
          'path', join(self._config.configdir(), 'artist')))
    ArtistRelationFactory.set_maxentries(
        self._config.get('ArtistRelation', 'maxentries', 500))

    if self._config.get('Lookup', 'QueueResults', 'False') == 'True':
      self._queue_lookup_results = True
    else:
      self._queue_lookup_results = False

    # FIXME:  log if one of those libs is not present
    if self._config.get('Lookup', 'UseLastFM', 'True') == 'True':
      self._lastfm = LastFM(config)
      self._lastfm.start()
      self._thres_lastfm_lookup = self._config.getint('Lookup',
          'ThresholdLastFM', self._thres_lastfm_lookup)

    if self._config.get('Lookup', 'UseEchoNest', 'True') == 'True':
      self._echonest = EchoNest(config)
      self._echonest.start()
      self._thres_echonest_lookup = self._config.getint('Lookup',
          'ThresholdEchoNest', self._thres_echonest_lookup)

    # read, normalize and update ranking factors
    factor = 0.0
    for k in self._factor_ranking.keys():
      self._factor_ranking[k] = self._config.getfloat('Ranking',
          "factor%s" % k, self._factor_ranking[k])
      factor += self._factor_ranking[k]
    for k in self._factor_ranking.keys():
      self._factor_ranking[k] /= factor
      self._config.set('Ranking', "factor%s" % k, self._factor_ranking[k])

    self._relation_decay = self._config.getfloat('Ranking', 'RelationDecay',
        self._relation_decay)

    self._queue_update_factor = self._config.getfloat('Rating',
        'QueueUpdateFactor', self._queue_update_factor)

    self._unplayed_rating = self._config.getfloat('Ranking',
        'UnplayedRating', self._unplayed_rating)

    self._thres_track_lastplayed = self._config.getint('Ranking',
        'ThresholdTrackLastPlayed', self._thres_track_lastplayed)
    self._thres_track_laststarted= self._config.getint('Ranking',
        'ThresholdTrackLastStarted', self._thres_track_laststarted)
    self._thres_track_lastqueued= self._config.getint('Ranking',
        'ThresholdTrackLastQueued', self._thres_track_lastqueued)
    self._thres_artist_lastplayed = self._config.getint('Ranking',
        'ThresholdArtistLastPlayed', self._thres_artist_lastplayed)
    self._thres_artist_laststarted= self._config.getint('Ranking',
        'ThresholdArtistLastStarted', self._thres_artist_laststarted)
    self._thres_artist_lastqueued= self._config.getint('Ranking',
        'ThresholdArtistLastQueued', self._thres_artist_lastqueued)

  def set_artist_threshold(self, played=-1, started=-1, queued=-1):
    if played != -1:
      self._thres_artist_lastplayed = played
      self._config.set('Ranking', 'ThresholdArtistLastPlayed', played)
    if started != -1:
      self._thres_artist_laststarted= started
      self._config.set('Ranking', 'ThresholdArtistLastStarted', started)
    if queue != -1:
      self._thres_artist_lastqueued= queued
      self._config.set('Ranking', 'ThresholdArtistLastQueued', queued)

  def set_artist_threshold(self, played=-1, started=-1, queued=-1):
    if played != -1:
      self._thres_artist_lastplayed = played
      self._config.set('Ranking', 'ThresholdArtistLastPlayed', played)
    if started != -1:
      self._thres_artist_laststarted= started
      self._config.set('Ranking', 'ThresholdArtistLastStarted', started)
    if queue != -1:
      self._thres_artist_lastqueued= queued
      self._config.set('Ranking', 'ThresholdArtistLastQueued', queued)

  def deactivate(self):
    self.queue_function(self._deactivate, None)

  def _deactivate(self, dummy=None):
    """ set all artists, albums, tracks and files to be not active """
    self.acquire()
    FileFactory.deactivate()
    self.release()

  def activate(self):
    self.queue_function(self._activate, None)

  def _activate(self, dummy=None):
    self.acquire()
    TrackRelationFactory.check_active()
    ArtistRelationFactory.check_active()
    self.release()

  def stop(self):
    if self._running:
      Worker.stop(self)
      self._save()
      if self._lastfm:
        self._lastfm.stop()
      if self._echonest:
        self._echonest.stop()
      self._running = False

  def acquire(self): self._sync.acquire()

  def release(self): self._sync.release()

  def load(self, locked=False):
    if not path.exists(self._library_filename):
      self._logger.info(u"not loading: %s not found" % self._library_filename)
    else:
      if not locked: self.acquire()
      self._logger.info(u"loading from %s" % self._library_filename)
      Factories.clear()
      try:
        timestamp = datetime.utcnow()
        input = open(self._library_filename, 'rb')
        Factories.load(input)
        self._client_data = pickle.load(input)
        input.close()
        self._logger.info(u"loading took %s" % ( (datetime.utcnow() - timestamp)))
        self.dump_stats()
      except IOError:
        self._logger.error('IOError')
        self._logger.error(format_exc())
      except EOFError:
        self._logger.error('EOFError')
        self._logger.error(format_exc())
        Factories.clear()
      except AttributeError:
        self._logger.error('AttributeError: Format changed?')
        self._logger.error(format_exc())
        Factories.clear()
      if not locked: self.release()

  def save(self):
    self._logger.info(u'queueing save')
    self.queue_function(self._save)

  def _save(self):
    self.acquire()
    self._logger.info(u"saving %s~" % self._library_filename)
    level = -1 # -1 for latest method, 0 for ascii
    timestamp = datetime.utcnow()
    output = open(u"%s~" % self._library_filename, 'wb')
    Factories.dump(output, level)
    pickle.dump(self._client_data, output, level)
    output.close()
    self._logger.info(u"saved %s~" % self._library_filename)
    if path.exists(self._library_filename):
      try:
        self._logger.info(u"removing %s" % self._library_filename)
        remove(self._library_filename)
      except OSError:
        self._logger.error('OSError')
        self._logger.error(format_exc())
    self._logger.info(u"moving %s~ to %s" % (self._library_filename,
        self._library_filename))
    rename(u"%s~" % self._library_filename, self._library_filename)
    self._logger.info(u"saving took %s" % ( (datetime.utcnow() - timestamp)))
    # save configuration
    self._config.save()
    self.release()

  def add_file(self, path, artist_name, track_title):
    self.queue_function(self._add_file, path, artist_name, track_title)

  def _add_file(self, path, artist_name, track_title):
    self.acquire()
    f = FileFactory.get(path, artist_name, track_title)
    f.activate()
    self.release()

  """ similar inputs """
  def similar_artists(self, artist_nameA, artist_nameB, match, source):
    #self._logger.debug(u"similar_artists %s %s %2.2f %s" % (artist_name0,
    #    artist_name1, match, source))
    self.queue_function(self._similar_artists, artist_nameA,
        artist_nameB, match, source)

  def _similar_artists(self, artist_nameA, artist_nameB, match, source,
      locked=False):
    if not locked: self.acquire()
    #self._logger.debug(u"%s: [%s]-[%s] %2.2f" % (source, artist_nameA,
    #    artist_nameB, match))
    artistA = ArtistFactory.by_key(artist_nameA)
    artistB = ArtistFactory.by_key(artist_nameB)
    #if not artistA: self._logger.debug(u"similar_artists[%s]: not found" %
    #    artist_nameA)
    #if not artistB: self._logger.debug(u"similar_artists[%s]: not found" %
    #    artist_nameB)
    if artistA and artistB:
      relation = ArtistRelationFactory.get(artistA, artistB)
      old_rating = relation.rating
      relation.rate(0.75 + 0.25 * match)
      self._logger.debug(u"%s [%s]-[%s] m(%2.2f) r(%2.2f|%2.2f)" % (source,
          artistA, artistB, match, relation.rating, old_rating))
      if self._queue_lookup_results:
        if self._lastfm:
          self._lastfm.similar_artists_low(self.similar_artists, artist_nameB,
              self._thres_lastfm_lookup)
        if self._echonest:
          self._echonest.similar_artists_low(self.similar_artists, artist_nameB,
              self._thres_lastfm_lookup)
    if not locked: self.release()

  def similar_tracks(self, artist_name0, track_title0,
      artist_name1, track_title1, match, source):
    #self._logger.info(u"similar_tracks %s %s %s %s %2.2f %s" % (artist_name0,
    #    track_title0, artist_name1, track_title1, match, source))
    self.queue_function(self._similar_tracks, artist_name0,
        track_title0, artist_name1, track_title1, match, source)

  def _similar_tracks(self, artist_nameA, track_titleA,
      artist_nameB, track_titleB, match, source, locked=False):
    if not locked: self.acquire()
    #self._logger.debug(u"%s: [%s-%s]-[%s-%s] %2.2f" % (source, artist_nameA,
    #    track_titleA, artist_nameB, track_titleB, match))
    trackA = TrackFactory.by_key(TrackFactory.get_key(artist_nameA,
        track_titleA))
    trackB = TrackFactory.by_key(TrackFactory.get_key(artist_nameB,
        track_titleB))
    #if not trackA: self._logger.debug(u"similar_tracks[%s-%s]: not found" %
    #      (artist_nameA, track_titleA))
    #if not trackB: self._logger.debug(u"similar_tracks[%s-%s]: not found" %
    #      (artist_nameB, track_titleB))
    if trackA and trackB:
      relation = TrackRelationFactory.get(trackA, trackB)
      old_rating = relation.rating
      relation.rate(0.75 + 0.25 * match)
      self._logger.debug(u"%s [%s]-[%s] m(%2.2f) r(%2.2f|%2.2f)" % (source,
          trackA, trackB, match, relation.rating, old_rating))
      if self._queue_lookup_results:
        if self._lastfm:
          self._lastfm.similar_tracks_low(self.similar_tracks, artist_nameB,
              track_titleB, self._thres_lastfm_lookup)
        if self._echonest:
          self._echonest.similar_tracks_low(self.similar_tracks, artist_nameB,
              track_titleB, self._thres_lastfm_lookup)
    if not locked: self.release()

  """ statistics """
  def nr_of_artists(self):
    return len(self._artists)

  def nr_of_albums(self):
    return len(self._albums)

  def nr_of_tracks(self):
    return len(self._tracks)

  def nr_of_files(self):
    return len(self._files)

  def nr_of_relations(self):
    return len(self._relations)

  def nr_of_active_tracks(self):
    nr_of_active_tracks= 0
    for track in self.tracks():
      if track['active']:
        nr_of_active_tracks+= 1
    return nr_of_active_tracks

  def __factor_relation(self, nr, maxnr): return -sqrt(1.0*nr/maxnr) + 1.0

  def __rating(self, struct, inc=True, mix=0.7, factor=1.0):
    """
    kept for docstring...

    mixed rating function.  updates the values of the structs
    'rolling_rating', 'stable_rating' and 'rating' based on 'playcount' and
    'skipcount' (or 'playfactor' and 'skipfactor' if present) and the last
    "action" (play or skip).  the '*rating' is between 0.0 and 1.0 with 0.5
    being unrated / unsure and > 0.5 being a positive and < 0.5 being a
    negativ rating.  the mix factor (0.0-1.0) scales the rating to be more
    stable (>0.5) or more jumpy (<0.5).  the factor determines how much of the
    old rating for rolling ratings should be "kept" with 1.0 being all new and
    0.0 being all old.  usefull for history relations.

    some thoughts:

    the goal is to determine the rating of track, artist, albums and
    relations of the three.

    depending on the subject (track, artist, album), the rating should adapt
    to plays and skips slow or fast.

    tracks are not played that often (compared to artists and albums).  rating
    should adapt fast here.  example: i play my new weekly favorite track
    around 10 times this week.  next week i do not want to hear it anymore.
    how many skips to push it below 0.5?  keep in mind, that it might just be
    played in the wrong mix, so the first skip should not be too penalizing.
    i guess ~3 skips should be enough to but it below or near 0.5. mix 0.7
    should work.

    initially i wanted to calculate the artist and album rating similarily
    (with a more stable mix of ~0.9), but: given that the shuffle algorithm
    only spits out stuff i like and ignores the stuff i hate.  let's take an
    artist which has a lot of songs that i don't like, but just this one track
    i absolutly love.  over time i played this track 40 times and skipped the
    other 11 tracks just once.  taking my mix factor of 0.9 i would end up
    with an artist rating of ~.8.  so the chances are high that i get
    suggested all tracks by this artist and need to skip them at least once.
    so artist rating would be near useless.  average rating of all tracks of
    this artist would be ~0.47.  with ~6 bad songs required to counter the
    high runner (average < 0.5).

    rolling rating which adapts fast to plays and skips:
      7 plays @0.5 (.75 .88 .94 .97 .98 .99 1)
      -> 7 skips @1.0 (.50 .25 .13 .06 .03 .02 .01)
        < 0.5 after 2nd skip, this is too bouncy
      7 skips @0.5 (.25 .13 .06 .03 .02 .01 0)
      -> 7 plays @0.0 (.50 .75 .88 .94 .97 .98 .99)
        > 0.5 after 2nd play, this is too bouncy
    stable rating which accounts for overall plays and skips:
      7 plays @0.5 (.67 .75 .80 .83 .86 .88 .89)
      -> 7 skips @0.89 (.80 .73 .67 .62 .57 .53 .50)
        < 0.5 after 8th skip, this is too slow
      7 skips @0.5 (.33 .25 .20 .17 .14 .13 .11)
      -> 7 plays @0.11 (.20 .27 .33 .38 .43 .47 .50)
        > 0.5 after 8th play, this is too slow
    mixing it together to get the best of both worlds (mix=0.7)
      7 plays @0.5 (0.6917 0.7875 0.8413 0.8740 0.8953 0.9102 0.9211)
      > 7 skips @0.92 (0.7094 0.5838 0.5040 0.4494 0.4093 0.3780 0.3523)
        < 0.5 after 4th skip, good penalty after 2nd skip
    """
    playfactor = struct.get('playfactor', struct['playcount'] * 1.0)
    skipfactor = struct.get('skipfactor', struct['skipcount'] * 1.0)
    if inc:
      new_rolling = (struct['rolling_rating'] +
          (1.0 - struct['rolling_rating']) / 2.0)
    else:
      new_rolling = (struct['rolling_rating'] -
          (struct['rolling_rating'] / 2.0))
    struct['rolling_rating'] = (
        factor * new_rolling + (1.0 - factor) * struct['rolling_rating'])
    new_stable = ((playfactor + 1.0) /
        (playfactor + skipfactor + 2.0))
    struct['stable_rating'] = (
        factor * new_stable + (1.0 - factor) * struct['stable_rating'])
    struct['rating'] = (struct['stable_rating'] * mix +
        struct['rolling_rating'] * (1.0 - mix))

  def get_file(self, path):
    f = FileFactory.by_path(path)
    if not f:
      self._logger.error(u"file not recognized %s" % path)
      return None
    return f

  def file_started(self, file):
    self.queue_function(self._file_started, file)

  def _file_started(self, file):
    self.acquire()
    file.started()
    self.track_started(file.track)
    self.release()

  def file_stopped(self, file):
    self.queue_function(self._file_stopped, file)

  def _file_stopped(self, file):
    self.acquire()
    file.stopped()
    self.track_stopped(file.track)
    self.release()

  def file_skipped(self, file):
    self.queue_function(self._file_skipped, file)

  def _file_skipped(self, file):
    self.acquire()
    file.skipped()
    self.track_played(file.track, skipped=True, locked=True)
    self.release()

  def file_played(self, file):
    self.queue_function(self._file_played, file)

  def _file_played(self, file):
    self.acquire()
    file.played()
    self.track_played(file.track, skipped=False, locked=True)
    self.release()

  def file_queued(self, file):
    self.queue_function(self._file_queued, file)

  def _file_queued(self, file):
    self.acquire()
    file.queued()
    self.track_queued(file.track, locked=True)
    self.release()

  def lookup(self, track, locked=False):
    if not locked: self.acquire()
    ArtistRelationFactory.load_artist(track.artist)
    TrackRelationFactory.load_track(track)
    # queue similar artists / tracks lookup from lastfm
    if self._lastfm:
      self._lastfm.similar_artists(self.similar_artists, track.artist.name,
          self._thres_lastfm_lookup)
      self._lastfm.similar_tracks(self.similar_tracks, track.artist.name,
          track.title, self._thres_lastfm_lookup)

    # queue similar artists / tracks lookup from echonest
    if self._echonest:
      self._echonest.similar_artists(self.similar_artists, track.artist.name,
          self._thres_echonest_lookup)
      self._echonest.similar_tracks(self.similar_tracks, track.artist.name,
          track.title, self._thres_echonest_lookup)
    if not locked: self.release()

  def track_queued(self, track, locked=False):
    if not locked: self.acquire()
    self.lookup(track, locked=True)
    if not locked: self.release()

  def track_started(self, track, locked=False):
    if not locked: self.acquire()
    self._logger.info(u"started: %s" % track)
    timestamp= now()
    self._playing_track = track
    # add this track to the history
    self._history.extendleft([{'track': track,'skipped':False}])
    self.lookup(track, locked=True)
    self.update_ranking(locked=True)
    # CONFIG: max history length?
    if len(self._history) > 100: self._history.pop()
    if not locked: self.release()

  def track_stopped(self, track, locked=False):
    if not locked: self.acquire()
    self._playing_track = None
    if len(self._history) == 0 or self._history[0]['track'] != track:
      self._logger.error("stopped: no record of starting this track")
    else:
      self._history.popleft()
    if not locked: self.release()

  def track_skipped(self, track, locked=False):
    self.track_played(track, skipped=True, locked=locked)

  def file_queue_skip(self, file, locked=False):
    file.queued()
    return self.track_queue_skip(file.track, locked)

  def file_queue_add(self, file, locked=False):
    file.queued()
    return self.track_queue_add(file.track, locked)

  def track_queue_skip(self, track, locked=False):
    self.track_queue_updated(track, skipped=True, locked=locked)

  def track_queue_add(self, track, locked=False):
    self.track_queue_updated(track, skipped=False, locked=locked)

  def track_queue_updated(self, track, skipped, locked=False):
    # FIXME: locked?
    self.queue_function(self._track_queue_updated, track, skipped)

  def _track_queue_updated(self, track, skipped, locked=False):
    if self._playing_track:
      reference_track = self._playing_track
    elif self._lastplayed_track:
      reference_track = self._lastplayed_track
    else:
      return
    if not locked: self.acquire()
    relation = TrackRelationFactory.get(track, reference_track)
    relation.update(not skipped, self._queue_update_factor)
    self._logger.debug(relation)
    self.update_ranking()
    if not locked: self.release()

  def track_played(self, track, skipped= False, locked=False):
    if not locked: self.acquire()
    if len(self._history) == 0 or self._history[0]['track'] != track:
      self._logger.error(u"played: no record of starting this track")
      if not locked: self.release()
      return

    if not skipped: self._lastplayed_track = track

    timestamp= now()
    self._history[0]['skipped']= skipped

    if len(self._history) > 1:
      for i in range(min(self.maxhistory, len(self._history) - 1)):
        if skipped and self._history[1+i]['skipped']: continue

        factor= self.__factor_relation(i, self.maxhistory)
        hist_track= self._history[1+i]['track']
        hist_skipped = self._history[1+i]['skipped']

        track_relation = TrackRelationFactory.get(track, hist_track)
        track_relation.update(not hist_skipped, factor)
        self._logger.info(u"relation updated: %s" % track_relation)

    self._relation_resetted = False
    self.update_ranking()
    if not locked: self.release()

  def next_file(self, locked=False):
    self._logger.info(u"next file: start")
    if not locked: self.acquire()
    if not self._ranking_updated: self.update_ranking()
    best_ranking = 0
    best_tracks = []
    tt = []

    timestamp = now()

    # calculate last_*_timestamps (played / queued / started)
    track_lastplayed_timestamp = timestamp - self._thres_track_lastplayed
    track_laststarted_timestamp = timestamp - self._thres_track_laststarted
    track_lastqueued_timestamp = timestamp - self._thres_track_lastqueued
    artist_lastplayed_timestamp = timestamp - self._thres_artist_lastplayed
    artist_laststarted_timestamp = timestamp - self._thres_artist_laststarted
    artist_lastqueued_timestamp = timestamp - self._thres_artist_lastqueued

    has_active_tracks = False
    for track in TrackFactory.active_tracks():
      has_active_tracks = True
      artist = track.artist
      factor = 1.0
      if (track.lastplayed > track_lastplayed_timestamp):
        factor = min(factor,
            1.0 - ((1.0 * track.lastplayed - track_lastplayed_timestamp) /
            self._thres_track_lastplayed))
      if (artist.lastplayed > artist_lastplayed_timestamp):
        factor = min(factor,
            1.0 - ((1.0 * artist.lastplayed - artist_lastplayed_timestamp) /
            self._thres_artist_lastplayed))
      if (track.laststarted > track_laststarted_timestamp):
        factor = min(factor,
            1.0 - ((1.0 * track.laststarted - track_laststarted_timestamp) /
            self._thres_track_laststarted))
      if (artist.laststarted > artist_laststarted_timestamp):
        factor = min(factor,
            1.0 - ((1.0 * artist.laststarted - artist_laststarted_timestamp) /
            self._thres_artist_laststarted))
      if (track.lastqueued > track_lastqueued_timestamp):
        factor = min(factor,
            1.0 - ((1.0 * track.lastqueued - track_lastqueued_timestamp) /
            self._thres_track_lastqueued))
      if (artist.lastqueued > artist_lastqueued_timestamp):
        factor = min(factor,
            1.0 - ((1.0 * artist.lastqueued - artist_lastqueued_timestamp) /
            self._thres_artist_lastqueued))
      ranking = int(self._ranking_base * factor * track.ranking)

      if ranking > best_ranking:
        self._logger.debug("%2.2f (best=): %s" % (ranking, track))
        best_ranking = ranking
        best_tracks = [track]
      elif ranking == best_ranking:
        self._logger.debug("%2.2f (best+): %s" % (ranking, track))
        best_tracks.append(track)
      top_ten(tt, track, ranking)
    if not has_active_tracks:
      self._logger.error(u"No active tracks")
      if not locked: self.release()
      return None
    top_ten_dump(tt, self._logger.info, u"rank")
    self._logger.info("best tracks: %d" % (len(best_tracks)))
    best_track = choice(best_tracks)
    best_track.started()

    # pick the best file
    best_rating = 0.0
    best_files = []
    for file in best_track.files():
      if not file.active: continue
      t = file.playcount / (1.0 + file.skipcount)
      if t > best_rating:
        best_rating = t
        best_files = [file]
      elif t == best_rating:
        best_files.append(file)
    if not locked: self.release()
    self._logger.info(u"next file: stop")
    return choice(best_files)

  def reset(self, full=False):
    self._logger.info(u'queueing reset')
    self.queue_function(self._reset, full)

  def _reset(self, full=False):
    """ clear recent play history + unset last played track """
    self.acquire()
    self._history.clear()
    self._lastplayed_track = None
    self._playing_track = None
    for track in TrackFactory.active_tracks():
      track.ranking = 0.5
      track.relation = 0.5
      track.relation_old = 0.5
      track.relation_cnt = 0
    if full:
      for track in TrackFactory.active_tracks():
        track.lastplayed = 0
        track.laststarted = 0
        track.lastqueued = 0
      for artist in ArtistFactory.active_artists():
        artist.lastplayed = 0
        artist.laststarted = 0
        artist.lastqueued = 0
    self.release()

  def reset_ban(self, locked=False):
    if not locked: self.acquire()
    for track in TrackFactory.active_tracks():
      track.ban = False
    if not locked: self.release()

  def reset_boost(self, locked=False):
    if not locked: self.acquire()
    for track in TrackFactory.active_tracks():
      track.boost = False
    if not locked: self.release()

  def reset_seeds(self, locked=False):
    if not locked: self.acquire()
    self._seed_tracks = set()
    self._seed_artists = set()
    if not locked: self.release()

  def add_seed(self, track, locked=False):
    if not locked: self.acquire()
    self.lookup(track, True)
    self._seed_tracks.add(track)
    self._seed_artists.add(track.artist)
    if not locked: self.release()

  def seed(self, track, locked=False):
    """Calculate relations based on track as seed.
    """
    if not locked: self.acquire()
    benchmark = Benchmark()
    timestamp = now()
    seed_track = set()
    seed_artist = set()
    if track:
      seed_track.add(track)
      seed_artist.add(track.artist)
      self.lookup(track, True)

    # check artist relations
    cnt = 0
    benchmark.start()
    tt = []
    for seed_a in seed_artist.union(self._seed_artists):
      self._logger.info(u'check artist relations for {}'.format(seed_a))
      for artist_relation in ArtistRelationFactory.by_artist(seed_a):
        cnt += 1
        other_artist = artist_relation.artistA
        if artist_relation.artistA.name == seed_a.name:
          other_artist = artist_relation.artistB

        other_artist.relation_sum += artist_relation.rating
        other_artist.relation_cnt += 1
        other_artist.relation = (other_artist.relation_sum /
            other_artist.relation_cnt)
        top_ten(tt, u'artist related with {}({}/{}={}) to {}'.format(
              scale_rating(artist_relation.rating),
              scale_rating(other_artist.relation_sum),
              scale_rating(other_artist.relation_cnt),
              scale_rating(other_artist.relation),
              other_artist), artist_relation.rating)

        artist_relation.lastused = timestamp
    top_ten_dump(tt, self._logger.info)

    self._logger.info(u"update ranking: check artist took %s" % benchmark)
    self._logger.info(u"updated %d artist(s)" % cnt)

    cnt = 0
    benchmark.start()
    tt = []
    for seed_t in seed_track.union(self._seed_tracks):
      self._logger.info(u'check track relations for {}'.format(seed_t))
      for track_relation in TrackRelationFactory.by_track(seed_t):
        other_track = track_relation.trackA
        if track_relation.trackA.title == seed_t.title and \
            track_relation.trackA.artist.name == seed_t.artist.name:
          other_track = track_relation.trackB
        cnt += 1
        if not track.ban:
          other_track.relation_sum += track_relation.rating
          other_track.relation_cnt += 1
          other_track.relation = (other_track.relation_sum /
              other_track.relation_cnt)
          top_ten(tt, u'track related with {} to {}'.format(
              scale_rating(track_relation.rating), other_track),
              track_relation.rating)

        track_relation.lastused = timestamp
    top_ten_dump(tt, self._logger.info)
    self._logger.info(u"update ranking: check track took %s" % benchmark)
    self._logger.info(u"updated %d track(s)" % cnt)
    if not locked: self.release()

  def update_ranking(self, locked=False):
    if not self._update_queued:
      if not locked: self.acquire()
      self._update_queued = True
      self.queue_function(self._update_ranking, None)
      if not locked: self.release()

  def _update_ranking(self, locked=False):
    benchmark = Benchmark()
    if not locked: self.acquire()

    seed_track = None
    if self._lastplayed_track:
      seed_track = self._lastplayed_track
    elif self._playing_track:
      seed_track = self._playing_track

    if not self._relation_resetted:
      benchmark.start()
      self._relation_resetted = True
      for track in TrackFactory.active_tracks():
        track.relation_old = track.relation
        track.relation_sum = 0.0
        track.relation_cnt = 0
      for artist in ArtistFactory.active_artists():
        artist.relation_old = artist.relation
        artist.relation_sum = 0.0
        artist.relation_cnt = 0
      self._logger.info(u"update ranking: resetting took %s" % benchmark)

    has_active_tracks = False
    for track in TrackFactory.active_tracks():
      has_active_tracks = True
      break

    benchmark.start()
    # new relation = old relation * decay
    # e.g. for decay = 0.5 (0.75)
    # decfacA = 0.5 * 0.5 = 0.25 (0.75 * 0.5 = 0.375)
    # decfacB = 1.0 - 0.5 = 0.5 (1.0 - 0.75 = 0.25)
    # relation_old=0.75 -> 0.25+0.5*0.75=0.625 (0.375+0.25*0.75=0.5625)
    decfacA = self._relation_decay * 0.5
    decfacB = 1.0 - self._relation_decay
    for track in TrackFactory.active_tracks():
      if (track.relation_old > 0.501) or (track.relation_old < 0.499):
        track.relation = (decfacA + decfacB * track.relation_old)
      else:
        track.relation = 0.5

    for artist in ArtistFactory.active_artists():
      if (artist.relation_old > 0.501) or (artist.relation_old < 0.499):
        artist.relation = (decfacA + decfacB * artist.relation_old)
      else:
        artist.relation = 0.5

    self._logger.info(u"update ranking: set old relation + decay took %s" %
        benchmark)

    if has_active_tracks:
      self._ranking_updated = True
      self.seed(seed_track, True)
      benchmark.start()
      at = []
      tt = []
      for track in TrackFactory.active_tracks():
        """
        so we have:
          ranking   [0-1.0] (old)
          rating    [0-1.0]
          relation  [0-1.0]
          random    [0-1.0]
        and:
          factor min(track_lastplayed/started,artist_lastplayed/started) [0-1.0]
          ? moved to next_file()
        """
        artist = track.artist
        r = random()
        # calculate new ranking
        if track.boost:
          self._logger.info(u"pre boost: %s" % track)
          track.relation = (track.relation + 99.0) / 100.0
          self._logger.info(u"post boost: %s" % track)
        elif track.ban:
          track.relation = 0.0

        # mix with artist relation if we don't have a track relation
        if track.relation_cnt == 0:
          if artist.relation_cnt > 0:
            track.relation = (0.75 * track.relation + 0.25 * artist.relation)
            top_ten(at, u'relation cnt = {} with {} now {} to {}'.format(
                artist.relation_cnt, scale_rating(artist.relation),
                scale_rating(track.relation), artist), track.relation)
        else:
          top_ten(tt, u'relation cnt = {} with {} to {}'.format(
              track.relation_cnt, scale_rating(track.relation), track),
              track.relation)

        track.ranking = (
            self._factor_ranking['rating'] * track.get_rating() +
            self._factor_ranking['relation'] * track.relation +
            self._factor_ranking['random'] * r
            )

      self._logger.info(u"update ranking: took %s" % benchmark)
    self._update_queued = False
    if not locked: self.release()

  def dump_stats(self):
    self._logger.info(u"%d artists, %d tracks %d files" % (
        ArtistFactory.len(), TrackFactory.len(), FileFactory.len()))
Example #2
0
File: library.py Project: pscn/ads
  def __init__(self, config):
    Log.__init__(self, self._title)
    self._logger.info(u"init")
    Worker.__init__(self, lifo=False)
    self._config = config
    self._library_filename = self._config.get('Library', 'path',
        join(self._config.configdir(), 'library.pkl'))
    Factories.clear()
    Logger.set_logger(self._logger)

    if self._config.get('TrackRelation', 'use_db', True):
      TrackRelationFactory.use_db()
      TrackRelationFactory.set_path(self._config.get('TrackRelation',
          'path', join(self._config.configdir(), '')))
    else:
      TrackRelationFactory.use_fs()
      TrackRelationFactory.set_path(self._config.get('TrackRelation',
          'path', join(self._config.configdir(), 'track')))
    TrackRelationFactory.set_maxentries(
        self._config.get('TrackRelation', 'maxentries', 500))

    if self._config.get('ArtistRelation', 'use_db', True):
      ArtistRelationFactory.use_db()
      ArtistRelationFactory.set_path(self._config.get('ArtistRelation',
          'path', join(self._config.configdir(), '')))
    else:
      ArtistRelationFactory.use_fs()
      ArtistRelationFactory.set_path(self._config.get('ArtistRelation',
          'path', join(self._config.configdir(), 'artist')))
    ArtistRelationFactory.set_maxentries(
        self._config.get('ArtistRelation', 'maxentries', 500))

    if self._config.get('Lookup', 'QueueResults', 'False') == 'True':
      self._queue_lookup_results = True
    else:
      self._queue_lookup_results = False

    # FIXME:  log if one of those libs is not present
    if self._config.get('Lookup', 'UseLastFM', 'True') == 'True':
      self._lastfm = LastFM(config)
      self._lastfm.start()
      self._thres_lastfm_lookup = self._config.getint('Lookup',
          'ThresholdLastFM', self._thres_lastfm_lookup)

    if self._config.get('Lookup', 'UseEchoNest', 'True') == 'True':
      self._echonest = EchoNest(config)
      self._echonest.start()
      self._thres_echonest_lookup = self._config.getint('Lookup',
          'ThresholdEchoNest', self._thres_echonest_lookup)

    # read, normalize and update ranking factors
    factor = 0.0
    for k in self._factor_ranking.keys():
      self._factor_ranking[k] = self._config.getfloat('Ranking',
          "factor%s" % k, self._factor_ranking[k])
      factor += self._factor_ranking[k]
    for k in self._factor_ranking.keys():
      self._factor_ranking[k] /= factor
      self._config.set('Ranking', "factor%s" % k, self._factor_ranking[k])

    self._relation_decay = self._config.getfloat('Ranking', 'RelationDecay',
        self._relation_decay)

    self._queue_update_factor = self._config.getfloat('Rating',
        'QueueUpdateFactor', self._queue_update_factor)

    self._unplayed_rating = self._config.getfloat('Ranking',
        'UnplayedRating', self._unplayed_rating)

    self._thres_track_lastplayed = self._config.getint('Ranking',
        'ThresholdTrackLastPlayed', self._thres_track_lastplayed)
    self._thres_track_laststarted= self._config.getint('Ranking',
        'ThresholdTrackLastStarted', self._thres_track_laststarted)
    self._thres_track_lastqueued= self._config.getint('Ranking',
        'ThresholdTrackLastQueued', self._thres_track_lastqueued)
    self._thres_artist_lastplayed = self._config.getint('Ranking',
        'ThresholdArtistLastPlayed', self._thres_artist_lastplayed)
    self._thres_artist_laststarted= self._config.getint('Ranking',
        'ThresholdArtistLastStarted', self._thres_artist_laststarted)
    self._thres_artist_lastqueued= self._config.getint('Ranking',
        'ThresholdArtistLastQueued', self._thres_artist_lastqueued)