mirror of
https://github.com/SoPat712/allstarr.git
synced 2026-10-07 22:03:14 -04:00
fix: preserve valid provider pages in live parity
This commit is contained in:
7 files changed
+134
-21
No files matched your search
@@ -21,7 +21,10 @@
|
||||
- Deleted the unreachable `SafeJsonProxyClient` stack, its five DI registrations, and three tests that could not represent a production route. The real outbound-request guard remains; the focused safety lane passes 27/27 and the rebuilt final .NET fast lane passes 2,205/2,205.
|
||||
- Final local gates pass: .NET fast 2,205/2,205, release-critical 100/100, WebUI check 0 diagnostics, unit 45/45, build and budgets, browser 84/84, Apple gateway 19/19, all three Compose profiles, `dotnet format`, shell syntax, and `git diff --check`.
|
||||
- Current measured WebUI output is 45.5 KiB initial JavaScript and 21.2 KiB CSS. `app.css` is 6,683 lines, down from 7,124. The exact staged revision is net -3,297 production lines, -2,082 test lines, +78 WebUI lines, +168 tooling/CI lines, and +173 durable documentation lines: 2,393 additions versus 7,353 deletions overall (net -4,960). No Canvas UI or generated migration/model source is vendored.
|
||||
- Committed, pushed, and source-deployed `73ce04a18b0daa35f0af56f9f3072cbd575815e8` to the clean `.116` stack. All five containers are healthy; readiness and Jellyfin ping return 200.
|
||||
- The first authenticated LAN Jellyfin smoke completed 189 checks with 14 failures and 6 explicit blocks. Native list/detail/search, artist/album detail, playback bitrate, similar/mix items, playlist DTOs, 64 KiB stream bytes, concurrency, cancellation, external provider matrices, artwork, and Apple extension lyrics passed. The in-app browser itself crashed on both LAN and public URLs, so responsive live visual smoke remains blocked by the browser tool rather than server readiness.
|
||||
- Traced the four Apple GAMDL artist 502s to three over-limit artist-credit rows poisoning otherwise valid 100-item pages. The shared typed adapter now drops only invalid provider rows, and the external instant-mix failure path uses the standard Jellyfin envelope. The harness now compares music-constrained filters against the same direct query and treats identical native 404s as unavailable parity instead of demanding a route Jellyfin itself does not serve. Focused provider/protocol coverage passes 108/108; the full fast lane passes 2,207/2,207 in 67 seconds; format, shell syntax, and diff checks pass. Deployment and the full live rerun remain.
|
||||
|
||||
## Next entry point
|
||||
|
||||
Commit and push the exact tested revision, fast-forward the clean source deployment on `root@192.168.1.116`, then run live browser-only protocol smoke. Keep Canvas UI effects queued until a compatible grant or relicensing decision exists.
|
||||
Deploy the exact live-smoke correction revision and rerun the complete authenticated LAN Jellyfin smoke. Keep Canvas UI effects queued until a compatible grant or relicensing decision exists.
|
||||
@@ -179,6 +179,8 @@ Evidence in progress: the caller/composition audit deleted `IAudioContentStream`
|
||||
- [x] Report production deletions, test deletions, vendored Canvas UI lines, generated lines, and net handwritten change separately.
|
||||
- [ ] Commit, push, deploy, and run live checks only after exact revision authorization.
|
||||
|
||||
Live evidence: `73ce04a18b0daa35f0af56f9f3072cbd575815e8` was pushed and source-deployed to the clean `.116` stack after authorization; all five containers are healthy and readiness/ping return 200. The first authenticated LAN smoke completed 189 checks with 14 failures and 6 declared blocks. Exact native browse/detail/search, playback bitrate, mixes, playlist objects, 64 KiB stream bytes, cancellation/concurrency, provider matrices, artwork, and Apple extension lyrics passed. Four real Apple GAMDL artist failures came from three invalid rows poisoning 100-item pages; the shared adapter correction now preserves the valid rows. The remaining failed artwork/file/filter/root assertions were harness assumptions that contradicted direct Jellyfin's identical response and now require truthful direct parity. Focused correction coverage passes 108/108 and the full fast lane passes 2,207/2,207 in 67 seconds; format, shell syntax, and diff checks pass. The correction revision and full live rerun remain pending. The in-app browser crashed on both LAN and public URLs while direct health/assets remained available, so responsive live visual smoke remains tool-blocked.
|
||||
|
||||
### Completion gate
|
||||
|
||||
Current exact staged revision: production is 461 additions and 3,758 deletions (net -3,297); tests are 500 additions and 2,582 deletions (net -2,082); WebUI is 1,014 additions and 936 deletions (net +78); tooling/CI is 209 additions and 41 deletions (net +168); durable plan/handoff documentation is net +173. Total staged change is 2,393 additions and 7,353 deletions (net -4,960). Vendored Canvas UI and generated migration/model lines are both zero.
|
||||
@@ -195,4 +197,4 @@ This package is complete only when:
|
||||
|
||||
### Immediate next action
|
||||
|
||||
Create and push the exact tested revision, fast-forward the clean source deployment on `root@192.168.1.116`, then run browser-only readiness, direct-Jellyfin, artwork, lyrics, search, external traversal, playlist, and WebSocket smoke. Canvas UI source remains queued behind a GPL-compatible grant; do not recreate its effects locally.
|
||||
Commit and deploy the exact live-smoke correction, then rerun authenticated direct-Jellyfin, artwork, lyrics, search, external traversal, playlist, and WebSocket smoke. Canvas UI source remains queued behind a GPL-compatible grant; do not recreate its effects locally.
|
||||
@@ -1385,6 +1385,45 @@ public sealed class ProtocolRouteFixtureTests
|
||||
interaction.VerifyAll();
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task JellyfinExternalInstantMix_ProviderFailureKeepsTheStandardEnvelope()
|
||||
{
|
||||
var gateway = new Mock<IProtocolProviderGateway>(MockBehavior.Strict);
|
||||
gateway.Setup(service => service.GetSongAsync(
|
||||
It.IsAny<ProtocolExecutionContext>(), "deezer", "42"))
|
||||
.ThrowsAsync(new HttpRequestException("provider unavailable"));
|
||||
var interaction = new Mock<IJellyfinInteractionProtocolAdapter>(MockBehavior.Strict);
|
||||
interaction.Setup(adapter => adapter.CanRunOptionalUserWork(
|
||||
It.IsAny<ProtocolExecutionContext?>()))
|
||||
.Returns(true);
|
||||
interaction.Setup(adapter => adapter.ShapeInstantMix(
|
||||
It.Is<IReadOnlyList<Dictionary<string, object?>>>(items => items.Count == 0)))
|
||||
.Returns(new JellyfinInteractionProtocolAdapter().ShapeInstantMix([]));
|
||||
using var factory = new ProtocolFactory(
|
||||
"Jellyfin",
|
||||
request => request.RequestUri!.AbsolutePath == "/Users/Me"
|
||||
? Json(StatusCodes.Status200OK, """{"Id":"user-1"}""")
|
||||
: throw new InvalidOperationException($"Unexpected upstream request: {request.RequestUri}"),
|
||||
services =>
|
||||
{
|
||||
services.RemoveAll<IProtocolProviderGateway>();
|
||||
services.AddSingleton(gateway.Object);
|
||||
services.RemoveAll<IJellyfinInteractionProtocolAdapter>();
|
||||
services.AddSingleton(interaction.Object);
|
||||
});
|
||||
using var client = factory.CreateClient();
|
||||
|
||||
using var response = await client.GetAsync(
|
||||
"/Items/ext-deezer-song-42/InstantMix?Limit=2&api_key=fixture-key");
|
||||
|
||||
Assert.Equal(HttpStatusCode.OK, response.StatusCode);
|
||||
Assert.Equal(
|
||||
"{\"Items\":[],\"TotalRecordCount\":0,\"StartIndex\":0}",
|
||||
await response.Content.ReadAsStringAsync());
|
||||
gateway.VerifyAll();
|
||||
interaction.VerifyAll();
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[InlineData("/Albums/ext-deezer-album-42/InstantMix?Limit=2&api_key=fixture-key", "album")]
|
||||
[InlineData("/Artists/ext-deezer-artist-42/InstantMix?Limit=2&api_key=fixture-key", "artist")]
|
||||
|
||||
@@ -88,6 +88,26 @@ public sealed class DeezerMetadataCapabilityAdapterTests
|
||||
Assert.Equal("456", Assert.Single(outcome.RequireValue().Items).AlbumId!.Value);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task CollectionMapping_SkipsInvalidProviderRecordsInsteadOfFailingThePage()
|
||||
{
|
||||
var legacy = new Mock<IConcreteMetadataService>(MockBehavior.Strict);
|
||||
legacy.Setup(item => item.GetArtistAlbumsAsync("deezer", "27", It.IsAny<CancellationToken>()))
|
||||
.ReturnsAsync([
|
||||
new Album { Id = "1", ExternalId = "1", Title = "Valid", Artist = "Artist" },
|
||||
new Album { Id = "2", ExternalId = "2", Title = "Invalid", Artist = new string('x', 301) }
|
||||
]);
|
||||
var adapter = new DeezerMetadataCapabilityAdapter(legacy.Object);
|
||||
|
||||
var outcome = await adapter.GetArtistAlbumsAsync(
|
||||
Context(),
|
||||
new ProviderArtistItemsRequest(
|
||||
new ProviderExternalResourceId("deezer", ProviderResourceKind.Artist, "27"),
|
||||
new ProviderPageRequest(10)));
|
||||
|
||||
Assert.Equal("1", Assert.Single(outcome.RequireValue().Items).Id.Value);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task CursorThatLegacyProviderCannotHonor_ReturnsTypedNotSupported()
|
||||
{
|
||||
|
||||
@@ -1443,11 +1443,7 @@ public partial class JellyfinController : ControllerBase
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.LogError(ex, "Failed to create instant mix for external song {ItemId}", itemId);
|
||||
return _responseBuilder.CreateJsonResponse(new
|
||||
{
|
||||
Items = Array.Empty<object>(),
|
||||
TotalRecordCount = 0
|
||||
});
|
||||
return CreateProtocolResponse(_interactionProtocolAdapter.ShapeInstantMix([]));
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -105,7 +105,7 @@ public abstract class ConcreteMetadataCapabilityAdapter(
|
||||
var values = await fetch(context.CancellationToken);
|
||||
context.CancellationToken.ThrowIfCancellationRequested();
|
||||
return ProviderOutcome<ProviderPage<TTarget>>.Success(new(
|
||||
ProviderId, values.Select(map), isPartial: values.Count >= page.Limit));
|
||||
ProviderId, MapValid(values, map), isPartial: values.Count >= page.Limit));
|
||||
}
|
||||
catch (OperationCanceledException)
|
||||
{
|
||||
@@ -171,8 +171,9 @@ public abstract class ConcreteMetadataCapabilityAdapter(
|
||||
{
|
||||
var values = await fetch(context.CancellationToken);
|
||||
context.CancellationToken.ThrowIfCancellationRequested();
|
||||
var items = values.Skip(offset).Take(request.Page.Limit).Select(map).ToArray();
|
||||
var nextOffset = offset + items.Length;
|
||||
var pageValues = values.Skip(offset).Take(request.Page.Limit).ToArray();
|
||||
var items = MapValid(pageValues, map);
|
||||
var nextOffset = offset + pageValues.Length;
|
||||
var nextCursor = nextOffset < values.Count
|
||||
? nextOffset.ToString(CultureInfo.InvariantCulture)
|
||||
: null;
|
||||
@@ -189,6 +190,19 @@ public abstract class ConcreteMetadataCapabilityAdapter(
|
||||
}
|
||||
}
|
||||
|
||||
private static IReadOnlyList<TTarget> MapValid<TLegacy, TTarget>(
|
||||
IEnumerable<TLegacy> values,
|
||||
Func<TLegacy, TTarget> map)
|
||||
{
|
||||
var mapped = new List<TTarget>();
|
||||
foreach (var value in values)
|
||||
{
|
||||
try { mapped.Add(map(value)); }
|
||||
catch (Exception ex) when (ex is ArgumentException or InvalidOperationException) { }
|
||||
}
|
||||
return mapped;
|
||||
}
|
||||
|
||||
internal ProviderError? ValidateContext(ProviderExecutionContext context)
|
||||
{
|
||||
ArgumentNullException.ThrowIfNull(context);
|
||||
|
||||
@@ -182,6 +182,7 @@ fi
|
||||
echo "jellyfin-user=selected actor_bound=$actor_bound"
|
||||
|
||||
full_item_fields="AirTime,CanDelete,CanDownload,ChannelInfo,Chapters,Trickplay,ChildCount,CumulativeRunTimeTicks,CustomRating,DateCreated,DateLastMediaAdded,DisplayPreferencesId,Etag,ExternalUrls,Genres,ItemCounts,MediaSourceCount,MediaSources,OriginalTitle,Overview,ParentId,Path,People,PlayAccess,ProductionLocations,ProviderIds,PrimaryImageAspectRatio,RecursiveItemCount,Settings,SeriesStudio,SortName,SpecialEpisodeNumbers,Studios,Taglines,Tags,RemoteTrailers,MediaStreams,SeasonUserData,DateLastRefreshed,DateLastSaved,RefreshState,ChannelImage,EnableMediaSourceDisplay,Width,Height,ExtraIds,LocalTrailerCount,IsHD,SpecialFeatureCount"
|
||||
default_music_item_types="Audio,MusicAlbum,MusicArtist,Playlist,MusicGenre"
|
||||
items_query="Recursive=true&IncludeItemTypes=Audio&Limit=100&Fields=PrimaryImageAspectRatio%2CProviderIds%2CMediaSources%2CAlbumId%2CArtistItems%2CGenres"
|
||||
curl -fsS --max-time "$TIMEOUT_SECONDS" "${auth[@]}" \
|
||||
"$DIRECT_BASE/Users/$best_user_id/Items?$items_query" -o "$items_file"
|
||||
@@ -594,6 +595,10 @@ compare_binary() {
|
||||
"${direct_bytes%.*}" -gt 0 ]] && cmp -s "$direct_media_file" "$allstarr_media_file"; then
|
||||
signature="$("${sha256_command[@]}" "$allstarr_media_file" | awk '{print $1}')"
|
||||
printf 'PASS %-34s bytes=%s sha256=%s\n' "$label" "$allstarr_bytes" "$signature"
|
||||
elif [[ "$direct_code" == 404 && "$allstarr_code" == 404 &&
|
||||
"$direct_type" == "$allstarr_type" ]]; then
|
||||
printf 'PASS %-34s unavailable-identical status=%s type=%s\n' \
|
||||
"$label" "$allstarr_code" "$allstarr_type"
|
||||
else
|
||||
printf 'FAIL %-34s direct=%s/%s/%s allstarr=%s/%s/%s\n' \
|
||||
"$label" "$direct_code" "$direct_type" "$direct_bytes" \
|
||||
@@ -602,6 +607,25 @@ compare_binary() {
|
||||
fi
|
||||
}
|
||||
|
||||
check_authenticated_status_parity() {
|
||||
local label="$1" method="$2" direct_url="$3" allstarr_url="$4"
|
||||
local direct_code allstarr_code
|
||||
local -a method_args=(-X "$method")
|
||||
[[ "$method" == HEAD ]] && method_args=(--head)
|
||||
direct_code="$(curl -sS "${method_args[@]}" --max-time "$TIMEOUT_SECONDS" "${auth[@]}" \
|
||||
-o /dev/null -w '%{http_code}' "$direct_url" || true)"
|
||||
allstarr_code="$(curl -sS "${method_args[@]}" --max-time "$TIMEOUT_SECONDS" "${auth[@]}" \
|
||||
-o /dev/null -w '%{http_code}' "$allstarr_url" || true)"
|
||||
checks=$((checks + 1))
|
||||
if [[ "${direct_code:-000}" == "${allstarr_code:-000}" && "${allstarr_code:-000}" != 000 ]]; then
|
||||
printf 'PASS %-34s exact-status=%s\n' "$label" "$allstarr_code"
|
||||
else
|
||||
printf 'FAIL %-34s direct=%s allstarr=%s\n' \
|
||||
"$label" "${direct_code:-000}" "${allstarr_code:-000}"
|
||||
failures=$((failures + 1))
|
||||
fi
|
||||
}
|
||||
|
||||
compare_native_collection_items() {
|
||||
local label="$1" allstarr_url="$2" filter="${3:-.Items}"
|
||||
local item item_id item_count=0 invalid_count=0
|
||||
@@ -684,7 +708,7 @@ check_public_image() {
|
||||
}
|
||||
|
||||
check_range_parity() {
|
||||
local label="$1" direct_url="$2" allstarr_url="$3" credential_mode="${4:-header}"
|
||||
local label="$1" direct_url="$2" allstarr_url="$3" credential_mode="${4:-header}" allow_unavailable="${5:-0}"
|
||||
local direct_code allstarr_code direct_bytes allstarr_bytes direct_range allstarr_range direct_type allstarr_type signature
|
||||
local -a request_auth=("${auth[@]}")
|
||||
if [[ "$credential_mode" == query-api-key ]]; then
|
||||
@@ -718,6 +742,10 @@ check_range_parity() {
|
||||
signature="$("${sha256_command[@]}" "$allstarr_media_file" | awk '{print substr($1, 1, 12)}')"
|
||||
printf 'PASS %-34s bytes=65536 type=%s sha256=%s exact-body\n' \
|
||||
"$label" "$allstarr_type" "$signature"
|
||||
elif [[ "$allow_unavailable" == 1 && "$direct_code" == 404 && "$allstarr_code" == 404 &&
|
||||
"$direct_type" == "$allstarr_type" ]]; then
|
||||
printf 'PASS %-34s unavailable-identical status=%s type=%s\n' \
|
||||
"$label" "$allstarr_code" "$allstarr_type"
|
||||
else
|
||||
printf 'FAIL %-34s direct=%s/%s/%s allstarr=%s/%s/%s\n' \
|
||||
"$label" "$direct_code" "$direct_bytes" "$direct_range" \
|
||||
@@ -1160,13 +1188,13 @@ if [[ -n "$artist_id" ]]; then
|
||||
fi
|
||||
check_json "item filters" "$ALLSTARR_BASE/Items/Filters?UserId=$best_user_id" 'type == "object"'
|
||||
compare_projection "item filters exact data" \
|
||||
"$DIRECT_BASE/Items/Filters?UserId=$best_user_id" \
|
||||
"$ALLSTARR_BASE/Items/Filters?UserId=$best_user_id" \
|
||||
"$DIRECT_BASE/Items/Filters?UserId=$best_user_id&IncludeItemTypes=$default_music_item_types" \
|
||||
"$ALLSTARR_BASE/Items/Filters?UserId=$best_user_id&IncludeItemTypes=$default_music_item_types" \
|
||||
'.'
|
||||
check_json "item filters2" "$ALLSTARR_BASE/Items/Filters2?UserId=$best_user_id" 'type == "object"'
|
||||
compare_projection "item filters2 exact data" \
|
||||
"$DIRECT_BASE/Items/Filters2?UserId=$best_user_id" \
|
||||
"$ALLSTARR_BASE/Items/Filters2?UserId=$best_user_id" \
|
||||
"$DIRECT_BASE/Items/Filters2?UserId=$best_user_id&IncludeItemTypes=$default_music_item_types" \
|
||||
"$ALLSTARR_BASE/Items/Filters2?UserId=$best_user_id&IncludeItemTypes=$default_music_item_types" \
|
||||
'.'
|
||||
check_json "genres browse" "$ALLSTARR_BASE/Genres?UserId=$best_user_id&Limit=10" \
|
||||
'(.Items | type == "array") and all(.Items[]; .Type == "Genre")'
|
||||
@@ -1185,7 +1213,12 @@ check_json "legacy user views music only" "$ALLSTARR_BASE/Users/$best_user_id/Vi
|
||||
(.TotalRecordCount == (.Items | length)) and
|
||||
all(.Items[]; .CollectionType == "music")'
|
||||
check_json "music library root" "$ALLSTARR_BASE/Items/Root?UserId=$best_user_id" \
|
||||
'.Id != null and .CollectionType == "music"'
|
||||
'(.Id | type == "string" and length > 0) and .IsFolder == true'
|
||||
music_root_id="$(jq -r '.Id // empty' "$response_file")"
|
||||
compare_projection "music library root full object" \
|
||||
"$DIRECT_BASE/Items/$music_root_id" \
|
||||
"$ALLSTARR_BASE/Items/Root?UserId=$best_user_id" \
|
||||
'.'
|
||||
check_json "music-only counts" "$ALLSTARR_BASE/Items/Counts?UserId=$best_user_id" \
|
||||
'.MovieCount == 0 and .SeriesCount == 0 and .EpisodeCount == 0 and .MusicVideoCount == 0'
|
||||
check_json "playback info" "$ALLSTARR_BASE/Items/$media_id/PlaybackInfo?UserId=$best_user_id" \
|
||||
@@ -1946,9 +1979,11 @@ measure "allstarr playlist-list" "$ALLSTARR_BASE/Users/$best_user_id/Items?$play
|
||||
timing_delta "playlist proxy delta" "direct playlist-list" "allstarr playlist-list"
|
||||
|
||||
if [[ -n "$art_id" ]]; then
|
||||
check_code "artwork retrieval" "200,304" GET \
|
||||
check_authenticated_status_parity "artwork retrieval" GET \
|
||||
"$DIRECT_BASE/Items/$art_id/Images/Primary?maxWidth=300&maxHeight=300&UserId=$best_user_id" \
|
||||
"$ALLSTARR_BASE/Items/$art_id/Images/Primary?maxWidth=300&maxHeight=300&UserId=$best_user_id"
|
||||
check_code "artwork HEAD" "200,304" HEAD \
|
||||
check_authenticated_status_parity "artwork HEAD" HEAD \
|
||||
"$DIRECT_BASE/Items/$art_id/Images/Primary?maxWidth=300&maxHeight=300&UserId=$best_user_id" \
|
||||
"$ALLSTARR_BASE/Items/$art_id/Images/Primary?maxWidth=300&maxHeight=300&UserId=$best_user_id"
|
||||
check_public_image "artwork without token" \
|
||||
"$ALLSTARR_BASE/Items/$art_id/Images/Primary?maxWidth=300&maxHeight=300&UserId=$best_user_id"
|
||||
@@ -1984,8 +2019,12 @@ fi
|
||||
measure "direct lyrics" "$DIRECT_BASE/Audio/$lyrics_id/Lyrics?UserId=$best_user_id"
|
||||
measure "allstarr lyrics" "$ALLSTARR_BASE/Audio/$lyrics_id/Lyrics?UserId=$best_user_id"
|
||||
timing_delta "lyrics proxy delta" "direct lyrics" "allstarr lyrics"
|
||||
check_code "download HEAD" "200,206" HEAD "$ALLSTARR_BASE/Items/$media_id/Download?UserId=$best_user_id"
|
||||
check_code "file HEAD" "200,206" HEAD "$ALLSTARR_BASE/Items/$media_id/File?UserId=$best_user_id"
|
||||
check_authenticated_status_parity "download HEAD" HEAD \
|
||||
"$DIRECT_BASE/Items/$media_id/Download?UserId=$best_user_id" \
|
||||
"$ALLSTARR_BASE/Items/$media_id/Download?UserId=$best_user_id"
|
||||
check_authenticated_status_parity "file HEAD" HEAD \
|
||||
"$DIRECT_BASE/Items/$media_id/File?UserId=$best_user_id" \
|
||||
"$ALLSTARR_BASE/Items/$media_id/File?UserId=$best_user_id"
|
||||
check_code "stream HEAD" "200,206" HEAD "$ALLSTARR_BASE/Audio/$media_id/stream?static=true&UserId=$best_user_id"
|
||||
check_code "universal audio HEAD" "200,206,302" HEAD "$ALLSTARR_BASE/Audio/$media_id/universal?UserId=$best_user_id"
|
||||
check_code "stream bounded range" "206" GET \
|
||||
@@ -2005,7 +2044,7 @@ check_stream_cancellation "allstarr stream cancellation" \
|
||||
check_range_parity "Finer file ApiKey range parity" \
|
||||
"$DIRECT_BASE/Items/$media_id/File" \
|
||||
"$ALLSTARR_BASE/Items/$media_id/File" \
|
||||
query-api-key
|
||||
query-api-key 1
|
||||
measure "direct stream-64k" "$DIRECT_BASE/Audio/$media_id/stream?static=true&UserId=$best_user_id" \
|
||||
--range 0-65535 --max-filesize 65536
|
||||
measure "allstarr stream-64k" "$ALLSTARR_BASE/Audio/$media_id/stream?static=true&UserId=$best_user_id" \
|
||||
|
||||
Reference in new issue
Block a user