using SpotifyAPI.Web; using System.Web; using Microsoft.EntityFrameworkCore; public class SpotifyApiClient { private SpotifyClient _spotifyClient; private SpotifyClient? _userAuthorizedSpotifyClient; private ILogger _logger; public SpotifyApiClient(ILogger logger) { var config = SpotifyClientConfig.CreateDefault() .WithAuthenticator(new ClientCredentialsAuthenticator( AppConfiguration.Instance.SpotifyClientId, AppConfiguration.Instance.SpotifyClientSecret)); _spotifyClient = new SpotifyClient(config); _userAuthorizedSpotifyClient = null; _logger = logger; } public async Task WithUserAuthorizationAsync(User user) { var refreshResponse = await new OAuthClient().RequestToken( new AuthorizationCodeRefreshRequest( AppConfiguration.Instance.SpotifyClientId, AppConfiguration.Instance.SpotifyClientSecret, user.SpotifyAuthRefreshToken) ); var config = SpotifyClientConfig .CreateDefault() .WithAuthenticator(new AuthorizationCodeAuthenticator( AppConfiguration.Instance.SpotifyClientId, AppConfiguration.Instance.SpotifyClientSecret, new AuthorizationCodeTokenResponse() { RefreshToken = refreshResponse.RefreshToken, AccessToken = refreshResponse.AccessToken, TokenType = refreshResponse.TokenType, ExpiresIn = refreshResponse.ExpiresIn, Scope = refreshResponse.Scope, CreatedAt = refreshResponse.CreatedAt })); _userAuthorizedSpotifyClient = new SpotifyClient(config); return this; } private SpotifyClient UserAuthorizedSpotifyClient { get { if (_userAuthorizedSpotifyClient == null) { throw new Exception("Cannot perform Spotify API call without user authorization. Authorize Spotify access from your user page first!"); } return _userAuthorizedSpotifyClient; } } public async Task> GetTrackCandidatesAsync(string trackName, string artistName) { try { var searchResponse = await _spotifyClient.Search.Item(new SearchRequest(SearchRequest.Types.Track, $"{trackName} {artistName}") { Limit = 5 }); return searchResponse.Tracks.Items ?? new List(); } catch (APIException ex) { throw new Exception($"Error fetching tracks by query: \"{trackName} {artistName}\": {ex.Message}", ex); } } public string GetLoginRedirectUri() { return "http://127.0.0.1:5000/SpotifyLogin"; } private bool IsAuthTokenExpired(User user) { if (user.SpotifyAuthCreatedAt == null || user.SpotifyAuthExpiresAfterSeconds == null) { return true; } var expirationdate = user.SpotifyAuthCreatedAt.Value.AddSeconds(user.SpotifyAuthExpiresAfterSeconds.Value); return expirationdate < DateTime.UtcNow; } public async Task DeAuthorizeUserAsync(User user) { using (var dci = DataContext.Instance) { var isEntityTracked = dci.Entry(user).State != EntityState.Detached; if (!isEntityTracked) { user = dci.Users.Find(user.UserId); } user.SpotifyAuthAccessToken = string.Empty; user.SpotifyAuthRefreshToken = string.Empty; user.SpotifyAuthExpiresAfterSeconds = null; user.SpotifyAuthCreatedAt = null; await dci.SaveChangesAsync(); } } public async Task GetValidAuthorizationTokenAsync(User user) { if (string.IsNullOrEmpty(user.SpotifyAuthAccessToken)) { // user either never connected Spotify or we failed to refresh token - user needs to re-authenticate return string.Empty; } if (!this.IsAuthTokenExpired(user)) { return user.SpotifyAuthAccessToken; } // if token is expired, attempt a refresh var dci = DataContext.Instance; var isEntityTracked = dci.Entry(user).State != EntityState.Detached; if (!isEntityTracked) { user = dci.Users.Find(user.UserId); } try { var oAuthResponse = await new OAuthClient().RequestToken( new AuthorizationCodeRefreshRequest(AppConfiguration.Instance.SpotifyClientId, AppConfiguration.Instance.SpotifyClientSecret, user.SpotifyAuthRefreshToken) ); user.SpotifyAuthAccessToken = oAuthResponse.AccessToken; user.SpotifyAuthExpiresAfterSeconds = oAuthResponse.ExpiresIn; user.SpotifyAuthCreatedAt = oAuthResponse.CreatedAt; user.SpotifyAuthRefreshToken = oAuthResponse.RefreshToken; return user.SpotifyAuthAccessToken; } catch (Exception ex) { _logger.LogWarning($"Failed to refresh SpotifyAuth token for user {user.LdapUserName}: {ex.Message}"); await DeAuthorizeUserAsync(user); return string.Empty; } finally { await dci.SaveChangesAsync(); dci.Dispose(); } } public async Task IsUserAuthenticatedAsync(User user) { return !string.IsNullOrEmpty(await this.GetValidAuthorizationTokenAsync(user)); } public async Task GetTrackByIdAsync(string trackId) { try { return await _spotifyClient.Tracks.Get(trackId); } catch (APIException ex) { throw new Exception($"Error fetching track by ID: {trackId}: {ex.Message}", ex); } } public async Task CreateSpotifyPlaylist(string playlistTitle, string description, bool IncludesUnCategorizedSongs, bool IncludesLikedSongs, User createdBy) { try { // for now hardcoded with my user ID var playlistCreationRequest = new PlaylistCreateRequest(playlistTitle); playlistCreationRequest.Public = true; playlistCreationRequest.Collaborative = false; playlistCreationRequest.Description = description; var currentUser = await UserAuthorizedSpotifyClient.UserProfile.Current(); var playlist = await UserAuthorizedSpotifyClient.Playlists.Create(currentUser.Id, playlistCreationRequest); _logger.LogWarning($"Creating new playlist '{playlistTitle}'"); using (var dci = DataContext.Instance) { var trackedUserEntity = dci.Users.Find(createdBy.UserId); var newPlaylist = new SmartPlaylistDefinition() { Title = playlistTitle, Description = description, Categories = new List(), IncludesLikedSongs = IncludesLikedSongs, IncludesUnCategorizedSongs = IncludesUnCategorizedSongs, SpotifyPlaylistId = playlist.Id, CreatedBy = trackedUserEntity, ExplicitlyExcludedSongs = new List(), ExplicitlyIncludedSongs = new List() }; var trackedEntity = dci.SmartPlaylistDefinitions.Add(newPlaylist); await dci.SaveChangesAsync(); return trackedEntity.Entity; } } catch (APIException ex) { throw new Exception($"Error creating playlist with title: {playlistTitle}: {ex.Message}", ex); } } public async Task> GetSongsInPlaylist(string playlistId) { try { // for now hardcoded with my user ID var ids = new List(); var firstContentPage = await UserAuthorizedSpotifyClient.Playlists.GetItems(playlistId); var allPages = await UserAuthorizedSpotifyClient.PaginateAll(firstContentPage); ids.AddRange(allPages.Select(track => (track.Track as FullTrack).Id)); return ids; } catch (APIException ex) { throw new Exception($"Error fetching playlist contents for playlist with id: {playlistId}: {ex.Message}", ex); } } public async Task AddSongsToPlaylist(string playlistId, List songIds) { if (songIds.Count == 0) { _logger.LogWarning($"No songs to add to playlist with id '{playlistId}'"); return string.Empty; } try { // for now hardcoded with my user ID var addItemRequest = new PlaylistAddItemsRequest(songIds.Select(id => $"spotify:track:{id}").ToList()); _logger.LogWarning($"Adding songs to playlist with id '{playlistId}'"); var response = await UserAuthorizedSpotifyClient.Playlists.AddItems(playlistId, addItemRequest); return response.SnapshotId; } catch (APIException ex) { throw new Exception($"Error adding songs to playlist with id: {playlistId}: {ex.Message}", ex); } } public async Task RemoveSongsFromPlaylist(string playlistId, List songIds) { if (songIds.Count == 0) { _logger.LogWarning($"No songs to remove from playlist with id '{playlistId}'"); return string.Empty; } try { // for now hardcoded with my user ID var removeItemsRequest = new PlaylistRemoveItemsRequest(); removeItemsRequest.Tracks = new List(); foreach (var song in songIds) { var item = new PlaylistRemoveItemsRequest.Item() { Uri = $"spotify:track:{song}", }; removeItemsRequest.Tracks.Add(item); } _logger.LogWarning($"Removing songs from playlist with id '{playlistId}'"); var response = await UserAuthorizedSpotifyClient.Playlists.RemoveItems(playlistId, removeItemsRequest); return response.SnapshotId; } catch (APIException ex) { throw new Exception($"Error removing songs from playlist with id: {playlistId}: {ex.Message}", ex); } } }