290 lines
11 KiB
C#

using SpotifyAPI.Web;
using System.Web;
using Microsoft.EntityFrameworkCore;
public class SpotifyApiClient
{
private SpotifyClient _spotifyClient;
private SpotifyClient? _userAuthorizedSpotifyClient;
private ILogger<SpotifyApiClient> _logger;
public SpotifyApiClient(ILogger<SpotifyApiClient> 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<SpotifyApiClient> 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<List<FullTrack>> 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<FullTrack>();
}
catch (APIException ex)
{
throw new Exception($"Error fetching tracks by query: \"{trackName} {artistName}\": {ex.Message}", ex);
}
}
public string GetLoginRedirectUri()
{
return AppConfiguration.Instance.WebUIBaseURL + (AppConfiguration.Instance.WebUIBaseURL.EndsWith("/") ? "SpotifyLogin" : "/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<string> GetValidAuthorizationTokenAsync(User user)
{
if (string.IsNullOrEmpty(user.SpotifyAuthAccessToken) || string.IsNullOrEmpty(user.SpotifyAuthRefreshToken))
{
// 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<bool> IsUserAuthenticatedAsync(User user)
{
return !string.IsNullOrEmpty(await this.GetValidAuthorizationTokenAsync(user));
}
public async Task<FullTrack> 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<SmartPlaylistDefinition> 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<SuggestionHelper>(),
IncludesLikedSongs = IncludesLikedSongs,
IncludesUnCategorizedSongs = IncludesUnCategorizedSongs,
SpotifyPlaylistId = playlist.Id,
CreatedBy = trackedUserEntity,
ExplicitlyExcludedSongs = new List<Song>(),
ExplicitlyIncludedSongs = new List<Song>()
};
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<List<string>> GetSongsInPlaylist(string playlistId)
{
try
{
// for now hardcoded with my user ID
var ids = new List<string>();
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<string> AddSongsToPlaylist(string playlistId, List<string> 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<string> RemoveSongsFromPlaylist(string playlistId, List<string> 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<PlaylistRemoveItemsRequest.Item>();
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);
}
}
}