package ctbrec.sites.bonga; import static ctbrec.Model.State.*; import static ctbrec.io.HttpConstants.*; import java.io.IOException; import java.io.InputStream; import java.net.URLEncoder; import java.nio.charset.StandardCharsets; import java.util.ArrayList; import java.util.Collections; import java.util.List; import java.util.Locale; import java.util.concurrent.ExecutionException; import org.json.JSONArray; import org.json.JSONObject; import org.jsoup.nodes.Element; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import com.iheartradio.m3u8.Encoding; import com.iheartradio.m3u8.Format; import com.iheartradio.m3u8.ParseException; import com.iheartradio.m3u8.ParsingMode; import com.iheartradio.m3u8.PlaylistException; import com.iheartradio.m3u8.PlaylistParser; import com.iheartradio.m3u8.data.MasterPlaylist; import com.iheartradio.m3u8.data.Playlist; import com.iheartradio.m3u8.data.PlaylistData; import com.iheartradio.m3u8.data.StreamInfo; import ctbrec.AbstractModel; import ctbrec.Config; import ctbrec.Model; import ctbrec.io.HtmlParser; import ctbrec.io.HttpException; import ctbrec.recorder.download.StreamSource; import okhttp3.FormBody; import okhttp3.Request; import okhttp3.RequestBody; import okhttp3.Response; public class BongaCamsModel extends AbstractModel { private static final String ARGS = "args[]"; private static final Logger LOG = LoggerFactory.getLogger(BongaCamsModel.class); private static final String SUCCESS = "success"; private static final String STATUS = "status"; private int userId; private boolean online = false; private transient List streamSources = new ArrayList<>(); private int[] resolution; @Override public boolean isOnline(boolean ignoreCache) throws IOException, ExecutionException, InterruptedException { if (ignoreCache) { JSONObject roomData = getRoomData(); if (!roomData.has("performerData")) { return false; } JSONObject performerData = roomData.getJSONObject("performerData"); setDisplayName(performerData.optString("displayName")); String url = BongaCams.baseUrl + "/tools/listing_v3.php?livetab=&online_only=true&offset=0&model_search%5Bdisplay_name%5D%5Btext%5D=" + URLEncoder.encode(getDisplayName(), StandardCharsets.UTF_8.name()) + "&_online_filter=0"; LOG.trace("Online Check: {}", url); Request req = new Request.Builder() .url(url) .header(USER_AGENT, Config.getInstance().getSettings().httpUserAgentMobile) .header(ACCEPT, MIMETYPE_APPLICATION_JSON) .header(ACCEPT_LANGUAGE, "en") .header(X_REQUESTED_WITH, XML_HTTP_REQUEST) .header(REFERER, getSite().getBaseUrl()) .build(); try (Response resp = site.getHttpClient().execute(req)) { String body = resp.body().string(); LOG.trace(body); JSONObject json = new JSONObject(body); if (json.optString(STATUS).equals(SUCCESS)) { JSONArray models = json.getJSONArray("models"); for (int i = 0; i < models.length(); i++) { JSONObject model = models.getJSONObject(i); setDescription(model.optString("topic")); String username = model.optString("username"); if (username.equalsIgnoreCase(getName())) { boolean away = model.optBoolean("is_away"); String room = model.optString("room"); online = !away && model.optBoolean("online") && room.equalsIgnoreCase("public") && isStreamAvailable(); onlineState = Model.State.ONLINE; break; } } } else { online = false; } } } return online; } private boolean isStreamAvailable() throws IOException { String url = getStreamUrl(); Request req = new Request.Builder().url(url).build(); try(Response resp = site.getHttpClient().execute(req)) { if(resp.isSuccessful()) { String body = resp.body().string(); return body.contains("#EXT-X-STREAM-INF"); } else { return false; } } } private JSONObject getRoomData() throws IOException { String url = BongaCams.baseUrl + "/tools/amf.php"; RequestBody body = new FormBody.Builder() .add("method", "getRoomData") .add(ARGS, getName()) .add(ARGS, "false") .build(); Request request = new Request.Builder() .url(url) .addHeader(USER_AGENT, Config.getInstance().getSettings().httpUserAgent) .addHeader(ACCEPT, MIMETYPE_APPLICATION_JSON) .addHeader(ACCEPT_LANGUAGE, Locale.ENGLISH.getLanguage()) .addHeader(REFERER, BongaCams.baseUrl) .addHeader(X_REQUESTED_WITH, XML_HTTP_REQUEST) .post(body) .build(); try(Response response = site.getHttpClient().execute(request)) { if(response.isSuccessful()) { JSONObject json = new JSONObject(response.body().string()); return json; } else { throw new IOException(response.code() + " " + response.message()); } } } public void setOnline(boolean online) { this.online = online; } @Override public State getOnlineState(boolean failFast) throws IOException, ExecutionException { if(failFast) { return onlineState; } else { if(onlineState == UNKNOWN) { return online ? ONLINE : OFFLINE; } return onlineState; } } @Override public void setOnlineState(State onlineState) { this.onlineState = onlineState; } @Override public List getStreamSources() throws IOException, ExecutionException, ParseException, PlaylistException { String streamUrl = getStreamUrl(); Request req = new Request.Builder().url(streamUrl).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(); streamSources.clear(); for (PlaylistData playlistData : master.getPlaylists()) { 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.add(streamsource); } } else { throw new HttpException(response.code(), response.message()); } } return streamSources; } private String getStreamUrl() throws IOException { JSONObject roomData = getRoomData(); if(roomData.optString(STATUS).equals(SUCCESS)) { JSONObject localData = roomData.getJSONObject("localData"); String server = localData.getString("videoServerUrl"); return "https:" + server + "/hls/stream_" + getName() + "/playlist.m3u8"; } else { throw new IOException("Request was not successful: " + roomData.toString(2)); } } @Override public void invalidateCacheEntries() { resolution = null; } @Override public void receiveTip(Double tokens) throws IOException { String url = BongaCams.baseUrl + "/chat-ajax-amf-service?" + System.currentTimeMillis(); userId = ((BongaCamsHttpClient)site.getHttpClient()).getUserId(); RequestBody body = new FormBody.Builder() .add("method", "tipModel") .add(ARGS, getName()) .add(ARGS, Integer.toString(tokens.intValue())) .add(ARGS, Integer.toString(userId)) .add("args[3]", "") .build(); Request request = new Request.Builder() .url(url) .addHeader(USER_AGENT, Config.getInstance().getSettings().httpUserAgent) .addHeader(ACCEPT, MIMETYPE_APPLICATION_JSON) .addHeader(ACCEPT_LANGUAGE, Locale.ENGLISH.getLanguage()) .addHeader(REFERER, BongaCams.baseUrl + '/' + getName()) .addHeader(X_REQUESTED_WITH, XML_HTTP_REQUEST) .post(body) .build(); try(Response response = site.getHttpClient().execute(request)) { if(response.isSuccessful()) { JSONObject json = new JSONObject(response.body().string()); if(!json.optString(STATUS).equals(SUCCESS)) { LOG.error("Sending tip failed {}", json.toString(2)); throw new IOException("Sending tip failed"); } } else { throw new IOException(response.code() + ' ' + response.message()); } } } @Override public int[] getStreamResolution(boolean failFast) throws ExecutionException { if(resolution == null) { if(failFast) { return new int[2]; } try { if(!isOnline()) { return new int[2]; } List sources = getStreamSources(); Collections.sort(sources); StreamSource best = sources.get(sources.size()-1); resolution = new int[] {best.width, best.height}; } catch (InterruptedException e) { Thread.currentThread().interrupt(); LOG.warn("Couldn't determine stream resolution for {} - {}", getName(), e.getMessage()); } catch (ExecutionException | IOException | ParseException | PlaylistException e) { LOG.warn("Couldn't determine stream resolution for {} - {}", getName(), e.getMessage()); } return resolution; } else { return resolution; } } @Override public boolean follow() throws IOException { if(!getSite().login()) { throw new IOException("Not logged in"); } String url = getSite().getBaseUrl() + "/follow/" + getName(); LOG.debug("Calling {}", url); RequestBody body = new FormBody.Builder() .add("src", "public-chat") .add("_csrf_token", getCsrfToken()) .build(); Request req = new Request.Builder() .url(url) .method("POST", body) .header(ACCEPT, "*/*") .header(ACCEPT_LANGUAGE, Locale.ENGLISH.getLanguage()) .header(REFERER, getUrl()) .header(USER_AGENT, Config.getInstance().getSettings().httpUserAgent) .header(X_REQUESTED_WITH, XML_HTTP_REQUEST) .build(); try(Response resp = site.getHttpClient().execute(req)) { if(resp.isSuccessful()) { String msg = resp.body().string(); JSONObject json = new JSONObject(msg); if(json.optBoolean(SUCCESS)) { LOG.debug("Follow/Unfollow -> {}", msg); return true; } else { LOG.debug(msg); throw new IOException("Response was " + msg); } } else { throw new HttpException(resp.code(), resp.message()); } } } private String getCsrfToken() throws IOException { Request req = new Request.Builder() .url(getUrl()) .header(ACCEPT, "*/*") .header(ACCEPT_LANGUAGE, Locale.ENGLISH.getLanguage()) .header(REFERER, BongaCams.baseUrl) .header(USER_AGENT, Config.getInstance().getSettings().httpUserAgent) .build(); try(Response resp = site.getHttpClient().execute(req)) { if(resp.isSuccessful()) { String content = resp.body().string(); Element html = HtmlParser.getTag(content, "html"); String csrfToken = html.attr("data-csrf_value"); LOG.debug("CSRF-Token {}", csrfToken); return csrfToken; } else { throw new HttpException(resp.code(), resp.message()); } } } @Override public boolean unfollow() throws IOException { if (!getSite().login()) { throw new IOException("Not logged in"); } String url = getSite().getBaseUrl() + "/unfollow/" + getName() + '/' + getUserId(); LOG.debug("Calling {}", url); RequestBody body = new FormBody.Builder() .add("_csrf_token", getCsrfToken()) .build(); Request req = new Request.Builder() .url(url) .method("POST", body) .header(ACCEPT, "*/*") .header(ACCEPT_LANGUAGE, Locale.ENGLISH.getLanguage()) .header(REFERER, getUrl()) .header(USER_AGENT, Config.getInstance().getSettings().httpUserAgent) .header(X_REQUESTED_WITH, XML_HTTP_REQUEST) .build(); try (Response resp = site.getHttpClient().execute(req)) { if (resp.isSuccessful()) { String msg = resp.body().string(); JSONObject json = new JSONObject(msg); if (json.optBoolean(SUCCESS)) { LOG.debug("Follow/Unfollow -> {}", msg); return true; } else { LOG.debug(msg); throw new IOException("Response was " + msg); } } else { throw new HttpException(resp.code(), resp.message()); } } } public int getUserId() throws IOException { if (userId == 0) { JSONObject roomData = getRoomData(); userId = roomData.getJSONObject("performerData").getInt("userId"); } return userId; } public void setUserId(int userId) { this.userId = userId; } }