mirror of
https://github.com/SoPat712/allstarr.git
synced 2026-10-07 22:03:14 -04:00
fix(jellyfin): preserve native root and HEAD parity
This commit is contained in:
6 files changed
+56
-26
No files matched your search
@@ -20,7 +20,6 @@
|
||||
{
|
||||
"protocol": "jellyfin",
|
||||
"method": "HEAD",
|
||||
"upstreamMethod": "GET",
|
||||
"path": "/Items/local-song/Download?api_key=fixture-key",
|
||||
"verificationPath": "/Users/Me?api_key=fixture-key",
|
||||
"streamPath": "/Items/local-song/Download?api_key=fixture-key",
|
||||
|
||||
@@ -501,7 +501,7 @@ public sealed class ProtocolRouteFixtureTests
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task JellyfinMusicRoot_UsesConfiguredUserScope()
|
||||
public async Task JellyfinMusicRoot_UsesFullItemRouteAndCallerAuthentication()
|
||||
{
|
||||
var observedRequests = new List<string>();
|
||||
using var factory = new ProtocolFactory(
|
||||
@@ -511,7 +511,8 @@ public sealed class ProtocolRouteFixtureTests
|
||||
observedRequests.Add(request.RequestUri!.PathAndQuery);
|
||||
return request.RequestUri.AbsolutePath == "/Users/Me"
|
||||
? Json(StatusCodes.Status200OK, """{"Id":"user-1","Name":"Fixture User"}""")
|
||||
: ItemLookup("""{"Id":"music-1","Type":"CollectionFolder","CollectionType":"music"}""");
|
||||
: Json(StatusCodes.Status200OK,
|
||||
"""{"Id":"music-1","Type":"CollectionFolder","CollectionType":"music","Etag":"full-object"}""");
|
||||
},
|
||||
configuration: new Dictionary<string, string?>
|
||||
{
|
||||
@@ -521,13 +522,41 @@ public sealed class ProtocolRouteFixtureTests
|
||||
using var client = factory.CreateClient();
|
||||
|
||||
using var response = await client.GetAsync("/Items/Root?api_key=fixture-key");
|
||||
using var body = JsonDocument.Parse(await response.Content.ReadAsStringAsync());
|
||||
|
||||
Assert.Equal(HttpStatusCode.OK, response.StatusCode);
|
||||
Assert.Equal("full-object", body.RootElement.GetProperty("Etag").GetString());
|
||||
Assert.Equal(
|
||||
["/Users/Me?api_key=fixture-key", "/Items?ids=music-1&limit=1&userId=user-1"],
|
||||
["/Users/Me?api_key=fixture-key", "/Items/music-1?api_key=fixture-key"],
|
||||
observedRequests);
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[InlineData("Download")]
|
||||
[InlineData("File")]
|
||||
public async Task JellyfinNativeFileHead_PreservesMethodAndStatus(string route)
|
||||
{
|
||||
HttpMethod? observedMethod = null;
|
||||
using var factory = new ProtocolFactory("Jellyfin", request =>
|
||||
{
|
||||
if (request.RequestUri!.AbsolutePath == "/Users/Me")
|
||||
return Json(StatusCodes.Status200OK, """{"Id":"user-1","Name":"Fixture User"}""");
|
||||
if (request.RequestUri.AbsolutePath == "/Items")
|
||||
return ItemLookup("""{"Id":"0123456789abcdef0123456789abcdef","Type":"Audio"}""");
|
||||
observedMethod = request.Method;
|
||||
return new HttpResponseMessage(HttpStatusCode.MethodNotAllowed);
|
||||
});
|
||||
using var client = factory.CreateClient();
|
||||
using var request = new HttpRequestMessage(
|
||||
HttpMethod.Head,
|
||||
$"/Items/0123456789abcdef0123456789abcdef/{route}?api_key=fixture-key");
|
||||
|
||||
using var response = await client.SendAsync(request);
|
||||
|
||||
Assert.Equal(HttpStatusCode.MethodNotAllowed, response.StatusCode);
|
||||
Assert.Equal(HttpMethod.Head, observedMethod);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task JellyfinSearchAdapter_PreservesFixtureStatusBodyAndPaging()
|
||||
{
|
||||
@@ -2318,6 +2347,8 @@ public sealed class ProtocolRouteFixtureTests
|
||||
var browseItem = Assert.Single(browse.RootElement.GetProperty("Items").EnumerateArray());
|
||||
Assert.Equal(nativeId, browseItem.GetProperty("Id").GetString());
|
||||
Assert.Equal(50, browseItem.GetProperty("ChildCount").GetInt32());
|
||||
Assert.Equal("spotify",
|
||||
browseItem.GetProperty("ProviderIds").GetProperty("AllstarrSource").GetString());
|
||||
Assert.True(browseItem.GetProperty("CanDelete").GetBoolean());
|
||||
Assert.Equal(
|
||||
"keep-native-fields",
|
||||
|
||||
@@ -38,7 +38,7 @@ public partial class JellyfinController
|
||||
fullPath = $"{fullPath}{Request.QueryString.Value}";
|
||||
}
|
||||
|
||||
return await ProxyJellyfinStream(fullPath, itemId, useGetForHead: true);
|
||||
return await ProxyJellyfinStream(fullPath, itemId);
|
||||
}
|
||||
|
||||
// Handle external content
|
||||
@@ -84,17 +84,14 @@ public partial class JellyfinController
|
||||
/// <summary>
|
||||
/// Proxies a stream from Jellyfin with proper header forwarding.
|
||||
/// </summary>
|
||||
private async Task<IActionResult> ProxyJellyfinStream(
|
||||
string path,
|
||||
string itemId,
|
||||
bool useGetForHead = false)
|
||||
private async Task<IActionResult> ProxyJellyfinStream(string path, string itemId)
|
||||
{
|
||||
var jellyfinUrl = $"{_settings.Url?.TrimEnd('/')}/{path}";
|
||||
|
||||
try
|
||||
{
|
||||
var request = new HttpRequestMessage(
|
||||
HttpMethods.IsHead(Request.Method) && !useGetForHead ? HttpMethod.Head : HttpMethod.Get,
|
||||
HttpMethods.IsHead(Request.Method) ? HttpMethod.Head : HttpMethod.Get,
|
||||
jellyfinUrl);
|
||||
|
||||
// Forward auth headers
|
||||
|
||||
@@ -74,9 +74,8 @@ public partial class JellyfinController
|
||||
});
|
||||
}
|
||||
|
||||
var (body, statusCode) = await _proxyService.GetItemAsync(
|
||||
Uri.EscapeDataString(musicLibraryId),
|
||||
Request.Headers);
|
||||
var endpoint = $"Items/{Uri.EscapeDataString(musicLibraryId)}{Request.QueryString.Value}";
|
||||
var (body, statusCode) = await _proxyService.GetJsonAsync(endpoint, null, Request.Headers);
|
||||
return HandleProxyResponse(body, statusCode);
|
||||
}
|
||||
|
||||
|
||||
@@ -1093,6 +1093,9 @@ public partial class JellyfinController
|
||||
item["ChildCount"] = playlist.Tracks.Count;
|
||||
item["RunTimeTicks"] = playlist.Tracks.Sum(track => track.DurationMilliseconds) *
|
||||
TimeSpan.TicksPerMillisecond;
|
||||
var providerIds = item["ProviderIds"] as JsonObject ?? [];
|
||||
item["ProviderIds"] = providerIds;
|
||||
providerIds["AllstarrSource"] = playlist.SourceProviderId;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -1621,6 +1621,7 @@ if [[ -s "$direct_playlists_file" && -s "$allstarr_playlists_file" ]]; then
|
||||
first(.Items[] as $direct |
|
||||
select(any($proxy[0].Items[];
|
||||
.Id == $direct.Id and
|
||||
.ProviderIds.AllstarrSource == null and
|
||||
(.ChildCount // 0) == ($direct.ChildCount // 0))) |
|
||||
$direct.Id) // empty' "$direct_playlists_file")"
|
||||
fi
|
||||
@@ -1691,28 +1692,28 @@ elif [[ -n "$INJECTED_PLAYLIST_ID" ]]; then
|
||||
--slurpfile full "$external_search_file"
|
||||
[[ -n "$virtual_playlist_id" ]] || virtual_playlist_id="$INJECTED_PLAYLIST_ID"
|
||||
fi
|
||||
alias_ids_json="$(jq -c '[.Items[] | select(.ProviderIds.AllstarrSource != null) | .Id]' \
|
||||
"$allstarr_playlists_file" 2>/dev/null || printf '[]')"
|
||||
native_direct_filter="$alias_ids_json as \$aliases | [.Items[] | select(.Id as \$id | \$aliases | index(\$id) | not)]"
|
||||
native_allstarr_filter="$alias_ids_json as \$aliases | [.Items[] | select(
|
||||
(.Id as \$id | \$aliases | index(\$id) | not) and
|
||||
(((.Id // \"\") | startswith(\"allstarr-vpl-\")) | not) and
|
||||
(((.Id // \"\") | test(\"^ext-.+-playlist-\"; \"i\")) | not))]"
|
||||
compare_structure "native playlist structure parity" \
|
||||
"$DIRECT_BASE/Users/$best_user_id/Items?$playlist_query" \
|
||||
"$ALLSTARR_BASE/Users/$best_user_id/Items?$playlist_query" \
|
||||
'.Items' \
|
||||
'[.Items[] | select(
|
||||
((((.Id // "") | startswith("allstarr-vpl-")) or
|
||||
((.Id // "") | test("^ext-.+-playlist-"; "i"))) | not))]'
|
||||
"$native_direct_filter" \
|
||||
"$native_allstarr_filter"
|
||||
compare_projection "native playlist stable data" \
|
||||
"$DIRECT_BASE/Users/$best_user_id/Items?$playlist_query" \
|
||||
"$ALLSTARR_BASE/Users/$best_user_id/Items?$playlist_query" \
|
||||
'[.Items[] | {Id,Name,Type,ImageTags,ProviderIds}] | sort_by(.Id)' \
|
||||
'[.Items[] | select(
|
||||
((((.Id // "") | startswith("allstarr-vpl-")) or
|
||||
((.Id // "") | test("^ext-.+-playlist-"; "i"))) | not)) |
|
||||
{Id,Name,Type,ImageTags,ProviderIds}] | sort_by(.Id)'
|
||||
"$native_direct_filter | [.[] | {Id,Name,Type,ImageTags,ProviderIds}] | sort_by(.Id)" \
|
||||
"$native_allstarr_filter | [.[] | {Id,Name,Type,ImageTags,ProviderIds}] | sort_by(.Id)"
|
||||
compare_projection "native playlist full objects" \
|
||||
"$DIRECT_BASE/Users/$best_user_id/Items?$playlist_query" \
|
||||
"$ALLSTARR_BASE/Users/$best_user_id/Items?$playlist_query" \
|
||||
'[.Items[]] | sort_by(.Id)' \
|
||||
'[.Items[] | select(
|
||||
((((.Id // "") | startswith("allstarr-vpl-")) or
|
||||
((.Id // "") | test("^ext-.+-playlist-"; "i"))) | not))] | sort_by(.Id)'
|
||||
"$native_direct_filter | sort_by(.Id)" \
|
||||
"$native_allstarr_filter | sort_by(.Id)"
|
||||
if [[ -n "$playlist_id" ]]; then
|
||||
check_json "playlist entries music only" "$ALLSTARR_BASE/Playlists/$playlist_id/Items?UserId=$best_user_id&Limit=100" \
|
||||
'(.Items | type == "array") and all(.Items[]; .Type == "Audio")'
|
||||
|
||||
Reference in new issue
Block a user