using System; using System.Collections.Generic; using System.Linq; using System.Threading.Tasks; using NLog; using PetaPoco; using PetaPoco.Business; using Sony.Filtr.Contracts.Definitions; using Sony.Filtr.Contracts.Entities; using Sony.Filtr.PlaylistSynchronization.Data; using Sony.Filtr.PlaylistSynchronization.Synchronizer; using Sony.Filtr.SpotifyWebAPI; using MySql.Data.MySqlClient; using Sony.Filtr.Database; using Sony.Filtr.ErrorLogging; using Sony.Filtr.SpotifyImages; using Sony.Filtr.Utility.Extensions; using Sony.Filtr.ApolloAPI; using Sony.Filtr.VendorToSpotify; using Sony.Filtr.DeezerAPI; using Sony.Filtr.SoundCloud; using System.Net.Http; using System.Data.Common; using Sony.Filtr.YouTube.Api; namespace Sony.Filtr.PlaylistSynchronization { public class PlaylistSynchronizationManager { private readonly IApolloWebApi _vendorApi; private readonly SpotifySynchronizer _spotifySynchronizer; private readonly DeezerSynchronizer _deezerSynchronizer; private readonly YoutubeSynchronizer _youtubeSynchronizer; private readonly SpotifyImageDownloader _spotifyImageHandler; private readonly SoundCloudSynchronizer _soundCloudSynchronizer; private Logger _logger; public PlaylistSynchronizationManager( IApolloWebApi vendorApi, SpotifySynchronizer spotifySynchronizer, DeezerSynchronizer deezerSynchronizer, YoutubeSynchronizer youtubeSynchronizer, SpotifyImageDownloader spotifyImageHandler, SoundCloudSynchronizer soundCloudSynchronizer) { _vendorApi = vendorApi; _spotifySynchronizer = spotifySynchronizer; _deezerSynchronizer = deezerSynchronizer; _youtubeSynchronizer = youtubeSynchronizer; _spotifyImageHandler = spotifyImageHandler; _soundCloudSynchronizer = soundCloudSynchronizer; _logger = LogManager.GetLogger("PlaylistSynchronization"); } public Data.PlaylistSynchronization AddPlaylistSynchronization(Data.PlaylistSynchronization playlistSynchronization) { PetaPocoRepository.Instance.Insert(playlistSynchronization); var newPlaylistSync = GetPlaylistSynchronization(playlistSynchronization.Id); SetInsertMedia(playlistSynchronization.InsertMedia, newPlaylistSync); newPlaylistSync.InsertMedia = GetInsertMedia(newPlaylistSync); return newPlaylistSync; } private void UpdatePlaylistSynchronizationResult(Data.PlaylistSynchronization playlistSynchronization) { PetaPocoRepository.Instance.Update(playlistSynchronization, new List() { "SourceTitle", "SourceImage", "SourceTrackCount", "SynchronizedTrackCount", "Description", "LastUpdated", "Error" }); } public void UpdatePlaylistSynchronization(Data.PlaylistSynchronization playlistSynchronization) { var savedSync = GetPlaylistSynchronization(playlistSynchronization.Id); if (savedSync == null) { throw new PlaylistSynchronizationException("Playlist not saved, can't update", SyncError.DB); } PetaPocoRepository.Instance.Update(playlistSynchronization, new List() { "FromPlaylistId", "ToPlaylistId", "ToServiceAccountId", "Title", "Description", "Active", "TitleCopyMode", "DescriptionCopyMode" }); SetInsertMedia(playlistSynchronization.InsertMedia, playlistSynchronization); } public Data.PlaylistSynchronization GetPlaylistSynchronization(int playlistSynchronizationId) { var sync = PetaPocoRepository.Instance.SingleOrDefault(playlistSynchronizationId); if (sync != null) { sync.InsertMedia = GetInsertMedia(sync); } return sync; } public async Task GetPlaylistSynchronizationWithLatestLogAsync(int playlistSynchronizationId) { var results = await this.GetPlaylistSynchronizationsWithLogsInternal(null, null, playlistSynchronizationId); var sync = results.FirstOrDefault(); if (sync != null) { sync.Synchronization.InsertMedia = GetInsertMedia(sync.Synchronization); } return sync; } public async Task> GetPlaylistSynchronizationsWithLogsAsync(Application application = null, Nullable active = null) { var syncs = await this.GetPlaylistSynchronizationsWithLogsInternal(application, active, null); ConnectInsertMediaToSyncList(syncs.Select(s => s.Synchronization).ToList()); return syncs; } private async Task> GetPlaylistSynchronizationsWithLogsInternal(Application application = null, Nullable active = null, Nullable syncId = null) { var results = new List(); using (var conn = await DatabaseHandler.GetOpenReadOnlyConnectionAsync()) { var sqlString = @" SELECT sync.Id, sync.ApplicationId, sync.FromPlaylistId, sync.FromServiceType, sync.FromMusicServiceId, sync.ToPlaylistId, sync.ToServiceAccountId, sync.Title, sync.Description, sync.LastUpdated, sync.Active, sync.TitleCopyMode, sync.DescriptionCopyMode, sync.CreatedAt, sync.SourceTitle, sync.SourceImage, sync.SourceTrackCount, sync.SynchronizedTrackCount, sync.AppendTrackList, sync.Error AS HasError, log.Time, log.MadeChanges, log.AddedTracks, log.DeletedDuplicates, log.DeletedTracks, log.SourceTracks, log.TargetTrackCount, log.ErrorMessage, log.TriggeredManually, log.Error AS ErrorType FROM tblPlaylistSynchronization sync LEFT JOIN ( select log.* from tblPlaylistSynchronizationLog log INNER JOIN ( SELECT PlaylistSynchronizationId, MAX(TIME) maxDate FROM tblPlaylistSynchronizationLog GROUP BY PlaylistSynchronizationId ) maxTime ON maxTime.PlaylistSynchronizationId = log.PlaylistSynchronizationId AND maxTime.maxDate = log.Time ) log on log.PlaylistSynchronizationId = sync.Id WHERE ((@applicationId IS NULL) OR (sync.ApplicationId = @applicationId)) AND ((@isActive IS NULL) OR (sync.Active = @isActive)) AND ((@syncId is NULL) OR (sync.Id = @syncId));"; var command = new MySqlCommand(sqlString, conn); command.Parameters.AddWithValue("@applicationId", application == null ? null : (object)application.ID); command.Parameters.AddWithValue("@isActive", active.HasValue ? active.Value : (object)null); command.Parameters.AddWithValue("@syncId", syncId.HasValue ? syncId.Value : (object)null); using (var reader = await command.ExecuteReaderAsync()) { while (await reader.ReadAsync()) { results.Add(new PlaylistSyncWithLog(ReadSynchronization(reader), ReadSyncResult(reader))); } } } return results; } public List GetPlaylistSynchronizations(Application application = null, bool onlyActive = false) { var synchronizations = PetaPocoRepository.Instance.Fetch(); if (onlyActive) synchronizations = synchronizations.Where(s => s.Active).ToList(); if (application != null) synchronizations = synchronizations.Where(s => s.ApplicationId == application.ID).ToList(); synchronizations = ConnectInsertMediaToSyncList(synchronizations); return synchronizations; } public Data.PlaylistSynchronization GetPlaylistSynchronization(string toPlaylistId) { var synchronization = PetaPocoRepository.Instance.Fetch(new Sql("WHERE ToPlaylistId = @0", toPlaylistId)).FirstOrDefault(); if (synchronization != null) synchronization.InsertMedia = GetInsertMedia(synchronization); return synchronization; } public void DeletePlaylistSynchronization(Data.PlaylistSynchronization playlistSynchronization) { DeleteInsertMedia(playlistSynchronization); PetaPocoRepository.Instance.Delete(playlistSynchronization); } public async Task ExecutePlaylistSyncAsync(Data.PlaylistSynchronization playlistSynchronization, ServiceAccount toAccount, bool triggeredManually = false) { PlaylistSynchronizationResult result = new PlaylistSynchronizationResult(); try { if (toAccount == null) { throw new PlaylistSynchronizationException($"Could not find service account for playlistSync with id {playlistSynchronization.Id}", SyncError.NoServiceAccount); } if (playlistSynchronization.ApplicationId != toAccount.ApplicationId) { throw new PlaylistSynchronizationException($"Service account application ({toAccount.ApplicationId}) does not match application id of synchronization ({playlistSynchronization.ApplicationId}).", SyncError.ApplicationMismatch); } _logger.Debug("Getting generic playlist"); var genericPlaylist = await GetGenericPlaylist(playlistSynchronization); _logger.Debug("Got generic playlist: {0}", genericPlaylist.Name); playlistSynchronization.SourceTitle = genericPlaylist.Name; playlistSynchronization.SourceImage = genericPlaylist.Image; playlistSynchronization.SourceTrackCount = genericPlaylist.Tracks.Count; if (toAccount.MusicServiceId == (int)MusicService.Deezer) { result = await _deezerSynchronizer.CopyToPlaylistAsync(genericPlaylist, playlistSynchronization, toAccount); } else if (toAccount.MusicServiceId == (int)MusicService.YouTube) { result = await _youtubeSynchronizer.CopyToPlaylistAsync(genericPlaylist, playlistSynchronization, toAccount); } else if (toAccount.MusicServiceId == (int)MusicService.Spotify) { result = await _spotifySynchronizer.CopyToPlaylistAsync(genericPlaylist, playlistSynchronization, toAccount); } else if (toAccount.MusicServiceId == (int)MusicService.SoundCloud) { result = await _soundCloudSynchronizer.CopyToPlaylistAsync(genericPlaylist, playlistSynchronization, toAccount); } else { throw new PlaylistSynchronizationException($"Playlist sync with id {playlistSynchronization.Id} has service account with music service {toAccount.MusicServiceId} which is not supported", SyncError.NotSupported); } playlistSynchronization.SynchronizedTrackCount = result.SyncedTrackCount; playlistSynchronization.LastUpdated = DateTime.UtcNow; playlistSynchronization.Error = result.Error.HasValue; } catch (Exception ex) { _logger.Error(ex); ErrorLoggingManager.Instance.LogError(ex); playlistSynchronization.Error = true; result = PlaylistSynchronizationResult.FromException(ex); } result.TriggeredManually = triggeredManually; UpdatePlaylistSynchronizationResult(playlistSynchronization); await AddSyncLog(playlistSynchronization, result); return result; } private async Task GetGenericPlaylist(Data.PlaylistSynchronization playlistSynchronization) { var fromSpotifyLink = new SpotifyLink(playlistSynchronization.FromPlaylistId); var spotifyPlaylist = (await this._vendorApi.GetPlaylistByIdWithAllTracksAsync(fromSpotifyLink.ExtractPlaylistID(), cacheExpiration: TimeSpan.FromSeconds(0))).ToSpotify(); if (spotifyPlaylist == null) { throw new PlaylistSynchronizationException($"Source playlist could not be loaded from Spotify. id: '{fromSpotifyLink.ExtractPlaylistID()}'", SyncError.NoSourcePlaylist); } if (spotifyPlaylist.Tracks == null || spotifyPlaylist.Tracks.items.Any(i => i == null) || spotifyPlaylist.Tracks.items.Any(i => i.track == null) || spotifyPlaylist.Tracks.items.Any(i => i.track.artists == null)) { throw new PlaylistSynchronizationException($"Source playlist loaded from Spotify contains empty tracks. id: '{fromSpotifyLink.ExtractPlaylistID()}'", SyncError.VendorSpecific); } var image = await _spotifyImageHandler.StoreLargestPlaylistImageAsync(spotifyPlaylist.id, spotifyPlaylist.images?.FirstOrDefault()?.url); var genericPlaylist = new GenericPlaylist() { Name = spotifyPlaylist.name, Description = spotifyPlaylist.description, Image = image, Tracks = spotifyPlaylist.Tracks.items.Where(item => !item.is_local).Select(t => new GenericTrack() { Artists = t.track.artists.Select(a => a.name).ToList(), ISRC = t.track.external_ids.isrc, Name = t.track.name, //DeezerId = p.DeezerId, SpotifyUri = t.track.uri }).ToList(), User = fromSpotifyLink.ExtractSpotifyPlaylistUserName(), }; return genericPlaylist; } public async Task AddSyncLog(Data.PlaylistSynchronization playlistSync, PlaylistSynchronizationResult result) { using (var conn = await DatabaseHandler.GetOpenConnectionAsync()) { const string sqlString = @" INSERT INTO tblPlaylistSynchronizationLog (PlaylistSynchronizationId, MadeChanges, AddedTracks, DeletedTracks, DeletedDuplicates, SourceTracks, TargetTrackCount, ErrorMessage, TriggeredManually, Error) VALUES (@syncId, @changes, @addedTracks, @deletedTracks, @deletedDuplicates, @sourceTracks, @targetTrackCount, @errorMessage, @triggeredManually, @error)"; var command = new MySqlCommand(sqlString, conn); command.Parameters.AddWithValue("@syncId", playlistSync.Id); command.Parameters.AddWithValue("@changes", result.MadeChange); command.Parameters.AddWithValue("@addedTracks", result.AddedTracks); command.Parameters.AddWithValue("@deletedTracks", result.DeletedTracks); command.Parameters.AddWithValue("@deletedDuplicates", result.DeletedDuplicates); command.Parameters.AddWithValue("@sourceTracks", result.SourceTrackCount); command.Parameters.AddWithValue("@targetTrackCount", result.SyncedTrackCount); command.Parameters.AddWithValue("@errorMessage", result.ErrorText); command.Parameters.AddWithValue("@triggeredManually", result.TriggeredManually); command.Parameters.AddWithValue("@error", result.Error.HasValue ? result.Error.ToString() : null); await command.ExecuteNonQueryAsync(); } } public async Task> GetSyncLogAsync(Data.PlaylistSynchronization playlistSync) { List results = new List(); using (var conn = await DatabaseHandler.GetOpenReadOnlyConnectionAsync()) { var sqlString = @" SELECT Time, MadeChanges, AddedTracks, DeletedTracks, DeletedDuplicates, SourceTracks, TargetTrackCount, ErrorMessage, TriggeredManually, Error as ErrorType FROM tblPlaylistSynchronizationLog WHERE PlaylistSynchronizationId=@syncId ORDER BY Time DESC;"; var command = new MySqlCommand(sqlString, conn); command.Parameters.AddWithValue("@syncId", playlistSync.Id); using (var reader = await command.ExecuteReaderAsync()) { while (await reader.ReadAsync()) { results.Add(ReadSyncResult(reader)); } } } return results; } private static Data.PlaylistSynchronization ReadSynchronization(DbDataReader reader) { return new Data.PlaylistSynchronization() { Active = reader.GetBoolean("Active"), AppendTrackList = reader.GetBoolean("AppendTrackList"), ApplicationId = reader.GetInt32("ApplicationId"), CreatedAt = reader.GetDateTime("CreatedAt"), Description = reader.GetString("Description"), DescriptionCopyMode = (FieldCopyMode)reader.GetInt32("DescriptionCopyMode"), Error = reader.GetBoolean("HasError"), FromMusicServiceId = reader.GetInt32("FromMusicServiceId"), FromPlaylistId = reader.GetString("FromPlaylistId"), FromServiceType = (ServiceType)reader.GetInt32("FromServiceType"), Id = reader.GetInt32("Id"), LastUpdated = reader.GetDateTimeOrDefault("LastUpdated"), SourceImage = reader.GetString("SourceImage"), SourceTitle = reader.GetString("SourceTitle"), SourceTrackCount = reader.GetInt32("SourceTrackCount"), SynchronizedTrackCount = reader.GetInt32("SynchronizedTrackCount"), Title = reader.GetString("Title"), TitleCopyMode = (FieldCopyMode)reader.GetInt32("TitleCopyMode"), ToPlaylistId = reader.GetString("ToPlaylistId"), ToServiceAccountId = reader.GetInt32("ToServiceAccountId") }; } private static PlaylistSynchronizationResult ReadSyncResult(DbDataReader reader) { Nullable time = reader.GetDateTimeOrDefault("Time"); return time.HasValue ? new PlaylistSynchronizationResult() { Time = time, MadeChange = reader.GetBoolean("MadeChanges"), AddedTracks = reader.GetInt32("AddedTracks"), DeletedTracks = reader.GetInt32("DeletedTracks"), DeletedDuplicates = reader.GetInt32("DeletedDuplicates"), SourceTrackCount = reader.GetInt32("SourceTracks"), SyncedTrackCount = reader.GetInt32("TargetTrackCount"), ErrorText = reader.GetString("ErrorMessage"), TriggeredManually = reader.GetBoolean("TriggeredManually"), Error = reader.GetString("ErrorType").ParseAsEnumNullable() } : new PlaylistSynchronizationResult(); } private List ConnectInsertMediaToSyncList(List playlistSynchronizations) { var mediaItems = GetAllInsertMedia(); playlistSynchronizations.ForEach(p => p.InsertMedia = mediaItems.Where(m => m.PlaylistSynchronizationId == p.Id).ToList()); return playlistSynchronizations; } public List GetInsertMedia(Data.PlaylistSynchronization playlistSync) { return PetaPocoRepository.Instance.Fetch(new Sql("WHERE PlaylistSynchronizationId = @0", playlistSync.Id)).OrderBy(m => m.InsertPosition).ToList(); } public List GetAllInsertMedia() { return PetaPocoRepository.Instance.Fetch().OrderBy(m => m.InsertPosition).ToList(); } public void SetInsertMedia(List mediaToInsert, Data.PlaylistSynchronization playlistSync) { PetaPocoRepository.Instance.Delete(new Sql("WHERE PlaylistSynchronizationId = @0", playlistSync.Id)); if (mediaToInsert == null || !mediaToInsert.Any()) return; foreach (var media in mediaToInsert) { media.PlaylistSynchronizationId = playlistSync.Id; PetaPocoRepository.Instance.Insert(media); } } public void DeleteInsertMedia(Data.PlaylistSynchronization playlistSync) { PetaPocoRepository.Instance.Delete(new Sql("WHERE PlaylistSynchronizationId = @0", playlistSync.Id)); } } public class PlaylistSynchronizationResult { public DateTime? Time { get; set; } public int SyncedTrackCount { get; set; } public int SourceTrackCount { get; set; } public int AddedTracks { get; set; } public int DeletedTracks { get; set; } public int DeletedDuplicates { get; set; } public bool MadeChange { get; set; } public string ErrorText { get; set; } public bool TriggeredManually { get; set; } public Nullable Error { get; set; } public static PlaylistSynchronizationResult FromSuccess(SyncCounts counts, int syncedTrackCount) { return new PlaylistSynchronizationResult() { Time = DateTime.Now, SyncedTrackCount = syncedTrackCount, AddedTracks = counts.Added, DeletedDuplicates = counts.Duplicates, DeletedTracks = counts.Removed, MadeChange = counts.Added > 0 || counts.Duplicates > 0 || counts.Removed > 0 }; } public static PlaylistSynchronizationResult FromError(SyncError result, string message) { return new PlaylistSynchronizationResult() { Time = DateTime.Now, Error = result, ErrorText = message }; } public static PlaylistSynchronizationResult FromException(Exception ex) { var result = new PlaylistSynchronizationResult() { Time = DateTime.Now, ErrorText = ex.GetFullMessage(ExceptionData.Message) }; switch (ex) { case SpotifyWebAPIException spotifyEx: switch (spotifyEx.StatusCode) { case System.Net.HttpStatusCode.BadRequest: result.Error = SyncError.Unauthorized; break; case System.Net.HttpStatusCode.Unauthorized: result.Error = SyncError.Unauthorized; break; default: result.Error = SyncError.VendorSpecific; break; } break; case DeezerException deezerEx: switch (deezerEx.Error.code) { case (int)System.Net.HttpStatusCode.BadRequest: result.Error = SyncError.Unauthorized; break; case (int)System.Net.HttpStatusCode.Unauthorized: result.Error = SyncError.Unauthorized; break; default: result.Error = SyncError.VendorSpecific; break; } break; case SoundCloudException soundEx: switch (soundEx.StatusCode) { case System.Net.HttpStatusCode.BadRequest: result.Error = SyncError.Unauthorized; break; case System.Net.HttpStatusCode.Unauthorized: result.Error = SyncError.Unauthorized; break; default: result.Error = SyncError.VendorSpecific; break; } break; case YoutubeApiException youtubeEx: switch (youtubeEx.StatusCode) { case System.Net.HttpStatusCode.BadRequest: result.Error = SyncError.Unauthorized; break; case System.Net.HttpStatusCode.Unauthorized: result.Error = SyncError.Unauthorized; break; default: result.Error = SyncError.VendorSpecific; break; } break; case HttpRequestException httpRequestEx: result.Error = SyncError.BadRequest; break; case PlaylistSynchronizationException syncEx: result.Error = syncEx.Error; break; default: result.Error = SyncError.Unknown; break; } return result; } } public struct SyncCounts { public readonly int Added; public readonly int Duplicates; public readonly int Removed; public SyncCounts(int added, int duplicates, int removed) { this.Added = added; this.Duplicates = duplicates; this.Removed = removed; } } public enum SyncError { Unknown, NoSourcePlaylist, NoTargetPlaylist, BadRequest, Unauthorized, NoServiceAccount, ApplicationMismatch, DB, NotSupported, VendorSpecific } }