Merge remote-tracking branch 'origin/dev' into j3/feat/pagination
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 ();
|
||||
}
|
||||
}
|
||||
@@ -2,7 +2,6 @@ namespace Webzine.Business;
|
||||
|
||||
using Webzine.Business.Contracts;
|
||||
using Webzine.Business.Contracts.Dto;
|
||||
using Webzine.Entity;
|
||||
using Webzine.Repository.Contracts;
|
||||
|
||||
/// <summary>
|
||||
@@ -34,37 +33,22 @@ public class DashboardService : IDashboardService
|
||||
/// <inheritdoc/>
|
||||
public DashboardDTO GetDashboardData()
|
||||
{
|
||||
IEnumerable<Titre> titres = this.titreRepository.FindAll();
|
||||
|
||||
Artiste? artisteLePlusChronique = titres
|
||||
.GroupBy(t => t.Artiste)
|
||||
.OrderByDescending(g => g.Count())
|
||||
.FirstOrDefault()
|
||||
?.Key;
|
||||
|
||||
Artiste? albumLePlusChronique = titres
|
||||
.GroupBy(t => (t.Artiste, t.Album))
|
||||
.GroupBy(g => g.Key.Artiste)
|
||||
.OrderByDescending(g => g.Count())
|
||||
.FirstOrDefault()
|
||||
?.Key;
|
||||
|
||||
Titre? musiqueLaPlusJouee = titres
|
||||
.OrderByDescending(t => t.NbLectures)
|
||||
.FirstOrDefault();
|
||||
string artisteLePlusChronique = this.titreRepository.FindMostReviewedArtistName() ?? string.Empty;
|
||||
string albumLePlusChronique = this.titreRepository.FindArtistNameWithMostReviewedAlbums() ?? string.Empty;
|
||||
var musiqueLaPlusJouee = this.titreRepository.FindMostPlayedTitle();
|
||||
|
||||
return new DashboardDTO
|
||||
{
|
||||
NombreArtistes = this.artisteRepository.Count(),
|
||||
ArtisteLePlusChronique = artisteLePlusChronique.Nom,
|
||||
AlbumLePlusChronique = albumLePlusChronique.Nom,
|
||||
NombreBiographies = this.artisteRepository.Count(a => !string.IsNullOrEmpty(a.Biographie)),
|
||||
IdMusiqueLaPlusJouee = musiqueLaPlusJouee.IdTitre,
|
||||
MusiqueLaPlusJouee = musiqueLaPlusJouee.Libelle,
|
||||
ArtisteLePlusChronique = artisteLePlusChronique,
|
||||
AlbumLePlusChronique = albumLePlusChronique,
|
||||
NombreBiographies = this.artisteRepository.CountWithBiography(),
|
||||
IdMusiqueLaPlusJouee = musiqueLaPlusJouee?.IdTitre ?? 0,
|
||||
MusiqueLaPlusJouee = musiqueLaPlusJouee?.Libelle ?? string.Empty,
|
||||
NombreTitres = this.titreRepository.Count(),
|
||||
NombreGenres = this.styleRepository.Count(),
|
||||
NombreLectures = titres.Sum(t => t.NbLectures),
|
||||
NombreLikes = titres.Sum(t => t.NbLikes),
|
||||
NombreLectures = this.titreRepository.CountLecture(),
|
||||
NombreLikes = this.titreRepository.CountLike(),
|
||||
};
|
||||
}
|
||||
}
|
||||
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;
|
||||
}
|
||||
}
|
||||
}
|
||||
162
Webzine.Business/Seeders/SeedDataSpotify.cs
Normal file
162
Webzine.Business/Seeders/SeedDataSpotify.cs
Normal 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);
|
||||
}
|
||||
}
|
||||
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,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>
|
||||
|
||||
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,5 +0,0 @@
|
||||
namespace Webzine.Entity.Fixtures;
|
||||
|
||||
public class SeedDataSpotify
|
||||
{
|
||||
}
|
||||
@@ -72,5 +72,11 @@ namespace Webzine.Repository.Contracts
|
||||
/// <param name="predicate">Le prédicat de filtrage.</param>
|
||||
/// <returns>Le nombre d'artistes correspondants.</returns>
|
||||
int Count(Func<Artiste, bool> predicate);
|
||||
|
||||
/// <summary>
|
||||
/// Récupère le nombre d'artistes ayant une biographie renseignée.
|
||||
/// </summary>
|
||||
/// <returns>Le nombre d'artistes avec biographie.</returns>
|
||||
int CountWithBiography();
|
||||
}
|
||||
}
|
||||
@@ -77,5 +77,35 @@
|
||||
/// </summary>
|
||||
/// <param name="titre">L'objet titre à mettre à jour.</param>
|
||||
void Update(Titre titre);
|
||||
|
||||
/// <summary>
|
||||
/// Retourne le nombre total de likes.
|
||||
/// </summary>
|
||||
/// <returns>Integer.</returns>
|
||||
int CountLike();
|
||||
|
||||
/// <summary>
|
||||
/// Retourne le nombre total de lecture.
|
||||
/// </summary>
|
||||
/// <returns>Integer.</returns>
|
||||
int CountLecture();
|
||||
|
||||
/// <summary>
|
||||
/// Retourne le nom de l'artiste ayant le plus de titres chroniqués.
|
||||
/// </summary>
|
||||
/// <returns>Le nom de l'artiste le plus chroniqué, ou null si aucun titre n'existe.</returns>
|
||||
string? FindMostReviewedArtistName();
|
||||
|
||||
/// <summary>
|
||||
/// Retourne le nom de l'artiste ayant le plus d'albums chroniqués.
|
||||
/// </summary>
|
||||
/// <returns>Le nom de l'artiste concerné, ou null si aucun titre n'existe.</returns>
|
||||
string? FindArtistNameWithMostReviewedAlbums();
|
||||
|
||||
/// <summary>
|
||||
/// Retourne l'identifiant et le libellé du titre le plus joué.
|
||||
/// </summary>
|
||||
/// <returns>Un tuple contenant l'identifiant et le libellé du titre le plus joué, ou null si aucun titre n'existe.</returns>
|
||||
(int IdTitre, string Libelle)? FindMostPlayedTitle();
|
||||
}
|
||||
}
|
||||
@@ -208,6 +208,22 @@ namespace Webzine.Repository
|
||||
}
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
public int CountWithBiography()
|
||||
{
|
||||
try
|
||||
{
|
||||
int count = this.context.Artistes.Count(a => !string.IsNullOrEmpty(a.Biographie));
|
||||
this.logger.LogDebug("Nombre d'artistes avec biographie: {Count}", count);
|
||||
return count;
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
this.logger.LogError(ex, "Erreur lors du comptage des artistes avec biographie.");
|
||||
throw;
|
||||
}
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
public IEnumerable<Artiste> FindArtistes(int offset, int limit)
|
||||
{
|
||||
@@ -217,8 +233,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;
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -33,7 +33,6 @@ public class DbTitreRepository : ITitreRepository
|
||||
try
|
||||
{
|
||||
this.logger.LogInformation("Ajout d'un nouveau titre: {Libelle}", titre.Libelle);
|
||||
this.logger.LogDebug("Début de l'ajout du titre en base de données");
|
||||
|
||||
this.context.Titres.Add(titre);
|
||||
this.context.SaveChanges();
|
||||
@@ -57,7 +56,6 @@ public class DbTitreRepository : ITitreRepository
|
||||
{
|
||||
try
|
||||
{
|
||||
this.logger.LogDebug("Comptage des titres en base de données");
|
||||
var count = this.context.Titres.Count();
|
||||
this.logger.LogDebug("Nombre total de titres: {Count}", count);
|
||||
return count;
|
||||
@@ -75,7 +73,6 @@ public class DbTitreRepository : ITitreRepository
|
||||
try
|
||||
{
|
||||
this.logger.LogInformation("Suppression du titre avec l'ID: {IdTitre}", titre.IdTitre);
|
||||
this.logger.LogDebug("Début de la suppression du titre en base de données");
|
||||
|
||||
this.context.Titres.Remove(titre);
|
||||
this.context.SaveChanges();
|
||||
@@ -100,15 +97,13 @@ public class DbTitreRepository : ITitreRepository
|
||||
try
|
||||
{
|
||||
this.logger.LogDebug("Recherche des titres avec offset: {Offset}, limit: {Limit}", offset, limit);
|
||||
this.logger.LogDebug("Préparation de la requête avec les inclusions Artiste et Styles");
|
||||
|
||||
var titres = this.context.Titres
|
||||
.OrderByDescending(t => t.DateCreation)
|
||||
.ThenBy(t => t.Libelle)
|
||||
.Include(t => t.Artiste)
|
||||
.Include(t => t.Styles)
|
||||
.Skip(offset)
|
||||
.Take(limit)
|
||||
.Paginate(offset, limit)
|
||||
.AsNoTracking();
|
||||
|
||||
return titres;
|
||||
@@ -126,12 +121,10 @@ public class DbTitreRepository : ITitreRepository
|
||||
try
|
||||
{
|
||||
this.logger.LogInformation("Incrémentation du nombre de lectures pour le titre ID: {IdTitre}", titre.IdTitre);
|
||||
this.logger.LogDebug("Recherche du titre en base de données");
|
||||
|
||||
var existingTitre = this.context.Titres.Find(titre.IdTitre);
|
||||
if (existingTitre != null)
|
||||
{
|
||||
this.logger.LogDebug("Titre trouvé, incrémentation du compteur de lectures");
|
||||
existingTitre.NbLectures++;
|
||||
this.context.SaveChanges();
|
||||
this.logger.LogDebug("Nouveau nombre de lectures: {NbLectures}", existingTitre.NbLectures);
|
||||
@@ -159,12 +152,10 @@ public class DbTitreRepository : ITitreRepository
|
||||
try
|
||||
{
|
||||
this.logger.LogInformation("Incrémentation du nombre de likes pour le titre ID: {IdTitre}", titre.IdTitre);
|
||||
this.logger.LogDebug("Recherche du titre en base de données");
|
||||
|
||||
var existingTitre = this.context.Titres.Find(titre.IdTitre);
|
||||
if (existingTitre != null)
|
||||
{
|
||||
this.logger.LogDebug("Titre trouvé, incrémentation du compteur de likes");
|
||||
existingTitre.NbLikes++;
|
||||
this.context.SaveChanges();
|
||||
this.logger.LogDebug("Nouveau nombre de likes: {NbLikes}", existingTitre.NbLikes);
|
||||
@@ -282,9 +273,6 @@ public class DbTitreRepository : ITitreRepository
|
||||
{
|
||||
try
|
||||
{
|
||||
this.logger.LogDebug("Récupération de tous les titres");
|
||||
this.logger.LogDebug("Préparation de la requête avec les inclusions Artiste et Styles");
|
||||
|
||||
var titres = this.context.Titres
|
||||
.Include(t => t.Artiste)
|
||||
.Include(t => t.Styles)
|
||||
@@ -308,7 +296,6 @@ public class DbTitreRepository : ITitreRepository
|
||||
try
|
||||
{
|
||||
this.logger.LogInformation("Recherche des titres par style: {Libelle}", libelle);
|
||||
this.logger.LogDebug("Préparation de la requête de recherche par style");
|
||||
|
||||
var titres = this.context.Titres
|
||||
.Include(t => t.Artiste)
|
||||
@@ -326,4 +313,100 @@ public class DbTitreRepository : ITitreRepository
|
||||
throw;
|
||||
}
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
public int CountLike()
|
||||
{
|
||||
try
|
||||
{
|
||||
var likes = this.context.Titres.Sum(t => t.NbLikes);
|
||||
return likes;
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
this.logger.LogError(ex, "Erreur lors de la récupération des likes.");
|
||||
throw;
|
||||
}
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
public int CountLecture()
|
||||
{
|
||||
try
|
||||
{
|
||||
var lectures = this.context.Titres.Sum(t => t.NbLectures);
|
||||
return lectures;
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
this.logger.LogError(ex, "Erreur lors de la récupération des lectures.");
|
||||
throw;
|
||||
}
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
public string? FindMostReviewedArtistName()
|
||||
{
|
||||
try
|
||||
{
|
||||
return this.context.Titres
|
||||
.AsNoTracking()
|
||||
.GroupBy(t => new { t.IdArtiste, t.Artiste.Nom })
|
||||
.OrderByDescending(g => g.Count())
|
||||
.ThenBy(g => g.Key.Nom)
|
||||
.Select(g => g.Key.Nom)
|
||||
.FirstOrDefault();
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
this.logger.LogError(ex, "Erreur lors de la recherche de l'artiste le plus chroniqué.");
|
||||
throw;
|
||||
}
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
public string? FindArtistNameWithMostReviewedAlbums()
|
||||
{
|
||||
try
|
||||
{
|
||||
return this.context.Titres
|
||||
.AsNoTracking()
|
||||
.GroupBy(t => new { t.IdArtiste, t.Artiste.Nom })
|
||||
.Select(g => new
|
||||
{
|
||||
g.Key.Nom,
|
||||
AlbumCount = g.Select(t => t.Album).Distinct().Count(),
|
||||
})
|
||||
.OrderByDescending(x => x.AlbumCount)
|
||||
.ThenBy(x => x.Nom)
|
||||
.Select(x => x.Nom)
|
||||
.FirstOrDefault();
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
this.logger.LogError(ex, "Erreur lors de la recherche de l'artiste avec le plus d'albums chroniqués.");
|
||||
throw;
|
||||
}
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
public (int IdTitre, string Libelle)? FindMostPlayedTitle()
|
||||
{
|
||||
try
|
||||
{
|
||||
var result = this.context.Titres
|
||||
.AsNoTracking()
|
||||
.OrderByDescending(t => t.NbLectures)
|
||||
.ThenBy(t => t.Libelle)
|
||||
.Select(t => new { t.IdTitre, t.Libelle })
|
||||
.FirstOrDefault();
|
||||
|
||||
return result == null ? null : (result.IdTitre, result.Libelle);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
this.logger.LogError(ex, "Erreur lors de la recherche du titre le plus joué.");
|
||||
throw;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -107,13 +107,18 @@ namespace Webzine.Repository
|
||||
return this.dataStore.Artistes.Count(predicate);
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
public int CountWithBiography()
|
||||
{
|
||||
return this.dataStore.Artistes.Count(a => !string.IsNullOrEmpty(a.Biographie));
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
public IEnumerable<Artiste> FindArtistes(int offset, int limit)
|
||||
{
|
||||
return this.dataStore.Artistes
|
||||
.OrderBy(a => a.Nom)
|
||||
.Skip(offset)
|
||||
.Take(limit)
|
||||
.Paginate(offset, limit)
|
||||
.ToList();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -63,8 +63,7 @@ namespace Webzine.Repository
|
||||
{
|
||||
return this.dataStore.Commentaires
|
||||
.OrderByDescending(c => c.DateCreation)
|
||||
.Skip(offset)
|
||||
.Take(limit);
|
||||
.Paginate(offset, limit);
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
|
||||
@@ -77,8 +77,7 @@ public class LocalStyleRepository : IStyleRepository
|
||||
{
|
||||
return this.dataStore.Styles
|
||||
.OrderBy(s => s.Libelle)
|
||||
.Skip(offset)
|
||||
.Take(limit)
|
||||
.Paginate(offset, limit)
|
||||
.ToList();
|
||||
}
|
||||
}
|
||||
@@ -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/>
|
||||
@@ -132,4 +131,54 @@ public class LocalTitreRepository : ITitreRepository
|
||||
existingTitre.IdArtiste = titre.IdArtiste;
|
||||
existingTitre.Styles = titre.Styles;
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
public int CountLike()
|
||||
{
|
||||
return this.dataStore.Titres.Sum(t => t.NbLikes);
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
public int CountLecture()
|
||||
{
|
||||
return this.dataStore.Titres.Sum(t => t.NbLectures);
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
public string? FindMostReviewedArtistName()
|
||||
{
|
||||
return this.dataStore.Titres
|
||||
.GroupBy(t => t.Artiste)
|
||||
.OrderByDescending(g => g.Count())
|
||||
.ThenBy(g => g.Key?.Nom)
|
||||
.Select(g => g.Key?.Nom)
|
||||
.FirstOrDefault();
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
public string? FindArtistNameWithMostReviewedAlbums()
|
||||
{
|
||||
return this.dataStore.Titres
|
||||
.GroupBy(t => t.Artiste)
|
||||
.Select(g => new
|
||||
{
|
||||
ArtistName = g.Key?.Nom,
|
||||
AlbumCount = g.Select(t => t.Album).Distinct().Count(),
|
||||
})
|
||||
.OrderByDescending(x => x.AlbumCount)
|
||||
.ThenBy(x => x.ArtistName)
|
||||
.Select(x => x.ArtistName)
|
||||
.FirstOrDefault();
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
public (int IdTitre, string Libelle)? FindMostPlayedTitle()
|
||||
{
|
||||
Titre? titre = this.dataStore.Titres
|
||||
.OrderByDescending(t => t.NbLectures)
|
||||
.ThenBy(t => t.Libelle)
|
||||
.FirstOrDefault();
|
||||
|
||||
return titre == null ? null : (titre.IdTitre, titre.Libelle);
|
||||
}
|
||||
}
|
||||
56
Webzine.Repository/RepositoryPaginationExtensions.cs
Normal file
56
Webzine.Repository/RepositoryPaginationExtensions.cs
Normal 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.");
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -81,8 +81,8 @@ public class ArtisteController : Controller
|
||||
Biographie = model.Biographie,
|
||||
};
|
||||
|
||||
// Persister les données.
|
||||
this.artisteRepository.Add(artiste);
|
||||
this.logger.LogInformation("Création d'un nouvel artiste: {Nom}", artiste.Nom);
|
||||
|
||||
// Renvoyer sur la page Index.
|
||||
return this.RedirectToAction("Index");
|
||||
|
||||
@@ -1,11 +1,11 @@
|
||||
namespace Webzine.WebApplication.Areas.Administration.Controllers;
|
||||
|
||||
namespace Webzine.WebApplication.Areas.Administration.Controllers
|
||||
{
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
|
||||
using ViewModels.Styles;
|
||||
|
||||
using Webzine.Entity;
|
||||
using Webzine.Repository.Contracts;
|
||||
using Webzine.WebApplication.Areas.Administration.ViewModels.Style;
|
||||
using Webzine.WebApplication.Areas.Administration.ViewModels.Styles;
|
||||
|
||||
/// <summary>
|
||||
/// Controleur pour la gestion des styles dans l'administration du webzine.
|
||||
@@ -172,3 +172,4 @@ public class StyleController : Controller
|
||||
return this.RedirectToAction("Index");
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -252,24 +252,18 @@ public class TitreController : Controller
|
||||
/// <summary>
|
||||
/// Méthode POST pour supprimer un titre.
|
||||
/// </summary>
|
||||
/// <param name="model">Le titre à supprimer.</param>
|
||||
/// <param name="id">L'identifiant du titre à supprimer, utilisé pour récupérer les données du titre à partir de la liste des titres générés.</param>
|
||||
/// <returns>Redirige vers la page d'index d'admin titre.</returns>
|
||||
[HttpPost]
|
||||
public IActionResult Delete(AdminTitreDelete model)
|
||||
public IActionResult DeleteTitre(int id)
|
||||
{
|
||||
var titre = this.titreRepository.Find(model.Id);
|
||||
|
||||
if (!this.ModelState.IsValid)
|
||||
{
|
||||
return this.View(model);
|
||||
}
|
||||
var titre = this.titreRepository.Find(id);
|
||||
|
||||
if (titre != null)
|
||||
{
|
||||
this.titreRepository.Delete(titre);
|
||||
return this.RedirectToAction("Index");
|
||||
}
|
||||
|
||||
return this.View(model);
|
||||
return this.RedirectToAction("Index");
|
||||
}
|
||||
}
|
||||
@@ -1,5 +1,9 @@
|
||||
@model Webzine.WebApplication.Areas.Administration.ViewModels.Titre.AdminTitreDelete
|
||||
|
||||
@{
|
||||
ViewData["Title"] = "Supprimer un titre";
|
||||
}
|
||||
|
||||
<div class="container mt-4">
|
||||
|
||||
<h1 class="mb-3">Supprimer un titre</h1>
|
||||
@@ -13,7 +17,7 @@
|
||||
@Model.Artiste ?
|
||||
</p>
|
||||
|
||||
<form asp-action="Delete" method="post">
|
||||
<form asp-action="DeleteTitre" method="post">
|
||||
|
||||
<input type="hidden" asp-for="Id"/>
|
||||
|
||||
|
||||
@@ -38,6 +38,7 @@ namespace Webzine.WebApplication.Controllers
|
||||
/// Affiche le detail d'un titre specifique.
|
||||
/// </summary>
|
||||
/// <param name="id">Identifiant du titre.</param>
|
||||
/// <param name="model">Model de donnée pour un commentaire.</param>
|
||||
/// <returns>Vue des details ou 404 si introuvable.</returns>
|
||||
public IActionResult Index(int id)
|
||||
{
|
||||
@@ -48,33 +49,11 @@ namespace Webzine.WebApplication.Controllers
|
||||
if (titre == null)
|
||||
{
|
||||
this.logger.LogWarning("Titre avec ID {Id} introuvable.", id);
|
||||
return this.RedirectToAction("Index");
|
||||
return this.RedirectToAction("Index", "Accueil");
|
||||
}
|
||||
|
||||
this.titreRepository.IncrementNbLectures(titre);
|
||||
|
||||
var vm = new TitreDetail
|
||||
{
|
||||
Details = new TitreContent
|
||||
{
|
||||
IdTitre = titre.IdTitre,
|
||||
Libelle = titre.Libelle,
|
||||
Chronique = titre.Chronique,
|
||||
DateSortie = titre.DateSortie,
|
||||
NbLikes = titre.NbLikes,
|
||||
UrlJaquette = titre.UrlJaquette,
|
||||
UrlEcoute = titre.UrlEcoute,
|
||||
ArtisteNom = titre.Artiste.Nom,
|
||||
Styles = titre.Styles,
|
||||
Commentaires = titre.Commentaires,
|
||||
},
|
||||
CommentForm = new TitreComment
|
||||
{
|
||||
IdTitre = titre.IdTitre,
|
||||
},
|
||||
};
|
||||
|
||||
return this.View(vm);
|
||||
return this.View(this.BuildTitreDetailViewModel(titre));
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
@@ -100,25 +79,18 @@ namespace Webzine.WebApplication.Controllers
|
||||
/// <summary>
|
||||
/// Ajoute un like a un titre.
|
||||
/// </summary>
|
||||
/// <param name="model">Modele contenant l'identifiant du titre.</param>
|
||||
/// <param name="id">Identifiant du titre a liker.</param>
|
||||
/// <returns>Redirection vers la page detail.</returns>
|
||||
[HttpPost]
|
||||
public IActionResult Like(TitreLike model)
|
||||
public IActionResult Like(int id)
|
||||
{
|
||||
this.logger.LogInformation("Ajout d'un like pour le titre ID {Id}.", model.IdTitre);
|
||||
|
||||
var titre = this.titreRepository.Find(model.IdTitre);
|
||||
|
||||
if (titre == null)
|
||||
{
|
||||
this.logger.LogWarning("Impossible d'ajouter un like. Titre ID {Id} introuvable.", model.IdTitre);
|
||||
}
|
||||
else
|
||||
var titre = this.titreRepository.Find(id);
|
||||
if (titre != null)
|
||||
{
|
||||
this.titreRepository.IncrementNbLikes(titre);
|
||||
}
|
||||
|
||||
return this.RedirectToAction("Index", new { id = model.IdTitre });
|
||||
return this.RedirectToAction("Index", new { id });
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
@@ -127,16 +99,23 @@ namespace Webzine.WebApplication.Controllers
|
||||
/// <param name="model">Donnees du commentaire.</param>
|
||||
/// <returns>Redirection vers la page detail.</returns>
|
||||
[HttpPost]
|
||||
public IActionResult Comment(TitreComment model)
|
||||
public IActionResult Comment([Bind(Prefix = "CommentForm")] TitreComment model)
|
||||
{
|
||||
if (!this.ModelState.IsValid)
|
||||
{
|
||||
var titre = this.titreRepository.Find(model.IdTitre);
|
||||
|
||||
if (titre == null)
|
||||
{
|
||||
this.logger.LogWarning("Impossible d'ajouter le commentaire. Titre ID {Id} introuvable.", model.IdTitre);
|
||||
return this.RedirectToAction("Index");
|
||||
this.logger.LogWarning("Titre avec ID {Id} introuvable pour ajout de commentaire.", model.IdTitre);
|
||||
return this.RedirectToAction("Index", "Accueil");
|
||||
}
|
||||
|
||||
return this.View("Index", this.BuildTitreDetailViewModel(titre, model));
|
||||
}
|
||||
|
||||
var titreToUpdate = this.titreRepository.Find(model.IdTitre);
|
||||
if (titreToUpdate != null)
|
||||
{
|
||||
var commentaire = new Commentaire
|
||||
{
|
||||
Auteur = model.Auteur,
|
||||
@@ -145,13 +124,43 @@ namespace Webzine.WebApplication.Controllers
|
||||
IdTitre = model.IdTitre,
|
||||
};
|
||||
|
||||
titre.Commentaires.Add(commentaire);
|
||||
|
||||
this.logger.LogInformation("Commentaire ajoute avec succes au titre ID {Id}.", model.IdTitre);
|
||||
titreToUpdate.Commentaires.Add(commentaire);
|
||||
this.titreRepository.Update(titreToUpdate);
|
||||
}
|
||||
|
||||
return this.RedirectToAction("Index", new { id = model.IdTitre });
|
||||
}
|
||||
|
||||
private TitreDetail BuildTitreDetailViewModel(Titre titre, TitreComment? commentForm = null)
|
||||
{
|
||||
return new TitreDetail
|
||||
{
|
||||
Details = new TitreContent
|
||||
{
|
||||
IdTitre = titre.IdTitre,
|
||||
Libelle = titre.Libelle,
|
||||
Chronique = titre.Chronique,
|
||||
DateSortie = titre.DateSortie,
|
||||
NbLikes = titre.NbLikes,
|
||||
UrlJaquette = titre.UrlJaquette,
|
||||
UrlEcoute = titre.UrlEcoute,
|
||||
UrlEmbedEcoute = BuildSpotifyEmbedUrl(titre.UrlEcoute),
|
||||
ArtisteNom = titre.Artiste.Nom,
|
||||
Styles = titre.Styles,
|
||||
Commentaires = titre.Commentaires,
|
||||
},
|
||||
CommentForm = commentForm ?? new TitreComment
|
||||
{
|
||||
IdTitre = titre.IdTitre,
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Mappe une entite Titre vers un item de la liste de titres pour l'affichage dans la vue de style.
|
||||
/// </summary>
|
||||
/// <param name="titre">Le titre à mapper.</param>
|
||||
/// <returns>L'item de la liste de titres.</returns>
|
||||
private static TitreStyleItem MapTitreItem(Titre titre)
|
||||
{
|
||||
return new TitreStyleItem
|
||||
@@ -163,5 +172,23 @@ namespace Webzine.WebApplication.Controllers
|
||||
Duree = titre.Duree,
|
||||
};
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Construit une URL d'intégration Spotify à partir de l'URL d'écoute d'un titre.
|
||||
/// </summary>
|
||||
/// <param name="urlEcoute">L'URL d'écoute du titre.</param>
|
||||
/// <returns>L'URL d'intégration Spotify ou null si l'URL n'est pas valide.</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}";
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -23,6 +23,16 @@ public static class RouteConfiguration
|
||||
pattern: "artiste/{nom}",
|
||||
defaults: new { controller = "Artiste", action = "Index" });
|
||||
|
||||
endpoints.MapControllerRoute(
|
||||
name: "TitreLike",
|
||||
pattern: "titre/{id}/like",
|
||||
defaults: new { controller = "Titre", action = "Like" });
|
||||
|
||||
endpoints.MapControllerRoute(
|
||||
name: "TitreComment",
|
||||
pattern: "titre/{id}/comment",
|
||||
defaults: new { controller = "Titre", action = "Comment" });
|
||||
|
||||
// ----------- ADMIN -----------
|
||||
var adminRoutes = new Dictionary<string, string>
|
||||
{
|
||||
@@ -37,7 +47,7 @@ public static class RouteConfiguration
|
||||
defaults: new { area = "Administration", controller = route.Value, action = "Index" });
|
||||
}
|
||||
|
||||
// --- AUTRE PROUTES ---
|
||||
// --- AUTRES ROUTES ---
|
||||
endpoints.MapControllerRoute(
|
||||
name: "areas",
|
||||
pattern: "{area:exists}/{controller=Home}/{action=Index}/{id?}");
|
||||
|
||||
@@ -23,6 +23,15 @@ public class ValidationActionFilter : IActionFilter
|
||||
/// <inheritdoc/>
|
||||
public void OnActionExecuting(ActionExecutingContext context)
|
||||
{
|
||||
string controllerName = context.RouteData.Values["controller"]?.ToString() ?? string.Empty;
|
||||
string actionName = context.RouteData.Values["action"]?.ToString() ?? string.Empty;
|
||||
|
||||
if (controllerName.Equals("Titre", StringComparison.OrdinalIgnoreCase)
|
||||
&& actionName.Equals("Comment", StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
if (!context.ModelState.IsValid)
|
||||
{
|
||||
var erreurs = context.ModelState
|
||||
@@ -35,8 +44,6 @@ public class ValidationActionFilter : IActionFilter
|
||||
context.ActionDescriptor.DisplayName,
|
||||
string.Join(" | ", erreurs));
|
||||
|
||||
string actionName = context.RouteData.Values["action"]?.ToString() ?? string.Empty;
|
||||
|
||||
// cas spécial: titre details
|
||||
if (actionName.Equals("Index", StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
|
||||
@@ -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();
|
||||
@@ -118,11 +127,13 @@ try
|
||||
|
||||
if (seederType == SeederType.Local)
|
||||
{
|
||||
repo.SeedBaseDeDonnees();
|
||||
repo.SeedBaseDeDonnees(nbArtistes: 1000, nbTitres: 50000, maxStyles: 50);
|
||||
}
|
||||
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);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -139,11 +150,13 @@ try
|
||||
var commentaires = new List<Commentaire>();
|
||||
var titres = SeedDataLocal.GenererListeTitre(500, artistes, styles, albums);
|
||||
|
||||
int commentaireIdStart = 1;
|
||||
foreach (var titre in titres)
|
||||
{
|
||||
var commentairesForTitre = SeedDataLocal.GenererListeCommentaire(titre, 0, 5);
|
||||
var commentairesForTitre = SeedDataLocal.GenererListeCommentaire(titre, 0, 5, commentaireIdStart);
|
||||
titre.Commentaires.AddRange(commentairesForTitre);
|
||||
commentaires.AddRange(commentairesForTitre);
|
||||
commentaireIdStart += commentairesForTitre.Count;
|
||||
}
|
||||
|
||||
store.Artistes.AddRange(artistes);
|
||||
@@ -155,8 +168,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 +181,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();
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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">
|
||||
|
||||
@@ -59,8 +59,7 @@
|
||||
<!-- ACTION BUTTONS -->
|
||||
<div class="d-flex gap-2">
|
||||
|
||||
<form asp-action="Like" method="post">
|
||||
<input type="hidden" name="IdTitre" value="@Model.Details.IdTitre"/>
|
||||
<form asp-action="Like" asp-controller="Titre" asp-route-id="@Model.Details.IdTitre" method="post">
|
||||
<button type="submit" class="btn btn-outline-primary btn-sm">
|
||||
<i class="fa fa-thumbs-up me-1"></i> Like
|
||||
</button>
|
||||
@@ -94,14 +93,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>
|
||||
}
|
||||
|
||||
@@ -111,31 +124,32 @@
|
||||
|
||||
<h4 class="mb-4">Donne ton avis sur le titre</h4>
|
||||
|
||||
<form asp-action="Comment" method="post">
|
||||
<input type="hidden" name="IdTitre" value="@Model.Details.IdTitre"/>
|
||||
<form asp-action="Comment" asp-controller="Titre" asp-route-id="@Model.Details.IdTitre" method="post">
|
||||
<input asp-for="CommentForm.IdTitre" type="hidden" />
|
||||
<div asp-validation-summary="ModelOnly" class="text-danger mb-3"></div>
|
||||
|
||||
<div class="row mb-3 align-items-center">
|
||||
<label class="col-sm-2 col-form-label">
|
||||
<label asp-for="CommentForm.Auteur" class="col-sm-2 col-form-label">
|
||||
Nom<span class="text-danger">*</span>
|
||||
</label>
|
||||
<div class="col">
|
||||
<input name="Auteur"
|
||||
<input asp-for="CommentForm.Auteur"
|
||||
class="form-control input-full"
|
||||
placeholder="Votre nom"
|
||||
required/>
|
||||
placeholder="Votre nom" />
|
||||
<span asp-validation-for="CommentForm.Auteur" class="text-danger"></span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="row mb-3 align-items-start">
|
||||
<label class="col-sm-2 col-form-label">
|
||||
<label asp-for="CommentForm.Contenu" class="col-sm-2 col-form-label">
|
||||
Commentaire<span class="text-danger">*</span>
|
||||
</label>
|
||||
<div class="col">
|
||||
<textarea name="Contenu"
|
||||
<textarea asp-for="CommentForm.Contenu"
|
||||
rows="3"
|
||||
class="form-control input-full"
|
||||
placeholder="Votre commentaire..."
|
||||
required></textarea>
|
||||
placeholder="Votre commentaire..."></textarea>
|
||||
<span asp-validation-for="CommentForm.Contenu" class="text-danger"></span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
@@ -1,4 +1,8 @@
|
||||
{
|
||||
"Seeder": "Local",
|
||||
"Repository": "Db"
|
||||
"Repository": "Db",
|
||||
"SpotifySeeder": {
|
||||
"ClientId": "",
|
||||
"ClientSecret": ""
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,4 +1,8 @@
|
||||
{
|
||||
"Seeder": "Local",
|
||||
"Repository": "Db"
|
||||
"Repository": "Db",
|
||||
"SpotifySeeder": {
|
||||
"ClientId": "",
|
||||
"ClientSecret": ""
|
||||
}
|
||||
}
|
||||
@@ -2,7 +2,8 @@
|
||||
"Logging": {
|
||||
"LogLevel": {
|
||||
"Default": "Information",
|
||||
"Microsoft.AspNetCore": "Warning"
|
||||
"Microsoft.AspNetCore": "Warning",
|
||||
"Webzine.Repository": "Debug"
|
||||
}
|
||||
},
|
||||
"Webzine": {
|
||||
@@ -14,6 +15,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
|
||||
|
||||
@@ -29,6 +29,7 @@
|
||||
<rules>
|
||||
<!-- Vos logs d'application en Debug+ -->
|
||||
<logger name="Webzine.WebApplication.*" minlevel="Info" writeTo="allfile,ownfile-web,console" />
|
||||
<logger name="Webzine.Repository.*" minlevel="Debug" writeTo="allfile,ownfile-web,console" />
|
||||
|
||||
<!-- Logs Microsoft en Warning+ sauf Hosting.Lifetime -->
|
||||
<logger name="Microsoft.*" minlevel="Warn" writeTo="allfile" final="true" />
|
||||
|
||||
Reference in New Issue
Block a user