From 0c4cdfb795e073d3128cb8165fb21c52814fc2bb Mon Sep 17 00:00:00 2001 From: 0xb00bface <0xboobface@gmail.com> Date: Sat, 7 May 2022 17:33:54 +0200 Subject: [PATCH] Fix construction of Stripchat streaming URLs --- CHANGELOG.md | 3 +- .../sites/stripchat/StripchatModel.java | 185 +++++++++++------- 2 files changed, 117 insertions(+), 71 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index f90a1c92..e631f907 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,7 +4,8 @@ * Save coonfig in a sub-directory for each version. * Add setting to disable tab dragging, because that might be the cause for tab freezes -* Fix thumbnails for Stripchat +* Fix Stripchat recordings +* Fix Stripchat thumbsnails 4.7.5 ======================== diff --git a/common/src/main/java/ctbrec/sites/stripchat/StripchatModel.java b/common/src/main/java/ctbrec/sites/stripchat/StripchatModel.java index 40f76b92..96072ef3 100644 --- a/common/src/main/java/ctbrec/sites/stripchat/StripchatModel.java +++ b/common/src/main/java/ctbrec/sites/stripchat/StripchatModel.java @@ -1,38 +1,42 @@ package ctbrec.sites.stripchat; -import static ctbrec.Model.State.*; -import static ctbrec.io.HttpConstants.*; -import static ctbrec.sites.stripchat.StripchatHttpClient.*; - -import java.io.IOException; -import java.util.ArrayList; -import java.util.List; -import java.util.Locale; -import java.util.concurrent.ExecutionException; -import java.util.stream.Collectors; - -import javax.xml.bind.JAXBException; - +import com.iheartradio.m3u8.*; +import com.iheartradio.m3u8.data.MasterPlaylist; +import com.iheartradio.m3u8.data.Playlist; +import com.iheartradio.m3u8.data.PlaylistData; +import ctbrec.AbstractModel; +import ctbrec.Config; +import ctbrec.io.HttpException; +import ctbrec.recorder.download.StreamSource; +import ctbrec.sites.Site; +import okhttp3.Request; +import okhttp3.RequestBody; +import okhttp3.Response; import org.json.JSONArray; import org.json.JSONObject; import org.slf4j.Logger; import org.slf4j.LoggerFactory; -import com.iheartradio.m3u8.ParseException; -import com.iheartradio.m3u8.PlaylistException; +import java.io.ByteArrayInputStream; +import java.io.IOException; +import java.io.InputStream; +import java.util.ArrayList; +import java.util.List; +import java.util.Locale; +import java.util.Objects; +import java.util.concurrent.ExecutionException; -import ctbrec.AbstractModel; -import ctbrec.Config; -import ctbrec.io.HttpException; -import ctbrec.recorder.download.StreamSource; -import okhttp3.Request; -import okhttp3.RequestBody; -import okhttp3.Response; +import static ctbrec.Model.State.*; +import static ctbrec.io.HttpConstants.*; +import static ctbrec.sites.stripchat.StripchatHttpClient.JSON; +import static java.nio.charset.StandardCharsets.UTF_8; public class StripchatModel extends AbstractModel { - private static final transient Logger LOG = LoggerFactory.getLogger(StripchatModel.class); + private static final Logger LOG = LoggerFactory.getLogger(StripchatModel.class); private String status = null; - private int[] resolution = new int[] {0, 0}; + private int[] resolution = new int[]{0, 0}; + + private static StripchatConfig stripchatConfig; @Override public boolean isOnline(boolean ignoreCache) throws IOException, ExecutionException, InterruptedException { @@ -49,25 +53,14 @@ public class StripchatModel extends AbstractModel { private void mapOnlineState(String status) { switch (status) { - case "public": - setOnlineState(ONLINE); - break; - case "idle": - setOnlineState(AWAY); - break; - case "private": - case "p2p": - case "groupShow": - case "virtualPrivate": - setOnlineState(PRIVATE); - break; - case "off": - setOnlineState(OFFLINE); - break; - default: - LOG.debug("Unknown online state {} for model {}", status, getName()); - setOnlineState(OFFLINE); - break; + case "public" -> setOnlineState(ONLINE); + case "idle" -> setOnlineState(AWAY); + case "private", "p2p", "groupShow", "virtualPrivate" -> setOnlineState(PRIVATE); + case "off" -> setOnlineState(OFFLINE); + default -> { + LOG.debug("Unknown online state {} for model {}", status, getName()); + setOnlineState(OFFLINE); + } } } @@ -93,7 +86,49 @@ public class StripchatModel extends AbstractModel { } @Override - public List getStreamSources() throws IOException, ExecutionException, ParseException, PlaylistException, JAXBException { + public List getStreamSources() throws IOException, ExecutionException, ParseException, PlaylistException { + MasterPlaylist masterPlaylist = getMasterPlaylist(); + List sources = new ArrayList<>(); + for (PlaylistData playlist : masterPlaylist.getPlaylists()) { + if (playlist.hasStreamInfo()) { + StreamSource src = new StreamSource(); + src.bandwidth = playlist.getStreamInfo().getBandwidth(); + src.height = playlist.getStreamInfo().getResolution().height; + src.mediaPlaylistUrl = playlist.getUri(); + if (src.mediaPlaylistUrl.contains("?")) { + src.mediaPlaylistUrl = src.mediaPlaylistUrl.substring(0, src.mediaPlaylistUrl.lastIndexOf('?')); + } + LOG.trace("Media playlist {}", src.mediaPlaylistUrl); + sources.add(src); + } + } + return sources; + } + + private MasterPlaylist getMasterPlaylist() throws IOException, ParseException, PlaylistException { + String url = getMasterPlaylistUrl(); + LOG.trace("Loading master playlist {}", url); + Request req = new Request.Builder() + .url(url) + .header(USER_AGENT, Config.getInstance().getSettings().httpUserAgent) + .build(); + try (Response response = getSite().getHttpClient().execute(req)) { + if (response.isSuccessful()) { + String body = response.body().string(); + LOG.trace(body); + InputStream inputStream = new ByteArrayInputStream(body.getBytes(UTF_8)); + PlaylistParser parser = new PlaylistParser(inputStream, Format.EXT_M3U, Encoding.UTF_8, ParsingMode.LENIENT); + Playlist playlist = parser.parse(); + MasterPlaylist master = playlist.getMasterPlaylist(); + return master; + } else { + throw new HttpException(response.code(), response.message()); + } + } + } + + private String getMasterPlaylistUrl() throws IOException { + loadStripchatConfig(getSite()); String name = getName(); String url = getSite().getBaseUrl() + "/api/front/models/username/" + name + "/cam?triggerRequest=loadCam"; Request req = new Request.Builder() @@ -110,29 +145,35 @@ public class StripchatModel extends AbstractModel { String streamName = jsonResponse.optString("streamName", jsonResponse.optString("")); JSONObject viewServers = jsonResponse.getJSONObject("viewServers"); String serverName = viewServers.optString("flashphoner-hls"); - JSONObject broadcastSettings = jsonResponse.getJSONObject("broadcastSettings"); - List sources = new ArrayList<>(); - StreamSource best = new StreamSource(); - best.height = broadcastSettings.optInt("height"); - best.width = broadcastSettings.optInt("width"); - best.mediaPlaylistUrl = "https://b-" + serverName + ".stripst.com/hls/" + streamName + '/' + streamName + ".m3u8"; - sources.add(best); - JSONObject presets = broadcastSettings.optJSONObject("presets"); - Object defaultObject = presets.get("testing"); - if (defaultObject instanceof JSONObject) { - JSONObject defaults = (JSONObject) defaultObject; - JSONArray heights = defaults.names(); - for (int i = 0; i < heights.length(); i++) { - String h = heights.getString(i); - StreamSource streamSource = new StreamSource(); - streamSource.height = Integer.parseInt(h.replaceAll("[^\\d]", "")); - streamSource.width = streamSource.height * best.getWidth() / best.getHeight(); - String source = streamName + '_' + streamSource.height + 'p'; - streamSource.mediaPlaylistUrl = "https://b-" + serverName + ".stripst.com/hls/" + source + '/' + source + ".m3u8"; - sources.add(streamSource); - } - } - return sources.stream().sorted((a, b) -> a.height - b.height).collect(Collectors.toList()); + return "https://b-" + serverName + '.' + stripchatConfig.hlsStreamHost + "/hls/" + streamName + "/master/" + streamName + "_auto.m3u8"; + } else { + throw new HttpException(response.code(), response.message()); + } + } + } + + private static synchronized void loadStripchatConfig(Site site) throws IOException { + if (stripchatConfig != null) { + return; + } + Request req = new Request.Builder() + .url(site.getBaseUrl() + "/api/front/v2/config?uniq=g8wizmarpvck4dj5") + .header(ACCEPT, "*/*") + .header(ACCEPT_LANGUAGE, Locale.ENGLISH.getLanguage()) + .header(X_REQUESTED_WITH, XML_HTTP_REQUEST) + .header(USER_AGENT, Config.getInstance().getSettings().httpUserAgent) + .header(REFERER, site.getBaseUrl()) + .build(); + + LOG.debug("Loading config from {}", req.url()); + try (Response response = site.getHttpClient().execute(req)) { + if (response.isSuccessful()) { + JSONObject json = new JSONObject(Objects.requireNonNull(response.body(), "HTTP response body is null").string()); + LOG.trace(json.toString(2)); + JSONObject config = json.getJSONObject("config"); + String hlsStreamHost = config.getString("hlsStreamHost"); + stripchatConfig = new StripchatConfig(); + stripchatConfig.hlsStreamHost = hlsStreamHost; } else { throw new HttpException(response.code(), response.message()); } @@ -143,7 +184,7 @@ public class StripchatModel extends AbstractModel { @Override public void invalidateCacheEntries() { status = null; - resolution = new int[] { 0, 0 }; + resolution = new int[]{0, 0}; } @Override @@ -158,9 +199,9 @@ public class StripchatModel extends AbstractModel { List sources = getStreamSources(); if (!sources.isEmpty()) { StreamSource best = sources.get(sources.size() - 1); - resolution = new int[] { best.getWidth(), best.getHeight() }; + resolution = new int[]{best.getWidth(), best.getHeight()}; } - } catch (IOException | ParseException | PlaylistException | JAXBException e) { + } catch (IOException | ParseException | PlaylistException e) { throw new ExecutionException(e); } } @@ -233,4 +274,8 @@ public class StripchatModel extends AbstractModel { } } } + + private static class StripchatConfig { + String hlsStreamHost; + } }