/// <summary> /// Returns a PLS file format for streaming playlists, used for stations like TuneIn Radio. /// </summary> /// <returns></returns> public async Task <IActionResult> TuneInV3() { // Build the PLS file. // PLS format is very simple: https://en.wikipedia.org/wiki/PLS_(file_format) var plsBuilder = new StringBuilder(); plsBuilder.AppendLine("[playlist]"); // The header var songsWithRanking = await DbSession.Query <Song, Songs_RankStandings>() .As <Songs_RankStandings.Result>() .ToListAsync(); var userPrefs = new UserSongPreferences(); const int totalSongCount = 25; var songPicks = Enumerable .Range(0, totalSongCount) .Select(s => userPrefs.PickSong(songsWithRanking)) .ToList(); var songs = await DbSession.LoadWithoutNulls <Song>(songPicks.Select(p => p.SongId)); for (var i = 0; i < songs.Count; i++) { var oneBasedIndex = i + 1; // File1=http://thefile.mp3 plsBuilder.AppendLine(); plsBuilder.Append("File"); plsBuilder.Append(oneBasedIndex); plsBuilder.Append('='); plsBuilder.AppendLine(songs[i].Uri.ToString()); // Title1=Shema Yisrael by Barry & Batya Segal plsBuilder.Append("Title"); plsBuilder.Append(oneBasedIndex); plsBuilder.Append('='); var songName = string.IsNullOrEmpty(songs[i].HebrewName) ? songs[i].Name : songs[i].Name + " " + songs[i].HebrewName; plsBuilder.Append(songName); plsBuilder.Append(" by "); plsBuilder.AppendLine(songs[i].Artist); } // NumberOfEntries=20 plsBuilder.AppendLine(); plsBuilder.Append("NumberOfEntries"); plsBuilder.Append('='); plsBuilder.Append(songs.Count); var plsBytes = Encoding.UTF8.GetBytes(plsBuilder.ToString()); return(File(plsBytes, "application/pls+xml", "ChavahTuneInStream.pls")); }
public async Task <Song> ChooseSong() { // HOT PATH: This method greatly impacts the UI. The user waits for this method before ever hearing a song. // We want to send back the next song ASAP. var userPreferences = default(UserSongPreferences); var songsWithRanking = default(IList <Songs_RankStandings.Result>); // Aggressive caching for the UserSongPreferences and SongsWithRanking. These don't change often. using (DbSession.Advanced.DocumentStore.AggressivelyCacheFor(TimeSpan.FromDays(1))) { var user = await GetUser(); // This is NOT an unbounded result set: // This queries the Songs_RankStandings index, which will reduce the results. Max number of results will be the number of CommunityRankStanding enum constants. songsWithRanking = await DbSession.Query <Song, Songs_RankStandings>() .As <Songs_RankStandings.Result>() .ToListAsync(); if (user != null) { userPreferences = await DbSession.Query <Like, Likes_SongPreferences>() .As <UserSongPreferences>() .FirstOrDefaultAsync(u => u.UserId == user.Id); } if (userPreferences == null) { userPreferences = new UserSongPreferences(); } } // Run the song picking algorithm. var songPick = userPreferences.PickSong(songsWithRanking); if (string.IsNullOrEmpty(songPick.SongId)) { logger.LogWarning("Chose song but ended up with an empty Song ID.", songPick); return(await PickRandomSong()); } var song = await DbSession.LoadRequiredAsync <Song>(songPick.SongId); var songLikeDislike = userPreferences.Songs.FirstOrDefault(s => s.SongId == song.Id); var songLikeStatus = songLikeDislike?.LikeCount > 0 ? LikeStatus.Like : songLikeDislike?.DislikeCount > 0 ? LikeStatus.Dislike : LikeStatus.None; return(song.ToDto(songLikeStatus, songPick)); }
public async Task <List <string> > GetPrefsDebug(string email) { var stopWatch = new System.Diagnostics.Stopwatch(); stopWatch.Start(); var userId = "AppUsers/" + email; var userPreferences = await DbSession.Query <Like, Likes_SongPreferences>() .As <UserSongPreferences>() .FirstOrDefaultAsync(u => u.UserId == userId); var userPrefsTime = stopWatch.Elapsed; stopWatch.Restart(); if (userPreferences == null) { userPreferences = new UserSongPreferences(); } var songsWithRanking = await DbSession.Query <Song, Songs_RankStandings>() .As <Songs_RankStandings.Result>() .ToListAsync(); var rankingTime = stopWatch.Elapsed; stopWatch.Restart(); var table = userPreferences.BuildSongWeightsTable(songsWithRanking); var tableTime = stopWatch.Elapsed; stopWatch.Stop(); var songsOrderedByWeight = table .Select(s => (SongId: s.Key, s.Value.Weight, s.Value.ArtistMultiplier, s.Value.AlbumMultiplier, SongMultipler: s.Value.SongMultiplier, s.Value.TagMultiplier, RankMultiplier: s.Value.CommunityRankMultiplier, s.Value.AgeMultiplier)) .OrderByDescending(s => s.Weight) .Select(s => $"Song ID {s.SongId}, Weight {s.Weight}, Artist multiplier: {s.ArtistMultiplier}, Album multipler: {s.AlbumMultiplier}, Song multiplier: {s.SongMultipler}, Tag multiplier {s.TagMultiplier}, Rank multiplier: {s.RankMultiplier}, Age multiplier: {s.AgeMultiplier}") .ToList(); songsOrderedByWeight.Insert(0, $"Performance statistics: Total query time {tableTime + rankingTime + userPrefsTime}. Querying user prefs {userPrefsTime}, querying ranking {rankingTime}, building table {tableTime}"); return(songsOrderedByWeight); }
public async Task <ActionResult> GetNextSong() { var userPreferences = new UserSongPreferences(); var songsWithRanking = default(IList <Songs_RankStandings.Result>); // Aggressive caching for the UserSongPreferences and SongsWithRanking. These don't change often. using (var cache = DbSession.Advanced.DocumentStore.AggressivelyCacheFor(TimeSpan.FromDays(1))) { // This is NOT an unbounded result set: // This queries the Songs_RankStandings index, which will reduce the results. Max number of results will be the number of CommunityRankStanding enum constants. songsWithRanking = await DbSession.Query <Song, Songs_RankStandings>() .As <Songs_RankStandings.Result>() .ToListAsync(); } var songPick = userPreferences.PickSong(songsWithRanking); var song = await DbSession.LoadRequiredAsync <Song>(songPick.SongId); return(Redirect(song.Uri.ToString())); }
public async Task <List <Song> > ChooseSongBatch() { const int songsInBatch = 5; var userPreferences = default(UserSongPreferences); var songsWithRanking = default(IList <Songs_RankStandings.Result>); // Aggressive caching for the UserSongPreferences and SongsWithRanking. These don't change often. using (var cache = DbSession.Advanced.DocumentStore.AggressivelyCacheFor(TimeSpan.FromDays(1))) { var user = await GetUser(); // This is NOT an unbounded result set: // This queries the Songs_RankStandings index, which will reduce the results. Max number of results will be the number of CommunityRankStanding enum constants. songsWithRanking = await DbSession.Query <Song, Songs_RankStandings>() .As <Songs_RankStandings.Result>() .ToListAsync(); if (user != null) { userPreferences = await DbSession.Query <Like, Likes_SongPreferences>() .As <UserSongPreferences>() .FirstOrDefaultAsync(u => u.UserId == user.Id); } if (userPreferences == null) { userPreferences = new UserSongPreferences(); } } // Run the song picking algorithm. var batch = new List <Song>(songsInBatch); var pickedSongs = Enumerable.Range(0, songsInBatch) .Select(_ => userPreferences.PickSong(songsWithRanking)) .ToList(); if (pickedSongs.Any(s => string.IsNullOrEmpty(s.SongId))) { logger.LogWarning("Picked songs for batch, but returned one or more empty song IDs {pickedSongs}", pickedSongs); } // Make a single trip to the database to load all the picked songs. var pickedSongIds = pickedSongs .Select(s => s.SongId) .ToList(); var songs = await DbSession .LoadWithoutNulls <Song>(pickedSongIds); var songDtos = new List <Song>(songs.Count); for (var i = 0; i < songs.Count; i++) { var song = songs[i]; if (song != null) { var pickReasons = pickedSongs[i]; var songLikeDislike = userPreferences.Songs.FirstOrDefault(s => s.SongId == song.Id); var songLikeStatus = songLikeDislike?.LikeCount > 0 ? LikeStatus.Like : songLikeDislike?.DislikeCount > 0 ? LikeStatus.Dislike : LikeStatus.None; var dto = song.ToDto(songLikeStatus, pickReasons); songDtos.Add(dto); } } return(songDtos); }