using Sony.Filtr.Contracts.Definitions; using Sony.Filtr.Database; using Sony.Filtr.Playlists.Models; using Sony.Filtr.Playlists.Spotify; using Sony.Filtr.Tasks.Helpers; using Sony.Filtr.Utility.Extensions; using System; using System.Collections.Generic; using System.Linq; using System.Threading.Tasks; using Sony.Filtr.ApolloAPI; using SpotifyPlaylist = Sony.Filtr.Contracts.Entities.SpotifyPlaylist; using System.Threading.Tasks.Dataflow; using System.Text; using Sony.Filtr.Contracts.Entities.Buzz; using Sony.Filtr.Buzz; using Sony.Filtr.Utility; using Sony.Filtr.Core.SpotifyUserCountries; using System.IO; using System.Threading; using Sony.Filtr.Functional; namespace Sony.Filtr.Tasks.Tasks { public class UpdatePlaylistTracklistTask : UpdatePlaylistInfoBase { private readonly IApolloWebApi _apolloWebApi; private readonly AsyncLogger _logger; private readonly AsyncLogger _loggerTracklistChanges; private readonly AsyncLogger _loggerUpdateDatesDuration; private readonly AsyncLogger _loggerFullPlaylistUpdateDuration; private readonly AsyncLogger _loggerRelatedDataDuration; private readonly AsyncLogger _loggerLoadExistingDuration; private readonly AsyncLogger _playlistInvalidTracks; private readonly ActionTracker blocksCountTracker; private readonly Func, string, Task> SetSpotifyTrackListAndUpdateStatisticsAsync; public UpdatePlaylistTracklistTask( IApolloWebApi apolloWebApi, UpdatePlaylistsHelper updatePlaylistsHelper, SpotifyPlaylistManager spotifyPlaylistManager, SpotifyPlaylistHistoricTrackListManager historicTrackListManager, BuzzAccountManager buzzAccountManager, SpotifyUserCountryManager spotifyUserCountryManager) : base(spotifyPlaylistManager, historicTrackListManager, updatePlaylistsHelper, buzzAccountManager, spotifyUserCountryManager) { this._apolloWebApi = apolloWebApi; _logger = AsyncLogger.GetLogger("UpdatePlaylistTracklistTask"); _loggerTracklistChanges = AsyncLogger.GetLogger("UpdatePlaylistTracklistTask_Tracklistchanges"); _loggerUpdateDatesDuration = AsyncLogger.GetLogger("UpdateDatesDuration"); _loggerFullPlaylistUpdateDuration = AsyncLogger.GetLogger("UpdateFullyDuration"); _loggerRelatedDataDuration = AsyncLogger.GetLogger("RelatedDataUpdateDuration"); _loggerLoadExistingDuration = AsyncLogger.GetLogger("LoadExistingDuration"); _playlistInvalidTracks = AsyncLogger.GetLogger("PlaylistInvalidTracks"); this.blocksCountTracker = new ActionTracker(Path.Combine("logs", $"blocksCount_{DateTime.Now.ToString("yyyy_MM_dd")}.txt"), 10); this.SetSpotifyTrackListAndUpdateStatisticsAsync = ((Func, string, Task>)_spotifyPlaylistManager.SetSpotifyTrackListAndUpdateStatistics) .Tuple() .ToUnit() .OnFailureWithRethrow((tuple, result) => { _loggerTracklistChanges.ErrorAsync(result.Exception, () => $"Could not update playlist current tracklist or statistics. PlaylistId: {tuple.Item1} Market: {tuple.Item3} Error: {result.Exception.Message} Duration:{result.Duration.TotalSeconds}secs"); }) .OnSuccessUnsafe((tuple, result) => _loggerTracklistChanges.WarnAsync(() => $"Updated playlist current tracklist and statistics for '{tuple.Item1}'")) .UnwrapTuple(); } private static int GetVendorLoadThreadsCount() { return Maybe.GetAppSettingsIntOrDefault("UpdatePlaylistTracklistTask_VendorLoad_Threads", 2); } private static int LoadExistingDataThreadsCount() { return Maybe.GetAppSettingsIntOrDefault("UpdatePlaylistTracklistTask_LoadExisting_Threads", 3); } private static int UpdateRelatedDataThreadsCount() { return Maybe.GetAppSettingsIntOrDefault("UpdatePlaylistTracklistTask_UpdateRelatedData_Threads", 2); } protected override void LogErrorOnly(Exception ex, Func getMessage) { this._logger.ErrorAsync(ex, getMessage); } protected override async Task ExecuteJobAsync(Guid scheduledTaskLogId) { var spotifyPlaylists = await GetPlaylistForTracklistUpdateAsync(); int totalToProcess = spotifyPlaylists.Length; var progressTracker = new ProgressTracker(totalToProcess); progressTracker.NextAsync(0).FireAndForget(); string progressChartFilePath = $"logs/ProgressChart_{DateTime.Now.ToString("yyyy_MM_dd_HH_mm")}"; _logger.InfoAsync(() => $"Total to process: {totalToProcess}").FireAndForget(); Func> vendorLoadPlaylistWithTracks = async playlistInput => await _apolloWebApi.GetPlaylistByIdWithAllTracksHavingSnapshotIdAsync( playlistInput.PlaylistId, playlistInput.SnapshotId, TimeSpan.FromSeconds(0), Constants.Spotify.VendorPlaylistWithTracksFullFieldsList); TransformBlock> vendorLoadPlaylistWithTracksBlock = vendorLoadPlaylistWithTracks .Timeout(TimeSpan.FromSeconds(8)) .Retry(1) .Map( inputTransform: async input => input, outputTransform: async (input, spotifyPlaylist, input1) => { if (spotifyPlaylist == null) { return PlaylistToProcess.CreateRemoved(input); } BuzzUser buzzUser = base.GetBuzzUserByNameOrDefault(spotifyPlaylist.owner?.id); await VerifyAndHandlePlaylistInvalidTrack(spotifyPlaylist, invalidTracksMessage => _playlistInvalidTracks.InfoAsync(() => invalidTracksMessage)); return PlaylistToProcess.Create( input, ShouldSaveTracklistHistory(buzzUser?.BuzzCategoryId, spotifyPlaylist.id), !String.Equals(spotifyPlaylist.snapshot_id, input.SnapshotId), spotifyPlaylist .Tracks .items .Where(ApolloAPI.Models.SpotifyPlaylist.IsValidTrack) .ItemIndex() .Select(t => _updatePlaylistsHelper.BuildSpotifyPlaylistTrack(t.Item, t.Index)) .ToList(), spotifyPlaylist.GetSummary(), buzzUser); } ) .TryCatch() .OnFailure((input, playlistResult) => this.LogError(playlistResult.Exception, () => $"Could not request Spotify data for '{input.PlaylistId}'")) .OnSuccess((input, playlistResult) => { if (playlistResult.Value.IsRemoved) { _logger.WarnAsync(() => $"Playlist is removed: {input.PlaylistId}"); } else { _logger.InfoAsync(() => $"{playlistResult.Value.LoadSummary}:::Snapshot changed:{playlistResult.Value.HasSnapshotIdChanged}"); } }) .AsTransformBlock(new ExecutionDataflowBlockOptions { MaxDegreeOfParallelism = GetVendorLoadThreadsCount(), EnsureOrdered = false, SingleProducerConstrained = true, BoundedCapacity = 500 }); Func> loadExisting = async input => { var existingTracklistTask = _spotifyPlaylistManager.GetTrackListAsync(input.Input.PlaylistId); var existingPlaylistTask = _spotifyPlaylistManager.GetPlaylistByIdAsync(input.Input.PlaylistId); await Task.WhenAll(existingPlaylistTask, existingTracklistTask); return new PlaylistToSave(input, await existingPlaylistTask, await existingTracklistTask); }; TransformBlock, Result> loadExistingBlock = loadExisting .Timeout(TimeSpan.FromSeconds(10)) .Retry(1) .Duration((toProcess, toSave, ts) => _loggerLoadExistingDuration.TraceAsync(() => $"Existing data for playlist '{toProcess.Input.PlaylistId}' loaded in {ts.TotalSeconds} secs")) .TryCatch() .OnFailure((toProcess, toSaveResult) => this.LogError(toSaveResult.Exception, () => $"Could not read existing data from DB. PlaylistId: '{toProcess.Input.PlaylistId}'")) .AcceptResult() .AsTransformBlock(new ExecutionDataflowBlockOptions { MaxDegreeOfParallelism = LoadExistingDataThreadsCount(), SingleProducerConstrained = true, EnsureOrdered = false }); Func updateRelatedData = this.UpdateRelatedData; TransformBlock, Result> updateRelatedDataBlock = updateRelatedData .ToUnit() .Timeout(TimeSpan.FromMinutes(2)) .Retry(1) .Duration((toSave, unit, ts) => _loggerRelatedDataDuration.TraceAsync(() => $"Related data for playlist '{toSave.PlaylistToProcess.Input.PlaylistId}' updated in {ts.TotalSeconds} secs")) .Map( inputTransform: toSave => toSave, outputTransform: (toSave, unit, toSave1) => toSave ) .TryCatch() .OnFailure((toSave, toSaveResult) => this.LogError(toSaveResult.Exception, () => $"Could not update playlist tracklist. PlaylistId: '{toSave.PlaylistToProcess.Input.PlaylistId}'")) .OnSuccess((toSave, toSaveResult) => _logger.InfoAsync(() => $"Updated related data '{toSave.PlaylistToProcess.Input.ToString()}'")) .AcceptResult() .AsTransformBlock(new ExecutionDataflowBlockOptions { MaxDegreeOfParallelism = UpdateRelatedDataThreadsCount(), EnsureOrdered = false }); Func, Task> updatePlaylistTracksInfo = this._spotifyPlaylistManager.UpdatePlaylistTracksInfo; ActionBlock>> updatePlaylistTrackInfosBlock = updatePlaylistTracksInfo .ToUnit() .Timeout(TimeSpan.FromSeconds(30)) .Retry(1) .Duration((playlists, unit, ts) => _loggerFullPlaylistUpdateDuration.TraceAsync(() => $"Fully updated {playlists.Count()} playlists in {ts.TotalSeconds} secs")) .Map>, Unit, IEnumerable, Unit>( inputTransform: toSave => toSave.Select(p => new SpotifyPlaylist() { PlaylistUri = $"spotify:playlist:{p.Value.ExistingPlaylist.PlaylistId}", PlaylistId = p.Value.ExistingPlaylist.PlaylistId, TrackCount = p.Value .PlaylistToProcess.SpotifyTracks.Count(), Duration = p.Value.PlaylistToProcess.SpotifyTracks.Sum(t => (long)t.Track.Duration), TrackLatestAdded = _updatePlaylistsHelper.CalculateTrackLatestAdded(p.Value.PlaylistToProcess.SpotifyTracks), UpdateDate = DateTime.UtcNow, }) .ToList(), outputTransform: (playlists, unit, toSave) => Unit.Default ) .TryCatch() .OnFailure((playlists, unitResult) => this.LogError(unitResult.Exception, () => $"Could not update playlist tracks info.")) .OnSuccess((playlists, unitResult) => _logger.InfoAsync(new StringBuilder().AppendLineForEach(playlists, pl => $"+++ Playlist track info updated {pl.Value.PlaylistToProcess.Input.ToString()}").ToString)) .OnSuccess((playlists, unitResult) => progressTracker.NextAsync(playlists.Count()).FireAndForget()) .AsActionBlock(new ExecutionDataflowBlockOptions() { MaxDegreeOfParallelism = 1 }); Func, Task> updatePlaylistsUpdateDatesAsync = this._spotifyPlaylistManager.UpdatePlaylistsDateAsync; ActionBlock>> updatePlaylistsDatesBlock = updatePlaylistsUpdateDatesAsync .ToUnit() .Timeout(TimeSpan.FromSeconds(30)) .Retry(1) .Duration((ids, unit, ts) => _loggerUpdateDatesDuration.TraceAsync(() => $"Updated dates for {ids.Count()} playlists in {ts.TotalSeconds} secs")) .Map>, Unit, IEnumerable, Unit>( inputTransform: playlists => playlists.Select(p => p.Value.Input.PlaylistId).ToList(), outputTransform: (ids, unit, playlists) => Unit.Default ) .TryCatch() .OnFailure((playlists, unitResult) => this.LogError(unitResult.Exception, () => $"Could NOT update playlists dates (tblSpotifyPlaylist UpdateDate/tblSpotifyPlaylistTrackList2 Timestamp)")) .OnSuccess((playlists, unitResult) => _logger.InfoAsync(new StringBuilder().AppendLineForEach(playlists, pl => $"+++ Playlist update date updated {pl.Value.Input.ToString()}").ToString)) .OnSuccess((playlists, unitResult) => progressTracker.NextAsync(playlists.Count()).FireAndForget()) .AsActionBlock(new ExecutionDataflowBlockOptions() { EnsureOrdered = false, MaxDegreeOfParallelism = 1 }); var linkOptions = new DataflowLinkOptions { PropagateCompletion = true }; BatchBlock> updateDatesBatchBlock = new BatchBlock>(100); BatchBlock> updateTrackInfoBatchBlock = new BatchBlock>(100); vendorLoadPlaylistWithTracksBlock.LinkTo(updateDatesBatchBlock, linkOptions, dto => dto.IsOk && dto.Value.IsRemoved == false && !dto.Value.HasSnapshotIdChanged); vendorLoadPlaylistWithTracksBlock.LinkTo(loadExistingBlock, linkOptions, dto => dto.IsOk && dto.Value.IsRemoved == false && dto.Value.HasSnapshotIdChanged); vendorLoadPlaylistWithTracksBlock.LinkTo(DataflowBlock.NullTarget>()); updateDatesBatchBlock.LinkTo(updatePlaylistsDatesBlock, linkOptions); loadExistingBlock.LinkTo(updateRelatedDataBlock, linkOptions, dto => dto.IsOk); loadExistingBlock.LinkTo(DataflowBlock.NullTarget>()); updateRelatedDataBlock.LinkTo(updateTrackInfoBatchBlock, linkOptions); updateTrackInfoBatchBlock.LinkTo(updatePlaylistTrackInfosBlock, linkOptions); var cts = new CancellationTokenSource(); var blockCountLogTask = TaskHelper.CreateLongRunningLoggingThread( cts.Token, () => $@" vendorLoadPlaylistWithTracksBlock: {vendorLoadPlaylistWithTracksBlock.InputCount}/{vendorLoadPlaylistWithTracksBlock.OutputCount} updatePlaylistsDatesBlock: {updatePlaylistsDatesBlock.InputCount} updateRelatedDataBlock: {updateRelatedDataBlock.InputCount}/{updateRelatedDataBlock.OutputCount} loadExistingBlock: {loadExistingBlock.InputCount}/{loadExistingBlock.OutputCount} ", this.blocksCountTracker, TimeSpan.FromSeconds(60)); var progressTrackerTask = TaskHelper.CreateLongRunningThread( cts.Token, () => GenerateProgressChart(progressTracker, progressChartFilePath), TimeSpan.FromMinutes(5) ); foreach (var playlistToProcess in spotifyPlaylists) { await vendorLoadPlaylistWithTracksBlock.SendAsync(playlistToProcess); } vendorLoadPlaylistWithTracksBlock.Complete(); await Task.WhenAll(updatePlaylistTrackInfosBlock.Completion, updatePlaylistsDatesBlock.Completion); cts.Cancel(); await Task.WhenAll(blockCountLogTask, progressTrackerTask); await this.blocksCountTracker.CompleteAsync(); GenerateProgressChart(progressTracker, progressChartFilePath); return null; } private async Task UpdateRelatedData(PlaylistToSave dto) { await _updatePlaylistsHelper.SetEarliestAddedDateAsync(dto.PlaylistToProcess.SpotifyTracks, dto.ExistingTracks, dto.ExistingPlaylist.PlaylistId, dto.ExistingPlaylist.User); if (dto.PlaylistToProcess.SaveTracklistHistory) { await SaveTracklistHistoryAsync(dto.ExistingPlaylist.PlaylistId, dto.PlaylistToProcess.SpotifyTracks); } if (_updatePlaylistsHelper.IsTrackListChanged(dto.ExistingTracks, dto.PlaylistToProcess.SpotifyTracks)) { await SetSpotifyTrackListAndUpdateStatisticsAsync( dto.ExistingPlaylist.PlaylistId, dto.PlaylistToProcess.SpotifyTracks, base.GetCountry( dto.PlaylistToProcess.Input.CountryCode, dto.PlaylistToProcess.BuzzUser) ?? Constants.Spotify.DefaultPlaylistCountryCodeForStatistics); await AddTracksWithRelatedDataAsync(dto.PlaylistToProcess.SpotifyTracks); } else { await _spotifyPlaylistManager.UpdateCurrentTracklistForTodayAsync(dto.ExistingPlaylist.PlaylistId); _loggerTracklistChanges.InfoAsync(() => $"{dto.ExistingPlaylist.PlaylistId} tracklist did not change").FireAndForget(); } } private async Task SaveTracklistHistoryAsync(string playlistId, IEnumerable playlistTracks) { var today = DateTime.UtcNow.Date; if((await this._historicTrackListManager.DoesPlaylistHaveHistoryForDate(playlistId, today)) == false) { await _historicTrackListManager.AddSpotifyTrackListHistoryAsync(playlistId, playlistTracks, today); } } private async Task GetPlaylistForTracklistUpdateAsync() { var list = new List(); //list.Add(new PlaylistInput("1PQNGPYni1V1nFdLLyF6D5", "test", 1, "ua")); //return list.ToArray(); using (var conn = await DatabaseHandler.GetOpenReadOnlyConnectionAsync()) { using (var cmd = conn.CreateCommand()) { var sql = $@" SELECT p.PlaylistId, p.SnapshotId, p.CountryCode FROM tblSpotifyPlaylist AS p LEFT JOIN tblPlaylistIgnoredPlaylist AS ip ON ip.playlistId=p.playlistUri AND ip.musicServiceId={(int)MusicService.Spotify} WHERE p.Removed=0 AND ip.playlistId IS NULL AND p.SaveTracklist = 1 AND p.buzzCategoryId IN ({(int)StaticBuzzCategory.SonyMusic}, {(int)StaticBuzzCategory.Spotify}) ORDER BY p.TrackLatestAdded DESC;"; cmd.CommandText = sql; using (var sqlReader = await cmd.ExecuteReaderAsync()) { int i = 0; while (await sqlReader.ReadAsync()) { list.Add(new PlaylistInput(sqlReader.GetString("PlaylistId"), sqlReader.GetString("SnapshotId"), i++, sqlReader.GetString("CountryCode"))); } } } } return list.ToArray(); } } internal struct PlaylistInput { public readonly string PlaylistId; public readonly int Index; public readonly string SnapshotId; public readonly string CountryCode; public PlaylistInput(string playlistId, string snapshotId, int index, string countryCode) { this.PlaylistId = playlistId; this.Index = index; this.SnapshotId = snapshotId; this.CountryCode = countryCode; } public override string ToString() { return $"PlaylistId '{this.PlaylistId}' Index: {this.Index}"; } } internal struct PlaylistToProcess { public readonly PlaylistInput Input; public readonly IEnumerable SpotifyTracks; public readonly bool HasSnapshotIdChanged; public readonly bool SaveTracklistHistory; public readonly string LoadSummary; public readonly bool IsRemoved; public readonly BuzzUser BuzzUser; private PlaylistToProcess( PlaylistInput input, bool shouldSaveTracklistHistory, bool hasSnapshotChanged, IEnumerable spotifyTracks, string loadSummary, bool isRemoved, BuzzUser buzzUser) { this.Input = input; this.SaveTracklistHistory = shouldSaveTracklistHistory; this.SpotifyTracks = spotifyTracks; this.HasSnapshotIdChanged = hasSnapshotChanged; this.LoadSummary = loadSummary; this.IsRemoved = isRemoved; this.BuzzUser = buzzUser; } public static PlaylistToProcess Create( PlaylistInput input, bool shouldSaveTracklistHistory, bool hasSnapshotChanged, IEnumerable spotifyTracks, string loadSummary, BuzzUser buzzUser) { return new PlaylistToProcess(input, shouldSaveTracklistHistory, hasSnapshotChanged, spotifyTracks, loadSummary, false, buzzUser); } public static PlaylistToProcess CreateRemoved( PlaylistInput input) { return new PlaylistToProcess(input, false, false, null, String.Empty, true, null); } } internal struct PlaylistToSave { public readonly IEnumerable ExistingTracks; public readonly SpotifyPlaylist ExistingPlaylist; public readonly PlaylistToProcess PlaylistToProcess; public PlaylistToSave(PlaylistToProcess toProcess, SpotifyPlaylist existingPlaylist, IEnumerable existingTracks) { this.PlaylistToProcess = toProcess; this.ExistingPlaylist = existingPlaylist; this.ExistingTracks = existingTracks; } } }