https fixes

This commit is contained in:
Radek Davidek 2026-03-04 14:14:49 +01:00
parent fcef5e54a9
commit 2b3f3810d4
2 changed files with 129 additions and 3 deletions

View File

@ -28,6 +28,8 @@ import java.util.List;
import java.util.Map;
import java.util.Objects;
import java.util.concurrent.Executors;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
public final class XtreamPlayerApplication {
private static final int DEFAULT_PORT = 8080;
@ -35,6 +37,7 @@ public final class XtreamPlayerApplication {
private static final String ATTR_REQ_START_NANOS = "reqStartNanos";
private static final ObjectMapper OBJECT_MAPPER = new ObjectMapper();
private static final Logger LOGGER = LogManager.getLogger(XtreamPlayerApplication.class);
private static final Pattern URI_ATTR_PATTERN = Pattern.compile("URI=\"([^\"]+)\"");
private static final HttpClient HTTP_CLIENT = HttpClient.newBuilder()
.connectTimeout(Duration.ofSeconds(20))
.followRedirects(HttpClient.Redirect.NORMAL)
@ -60,6 +63,7 @@ public final class XtreamPlayerApplication {
server.createContext("/api/test-login", new TestLoginHandler(configStore));
server.createContext("/api/xtream", new XtreamProxyHandler(configStore));
server.createContext("/api/stream-url", new StreamUrlHandler(configStore));
server.createContext("/api/stream-proxy", new StreamProxyHandler());
server.createContext("/api/open-in-player", new OpenInPlayerHandler(configStore));
server.createContext("/api/library/load", new LibraryLoadHandler(libraryService));
server.createContext("/api/library/status", new LibraryStatusHandler(libraryService));
@ -203,6 +207,66 @@ public final class XtreamPlayerApplication {
}
}
private record StreamProxyHandler() implements HttpHandler {
@Override
public void handle(HttpExchange exchange) throws IOException {
Map<String, String> query = parseKeyValue(exchange.getRequestURI().getRawQuery());
logApiRequest(exchange, "/api/stream-proxy", query);
if (!"GET".equalsIgnoreCase(exchange.getRequestMethod())) {
methodNotAllowed(exchange, "GET");
return;
}
String rawUrl = query.getOrDefault("url", "").trim();
if (rawUrl.isBlank()) {
writeJson(exchange, 400, errorJson("Missing url parameter."));
return;
}
URI target;
try {
target = URI.create(rawUrl);
} catch (Exception exception) {
writeJson(exchange, 400, errorJson("Invalid url parameter."));
return;
}
String scheme = target.getScheme() == null ? "" : target.getScheme().toLowerCase(Locale.ROOT);
if (!"http".equals(scheme) && !"https".equals(scheme)) {
writeJson(exchange, 400, errorJson("Unsupported URL protocol."));
return;
}
try {
HttpRequest request = HttpRequest.newBuilder(target)
.GET()
.timeout(Duration.ofSeconds(60))
.header("User-Agent", "XtreamPlayer/1.0")
.header("Accept", "*/*")
.build();
HttpResponse<byte[]> response = HTTP_CLIENT.send(request, HttpResponse.BodyHandlers.ofByteArray());
String contentType = response.headers().firstValue("Content-Type").orElse("application/octet-stream");
byte[] body = response.body() == null ? new byte[0] : response.body();
if (isHlsPlaylist(target, contentType)) {
String rewritten = rewritePlaylistForProxy(target, body);
exchange.getResponseHeaders().set("Content-Type", "application/vnd.apple.mpegurl; charset=utf-8");
writeBytes(exchange, response.statusCode(), rewritten.getBytes(StandardCharsets.UTF_8),
"application/vnd.apple.mpegurl; charset=utf-8");
return;
}
exchange.getResponseHeaders().set("Content-Type", contentType);
writeBytes(exchange, response.statusCode(), body, contentType);
} catch (InterruptedException interruptedException) {
Thread.currentThread().interrupt();
writeJson(exchange, 500, errorJson("Stream proxy interrupted."));
} catch (Exception exception) {
LOGGER.error("Stream proxy failed for {}", maskUri(target), exception);
writeJson(exchange, 502, errorJson("Unable to proxy stream: " + exception.getMessage()));
}
}
}
private record OpenInPlayerHandler(ConfigStore configStore) implements HttpHandler {
@Override
public void handle(HttpExchange exchange) throws IOException {
@ -641,6 +705,50 @@ public final class XtreamPlayerApplication {
return URI.create(config.serverUrl() + "/player_api.php?" + query);
}
private static boolean isHlsPlaylist(URI target, String contentType) {
String path = target.getPath() == null ? "" : target.getPath().toLowerCase(Locale.ROOT);
String ct = contentType == null ? "" : contentType.toLowerCase(Locale.ROOT);
return path.endsWith(".m3u8")
|| ct.contains("application/vnd.apple.mpegurl")
|| ct.contains("application/x-mpegurl");
}
private static String rewritePlaylistForProxy(URI baseUri, byte[] bodyBytes) {
String raw = new String(bodyBytes, StandardCharsets.UTF_8);
String[] lines = raw.split("\\r?\\n", -1);
StringBuilder out = new StringBuilder(raw.length() + 256);
for (String line : lines) {
String trimmed = line.trim();
if (trimmed.isEmpty()) {
out.append('\n');
continue;
}
if (trimmed.startsWith("#")) {
out.append(rewriteTagUris(line, baseUri)).append('\n');
continue;
}
URI absolute = baseUri.resolve(trimmed);
out.append(proxyStreamUrl(absolute.toString())).append('\n');
}
return out.toString();
}
private static String rewriteTagUris(String line, URI baseUri) {
Matcher matcher = URI_ATTR_PATTERN.matcher(line);
StringBuilder out = new StringBuilder(line.length() + 64);
while (matcher.find()) {
String current = matcher.group(1);
String rewritten = proxyStreamUrl(baseUri.resolve(current).toString());
matcher.appendReplacement(out, "URI=\"" + Matcher.quoteReplacement(rewritten) + "\"");
}
matcher.appendTail(out);
return out.toString();
}
private static String proxyStreamUrl(String absoluteUrl) {
return "/api/stream-proxy?url=" + urlEncode(absoluteUrl);
}
private static Map<String, String> parseKeyValue(String raw) {
Map<String, String> result = new LinkedHashMap<>();
if (raw == null || raw.isBlank()) {

View File

@ -1493,6 +1493,7 @@
function setPlayer(title, url, info = {}) {
el.playerTitle.textContent = title || "Player";
const systemPlayerUrl = buildSystemPlayerHref(title, url, info);
const playbackUrl = buildBrowserPlaybackUrl(url);
el.openDirect.dataset.url = url;
el.openDirect.disabled = !url;
el.openSystemPlayer.dataset.url = systemPlayerUrl;
@ -1512,7 +1513,7 @@
setSubtitleStatus("No subtitle loaded.", false);
scheduleEmbeddedSubtitleScan();
if (isLikelyHls(url) && shouldUseHlsJs()) {
if (isLikelyHls(playbackUrl) && shouldUseHlsJs()) {
state.currentStreamInfo.playbackEngine = "hls.js";
renderStreamInfo();
hlsInstance = new window.Hls({
@ -1531,7 +1532,7 @@
: (Array.isArray(hlsInstance?.subtitleTracks) ? hlsInstance.subtitleTracks : []);
refreshEmbeddedSubtitleTracks();
});
hlsInstance.loadSource(url);
hlsInstance.loadSource(playbackUrl);
hlsInstance.attachMedia(el.player);
hlsInstance.on(window.Hls.Events.ERROR, (_event, data) => {
if (!data?.fatal) {
@ -1545,7 +1546,7 @@
}
});
} else {
el.player.src = url;
el.player.src = playbackUrl;
}
const playbackPromise = el.player.play();
@ -1579,6 +1580,23 @@
return `/api/open-in-player?${params.toString()}`;
}
function buildBrowserPlaybackUrl(urlRaw) {
const url = String(urlRaw || "").trim();
if (!url) {
return url;
}
try {
const pageIsHttps = window.location.protocol === "https:";
const target = new URL(url, window.location.href);
if (pageIsHttps && target.protocol === "http:") {
return `/api/stream-proxy?url=${encodeURIComponent(target.toString())}`;
}
} catch (error) {
return url;
}
return url;
}
function resetPlayerElement() {
disposeHls();
el.player.pause();