/// <summary> /// Builds a dictionary containing Song ID keys and weight values. /// The weight values are determined by song popularity, song like, artist like, album like, and tag like. /// </summary> /// <param name="songsWithRanking">Songs grouped by community rank standing.</param> /// <returns></returns> /// <remarks> /// Based off of the song weights algorithm described here: /// http://stackoverflow.com/questions/3345788/algorithm-for-picking-thumbed-up-items/3345838#3345838 /// /// The song weights algorithm is loosely based on the more general Multiplicative Weight Update Algorithm (MWUA), described here: /// https://jeremykun.com/2017/02/27/the-reasonable-effectiveness-of-the-multiplicative-weights-update-algorithm/ /// </remarks> internal Dictionary <string, SongWeight> BuildSongWeightsTable(IList <Songs_RankStandings.Result> songsWithRanking) { // Generate a table containing all the songs. The table will be a dictionary of SongID keys and weight values. // Each song takes up N weight, where N starts out as 1, but can grow or shrink depending on whether that song/artist/album/tags is liked or disliked. var totalSongCount = songsWithRanking.Sum(s => s.SongIds.Count); var songWeights = new Dictionary <string, SongWeight>(totalSongCount); foreach (var ranking in songsWithRanking) { var rankingMultipler = GetWeightMultiplier(ranking.Standing); var songIdsAndDates = ranking.SongIds.Zip(ranking.SongUploadDates); foreach (var(songId, date) in songIdsAndDates) { // Give it a weight based on its community rank. // Multiply that weight by the song's age (newer songs are played more often.) var ageMultiplier = GetAgeMultiplier(date); var rankAndAgeWeight = SongWeight.Default() .WithCommunityRankMultiplier(rankingMultipler) .WithAgeMultiplier(ageMultiplier); songWeights[songId] = rankAndAgeWeight; } } // Now we've generated the table with all songs, each weighted according to their community ranking. // Next, adjust the weight based on whether we like this song or not. foreach (var likedSong in Songs) { if (songWeights.TryGetValue(likedSong.SongId, out var existingWeight)) { var songMultiplier = GetSongLikeDislikeMultiplier(likedSong); songWeights[likedSong.SongId] = existingWeight.WithSongMultiplier(songMultiplier); } } // Next, adjust the weight based on whether we like this artist or not. var artistPrefs = Artists.GroupBy(a => a.Name); foreach (var artist in artistPrefs) { var artistMultiplier = GetArtistMultiplier(artist); if (artistMultiplier != 1.0) { foreach (var pref in artist) { if (songWeights.TryGetValue(pref.SongId, out var existingWeight)) { songWeights[pref.SongId] = existingWeight.WithArtistMultiplier(artistMultiplier); } } } } // Next, adjust the weight based on whether we like this album or not. var albumPrefs = Albums.GroupBy(a => a.Name); foreach (var album in albumPrefs) { var albumMultiplier = GetAlbumMultiplier(album); if (albumMultiplier != 1.0) { foreach (var pref in album) { if (songWeights.TryGetValue(pref.SongId, out var existingWeight)) { songWeights[pref.SongId] = existingWeight.WithAlbumMultiplier(albumMultiplier); } } } } // Adjust the weight based on whether we like the tags of the song or not. var tagLikeDislikeDifferences = CreateTagLikeDislikeDifferences(Tags); var songTags = Tags.GroupBy(t => t.SongId); var songsWithTagMultipliers = songTags.Select(tag => (SongId: tag.Key, TagsMultiplier: GetCumulativeTagMultiplier(tag, tagLikeDislikeDifferences))); foreach (var(SongId, TagsMultiplier) in songsWithTagMultipliers) { if (songWeights.TryGetValue(SongId, out var existingWeight)) { songWeights[SongId] = existingWeight.WithTagMultiplier(TagsMultiplier); } } return(songWeights); }