diff --git a/octo-fiesta/Models/MusicModels.cs b/octo-fiesta/Models/MusicModels.cs
index da0f4cb..9055b2d 100644
--- a/octo-fiesta/Models/MusicModels.cs
+++ b/octo-fiesta/Models/MusicModels.cs
@@ -18,10 +18,57 @@ public class Song
public string? AlbumId { get; set; }
public int? Duration { get; set; } // En secondes
public int? Track { get; set; }
+ public int? DiscNumber { get; set; }
+ public int? TotalTracks { get; set; }
public int? Year { get; set; }
public string? Genre { get; set; }
public string? CoverArtUrl { get; set; }
+ ///
+ /// URL de la cover en haute résolution (pour embedding)
+ ///
+ public string? CoverArtUrlLarge { get; set; }
+
+ ///
+ /// BPM (beats per minute) si disponible
+ ///
+ public int? Bpm { get; set; }
+
+ ///
+ /// ISRC (International Standard Recording Code)
+ ///
+ public string? Isrc { get; set; }
+
+ ///
+ /// Date de sortie complète (format: YYYY-MM-DD)
+ ///
+ public string? ReleaseDate { get; set; }
+
+ ///
+ /// Nom de l'album artiste (peut différer de l'artiste du track)
+ ///
+ public string? AlbumArtist { get; set; }
+
+ ///
+ /// Compositeur(s)
+ ///
+ public string? Composer { get; set; }
+
+ ///
+ /// Label de l'album
+ ///
+ public string? Label { get; set; }
+
+ ///
+ /// Copyright
+ ///
+ public string? Copyright { get; set; }
+
+ ///
+ /// Artistes contributeurs (featurings, etc.)
+ ///
+ public List Contributors { get; set; } = new();
+
///
/// Indique si la chanson est disponible localement ou doit être téléchargée
///
diff --git a/octo-fiesta/Services/DeezerDownloadService.cs b/octo-fiesta/Services/DeezerDownloadService.cs
index bbb679c..e0d7f16 100644
--- a/octo-fiesta/Services/DeezerDownloadService.cs
+++ b/octo-fiesta/Services/DeezerDownloadService.cs
@@ -5,6 +5,8 @@ using Org.BouncyCastle.Crypto.Engines;
using Org.BouncyCastle.Crypto.Modes;
using Org.BouncyCastle.Crypto.Parameters;
using octo_fiesta.Models;
+using TagLib;
+using IOFile = System.IO.File;
namespace octo_fiesta.Services;
@@ -84,7 +86,7 @@ public class DeezerDownloadService : IDownloadService
// Vérifier si déjà téléchargé
var existingPath = await _localLibraryService.GetLocalPathForExternalSongAsync(externalProvider, externalId);
- if (existingPath != null && File.Exists(existingPath))
+ if (existingPath != null && IOFile.Exists(existingPath))
{
_logger.LogInformation("Song already downloaded: {Path}", existingPath);
return existingPath;
@@ -161,7 +163,7 @@ public class DeezerDownloadService : IDownloadService
public async Task DownloadAndStreamAsync(string externalProvider, string externalId, CancellationToken cancellationToken = default)
{
var localPath = await DownloadSongAsync(externalProvider, externalId, cancellationToken);
- return File.OpenRead(localPath);
+ return IOFile.OpenRead(localPath);
}
public DownloadInfo? GetDownloadStatus(string songId)
@@ -391,13 +393,163 @@ public class DeezerDownloadService : IDownloadService
// Télécharger et déchiffrer
await using var responseStream = await response.Content.ReadAsStreamAsync(cancellationToken);
- await using var outputFile = File.Create(outputPath);
+ await using var outputFile = IOFile.Create(outputPath);
await DecryptAndWriteStreamAsync(responseStream, outputFile, trackId, cancellationToken);
+
+ // Fermer le fichier avant d'écrire les métadonnées
+ await outputFile.DisposeAsync();
+
+ // Écrire les métadonnées et la cover art
+ await WriteMetadataAsync(outputPath, song, cancellationToken);
return outputPath;
}
+ ///
+ /// Écrit les métadonnées ID3/Vorbis et la cover art dans le fichier audio
+ ///
+ private async Task WriteMetadataAsync(string filePath, Song song, CancellationToken cancellationToken)
+ {
+ try
+ {
+ _logger.LogInformation("Writing metadata to: {Path}", filePath);
+
+ using var tagFile = TagLib.File.Create(filePath);
+
+ // Métadonnées de base
+ tagFile.Tag.Title = song.Title;
+ tagFile.Tag.Performers = new[] { song.Artist };
+ tagFile.Tag.Album = song.Album;
+
+ // Album artist (peut différer de l'artiste du track pour les compilations)
+ if (!string.IsNullOrEmpty(song.AlbumArtist))
+ {
+ tagFile.Tag.AlbumArtists = new[] { song.AlbumArtist };
+ }
+ else
+ {
+ tagFile.Tag.AlbumArtists = new[] { song.Artist };
+ }
+
+ // Numéro de piste
+ if (song.Track.HasValue)
+ {
+ tagFile.Tag.Track = (uint)song.Track.Value;
+ }
+
+ // Nombre total de pistes
+ if (song.TotalTracks.HasValue)
+ {
+ tagFile.Tag.TrackCount = (uint)song.TotalTracks.Value;
+ }
+
+ // Numéro de disque
+ if (song.DiscNumber.HasValue)
+ {
+ tagFile.Tag.Disc = (uint)song.DiscNumber.Value;
+ }
+
+ // Année
+ if (song.Year.HasValue)
+ {
+ tagFile.Tag.Year = (uint)song.Year.Value;
+ }
+
+ // Genre
+ if (!string.IsNullOrEmpty(song.Genre))
+ {
+ tagFile.Tag.Genres = new[] { song.Genre };
+ }
+
+ // BPM
+ if (song.Bpm.HasValue)
+ {
+ tagFile.Tag.BeatsPerMinute = (uint)song.Bpm.Value;
+ }
+
+ // ISRC (stocké dans le commentaire si pas de champ dédié, ou via MusicBrainz ID)
+ // TagLib ne supporte pas directement l'ISRC, mais on peut l'ajouter au commentaire
+ var comments = new List();
+ if (!string.IsNullOrEmpty(song.Isrc))
+ {
+ comments.Add($"ISRC: {song.Isrc}");
+ }
+
+ // Contributeurs dans le commentaire
+ if (song.Contributors.Count > 0)
+ {
+ tagFile.Tag.Composers = song.Contributors.ToArray();
+ }
+
+ // Copyright
+ if (!string.IsNullOrEmpty(song.Copyright))
+ {
+ tagFile.Tag.Copyright = song.Copyright;
+ }
+
+ // Commentaire avec infos supplémentaires
+ if (comments.Count > 0)
+ {
+ tagFile.Tag.Comment = string.Join(" | ", comments);
+ }
+
+ // Télécharger et intégrer la cover art
+ var coverUrl = song.CoverArtUrlLarge ?? song.CoverArtUrl;
+ if (!string.IsNullOrEmpty(coverUrl))
+ {
+ try
+ {
+ var coverData = await DownloadCoverArtAsync(coverUrl, cancellationToken);
+ if (coverData != null && coverData.Length > 0)
+ {
+ var mimeType = coverUrl.Contains(".png") ? "image/png" : "image/jpeg";
+ var picture = new TagLib.Picture
+ {
+ Type = TagLib.PictureType.FrontCover,
+ MimeType = mimeType,
+ Description = "Cover",
+ Data = new TagLib.ByteVector(coverData)
+ };
+ tagFile.Tag.Pictures = new TagLib.IPicture[] { picture };
+ _logger.LogInformation("Cover art embedded: {Size} bytes", coverData.Length);
+ }
+ }
+ catch (Exception ex)
+ {
+ _logger.LogWarning(ex, "Failed to download cover art from {Url}", coverUrl);
+ }
+ }
+
+ // Sauvegarder les modifications
+ tagFile.Save();
+ _logger.LogInformation("Metadata written successfully to: {Path}", filePath);
+ }
+ catch (Exception ex)
+ {
+ _logger.LogError(ex, "Failed to write metadata to: {Path}", filePath);
+ // Ne pas propager l'erreur - le fichier est téléchargé, juste sans métadonnées
+ }
+ }
+
+ ///
+ /// Télécharge la cover art depuis une URL
+ ///
+ private async Task DownloadCoverArtAsync(string url, CancellationToken cancellationToken)
+ {
+ try
+ {
+ var response = await _httpClient.GetAsync(url, cancellationToken);
+ response.EnsureSuccessStatusCode();
+ return await response.Content.ReadAsByteArrayAsync(cancellationToken);
+ }
+ catch (Exception ex)
+ {
+ _logger.LogWarning(ex, "Failed to download cover art from {Url}", url);
+ return null;
+ }
+ }
+
#endregion
#region Decryption
@@ -659,7 +811,7 @@ public static class PathHelper
///
public static string ResolveUniquePath(string basePath)
{
- if (!File.Exists(basePath))
+ if (!IOFile.Exists(basePath))
{
return basePath;
}
@@ -674,7 +826,7 @@ public static class PathHelper
{
uniquePath = Path.Combine(directory, $"{fileNameWithoutExt} ({counter}){extension}");
counter++;
- } while (File.Exists(uniquePath));
+ } while (IOFile.Exists(uniquePath));
return uniquePath;
}
diff --git a/octo-fiesta/Services/DeezerMetadataService.cs b/octo-fiesta/Services/DeezerMetadataService.cs
index d042178..57060e1 100644
--- a/octo-fiesta/Services/DeezerMetadataService.cs
+++ b/octo-fiesta/Services/DeezerMetadataService.cs
@@ -134,7 +134,65 @@ public class DeezerMetadataService : IMusicMetadataService
if (track.TryGetProperty("error", out _)) return null;
- return ParseDeezerTrack(track);
+ // Pour un track individuel, on récupère les métadonnées complètes
+ var song = ParseDeezerTrackFull(track);
+
+ // Récupérer les infos supplémentaires depuis l'album (genre, nombre total de tracks, label, copyright)
+ if (track.TryGetProperty("album", out var albumRef) &&
+ albumRef.TryGetProperty("id", out var albumIdEl))
+ {
+ var albumId = albumIdEl.GetInt64().ToString();
+ try
+ {
+ var albumUrl = $"{BaseUrl}/album/{albumId}";
+ var albumResponse = await _httpClient.GetAsync(albumUrl);
+ if (albumResponse.IsSuccessStatusCode)
+ {
+ var albumJson = await albumResponse.Content.ReadAsStringAsync();
+ var albumData = JsonDocument.Parse(albumJson).RootElement;
+
+ // Genre
+ if (albumData.TryGetProperty("genres", out var genres) &&
+ genres.TryGetProperty("data", out var genresData) &&
+ genresData.GetArrayLength() > 0 &&
+ genresData[0].TryGetProperty("name", out var genreName))
+ {
+ song.Genre = genreName.GetString();
+ }
+
+ // Nombre total de tracks
+ if (albumData.TryGetProperty("nb_tracks", out var nbTracks))
+ {
+ song.TotalTracks = nbTracks.GetInt32();
+ }
+
+ // Label
+ if (albumData.TryGetProperty("label", out var label))
+ {
+ song.Label = label.GetString();
+ }
+
+ // Cover art XL si pas déjà définie
+ if (string.IsNullOrEmpty(song.CoverArtUrlLarge))
+ {
+ if (albumData.TryGetProperty("cover_xl", out var coverXl))
+ {
+ song.CoverArtUrlLarge = coverXl.GetString();
+ }
+ else if (albumData.TryGetProperty("cover_big", out var coverBig))
+ {
+ song.CoverArtUrlLarge = coverBig.GetString();
+ }
+ }
+ }
+ }
+ catch
+ {
+ // Si on ne peut pas récupérer l'album, on continue avec les infos du track
+ }
+ }
+
+ return song;
}
public async Task GetAlbumAsync(string externalProvider, string externalId)
@@ -249,6 +307,128 @@ public class DeezerMetadataService : IMusicMetadataService
};
}
+ ///
+ /// Parse un track Deezer avec toutes les métadonnées disponibles
+ /// Utilisé pour GetSongAsync qui retourne des données complètes
+ ///
+ private Song ParseDeezerTrackFull(JsonElement track)
+ {
+ var externalId = track.GetProperty("id").GetInt64().ToString();
+
+ // Track position et disc number
+ int? trackNumber = track.TryGetProperty("track_position", out var trackPos)
+ ? trackPos.GetInt32()
+ : null;
+ int? discNumber = track.TryGetProperty("disk_number", out var diskNum)
+ ? diskNum.GetInt32()
+ : null;
+
+ // BPM
+ int? bpm = track.TryGetProperty("bpm", out var bpmVal) && bpmVal.ValueKind == JsonValueKind.Number
+ ? (int)bpmVal.GetDouble()
+ : null;
+
+ // ISRC
+ string? isrc = track.TryGetProperty("isrc", out var isrcVal)
+ ? isrcVal.GetString()
+ : null;
+
+ // Release date from album
+ string? releaseDate = null;
+ int? year = null;
+ if (track.TryGetProperty("release_date", out var relDate))
+ {
+ releaseDate = relDate.GetString();
+ if (!string.IsNullOrEmpty(releaseDate) && releaseDate.Length >= 4)
+ {
+ if (int.TryParse(releaseDate.Substring(0, 4), out var y))
+ year = y;
+ }
+ }
+ else if (track.TryGetProperty("album", out var albumForDate) &&
+ albumForDate.TryGetProperty("release_date", out var albumRelDate))
+ {
+ releaseDate = albumRelDate.GetString();
+ if (!string.IsNullOrEmpty(releaseDate) && releaseDate.Length >= 4)
+ {
+ if (int.TryParse(releaseDate.Substring(0, 4), out var y))
+ year = y;
+ }
+ }
+
+ // Contributors
+ var contributors = new List();
+ if (track.TryGetProperty("contributors", out var contribs))
+ {
+ foreach (var contrib in contribs.EnumerateArray())
+ {
+ if (contrib.TryGetProperty("name", out var contribName))
+ {
+ var name = contribName.GetString();
+ if (!string.IsNullOrEmpty(name))
+ contributors.Add(name);
+ }
+ }
+ }
+
+ // Album artist (premier artiste de l'album, ou artiste principal du track)
+ string? albumArtist = null;
+ if (track.TryGetProperty("album", out var albumForArtist) &&
+ albumForArtist.TryGetProperty("artist", out var albumArtistEl))
+ {
+ albumArtist = albumArtistEl.TryGetProperty("name", out var aName)
+ ? aName.GetString()
+ : null;
+ }
+
+ // Cover art URLs (différentes tailles)
+ string? coverMedium = null;
+ string? coverLarge = null;
+ if (track.TryGetProperty("album", out var albumForCover))
+ {
+ coverMedium = albumForCover.TryGetProperty("cover_medium", out var cm)
+ ? cm.GetString()
+ : null;
+ coverLarge = albumForCover.TryGetProperty("cover_xl", out var cxl)
+ ? cxl.GetString()
+ : (albumForCover.TryGetProperty("cover_big", out var cb) ? cb.GetString() : null);
+ }
+
+ return new Song
+ {
+ Id = $"ext-deezer-song-{externalId}",
+ Title = track.GetProperty("title").GetString() ?? "",
+ Artist = track.TryGetProperty("artist", out var artist)
+ ? artist.GetProperty("name").GetString() ?? ""
+ : "",
+ ArtistId = track.TryGetProperty("artist", out var artistForId)
+ ? $"ext-deezer-artist-{artistForId.GetProperty("id").GetInt64()}"
+ : null,
+ Album = track.TryGetProperty("album", out var album)
+ ? album.GetProperty("title").GetString() ?? ""
+ : "",
+ AlbumId = track.TryGetProperty("album", out var albumForId)
+ ? $"ext-deezer-album-{albumForId.GetProperty("id").GetInt64()}"
+ : null,
+ Duration = track.TryGetProperty("duration", out var duration)
+ ? duration.GetInt32()
+ : null,
+ Track = trackNumber,
+ DiscNumber = discNumber,
+ Year = year,
+ Bpm = bpm,
+ Isrc = isrc,
+ ReleaseDate = releaseDate,
+ AlbumArtist = albumArtist,
+ Contributors = contributors,
+ CoverArtUrl = coverMedium,
+ CoverArtUrlLarge = coverLarge,
+ IsLocal = false,
+ ExternalProvider = "deezer",
+ ExternalId = externalId
+ };
+ }
+
private Album ParseDeezerAlbum(JsonElement album)
{
var externalId = album.GetProperty("id").GetInt64().ToString();
diff --git a/octo-fiesta/octo-fiesta.csproj b/octo-fiesta/octo-fiesta.csproj
index 07c460a..ab568f4 100644
--- a/octo-fiesta/octo-fiesta.csproj
+++ b/octo-fiesta/octo-fiesta.csproj
@@ -11,6 +11,7 @@
+