From 87925a57de059d76da87928dc987bc990725103d Mon Sep 17 00:00:00 2001 From: Radek Davidek Date: Wed, 4 Mar 2026 14:36:10 +0100 Subject: [PATCH] fix --- .../xtreamplayer/XtreamPlayerApplication.java | 114 ++++++++++++++---- 1 file changed, 92 insertions(+), 22 deletions(-) diff --git a/src/main/java/cz/kamma/xtreamplayer/XtreamPlayerApplication.java b/src/main/java/cz/kamma/xtreamplayer/XtreamPlayerApplication.java index c7367e7..cc5f3e0 100644 --- a/src/main/java/cz/kamma/xtreamplayer/XtreamPlayerApplication.java +++ b/src/main/java/cz/kamma/xtreamplayer/XtreamPlayerApplication.java @@ -251,28 +251,7 @@ public final class XtreamPlayerApplication { List attemptErrors = new ArrayList<>(); for (URI candidate : attempts) { try { - HttpRequest.Builder requestBuilder = HttpRequest.newBuilder(candidate) - .GET() - .timeout(Duration.ofSeconds(60)) - .header("User-Agent", firstNonBlank( - exchange.getRequestHeaders().getFirst("User-Agent"), - DEFAULT_BROWSER_UA - )) - .header("Accept", firstNonBlank( - exchange.getRequestHeaders().getFirst("Accept"), - "*/*" - )); - copyRequestHeaderIfPresent(exchange, requestBuilder, "Range"); - copyRequestHeaderIfPresent(exchange, requestBuilder, "If-Range"); - if (!sourceUrl.isBlank()) { - requestBuilder.header("Referer", sourceUrl); - String origin = originFromUrl(sourceUrl); - if (!origin.isBlank()) { - requestBuilder.header("Origin", origin); - } - } - HttpRequest request = requestBuilder.build(); - HttpResponse candidateResponse = HTTP_CLIENT.send(request, HttpResponse.BodyHandlers.ofByteArray()); + HttpResponse candidateResponse = sendStreamRequest(exchange, candidate, sourceUrl); if (response == null || response.statusCode() >= 400) { response = candidateResponse; usedTarget = candidate; @@ -298,6 +277,17 @@ public final class XtreamPlayerApplication { } String contentType = response.headers().firstValue("Content-Type").orElse("application/octet-stream"); byte[] body = response.body() == null ? new byte[0] : response.body(); + + if (response.statusCode() == 403 && !sourceUrl.isBlank() && !isHlsPlaylist(usedTarget, contentType)) { + UpstreamResult retried = retrySegmentUsingFreshPlaylist(exchange, target, sourceUrl); + if (retried != null) { + response = retried.response(); + usedTarget = retried.uri(); + contentType = response.headers().firstValue("Content-Type").orElse("application/octet-stream"); + body = response.body() == null ? new byte[0] : response.body(); + } + } + if (response.statusCode() >= 400) { LOGGER.warn( "Stream proxy upstream returned status={} uri={} bytes={} contentType={}", @@ -811,6 +801,86 @@ public final class XtreamPlayerApplication { return "/api/stream-proxy?url=" + urlEncode(absoluteUrl) + "&src=" + urlEncode(sourceUrl); } + private static UpstreamResult retrySegmentUsingFreshPlaylist(HttpExchange exchange, URI originalSegmentUri, String sourceUrl) + throws IOException, InterruptedException { + URI sourceUri = URI.create(sourceUrl); + HttpResponse playlistResponse = sendStreamRequest(exchange, sourceUri, sourceUrl); + if (playlistResponse.statusCode() >= 400) { + return null; + } + String playlistType = playlistResponse.headers().firstValue("Content-Type").orElse(""); + if (!isHlsPlaylist(sourceUri, playlistType)) { + return null; + } + + String segmentName = pathBasename(originalSegmentUri); + if (segmentName.isBlank()) { + return null; + } + String playlist = new String( + playlistResponse.body() == null ? new byte[0] : playlistResponse.body(), + StandardCharsets.UTF_8 + ); + String[] lines = playlist.split("\\r?\\n"); + UpstreamResult fallback = null; + for (String line : lines) { + String trimmed = line.trim(); + if (trimmed.isBlank() || trimmed.startsWith("#")) { + continue; + } + URI candidate = sourceUri.resolve(trimmed); + if (!segmentName.equals(pathBasename(candidate))) { + continue; + } + HttpResponse response = sendStreamRequest(exchange, candidate, sourceUrl); + UpstreamResult result = new UpstreamResult(response, candidate); + fallback = result; + if (response.statusCode() < 400) { + LOGGER.info("Segment retry via fresh playlist succeeded uri={}", maskUri(candidate)); + return result; + } + } + return fallback; + } + + private static HttpResponse sendStreamRequest(HttpExchange exchange, URI candidate, String sourceUrl) + throws IOException, InterruptedException { + HttpRequest.Builder requestBuilder = HttpRequest.newBuilder(candidate) + .GET() + .timeout(Duration.ofSeconds(60)) + .header("User-Agent", firstNonBlank( + exchange.getRequestHeaders().getFirst("User-Agent"), + DEFAULT_BROWSER_UA + )) + .header("Accept", firstNonBlank( + exchange.getRequestHeaders().getFirst("Accept"), + "*/*" + )); + copyRequestHeaderIfPresent(exchange, requestBuilder, "Range"); + copyRequestHeaderIfPresent(exchange, requestBuilder, "If-Range"); + if (sourceUrl != null && !sourceUrl.isBlank()) { + requestBuilder.header("Referer", sourceUrl); + String origin = originFromUrl(sourceUrl); + if (!origin.isBlank()) { + requestBuilder.header("Origin", origin); + } + } + HttpRequest request = requestBuilder.build(); + return HTTP_CLIENT.send(request, HttpResponse.BodyHandlers.ofByteArray()); + } + + private static String pathBasename(URI uri) { + String path = uri == null || uri.getPath() == null ? "" : uri.getPath(); + int index = path.lastIndexOf('/'); + if (index < 0 || index == path.length() - 1) { + return path; + } + return path.substring(index + 1); + } + + private record UpstreamResult(HttpResponse response, URI uri) { + } + private static String originFromUrl(String url) { try { URI uri = URI.create(url);