290 lines
11 KiB
C#
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);
|
|
}
|
|
}
|
|
} |