package ctbrec.sites.stripchat; 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 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 java.io.ByteArrayInputStream; import java.io.IOException; import java.io.InputStream; import java.text.MessageFormat; import java.util.ArrayList; import java.util.List; import java.util.Locale; import java.util.concurrent.ExecutionException; 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 Logger LOG = LoggerFactory.getLogger(StripchatModel.class); private String status = null; private int[] resolution = new int[]{0, 0}; @Override public boolean isOnline(boolean ignoreCache) throws IOException, ExecutionException, InterruptedException { if (ignoreCache || status == null) { JSONObject jsonResponse = loadModelInfo(); if (jsonResponse.has("user")) { JSONObject user = jsonResponse.getJSONObject("user"); status = user.optString("status"); mapOnlineState(status); } } return onlineState == ONLINE; } private void mapOnlineState(String status) { switch (status) { 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); } } } private JSONObject loadModelInfo() throws IOException { String name = getName(); String url = getSite().getBaseUrl() + "/api/front/users/username/" + name; 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) .header(REFERER, getUrl()) .build(); try (Response response = site.getHttpClient().execute(req)) { if (response.isSuccessful()) { JSONObject jsonResponse = new JSONObject(response.body().string()); return jsonResponse; } else { throw new HttpException(response.code(), response.message()); } } } @Override 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 { String name = getName(); String url = getSite().getBaseUrl() + "/api/front/models/username/" + name + "/cam?triggerRequest=loadCam"; 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) .header(REFERER, getUrl()) .build(); try (Response response = site.getHttpClient().execute(req)) { if (response.isSuccessful()) { JSONObject jsonResponse = new JSONObject(response.body().string()); String streamName = jsonResponse.optString("streamName", jsonResponse.optString("")); JSONObject viewServers = jsonResponse.getJSONObject("viewServers"); String serverName = viewServers.optString("flashphoner-hls"); String hslUrlTemplate = "https://b-{0}.doppiocdn.com/hls/{1}/master/{1}_auto.m3u8"; return MessageFormat.format(hslUrlTemplate, serverName, streamName); } else { throw new HttpException(response.code(), response.message()); } } } @Override public void invalidateCacheEntries() { status = null; resolution = new int[]{0, 0}; } @Override public void receiveTip(Double tokens) throws IOException { // not implemented } @Override public int[] getStreamResolution(boolean failFast) throws ExecutionException { if (!failFast) { try { List sources = getStreamSources(); if (!sources.isEmpty()) { StreamSource best = sources.get(sources.size() - 1); resolution = new int[]{best.getWidth(), best.getHeight()}; } } catch (IOException | ParseException | PlaylistException e) { throw new ExecutionException(e); } } return resolution; } @Override public boolean follow() throws IOException { getSite().getHttpClient().login(); JSONObject modelInfo = loadModelInfo(); JSONObject user = modelInfo.getJSONObject("user"); long modelId = user.optLong("id"); StripchatHttpClient client = (StripchatHttpClient) getSite().getHttpClient(); String url = Stripchat.baseUri + "/api/front/users/" + client.getUserId() + "/favorites/" + modelId; JSONObject requestParams = new JSONObject(); requestParams.put("csrfToken", client.getCsrfToken()); requestParams.put("csrfTimestamp", client.getCsrfTimestamp()); requestParams.put("csrfNotifyTimestamp", client.getCsrfNotifyTimestamp()); RequestBody body = RequestBody.Companion.create(requestParams.toString(), JSON); Request request = new Request.Builder() .url(url) .header(ACCEPT, "*/*") .header(USER_AGENT, Config.getInstance().getSettings().httpUserAgent) .header(ORIGIN, Stripchat.baseUri) .header(REFERER, Stripchat.baseUri + '/' + getName()) .header(CONTENT_TYPE, MIMETYPE_APPLICATION_JSON) .put(body) .build(); try (Response response = client.execute(request)) { if (response.isSuccessful()) { return true; } else { throw new HttpException(response.code(), response.message()); } } } @Override public boolean unfollow() throws IOException { getSite().getHttpClient().login(); JSONObject modelInfo = loadModelInfo(); JSONObject user = modelInfo.getJSONObject("user"); long modelId = user.optLong("id"); JSONArray favoriteIds = new JSONArray(); favoriteIds.put(modelId); StripchatHttpClient client = (StripchatHttpClient) getSite().getHttpClient(); String url = Stripchat.baseUri + "/api/front/users/" + client.getUserId() + "/favorites"; JSONObject requestParams = new JSONObject(); requestParams.put("favoriteIds", favoriteIds); requestParams.put("csrfToken", client.getCsrfToken()); requestParams.put("csrfTimestamp", client.getCsrfTimestamp()); requestParams.put("csrfNotifyTimestamp", client.getCsrfNotifyTimestamp()); RequestBody body = RequestBody.Companion.create(requestParams.toString(), JSON); Request request = new Request.Builder() .url(url) .header(ACCEPT, MIMETYPE_APPLICATION_JSON) .header(USER_AGENT, Config.getInstance().getSettings().httpUserAgent) .header(ORIGIN, Stripchat.baseUri) .header(REFERER, Stripchat.baseUri) .header(CONTENT_TYPE, MIMETYPE_APPLICATION_JSON) .delete(body) .build(); try (Response response = client.execute(request)) { if (response.isSuccessful()) { return true; } else { throw new HttpException(response.code(), response.message()); } } } }