feat: add configurable audio quality selection for Deezer downloads

This commit is contained in:
V1ck3s
2026-01-02 22:03:46 +01:00
committed by Vickes
parent 5b736ea61a
commit 7eb101ea29
4 changed files with 100 additions and 14 deletions

View File

@@ -10,3 +10,7 @@ DEEZER_ARL=your-deezer-arl-token
# Fallback ARL token (optional) # Fallback ARL token (optional)
DEEZER_ARL_FALLBACK= 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=

View File

@@ -58,6 +58,10 @@ The easiest way to run Octo-Fiesta is with Docker Compose.
# Deezer ARL token (required) # Deezer ARL token (required)
DEEZER_ARL=your-deezer-arl-token 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** 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:Arl` | Your Deezer ARL token (required for downloads) |
| `Deezer:ArlFallback` | Backup ARL token if primary fails | | `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 ### Getting a Deezer ARL Token

View File

@@ -15,5 +15,7 @@ services:
- Deezer__Arl=${DEEZER_ARL} - Deezer__Arl=${DEEZER_ARL}
# Fallback ARL token (optional) # Fallback ARL token (optional)
- Deezer__ArlFallback=${DEEZER_ARL_FALLBACK:-} - Deezer__ArlFallback=${DEEZER_ARL_FALLBACK:-}
# Preferred audio quality: FLAC, MP3_320, MP3_128 (optional, defaults to highest available)
- Deezer__Quality=${DEEZER_QUALITY:-}
volumes: volumes:
- ${DOWNLOAD_PATH:-./downloads}:/app/downloads - ${DOWNLOAD_PATH:-./downloads}:/app/downloads

View File

@@ -18,6 +18,11 @@ public class DeezerDownloaderSettings
public string? Arl { get; set; } public string? Arl { get; set; }
public string? ArlFallback { get; set; } public string? ArlFallback { get; set; }
public string DownloadPath { get; set; } = "./downloads"; public string DownloadPath { get; set; } = "./downloads";
/// <summary>
/// Preferred audio quality: FLAC, MP3_320, MP3_128
/// If not specified or unavailable, the highest available quality will be used.
/// </summary>
public string? Quality { get; set; }
} }
/// <summary> /// <summary>
@@ -35,6 +40,7 @@ public class DeezerDownloadService : IDownloadService
private readonly string _downloadPath; private readonly string _downloadPath;
private readonly string? _arl; private readonly string? _arl;
private readonly string? _arlFallback; private readonly string? _arlFallback;
private readonly string? _preferredQuality;
private string? _apiToken; private string? _apiToken;
private string? _licenseToken; private string? _licenseToken;
@@ -69,6 +75,7 @@ public class DeezerDownloadService : IDownloadService
_downloadPath = configuration["Library:DownloadPath"] ?? "./downloads"; _downloadPath = configuration["Library:DownloadPath"] ?? "./downloads";
_arl = configuration["Deezer:Arl"]; _arl = configuration["Deezer:Arl"];
_arlFallback = configuration["Deezer:ArlFallback"]; _arlFallback = configuration["Deezer:ArlFallback"];
_preferredQuality = configuration["Deezer:Quality"];
if (!Directory.Exists(_downloadPath)) if (!Directory.Exists(_downloadPath))
{ {
@@ -280,6 +287,9 @@ public class DeezerDownloadService : IDownloadService
: ""; : "";
// Get download URL via media API // Get download URL via media API
// Build format list based on preferred quality
var formatsList = BuildFormatsList(_preferredQuality);
var mediaRequest = new var mediaRequest = new
{ {
license_token = _licenseToken, license_token = _licenseToken,
@@ -288,12 +298,7 @@ public class DeezerDownloadService : IDownloadService
new new
{ {
type = "FULL", type = "FULL",
formats = new[] formats = formatsList
{
new { cipher = "BF_CBC_STRIPE", format = "MP3_128" },
new { cipher = "BF_CBC_STRIPE", format = "MP3_320" },
new { cipher = "BF_CBC_STRIPE", format = "FLAC" }
}
} }
}, },
track_tokens = new[] { trackToken } 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"); throw new Exception("No media sources available - track may be unavailable in your region");
} }
string? downloadUrl = null; // Build a dictionary of available formats
string? format = null; var availableFormats = new Dictionary<string, string>();
foreach (var mediaItem in media.EnumerateArray()) 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) sources.GetArrayLength() > 0)
{ {
downloadUrl = sources[0].GetProperty("url").GetString(); var fmt = formatEl.GetString();
format = mediaItem.GetProperty("format").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; break;
} }
} }
if (string.IsNullOrEmpty(downloadUrl)) 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 return new DownloadResult
{ {
DownloadUrl = downloadUrl, DownloadUrl = downloadUrl,
Format = format ?? "MP3_128", Format = selectedFormat ?? "MP3_128",
Title = title, Title = title,
Artist = artist Artist = artist
}; };
@@ -642,6 +679,44 @@ public class DeezerDownloadService : IDownloadService
#region Utility Methods #region Utility Methods
/// <summary>
/// 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.
/// </summary>
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<T> RetryWithBackoffAsync<T>(Func<Task<T>> action, int maxRetries = 3, int initialDelayMs = 1000) private async Task<T> RetryWithBackoffAsync<T>(Func<Task<T>> action, int maxRetries = 3, int initialDelayMs = 1000)
{ {
Exception? lastException = null; Exception? lastException = null;