This commit is contained in:
Loic Masi
2026-04-05 18:42:19 +02:00
35 changed files with 872 additions and 41 deletions

View 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 ();
}
}

View 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 ();
}
}

View 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 ();
}
}

View 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 ();
}
}

View 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; }
}

View 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; }
}

View File

@@ -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; }
}
}

View 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; }
}
}

View 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; }
}
}

View 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 ();
}
}

View 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;
}
}
}

View File

@@ -0,0 +1,162 @@
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;
}
/// <summary>
/// Générer les données de la base a l'aide des données de spotify.
/// </summary>
/// <param name="cancellationToken">Cancellation token.</param>
/// <returns>Jeu de données.</returns>
/// <exception cref="InvalidOperationException">Erreur de connexion a spotify.</exception>
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>();
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).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);
}
}

View 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;
}

View File

@@ -8,7 +8,7 @@
<ItemGroup>
<PackageReference Include="Faker.Net" Version="2.0.163" />
<PackageReference Include="Microsoft.Extensions.Logging.Abstractions" Version="10.0.5" />
<PackageReference Include="Microsoft.Extensions.Options" Version="10.0.0" />
<PackageReference Include="NLog" Version="6.1.1" />
<PackageReference Include="StyleCop.Analyzers" Version="1.1.118">
<PrivateAssets>all</PrivateAssets>
@@ -26,7 +26,7 @@
<ProjectReference Include="..\Webzine.Business.Contracts\Webzine.Business.Contracts.csproj" />
<ProjectReference Include="..\Webzine.Entity\Webzine.Entity.csproj" />
<ProjectReference Include="..\Webzine.Repository.Contracts\Webzine.Repository.Contracts.csproj" />
<PackageReference Include="Microsoft.Extensions.Logging.Abstractions" Version="10.0.0-preview.1.25080.5" />
<PackageReference Include="Microsoft.Extensions.Logging.Abstractions" Version="10.0.5" />
</ItemGroup>
</Project>

View File

@@ -0,0 +1,172 @@
# Récapitulatif des modifications pour le seeder Spotify
## Objectif
Ajout dun seeder capable de récupérer de vraies données depuis lAPI Spotify pour alimenter lapplication 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 à lAPI Spotify via `HttpClient`
- authentification avec le flux OAuth `client_credentials`
- récupération dartistes 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 dun 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 dun objet `SeedDataSet` contenant :
- `Artistes`
- `Styles`
- `Titres`
- `Commentaires`
### Pourquoi
Cela permet dunifier le résultat du seeding, quil 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 quelle 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 lapplication
### 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
- quavec 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 lapplication
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 laffichage.
## 8. Point dattention
Je nai pas pu valider la compilation complète dans cet environnement à cause dun 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)

View 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 ();
}

View File

@@ -1,5 +0,0 @@
namespace Webzine.Entity.Fixtures;
public class SeedDataSpotify
{
}

View File

@@ -217,8 +217,7 @@ namespace Webzine.Repository
.AsNoTracking()
.OrderBy(a => a.Nom)
.Include(t => t.Titres)
.Skip(offset)
.Take(limit);
.Paginate(offset, limit);
this.logger.LogDebug("Page {PageNumber} d'artistes récupérée avec {PageSize} artistes par page.", offset, limit);
return artistes;
}

View File

@@ -103,8 +103,7 @@ public class DbCommentaireRepository : ICommentaireRepository
.AsNoTracking()
.Include(c => c.Titre)
.OrderByDescending(c => c.DateCreation)
.Skip(offset)
.Take(limit);
.Paginate(offset, limit);
return commentaires;
}

View File

@@ -32,6 +32,7 @@ namespace Webzine.Repository
/// <param name="minStyles">Nombre min de style.</param>
/// <param name="maxStyles">Nombre mac de style.</param>
public void SeedBaseDeDonnees(
SeedDataSet? jeuDeDonnees = null,
int nbArtistes = 100,
int nbTitres = 500,
int minStyles = 15,
@@ -45,6 +46,16 @@ namespace Webzine.Repository
return;
}
if (jeuDeDonnees is not null)
{
this.context.Artistes.AddRange(jeuDeDonnees.Artistes);
this.context.Styles.AddRange(jeuDeDonnees.Styles);
this.context.Titres.AddRange(jeuDeDonnees.Titres);
this.context.Commentaires.AddRange(jeuDeDonnees.Commentaires);
this.context.SaveChanges();
return;
}
List<Artiste> artistes = SeedDataLocal.GenererListeArtiste(nbArtistes);
List<Style> styles = SeedDataLocal.GenererListeStyle(minStyles, maxStyles);

View File

@@ -191,8 +191,7 @@ public class DbStyleRepository : IStyleRepository
var styles = this.context.Styles
.AsNoTracking()
.OrderBy(s => s.Libelle)
.Skip(offset)
.Take(limit);
.Paginate(offset, limit);
this.logger.LogDebug("La liste paginée de styles a été récupérée.");
return styles;

View File

@@ -107,8 +107,7 @@ public class DbTitreRepository : ITitreRepository
.ThenBy(t => t.Libelle)
.Include(t => t.Artiste)
.Include(t => t.Styles)
.Skip(offset)
.Take(limit)
.Paginate(offset, limit)
.AsNoTracking();
return titres;

View File

@@ -112,8 +112,7 @@ namespace Webzine.Repository
{
return this.dataStore.Artistes
.OrderBy(a => a.Nom)
.Skip(offset)
.Take(limit)
.Paginate(offset, limit)
.ToList();
}
}

View File

@@ -63,8 +63,7 @@ namespace Webzine.Repository
{
return this.dataStore.Commentaires
.OrderByDescending(c => c.DateCreation)
.Skip(offset)
.Take(limit);
.Paginate(offset, limit);
}
/// <inheritdoc/>

View File

@@ -77,8 +77,7 @@ public class LocalStyleRepository : IStyleRepository
{
return this.dataStore.Styles
.OrderBy(s => s.Libelle)
.Skip(offset)
.Take(limit)
.Paginate(offset, limit)
.ToList();
}
}

View File

@@ -50,8 +50,7 @@ public class LocalTitreRepository : ITitreRepository
return this.dataStore.Titres
.OrderByDescending(t => t.DateCreation)
.ThenBy(t => t.Libelle)
.Skip(offset)
.Take(limit);
.Paginate(offset, limit);
}
/// <inheritdoc/>

View File

@@ -0,0 +1,56 @@
namespace Webzine.Repository;
/// <summary>
/// Fournit des méthodes génériques pour paginer des collections dans la couche Repository.
/// </summary>
public static class RepositoryPaginationExtensions
{
/// <summary>
/// Retourne une portion paginée d'une requête.
/// </summary>
/// <typeparam name="T">Type des éléments paginés.</typeparam>
/// <param name="source">Source à paginer.</param>
/// <param name="offset">Nombre d'éléments à ignorer.</param>
/// <param name="limit">Nombre maximal d'éléments à retourner.</param>
/// <returns>La requête paginée.</returns>
public static IQueryable<T> Paginate<T>(this IQueryable<T> source, int offset, int limit)
{
ArgumentNullException.ThrowIfNull(source);
ValidatePaginationArguments(offset, limit);
return source
.Skip(offset)
.Take(limit);
}
/// <summary>
/// Retourne une portion paginée d'une collection en mémoire.
/// </summary>
/// <typeparam name="T">Type des éléments paginés.</typeparam>
/// <param name="source">Source à paginer.</param>
/// <param name="offset">Nombre d'éléments à ignorer.</param>
/// <param name="limit">Nombre maximal d'éléments à retourner.</param>
/// <returns>La collection paginée.</returns>
public static IEnumerable<T> Paginate<T>(this IEnumerable<T> source, int offset, int limit)
{
ArgumentNullException.ThrowIfNull(source);
ValidatePaginationArguments(offset, limit);
return source
.Skip(offset)
.Take(limit);
}
private static void ValidatePaginationArguments(int offset, int limit)
{
if (offset < 0)
{
throw new ArgumentOutOfRangeException(nameof(offset), "L'offset doit être supérieur ou égal à 0.");
}
if (limit <= 0)
{
throw new ArgumentOutOfRangeException(nameof(limit), "La limite doit être strictement supérieure à 0.");
}
}
}

View File

@@ -64,6 +64,7 @@ namespace Webzine.WebApplication.Controllers
NbLikes = titre.NbLikes,
UrlJaquette = titre.UrlJaquette,
UrlEcoute = titre.UrlEcoute,
UrlEmbedEcoute = BuildSpotifyEmbedUrl(titre.UrlEcoute),
ArtisteNom = titre.Artiste.Nom,
Styles = titre.Styles,
Commentaires = titre.Commentaires,
@@ -163,5 +164,23 @@ namespace Webzine.WebApplication.Controllers
Duree = titre.Duree,
};
}
/// <summary>
///
/// </summary>
/// <param name="urlEcoute"></param>
/// <returns></returns>
private static string? BuildSpotifyEmbedUrl(string? urlEcoute)
{
if (string.IsNullOrWhiteSpace(urlEcoute))
{
return null;
}
var trackId = urlEcoute.Split('/').LastOrDefault();
return string.IsNullOrWhiteSpace(trackId)
? null
: $"https://open.spotify.com/embed/track/{trackId}";
}
}
}

View File

@@ -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
using Microsoft.EntityFrameworkCore;
@@ -8,6 +8,7 @@ using NLog.Web;
using Webzine.Business;
using Webzine.Business.Contracts;
using Webzine.Business.Seeders;
using Webzine.EntitiesContext;
using Webzine.Entity;
using Webzine.Entity.Fixtures;
@@ -32,7 +33,11 @@ try
{
// options.Filters.Add<GlobalExceptionFilter>();
options.Filters.Add<ValidationActionFilter>();
})
options.Filters.Add<GlobalExceptionFilter>();
});
// Ajoute les services necessaires pour permettre l'utilisation des controllers avec des vues.
builder.Services.AddControllersWithViews()
// Ajoute la compilation des vues lors de l'execution de l'application.
// Cela nous evite de recompiler l'application a chaque modification de vue.
@@ -42,6 +47,8 @@ try
// NLog: Setup NLog for Dependency injection
builder.Logging.ClearProviders();
builder.Host.UseNLog();
builder.Services.Configure<SpotifySeederOptions>(builder.Configuration.GetSection("SpotifySeeder"));
builder.Services.AddHttpClient<SeedDataSpotify>();
builder.Services.Configure<EfPerformanceOptions>(options =>
{
@@ -50,9 +57,11 @@ try
builder.Services.AddSingleton<EfSlowQueryInterceptor>();
// 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 seederType = builder.Configuration.GetValue<SeederType>("Seeder");
var shouldSeed = args.Contains("--seed");
if (repositoryType == RepositoryType.Db)
{
if (builder.Environment.IsProduction())
@@ -96,7 +105,7 @@ try
builder.Services.AddScoped<GlobalExceptionFilter>();
// 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();
var app = builder.Build();
@@ -122,7 +131,9 @@ try
}
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);
}
}
}
@@ -155,8 +166,7 @@ try
app.UseResponseCompression();
// Active la possibilité de servir des fichiers statiques presents dans
// le dossier wwwroot.
// Active la possibilite de servir des fichiers statiques presents dans le dossier wwwroot.
app.UseStaticFiles(new StaticFileOptions
{
OnPrepareResponse = ctx =>
@@ -169,7 +179,7 @@ try
// Active le middleware permettant le routage des requetes entrantes.
app.UseRouting();
// Appelle les routes définies dans le dossier Extensions.
// Appelle les routes definies dans le dossier Extensions.
app.MapCustomRoutes();
app.Run();

View File

@@ -42,6 +42,11 @@ public class TitreContent
/// </summary>
public string UrlEcoute { get; set; }
/// <summary>
/// Définit l'url du lecteur embarqué du titre.
/// </summary>
public string? UrlEmbedEcoute { get; set; }
/// <summary>
/// Définit le nom de l'artiste associé au titre.
/// </summary>

View File

@@ -60,7 +60,7 @@
@foreach (var style in titre.Styles)
{
<a asp-controller="Titre"
<a asp-controller="Titres"
asp-action="Style"
asp-route-id="@style.Libelle"
class="text-decoration-none me-1">

View File

@@ -94,14 +94,28 @@
</div>
<!-- LECTEUR -->
@if (!string.IsNullOrEmpty(Model.Details.UrlEcoute))
@if (!string.IsNullOrEmpty(Model.Details.UrlEmbedEcoute))
{
<div class="mt-4">
<iframe width="100%" height="315"
src="@Model.Details.UrlEcoute"
title="Lecteur"
allowfullscreen>
<h4 class="mb-3">Ecouter le titre</h4>
<iframe src="@Model.Details.UrlEmbedEcoute"
width="100%"
height="152"
title="Lecteur Spotify"
frameborder="0"
allowfullscreen
allow="autoplay; clipboard-write; encrypted-media; fullscreen; picture-in-picture"
loading="lazy">
</iframe>
@if (!string.IsNullOrEmpty(Model.Details.UrlEcoute))
{
<p class="mt-2 mb-0">
<a href="@Model.Details.UrlEcoute" target="_blank" rel="noopener noreferrer">
Ouvrir dans Spotify
</a>
</p>
}
</div>
}
@@ -196,4 +210,4 @@
</div>
</div>
</div>

View File

@@ -1,4 +1,8 @@
{
"Seeder": "Local",
"Repository": "Db"
"Seeder": "Spotify",
"Repository": "Db",
"SpotifySeeder": {
"ClientId": "",
"ClientSecret": ""
}
}

View File

@@ -1,4 +1,8 @@
{
"Seeder": "Local",
"Repository": "Db"
}
"Repository": "Db",
"SpotifySeeder": {
"ClientId": "",
"ClientSecret": ""
}
}

View File

@@ -14,6 +14,16 @@
"SqliteConnection": "Data Source=Data/webzine.sqlite",
"PostGreSQLConnection": "Host=localhost;Port=5432;Username=admin;Password=admin123;Database=webzine_db"
},
"SpotifySeeder": {
"ClientId": "",
"ClientSecret": "",
"Market": "FR",
"Genres": [ "rock", "pop", "jazz", "hip hop", "electronic", "metal", "hyper pop", "shatta", "french%20rap" ],
"ArtistsPerGenre": 5,
"AlbumsPerArtist": 20,
"TracksPerAlbum": 40,
"MaxCommentsPerTrack": 3
},
"AllowedHosts": "*",
"EfPerformance": {
"SeuilMs": 60