diff --git a/.env.example b/.env.example index 87fca43..67dd47a 100644 --- a/.env.example +++ b/.env.example @@ -10,3 +10,7 @@ DEEZER_ARL=your-deezer-arl-token # Fallback ARL token (optional) DEEZER_ARL_FALLBACK= + +# Preferred audio quality: FLAC, MP3_320, MP3_128 (optional) +# If not specified, the highest available quality for your account will be used +DEEZER_QUALITY= diff --git a/README.md b/README.md index fed88b5..a6011e8 100644 --- a/README.md +++ b/README.md @@ -58,6 +58,10 @@ The easiest way to run Octo-Fiesta is with Docker Compose. # Deezer ARL token (required) DEEZER_ARL=your-deezer-arl-token + + # Preferred audio quality (optional): FLAC, MP3_320, MP3_128 + # If not set, the highest available quality for your account will be used + DEEZER_QUALITY= ``` 3. **Start the container** @@ -93,6 +97,7 @@ The easiest way to run Octo-Fiesta is with Docker Compose. |---------|-------------| | `Deezer:Arl` | Your Deezer ARL token (required for downloads) | | `Deezer:ArlFallback` | Backup ARL token if primary fails | +| `Deezer:Quality` | Preferred audio quality: `FLAC`, `MP3_320`, `MP3_128`. If not specified, the highest available quality for your account will be used | ### Getting a Deezer ARL Token diff --git a/docker-compose.yml b/docker-compose.yml index 7baf567..d8e1afd 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -15,5 +15,7 @@ services: - Deezer__Arl=${DEEZER_ARL} # Fallback ARL token (optional) - Deezer__ArlFallback=${DEEZER_ARL_FALLBACK:-} + # Preferred audio quality: FLAC, MP3_320, MP3_128 (optional, defaults to highest available) + - Deezer__Quality=${DEEZER_QUALITY:-} volumes: - ${DOWNLOAD_PATH:-./downloads}:/app/downloads diff --git a/octo-fiesta/Services/DeezerDownloadService.cs b/octo-fiesta/Services/DeezerDownloadService.cs index 0442bd6..a6d45bd 100644 --- a/octo-fiesta/Services/DeezerDownloadService.cs +++ b/octo-fiesta/Services/DeezerDownloadService.cs @@ -18,6 +18,11 @@ public class DeezerDownloaderSettings public string? Arl { get; set; } public string? ArlFallback { get; set; } public string DownloadPath { get; set; } = "./downloads"; + /// + /// Preferred audio quality: FLAC, MP3_320, MP3_128 + /// If not specified or unavailable, the highest available quality will be used. + /// + public string? Quality { get; set; } } /// @@ -35,6 +40,7 @@ public class DeezerDownloadService : IDownloadService private readonly string _downloadPath; private readonly string? _arl; private readonly string? _arlFallback; + private readonly string? _preferredQuality; private string? _apiToken; private string? _licenseToken; @@ -69,6 +75,7 @@ public class DeezerDownloadService : IDownloadService _downloadPath = configuration["Library:DownloadPath"] ?? "./downloads"; _arl = configuration["Deezer:Arl"]; _arlFallback = configuration["Deezer:ArlFallback"]; + _preferredQuality = configuration["Deezer:Quality"]; if (!Directory.Exists(_downloadPath)) { @@ -280,6 +287,9 @@ public class DeezerDownloadService : IDownloadService : ""; // Get download URL via media API + // Build format list based on preferred quality + var formatsList = BuildFormatsList(_preferredQuality); + var mediaRequest = new { license_token = _licenseToken, @@ -288,12 +298,7 @@ public class DeezerDownloadService : IDownloadService new { type = "FULL", - formats = new[] - { - new { cipher = "BF_CBC_STRIPE", format = "MP3_128" }, - new { cipher = "BF_CBC_STRIPE", format = "MP3_320" }, - new { cipher = "BF_CBC_STRIPE", format = "FLAC" } - } + formats = formatsList } }, track_tokens = new[] { trackToken } @@ -326,29 +331,61 @@ public class DeezerDownloadService : IDownloadService throw new Exception("No media sources available - track may be unavailable in your region"); } - string? downloadUrl = null; - string? format = null; - + // Build a dictionary of available formats + var availableFormats = new Dictionary(); foreach (var mediaItem in media.EnumerateArray()) { - if (mediaItem.TryGetProperty("sources", out var sources) && + if (mediaItem.TryGetProperty("format", out var formatEl) && + mediaItem.TryGetProperty("sources", out var sources) && sources.GetArrayLength() > 0) { - downloadUrl = sources[0].GetProperty("url").GetString(); - format = mediaItem.GetProperty("format").GetString(); + var fmt = formatEl.GetString(); + var url = sources[0].GetProperty("url").GetString(); + if (!string.IsNullOrEmpty(fmt) && !string.IsNullOrEmpty(url)) + { + availableFormats[fmt] = url; + } + } + } + + if (availableFormats.Count == 0) + { + throw new Exception("No download URL found in media sources - track may be region locked"); + } + + // Log available formats for debugging + _logger.LogInformation("Available formats from Deezer: {Formats}", string.Join(", ", availableFormats.Keys)); + + // Quality priority order (highest to lowest) + // Since we already filtered the requested formats based on preference, + // we just need to pick the best one available + var qualityPriority = new[] { "FLAC", "MP3_320", "MP3_128" }; + + string? selectedFormat = null; + string? downloadUrl = null; + + // Select the best available quality from what Deezer returned + foreach (var quality in qualityPriority) + { + if (availableFormats.TryGetValue(quality, out var url)) + { + selectedFormat = quality; + downloadUrl = url; break; } } if (string.IsNullOrEmpty(downloadUrl)) { - throw new Exception("No download URL found in media sources - track may be region locked"); + throw new Exception("No compatible format found in available media sources"); } + _logger.LogInformation("Selected quality: {Format}", selectedFormat); + return new DownloadResult { DownloadUrl = downloadUrl, - Format = format ?? "MP3_128", + Format = selectedFormat ?? "MP3_128", Title = title, Artist = artist }; @@ -642,6 +679,44 @@ public class DeezerDownloadService : IDownloadService #region Utility Methods + /// + /// Builds the list of formats to request from Deezer based on preferred quality. + /// If a specific quality is preferred, only request that quality and lower. + /// This prevents Deezer from returning higher quality formats when user wants a specific one. + /// + private static object[] BuildFormatsList(string? preferredQuality) + { + var allFormats = new[] + { + new { cipher = "BF_CBC_STRIPE", format = "FLAC" }, + new { cipher = "BF_CBC_STRIPE", format = "MP3_320" }, + new { cipher = "BF_CBC_STRIPE", format = "MP3_128" } + }; + + if (string.IsNullOrEmpty(preferredQuality)) + { + // No preference, request all formats (highest quality will be selected) + return allFormats; + } + + var preferred = preferredQuality.ToUpperInvariant(); + + return preferred switch + { + "FLAC" => allFormats, // Request all, FLAC will be preferred + "MP3_320" => new object[] + { + new { cipher = "BF_CBC_STRIPE", format = "MP3_320" }, + new { cipher = "BF_CBC_STRIPE", format = "MP3_128" } + }, + "MP3_128" => new object[] + { + new { cipher = "BF_CBC_STRIPE", format = "MP3_128" } + }, + _ => allFormats // Unknown preference, request all + }; + } + private async Task RetryWithBackoffAsync(Func> action, int maxRetries = 3, int initialDelayMs = 1000) { Exception? lastException = null;