mirror of
https://github.com/SoPat712/allstarr.git
synced 2026-10-07 14:03:14 -04:00
Compare commits
2
Commits
90f79c8d31
...
93894ba8de
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
93894ba8de
|
||
|
|
ac630455b1
|
No files matched your search
@@ -1,5 +1,6 @@
|
||||
using allstarr.Core.Capabilities;
|
||||
using allstarr.Services.Common;
|
||||
using Microsoft.AspNetCore.Http;
|
||||
|
||||
namespace allstarr.Tests;
|
||||
|
||||
@@ -28,4 +29,24 @@ public sealed class AudioQualityPolicyTests
|
||||
public void SubsonicBandwidthCap_CanOnlyLowerPlaybackQuality(
|
||||
string? maxBitRate, ProviderAudioQuality expected) =>
|
||||
Assert.Equal(expected, StreamQualityHelper.FromSubsonicMaxBitRate(maxBitRate));
|
||||
|
||||
[Theory]
|
||||
[InlineData("AudioCodec", "aac")]
|
||||
[InlineData("AudioCodec", "flac,aac")]
|
||||
[InlineData("TranscodingContainer", "m4a")]
|
||||
public void JellyfinLossyRequest_OutranksBroadDeviceBandwidth(
|
||||
string key,
|
||||
string value)
|
||||
{
|
||||
var context = new DefaultHttpContext();
|
||||
context.Request.QueryString = QueryString.Create(
|
||||
new KeyValuePair<string, string?>[]
|
||||
{
|
||||
new("MaxStreamingBitrate", "140000000"),
|
||||
new(key, value)
|
||||
});
|
||||
|
||||
Assert.Equal(StreamQuality.High,
|
||||
StreamQualityHelper.ParseFromQueryString(context.Request.Query));
|
||||
}
|
||||
}
|
||||
@@ -14,6 +14,6 @@ public sealed class MigrationModelSnapshotTests
|
||||
using var context = new AllstarrDbContext(options);
|
||||
|
||||
Assert.False(context.Database.HasPendingModelChanges());
|
||||
Assert.Equal("20260820173000_RepairProviderAccountCreatorIdentity", context.Database.GetMigrations().Last());
|
||||
Assert.Equal("20260825020000_EnforcePlaylistImportPolicies", context.Database.GetMigrations().Last());
|
||||
}
|
||||
}
|
||||
@@ -65,23 +65,23 @@ public partial class JellyfinController
|
||||
[HttpGet("Items/Root", Order = 1)]
|
||||
public async Task<IActionResult> GetMusicLibraryRoot()
|
||||
{
|
||||
var musicLibraryId = await _proxyService.GetMusicLibraryIdForFilteringAsync();
|
||||
if (string.IsNullOrWhiteSpace(musicLibraryId))
|
||||
{
|
||||
return StatusCode(StatusCodes.Status503ServiceUnavailable, new
|
||||
{
|
||||
error = "A Jellyfin music library could not be identified."
|
||||
});
|
||||
}
|
||||
|
||||
var credentialQuery = Request.Query
|
||||
.Where(item => item.Key.Equals("api_key", StringComparison.OrdinalIgnoreCase) ||
|
||||
item.Key.Equals("access_token", StringComparison.OrdinalIgnoreCase) ||
|
||||
item.Key.Equals("ApiKey", StringComparison.OrdinalIgnoreCase) ||
|
||||
item.Key.Equals("UserId", StringComparison.OrdinalIgnoreCase))
|
||||
.SelectMany(item => item.Value.Select(value =>
|
||||
new KeyValuePair<string, string?>(item.Key, value)));
|
||||
var endpoint = $"Items/{Uri.EscapeDataString(musicLibraryId)}{QueryString.Create(credentialQuery)}";
|
||||
new KeyValuePair<string, string?>(item.Key, value)))
|
||||
.ToArray();
|
||||
var queryString = QueryString.Create(credentialQuery);
|
||||
var musicLibraryId = await _proxyService.GetMusicLibraryIdForFilteringAsync(
|
||||
queryString.Value,
|
||||
Request.Headers);
|
||||
if (string.IsNullOrWhiteSpace(musicLibraryId))
|
||||
return StatusCode(StatusCodes.Status503ServiceUnavailable,
|
||||
new { error = "A Jellyfin music library could not be identified." });
|
||||
|
||||
var endpoint = $"Items/{Uri.EscapeDataString(musicLibraryId)}{queryString}";
|
||||
var (body, statusCode) = await _proxyService.GetJsonAsync(endpoint, null, Request.Headers);
|
||||
return HandleProxyResponse(body, statusCode);
|
||||
}
|
||||
@@ -129,9 +129,7 @@ public partial class JellyfinController
|
||||
var song = await GetProviderSongAsync(provider, externalId, HttpContext.RequestAborted);
|
||||
if (song == null) return NotFound(new { error = "Track metadata was not found." });
|
||||
|
||||
// PlaybackInfo must describe the same synthesized item that the client
|
||||
// requested. Provider metadata adapters may return a canonical/alternate
|
||||
// identifier; using it here would build stream URLs for a different item.
|
||||
// A provider's canonical ID could build stream URLs for a different synthetic item.
|
||||
song.Id = itemId;
|
||||
song.ExternalProvider = provider;
|
||||
song.ExternalId = externalId;
|
||||
|
||||
@@ -2,42 +2,13 @@ using allstarr.Core.Capabilities;
|
||||
|
||||
namespace allstarr.Services.Common;
|
||||
|
||||
/// <summary>
|
||||
/// Represents the quality tier requested by a client for streaming.
|
||||
/// Used to map client transcoding parameters to provider-specific quality levels.
|
||||
/// The shared audio-quality setting is the maximum; client requests can only go lower.
|
||||
/// </summary>
|
||||
public enum StreamQuality
|
||||
{
|
||||
/// <summary>
|
||||
/// Use the quality configured in Settings (default behavior).
|
||||
/// This is the "Lossless" / "no transcoding" selection in a client.
|
||||
/// </summary>
|
||||
Original,
|
||||
|
||||
/// <summary>
|
||||
/// High quality lossy (e.g., 320kbps AAC/MP3).
|
||||
/// Covers client selections: 320K, 256K, 192K.
|
||||
/// Maps to: Deezer MP3_320, Qobuz MP3_320.
|
||||
/// </summary>
|
||||
High,
|
||||
|
||||
/// <summary>
|
||||
/// Low quality lossy (e.g., 96-128kbps AAC/MP3).
|
||||
/// Covers client selections: 128K, 64K.
|
||||
/// Maps to: Deezer MP3_128, Qobuz MP3_320 (lowest available).
|
||||
/// </summary>
|
||||
Low
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Parses Jellyfin client transcoding query parameters to determine
|
||||
/// the requested stream quality tier for external tracks.
|
||||
///
|
||||
/// Typical client quality options: Lossless, 320K, 256K, 192K, 128K, 64K
|
||||
/// These are mapped to StreamQuality tiers which providers then translate
|
||||
/// to their own quality levels, capped at the shared setting.
|
||||
/// </summary>
|
||||
public static class StreamQualityHelper
|
||||
{
|
||||
public static ProviderAudioQuality FromSubsonicMaxBitRate(string? kilobitsPerSecond) =>
|
||||
@@ -45,74 +16,39 @@ public static class StreamQualityHelper
|
||||
? value < 192 ? ProviderAudioQuality.DataSaver : ProviderAudioQuality.Lossy
|
||||
: ProviderAudioQuality.Any;
|
||||
|
||||
/// <summary>
|
||||
/// Parses the request query string to determine what quality the client wants.
|
||||
/// Jellyfin clients send parameters like AudioBitRate, MaxStreamingBitrate,
|
||||
/// AudioCodec, TranscodingContainer when requesting transcoded streams.
|
||||
/// </summary>
|
||||
public static StreamQuality ParseFromQueryString(IQueryCollection query)
|
||||
{
|
||||
// Check for explicit audio bitrate (e.g., AudioBitRate=128000)
|
||||
if (query.TryGetValue("AudioBitRate", out var audioBitRateVal) &&
|
||||
int.TryParse(audioBitRateVal.FirstOrDefault(), out var audioBitRate))
|
||||
{
|
||||
if (TryReadBitRate(query, "AudioBitRate", out var audioBitRate) ||
|
||||
TryReadBitRate(query, "audioBitRate", out audioBitRate))
|
||||
return MapBitRateToQuality(audioBitRate);
|
||||
}
|
||||
|
||||
// Check for MaxStreamingBitrate (e.g., MaxStreamingBitrate=140000000 for lossless)
|
||||
if (RequestsLossyAudio(query, "AudioCodec") ||
|
||||
RequestsLossyAudio(query, "TranscodingContainer"))
|
||||
return StreamQuality.High;
|
||||
|
||||
if (query.TryGetValue("MaxStreamingBitrate", out var maxBitrateVal) &&
|
||||
long.TryParse(maxBitrateVal.FirstOrDefault(), out var maxBitrate))
|
||||
{
|
||||
// Very high values (>= 10Mbps) indicate lossless / no transcoding
|
||||
if (maxBitrate >= 10_000_000)
|
||||
{
|
||||
return StreamQuality.Original;
|
||||
}
|
||||
return maxBitrate >= 10_000_000
|
||||
? StreamQuality.Original
|
||||
: MapBitRateToQuality((int)maxBitrate);
|
||||
|
||||
// MaxStreamingBitrate is reported in bits per second.
|
||||
return MapBitRateToQuality((int)maxBitrate);
|
||||
}
|
||||
|
||||
// Check for audioBitRate (lowercase variant used by some clients)
|
||||
if (query.TryGetValue("audioBitRate", out var audioBitRateLower) &&
|
||||
int.TryParse(audioBitRateLower.FirstOrDefault(), out var audioBitRateLowerVal))
|
||||
{
|
||||
return MapBitRateToQuality(audioBitRateLowerVal);
|
||||
}
|
||||
|
||||
// Check TranscodingContainer — if client requests mp3/aac, they want lossy
|
||||
if (query.TryGetValue("TranscodingContainer", out var container))
|
||||
{
|
||||
var containerStr = container.FirstOrDefault()?.ToLowerInvariant();
|
||||
if (containerStr is "mp3" or "aac" or "m4a")
|
||||
{
|
||||
// Container specified but no bitrate — default to High (320kbps)
|
||||
return StreamQuality.High;
|
||||
}
|
||||
}
|
||||
|
||||
// No transcoding parameters — use the quality selected in Settings.
|
||||
return StreamQuality.Original;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Maps a bitrate value (in bps) to a StreamQuality tier.
|
||||
/// Client options are typically: Lossless, 320K, 256K, 192K, 128K, 64K
|
||||
///
|
||||
/// >= 192kbps → High (covers 320K, 256K, 192K selections)
|
||||
/// < 192kbps → Low (covers 128K, 64K selections)
|
||||
/// </summary>
|
||||
internal static StreamQuality MapBitRateToQuality(int bitRate)
|
||||
{
|
||||
// >= 192kbps → High (320kbps tier)
|
||||
// Covers client selections: 320K, 256K, 192K
|
||||
if (bitRate >= 192_000)
|
||||
{
|
||||
return StreamQuality.High;
|
||||
}
|
||||
private static bool RequestsLossyAudio(IQueryCollection query, string key) =>
|
||||
query.TryGetValue(key, out var values) &&
|
||||
values.SelectMany(value => value?.Split(
|
||||
',', StringSplitOptions.RemoveEmptyEntries | StringSplitOptions.TrimEntries) ?? [])
|
||||
.Any(value => value.Equals("mp3", StringComparison.OrdinalIgnoreCase) ||
|
||||
value.Equals("aac", StringComparison.OrdinalIgnoreCase) ||
|
||||
value.Equals("m4a", StringComparison.OrdinalIgnoreCase));
|
||||
|
||||
// < 192kbps → Low (96-128kbps tier)
|
||||
// Covers client selections: 128K, 64K
|
||||
return StreamQuality.Low;
|
||||
private static bool TryReadBitRate(IQueryCollection query, string key, out int bitRate)
|
||||
{
|
||||
bitRate = 0;
|
||||
return query.TryGetValue(key, out var value) && int.TryParse(value.FirstOrDefault(), out bitRate);
|
||||
}
|
||||
|
||||
internal static StreamQuality MapBitRateToQuality(int bitRate) =>
|
||||
bitRate >= 192_000 ? StreamQuality.High : StreamQuality.Low;
|
||||
}
|
||||
Reference in new issue
Block a user