337 lines
12 KiB
C#
337 lines
12 KiB
C#
namespace Webzine.Entity.Fixtures;
|
|
|
|
using System.Globalization;
|
|
using System.Net.Http.Headers;
|
|
using System.Net.Http.Json;
|
|
using System.Text;
|
|
using System.Text.Json.Serialization;
|
|
|
|
using Microsoft.Extensions.Options;
|
|
|
|
public class SpotifySeederOptions
|
|
{
|
|
public string ClientId { get; set; } = string.Empty;
|
|
|
|
public string ClientSecret { get; set; } = string.Empty;
|
|
|
|
public string Market { get; set; } = "FR";
|
|
|
|
public List<string> Genres { get; set; } =
|
|
[
|
|
"rock",
|
|
"pop",
|
|
"jazz",
|
|
"hip hop",
|
|
"electronic",
|
|
"metal",
|
|
];
|
|
|
|
public int ArtistsPerGenre { get; set; } = 4;
|
|
|
|
public int AlbumsPerArtist { get; set; } = 2;
|
|
|
|
public int TracksPerAlbum { get; set; } = 4;
|
|
|
|
public int MaxCommentsPerTrack { get; set; } = 3;
|
|
}
|
|
|
|
public class SeedDataSpotify
|
|
{
|
|
private readonly HttpClient httpClient;
|
|
private readonly SpotifySeederOptions options;
|
|
|
|
public SeedDataSpotify(HttpClient httpClient, IOptions<SpotifySeederOptions> optionsAccessor)
|
|
{
|
|
this.httpClient = httpClient;
|
|
this.options = optionsAccessor.Value;
|
|
}
|
|
|
|
/// <summary>
|
|
///
|
|
/// </summary>
|
|
/// <param name="cancellationToken"></param>
|
|
/// <returns><placeholder>A <see cref="Task"/> representing the asynchronous operation.</placeholder></returns>
|
|
/// <exception cref="InvalidOperationException"></exception>
|
|
public async Task<SeedDataSet> GenererJeuDeDonneesAsync(CancellationToken cancellationToken = default)
|
|
{
|
|
if (string.IsNullOrWhiteSpace(this.options.ClientId) || string.IsNullOrWhiteSpace(this.options.ClientSecret))
|
|
{
|
|
throw new InvalidOperationException("Renseignez SpotifySeeder:ClientId et SpotifySeeder:ClientSecret.");
|
|
}
|
|
|
|
var token = await this.GetTokenAsync(cancellationToken);
|
|
var styles = new Dictionary<string, Style>(StringComparer.OrdinalIgnoreCase);
|
|
var artistes = new List<Artiste>();
|
|
var titres = new List<Titre>();
|
|
var commentaires = new List<Commentaire>();
|
|
var artistIds = new HashSet<string>(StringComparer.OrdinalIgnoreCase);
|
|
int nextArtistId = 1;
|
|
int nextStyleId = 1;
|
|
int nextTitreId = 1;
|
|
int nextCommentaireId = 1;
|
|
|
|
foreach (var genre in this.options.Genres.Where(g => !string.IsNullOrWhiteSpace(g)))
|
|
{
|
|
var artistesSpotify = await this.GetAsync<SpotifySearchArtistsResponse>(
|
|
$"https://api.spotify.com/v1/search?q={Uri.EscapeDataString($"genre:\"{genre}\"")}&type=artist&market={this.options.Market}&limit={Math.Clamp(this.options.ArtistsPerGenre, 1, 10)}",
|
|
token,
|
|
cancellationToken);
|
|
|
|
foreach (var artisteSpotify in artistesSpotify?.Artists?.Items ??[])
|
|
{
|
|
if (string.IsNullOrWhiteSpace(artisteSpotify.Id) || !artistIds.Add(artisteSpotify.Id))
|
|
{
|
|
continue;
|
|
}
|
|
|
|
var stylesTitre = (artisteSpotify.Genres.Count > 0 ? artisteSpotify.Genres :[genre])
|
|
.Select(g => this.GetOrCreateStyle(styles, NormalizeGenre(g), ref nextStyleId))
|
|
.DistinctBy(s => s.IdStyle)
|
|
.ToList();
|
|
|
|
var artiste = new Artiste
|
|
{
|
|
IdArtiste = nextArtistId++,
|
|
Nom = Trim(artisteSpotify.Name, 50, "Artiste Spotify"),
|
|
Biographie = Trim(
|
|
$"{artisteSpotify.Name} est un artiste present sur Spotify, associe aux styles {string.Join(", ", stylesTitre.Select(s => s.Libelle))}.",
|
|
4000,
|
|
"Artiste issu du catalogue Spotify."),
|
|
Titres = new List<Titre>(),
|
|
};
|
|
|
|
artistes.Add(artiste);
|
|
|
|
var albums = await this.GetAsync<SpotifyAlbumsResponse>(
|
|
$"https://api.spotify.com/v1/artists/{artisteSpotify.Id}/albums?include_groups=album,single&market={this.options.Market}",
|
|
token,
|
|
cancellationToken);
|
|
|
|
foreach (var album in albums?.Items?.GroupBy(a => a.Name, StringComparer.OrdinalIgnoreCase).Select(g => g.First()) ??[])
|
|
{
|
|
var tracks = await this.GetAsync<SpotifyTracksResponse>(
|
|
$"https://api.spotify.com/v1/albums/{album.Id}/tracks?market={this.options.Market}&limit={Math.Clamp(this.options.TracksPerAlbum, 1, 10)}",
|
|
token,
|
|
cancellationToken);
|
|
|
|
foreach (var track in tracks?.Items ??[])
|
|
{
|
|
var titre = new Titre
|
|
{
|
|
IdTitre = nextTitreId++,
|
|
IdArtiste = artiste.IdArtiste,
|
|
Artiste = artiste,
|
|
Libelle = Trim(track.Name, 200, "Titre Spotify"),
|
|
Chronique = Trim(
|
|
$"{track.Name} est un titre de {artiste.Nom}, issu de l'album {album.Name}. Cette fiche a ete generee depuis Spotify.",
|
|
4000,
|
|
"Titre importe depuis Spotify."),
|
|
DateCreation = DateTime.UtcNow,
|
|
DateSortie = ParseDate(album.ReleaseDate),
|
|
Duree = Math.Max(1, track.DurationMs / 1000),
|
|
UrlJaquette = Trim(album.Images.FirstOrDefault()?.Url, 250, "https://placehold.co/640x640"),
|
|
UrlEcoute = Trim(track.ExternalUrls?.Spotify ?? $"https://open.spotify.com/track/{track.Id}", 250, string.Empty),
|
|
NbLectures = Random.Shared.Next(500, 50000),
|
|
NbLikes = Random.Shared.Next(50, 5000),
|
|
Album = Trim(album.Name, 200, "Album Spotify"),
|
|
Commentaires = new List<Commentaire>(),
|
|
Styles = new List<Style>(stylesTitre),
|
|
};
|
|
|
|
var commentairesTitre = SeedDataLocal.GenererListeCommentaire(
|
|
titre,
|
|
0,
|
|
Math.Clamp(this.options.MaxCommentsPerTrack, 0, 10),
|
|
nextCommentaireId);
|
|
|
|
nextCommentaireId += commentairesTitre.Count;
|
|
|
|
titre.Commentaires.AddRange(commentairesTitre);
|
|
commentaires.AddRange(commentairesTitre);
|
|
titres.Add(titre);
|
|
artiste.Titres.Add(titre);
|
|
|
|
foreach (var style in titre.Styles)
|
|
{
|
|
style.Titres.Add(titre);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
return new SeedDataSet
|
|
{
|
|
Artistes = artistes,
|
|
Styles = styles.Values.OrderBy(s => s.IdStyle).ToList(),
|
|
Titres = titres,
|
|
Commentaires = commentaires,
|
|
};
|
|
}
|
|
|
|
private static string NormalizeGenre(string genre)
|
|
{
|
|
var value = string.IsNullOrWhiteSpace(genre) ? "Musique" : genre.Trim().Replace('-', ' ');
|
|
return CultureInfo.InvariantCulture.TextInfo.ToTitleCase(value);
|
|
}
|
|
|
|
private static string Trim(string? value, int maxLength, string fallback)
|
|
{
|
|
var result = string.IsNullOrWhiteSpace(value) ? fallback : value.Trim();
|
|
return result.Length > maxLength ? result[..maxLength] : result;
|
|
}
|
|
|
|
private static DateTime ParseDate(string? value)
|
|
{
|
|
if (string.IsNullOrWhiteSpace(value))
|
|
{
|
|
return DateTime.UtcNow;
|
|
}
|
|
|
|
if (DateTime.TryParse(value, CultureInfo.InvariantCulture, DateTimeStyles.AssumeUniversal, out var date))
|
|
{
|
|
return DateTime.SpecifyKind(date, DateTimeKind.Utc);
|
|
}
|
|
|
|
if (DateTime.TryParseExact(value, "yyyy-MM", CultureInfo.InvariantCulture, DateTimeStyles.None, out date))
|
|
{
|
|
return DateTime.SpecifyKind(date, DateTimeKind.Utc);
|
|
}
|
|
|
|
if (DateTime.TryParseExact(value, "yyyy", CultureInfo.InvariantCulture, DateTimeStyles.None, out date))
|
|
{
|
|
return DateTime.SpecifyKind(date, DateTimeKind.Utc);
|
|
}
|
|
|
|
return DateTime.UtcNow;
|
|
}
|
|
|
|
private Style GetOrCreateStyle(Dictionary<string, Style> styles, string libelle, ref int nextStyleId)
|
|
{
|
|
if (!styles.TryGetValue(libelle, out var style))
|
|
{
|
|
style = new Style
|
|
{
|
|
IdStyle = nextStyleId++,
|
|
Libelle = Trim(libelle, 50, "Musique"),
|
|
Titres = new List<Titre>(),
|
|
};
|
|
|
|
styles.Add(style.Libelle, style);
|
|
}
|
|
|
|
return style;
|
|
}
|
|
|
|
private async Task<string> GetTokenAsync(CancellationToken cancellationToken)
|
|
{
|
|
using var request = new HttpRequestMessage(HttpMethod.Post, "https://accounts.spotify.com/api/token");
|
|
request.Content = new FormUrlEncodedContent(
|
|
[
|
|
new KeyValuePair<string, string>("grant_type", "client_credentials"),
|
|
]);
|
|
|
|
var basic = Convert.ToBase64String(Encoding.UTF8.GetBytes($"{this.options.ClientId}:{this.options.ClientSecret}"));
|
|
request.Headers.Authorization = new AuthenticationHeaderValue("Basic", basic);
|
|
|
|
using var response = await this.httpClient.SendAsync(request, cancellationToken);
|
|
response.EnsureSuccessStatusCode();
|
|
|
|
var payload = await response.Content.ReadFromJsonAsync<SpotifyTokenResponse>(cancellationToken: cancellationToken);
|
|
return payload?.AccessToken ?? throw new InvalidOperationException("Token Spotify introuvable.");
|
|
}
|
|
|
|
private async Task<T?> GetAsync<T>(string url, string token, CancellationToken cancellationToken)
|
|
{
|
|
using var request = new HttpRequestMessage(HttpMethod.Get, url);
|
|
request.Headers.Authorization = new AuthenticationHeaderValue("Bearer", token);
|
|
|
|
using var response = await this.httpClient.SendAsync(request, cancellationToken);
|
|
response.EnsureSuccessStatusCode();
|
|
return await response.Content.ReadFromJsonAsync<T>(cancellationToken: cancellationToken);
|
|
}
|
|
|
|
private sealed class SpotifyTokenResponse
|
|
{
|
|
[JsonPropertyName("access_token")]
|
|
public string? AccessToken { get; set; }
|
|
}
|
|
|
|
private sealed class SpotifySearchArtistsResponse
|
|
{
|
|
[JsonPropertyName("artists")]
|
|
public SpotifyArtistsContainer? Artists { get; set; }
|
|
}
|
|
|
|
private sealed class SpotifyArtistsContainer
|
|
{
|
|
[JsonPropertyName("items")]
|
|
public List<SpotifyArtist> Items { get; set; } = new ();
|
|
}
|
|
|
|
private sealed class SpotifyArtist
|
|
{
|
|
[JsonPropertyName("id")]
|
|
public string Id { get; set; } = string.Empty;
|
|
|
|
[JsonPropertyName("name")]
|
|
public string Name { get; set; } = string.Empty;
|
|
|
|
[JsonPropertyName("genres")]
|
|
public List<string> Genres { get; set; } = new ();
|
|
}
|
|
|
|
private sealed class SpotifyAlbumsResponse
|
|
{
|
|
[JsonPropertyName("items")]
|
|
public List<SpotifyAlbum> Items { get; set; } = new ();
|
|
}
|
|
|
|
private sealed class SpotifyAlbum
|
|
{
|
|
[JsonPropertyName("id")]
|
|
public string Id { get; set; } = string.Empty;
|
|
|
|
[JsonPropertyName("name")]
|
|
public string Name { get; set; } = string.Empty;
|
|
|
|
[JsonPropertyName("release_date")]
|
|
public string? ReleaseDate { get; set; }
|
|
|
|
[JsonPropertyName("images")]
|
|
public List<SpotifyImage> Images { get; set; } = new ();
|
|
}
|
|
|
|
private sealed class SpotifyTracksResponse
|
|
{
|
|
[JsonPropertyName("items")]
|
|
public List<SpotifyTrack> Items { get; set; } = new ();
|
|
}
|
|
|
|
private sealed class SpotifyTrack
|
|
{
|
|
[JsonPropertyName("id")]
|
|
public string Id { get; set; } = string.Empty;
|
|
|
|
[JsonPropertyName("name")]
|
|
public string Name { get; set; } = string.Empty;
|
|
|
|
[JsonPropertyName("duration_ms")]
|
|
public int DurationMs { get; set; }
|
|
|
|
[JsonPropertyName("external_urls")]
|
|
public SpotifyExternalUrls? ExternalUrls { get; set; }
|
|
}
|
|
|
|
private sealed class SpotifyImage
|
|
{
|
|
[JsonPropertyName("url")]
|
|
public string? Url { get; set; }
|
|
}
|
|
|
|
private sealed class SpotifyExternalUrls
|
|
{
|
|
[JsonPropertyName("spotify")]
|
|
public string? Spotify { get; set; }
|
|
}
|
|
} |