package ctbrec.sites.streamate; import com.iheartradio.m3u8.ParseException; import com.iheartradio.m3u8.PlaylistException; import com.squareup.moshi.JsonReader; import com.squareup.moshi.JsonWriter; import ctbrec.AbstractModel; import ctbrec.Config; import ctbrec.NotImplementedExcetion; 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.IOException; import java.net.URLEncoder; import java.util.*; import java.util.concurrent.ExecutionException; import static ctbrec.ErrorMessages.HTTP_RESPONSE_BODY_IS_NULL; import static ctbrec.Model.State.*; import static ctbrec.io.HttpConstants.*; import static ctbrec.sites.streamate.StreamateHttpClient.JSON; import static java.nio.charset.StandardCharsets.UTF_8; public class StreamateModel extends AbstractModel { private static final String ORIGIN = "origin"; private static final Logger LOG = LoggerFactory.getLogger(StreamateModel.class); private static final Long MODEL_ID_UNDEFINED = -1L; private boolean online = false; private final transient List streamSources = new ArrayList<>(); private int[] resolution; private Long id = MODEL_ID_UNDEFINED; @Override public boolean isOnline(boolean ignoreCache) throws IOException, ExecutionException, InterruptedException { if (ignoreCache) { String url = "https://sea1c-ls.naiadsystems.com/sea1c-edge-ls/80/live/s:" + getName() + ".json"; Request req = new Request.Builder().url(url) .addHeader(USER_AGENT, Config.getInstance().getSettings().httpUserAgent) .addHeader(ACCEPT, "*/*") .addHeader(ACCEPT_LANGUAGE, Locale.ENGLISH.getLanguage()) .addHeader(REFERER, Streamate.BASE_URL + '/' + getName()) .addHeader(X_REQUESTED_WITH, XML_HTTP_REQUEST) .build(); try (Response response = site.getHttpClient().execute(req)) { online = response.isSuccessful(); } } return online; } public void setOnline(boolean online) { this.online = online; } @Override public State getOnlineState(boolean failFast) throws IOException, ExecutionException { if (!failFast && onlineState == UNKNOWN) { return online ? ONLINE : OFFLINE; } return onlineState; } @Override public List getStreamSources() throws IOException, ExecutionException, ParseException, PlaylistException { String url = "https://sea1c-ls.naiadsystems.com/sea1c-edge-ls/80/live/s:" + getName() + ".json"; Request req = new Request.Builder().url(url) .addHeader(USER_AGENT, Config.getInstance().getSettings().httpUserAgent) .addHeader(ACCEPT, "*/*") .addHeader(ACCEPT_LANGUAGE, Locale.ENGLISH.getLanguage()) .addHeader(REFERER, Streamate.BASE_URL + '/' + getName()) .addHeader(X_REQUESTED_WITH, XML_HTTP_REQUEST) .build(); try (Response response = site.getHttpClient().execute(req)) { if (response.isSuccessful()) { String body = Objects.requireNonNull(response.body(), HTTP_RESPONSE_BODY_IS_NULL).string(); JSONObject json = new JSONObject(body); JSONObject formats = json.getJSONObject("formats"); JSONObject hls = formats.getJSONObject("mp4-hls"); // add encodings JSONArray encodings = hls.getJSONArray("encodings"); streamSources.clear(); for (int i = 0; i < encodings.length(); i++) { JSONObject encoding = encodings.getJSONObject(i); StreamSource src = new StreamSource(); src.mediaPlaylistUrl = encoding.getString("location"); src.width = encoding.optInt("videoWidth"); src.height = encoding.optInt("videoHeight"); src.bandwidth = (encoding.optInt("videoKbps") + encoding.optInt("audioKbps")) * 1024; streamSources.add(src); } // add raw source stream if (hls.has(ORIGIN) && !hls.isNull(ORIGIN)) { JSONObject origin = hls.getJSONObject(ORIGIN); StreamSource src = new StreamSource(); src.mediaPlaylistUrl = origin.getString("location"); src.width = origin.optInt("videoWidth"); src.height = origin.optInt("videoHeight"); src.bandwidth = (origin.optInt("videoKbps") + origin.optInt("audioKbps")) * 1024; src.height = StreamSource.ORIGIN; streamSources.add(src); } } else { throw new HttpException(response.code(), response.message()); } } return streamSources; } @Override public void invalidateCacheEntries() { resolution = null; } void loadModelId() throws IOException { String url = "https://www.streamate.com/api/performer/lookup?nicknames" + URLEncoder.encode(getName(), UTF_8); Request req = new Request.Builder().url(url) .addHeader(USER_AGENT, Config.getInstance().getSettings().httpUserAgent) .addHeader(ACCEPT, "*/*") .addHeader(ACCEPT_LANGUAGE, Locale.ENGLISH.getLanguage()) .addHeader(REFERER, Streamate.BASE_URL + '/' + getName()) .addHeader(X_REQUESTED_WITH, XML_HTTP_REQUEST) .build(); try (Response response = site.getHttpClient().execute(req)) { if (response.isSuccessful()) { String body = Objects.requireNonNull(response.body(), HTTP_RESPONSE_BODY_IS_NULL).string(); id = new JSONObject(body).getJSONObject("result").getLong(getName()); } else { throw new HttpException(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); if (best.height == StreamSource.ORIGIN) { best = sources.get(sources.size() - 2); } resolution = new int[]{best.width, best.height}; } catch (InterruptedException e) { LOG.warn("Couldn't determine stream resolution for {} - {}", getName(), e.getMessage()); Thread.currentThread().interrupt(); } catch (ExecutionException | IOException | ParseException | PlaylistException e) { LOG.warn("Couldn't determine stream resolution for {} - {}", getName(), e.getMessage()); } } return resolution; } @Override public boolean follow() throws IOException { return follow(true); } @Override public boolean unfollow() throws IOException { return follow(false); } private boolean follow(boolean follow) throws IOException { StreamateHttpClient client = (StreamateHttpClient) getSite().getHttpClient(); client.login(); String saKey = client.getSaKey(); Long userId = client.getUserId(); JSONObject requestParams = new JSONObject(); requestParams.put("sakey", saKey); requestParams.put("userid", userId); requestParams.put("pid", id); requestParams.put("domain", "streamate.com"); requestParams.put("fav", follow); RequestBody body = RequestBody.Companion.create(requestParams.toString(), JSON); String url = site.getBaseUrl() + "/ajax/fav-notify.php?userid=" + userId + "&sakey=" + saKey + "&pid=" + id + "&fav=" + follow + "&domain=streamate.com"; Request request = new Request.Builder() .url(url) .addHeader(USER_AGENT, Config.getInstance().getSettings().httpUserAgent) .addHeader(ACCEPT, "application/json, */*") .addHeader(ACCEPT_LANGUAGE, Locale.ENGLISH.getLanguage()) .addHeader(REFERER, getSite().getBaseUrl()) .post(body) .build(); try (Response response = getSite().getHttpClient().execute(request)) { String content = Objects.requireNonNull(response.body(), HTTP_RESPONSE_BODY_IS_NULL).string(); if (response.isSuccessful()) { JSONObject json = new JSONObject(content); return json.optBoolean("success"); } else { throw new HttpException(response.code(), response.message()); } } } public Long getId() { return id; } public void setId(Long id) { this.id = id; } @Override public void readSiteSpecificData(JsonReader reader) throws IOException { reader.nextName(); id = reader.nextLong(); } @Override public void writeSiteSpecificData(JsonWriter writer) throws IOException { if (id == null || Objects.equals(id, MODEL_ID_UNDEFINED)) { try { loadModelId(); } catch (IOException e) { LOG.error("Couldn't load model ID for {}. This can cause problems with saving / loading the model", getName(), e); } } writer.name("id").value(id); } @Override public void receiveTip(Double tokens) throws IOException { throw new NotImplementedExcetion("Tipping is not implemented for Streamate"); } }