using MoreLinq; using MySql.Data.MySqlClient; using NLog; using Sony.Filtr.Applications; using Sony.Filtr.Buzz; using Sony.Filtr.Contracts.Definitions; using Sony.Filtr.Contracts.Entities; using Sony.Filtr.Contracts.Entities.Buzz; using Sony.Filtr.Core.Factory; using Sony.Filtr.Core.SpotifyBrowse; using Sony.Filtr.Core.SpotifyRegion; using Sony.Filtr.Database; using Sony.Filtr.ErrorLogging; using Sony.Filtr.Playlists.Spotify; using Sony.Filtr.SpotifyWebAPI; using Sony.Filtr.VendorToSpotify; using Sony.Filtr.Utility; using Sony.Filtr.Utility.Extensions; using System; using System.Collections.Concurrent; using System.Collections.Generic; using System.IO; using System.Linq; using System.Net; using System.Threading.Tasks; using Sony.Filtr.Playlists.Spotify.Model; using Sony.Filtr.ApolloAPI; using Sony.Filtr.ApolloAPI.Models; namespace Sony.Filtr.Tasks.Tasks.Spotify { public class ImportSpotifyBrowse : IScheduledTask { private readonly SpotifyWebApi _spotifyWebApi; private readonly IApolloWebApi _vendorApi; private readonly ApplicationInstanceManager _applicationInstanceManager; private readonly SpotifyBrowseManager _spotifyBrowseManager; private readonly ErrorLoggingManager _errorLoggingManager; private readonly SpotifyRegionManager _spotifyRegionManager; private readonly BuzzAccountManager _buzzAccountManager; private readonly StorageFactory _storageFactory; private readonly SpotifyPlaylistManager _spotifyPlaylistManager; private const string _FileDirectory = @"spotifybrowse\"; private readonly Logger _logger; private const string _FieldTerminator = "|"; private readonly List _hiddenCategories = new List() { "word", "inspirational", }; public ImportSpotifyBrowse(IApolloWebApi vendorApi, ApplicationInstanceManager applicationInstanceManager, SpotifyBrowseManager spotifyBrowseManager, ErrorLoggingManager errorLoggingManager, SpotifyRegionManager spotifyRegionManager, BuzzAccountManager buzzAccountManager, StorageFactory storageFactory, SpotifyPlaylistManager spotifyPlaylistManager) { _vendorApi = vendorApi; _applicationInstanceManager = applicationInstanceManager; _spotifyBrowseManager = spotifyBrowseManager; _errorLoggingManager = errorLoggingManager; _spotifyRegionManager = spotifyRegionManager; _buzzAccountManager = buzzAccountManager; _storageFactory = storageFactory; _spotifyPlaylistManager = spotifyPlaylistManager; _logger = LogManager.GetLogger("ImportSpotifyBrowse"); } public async Task ExecuteAsync(Guid scheduledTaskLogId) { var regions = await GetRegionsAsync(); var existingBrowseData = _spotifyBrowseManager.GetSpotifyBrowseData(DateTime.Today); var existingDataLookup = new ConcurrentDictionary>(existingBrowseData.GroupBy(g => g.Market).ToDictionary(k => k.Key, v => v.Select(vi => vi.Category).ToHashSet(StringComparer.InvariantCultureIgnoreCase), StringComparer.InvariantCultureIgnoreCase)); var existingBuzzUsers = await _buzzAccountManager.GetBuzzUsersAsync(musicServiceId: (int)MusicService.Spotify); var buzzUserLookup = existingBuzzUsers.Select(b => b.Username).ToHashSet(); if (!Directory.Exists(_FileDirectory)) { Directory.CreateDirectory(_FileDirectory); } ServicePointManager.DefaultConnectionLimit = 30; try { var playlists = new ConcurrentBag(); await regions.ForEachAsync(2, async region => { try { HashSet existingDataForRegion = existingDataLookup.GetValueOrDefault(region) ?? new HashSet(); var playlistsForRegion = await ImportRegionAsync(region, existingDataForRegion); playlistsForRegion.ForEach(p=> playlists.Add(p)); if (playlistsForRegion.Any()) { await UpdatePeakPositionsForRegionAsync(region); } } catch (Exception ex) { _logger.Error("Got error for region {0}: {1}", region, ex); _errorLoggingManager.LogError(ex); } }); try { await AddNewBuzzUsers(playlists, buzzUserLookup); } catch(Exception ex) { _logger.Error(ex, "Could not add new buzz users."); } if (playlists.Any()) { await SaveBrowseExportFileAsync(DateTime.Today); } _logger.Info("All done!"); } catch (Exception ex) { _logger.Error(ex, "Exception :( "); _errorLoggingManager.LogError(ex); } return null; } private async Task AddNewBuzzUsers(IEnumerable playlists, HashSet buzzUserLookup) { var playlistUsers = playlists.Select(p => p.owner.id).Distinct(); var usersToAdd = playlistUsers.Except(buzzUserLookup, StringComparer.InvariantCultureIgnoreCase); foreach(var user in usersToAdd) { try { var spotifyUser = await _vendorApi.GetUserAsync(user); await _buzzAccountManager.AddBuzzUserAsync(new BuzzUser() { MusicServiceId = (int)MusicService.Spotify, Username = user, DisplayName = spotifyUser.display_name, ImportPlaylists = false, Tags = new List() { "from-browse" } }); } catch(Exception ex) { _logger.Error(ex, $"Could not add new buzz user {user}"); } } } private async Task> GetRegionsAsync() { var applicationRegions = _applicationInstanceManager.GetApplications().Select(a=> a.SpotifyRegionCode).Where(a => a.Length == 2).Distinct(); var spotifyRegions = await _spotifyRegionManager.GetAvailableRegionsAsync(); return applicationRegions.Union(spotifyRegions).Distinct(StringComparer.InvariantCultureIgnoreCase).Select(r=> r.ToLowerInvariant()).ToList(); } private async Task> ImportRegionAsync(string region, HashSet existingDataForRegion) { var playlistsPerCategory = new ConcurrentDictionary>(); if (!existingDataForRegion.Contains("featured")) { _logger.Info("Getting featured playlists for {0}...", region); var featuredPlaylists = await _vendorApi.GetBrowseFeaturedPlaylistsAsync(region); if (featuredPlaylists?.playlists != null) { _logger.Info("Loaded {0}!", featuredPlaylists.playlists.total); playlistsPerCategory.TryAdd("featured", featuredPlaylists.playlists.items); } } var browseCategories = await GetAllCategoriesAsync(region); _logger.Info("Loaded {0} categories.", browseCategories.Count); await browseCategories.ForEachAsync(2, async category => { try { if (!existingDataForRegion.Contains(category.Key)) { var categoryPlaylists = await _vendorApi.GetAllBrowseCategoryPlaylistsAsync(category.Key, region); if (categoryPlaylists != null && categoryPlaylists.Any()) { _logger.Info("Loaded {0} playlists for category {1}.", categoryPlaylists.Count(), category.Value); playlistsPerCategory.TryAdd(category.Key, categoryPlaylists.ToList()); } else { _logger.Warn("Could not load playlists for category {0} in {1}", category.Key, region); } } } catch (Exception ex) { _logger.Error("Playlist load failed for market {0} with category {1}. Exception: {2}", region, category, ex); _errorLoggingManager.LogError(ex); } }); var filePath = await SaveCategoriesToCsvAsync(browseCategories, region); await BulkAddCategoriesAsync(filePath); File.Delete(filePath); var playlistsToAdd = playlistsPerCategory.SelectMany(p => p.Value).Select(p => new SpotifyPlaylistTrackingReference() { PlaylistId = p.id, User = p.owner.id, Name = p.name, SaveTracklist = true}).ToList(); _logger.Debug($"Adding {playlistsToAdd.Count} playlists"); await _spotifyPlaylistManager.AddSpotifyPlaylistsForTrackingAsync(playlistsToAdd); await _spotifyBrowseManager.SaveBrowseDataAsync(playlistsPerCategory.ToDictionary(k => k.Key, v => v.Value.ToSpotify()), DateTime.Today, region); _logger.Info("Done with {0}.", region); return playlistsPerCategory.SelectMany(a => a.Value).ToList(); } private async Task> GetAllCategoriesAsync(string region) { var browseCategories = await _vendorApi.GetAllBrowseCategoriesAsync(region); var categoryDictionary = browseCategories.ToDictionary(k => k.id, v => v.name); if (categoryDictionary.Any()) { foreach (var hiddenCategory in _hiddenCategories) { if (!categoryDictionary.ContainsKey(hiddenCategory)) categoryDictionary.Add(hiddenCategory, hiddenCategory); } } return categoryDictionary; } private async Task SaveCategoriesToCsvAsync(Dictionary categories, string region) { var filePath = _FileDirectory + "browseCategories" + region + ".csv"; using (var saveFile = File.Open(filePath, FileMode.OpenOrCreate)) { using (var fileWriter = new StreamWriter(saveFile)) { foreach (var category in categories) { var csvLine = string.Join(_FieldTerminator, region, category.Key, string.IsNullOrWhiteSpace(category.Value) ? category.Key : category.Value); await fileWriter.WriteLineAsync(csvLine); } } } return filePath; } private async Task BulkAddCategoriesAsync(string filePath) { using (var conn = await DatabaseHandler.GetOpenConnectionAsync()) { var bulkLoader = new MySqlBulkLoader(conn); bulkLoader.Local = true; bulkLoader.FileName = filePath; bulkLoader.TableName = "tblSpotifyBrowseCategoryName"; bulkLoader.FieldTerminator = _FieldTerminator; bulkLoader.LineTerminator = Environment.NewLine; bulkLoader.CharacterSet = "utf8mb4"; bulkLoader.ConflictOption = MySqlBulkLoaderConflictOption.Replace; await bulkLoader.LoadAsync(); } } public async Task SaveBrowseExportFileAsync(DateTime browseDate) { _logger.Info("Saving export file for {0}.", browseDate.ToString("yyyy-MM-dd")); var playlistBrowseFileName = _spotifyBrowseManager.GetSpotifyBrowseExportFileName(browseDate); var browseExport = await _spotifyBrowseManager.GetSpotifyBrowseForExportAsync(null, browseDate); var directory = @"browseExport\"; if (!Directory.Exists(directory)) { Directory.CreateDirectory(directory); } var playlistBrowseFilePath = directory + playlistBrowseFileName; await SaveExcelFileAsync(browseExport, browseDate, playlistBrowseFilePath); var existingFiles = await _storageFactory.ListObjectsAsync(_spotifyBrowseManager.SpotifyBrowseS3BucketName); // Delete file if it already exists on S3. if (existingFiles != null && existingFiles.S3Objects.Any(s => s.Key.Equals(playlistBrowseFileName, StringComparison.InvariantCultureIgnoreCase))) { await _storageFactory.DeleteObjectAsync(_spotifyBrowseManager.SpotifyBrowseS3BucketName, playlistBrowseFileName); _logger.Info("Deleting old export file.", browseDate.ToString("yyyy-MM-dd")); } await _storageFactory.PutObjectAsync(playlistBrowseFileName, _spotifyBrowseManager.SpotifyBrowseS3BucketName, File.OpenRead(playlistBrowseFilePath)); _logger.Info($"Done saving {playlistBrowseFileName}."); } private async Task SaveExcelFileAsync(List browseItems, DateTime date, string filePath) { using(var excelFile = File.OpenWrite(filePath)) { var fileStream = ExcelFileUtility.GetExcelStream(browseItems, date); await fileStream.CopyToAsync(excelFile); } } private async Task UpdatePeakPositionsForRegionAsync(string region) { _logger.Info($"Updating peak positions for {region}."); using (var conn = await DatabaseHandler.GetOpenConnectionAsync()) { const string sql = "REPLACE INTO tblSpotifyBrowsePeakPosition2 (PlaylistId, Category, Market, PeakPosition) " + "(SELECT PlaylistId, Category, Market, Min(Position) AS Peak " + "FROM tblSpotifyBrowseFeaturedPlaylist2 " + "WHERE Market = @market " + "GROUP BY PlaylistId, Category)"; MySqlCommand cmd = new MySqlCommand(sql, conn); cmd.Parameters.AddWithValue("@market", region); await cmd.ExecuteNonQueryAsync(); } _logger.Info($"Peak positions updated for {region}."); } } }