using MoreLinq; using Nancy; using Sony.Filtr.AdminAPI.Modules.AnalyzePlaylist.ViewModels; using Sony.Filtr.AdminAPI.Utility; using Sony.Filtr.Applications; using Sony.Filtr.Buzz; using Sony.Filtr.Contracts.Abstractions; using Sony.Filtr.Contracts.Definitions; using Sony.Filtr.Core.SonyMusic; using Sony.Filtr.Playlists.Spotify; using Sony.Filtr.SpotifyWebAPI; using Sony.Filtr.SpotifyWebAPI.Model; using Sony.Filtr.Utility.Extensions; using System; using System.Collections.Generic; using System.Linq; using System.Threading.Tasks; using Sony.Filtr.Playlists.Spotify.Model.History; using Sony.Filtr.AdminAPI.Extensions; using Sony.Filtr.Playlists.Spotify.Model; using Sony.Filtr.ApolloAPI; using Sony.Filtr.ApolloAPI.Models; namespace Sony.Filtr.AdminAPI.Modules.AnalyzePlaylist { public class AnalyzePlaylistModule : BaseModule { private readonly SonyMusicManager _sonyMusicManager; private readonly ISpotifyRegionManager _spotifyRegionManager; private readonly SpotifyPlaylistHistoricTrackListManager _historicTrackListManager; private readonly BuzzAccountManager _buzzAccountManager; private readonly SpotifyWebApi _spotifyWebApi; private readonly IApolloWebApi _apolloWebApi; private readonly SpotifyPlaylistManager _spotifyPlaylistManager; private readonly TimeSpan SonyTracksAnalyzeCache = TimeSpan.FromMinutes(15); public AnalyzePlaylistModule(IApolloWebApi apolloWebApi, SpotifyWebApi spotifyWebApi, SonyMusicManager sonyMusicManager, ApplicationInstanceManager applicationInstanceManager, BuzzAccountManager buzzAccountManager, SpotifyPlaylistManager spotifyPlaylistManager, ISpotifyRegionManager spotifyRegionManager, SpotifyPlaylistHistoricTrackListManager historicTrackListManager) : base(applicationInstanceManager) { _sonyMusicManager = sonyMusicManager; _spotifyRegionManager = spotifyRegionManager; _historicTrackListManager = historicTrackListManager; _buzzAccountManager = buzzAccountManager; _spotifyWebApi = spotifyWebApi; _spotifyPlaylistManager = spotifyPlaylistManager; this._apolloWebApi = apolloWebApi; Get["AnalyzePlaylist", "/sonyTracks/analyze", true] = async (parameters, ct) => { var playlistId = Request.GetRequiredPlaylistId(); var isrc = (string)Request.Query.isrc; string region = await GetSpotifyRegion(); SpotifyPlaylist playlist = await this._apolloWebApi.GetPlaylistByIdWithAllTracksAsync(playlistId, SonyTracksAnalyzeCache, playlistFields: null); if (playlist == null) { ErrorLogging.ErrorLoggingManager.Instance.LogError($"Could not load the playlist from Spotify API. PlaylistId: {playlistId}"); return new Response() { StatusCode = HttpStatusCode.BadRequest, ReasonPhrase = "Could not load the playlist from Spotify API" }; } SonyTracksResponse response = await BuildSonyTrackResponse(playlist, region, isrc); return Response.AsJson(response); }; } private async Task GetSpotifyRegion() { var region = (string)Request.Query.market ?? (string)Request.Query.region; //TODO: We should only need to use region return await GetRealSpotifyRegionAsync(region); } private async Task> CreateTrackViewModel(SpotifyPlaylist playlist, string region, string isrc) { SpotifyTrackAnalyzeData[] tracks = null; if (this._spotifyPlaylistManager.GetPersonalizedPlaylist(playlist.id) != null) { tracks = await _spotifyPlaylistManager.GetPlaylistTracksAnalyzeDataById(playlist.id); } else { tracks = playlist.Tracks.items .Where(t => t.track != null) .Select((t, index) => t.ToDbDataFormat(index)) .ToArray(); } if (!tracks.Any()) { return Enumerable.Empty(); } var trackIds = tracks.Select(t => t.TrackId).Distinct().AsEnumerable(); var trackAlbumIds = tracks .Where(t => !string.IsNullOrWhiteSpace(t.Album.Id)) .Select(a => a.Album.Id) .Distinct() .ToList(); var historicPositions = new Dictionary>(); var albumDictionary = new Dictionary(); var audioDictionary = new Dictionary(); var earliestAddedDates = new Dictionary(); var tasks = new List { Task.Run(async () => { var albums = await this._apolloWebApi.GetAllAlbumInfoAsync(trackAlbumIds, region, "albums(id,release_date,release_date_precision,images)"); albumDictionary = albums .ToDictionary(k => k.id, v => v); }), Task.Run(async () => { var audiofeatures = await this._apolloWebApi.GetAllAudioFeatureForTracksAsync(trackIds); audioDictionary = audiofeatures? .Where(f => f != null) .ToDictionary(f => f.id, f => f); }), Task.Run(async () => { earliestAddedDates = await _historicTrackListManager.GetPlaylistTrackDates(playlist.id); }), Task.Run(async () => { historicPositions = await GetTrackPositionChangesByIdAsync(playlist.id, isrc); }) }; await tasks.EnsureAllSuccessAsync(TaskScheduler.FromCurrentSynchronizationContext()); SonyTrackViewModel[] trackViewModels = BuildTrackViewModel(tracks, albumDictionary, audioDictionary, earliestAddedDates, historicPositions); return await SetIsSonyAsync(trackViewModels, region); } private async Task SetIsSonyAsync(SonyTrackViewModel[] trackViewModels, string region) { var albumUpcs = trackViewModels.Select(p => p.AlbumUpc).FilterNull().Distinct().ToArray(); var isrcs = trackViewModels.Select(p => p.ISRC).FilterNull().Distinct().ToArray(); var sonyUpcs = await _sonyMusicManager.GetSonyAlbumAsync(albumUpcs, MusicService.Spotify, region); var overrideIsSonyIsrcs = await _sonyMusicManager.GetIsSonyOverrideByIsrcAsync(isrcs, region); foreach (var trackViewModel in trackViewModels) { trackViewModel.IsSony = DetermineIsSony(trackViewModel, sonyUpcs, overrideIsSonyIsrcs); trackViewModel.SonyRelease = sonyUpcs.GetValueOrNull(trackViewModel.AlbumUpc); } return trackViewModels; } private async Task>> GetTrackPositionChangesByIdAsync(string playlistId, string isrc) { return await _historicTrackListManager.GetTrackPositionChangesByPlaylistByIdAsync(playlistId, DateTime.Today.AddDays(-14), isrc); } private async Task GetRealSpotifyRegionAsync(string region) { if (region == null) { return null; } var regions = await _spotifyRegionManager.GetAvailableRegionsAsync(); if (regions.Contains(region, StringComparer.InvariantCultureIgnoreCase)) { return region; } return null; } private bool DetermineIsSony(SonyTrackViewModel track, Dictionary sonyUpcs, Dictionary overrideIsSonyIsrcs) { if (!string.IsNullOrWhiteSpace(track.ISRC) && overrideIsSonyIsrcs.ContainsKey(track.ISRC)) { return overrideIsSonyIsrcs[track.ISRC]; } return track.AlbumUpc != null && sonyUpcs.GetValueOrNull(track.AlbumUpc) == SpotifyAnalyticsAccount.Sony; } private async Task BuildSonyTrackResponse(SpotifyPlaylist playlist, string region, string isrc) { IEnumerable tracks = await CreateTrackViewModel(playlist, region, isrc); var userData = await GetUserData(playlist.owner?.id); return new SonyTracksResponse() { Name = playlist.name, Description = playlist.description, ImageUrl = playlist.GetImageUrl(), Followers = playlist.followers.total ?? 0, PlaylistUri = playlist.uri, Tracks = tracks, Percentage = tracks.GetSonyTrackPercentage(), BuzzCategoryId = userData.BuzzCategoryId, TracksLastAdded = tracks.GetTrackLastAdded(), UserDisplayName = userData.DisplayName, UserId = userData.UserId }; } private async Task<(string UserId, string DisplayName, Nullable BuzzCategoryId)> GetUserData(string userName) { var buzzUser = await _buzzAccountManager.GetBuzzUserAsync(userName, ServiceType.Spotify); string userDisplayName = buzzUser?.DisplayName; string userId = buzzUser?.Username; if (buzzUser?.DisplayName == null) { var userResponse = await _spotifyWebApi.GetUserAsync(userName); userDisplayName = userResponse?.display_name; userId = userResponse?.id; } return (userId, userDisplayName, buzzUser?.BuzzCategoryId); } private SonyTrackViewModel[] BuildTrackViewModel( IEnumerable currentPlaylistTracks, Dictionary albumDictionary, Dictionary audioFeatureDictionary, Dictionary earliestAddedDates, Dictionary> historicPositions) { return currentPlaylistTracks .GroupBy(t => t.Isrc) .SelectMany(tracksIsrcGroup => { var groupViewModels = tracksIsrcGroup .Select(t => BuildTrackViewModel(t, albumDictionary, audioFeatureDictionary, earliestAddedDates)) .ToList(); historicPositions.GetValueOrDefault(tracksIsrcGroup.Key).CalculateTrendAndIsEntry(groupViewModels); groupViewModels.ForEach(vm => vm.Position++); return groupViewModels; }) .OrderBy(vm => vm.Position) .ToArray(); } private SonyTrackViewModel BuildTrackViewModel( SpotifyTrackAnalyzeData track, Dictionary albumDictionary, Dictionary audioFeatureDictionary, Dictionary earliestAddedDates) { var viewModel = new SonyTrackViewModel() { ArtistName = string.Join(", ", track.Artists.Select(a => a.Name)), Artists = track.Artists.Select(a => new AdminAPI.ViewModels.SpotifyArtistReferenceViewModel() { Name = a.Name, Uri = a.GetArtistUri() }).ToList(), Name = track.TrackName, ISRC = track.Isrc, TrackUri = String.IsNullOrWhiteSpace(track.TrackId) ? "" : Sony.Filtr.Contracts.Entities.SpotifyLink.FromTrackId(track.TrackId).Uri, AddedDate = track.AddedAt, Popularity = track.Popularity, Position = track.PlaylistIndex }; var albumId = track.Album?.Id; if (albumId != null) { var album = albumDictionary.GetValueOrDefault(albumId); if (album != null) { viewModel.AlbumReleaseDate = album.release_date; viewModel.AlbumUpc = album.external_ids?.upc; var albumImageUrl = album.images?.OrderBy(i => i.height).FirstOrDefault()?.url; if (albumImageUrl != null) { albumImageUrl = ForceHttps(albumImageUrl); } viewModel.AlbumImageUrl = albumImageUrl; album.images.OrderBy(i => i.height); } } if (audioFeatureDictionary != null && track.TrackId != null && audioFeatureDictionary.ContainsKey(track.TrackId)) { var audio = audioFeatureDictionary[track.TrackId]; viewModel.Danceability = (float?)audio.danceability; viewModel.Energy = (float?)audio.energy; viewModel.AudioKey = audio.key; viewModel.Loudness = (float?)audio.loudness; viewModel.AudioMode = audio.mode; viewModel.Speechiness = (float?)audio.speechiness; viewModel.Acousticness = (float?)audio.acousticness; viewModel.Instrumentalness = (float?)audio.instrumentalness; viewModel.Liveness = (float?)audio.liveness; viewModel.Valence = (float?)audio.valence; viewModel.Tempo = (float?)audio.tempo; } var isrc = track.Isrc; if (isrc != null) { if (earliestAddedDates != null) { viewModel.EarliestAdded = earliestAddedDates.ContainsKey(isrc) ? earliestAddedDates[isrc].EarliestAddeDate : track.AddedAt; } } return viewModel; } private string ForceHttps(string url) { //We force https. var uriBuilder = new UriBuilder(url) { Scheme = "https", Port = -1 }; return uriBuilder.ToString(); } } }