From f3a13a6f0642ce6a764ae53c7a2a4203a5e092ab Mon Sep 17 00:00:00 2001 From: 0xboobface <0xboobface@gmail.com> Date: Fri, 27 Dec 2019 13:51:28 +0100 Subject: [PATCH] Fix CamSoda downloads Some models now have a different stream URL. ctbrec has to distiguish between the old and the new URLs --- .../sites/camsoda/CamsodaUpdateService.java | 10 - .../ctbrec/sites/camsoda/CamsodaModel.java | 230 +++++++++++------- 2 files changed, 142 insertions(+), 98 deletions(-) diff --git a/client/src/main/java/ctbrec/ui/sites/camsoda/CamsodaUpdateService.java b/client/src/main/java/ctbrec/ui/sites/camsoda/CamsodaUpdateService.java index 95dd9ebe..761b2d1d 100644 --- a/client/src/main/java/ctbrec/ui/sites/camsoda/CamsodaUpdateService.java +++ b/client/src/main/java/ctbrec/ui/sites/camsoda/CamsodaUpdateService.java @@ -66,15 +66,11 @@ public class CamsodaUpdateService extends PaginatedScheduledService { if(result.has("tpl")) { JSONArray tpl = result.getJSONArray("tpl"); String name = tpl.getString(getTemplateIndex(template, "username")); - // int connections = tpl.getInt(2); - String streamName = tpl.getString(getTemplateIndex(template, "stream_name")); CamsodaModel model = (CamsodaModel) camsoda.createModel(name); model.setDescription(tpl.getString(getTemplateIndex(template, "subject_html"))); model.setSortOrder(tpl.getFloat(getTemplateIndex(template, "sort_value"))); String preview = "https:" + tpl.getString(getTemplateIndex(template, "thumb")); model.setPreview(preview); - JSONArray edgeServers = tpl.getJSONArray(getTemplateIndex(template, "edge_servers")); - model.setStreamUrl("https://" + edgeServers.getString(0) + "/cam/mp4:" + streamName + "_h264_aac_480p/playlist.m3u8"); String displayName = tpl.getString(getTemplateIndex(template, "display_name")); model.setDisplayName(displayName.replaceAll("[^a-zA-Z0-9]", "")); if(model.getDisplayName().isBlank()) { @@ -84,7 +80,6 @@ public class CamsodaUpdateService extends PaginatedScheduledService { } else { String name = result.getString("username"); CamsodaModel model = (CamsodaModel) camsoda.createModel(name); - String streamName = result.getString("stream_name"); model.setSortOrder(result.getFloat("sort_value")); models.add(model); if(result.has("status")) { @@ -98,11 +93,6 @@ public class CamsodaUpdateService extends PaginatedScheduledService { } } - if(result.has("edge_servers")) { - JSONArray edgeServers = result.getJSONArray("edge_servers"); - model.setStreamUrl("https://" + edgeServers.getString(0) + "/cam/mp4:" + streamName + "_h264_aac_480p/playlist.m3u8"); - } - if(result.has("thumb")) { String previewUrl = "https:" + result.getString("thumb"); model.setPreview(previewUrl); diff --git a/common/src/main/java/ctbrec/sites/camsoda/CamsodaModel.java b/common/src/main/java/ctbrec/sites/camsoda/CamsodaModel.java index 85198e9a..ef1f18ba 100644 --- a/common/src/main/java/ctbrec/sites/camsoda/CamsodaModel.java +++ b/common/src/main/java/ctbrec/sites/camsoda/CamsodaModel.java @@ -1,14 +1,18 @@ package ctbrec.sites.camsoda; import static ctbrec.Model.State.*; +import static ctbrec.io.HttpConstants.*; import java.io.IOException; import java.io.InputStream; import java.util.Collections; import java.util.List; +import java.util.Locale; import java.util.Objects; +import java.util.Random; import java.util.concurrent.ExecutionException; +import org.json.JSONException; import org.json.JSONObject; import org.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -35,41 +39,128 @@ import okhttp3.Response; public class CamsodaModel extends AbstractModel { + private static final String STREAM_NAME = "stream_name"; + private static final String EDGE_SERVERS = "edge_servers"; + private static final String STATUS = "status"; private static final Logger LOG = LoggerFactory.getLogger(CamsodaModel.class); private String streamUrl; private List streamSources = null; private float sortOrder = 0; + private Random random = new Random(); int[] resolution = new int[2]; + boolean oldStreamUrl = true; public String getStreamUrl() throws IOException { - if(streamUrl == null) { - // load model - loadModel(); + if (streamUrl == null) { + if(oldStreamUrl) { + loadModel(); + } else { + getNewStreamUrl(); + } } return streamUrl; } + public String getNewStreamUrl() throws IOException { + String guestUsername = "guest_" + 10_000 + random.nextInt(50_000); + String url = site.getBaseUrl() + "/api/v1/video/vtoken/" + getName() + "?username=" + guestUsername; + Request req = new Request.Builder() + .url(url) + .header(ACCEPT, MIMETYPE_APPLICATION_JSON) + .header(ACCEPT_LANGUAGE, Locale.ENGLISH.getLanguage()) + .header(X_REQUESTED_WITH, XML_HTTP_REQUEST) + .header(USER_AGENT, Config.getInstance().getSettings().httpUserAgent) + .build(); + try (Response response = site.getHttpClient().execute(req)) { + if (response.isSuccessful()) { + JSONObject jsonResponse = new JSONObject(response.body().string()); + if (jsonResponse.optInt(STATUS) == 1) { + String edgeServer = jsonResponse.getJSONArray(EDGE_SERVERS).getString(0); + String streamName = jsonResponse.getString(STREAM_NAME); + String token = jsonResponse.getString("token"); + streamUrl = "https://" + edgeServer + '/' + streamName + "_h264_aac_480p/index.m3u8?token=" + token; + } else { + throw new JSONException("Response does not contain a token"); + } + } else { + throw new HttpException(response.code(), response.message()); + } + } + return streamUrl; + } + + @Override + public List getStreamSources() throws IOException, ExecutionException, ParseException, PlaylistException { + String playlistUrl = getStreamUrl(); + if (playlistUrl == null) { + return Collections.emptyList(); + } + LOG.debug("Loading playlist {}", playlistUrl); + Request req = new Request.Builder() + .url(playlistUrl) + .header(ACCEPT_LANGUAGE, Locale.ENGLISH.getLanguage()) + .header(USER_AGENT, Config.getInstance().getSettings().httpUserAgent) + .build(); + try (Response response = site.getHttpClient().execute(req)) { + if (response.isSuccessful()) { + InputStream inputStream = response.body().byteStream(); + PlaylistParser parser = new PlaylistParser(inputStream, Format.EXT_M3U, Encoding.UTF_8, ParsingMode.LENIENT); + Playlist playlist = parser.parse(); + MasterPlaylist master = playlist.getMasterPlaylist(); + PlaylistData playlistData = master.getPlaylists().get(0); + StreamSource streamsource = new StreamSource(); + if (oldStreamUrl) { + streamsource.mediaPlaylistUrl = streamUrl.replace("playlist.m3u8", playlistData.getUri()); + } else { + int cutOffAt = playlistUrl.indexOf("index.m3u8"); + String segmentPlaylistUrl = playlistUrl.substring(0, cutOffAt) + playlistData.getUri(); + streamsource.mediaPlaylistUrl = segmentPlaylistUrl; + } + if (playlistData.hasStreamInfo()) { + StreamInfo info = playlistData.getStreamInfo(); + streamsource.bandwidth = info.getBandwidth(); + streamsource.width = info.hasResolution() ? info.getResolution().width : 0; + streamsource.height = info.hasResolution() ? info.getResolution().height : 0; + } else { + streamsource.bandwidth = 0; + streamsource.width = 0; + streamsource.height = 0; + } + streamSources = Collections.singletonList(streamsource); + } else { + LOG.trace("Response: {}", response.body().string()); + throw new HttpException(response.code(), response.message()); + } + } + return streamSources; + } + private void loadModel() throws IOException { String modelUrl = site.getBaseUrl() + "/api/v1/user/" + getName(); - Request req = new Request.Builder().url(modelUrl).build(); - Response response = site.getHttpClient().execute(req); - try { - JSONObject result = new JSONObject(response.body().string()); - if(result.getBoolean("status")) { - JSONObject chat = result.getJSONObject("user").getJSONObject("chat"); - String status = chat.getString("status"); - setOnlineStateByStatus(status); - if(chat.has("edge_servers")) { - String edgeServer = chat.getJSONArray("edge_servers").getString(0); - String streamName = chat.getString("stream_name"); - streamUrl = "https://" + edgeServer + "/cam/mp4:" + streamName + "_h264_aac_480p/playlist.m3u8"; + Request req = new Request.Builder() + .url(modelUrl) + .header(ACCEPT, MIMETYPE_APPLICATION_JSON) + .header(ACCEPT_LANGUAGE, Locale.ENGLISH.getLanguage()) + .header(X_REQUESTED_WITH, XML_HTTP_REQUEST) + .header(USER_AGENT, Config.getInstance().getSettings().httpUserAgent) + .build(); + try (Response response = site.getHttpClient().execute(req)) { + if (response.isSuccessful()) { + JSONObject result = new JSONObject(response.body().string()); + if (result.getBoolean(STATUS)) { + JSONObject chat = result.getJSONObject("user").getJSONObject("chat"); + String status = chat.getString(STATUS); + oldStreamUrl = !chat.getString(STREAM_NAME).contains("/"); + if (oldStreamUrl && chat.has(EDGE_SERVERS)) { + String edgeServer = chat.getJSONArray(EDGE_SERVERS).getString(0); + String streamName = chat.getString(STREAM_NAME); + streamUrl = "https://" + edgeServer + "/cam/mp4:" + streamName + "_h264_aac_480p/playlist.m3u8"; + } + setOnlineStateByStatus(status); + } else { + throw new IOException("Result was not ok"); } - - } else { - throw new IOException("Result was not ok"); - } - } finally { - response.close(); + } else throw new HttpException(response.code(), response.message()); } } @@ -116,39 +207,6 @@ public class CamsodaModel extends AbstractModel { } } - @Override - public List getStreamSources() throws IOException, ExecutionException, ParseException, PlaylistException { - String streamUrl = getStreamUrl(); - if(streamUrl == null) { - return Collections.emptyList(); - } - Request req = new Request.Builder().url(streamUrl).build(); - Response response = site.getHttpClient().execute(req); - try { - InputStream inputStream = response.body().byteStream(); - PlaylistParser parser = new PlaylistParser(inputStream, Format.EXT_M3U, Encoding.UTF_8, ParsingMode.LENIENT); - Playlist playlist = parser.parse(); - MasterPlaylist master = playlist.getMasterPlaylist(); - PlaylistData playlistData = master.getPlaylists().get(0); - StreamSource streamsource = new StreamSource(); - streamsource.mediaPlaylistUrl = streamUrl.replace("playlist.m3u8", playlistData.getUri()); - if(playlistData.hasStreamInfo()) { - StreamInfo info = playlistData.getStreamInfo(); - streamsource.bandwidth = info.getBandwidth(); - streamsource.width = info.hasResolution() ? info.getResolution().width : 0; - streamsource.height = info.hasResolution() ? info.getResolution().height : 0; - } else { - streamsource.bandwidth = 0; - streamsource.width = 0; - streamsource.height = 0; - } - streamSources = Collections.singletonList(streamsource); - } finally { - response.close(); - } - return streamSources; - } - @Override public void invalidateCacheEntries() { streamSources = null; @@ -156,31 +214,27 @@ public class CamsodaModel extends AbstractModel { @Override public int[] getStreamResolution(boolean failFast) throws ExecutionException { - if(failFast) { + if (failFast) { return resolution; } else { - if(failFast) { - return new int[] {0,0}; - } else { - try { - List streamSources = getStreamSources(); - if(streamSources.isEmpty()) { - return new int[] {0,0}; - } else { - StreamSource src = streamSources.get(0); - resolution = new int[] {src.width, src.height}; - return resolution; - } - } catch (IOException | ParseException | PlaylistException e) { - throw new ExecutionException(e); + try { + List streamSources = getStreamSources(); + if (streamSources.isEmpty()) { + return new int[] { 0, 0 }; + } else { + StreamSource src = streamSources.get(0); + resolution = new int[] { src.width, src.height }; + return resolution; } + } catch (IOException | ParseException | PlaylistException e) { + throw new ExecutionException(e); } } } @Override public void receiveTip(Double tokens) throws IOException { - String csrfToken = ((CamsodaHttpClient)site.getHttpClient()).getCsrfToken(); + String csrfToken = ((CamsodaHttpClient) site.getHttpClient()).getCsrfToken(); String url = site.getBaseUrl() + "/api/v1/tip/" + getName(); if (!Objects.equals(System.getenv("CTBREC_DEV"), "1")) { LOG.debug("Sending tip {}", url); @@ -191,14 +245,14 @@ public class CamsodaModel extends AbstractModel { Request request = new Request.Builder() .url(url) .post(body) - .addHeader("Referer", Camsoda.BASE_URI + '/' + getName()) - .addHeader("User-Agent", Config.getInstance().getSettings().httpUserAgent) - .addHeader("Accept", "application/json, text/plain, */*") - .addHeader("Accept-Language", "en") - .addHeader("X-CSRF-Token", csrfToken) + .addHeader(REFERER, Camsoda.BASE_URI + '/' + getName()) + .addHeader(USER_AGENT, Config.getInstance().getSettings().httpUserAgent) + .addHeader(ACCEPT, MIMETYPE_APPLICATION_JSON) + .addHeader(ACCEPT_LANGUAGE, Locale.ENGLISH.getLanguage()) + .addHeader(X_CSRF_TOKEN, csrfToken) .build(); - try(Response response = site.getHttpClient().execute(request)) { - if(!response.isSuccessful()) { + try (Response response = site.getHttpClient().execute(request)) { + if (!response.isSuccessful()) { throw new HttpException(response.code(), response.message()); } } @@ -213,13 +267,13 @@ public class CamsodaModel extends AbstractModel { Request request = new Request.Builder() .url(url) .post(RequestBody.create(null, "")) - .addHeader("Referer", Camsoda.BASE_URI + '/' + getName()) - .addHeader("User-Agent", Config.getInstance().getSettings().httpUserAgent) - .addHeader("Accept", "application/json, text/plain, */*") - .addHeader("Accept-Language", "en") - .addHeader("X-CSRF-Token", csrfToken) + .addHeader(REFERER, Camsoda.BASE_URI + '/' + getName()) + .addHeader(USER_AGENT, Config.getInstance().getSettings().httpUserAgent) + .addHeader(ACCEPT, MIMETYPE_APPLICATION_JSON) + .addHeader(ACCEPT_LANGUAGE, Locale.ENGLISH.getLanguage()) + .addHeader(X_CSRF_TOKEN, csrfToken) .build(); - try(Response response = site.getHttpClient().execute(request)) { + try (Response response = site.getHttpClient().execute(request)) { if (response.isSuccessful()) { return true; } else { @@ -236,11 +290,11 @@ public class CamsodaModel extends AbstractModel { Request request = new Request.Builder() .url(url) .post(RequestBody.create(null, "")) - .addHeader("Referer", Camsoda.BASE_URI + '/' + getName()) - .addHeader("User-Agent", Config.getInstance().getSettings().httpUserAgent) - .addHeader("Accept", "application/json, text/plain, */*") - .addHeader("Accept-Language", "en") - .addHeader("X-CSRF-Token", csrfToken) + .addHeader(REFERER, Camsoda.BASE_URI + '/' + getName()) + .addHeader(USER_AGENT, Config.getInstance().getSettings().httpUserAgent) + .addHeader(ACCEPT, MIMETYPE_APPLICATION_JSON) + .addHeader(ACCEPT_LANGUAGE, Locale.ENGLISH.getLanguage()) + .addHeader(X_CSRF_TOKEN, csrfToken) .build(); try (Response response = site.getHttpClient().execute(request)) { if (response.isSuccessful()) {