mirror of
https://github.com/SoPat712/allstarr.git
synced 2026-02-09 23:55:10 -05:00
feat: add metadatas to file after download
This commit is contained in:
@@ -18,10 +18,57 @@ public class Song
|
|||||||
public string? AlbumId { get; set; }
|
public string? AlbumId { get; set; }
|
||||||
public int? Duration { get; set; } // En secondes
|
public int? Duration { get; set; } // En secondes
|
||||||
public int? Track { get; set; }
|
public int? Track { get; set; }
|
||||||
|
public int? DiscNumber { get; set; }
|
||||||
|
public int? TotalTracks { get; set; }
|
||||||
public int? Year { get; set; }
|
public int? Year { get; set; }
|
||||||
public string? Genre { get; set; }
|
public string? Genre { get; set; }
|
||||||
public string? CoverArtUrl { get; set; }
|
public string? CoverArtUrl { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// URL de la cover en haute résolution (pour embedding)
|
||||||
|
/// </summary>
|
||||||
|
public string? CoverArtUrlLarge { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// BPM (beats per minute) si disponible
|
||||||
|
/// </summary>
|
||||||
|
public int? Bpm { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// ISRC (International Standard Recording Code)
|
||||||
|
/// </summary>
|
||||||
|
public string? Isrc { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Date de sortie complète (format: YYYY-MM-DD)
|
||||||
|
/// </summary>
|
||||||
|
public string? ReleaseDate { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Nom de l'album artiste (peut différer de l'artiste du track)
|
||||||
|
/// </summary>
|
||||||
|
public string? AlbumArtist { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Compositeur(s)
|
||||||
|
/// </summary>
|
||||||
|
public string? Composer { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Label de l'album
|
||||||
|
/// </summary>
|
||||||
|
public string? Label { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Copyright
|
||||||
|
/// </summary>
|
||||||
|
public string? Copyright { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Artistes contributeurs (featurings, etc.)
|
||||||
|
/// </summary>
|
||||||
|
public List<string> Contributors { get; set; } = new();
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Indique si la chanson est disponible localement ou doit être téléchargée
|
/// Indique si la chanson est disponible localement ou doit être téléchargée
|
||||||
/// </summary>
|
/// </summary>
|
||||||
|
|||||||
@@ -5,6 +5,8 @@ using Org.BouncyCastle.Crypto.Engines;
|
|||||||
using Org.BouncyCastle.Crypto.Modes;
|
using Org.BouncyCastle.Crypto.Modes;
|
||||||
using Org.BouncyCastle.Crypto.Parameters;
|
using Org.BouncyCastle.Crypto.Parameters;
|
||||||
using octo_fiesta.Models;
|
using octo_fiesta.Models;
|
||||||
|
using TagLib;
|
||||||
|
using IOFile = System.IO.File;
|
||||||
|
|
||||||
namespace octo_fiesta.Services;
|
namespace octo_fiesta.Services;
|
||||||
|
|
||||||
@@ -84,7 +86,7 @@ public class DeezerDownloadService : IDownloadService
|
|||||||
|
|
||||||
// Vérifier si déjà téléchargé
|
// Vérifier si déjà téléchargé
|
||||||
var existingPath = await _localLibraryService.GetLocalPathForExternalSongAsync(externalProvider, externalId);
|
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);
|
_logger.LogInformation("Song already downloaded: {Path}", existingPath);
|
||||||
return existingPath;
|
return existingPath;
|
||||||
@@ -161,7 +163,7 @@ public class DeezerDownloadService : IDownloadService
|
|||||||
public async Task<Stream> DownloadAndStreamAsync(string externalProvider, string externalId, CancellationToken cancellationToken = default)
|
public async Task<Stream> DownloadAndStreamAsync(string externalProvider, string externalId, CancellationToken cancellationToken = default)
|
||||||
{
|
{
|
||||||
var localPath = await DownloadSongAsync(externalProvider, externalId, cancellationToken);
|
var localPath = await DownloadSongAsync(externalProvider, externalId, cancellationToken);
|
||||||
return File.OpenRead(localPath);
|
return IOFile.OpenRead(localPath);
|
||||||
}
|
}
|
||||||
|
|
||||||
public DownloadInfo? GetDownloadStatus(string songId)
|
public DownloadInfo? GetDownloadStatus(string songId)
|
||||||
@@ -391,13 +393,163 @@ public class DeezerDownloadService : IDownloadService
|
|||||||
|
|
||||||
// Télécharger et déchiffrer
|
// Télécharger et déchiffrer
|
||||||
await using var responseStream = await response.Content.ReadAsStreamAsync(cancellationToken);
|
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);
|
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;
|
return outputPath;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Écrit les métadonnées ID3/Vorbis et la cover art dans le fichier audio
|
||||||
|
/// </summary>
|
||||||
|
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<string>();
|
||||||
|
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
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Télécharge la cover art depuis une URL
|
||||||
|
/// </summary>
|
||||||
|
private async Task<byte[]?> 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
|
#endregion
|
||||||
|
|
||||||
#region Decryption
|
#region Decryption
|
||||||
@@ -659,7 +811,7 @@ public static class PathHelper
|
|||||||
/// </summary>
|
/// </summary>
|
||||||
public static string ResolveUniquePath(string basePath)
|
public static string ResolveUniquePath(string basePath)
|
||||||
{
|
{
|
||||||
if (!File.Exists(basePath))
|
if (!IOFile.Exists(basePath))
|
||||||
{
|
{
|
||||||
return basePath;
|
return basePath;
|
||||||
}
|
}
|
||||||
@@ -674,7 +826,7 @@ public static class PathHelper
|
|||||||
{
|
{
|
||||||
uniquePath = Path.Combine(directory, $"{fileNameWithoutExt} ({counter}){extension}");
|
uniquePath = Path.Combine(directory, $"{fileNameWithoutExt} ({counter}){extension}");
|
||||||
counter++;
|
counter++;
|
||||||
} while (File.Exists(uniquePath));
|
} while (IOFile.Exists(uniquePath));
|
||||||
|
|
||||||
return uniquePath;
|
return uniquePath;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -134,7 +134,65 @@ public class DeezerMetadataService : IMusicMetadataService
|
|||||||
|
|
||||||
if (track.TryGetProperty("error", out _)) return null;
|
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<Album?> GetAlbumAsync(string externalProvider, string externalId)
|
public async Task<Album?> GetAlbumAsync(string externalProvider, string externalId)
|
||||||
@@ -249,6 +307,128 @@ public class DeezerMetadataService : IMusicMetadataService
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Parse un track Deezer avec toutes les métadonnées disponibles
|
||||||
|
/// Utilisé pour GetSongAsync qui retourne des données complètes
|
||||||
|
/// </summary>
|
||||||
|
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<string>();
|
||||||
|
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)
|
private Album ParseDeezerAlbum(JsonElement album)
|
||||||
{
|
{
|
||||||
var externalId = album.GetProperty("id").GetInt64().ToString();
|
var externalId = album.GetProperty("id").GetInt64().ToString();
|
||||||
|
|||||||
@@ -11,6 +11,7 @@
|
|||||||
<PackageReference Include="BouncyCastle.Cryptography" Version="2.6.2" />
|
<PackageReference Include="BouncyCastle.Cryptography" Version="2.6.2" />
|
||||||
<PackageReference Include="Microsoft.AspNetCore.OpenApi" Version="9.0.4" />
|
<PackageReference Include="Microsoft.AspNetCore.OpenApi" Version="9.0.4" />
|
||||||
<PackageReference Include="Swashbuckle.AspNetCore" Version="9.0.4" />
|
<PackageReference Include="Swashbuckle.AspNetCore" Version="9.0.4" />
|
||||||
|
<PackageReference Include="TagLibSharp" Version="2.3.0" />
|
||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
|
|
||||||
</Project>
|
</Project>
|
||||||
|
|||||||
Reference in New Issue
Block a user