mirror of
https://github.com/SoPat712/allstarr.git
synced 2026-02-09 23:55:10 -05:00
refactor: update comments and documentation to English for consistency
This commit is contained in:
@@ -1,13 +1,13 @@
|
|||||||
namespace octo_fiesta.Models;
|
namespace octo_fiesta.Models;
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Représente une chanson (locale ou externe)
|
/// Represents a song (local or external)
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public class Song
|
public class Song
|
||||||
{
|
{
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// ID unique. Pour les chansons externes, préfixé avec "ext-" + provider + "-" + id externe
|
/// Unique ID. For external songs, prefixed with "ext-" + provider + "-" + external id
|
||||||
/// Exemple: "ext-deezer-123456" ou "local-789"
|
/// Example: "ext-deezer-123456" or "local-789"
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public string Id { get; set; } = string.Empty;
|
public string Id { get; set; } = string.Empty;
|
||||||
|
|
||||||
@@ -16,7 +16,7 @@ public class Song
|
|||||||
public string? ArtistId { get; set; }
|
public string? ArtistId { get; set; }
|
||||||
public string Album { get; set; } = string.Empty;
|
public string Album { get; set; } = string.Empty;
|
||||||
public string? AlbumId { get; set; }
|
public string? AlbumId { get; set; }
|
||||||
public int? Duration { get; set; } // En secondes
|
public int? Duration { get; set; } // In seconds
|
||||||
public int? Track { get; set; }
|
public int? Track { get; set; }
|
||||||
public int? DiscNumber { get; set; }
|
public int? DiscNumber { get; set; }
|
||||||
public int? TotalTracks { get; set; }
|
public int? TotalTracks { get; set; }
|
||||||
@@ -25,12 +25,12 @@ public class Song
|
|||||||
public string? CoverArtUrl { get; set; }
|
public string? CoverArtUrl { get; set; }
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// URL de la cover en haute résolution (pour embedding)
|
/// High-resolution cover art URL (for embedding)
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public string? CoverArtUrlLarge { get; set; }
|
public string? CoverArtUrlLarge { get; set; }
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// BPM (beats per minute) si disponible
|
/// BPM (beats per minute) if available
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public int? Bpm { get; set; }
|
public int? Bpm { get; set; }
|
||||||
|
|
||||||
@@ -40,22 +40,22 @@ public class Song
|
|||||||
public string? Isrc { get; set; }
|
public string? Isrc { get; set; }
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Date de sortie complète (format: YYYY-MM-DD)
|
/// Full release date (format: YYYY-MM-DD)
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public string? ReleaseDate { get; set; }
|
public string? ReleaseDate { get; set; }
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Nom de l'album artiste (peut différer de l'artiste du track)
|
/// Album artist name (may differ from track artist)
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public string? AlbumArtist { get; set; }
|
public string? AlbumArtist { get; set; }
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Compositeur(s)
|
/// Composer(s)
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public string? Composer { get; set; }
|
public string? Composer { get; set; }
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Label de l'album
|
/// Album label
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public string? Label { get; set; }
|
public string? Label { get; set; }
|
||||||
|
|
||||||
@@ -65,33 +65,33 @@ public class Song
|
|||||||
public string? Copyright { get; set; }
|
public string? Copyright { get; set; }
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Artistes contributeurs (featurings, etc.)
|
/// Contributing artists (features, etc.)
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public List<string> Contributors { get; set; } = new();
|
public List<string> Contributors { get; set; } = new();
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Indique si la chanson est disponible localement ou doit être téléchargée
|
/// Indicates whether the song is available locally or needs to be downloaded
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public bool IsLocal { get; set; }
|
public bool IsLocal { get; set; }
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Provider externe (deezer, spotify, etc.) - null si local
|
/// External provider (deezer, spotify, etc.) - null if local
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public string? ExternalProvider { get; set; }
|
public string? ExternalProvider { get; set; }
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// ID sur le provider externe (pour le téléchargement)
|
/// ID on the external provider (for downloading)
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public string? ExternalId { get; set; }
|
public string? ExternalId { get; set; }
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Chemin du fichier local (si disponible)
|
/// Local file path (if available)
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public string? LocalPath { get; set; }
|
public string? LocalPath { get; set; }
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Représente un artiste
|
/// Represents an artist
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public class Artist
|
public class Artist
|
||||||
{
|
{
|
||||||
@@ -105,7 +105,7 @@ public class Artist
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Représente un album
|
/// Represents an album
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public class Album
|
public class Album
|
||||||
{
|
{
|
||||||
@@ -124,7 +124,7 @@ public class Album
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Résultat de recherche combinant résultats locaux et externes
|
/// Search result combining local and external results
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public class SearchResult
|
public class SearchResult
|
||||||
{
|
{
|
||||||
@@ -134,7 +134,7 @@ public class SearchResult
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// État du téléchargement d'une chanson
|
/// Download status of a song
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public enum DownloadStatus
|
public enum DownloadStatus
|
||||||
{
|
{
|
||||||
@@ -145,7 +145,7 @@ public enum DownloadStatus
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Information sur un téléchargement en cours ou terminé
|
/// Information about an ongoing or completed download
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public class DownloadInfo
|
public class DownloadInfo
|
||||||
{
|
{
|
||||||
@@ -153,7 +153,7 @@ public class DownloadInfo
|
|||||||
public string ExternalId { get; set; } = string.Empty;
|
public string ExternalId { get; set; } = string.Empty;
|
||||||
public string ExternalProvider { get; set; } = string.Empty;
|
public string ExternalProvider { get; set; } = string.Empty;
|
||||||
public DownloadStatus Status { get; set; }
|
public DownloadStatus Status { get; set; }
|
||||||
public double Progress { get; set; } // 0.0 à 1.0
|
public double Progress { get; set; } // 0.0 to 1.0
|
||||||
public string? LocalPath { get; set; }
|
public string? LocalPath { get; set; }
|
||||||
public string? ErrorMessage { get; set; }
|
public string? ErrorMessage { get; set; }
|
||||||
public DateTime StartedAt { get; set; }
|
public DateTime StartedAt { get; set; }
|
||||||
@@ -161,7 +161,7 @@ public class DownloadInfo
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Statut du scan de bibliothèque Subsonic
|
/// Subsonic library scan status
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public class ScanStatus
|
public class ScanStatus
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -14,7 +14,7 @@ builder.Services.AddSwaggerGen();
|
|||||||
builder.Services.Configure<SubsonicSettings>(
|
builder.Services.Configure<SubsonicSettings>(
|
||||||
builder.Configuration.GetSection("Subsonic"));
|
builder.Configuration.GetSection("Subsonic"));
|
||||||
|
|
||||||
// Services métier
|
// Business services
|
||||||
builder.Services.AddSingleton<ILocalLibraryService, LocalLibraryService>();
|
builder.Services.AddSingleton<ILocalLibraryService, LocalLibraryService>();
|
||||||
builder.Services.AddScoped<IMusicMetadataService, DeezerMetadataService>();
|
builder.Services.AddScoped<IMusicMetadataService, DeezerMetadataService>();
|
||||||
builder.Services.AddScoped<IDownloadService, DeezerDownloadService>();
|
builder.Services.AddScoped<IDownloadService, DeezerDownloadService>();
|
||||||
|
|||||||
@@ -11,7 +11,7 @@ using IOFile = System.IO.File;
|
|||||||
namespace octo_fiesta.Services;
|
namespace octo_fiesta.Services;
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Configuration pour le téléchargeur Deezer
|
/// Configuration for the Deezer downloader
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public class DeezerDownloaderSettings
|
public class DeezerDownloaderSettings
|
||||||
{
|
{
|
||||||
@@ -21,8 +21,8 @@ public class DeezerDownloaderSettings
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Port C# du DeezerDownloader JavaScript
|
/// C# port of the DeezerDownloader JavaScript
|
||||||
/// Gère l'authentification Deezer, le téléchargement et le déchiffrement des pistes
|
/// Handles Deezer authentication, track downloading and decryption
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public class DeezerDownloadService : IDownloadService
|
public class DeezerDownloadService : IDownloadService
|
||||||
{
|
{
|
||||||
@@ -84,7 +84,7 @@ public class DeezerDownloadService : IDownloadService
|
|||||||
|
|
||||||
var songId = $"ext-{externalProvider}-{externalId}";
|
var songId = $"ext-{externalProvider}-{externalId}";
|
||||||
|
|
||||||
// Vérifier si déjà téléchargé
|
// Check if already downloaded
|
||||||
var existingPath = await _localLibraryService.GetLocalPathForExternalSongAsync(externalProvider, externalId);
|
var existingPath = await _localLibraryService.GetLocalPathForExternalSongAsync(externalProvider, externalId);
|
||||||
if (existingPath != null && IOFile.Exists(existingPath))
|
if (existingPath != null && IOFile.Exists(existingPath))
|
||||||
{
|
{
|
||||||
@@ -92,7 +92,7 @@ public class DeezerDownloadService : IDownloadService
|
|||||||
return existingPath;
|
return existingPath;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Vérifier si téléchargement en cours
|
// Check if download in progress
|
||||||
if (_activeDownloads.TryGetValue(songId, out var activeDownload) && activeDownload.Status == DownloadStatus.InProgress)
|
if (_activeDownloads.TryGetValue(songId, out var activeDownload) && activeDownload.Status == DownloadStatus.InProgress)
|
||||||
{
|
{
|
||||||
_logger.LogInformation("Download already in progress for {SongId}", songId);
|
_logger.LogInformation("Download already in progress for {SongId}", songId);
|
||||||
@@ -112,7 +112,7 @@ public class DeezerDownloadService : IDownloadService
|
|||||||
await _downloadLock.WaitAsync(cancellationToken);
|
await _downloadLock.WaitAsync(cancellationToken);
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
// Récupérer les métadonnées
|
// Get metadata
|
||||||
var song = await _metadataService.GetSongAsync(externalProvider, externalId);
|
var song = await _metadataService.GetSongAsync(externalProvider, externalId);
|
||||||
if (song == null)
|
if (song == null)
|
||||||
{
|
{
|
||||||
@@ -140,7 +140,7 @@ public class DeezerDownloadService : IDownloadService
|
|||||||
song.LocalPath = localPath;
|
song.LocalPath = localPath;
|
||||||
await _localLibraryService.RegisterDownloadedSongAsync(song, localPath);
|
await _localLibraryService.RegisterDownloadedSongAsync(song, localPath);
|
||||||
|
|
||||||
// Déclencher un rescan de la bibliothèque Subsonic (avec debounce)
|
// Trigger a Subsonic library rescan (with debounce)
|
||||||
_ = _localLibraryService.TriggerLibraryScanAsync();
|
_ = _localLibraryService.TriggerLibraryScanAsync();
|
||||||
|
|
||||||
_logger.LogInformation("Download completed: {Path}", localPath);
|
_logger.LogInformation("Download completed: {Path}", localPath);
|
||||||
@@ -362,7 +362,7 @@ public class DeezerDownloadService : IDownloadService
|
|||||||
_logger.LogInformation("Track token obtained for: {Title} - {Artist}", downloadInfo.Title, downloadInfo.Artist);
|
_logger.LogInformation("Track token obtained for: {Title} - {Artist}", downloadInfo.Title, downloadInfo.Artist);
|
||||||
_logger.LogInformation("Using format: {Format}", downloadInfo.Format);
|
_logger.LogInformation("Using format: {Format}", downloadInfo.Format);
|
||||||
|
|
||||||
// Déterminer l'extension basée sur le format
|
// Determine extension based on format
|
||||||
var extension = downloadInfo.Format?.ToUpper() switch
|
var extension = downloadInfo.Format?.ToUpper() switch
|
||||||
{
|
{
|
||||||
"FLAC" => ".flac",
|
"FLAC" => ".flac",
|
||||||
@@ -379,7 +379,7 @@ public class DeezerDownloadService : IDownloadService
|
|||||||
// Resolve unique path if file already exists
|
// Resolve unique path if file already exists
|
||||||
outputPath = PathHelper.ResolveUniquePath(outputPath);
|
outputPath = PathHelper.ResolveUniquePath(outputPath);
|
||||||
|
|
||||||
// Télécharger le fichier chiffré
|
// Download the encrypted file
|
||||||
var response = await RetryWithBackoffAsync(async () =>
|
var response = await RetryWithBackoffAsync(async () =>
|
||||||
{
|
{
|
||||||
var request = new HttpRequestMessage(HttpMethod.Get, downloadInfo.DownloadUrl);
|
var request = new HttpRequestMessage(HttpMethod.Get, downloadInfo.DownloadUrl);
|
||||||
@@ -391,23 +391,23 @@ public class DeezerDownloadService : IDownloadService
|
|||||||
|
|
||||||
response.EnsureSuccessStatusCode();
|
response.EnsureSuccessStatusCode();
|
||||||
|
|
||||||
// Télécharger et déchiffrer
|
// Download and decrypt
|
||||||
await using var responseStream = await response.Content.ReadAsStreamAsync(cancellationToken);
|
await using var responseStream = await response.Content.ReadAsStreamAsync(cancellationToken);
|
||||||
await using var outputFile = IOFile.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
|
// Close file before writing metadata
|
||||||
await outputFile.DisposeAsync();
|
await outputFile.DisposeAsync();
|
||||||
|
|
||||||
// Écrire les métadonnées et la cover art
|
// Write metadata and cover art
|
||||||
await WriteMetadataAsync(outputPath, song, cancellationToken);
|
await WriteMetadataAsync(outputPath, song, cancellationToken);
|
||||||
|
|
||||||
return outputPath;
|
return outputPath;
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Écrit les métadonnées ID3/Vorbis et la cover art dans le fichier audio
|
/// Writes ID3/Vorbis metadata and cover art to the audio file
|
||||||
/// </summary>
|
/// </summary>
|
||||||
private async Task WriteMetadataAsync(string filePath, Song song, CancellationToken cancellationToken)
|
private async Task WriteMetadataAsync(string filePath, Song song, CancellationToken cancellationToken)
|
||||||
{
|
{
|
||||||
@@ -417,12 +417,12 @@ public class DeezerDownloadService : IDownloadService
|
|||||||
|
|
||||||
using var tagFile = TagLib.File.Create(filePath);
|
using var tagFile = TagLib.File.Create(filePath);
|
||||||
|
|
||||||
// Métadonnées de base
|
// Basic metadata
|
||||||
tagFile.Tag.Title = song.Title;
|
tagFile.Tag.Title = song.Title;
|
||||||
tagFile.Tag.Performers = new[] { song.Artist };
|
tagFile.Tag.Performers = new[] { song.Artist };
|
||||||
tagFile.Tag.Album = song.Album;
|
tagFile.Tag.Album = song.Album;
|
||||||
|
|
||||||
// Album artist (peut différer de l'artiste du track pour les compilations)
|
// Album artist (may differ from track artist for compilations)
|
||||||
if (!string.IsNullOrEmpty(song.AlbumArtist))
|
if (!string.IsNullOrEmpty(song.AlbumArtist))
|
||||||
{
|
{
|
||||||
tagFile.Tag.AlbumArtists = new[] { song.AlbumArtist };
|
tagFile.Tag.AlbumArtists = new[] { song.AlbumArtist };
|
||||||
@@ -432,25 +432,25 @@ public class DeezerDownloadService : IDownloadService
|
|||||||
tagFile.Tag.AlbumArtists = new[] { song.Artist };
|
tagFile.Tag.AlbumArtists = new[] { song.Artist };
|
||||||
}
|
}
|
||||||
|
|
||||||
// Numéro de piste
|
// Track number
|
||||||
if (song.Track.HasValue)
|
if (song.Track.HasValue)
|
||||||
{
|
{
|
||||||
tagFile.Tag.Track = (uint)song.Track.Value;
|
tagFile.Tag.Track = (uint)song.Track.Value;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Nombre total de pistes
|
// Total track count
|
||||||
if (song.TotalTracks.HasValue)
|
if (song.TotalTracks.HasValue)
|
||||||
{
|
{
|
||||||
tagFile.Tag.TrackCount = (uint)song.TotalTracks.Value;
|
tagFile.Tag.TrackCount = (uint)song.TotalTracks.Value;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Numéro de disque
|
// Disc number
|
||||||
if (song.DiscNumber.HasValue)
|
if (song.DiscNumber.HasValue)
|
||||||
{
|
{
|
||||||
tagFile.Tag.Disc = (uint)song.DiscNumber.Value;
|
tagFile.Tag.Disc = (uint)song.DiscNumber.Value;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Année
|
// Year
|
||||||
if (song.Year.HasValue)
|
if (song.Year.HasValue)
|
||||||
{
|
{
|
||||||
tagFile.Tag.Year = (uint)song.Year.Value;
|
tagFile.Tag.Year = (uint)song.Year.Value;
|
||||||
@@ -468,15 +468,15 @@ public class DeezerDownloadService : IDownloadService
|
|||||||
tagFile.Tag.BeatsPerMinute = (uint)song.Bpm.Value;
|
tagFile.Tag.BeatsPerMinute = (uint)song.Bpm.Value;
|
||||||
}
|
}
|
||||||
|
|
||||||
// ISRC (stocké dans le commentaire si pas de champ dédié, ou via MusicBrainz ID)
|
// ISRC (stored in comment if no dedicated field, or via MusicBrainz ID)
|
||||||
// TagLib ne supporte pas directement l'ISRC, mais on peut l'ajouter au commentaire
|
// TagLib doesn't directly support ISRC, but we can add it to comments
|
||||||
var comments = new List<string>();
|
var comments = new List<string>();
|
||||||
if (!string.IsNullOrEmpty(song.Isrc))
|
if (!string.IsNullOrEmpty(song.Isrc))
|
||||||
{
|
{
|
||||||
comments.Add($"ISRC: {song.Isrc}");
|
comments.Add($"ISRC: {song.Isrc}");
|
||||||
}
|
}
|
||||||
|
|
||||||
// Contributeurs dans le commentaire
|
// Contributors in comments
|
||||||
if (song.Contributors.Count > 0)
|
if (song.Contributors.Count > 0)
|
||||||
{
|
{
|
||||||
tagFile.Tag.Composers = song.Contributors.ToArray();
|
tagFile.Tag.Composers = song.Contributors.ToArray();
|
||||||
@@ -488,13 +488,13 @@ public class DeezerDownloadService : IDownloadService
|
|||||||
tagFile.Tag.Copyright = song.Copyright;
|
tagFile.Tag.Copyright = song.Copyright;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Commentaire avec infos supplémentaires
|
// Comment with additional info
|
||||||
if (comments.Count > 0)
|
if (comments.Count > 0)
|
||||||
{
|
{
|
||||||
tagFile.Tag.Comment = string.Join(" | ", comments);
|
tagFile.Tag.Comment = string.Join(" | ", comments);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Télécharger et intégrer la cover art
|
// Download and embed cover art
|
||||||
var coverUrl = song.CoverArtUrlLarge ?? song.CoverArtUrl;
|
var coverUrl = song.CoverArtUrlLarge ?? song.CoverArtUrl;
|
||||||
if (!string.IsNullOrEmpty(coverUrl))
|
if (!string.IsNullOrEmpty(coverUrl))
|
||||||
{
|
{
|
||||||
@@ -521,19 +521,19 @@ public class DeezerDownloadService : IDownloadService
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Sauvegarder les modifications
|
// Save changes
|
||||||
tagFile.Save();
|
tagFile.Save();
|
||||||
_logger.LogInformation("Metadata written successfully to: {Path}", filePath);
|
_logger.LogInformation("Metadata written successfully to: {Path}", filePath);
|
||||||
}
|
}
|
||||||
catch (Exception ex)
|
catch (Exception ex)
|
||||||
{
|
{
|
||||||
_logger.LogError(ex, "Failed to write metadata to: {Path}", filePath);
|
_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
|
// Don't propagate the error - the file is downloaded, just without metadata
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Télécharge la cover art depuis une URL
|
/// Downloads cover art from a URL
|
||||||
/// </summary>
|
/// </summary>
|
||||||
private async Task<byte[]?> DownloadCoverArtAsync(string url, CancellationToken cancellationToken)
|
private async Task<byte[]?> DownloadCoverArtAsync(string url, CancellationToken cancellationToken)
|
||||||
{
|
{
|
||||||
@@ -587,7 +587,7 @@ public class DeezerDownloadService : IDownloadService
|
|||||||
|
|
||||||
var chunk = buffer.AsSpan(0, bytesRead).ToArray();
|
var chunk = buffer.AsSpan(0, bytesRead).ToArray();
|
||||||
|
|
||||||
// Chaque 3ème chunk (index % 3 == 0) est chiffré
|
// Every 3rd chunk (index % 3 == 0) is encrypted
|
||||||
if (chunkIndex % 3 == 0 && bytesRead == 2048)
|
if (chunkIndex % 3 == 0 && bytesRead == 2048)
|
||||||
{
|
{
|
||||||
chunk = DecryptBlowfishCbc(chunk, bfKey, iv);
|
chunk = DecryptBlowfishCbc(chunk, bfKey, iv);
|
||||||
|
|||||||
@@ -4,7 +4,7 @@ using System.Text.Json;
|
|||||||
namespace octo_fiesta.Services;
|
namespace octo_fiesta.Services;
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Implémentation du service de métadonnées utilisant l'API Deezer (gratuite, pas besoin de clé)
|
/// Metadata service implementation using the Deezer API (free, no key required)
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public class DeezerMetadataService : IMusicMetadataService
|
public class DeezerMetadataService : IMusicMetadataService
|
||||||
{
|
{
|
||||||
@@ -105,7 +105,7 @@ public class DeezerMetadataService : IMusicMetadataService
|
|||||||
|
|
||||||
public async Task<SearchResult> SearchAllAsync(string query, int songLimit = 20, int albumLimit = 20, int artistLimit = 20)
|
public async Task<SearchResult> SearchAllAsync(string query, int songLimit = 20, int albumLimit = 20, int artistLimit = 20)
|
||||||
{
|
{
|
||||||
// Exécuter les recherches en parallèle
|
// Execute searches in parallel
|
||||||
var songsTask = SearchSongsAsync(query, songLimit);
|
var songsTask = SearchSongsAsync(query, songLimit);
|
||||||
var albumsTask = SearchAlbumsAsync(query, albumLimit);
|
var albumsTask = SearchAlbumsAsync(query, albumLimit);
|
||||||
var artistsTask = SearchArtistsAsync(query, artistLimit);
|
var artistsTask = SearchArtistsAsync(query, artistLimit);
|
||||||
@@ -134,10 +134,10 @@ public class DeezerMetadataService : IMusicMetadataService
|
|||||||
|
|
||||||
if (track.TryGetProperty("error", out _)) return null;
|
if (track.TryGetProperty("error", out _)) return null;
|
||||||
|
|
||||||
// Pour un track individuel, on récupère les métadonnées complètes
|
// For an individual track, get full metadata
|
||||||
var song = ParseDeezerTrackFull(track);
|
var song = ParseDeezerTrackFull(track);
|
||||||
|
|
||||||
// Récupérer les infos supplémentaires depuis l'album (genre, nombre total de tracks, label, copyright)
|
// Get additional info from album (genre, total track count, label, copyright)
|
||||||
if (track.TryGetProperty("album", out var albumRef) &&
|
if (track.TryGetProperty("album", out var albumRef) &&
|
||||||
albumRef.TryGetProperty("id", out var albumIdEl))
|
albumRef.TryGetProperty("id", out var albumIdEl))
|
||||||
{
|
{
|
||||||
@@ -160,7 +160,7 @@ public class DeezerMetadataService : IMusicMetadataService
|
|||||||
song.Genre = genreName.GetString();
|
song.Genre = genreName.GetString();
|
||||||
}
|
}
|
||||||
|
|
||||||
// Nombre total de tracks
|
// Total track count
|
||||||
if (albumData.TryGetProperty("nb_tracks", out var nbTracks))
|
if (albumData.TryGetProperty("nb_tracks", out var nbTracks))
|
||||||
{
|
{
|
||||||
song.TotalTracks = nbTracks.GetInt32();
|
song.TotalTracks = nbTracks.GetInt32();
|
||||||
@@ -172,7 +172,7 @@ public class DeezerMetadataService : IMusicMetadataService
|
|||||||
song.Label = label.GetString();
|
song.Label = label.GetString();
|
||||||
}
|
}
|
||||||
|
|
||||||
// Cover art XL si pas déjà définie
|
// Cover art XL if not already set
|
||||||
if (string.IsNullOrEmpty(song.CoverArtUrlLarge))
|
if (string.IsNullOrEmpty(song.CoverArtUrlLarge))
|
||||||
{
|
{
|
||||||
if (albumData.TryGetProperty("cover_xl", out var coverXl))
|
if (albumData.TryGetProperty("cover_xl", out var coverXl))
|
||||||
@@ -188,7 +188,7 @@ public class DeezerMetadataService : IMusicMetadataService
|
|||||||
}
|
}
|
||||||
catch
|
catch
|
||||||
{
|
{
|
||||||
// Si on ne peut pas récupérer l'album, on continue avec les infos du track
|
// If we can't get the album, continue with track info only
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -211,7 +211,7 @@ public class DeezerMetadataService : IMusicMetadataService
|
|||||||
|
|
||||||
var album = ParseDeezerAlbum(albumElement);
|
var album = ParseDeezerAlbum(albumElement);
|
||||||
|
|
||||||
// Récupérer les chansons de l'album
|
// Get album songs
|
||||||
if (albumElement.TryGetProperty("tracks", out var tracks) &&
|
if (albumElement.TryGetProperty("tracks", out var tracks) &&
|
||||||
tracks.TryGetProperty("data", out var tracksData))
|
tracks.TryGetProperty("data", out var tracksData))
|
||||||
{
|
{
|
||||||
@@ -308,8 +308,8 @@ public class DeezerMetadataService : IMusicMetadataService
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Parse un track Deezer avec toutes les métadonnées disponibles
|
/// Parses a Deezer track with all available metadata
|
||||||
/// Utilisé pour GetSongAsync qui retourne des données complètes
|
/// Used for GetSongAsync which returns complete data
|
||||||
/// </summary>
|
/// </summary>
|
||||||
private Song ParseDeezerTrackFull(JsonElement track)
|
private Song ParseDeezerTrackFull(JsonElement track)
|
||||||
{
|
{
|
||||||
@@ -371,7 +371,7 @@ public class DeezerMetadataService : IMusicMetadataService
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Album artist (premier artiste de l'album, ou artiste principal du track)
|
// Album artist (first artist from album, or main track artist)
|
||||||
string? albumArtist = null;
|
string? albumArtist = null;
|
||||||
if (track.TryGetProperty("album", out var albumForArtist) &&
|
if (track.TryGetProperty("album", out var albumForArtist) &&
|
||||||
albumForArtist.TryGetProperty("artist", out var albumArtistEl))
|
albumForArtist.TryGetProperty("artist", out var albumArtistEl))
|
||||||
@@ -381,7 +381,7 @@ public class DeezerMetadataService : IMusicMetadataService
|
|||||||
: null;
|
: null;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Cover art URLs (différentes tailles)
|
// Cover art URLs (different sizes)
|
||||||
string? coverMedium = null;
|
string? coverMedium = null;
|
||||||
string? coverLarge = null;
|
string? coverLarge = null;
|
||||||
if (track.TryGetProperty("album", out var albumForCover))
|
if (track.TryGetProperty("album", out var albumForCover))
|
||||||
|
|||||||
@@ -3,35 +3,35 @@ using octo_fiesta.Models;
|
|||||||
namespace octo_fiesta.Services;
|
namespace octo_fiesta.Services;
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Interface pour le service de téléchargement de musique (Deezspot ou autre)
|
/// Interface for the music download service (Deezspot or other)
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public interface IDownloadService
|
public interface IDownloadService
|
||||||
{
|
{
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Télécharge une chanson depuis un provider externe
|
/// Downloads a song from an external provider
|
||||||
/// </summary>
|
/// </summary>
|
||||||
/// <param name="externalProvider">Le provider (deezer, spotify)</param>
|
/// <param name="externalProvider">The provider (deezer, spotify)</param>
|
||||||
/// <param name="externalId">L'ID sur le provider externe</param>
|
/// <param name="externalId">The ID on the external provider</param>
|
||||||
/// <param name="cancellationToken">Token d'annulation</param>
|
/// <param name="cancellationToken">Cancellation token</param>
|
||||||
/// <returns>Le chemin du fichier téléchargé</returns>
|
/// <returns>The path to the downloaded file</returns>
|
||||||
Task<string> DownloadSongAsync(string externalProvider, string externalId, CancellationToken cancellationToken = default);
|
Task<string> DownloadSongAsync(string externalProvider, string externalId, CancellationToken cancellationToken = default);
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Télécharge une chanson et stream le résultat au fur et à mesure
|
/// Downloads a song and streams the result progressively
|
||||||
/// </summary>
|
/// </summary>
|
||||||
/// <param name="externalProvider">Le provider (deezer, spotify)</param>
|
/// <param name="externalProvider">The provider (deezer, spotify)</param>
|
||||||
/// <param name="externalId">L'ID sur le provider externe</param>
|
/// <param name="externalId">The ID on the external provider</param>
|
||||||
/// <param name="cancellationToken">Token d'annulation</param>
|
/// <param name="cancellationToken">Cancellation token</param>
|
||||||
/// <returns>Un stream du fichier audio</returns>
|
/// <returns>A stream of the audio file</returns>
|
||||||
Task<Stream> DownloadAndStreamAsync(string externalProvider, string externalId, CancellationToken cancellationToken = default);
|
Task<Stream> DownloadAndStreamAsync(string externalProvider, string externalId, CancellationToken cancellationToken = default);
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Vérifie si une chanson est en cours de téléchargement
|
/// Checks if a song is currently being downloaded
|
||||||
/// </summary>
|
/// </summary>
|
||||||
DownloadInfo? GetDownloadStatus(string songId);
|
DownloadInfo? GetDownloadStatus(string songId);
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Vérifie si le service est correctement configuré et fonctionnel
|
/// Checks if the service is properly configured and functional
|
||||||
/// </summary>
|
/// </summary>
|
||||||
Task<bool> IsAvailableAsync();
|
Task<bool> IsAvailableAsync();
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -3,51 +3,51 @@ using octo_fiesta.Models;
|
|||||||
namespace octo_fiesta.Services;
|
namespace octo_fiesta.Services;
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Interface pour le service de recherche de métadonnées musicales externes
|
/// Interface for external music metadata search service
|
||||||
/// (Deezer API, Spotify API, MusicBrainz, etc.)
|
/// (Deezer API, Spotify API, MusicBrainz, etc.)
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public interface IMusicMetadataService
|
public interface IMusicMetadataService
|
||||||
{
|
{
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Recherche des chansons sur les providers externes
|
/// Searches for songs on external providers
|
||||||
/// </summary>
|
/// </summary>
|
||||||
/// <param name="query">Terme de recherche</param>
|
/// <param name="query">Search term</param>
|
||||||
/// <param name="limit">Nombre maximum de résultats</param>
|
/// <param name="limit">Maximum number of results</param>
|
||||||
/// <returns>Liste des chansons trouvées</returns>
|
/// <returns>List of found songs</returns>
|
||||||
Task<List<Song>> SearchSongsAsync(string query, int limit = 20);
|
Task<List<Song>> SearchSongsAsync(string query, int limit = 20);
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Recherche des albums sur les providers externes
|
/// Searches for albums on external providers
|
||||||
/// </summary>
|
/// </summary>
|
||||||
Task<List<Album>> SearchAlbumsAsync(string query, int limit = 20);
|
Task<List<Album>> SearchAlbumsAsync(string query, int limit = 20);
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Recherche des artistes sur les providers externes
|
/// Searches for artists on external providers
|
||||||
/// </summary>
|
/// </summary>
|
||||||
Task<List<Artist>> SearchArtistsAsync(string query, int limit = 20);
|
Task<List<Artist>> SearchArtistsAsync(string query, int limit = 20);
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Recherche combinée (chansons, albums, artistes)
|
/// Combined search (songs, albums, artists)
|
||||||
/// </summary>
|
/// </summary>
|
||||||
Task<SearchResult> SearchAllAsync(string query, int songLimit = 20, int albumLimit = 20, int artistLimit = 20);
|
Task<SearchResult> SearchAllAsync(string query, int songLimit = 20, int albumLimit = 20, int artistLimit = 20);
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Récupère les détails d'une chanson externe
|
/// Gets details of an external song
|
||||||
/// </summary>
|
/// </summary>
|
||||||
Task<Song?> GetSongAsync(string externalProvider, string externalId);
|
Task<Song?> GetSongAsync(string externalProvider, string externalId);
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Récupère les détails d'un album externe avec ses chansons
|
/// Gets details of an external album with its songs
|
||||||
/// </summary>
|
/// </summary>
|
||||||
Task<Album?> GetAlbumAsync(string externalProvider, string externalId);
|
Task<Album?> GetAlbumAsync(string externalProvider, string externalId);
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Récupère les détails d'un artiste externe
|
/// Gets details of an external artist
|
||||||
/// </summary>
|
/// </summary>
|
||||||
Task<Artist?> GetArtistAsync(string externalProvider, string externalId);
|
Task<Artist?> GetArtistAsync(string externalProvider, string externalId);
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Récupère les albums d'un artiste
|
/// Gets an artist's albums
|
||||||
/// </summary>
|
/// </summary>
|
||||||
Task<List<Album>> GetArtistAlbumsAsync(string externalProvider, string externalId);
|
Task<List<Album>> GetArtistAlbumsAsync(string externalProvider, string externalId);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -6,51 +6,51 @@ using octo_fiesta.Models;
|
|||||||
namespace octo_fiesta.Services;
|
namespace octo_fiesta.Services;
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Interface pour la gestion de la bibliothèque locale de musiques
|
/// Interface for local music library management
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public interface ILocalLibraryService
|
public interface ILocalLibraryService
|
||||||
{
|
{
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Vérifie si une chanson externe existe déjà localement
|
/// Checks if an external song already exists locally
|
||||||
/// </summary>
|
/// </summary>
|
||||||
Task<string?> GetLocalPathForExternalSongAsync(string externalProvider, string externalId);
|
Task<string?> GetLocalPathForExternalSongAsync(string externalProvider, string externalId);
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Enregistre une chanson téléchargée dans la bibliothèque locale
|
/// Registers a downloaded song in the local library
|
||||||
/// </summary>
|
/// </summary>
|
||||||
Task RegisterDownloadedSongAsync(Song song, string localPath);
|
Task RegisterDownloadedSongAsync(Song song, string localPath);
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Récupère le mapping entre ID externe et ID local
|
/// Gets the mapping between external ID and local ID
|
||||||
/// </summary>
|
/// </summary>
|
||||||
Task<string?> GetLocalIdForExternalSongAsync(string externalProvider, string externalId);
|
Task<string?> GetLocalIdForExternalSongAsync(string externalProvider, string externalId);
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Parse un ID de chanson pour déterminer s'il est externe ou local
|
/// Parses a song ID to determine if it is external or local
|
||||||
/// </summary>
|
/// </summary>
|
||||||
(bool isExternal, string? provider, string? externalId) ParseSongId(string songId);
|
(bool isExternal, string? provider, string? externalId) ParseSongId(string songId);
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Parse un ID externe pour extraire le provider, le type et l'ID
|
/// Parses an external ID to extract the provider, type and ID
|
||||||
/// Format: ext-{provider}-{type}-{id} (ex: ext-deezer-artist-259, ext-deezer-album-96126, ext-deezer-song-12345)
|
/// Format: ext-{provider}-{type}-{id} (e.g., ext-deezer-artist-259, ext-deezer-album-96126, ext-deezer-song-12345)
|
||||||
/// Also supports legacy format: ext-{provider}-{id} (assumes song type)
|
/// Also supports legacy format: ext-{provider}-{id} (assumes song type)
|
||||||
/// </summary>
|
/// </summary>
|
||||||
(bool isExternal, string? provider, string? type, string? externalId) ParseExternalId(string id);
|
(bool isExternal, string? provider, string? type, string? externalId) ParseExternalId(string id);
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Déclenche un scan de la bibliothèque Subsonic
|
/// Triggers a Subsonic library scan
|
||||||
/// </summary>
|
/// </summary>
|
||||||
Task<bool> TriggerLibraryScanAsync();
|
Task<bool> TriggerLibraryScanAsync();
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Récupère le statut actuel du scan
|
/// Gets the current scan status
|
||||||
/// </summary>
|
/// </summary>
|
||||||
Task<ScanStatus?> GetScanStatusAsync();
|
Task<ScanStatus?> GetScanStatusAsync();
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Implémentation du service de bibliothèque locale
|
/// Local library service implementation
|
||||||
/// Utilise un fichier JSON simple pour stocker les mappings (peut être remplacé par une BDD)
|
/// Uses a simple JSON file to store mappings (can be replaced with a database)
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public class LocalLibraryService : ILocalLibraryService
|
public class LocalLibraryService : ILocalLibraryService
|
||||||
{
|
{
|
||||||
@@ -62,7 +62,7 @@ public class LocalLibraryService : ILocalLibraryService
|
|||||||
private Dictionary<string, LocalSongMapping>? _mappings;
|
private Dictionary<string, LocalSongMapping>? _mappings;
|
||||||
private readonly SemaphoreSlim _lock = new(1, 1);
|
private readonly SemaphoreSlim _lock = new(1, 1);
|
||||||
|
|
||||||
// Debounce pour éviter de déclencher trop de scans
|
// Debounce to avoid triggering too many scans
|
||||||
private DateTime _lastScanTrigger = DateTime.MinValue;
|
private DateTime _lastScanTrigger = DateTime.MinValue;
|
||||||
private readonly TimeSpan _scanDebounceInterval = TimeSpan.FromSeconds(30);
|
private readonly TimeSpan _scanDebounceInterval = TimeSpan.FromSeconds(30);
|
||||||
|
|
||||||
@@ -128,8 +128,8 @@ public class LocalLibraryService : ILocalLibraryService
|
|||||||
|
|
||||||
public async Task<string?> GetLocalIdForExternalSongAsync(string externalProvider, string externalId)
|
public async Task<string?> GetLocalIdForExternalSongAsync(string externalProvider, string externalId)
|
||||||
{
|
{
|
||||||
// Pour l'instant, on retourne null car on n'a pas encore d'intégration
|
// For now, return null as we don't yet have integration
|
||||||
// avec le serveur Subsonic pour récupérer l'ID local après scan
|
// with the Subsonic server to retrieve local ID after scan
|
||||||
await Task.CompletedTask;
|
await Task.CompletedTask;
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
@@ -206,7 +206,7 @@ public class LocalLibraryService : ILocalLibraryService
|
|||||||
|
|
||||||
public async Task<bool> TriggerLibraryScanAsync()
|
public async Task<bool> TriggerLibraryScanAsync()
|
||||||
{
|
{
|
||||||
// Debounce: éviter de déclencher trop de scans successifs
|
// Debounce: avoid triggering too many successive scans
|
||||||
var now = DateTime.UtcNow;
|
var now = DateTime.UtcNow;
|
||||||
if (now - _lastScanTrigger < _scanDebounceInterval)
|
if (now - _lastScanTrigger < _scanDebounceInterval)
|
||||||
{
|
{
|
||||||
@@ -219,8 +219,8 @@ public class LocalLibraryService : ILocalLibraryService
|
|||||||
|
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
// Appel à l'API Subsonic pour déclencher un scan
|
// Call Subsonic API to trigger a scan
|
||||||
// Note: Les credentials doivent être passés en paramètres (u, p ou t+s)
|
// Note: Credentials must be passed as parameters (u, p or t+s)
|
||||||
var url = $"{_subsonicSettings.Url}/rest/startScan?f=json";
|
var url = $"{_subsonicSettings.Url}/rest/startScan?f=json";
|
||||||
|
|
||||||
_logger.LogInformation("Triggering Subsonic library scan...");
|
_logger.LogInformation("Triggering Subsonic library scan...");
|
||||||
@@ -280,7 +280,7 @@ public class LocalLibraryService : ILocalLibraryService
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Représente le mapping entre une chanson externe et son fichier local
|
/// Represents the mapping between an external song and its local file
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public class LocalSongMapping
|
public class LocalSongMapping
|
||||||
{
|
{
|
||||||
|
|||||||
Reference in New Issue
Block a user