using MySql.Data.MySqlClient; using NLog; using Sony.Filtr.ApolloAPI; using Sony.Filtr.ApolloAPI.Models; using Sony.Filtr.Core.ResponseCollector; using Sony.Filtr.Core.SpotifyCharts; using Sony.Filtr.Core.SpotifyCharts.Api; using Sony.Filtr.Core.SpotifyCharts.Data; using Sony.Filtr.Database; using Sony.Filtr.ErrorLogging; using Sony.Filtr.Tasks.EventSource; using Sony.Filtr.Utility; using Sony.Filtr.Utility.Extensions; using System; using System.Collections.Concurrent; using System.Collections.Generic; using System.Diagnostics; using System.Globalization; using System.IO; using System.Linq; using System.Text; using System.Threading; using System.Threading.Tasks; using static Sony.Filtr.Functional.FunctionalProgramming; namespace Sony.Filtr.Tasks.Tasks.Spotify { public class FetchSpotifyCharts : IScheduledTask { private const string FileDirectory = @"spotifycharts\"; private readonly int _versionNumber = 1; private readonly SpotifyChartsManager _spotifyChartsManager; private readonly ErrorLoggingManager _errorLoggingManager; private readonly SpotifyChartsApi _spotifyChartsApi; private readonly IApolloWebApi _apolloWebApi; private readonly ResponseCollector _responseCollector; private readonly Logger _logger; private readonly Logger _loggerDuration; private readonly SpotifyChartsS3Api _s3ChartsApi; public FetchSpotifyCharts( SpotifyChartsManager spotifyChartsManager, ErrorLoggingManager errorLoggingManager, SpotifyChartsApi spotifyChartsApi, IApolloWebApi apolloWebApi, ResponseCollector responseCollector, SpotifyChartsS3Api s3ChartsApi) { _logger = LogManager.GetLogger("FetchSpotifyCharts"); _spotifyChartsManager = spotifyChartsManager; _errorLoggingManager = errorLoggingManager; _spotifyChartsApi = spotifyChartsApi; _logger = NLog.LogManager.GetLogger("FetchSpotifyCharts"); _responseCollector = responseCollector; _apolloWebApi = apolloWebApi; _s3ChartsApi = s3ChartsApi; _loggerDuration = LogManager.GetLogger("FetchSpotifyCharts_Duration"); } private static int GetDatesProcessConcurrentThreads() { return Maybe.GetAppSettingsIntOrDefault("FetchSpotifyCharts_Dates_Process_Threads", 1); } private async Task GetChartDataToProcessAsync(IEnumerable markets) { Func, ChartData[]> getMissingCharts = (startDate, existingChartsForMarket) => { var result = new List(); if (existingChartsForMarket == null || !existingChartsForMarket.Any()) { return result.ToArray(); } string market = existingChartsForMarket.First().Market; Func getChartKey = (date, listType, timeWindow) => $"{market}_{date.ToString("yyyy-MM-dd")}_{(int)listType}_{(int)timeWindow}"; var processedChartsMap = existingChartsForMarket.ToDictionary(c => getChartKey(c.ChartDate, c.ChartType, c.TimeWindow), c => c); Action addIfDoesNotExist = (date, listType, timeWindow) => { var ttt = existingChartsForMarket.FirstOrDefault(c => c.ChartDate.Year == date.Year && c.ChartDate.Month == date.Month && c.ChartDate.Day == date.Day && c.ChartType == listType && c.TimeWindow == timeWindow); if (!processedChartsMap.ContainsKey(getChartKey(date, listType, timeWindow))) { result.Add(new ChartData(market, date, listType, timeWindow)); } }; for (var currentDate = startDate; currentDate < DateTime.Now; currentDate = currentDate.AddDays(1)) { addIfDoesNotExist(currentDate, SpotifyChartsListType.Regional, SpotifyChartsTimeWindow.Daily); addIfDoesNotExist(currentDate, SpotifyChartsListType.Regional, SpotifyChartsTimeWindow.Weekly); addIfDoesNotExist(currentDate, SpotifyChartsListType.Viral, SpotifyChartsTimeWindow.Daily); addIfDoesNotExist(currentDate, SpotifyChartsListType.Viral, SpotifyChartsTimeWindow.Weekly); } return result.ToArray(); }; DateTime startDateToFindMissingChart = DateTime.Now.AddDays(-GetDaysToTrackChartsStartingFromToday()); var results = new ConcurrentBag(); await markets.ForEachAsync(2, async market => { var marketProcessedCharts = await _spotifyChartsManager.GetProcessedChartsForMarketAsync(market); foreach (var missingChart in getMissingCharts(startDateToFindMissingChart, marketProcessedCharts)) { results.Add(missingChart); } }); return results.ToArray(); } private static int GetDaysToTrackChartsStartingFromToday() { return Maybe.GetAppSettingsIntOrDefault("FetchSpotifyCharts_Get_Days_To_Track_Charts_Starting_From_Today", 14); } public async Task ExecuteAsync(Guid scheduledTaskLogId) { var taskLog = new ScheduledTaskLog(); var dataLog = new ScheduledTaskDataLog("SpotifyChart", DateTimeOffset.UtcNow, scheduledTaskLogId); _logger.Info("Fetching"); PrintChartsHeading(); if (!Directory.Exists(FileDirectory)) { Directory.CreateDirectory(FileDirectory); } var markets = await _spotifyChartsApi.GetMarketsAsync(); _logger.Log(LogLevel.Info, $"Markets amount: {markets.Count()}."); var importedRows = 0; ApolloFetchSpotifyChartsEventSource.Log.ProcessStart(markets.Count); Stopwatch swTotal = new Stopwatch(); swTotal.Start(); Func, Task>> getFromVendorMemoized = ((Func, Task>>)GetTracksFromVendor) .MemoizeCollectionThreadSafeAsync(t => t.id); var loadIsrcsForIds = ((Func, Task>>, ChartDataSaveInformation, Task>>)LoadIsrcForTracksFromSpotifyWebApi) .Partial(getFromVendorMemoized); var missingCharts = await GetChartDataToProcessAsync(markets); _logger.Info($"Found {missingCharts.Length} charts to process."); await missingCharts.ForEachAsync(GetDatesProcessConcurrentThreads(), async chart => { Stopwatch sw = new Stopwatch(); sw.Start(); ApolloFetchSpotifyChartsEventSource.Log.SaveChartDateStart(chart.ListType, chart.Market, chart.TimeWindow, chart.Date.Value.ToString("yyyy-MM-dd")); try { _logger.Debug($"Getting {chart.ToString()}"); var chartInfo = await _s3ChartsApi.GetChartDataFromS3Async(chart); if (chartInfo != null) { _logger.Info($"ChartItems count: {chartInfo.ChartItems.Count()}. {chart.ToString()}"); if ((await SpotifyDataContainsUpdates(chartInfo)) == false) { _logger.Warn($"Data received from Spotify is not fresh. Ignoring. {chart.ToString()}"); return; } Dictionary idToIsrc = await loadIsrcsForIds(chartInfo); await SaveChartDataAsync(chartInfo, idToIsrc); await _spotifyChartsManager.SetAvailableChartAsync(chart.Market, chartInfo.Date, chart.ListType, chart.TimeWindow, _versionNumber); _logger.Info($"Chart saved. {chart.ToString()}"); ApolloFetchSpotifyChartsEventSource.Log.SaveChartDateStop(chart.ListType, chart.Market, chart.TimeWindow, chartInfo.Date.ToString("yyyy-MM-dd"), sw, this._loggerDuration); } else { _logger.Info($"No chart available. {chart.ToString()}"); } var count = chartInfo?.ChartItems?.Count; if (count != null && count.HasValue) { Interlocked.Add(ref importedRows, count.Value); } } catch (Exception ex) { _errorLoggingManager.LogError(ex); _logger.Error(ex, $"Error in: {chart.ListType} for {chart.Market} {chart.Date.Value.ToShortDateString()}"); ApolloFetchSpotifyChartsEventSource.Log.SaveChartDateException(chart.ListType, chart.Market, chart.TimeWindow, chart.Date.Value.ToString("yyyy-MM-dd"), sw, this._loggerDuration, ex); } }); dataLog.Finished = DateTimeOffset.UtcNow; dataLog.Rows = importedRows; if (markets.Count() > 0) { _responseCollector.UploadFile("ResponseData/ChartsResponse.json", "FetchSpotifyCharts"); } taskLog.DataLogs.Add(dataLog); ApolloFetchSpotifyChartsEventSource.Log.ProcessStop(swTotal, this._loggerDuration); return taskLog; } private async Task SpotifyDataContainsUpdates(ChartDataSaveInformation spotifyChartData) { var dbChartData = await this._spotifyChartsManager.GetChartAsync( spotifyChartData.Market, spotifyChartData.ChartsListType, spotifyChartData.ChartsTimeWindow, spotifyChartData.Date.AddDays(-1)); foreach (var spotifyData in spotifyChartData.ChartItems) { var dbData = dbChartData.ChartPositions.SingleOrDefault(c => c.TrackLink.ExtractTrackID() == spotifyData.TrackId); if (dbData == null) { return true; } if (dbData.Position != spotifyData.Position) { return true; } } return false; } private async Task> GetTracksFromVendor(IEnumerable ids) { var response = await this._apolloWebApi.GetSpotifyTracks(ids, null); this._logger.Info($"{response.GetSummary()} - {ids.Count()} ids requested"); return response.tracks; } private async Task> LoadIsrcForTracksFromSpotifyWebApi(Func, Task>> getFromVendor, ChartDataSaveInformation chartData) { var trackIds = chartData.ChartItems.Select(i => i.TrackId).FilterNull().Distinct().ToList(); if (!trackIds.Any()) { return new Dictionary(); } return (await getFromVendor(trackIds)).ToDictionary(t => t.id, t => t.external_ids?.isrc); } private async Task SaveChartDataAsync(ChartDataSaveInformation chartData, Dictionary idToIsrc) { var now = DateTime.Now; var tempFilename = $"SpotifyCharts-{chartData.Market}-{chartData.Date.ToString("d", CultureInfo.GetCultureInfo("sv-se"))}-{chartData.ChartsTimeWindow}-{chartData.ChartsListType}-Processed-{now.ToString("yyyy-MM-dd hh-mm-ss")}.csv"; var filePath = Path.Combine(FileDirectory, tempFilename); await SaveTracksToCsvAsync(filePath, chartData, idToIsrc); await BulkAddChartDataAsync(filePath); File.Delete(filePath); } private async Task SaveTracksToCsvAsync(string filePath, ChartDataSaveInformation saveInformation, Dictionary idToIsrc) { using (var saveFile = File.Open(filePath, FileMode.OpenOrCreate)) { using (var fileWriter = new StreamWriter(saveFile, Encoding.UTF8)) { foreach (var chartTrackItem in saveInformation.ChartItems.ItemIndex()) { var chartItem = chartTrackItem.Item; string isrc = null; if (chartItem.TrackId != null) isrc = idToIsrc.GetValueOrDefault(chartItem.TrackId) ?? string.Empty; string trackName = null; if (!string.IsNullOrWhiteSpace(chartItem.TrackName)) trackName = "\"" + chartItem.TrackName + "\""; string artistName = null; if (!string.IsNullOrWhiteSpace(chartItem.ArtistName)) artistName = "\"" + chartItem.ArtistName + "\""; var csvLine = string.Join("|", string.Empty, saveInformation.Market, saveInformation.Date.ToString("d", CultureInfo.GetCultureInfo("sv-se")), chartTrackItem.Index + 1, // Chart position string.Empty, chartItem.TrackUrl, //.track.uri, trackName, artistName, string.Empty, //chartItem.track.album.uri, string.Empty, chartItem.NumberOfStreams, string.Empty, // Window type string.Empty, // Percent male string.Empty, //chartTrack.percent_age_group_0_17, string.Empty, //chartTrack.percent_age_group_18_24, string.Empty, //chartTrack.percent_age_group_25_29, string.Empty, //chartTrack.percent_age_group_30_34, string.Empty, //chartTrack.percent_age_group_35_44, string.Empty, //chartTrack.percent_age_group_45_54, string.Empty, //chartTrack.percent_age_group_55_plus, isrc, Convert.ToInt32(saveInformation.ChartsListType), Convert.ToInt32(saveInformation.ChartsTimeWindow) ); await fileWriter.WriteLineAsync(csvLine); } } } } private async Task BulkAddChartDataAsync(string filePath) { using (var conn = await DatabaseHandler.GetOpenConnectionAsync()) { var bulkLoader = new MySqlBulkLoader(conn) { Local = true, FileName = filePath, TableName = "tblSpotifyCharts", FieldTerminator = "|", LineTerminator = Environment.NewLine, ConflictOption = MySqlBulkLoaderConflictOption.Replace, CharacterSet = "utf8mb4", FieldQuotationCharacter = '"', //EscapeCharacter = '"', }; await bulkLoader.LoadAsync(); } } private void PrintChartsHeading() { Console.WriteLine("╔═════╗ ╔═╗ ╔═╗ ╔═════╗ ╔═════╗ ╔═════╗ ╔═════╗"); Console.WriteLine("║ ╔═══╝ ║ ║ ║ ║ ║ ╔═╗ ║ ║ ╔═╗ ║ ╚═╗ ╔═╝ ║ ╔═══╝"); Console.WriteLine("║ ║ ║ ║ ║ ║ ║ ║ ║ ║ ║ ╚═╝ ║ ║ ║ ║ ║ "); Console.WriteLine("║ ║ ║ ╚══╝ ║ ║ ╚═╝ ║ ║ ╔═╝ ║ ║ ║ ╚═══╗"); Console.WriteLine("║ ║ ║ ╔══╗ ║ ║ ╔═╗ ║ ║ ╚═╗ ║ ║ ╚═══╗ ║"); Console.WriteLine("║ ║ ║ ║ ║ ║ ║ ║ ║ ║ ║ ╔═╗ ║ ║ ║ ║ ║"); Console.WriteLine("║ ╚═══╗ ║ ║ ║ ║ ║ ║ ║ ║ ║ ║ ║ ║ ║ ║ ╔═══╝ ║"); Console.WriteLine("╚═════╝ ╚═╝ ╚═╝ ╚═╝ ╚═╝ ╚═╝ ╚═╝ ╚═╝ ╚═════╝"); } } }