From 52cdf8d60127fd47c05b3afe3412cd20e04287e1 Mon Sep 17 00:00:00 2001 From: 0xboobface <0xboobface@gmail.com> Date: Thu, 13 Dec 2018 23:48:16 +0100 Subject: [PATCH] Add classes and first code for Streamate --- .../java/ctbrec/ui/CamrecApplication.java | 2 + .../main/java/ctbrec/ui/SiteUiFactory.java | 8 + .../ui/sites/streamate/StreamateSiteUi.java | 33 +++ .../sites/streamate/StreamateTabProvider.java | 62 +++++ .../streamate/StreamateUpdateService.java | 95 +++++++ .../ctbrec/ui/sites/streamate/girls.sml | 18 ++ .../main/java/ctbrec/io/XmlParserUtils.java | 117 ++++++++ .../ctbrec/sites/streamate/Streamate.java | 193 ++++++++++++++ .../sites/streamate/StreamateHttpClient.java | 75 ++++++ .../sites/streamate/StreamateModel.java | 251 ++++++++++++++++++ 10 files changed, 854 insertions(+) create mode 100644 client/src/main/java/ctbrec/ui/sites/streamate/StreamateSiteUi.java create mode 100644 client/src/main/java/ctbrec/ui/sites/streamate/StreamateTabProvider.java create mode 100644 client/src/main/java/ctbrec/ui/sites/streamate/StreamateUpdateService.java create mode 100644 client/src/main/resources/ctbrec/ui/sites/streamate/girls.sml create mode 100644 common/src/main/java/ctbrec/io/XmlParserUtils.java create mode 100644 common/src/main/java/ctbrec/sites/streamate/Streamate.java create mode 100644 common/src/main/java/ctbrec/sites/streamate/StreamateHttpClient.java create mode 100644 common/src/main/java/ctbrec/sites/streamate/StreamateModel.java diff --git a/client/src/main/java/ctbrec/ui/CamrecApplication.java b/client/src/main/java/ctbrec/ui/CamrecApplication.java index d698dbdb..d91a9d5e 100644 --- a/client/src/main/java/ctbrec/ui/CamrecApplication.java +++ b/client/src/main/java/ctbrec/ui/CamrecApplication.java @@ -38,6 +38,7 @@ import ctbrec.sites.cam4.Cam4; import ctbrec.sites.camsoda.Camsoda; import ctbrec.sites.chaturbate.Chaturbate; import ctbrec.sites.mfc.MyFreeCams; +import ctbrec.sites.streamate.Streamate; import ctbrec.ui.settings.SettingsTab; import javafx.application.Application; import javafx.application.HostServices; @@ -76,6 +77,7 @@ public class CamrecApplication extends Application { sites.add(new Camsoda()); sites.add(new Chaturbate()); sites.add(new MyFreeCams()); + sites.add(new Streamate()); loadConfig(); registerAlertSystem(); createHttpClient(); diff --git a/client/src/main/java/ctbrec/ui/SiteUiFactory.java b/client/src/main/java/ctbrec/ui/SiteUiFactory.java index 8ef694d1..7475868c 100644 --- a/client/src/main/java/ctbrec/ui/SiteUiFactory.java +++ b/client/src/main/java/ctbrec/ui/SiteUiFactory.java @@ -6,11 +6,13 @@ import ctbrec.sites.cam4.Cam4; import ctbrec.sites.camsoda.Camsoda; import ctbrec.sites.chaturbate.Chaturbate; import ctbrec.sites.mfc.MyFreeCams; +import ctbrec.sites.streamate.Streamate; import ctbrec.ui.sites.bonga.BongaCamsSiteUi; import ctbrec.ui.sites.cam4.Cam4SiteUi; import ctbrec.ui.sites.camsoda.CamsodaSiteUi; import ctbrec.ui.sites.chaturbate.ChaturbateSiteUi; import ctbrec.ui.sites.myfreecams.MyFreeCamsSiteUi; +import ctbrec.ui.sites.streamate.StreamateSiteUi; public class SiteUiFactory { @@ -19,6 +21,7 @@ public class SiteUiFactory { private static CamsodaSiteUi camsodaSiteUi; private static ChaturbateSiteUi ctbSiteUi; private static MyFreeCamsSiteUi mfcSiteUi; + private static StreamateSiteUi streamateSiteUi; public static synchronized SiteUI getUi(Site site) { if (site instanceof BongaCams) { @@ -46,6 +49,11 @@ public class SiteUiFactory { mfcSiteUi = new MyFreeCamsSiteUi((MyFreeCams) site); } return mfcSiteUi; + } else if (site instanceof Streamate) { + if (streamateSiteUi == null) { + streamateSiteUi = new StreamateSiteUi((Streamate) site); + } + return streamateSiteUi; } throw new RuntimeException("Unknown site " + site.getName()); } diff --git a/client/src/main/java/ctbrec/ui/sites/streamate/StreamateSiteUi.java b/client/src/main/java/ctbrec/ui/sites/streamate/StreamateSiteUi.java new file mode 100644 index 00000000..8d31d020 --- /dev/null +++ b/client/src/main/java/ctbrec/ui/sites/streamate/StreamateSiteUi.java @@ -0,0 +1,33 @@ +package ctbrec.ui.sites.streamate; + +import java.io.IOException; + +import ctbrec.sites.ConfigUI; +import ctbrec.sites.streamate.Streamate; +import ctbrec.ui.SiteUI; +import ctbrec.ui.TabProvider; + +public class StreamateSiteUi implements SiteUI { + + private StreamateTabProvider tabProvider; + + public StreamateSiteUi(Streamate streamate) { + tabProvider = new StreamateTabProvider(streamate); + } + + @Override + public TabProvider getTabProvider() { + return tabProvider; + } + + @Override + public ConfigUI getConfigUI() { + return null; + } + + @Override + public boolean login() throws IOException { + return false; + } + +} diff --git a/client/src/main/java/ctbrec/ui/sites/streamate/StreamateTabProvider.java b/client/src/main/java/ctbrec/ui/sites/streamate/StreamateTabProvider.java new file mode 100644 index 00000000..5c74b825 --- /dev/null +++ b/client/src/main/java/ctbrec/ui/sites/streamate/StreamateTabProvider.java @@ -0,0 +1,62 @@ +package ctbrec.ui.sites.streamate; + +import java.io.ByteArrayOutputStream; +import java.io.IOException; +import java.io.InputStream; +import java.util.ArrayList; +import java.util.List; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import ctbrec.recorder.Recorder; +import ctbrec.sites.streamate.Streamate; +import ctbrec.ui.TabProvider; +import ctbrec.ui.ThumbOverviewTab; +import javafx.scene.Scene; +import javafx.scene.control.Tab; + +public class StreamateTabProvider extends TabProvider { + private static final transient Logger LOG = LoggerFactory.getLogger(StreamateTabProvider.class); + private Streamate streamate; + private Recorder recorder; + + public StreamateTabProvider(Streamate streamate) { + this.streamate = streamate; + this.recorder = streamate.getRecorder(); + } + + @Override + public List getTabs(Scene scene) { + List tabs = new ArrayList<>(); + try { + tabs.add(createTab("Girls", "/ctbrec/ui/sites/streamate/girls.sml")); + } catch (IOException e) { + LOG.error("Couldn't create streamate tab", e); + } + return tabs; + } + + @Override + public Tab getFollowedTab() { + return null; + } + + private Tab createTab(String title, String queryFile) throws IOException { + StreamateUpdateService updateService = new StreamateUpdateService(loadQuery(queryFile), streamate); + ThumbOverviewTab tab = new ThumbOverviewTab(title, updateService, streamate); + tab.setRecorder(recorder); + return tab; + } + + private String loadQuery(String file) throws IOException { + InputStream is = getClass().getResourceAsStream(file); + ByteArrayOutputStream bos = new ByteArrayOutputStream(); + byte[] b = new byte[1024]; + int len = -1; + while( (len = is.read(b)) >= 0) { + bos.write(b, 0, len); + } + return new String(bos.toByteArray(), "utf-8"); + } +} diff --git a/client/src/main/java/ctbrec/ui/sites/streamate/StreamateUpdateService.java b/client/src/main/java/ctbrec/ui/sites/streamate/StreamateUpdateService.java new file mode 100644 index 00000000..1c594e93 --- /dev/null +++ b/client/src/main/java/ctbrec/ui/sites/streamate/StreamateUpdateService.java @@ -0,0 +1,95 @@ +package ctbrec.ui.sites.streamate; + +import java.io.ByteArrayInputStream; +import java.io.IOException; +import java.util.ArrayList; +import java.util.List; + +import javax.xml.parsers.DocumentBuilderFactory; +import javax.xml.parsers.ParserConfigurationException; +import javax.xml.xpath.XPathExpressionException; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.w3c.dom.Document; +import org.w3c.dom.Node; +import org.w3c.dom.NodeList; +import org.xml.sax.SAXException; + +import ctbrec.Config; +import ctbrec.Model; +import ctbrec.io.XmlParserUtils; +import ctbrec.sites.streamate.Streamate; +import ctbrec.sites.streamate.StreamateModel; +import ctbrec.ui.PaginatedScheduledService; +import javafx.concurrent.Task; +import okhttp3.MediaType; +import okhttp3.Request; +import okhttp3.RequestBody; +import okhttp3.Response; + +public class StreamateUpdateService extends PaginatedScheduledService { + + private static final transient Logger LOG = LoggerFactory.getLogger(StreamateUpdateService.class); + + private static final String URL = "http://affiliate.streamate.com/SMLive/SMLResult.xml"; + private Streamate streamate; + private String query; + + public StreamateUpdateService(String query, Streamate streamate) { + this.query = query; + this.streamate = streamate; + } + + @Override + protected Task> createTask() { + return new Task>() { + @Override + public List call() throws IOException, SAXException, ParserConfigurationException, XPathExpressionException { + LOG.debug("Fetching page {}", URL); + String q = query + .replace("{maxresults}", "50") + .replace("{pagenum}", Integer.toString(page)); + //LOG.debug("Query:\n{}", q); + RequestBody body = RequestBody.create(MediaType.parse("text/xml"), q); + Request request = new Request.Builder() + .url(URL) + .addHeader("User-Agent", Config.getInstance().getSettings().httpUserAgent) + .addHeader("Accept", "text/xml, */*") + .addHeader("Accept-Language", "en") + .addHeader("Referer", streamate.getBaseUrl()) + .post(body) + .build(); + Response response = streamate.getHttpClient().execute(request); + if (response.isSuccessful()) { + List models = new ArrayList<>(); + String content = response.body().string(); + LOG.debug(content); + ByteArrayInputStream in = new ByteArrayInputStream(content.getBytes("utf-8")); + Document doc = DocumentBuilderFactory.newInstance().newDocumentBuilder().parse(in); + NodeList performers = doc.getElementsByTagName("Performer"); + for (int i = 0; i < performers.getLength(); i++) { + Node performer = performers.item(i); + String name = performer.getAttributes().getNamedItem("Name").getNodeValue(); + String id = performer.getAttributes().getNamedItem("Id").getNodeValue(); + String GoldShow = performer.getAttributes().getNamedItem("GoldShow").getNodeValue(); + String PreGoldShow = performer.getAttributes().getNamedItem("PreGoldShow").getNodeValue(); + String PartyChat = performer.getAttributes().getNamedItem("PartyChat").getNodeValue(); + StreamateModel model = (StreamateModel) streamate.createModel(name); + model.setId(id); + models.add(model); + Node pic = XmlParserUtils.getNodeWithXpath(performer, "Media/Pic/Full"); + String previewUrl = "https:" + pic.getAttributes().getNamedItem("Src").getNodeValue(); + model.setPreview(previewUrl); + LOG.debug("Name {} - {}{}{}", name, PartyChat, PreGoldShow, GoldShow); + } + return models; + } else { + int code = response.code(); + response.close(); + throw new IOException("HTTP status " + code); + } + } + }; + } +} diff --git a/client/src/main/resources/ctbrec/ui/sites/streamate/girls.sml b/client/src/main/resources/ctbrec/ui/sites/streamate/girls.sml new file mode 100644 index 00000000..f7841ae4 --- /dev/null +++ b/client/src/main/resources/ctbrec/ui/sites/streamate/girls.sml @@ -0,0 +1,18 @@ + + + + + + + + biopic, staticbiopic + + + + live,recorded + + + + \ No newline at end of file diff --git a/common/src/main/java/ctbrec/io/XmlParserUtils.java b/common/src/main/java/ctbrec/io/XmlParserUtils.java new file mode 100644 index 00000000..a1ac9cf6 --- /dev/null +++ b/common/src/main/java/ctbrec/io/XmlParserUtils.java @@ -0,0 +1,117 @@ +package ctbrec.io; + +import java.io.IOException; +import java.io.StringReader; +import java.util.List; + +import javax.xml.parsers.DocumentBuilder; +import javax.xml.parsers.DocumentBuilderFactory; +import javax.xml.parsers.ParserConfigurationException; +import javax.xml.xpath.XPath; +import javax.xml.xpath.XPathConstants; +import javax.xml.xpath.XPathExpressionException; +import javax.xml.xpath.XPathFactory; + +import org.w3c.dom.Document; +import org.w3c.dom.Node; +import org.w3c.dom.NodeList; +import org.xml.sax.InputSource; +import org.xml.sax.SAXException; + +public class XmlParserUtils { + + public static Document parse(String xml) throws ParserConfigurationException, SAXException, IOException { + DocumentBuilderFactory factory = DocumentBuilderFactory.newInstance(); + DocumentBuilder builder = factory.newDocumentBuilder(); + return builder.parse(new InputSource(new StringReader(xml))); + } + + public static Node getFirstElementByTagName(Document doc, String tagName) { + NodeList list = doc.getElementsByTagName(tagName); + if (list.getLength() > 0) { + return list.item(0); + } else { + return null; + } + } + + public static String getTextContent(Document doc, String tagName) { + Node node = getFirstElementByTagName(doc, tagName); + if (node != null) { + return node.getTextContent(); + } else { + return null; + } + } + + public static String getTextContent(Node parent, String tagName) { + Node node = findChildWithTagName(parent, tagName); + if (node != null) { + return node.getTextContent(); + } else { + return null; + } + } + + public static Node findChildWithTagName(Node parent, String tagName) { + if (parent == null) { + return null; + } + + NodeList childs = parent.getChildNodes(); + for (int i = 0; i < childs.getLength(); i++) { + Node child = childs.item(i); + if (child.getNodeName().equals(tagName)) { + return child; + } else if (child.hasChildNodes()) { + Node result = findChildWithTagName(child, tagName); + if (result != null) { + return result; + } + } + } + + return null; + } + + public static void getElementsByTagName(Node parent, String tagName, List result) { + if (parent == null) { + return; + } + + NodeList childs = parent.getChildNodes(); + for (int i = 0; i < childs.getLength(); i++) { + Node child = childs.item(i); + if (child.getNodeName().equals(tagName)) { + result.add(child); + } else if (child.hasChildNodes()) { + getElementsByTagName(child, tagName, result); + } + } + } + + public static String getStringWithXpath(String xml, String xpath) throws XPathExpressionException { + XPath xp = XPathFactory.newInstance().newXPath(); + return xp.evaluate(xpath, new InputSource(new StringReader(xml))); + } + + public static String getStringWithXpath(Node node, String xpath) throws XPathExpressionException { + XPath xp = XPathFactory.newInstance().newXPath(); + return xp.evaluate(xpath, node); + } + + public static Node getNodeWithXpath(String xml, String xpath) throws XPathExpressionException { + XPath xp = XPathFactory.newInstance().newXPath(); + return (Node) xp.evaluate(xpath, new InputSource(new StringReader(xml)), XPathConstants.NODE); + } + + public static Node getNodeWithXpath(Node node, String xpath) throws XPathExpressionException { + XPath xp = XPathFactory.newInstance().newXPath(); + return (Node) xp.evaluate(xpath, node, XPathConstants.NODE); + } + + public static NodeList getNodeListWithXpath(String xml, String xpath) throws XPathExpressionException { + XPath xp = XPathFactory.newInstance().newXPath(); + return (NodeList) xp.evaluate(xpath, new InputSource(new StringReader(xml)), XPathConstants.NODESET); + } +} diff --git a/common/src/main/java/ctbrec/sites/streamate/Streamate.java b/common/src/main/java/ctbrec/sites/streamate/Streamate.java new file mode 100644 index 00000000..e0da236b --- /dev/null +++ b/common/src/main/java/ctbrec/sites/streamate/Streamate.java @@ -0,0 +1,193 @@ +package ctbrec.sites.streamate; + +import java.io.IOException; +import java.util.Collections; +import java.util.List; +import java.util.regex.Matcher; +import java.util.regex.Pattern; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import ctbrec.Model; +import ctbrec.io.HttpClient; +import ctbrec.sites.AbstractSite; + +public class Streamate extends AbstractSite { + + private static final transient Logger LOG = LoggerFactory.getLogger(Streamate.class); + + public static final String BASE_URL = "https://www.streamate.com"; + + private StreamateHttpClient httpClient; + + @Override + public String getName() { + return "Streamate"; + } + + @Override + public String getBaseUrl() { + return BASE_URL; + } + + @Override + public String getAffiliateLink() { + return BASE_URL + "/landing/click/?AFNO=2-11330.2"; + } + + @Override + public Model createModel(String name) { + StreamateModel model = new StreamateModel(); + model.setName(name); + model.setUrl(BASE_URL + "/cam/" + name); + model.setDescription(""); + model.setSite(this); + return model; + } + + @Override + public Integer getTokenBalance() throws IOException { + // int userId = ((StreamateHttpClient)getHttpClient()).getUserId(); + // String url = Streamate.BASE_URL + "/tools/amf.php"; + // RequestBody body = new FormBody.Builder() + // .add("method", "ping") + // .add("args[]", Integer.toString(userId)) + // .build(); + // Request request = new Request.Builder() + // .url(url) + // .addHeader("User-Agent", Config.getInstance().getSettings().httpUserAgent) + // .addHeader("Accept", "application/json, text/javascript, */*") + // .addHeader("Accept-Language", "en") + // .addHeader("Referer", Streamate.BASE_URL) + // .addHeader("X-Requested-With", "XMLHttpRequest") + // .post(body) + // .build(); + // try(Response response = getHttpClient().execute(request)) { + // if(response.isSuccessful()) { + // JSONObject json = new JSONObject(response.body().string()); + // if(json.optString("status").equals("online")) { + // JSONObject userData = json.getJSONObject("userData"); + // return userData.getInt("balance"); + // } else { + // throw new IOException("Request was not successful: " + json.toString(2)); + // } + // } else { + // throw new HttpException(response.code(), response.message()); + // } + // } + return 0; + } + + @Override + public String getBuyTokensLink() { + return getAffiliateLink(); + } + + @Override + public synchronized boolean login() throws IOException { + return credentialsAvailable() && getHttpClient().login(); + } + + @Override + public HttpClient getHttpClient() { + if(httpClient == null) { + httpClient = new StreamateHttpClient(); + } + return httpClient; + } + + @Override + public void init() throws IOException { + } + + @Override + public void shutdown() { + if(httpClient != null) { + httpClient.shutdown(); + } + } + + @Override + public boolean supportsTips() { + return false; + } + + @Override + public boolean supportsFollow() { + return false; + } + + @Override + public boolean supportsSearch() { + return false; + } + + @Override + public boolean searchRequiresLogin() { + return true; + } + + @Override + public List search(String q) throws IOException, InterruptedException { + // String url = BASE_URL + "/tools/listing_v3.php?offset=0&model_search[display_name][text]=" + URLEncoder.encode(q, "utf-8"); + // Request req = new Request.Builder() + // .url(url) + // .addHeader("User-Agent", Config.getInstance().getSettings().httpUserAgent) + // .addHeader("Accept", "application/json, text/javascript, */*") + // .addHeader("Accept-Language", "en") + // .addHeader("Referer", Streamate.BASE_URL) + // .addHeader("X-Requested-With", "XMLHttpRequest") + // .build(); + // try(Response response = getHttpClient().execute(req)) { + // if(response.isSuccessful()) { + // String body = response.body().string(); + // JSONObject json = new JSONObject(body); + // if(json.optString("status").equals("success")) { + // List models = new ArrayList<>(); + // JSONArray results = json.getJSONArray("models"); + // for (int i = 0; i < results.length(); i++) { + // JSONObject result = results.getJSONObject(i); + // Model model = createModel(result.getString("username")); + // String thumb = result.getString("thumb_image"); + // if(thumb != null) { + // model.setPreview("https:" + thumb); + // } + // if(result.has("display_name")) { + // model.setDisplayName(result.getString("display_name")); + // } + // models.add(model); + // } + // return models; + // } else { + // LOG.warn("Search result: " + json.toString(2)); + // return Collections.emptyList(); + // } + // } else { + // throw new HttpException(response.code(), response.message()); + // } + // } + return Collections.emptyList(); + } + + @Override + public boolean isSiteForModel(Model m) { + return m instanceof StreamateModel; + } + + @Override + public boolean credentialsAvailable() { + return false; + } + + @Override + public Model createModelFromUrl(String url) { + Matcher m = Pattern.compile("https?://.*?streamate.com/cam/([^/]*?)/?").matcher(url); + if(m.matches()) { + String modelName = m.group(1); + return createModel(modelName); + } else { + return super.createModelFromUrl(url); + } + } +} diff --git a/common/src/main/java/ctbrec/sites/streamate/StreamateHttpClient.java b/common/src/main/java/ctbrec/sites/streamate/StreamateHttpClient.java new file mode 100644 index 00000000..6772eadd --- /dev/null +++ b/common/src/main/java/ctbrec/sites/streamate/StreamateHttpClient.java @@ -0,0 +1,75 @@ +package ctbrec.sites.streamate; + +import java.io.IOException; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import ctbrec.io.HttpClient; + +public class StreamateHttpClient extends HttpClient { + + private static final transient Logger LOG = LoggerFactory.getLogger(StreamateHttpClient.class); + + public StreamateHttpClient() { + super("streamate"); + } + + @Override + public synchronized boolean login() throws IOException { + if(loggedIn) { + return true; + } + + boolean cookiesWorked = checkLoginSuccess(); + if(cookiesWorked) { + loggedIn = true; + LOG.debug("Logged in with cookies"); + return true; + } + + return false; + } + + /** + * Check, if the login worked + * @throws IOException + */ + public boolean checkLoginSuccess() throws IOException { + return false; + // String modelName = getAnyModelName(); + // // we request the roomData of a random model, because it contains + // // user data, if the user is logged in, which we can use to verify, that the login worked + // String url = Streamate.BASE_URL + "/tools/amf.php"; + // RequestBody body = new FormBody.Builder() + // .add("method", "getRoomData") + // .add("args[]", modelName) + // .add("args[]", "false") + // //.add("method", "ping") // TODO alternative request, but + // //.add("args[]", ) // where to get the userId + // .build(); + // Request request = new Request.Builder() + // .url(url) + // .addHeader("User-Agent", Config.getInstance().getSettings().httpUserAgent) + // .addHeader("Accept", "application/json, text/javascript, */*") + // .addHeader("Accept-Language", "en") + // .addHeader("Referer", Streamate.BASE_URL) + // .addHeader("X-Requested-With", "XMLHttpRequest") + // .post(body) + // .build(); + // try(Response response = execute(request)) { + // if(response.isSuccessful()) { + // JSONObject json = new JSONObject(response.body().string()); + // if(json.optString("status").equals("success")) { + // JSONObject userData = json.getJSONObject("userData"); + // userId = userData.optInt("userId"); + // return userId > 0; + // } else { + // throw new IOException("Request was not successful: " + json.toString(2)); + // } + // } else { + // throw new HttpException(response.code(), response.message()); + // } + // } + } +} diff --git a/common/src/main/java/ctbrec/sites/streamate/StreamateModel.java b/common/src/main/java/ctbrec/sites/streamate/StreamateModel.java new file mode 100644 index 00000000..20c80be9 --- /dev/null +++ b/common/src/main/java/ctbrec/sites/streamate/StreamateModel.java @@ -0,0 +1,251 @@ +package ctbrec.sites.streamate; + +import static ctbrec.Model.State.*; + +import java.io.IOException; +import java.io.InputStream; +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; +import java.util.concurrent.ExecutionException; + +import org.json.JSONObject; +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.io.HttpException; +import ctbrec.recorder.download.StreamSource; +import okhttp3.FormBody; +import okhttp3.Request; +import okhttp3.RequestBody; +import okhttp3.Response; + +public class StreamateModel extends AbstractModel { + + private static final transient Logger LOG = LoggerFactory.getLogger(StreamateModel.class); + + private boolean online = false; + private List streamSources = new ArrayList<>(); + private int[] resolution; + private String id; + + @Override + public boolean isOnline(boolean ignoreCache) throws IOException, ExecutionException, InterruptedException { + if(ignoreCache) { + String url = getStreamUrl(); + Request req = new Request.Builder().url(url).build(); + try(Response resp = site.getHttpClient().execute(req)) { + online = resp.isSuccessful(); + } + } + return online; + } + + private JSONObject getRoomData() throws IOException { + String url = Streamate.BASE_URL + "/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", "application/json, text/javascript, */*") + .addHeader("Accept-Language", "en") + .addHeader("Referer", Streamate.BASE_URL) + .addHeader("X-Requested-With", "XMLHttpRequest") + .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(); + if (streamUrl == null) { + return Collections.emptyList(); + } + 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 { + String url = "https://hybridclient.naiadsystems.com/api/v1/config/?sabasic=&sakey=&sk=www.streamate.com&userid=0&version=6.3.16&ajax=1&name=" + getName(); + Request req = new Request.Builder() + .addHeader("User-Agent", Config.getInstance().getSettings().httpUserAgent) + .addHeader("Accept", "application/json, text/javascript, */*") + .addHeader("Accept-Language", "en") + .addHeader("Referer", Streamate.BASE_URL + '/' + getName()) + .addHeader("X-Requested-With", "XMLHttpRequest") + .url(url) + .build(); + try(Response response = site.getHttpClient().execute(req)) { + if(response.isSuccessful()) { + JSONObject json = new JSONObject(response.body().string()); + JSONObject performer = json.getJSONObject("performer"); + id = performer.getString("id"); + JSONObject stream = json.getJSONObject("stream"); + String sserver = stream.getString("serverId"); + String streamId = stream.getString("streamId"); + String wsHost = stream.getString("nodeHost"); + LOG.debug(json.toString(2)); + + String wsUrl = wsHost + "/socket.io/?" + + "performerid=" + id + + "&sserver=" + sserver + + "&streamid=" + streamId + + "&sakey=&sessiontype=preview&perfdiscountid=0&minduration=0&goldshowid=0&version=7&referrer=hybrid.client.6.3.16/avchat.swf&usertype=false&lang=en&EIO=3&transport=websocket"; + } else { + throw new IOException(response.code() + ' ' + response.message()); + } + } + return ""; + } + + @Override + public void invalidateCacheEntries() { + resolution = null; + } + + @Override + public void receiveTip(int tokens) throws IOException { + // String url = Streamate.BASE_URL + "/chat-ajax-amf-service?" + System.currentTimeMillis(); + // int userId = ((StreamateHttpClient)site.getHttpClient()).getUserId(); + // RequestBody body = new FormBody.Builder() + // .add("method", "tipModel") + // .add("args[]", getName()) + // .add("args[]", Integer.toString(tokens)) + // .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", "application/json, text/javascript, */*") + // .addHeader("Accept-Language", "en") + // .addHeader("Referer", Streamate.BASE_URL + '/' + getName()) + // .addHeader("X-Requested-With", "XMLHttpRequest") + // .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 streamSources = getStreamSources(); + Collections.sort(streamSources); + StreamSource best = streamSources.get(streamSources.size()-1); + resolution = new int[] {best.width, best.height}; + } catch (ExecutionException | IOException | ParseException | PlaylistException | InterruptedException e) { + LOG.warn("Couldn't determine stream resolution for {} - {}", getName(), e.getMessage()); + } + return resolution; + } else { + return resolution; + } + } + + @Override + public boolean follow() throws IOException { + return false; + } + + @Override + public boolean unfollow() throws IOException { + return false; + } + + public String getId() { + return id; + } + + public void setId(String id) { + this.id = id; + } +}