#145 : Ajout du seeder Spotify.
This commit is contained in:
34
Webzine.Business/DTOs/Spotify/SpotifyAlbumDto.cs
Normal file
34
Webzine.Business/DTOs/Spotify/SpotifyAlbumDto.cs
Normal file
@@ -0,0 +1,34 @@
|
|||||||
|
namespace Webzine.Business.DTOs.Spotify
|
||||||
|
{
|
||||||
|
using System.Text.Json.Serialization;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Objet album de spotify.
|
||||||
|
/// </summary>
|
||||||
|
public class SpotifyAlbumDto
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Id de l'album.
|
||||||
|
/// </summary>
|
||||||
|
[JsonPropertyName("id")]
|
||||||
|
public string Id { get; set; } = string.Empty;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Nom de l'album.
|
||||||
|
/// </summary>
|
||||||
|
[JsonPropertyName("name")]
|
||||||
|
public string Name { get; set; } = string.Empty;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Date de sortie.
|
||||||
|
/// </summary>
|
||||||
|
[JsonPropertyName("release_date")]
|
||||||
|
public string? ReleaseDate { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Urls de la jaquette.
|
||||||
|
/// </summary>
|
||||||
|
[JsonPropertyName("images")]
|
||||||
|
public List<SpotifyImageDto> Images { get; set; } = new ();
|
||||||
|
}
|
||||||
|
}
|
||||||
16
Webzine.Business/DTOs/Spotify/SpotifyAlbumsResponseDto.cs
Normal file
16
Webzine.Business/DTOs/Spotify/SpotifyAlbumsResponseDto.cs
Normal file
@@ -0,0 +1,16 @@
|
|||||||
|
namespace Webzine.Business.DTOs.Spotify
|
||||||
|
{
|
||||||
|
using System.Text.Json.Serialization;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Objet Spotify qui contient une liste d'album.
|
||||||
|
/// </summary>
|
||||||
|
public class SpotifyAlbumsResponseDto
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Container de plusieurs albums spotify.
|
||||||
|
/// </summary>
|
||||||
|
[JsonPropertyName("items")]
|
||||||
|
public List<SpotifyAlbumDto> Items { get; set; } = new ();
|
||||||
|
}
|
||||||
|
}
|
||||||
28
Webzine.Business/DTOs/Spotify/SpotifyArtistDto.cs
Normal file
28
Webzine.Business/DTOs/Spotify/SpotifyArtistDto.cs
Normal file
@@ -0,0 +1,28 @@
|
|||||||
|
namespace Webzine.Business.DTOs.Spotify
|
||||||
|
{
|
||||||
|
using System.Text.Json.Serialization;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Objet artiste retourne depuis spotify.
|
||||||
|
/// </summary>
|
||||||
|
public class SpotifyArtistDto
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Id de l'artiste.
|
||||||
|
/// </summary>
|
||||||
|
[JsonPropertyName("id")]
|
||||||
|
public string Id { get; set; } = string.Empty;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Nom de l'artiste.
|
||||||
|
/// </summary>
|
||||||
|
[JsonPropertyName("name")]
|
||||||
|
public string Name { get; set; } = string.Empty;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Genre musical de l'artiste.
|
||||||
|
/// </summary>
|
||||||
|
[JsonPropertyName("genres")]
|
||||||
|
public List<string> Genres { get; set; } = new ();
|
||||||
|
}
|
||||||
|
}
|
||||||
16
Webzine.Business/DTOs/Spotify/SpotifyArtistsContainerDto.cs
Normal file
16
Webzine.Business/DTOs/Spotify/SpotifyArtistsContainerDto.cs
Normal file
@@ -0,0 +1,16 @@
|
|||||||
|
namespace Webzine.Business.DTOs.Spotify
|
||||||
|
{
|
||||||
|
using System.Text.Json.Serialization;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Container d'artistes spotify.
|
||||||
|
/// </summary>
|
||||||
|
public class SpotifyArtistsContainerDto
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Liste d'artiste spotify.
|
||||||
|
/// </summary>
|
||||||
|
[JsonPropertyName("items")]
|
||||||
|
public List<SpotifyArtistDto> Items { get; set; } = new ();
|
||||||
|
}
|
||||||
|
}
|
||||||
9
Webzine.Business/DTOs/Spotify/SpotifyExternalUrlsDto.cs
Normal file
9
Webzine.Business/DTOs/Spotify/SpotifyExternalUrlsDto.cs
Normal file
@@ -0,0 +1,9 @@
|
|||||||
|
namespace Webzine.Business.DTOs.Spotify;
|
||||||
|
|
||||||
|
using System.Text.Json.Serialization;
|
||||||
|
|
||||||
|
public class SpotifyExternalUrlsDto
|
||||||
|
{
|
||||||
|
[JsonPropertyName("spotify")]
|
||||||
|
public string? Spotify { get; set; }
|
||||||
|
}
|
||||||
9
Webzine.Business/DTOs/Spotify/SpotifyImageDto.cs
Normal file
9
Webzine.Business/DTOs/Spotify/SpotifyImageDto.cs
Normal file
@@ -0,0 +1,9 @@
|
|||||||
|
namespace Webzine.Business.DTOs.Spotify;
|
||||||
|
|
||||||
|
using System.Text.Json.Serialization;
|
||||||
|
|
||||||
|
public class SpotifyImageDto
|
||||||
|
{
|
||||||
|
[JsonPropertyName("url")]
|
||||||
|
public string? Url { get; set; }
|
||||||
|
}
|
||||||
@@ -0,0 +1,16 @@
|
|||||||
|
namespace Webzine.Business.DTOs.Spotify
|
||||||
|
{
|
||||||
|
using System.Text.Json.Serialization;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Resultat d'une recherche spotify avec un objet artist.
|
||||||
|
/// </summary>
|
||||||
|
public class SpotifySearchArtistsResponseDto
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Liste d'artistes.
|
||||||
|
/// </summary>
|
||||||
|
[JsonPropertyName("artists")]
|
||||||
|
public SpotifyArtistsContainerDto? Artists { get; set; }
|
||||||
|
}
|
||||||
|
}
|
||||||
16
Webzine.Business/DTOs/Spotify/SpotifyTokenResponseDto.cs
Normal file
16
Webzine.Business/DTOs/Spotify/SpotifyTokenResponseDto.cs
Normal file
@@ -0,0 +1,16 @@
|
|||||||
|
namespace Webzine.Business.DTOs.Spotify
|
||||||
|
{
|
||||||
|
using System.Text.Json.Serialization;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Recuperation Token Bearer de spotify.
|
||||||
|
/// </summary>
|
||||||
|
public class SpotifyTokenResponseDto
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Jeton d'acces.
|
||||||
|
/// </summary>
|
||||||
|
[JsonPropertyName("access_token")]
|
||||||
|
public string? AccessToken { get; set; }
|
||||||
|
}
|
||||||
|
}
|
||||||
34
Webzine.Business/DTOs/Spotify/SpotifyTrackDto.cs
Normal file
34
Webzine.Business/DTOs/Spotify/SpotifyTrackDto.cs
Normal file
@@ -0,0 +1,34 @@
|
|||||||
|
namespace Webzine.Business.DTOs.Spotify
|
||||||
|
{
|
||||||
|
using System.Text.Json.Serialization;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Objet track de spotify.
|
||||||
|
/// </summary>
|
||||||
|
public class SpotifyTrackDto
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Id de la track.
|
||||||
|
/// </summary>
|
||||||
|
[JsonPropertyName("id")]
|
||||||
|
public string Id { get; set; } = string.Empty;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Nom de la musique.
|
||||||
|
/// </summary>
|
||||||
|
[JsonPropertyName("name")]
|
||||||
|
public string Name { get; set; } = string.Empty;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Duree de la musique en millieseconde.
|
||||||
|
/// </summary>
|
||||||
|
[JsonPropertyName("duration_ms")]
|
||||||
|
public int DurationMs { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// urls spotify.
|
||||||
|
/// </summary>
|
||||||
|
[JsonPropertyName("external_urls")]
|
||||||
|
public SpotifyExternalUrlsDto? ExternalUrls { get; set; }
|
||||||
|
}
|
||||||
|
}
|
||||||
16
Webzine.Business/DTOs/Spotify/SpotifyTracksResponseDto.cs
Normal file
16
Webzine.Business/DTOs/Spotify/SpotifyTracksResponseDto.cs
Normal file
@@ -0,0 +1,16 @@
|
|||||||
|
namespace Webzine.Business.DTOs.Spotify
|
||||||
|
{
|
||||||
|
using System.Text.Json.Serialization;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Reponse spotify qui contient une liste de tracks.
|
||||||
|
/// </summary>
|
||||||
|
public class SpotifyTracksResponseDto
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Container qui contient plusieurs tracks.
|
||||||
|
/// </summary>
|
||||||
|
[JsonPropertyName("items")]
|
||||||
|
public List<SpotifyTrackDto> Items { get; set; } = new ();
|
||||||
|
}
|
||||||
|
}
|
||||||
128
Webzine.Business/Mappers/Spotify/SpotifyMapper.cs
Normal file
128
Webzine.Business/Mappers/Spotify/SpotifyMapper.cs
Normal file
@@ -0,0 +1,128 @@
|
|||||||
|
namespace Webzine.Business.Mappers.Spotify
|
||||||
|
{
|
||||||
|
using System.Globalization;
|
||||||
|
|
||||||
|
using Webzine.Business.DTOs.Spotify;
|
||||||
|
using Webzine.Entity;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Mapper pour transformer les objets de Spotify (DTOs) en Entity.
|
||||||
|
/// </summary>
|
||||||
|
public static class SpotifyMapper
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Permet d'ajouter ou de creer un style.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="styles">Dictionnaire string, Style.</param>
|
||||||
|
/// <param name="genre">Genre.</param>
|
||||||
|
/// <param name="nextStyleId">Id du style.</param>
|
||||||
|
/// <returns>Le style.</returns>
|
||||||
|
public static Style GetOrCreateStyle(Dictionary<string, Style> styles, string genre, ref int nextStyleId)
|
||||||
|
{
|
||||||
|
// On verifie si le genre est présent dans la liste de styles.
|
||||||
|
if (!styles.TryGetValue(genre, out var style))
|
||||||
|
{
|
||||||
|
// Creation d'un nouveau style.
|
||||||
|
style = new Style
|
||||||
|
{
|
||||||
|
IdStyle = nextStyleId++,
|
||||||
|
Libelle = genre,
|
||||||
|
Titres = new List<Titre>(),
|
||||||
|
};
|
||||||
|
|
||||||
|
// Ajout dans la liste.
|
||||||
|
styles.Add(style.Libelle, style);
|
||||||
|
}
|
||||||
|
|
||||||
|
return style;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Creation d'un nouvel artiste a l'aide des infos Spotify.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="artisteSpotify">Artiste spotify.</param>
|
||||||
|
/// <param name="stylesTitre">Style spotify.</param>
|
||||||
|
/// <param name="idArtiste">Id de l'artiste.</param>
|
||||||
|
/// <returns>Artiste.</returns>
|
||||||
|
public static Artiste ToArtiste(SpotifyArtistDto artisteSpotify, List<Style> stylesTitre, int idArtiste)
|
||||||
|
{
|
||||||
|
// Spotify ne possède pas de Biographie pour les artistes.
|
||||||
|
// On affiche donc son nom, et les styles qui lui sont associés.
|
||||||
|
return new Artiste
|
||||||
|
{
|
||||||
|
IdArtiste = idArtiste,
|
||||||
|
Nom = artisteSpotify.Name,
|
||||||
|
Biographie = $"{artisteSpotify.Name} est un artiste present sur Spotify, associe aux styles {string.Join(", ", stylesTitre.Select(s => s.Libelle))}.",
|
||||||
|
Titres = new List<Titre>(),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Permet de creer un titre depuis les donn<6E>es de Spotify.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="track">Titre de spotify.</param>
|
||||||
|
/// <param name="album">Album de spotify.</param>
|
||||||
|
/// <param name="artiste">Artiste mappe.</param>
|
||||||
|
/// <param name="stylesTitre">Style du titre.</param>
|
||||||
|
/// <param name="idTitre">Id du titre.</param>
|
||||||
|
/// <returns>Nouveau Titre.</returns>
|
||||||
|
public static Titre ToTitre(SpotifyTrackDto track, SpotifyAlbumDto album, Artiste artiste, List<Style> stylesTitre, int idTitre)
|
||||||
|
{
|
||||||
|
// Spotify ne fournit pas les elements suivants : Chronique, DateCreation, NbLectures, NbLikes.
|
||||||
|
return new Titre
|
||||||
|
{
|
||||||
|
IdTitre = idTitre,
|
||||||
|
IdArtiste = artiste.IdArtiste,
|
||||||
|
Artiste = artiste,
|
||||||
|
Libelle = track.Name,
|
||||||
|
Chronique = $"{track.Name} est un titre de {artiste.Nom}, issu de l'album {album.Name}. Cette fiche a ete generee depuis Spotify.",
|
||||||
|
DateCreation = DateTime.UtcNow,
|
||||||
|
DateSortie = ParseDate(album.ReleaseDate),
|
||||||
|
Duree = Math.Max(1, track.DurationMs / 1000),
|
||||||
|
UrlJaquette = album.Images.FirstOrDefault()?.Url,
|
||||||
|
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 = album.Name,
|
||||||
|
Commentaires = new List<Commentaire>(),
|
||||||
|
Styles = new List<Style>(stylesTitre),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
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;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Permet de transformer une date recu en chaine de caractere en type DateTime.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="value">Date en chaine de caractere.</param>
|
||||||
|
/// <returns>DateTime.</returns>
|
||||||
|
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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
156
Webzine.Business/Seeders/SeedDataSpotify.cs
Normal file
156
Webzine.Business/Seeders/SeedDataSpotify.cs
Normal file
@@ -0,0 +1,156 @@
|
|||||||
|
namespace Webzine.Business.Seeders;
|
||||||
|
|
||||||
|
using System.Net.Http.Headers;
|
||||||
|
using System.Net.Http.Json;
|
||||||
|
using System.Text;
|
||||||
|
|
||||||
|
using Microsoft.Extensions.Options;
|
||||||
|
|
||||||
|
using Webzine.Business.DTOs.Spotify;
|
||||||
|
using Webzine.Business.Mappers.Spotify;
|
||||||
|
using Webzine.Entity;
|
||||||
|
using Webzine.Entity.Fixtures;
|
||||||
|
|
||||||
|
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;
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task<SeedDataSet> GenererJeuDeDonneesAsync(CancellationToken cancellationToken = default)
|
||||||
|
{
|
||||||
|
// Verification des parametres pour l'acces a Spotify.
|
||||||
|
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>();
|
||||||
|
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)
|
||||||
|
{
|
||||||
|
var artistesSpotify = await this.GetAsync<SpotifySearchArtistsResponseDto>(
|
||||||
|
$"https://api.spotify.com/v1/search?q={Uri.EscapeDataString($"genre:\"{genre}\"")}&type=artist&market={this.options.Market}&limit={this.options.ArtistsPerGenre}",
|
||||||
|
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 => SpotifyMapper.GetOrCreateStyle(styles, g, ref nextStyleId))
|
||||||
|
.DistinctBy(s => s.IdStyle)
|
||||||
|
.ToList();
|
||||||
|
|
||||||
|
var artiste = SpotifyMapper.ToArtiste(artisteSpotify, stylesTitre, nextArtistId++);
|
||||||
|
artistes.Add(artiste);
|
||||||
|
|
||||||
|
var albums = await this.GetAsync<SpotifyAlbumsResponseDto>(
|
||||||
|
$"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<SpotifyTracksResponseDto>(
|
||||||
|
$"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 = SpotifyMapper.ToTitre(track, album, artiste, stylesTitre, nextTitreId++);
|
||||||
|
|
||||||
|
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,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Recuperation du token d'access a Spotify.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="cancellationToken">Permet d'annuler la requete HTTP en cas de probleme.</param>
|
||||||
|
/// <returns>Le token d'acces a spotify.</returns>
|
||||||
|
/// <exception cref="InvalidOperationException">Le token spotify est introuvable.</exception>
|
||||||
|
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<SpotifyTokenResponseDto>(cancellationToken: cancellationToken);
|
||||||
|
return payload?.AccessToken ?? throw new InvalidOperationException("Token Spotify introuvable.");
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Recuperation d'information depuis spotify, en formattant
|
||||||
|
/// la requete avec le Bearer Token.
|
||||||
|
/// </summary>
|
||||||
|
/// <typeparam name="T">DTOs Spotify.</typeparam>
|
||||||
|
/// <param name="url">URL Spotify.</param>
|
||||||
|
/// <param name="token">Token Bearer pour authorisation d'acces a l'api.</param>
|
||||||
|
/// <param name="cancellationToken">Permet d'annuler la requete.</param>
|
||||||
|
/// <returns>Reponse Spotify deserialiser.</returns>
|
||||||
|
private async Task<T?> GetAsync<T>(string url, string token, CancellationToken cancellationToken)
|
||||||
|
{
|
||||||
|
// Formattage de la requete HTTP avec le Bearer token.
|
||||||
|
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);
|
||||||
|
}
|
||||||
|
}
|
||||||
28
Webzine.Business/Seeders/SpotifySeederOptions.cs
Normal file
28
Webzine.Business/Seeders/SpotifySeederOptions.cs
Normal file
@@ -0,0 +1,28 @@
|
|||||||
|
namespace Webzine.Business.Seeders;
|
||||||
|
|
||||||
|
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;
|
||||||
|
}
|
||||||
@@ -8,6 +8,7 @@
|
|||||||
|
|
||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
<PackageReference Include="Faker.Net" Version="2.0.163" />
|
<PackageReference Include="Faker.Net" Version="2.0.163" />
|
||||||
|
<PackageReference Include="Microsoft.Extensions.Options" Version="10.0.0" />
|
||||||
<PackageReference Include="NLog" Version="6.1.1" />
|
<PackageReference Include="NLog" Version="6.1.1" />
|
||||||
<PackageReference Include="StyleCop.Analyzers" Version="1.1.118">
|
<PackageReference Include="StyleCop.Analyzers" Version="1.1.118">
|
||||||
<PrivateAssets>all</PrivateAssets>
|
<PrivateAssets>all</PrivateAssets>
|
||||||
@@ -21,4 +22,8 @@
|
|||||||
</Content>
|
</Content>
|
||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
|
|
||||||
|
<ItemGroup>
|
||||||
|
<ProjectReference Include="..\Webzine.Entity\Webzine.Entity.csproj" />
|
||||||
|
</ItemGroup>
|
||||||
|
|
||||||
</Project>
|
</Project>
|
||||||
|
|||||||
172
Webzine.Documentation/SpotifySeederRecap.md
Normal file
172
Webzine.Documentation/SpotifySeederRecap.md
Normal file
@@ -0,0 +1,172 @@
|
|||||||
|
# Récapitulatif des modifications pour le seeder Spotify
|
||||||
|
|
||||||
|
## Objectif
|
||||||
|
Ajout d’un seeder capable de récupérer de vraies données depuis l’API Spotify pour alimenter l’application avec :
|
||||||
|
- des artistes
|
||||||
|
- des styles
|
||||||
|
- des titres
|
||||||
|
- des commentaires générés localement autour de ces vraies données
|
||||||
|
|
||||||
|
## 1. Nouveau seeder Spotify
|
||||||
|
|
||||||
|
### Fichier ajouté
|
||||||
|
[Webzine.Entity/Fixtures/SeedDataSpotify.cs](c:\Users\lmasi\Documents\DIIAGE\P4\Webzine\Webzine.Entity\Fixtures\SeedDataSpotify.cs)
|
||||||
|
|
||||||
|
### Ce qui a été fait
|
||||||
|
- création de la classe `SeedDataSpotify`
|
||||||
|
- ajout de la classe de configuration `SpotifySeederOptions`
|
||||||
|
- appel à l’API Spotify via `HttpClient`
|
||||||
|
- authentification avec le flux OAuth `client_credentials`
|
||||||
|
- récupération d’artistes par genre
|
||||||
|
- récupération des albums de chaque artiste
|
||||||
|
- récupération des titres de chaque album
|
||||||
|
- mapping des données Spotify vers les entités métier :
|
||||||
|
- `Artiste`
|
||||||
|
- `Style`
|
||||||
|
- `Titre`
|
||||||
|
- `Commentaire`
|
||||||
|
- génération de commentaires locaux via `SeedDataLocal`
|
||||||
|
- transformation des genres Spotify en `Style`
|
||||||
|
- génération de valeurs applicatives cohérentes :
|
||||||
|
- `Chronique`
|
||||||
|
- `NbLectures`
|
||||||
|
- `NbLikes`
|
||||||
|
- `DateSortie`
|
||||||
|
- `UrlJaquette`
|
||||||
|
- `UrlEcoute`
|
||||||
|
|
||||||
|
### Comportement
|
||||||
|
Le seeder produit un jeu de données exploitable par :
|
||||||
|
- le mode base de données
|
||||||
|
- le mode mémoire
|
||||||
|
|
||||||
|
## 2. Création d’un conteneur commun pour le seeding
|
||||||
|
|
||||||
|
### Fichier ajouté
|
||||||
|
[Webzine.Entity/Fixtures/SeedDataSet.cs](c:\Users\lmasi\Documents\DIIAGE\P4\Webzine\Webzine.Entity\Fixtures\SeedDataSet.cs)
|
||||||
|
|
||||||
|
### Ce qui a été fait
|
||||||
|
Ajout d’un objet `SeedDataSet` contenant :
|
||||||
|
- `Artistes`
|
||||||
|
- `Styles`
|
||||||
|
- `Titres`
|
||||||
|
- `Commentaires`
|
||||||
|
|
||||||
|
### Pourquoi
|
||||||
|
Cela permet d’unifier le résultat du seeding, qu’il vienne :
|
||||||
|
- du générateur local
|
||||||
|
- de Spotify
|
||||||
|
|
||||||
|
## 3. Adaptation du repository DB
|
||||||
|
|
||||||
|
### Fichier modifié
|
||||||
|
[Webzine.Repository/DbEntityRepository.cs](c:\Users\lmasi\Documents\DIIAGE\P4\Webzine\Webzine.Repository\DbEntityRepository.cs)
|
||||||
|
|
||||||
|
### Ce qui a été fait
|
||||||
|
Modification de `SeedBaseDeDonnees` pour qu’elle puisse :
|
||||||
|
- continuer à générer les données locales comme avant
|
||||||
|
- ou accepter un `SeedDataSet` externe déjà préparé
|
||||||
|
|
||||||
|
### Résultat
|
||||||
|
Le repository peut maintenant insérer en base :
|
||||||
|
- soit les données fake locales
|
||||||
|
- soit les données réelles récupérées depuis Spotify
|
||||||
|
|
||||||
|
## 4. Branchement du seeder dans le démarrage de l’application
|
||||||
|
|
||||||
|
### Fichier modifié
|
||||||
|
[Webzine.WebApplication/Program.cs](c:\Users\lmasi\Documents\DIIAGE\P4\Webzine\Webzine.WebApplication\Program.cs)
|
||||||
|
|
||||||
|
### Ce qui a été fait
|
||||||
|
- enregistrement de la configuration `SpotifySeeder`
|
||||||
|
- enregistrement de `SeedDataSpotify` via `AddHttpClient`
|
||||||
|
- lecture de `SeederType`
|
||||||
|
- ajout de la logique :
|
||||||
|
- si `Seeder = Spotify`, on appelle `SeedDataSpotify`
|
||||||
|
- sinon on garde `SeedDataLocal`
|
||||||
|
|
||||||
|
### Cas couverts
|
||||||
|
- `Repository = Db`
|
||||||
|
- `Repository = Local`
|
||||||
|
|
||||||
|
Donc le mode Spotify fonctionne aussi bien :
|
||||||
|
- avec SQLite/PostgreSQL
|
||||||
|
- qu’avec le store mémoire
|
||||||
|
|
||||||
|
## 5. Ajout de la configuration Spotify
|
||||||
|
|
||||||
|
### Fichiers modifiés
|
||||||
|
- [Webzine.WebApplication/appsettings.json](c:\Users\lmasi\Documents\DIIAGE\P4\Webzine\Webzine.WebApplication\appsettings.json)
|
||||||
|
- [Webzine.WebApplication/appsettings.Development.json](c:\Users\lmasi\Documents\DIIAGE\P4\Webzine\Webzine.WebApplication\appsettings.Development.json)
|
||||||
|
- [Webzine.WebApplication/appsettings.Production.json](c:\Users\lmasi\Documents\DIIAGE\P4\Webzine\Webzine.WebApplication\appsettings.Production.json)
|
||||||
|
|
||||||
|
### Ce qui a été ajouté
|
||||||
|
Section `SpotifySeeder` avec :
|
||||||
|
- `ClientId`
|
||||||
|
- `ClientSecret`
|
||||||
|
- `Market`
|
||||||
|
- `Genres`
|
||||||
|
- `ArtistsPerGenre`
|
||||||
|
- `AlbumsPerArtist`
|
||||||
|
- `TracksPerAlbum`
|
||||||
|
- `MaxCommentsPerTrack`
|
||||||
|
|
||||||
|
### Utilisation
|
||||||
|
Pour activer le seeder Spotify :
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"Seeder": "Spotify"
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
Et renseigner :
|
||||||
|
|
||||||
|
```json
|
||||||
|
"SpotifySeeder": {
|
||||||
|
"ClientId": "...",
|
||||||
|
"ClientSecret": "..."
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## 6. Dépendance ajoutée
|
||||||
|
|
||||||
|
### Fichier modifié
|
||||||
|
[Webzine.Entity/Webzine.Entity.csproj](c:\Users\lmasi\Documents\DIIAGE\P4\Webzine\Webzine.Entity\Webzine.Entity.csproj)
|
||||||
|
|
||||||
|
### Ce qui a été fait
|
||||||
|
Ajout du package :
|
||||||
|
- `Microsoft.Extensions.Options`
|
||||||
|
|
||||||
|
### Pourquoi
|
||||||
|
Pour injecter proprement `SpotifySeederOptions` dans `SeedDataSpotify`
|
||||||
|
|
||||||
|
## 7. Effet sur les données de l’application
|
||||||
|
|
||||||
|
Avec le seeder Spotify, les titres créés utilisent maintenant :
|
||||||
|
- un vrai nom de titre
|
||||||
|
- un vrai artiste
|
||||||
|
- un vrai album
|
||||||
|
- une vraie jaquette Spotify
|
||||||
|
- une vraie URL Spotify
|
||||||
|
- des styles dérivés des genres Spotify
|
||||||
|
|
||||||
|
Les commentaires restent générés localement pour enrichir l’affichage.
|
||||||
|
|
||||||
|
## 8. Point d’attention
|
||||||
|
|
||||||
|
Je n’ai pas pu valider la compilation complète dans cet environnement à cause d’un problème SDK/.NET local lié au workload resolver, pas à une erreur C# remontée sur les fichiers modifiés.
|
||||||
|
|
||||||
|
## 9. Fichiers impactés
|
||||||
|
|
||||||
|
### Ajoutés
|
||||||
|
- [Webzine.Entity/Fixtures/SeedDataSpotify.cs](c:\Users\lmasi\Documents\DIIAGE\P4\Webzine\Webzine.Entity\Fixtures\SeedDataSpotify.cs)
|
||||||
|
- [Webzine.Entity/Fixtures/SeedDataSet.cs](c:\Users\lmasi\Documents\DIIAGE\P4\Webzine\Webzine.Entity\Fixtures\SeedDataSet.cs)
|
||||||
|
|
||||||
|
### Modifiés
|
||||||
|
- [Webzine.Repository/DbEntityRepository.cs](c:\Users\lmasi\Documents\DIIAGE\P4\Webzine\Webzine.Repository\DbEntityRepository.cs)
|
||||||
|
- [Webzine.WebApplication/Program.cs](c:\Users\lmasi\Documents\DIIAGE\P4\Webzine\Webzine.WebApplication\Program.cs)
|
||||||
|
- [Webzine.WebApplication/appsettings.json](c:\Users\lmasi\Documents\DIIAGE\P4\Webzine\Webzine.WebApplication\appsettings.json)
|
||||||
|
- [Webzine.WebApplication/appsettings.Development.json](c:\Users\lmasi\Documents\DIIAGE\P4\Webzine\Webzine.WebApplication\appsettings.Development.json)
|
||||||
|
- [Webzine.WebApplication/appsettings.Production.json](c:\Users\lmasi\Documents\DIIAGE\P4\Webzine\Webzine.WebApplication\appsettings.Production.json)
|
||||||
|
- [Webzine.Entity/Webzine.Entity.csproj](c:\Users\lmasi\Documents\DIIAGE\P4\Webzine\Webzine.Entity\Webzine.Entity.csproj)
|
||||||
27
Webzine.Entity/Fixtures/SeedDataSet.cs
Normal file
27
Webzine.Entity/Fixtures/SeedDataSet.cs
Normal file
@@ -0,0 +1,27 @@
|
|||||||
|
namespace Webzine.Entity.Fixtures;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Représente un jeu de données prêt à être injecté dans les différents stockages de l'application.
|
||||||
|
/// </summary>
|
||||||
|
public class SeedDataSet
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Obtient ou définit les artistes générés.
|
||||||
|
/// </summary>
|
||||||
|
public List<Artiste> Artistes { get; set; } = new ();
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Obtient ou définit les styles générés.
|
||||||
|
/// </summary>
|
||||||
|
public List<Style> Styles { get; set; } = new ();
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Obtient ou définit les titres générés.
|
||||||
|
/// </summary>
|
||||||
|
public List<Titre> Titres { get; set; } = new ();
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Obtient ou définit les commentaires générés.
|
||||||
|
/// </summary>
|
||||||
|
public List<Commentaire> Commentaires { get; set; } = new ();
|
||||||
|
}
|
||||||
@@ -1,337 +0,0 @@
|
|||||||
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; }
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -9,7 +9,6 @@
|
|||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
<PackageReference Include="Bogus" Version="35.6.5" />
|
<PackageReference Include="Bogus" Version="35.6.5" />
|
||||||
<PackageReference Include="Faker.Net" Version="2.0.163" />
|
<PackageReference Include="Faker.Net" Version="2.0.163" />
|
||||||
<PackageReference Include="Microsoft.Extensions.Options" Version="10.0.0" />
|
|
||||||
<PackageReference Include="NLog" Version="6.1.1" />
|
<PackageReference Include="NLog" Version="6.1.1" />
|
||||||
<PackageReference Include="StyleCop.Analyzers" Version="1.1.118">
|
<PackageReference Include="StyleCop.Analyzers" Version="1.1.118">
|
||||||
<PrivateAssets>all</PrivateAssets>
|
<PrivateAssets>all</PrivateAssets>
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
// L'erreur SA1200 (ordre des using directives) est desactivée pour Program.cs
|
// L'erreur SA1200 (ordre des using directives) est desactivee pour Program.cs
|
||||||
#pragma warning disable SA1200
|
#pragma warning disable SA1200
|
||||||
|
|
||||||
using Microsoft.EntityFrameworkCore;
|
using Microsoft.EntityFrameworkCore;
|
||||||
@@ -6,6 +6,7 @@ using Microsoft.EntityFrameworkCore;
|
|||||||
using NLog;
|
using NLog;
|
||||||
using NLog.Web;
|
using NLog.Web;
|
||||||
|
|
||||||
|
using Webzine.Business.Seeders;
|
||||||
using Webzine.EntitiesContext;
|
using Webzine.EntitiesContext;
|
||||||
using Webzine.Entity;
|
using Webzine.Entity;
|
||||||
using Webzine.Entity.Fixtures;
|
using Webzine.Entity.Fixtures;
|
||||||
@@ -22,8 +23,7 @@ try
|
|||||||
{
|
{
|
||||||
var builder = WebApplication.CreateBuilder(args);
|
var builder = WebApplication.CreateBuilder(args);
|
||||||
|
|
||||||
// Ajoute les services necessaires pour permettre l'utilisation des
|
// Ajoute les services necessaires pour permettre l'utilisation des controllers avec des vues.
|
||||||
// controllers avec des vues.
|
|
||||||
builder.Services.AddControllersWithViews()
|
builder.Services.AddControllersWithViews()
|
||||||
|
|
||||||
// Ajoute la compilation des vues lors de l'execution de l'application.
|
// Ajoute la compilation des vues lors de l'execution de l'application.
|
||||||
@@ -34,11 +34,14 @@ try
|
|||||||
// NLog: Setup NLog for Dependency injection
|
// NLog: Setup NLog for Dependency injection
|
||||||
builder.Logging.ClearProviders();
|
builder.Logging.ClearProviders();
|
||||||
builder.Host.UseNLog();
|
builder.Host.UseNLog();
|
||||||
|
builder.Services.Configure<SpotifySeederOptions>(builder.Configuration.GetSection("SpotifySeeder"));
|
||||||
|
builder.Services.AddHttpClient<SeedDataSpotify>();
|
||||||
|
|
||||||
// En fonction de la configuration, utilise soit les repositories basés sur une base de données, soit les repositories basés sur des listes locales.
|
// En fonction de la configuration, utilise soit les repositories bases sur une base de donnees, soit les repositories bases sur des listes locales.
|
||||||
var repositoryType = builder.Configuration.GetValue<RepositoryType>("Repository");
|
var repositoryType = builder.Configuration.GetValue<RepositoryType>("Repository");
|
||||||
var seederType = builder.Configuration.GetValue<SeederType>("Seeder");
|
var seederType = builder.Configuration.GetValue<SeederType>("Seeder");
|
||||||
var shouldSeed = args.Contains("--seed");
|
var shouldSeed = args.Contains("--seed");
|
||||||
|
|
||||||
if (repositoryType == RepositoryType.Db)
|
if (repositoryType == RepositoryType.Db)
|
||||||
{
|
{
|
||||||
if (builder.Environment.IsProduction())
|
if (builder.Environment.IsProduction())
|
||||||
@@ -68,7 +71,7 @@ try
|
|||||||
}
|
}
|
||||||
|
|
||||||
// https://learn.microsoft.com/fr-fr/aspnet/core/performance/response-compression?view=aspnetcore-10.0#configuration
|
// https://learn.microsoft.com/fr-fr/aspnet/core/performance/response-compression?view=aspnetcore-10.0#configuration
|
||||||
// Ajoute le service de compression des réponses HTTP pour réduire la taille des données envoyées au client et améliorer les performances de l'application.
|
// Ajoute le service de compression des reponses HTTP pour reduire la taille des donnees envoyees au client et ameliorer les performances de l'application.
|
||||||
builder.Services.AddResponseCompression();
|
builder.Services.AddResponseCompression();
|
||||||
|
|
||||||
var app = builder.Build();
|
var app = builder.Build();
|
||||||
@@ -90,7 +93,9 @@ try
|
|||||||
}
|
}
|
||||||
else if (seederType == SeederType.Spotify)
|
else if (seederType == SeederType.Spotify)
|
||||||
{
|
{
|
||||||
// Seed à l'aide de l'API Spotify.
|
var spotifySeeder = scope.ServiceProvider.GetRequiredService<SeedDataSpotify>();
|
||||||
|
var jeuDeDonnees = await spotifySeeder.GenererJeuDeDonneesAsync();
|
||||||
|
repo.SeedBaseDeDonnees(jeuDeDonnees);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -123,8 +128,7 @@ try
|
|||||||
|
|
||||||
app.UseResponseCompression();
|
app.UseResponseCompression();
|
||||||
|
|
||||||
// Active la possibilité de servir des fichiers statiques presents dans
|
// Active la possibilite de servir des fichiers statiques presents dans le dossier wwwroot.
|
||||||
// le dossier wwwroot.
|
|
||||||
app.UseStaticFiles(new StaticFileOptions
|
app.UseStaticFiles(new StaticFileOptions
|
||||||
{
|
{
|
||||||
OnPrepareResponse = ctx =>
|
OnPrepareResponse = ctx =>
|
||||||
@@ -137,7 +141,7 @@ try
|
|||||||
// Active le middleware permettant le routage des requetes entrantes.
|
// Active le middleware permettant le routage des requetes entrantes.
|
||||||
app.UseRouting();
|
app.UseRouting();
|
||||||
|
|
||||||
// Appelle les routes définies dans le dossier Extensions.
|
// Appelle les routes definies dans le dossier Extensions.
|
||||||
app.MapCustomRoutes();
|
app.MapCustomRoutes();
|
||||||
|
|
||||||
app.Run();
|
app.Run();
|
||||||
|
|||||||
@@ -34,6 +34,7 @@
|
|||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
|
|
||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
|
<ProjectReference Include="..\Webzine.Business\Webzine.Business.csproj" />
|
||||||
<ProjectReference Include="..\Webzine.EntitiesContext\Webzine.EntitiesContext.csproj" />
|
<ProjectReference Include="..\Webzine.EntitiesContext\Webzine.EntitiesContext.csproj" />
|
||||||
<ProjectReference Include="..\Webzine.Entity\Webzine.Entity.csproj" />
|
<ProjectReference Include="..\Webzine.Entity\Webzine.Entity.csproj" />
|
||||||
<ProjectReference Include="..\Webzine.Repository\Webzine.Repository.csproj" />
|
<ProjectReference Include="..\Webzine.Repository\Webzine.Repository.csproj" />
|
||||||
|
|||||||
@@ -17,10 +17,10 @@
|
|||||||
"ClientId": "754247cf73e047bf9d6acf44977b6c4a",
|
"ClientId": "754247cf73e047bf9d6acf44977b6c4a",
|
||||||
"ClientSecret": "0e674c5ac1c249f1af711fb08a919a02",
|
"ClientSecret": "0e674c5ac1c249f1af711fb08a919a02",
|
||||||
"Market": "FR",
|
"Market": "FR",
|
||||||
"Genres": [ "rock", "pop", "jazz", "hip hop", "electronic", "metal" ],
|
"Genres": [ "rock", "pop", "jazz", "hip hop", "electronic", "metal", "hyper pop" ],
|
||||||
"ArtistsPerGenre": 4,
|
"ArtistsPerGenre": 50,
|
||||||
"AlbumsPerArtist": 2,
|
"AlbumsPerArtist": 20,
|
||||||
"TracksPerAlbum": 4,
|
"TracksPerAlbum": 40,
|
||||||
"MaxCommentsPerTrack": 3
|
"MaxCommentsPerTrack": 3
|
||||||
},
|
},
|
||||||
"AllowedHosts": "*"
|
"AllowedHosts": "*"
|
||||||
|
|||||||
Reference in New Issue
Block a user