diff --git a/client/src/main/java/ctbrec/ui/CamrecApplication.java b/client/src/main/java/ctbrec/ui/CamrecApplication.java index 59165e9a..7799b036 100644 --- a/client/src/main/java/ctbrec/ui/CamrecApplication.java +++ b/client/src/main/java/ctbrec/ui/CamrecApplication.java @@ -42,6 +42,7 @@ import ctbrec.sites.cam4.Cam4; import ctbrec.sites.camsoda.Camsoda; import ctbrec.sites.chaturbate.Chaturbate; import ctbrec.sites.fc2live.Fc2Live; +import ctbrec.sites.flirt4free.Flirt4Free; import ctbrec.sites.jasmin.LiveJasmin; import ctbrec.sites.mfc.MyFreeCams; import ctbrec.sites.streamate.Streamate; @@ -86,6 +87,7 @@ public class CamrecApplication extends Application { sites.add(new Camsoda()); sites.add(new Chaturbate()); sites.add(new Fc2Live()); + sites.add(new Flirt4Free()); sites.add(new LiveJasmin()); sites.add(new MyFreeCams()); sites.add(new Streamate()); diff --git a/client/src/main/java/ctbrec/ui/SiteUiFactory.java b/client/src/main/java/ctbrec/ui/SiteUiFactory.java index 1a7ce137..2135d854 100644 --- a/client/src/main/java/ctbrec/ui/SiteUiFactory.java +++ b/client/src/main/java/ctbrec/ui/SiteUiFactory.java @@ -6,6 +6,7 @@ import ctbrec.sites.cam4.Cam4; import ctbrec.sites.camsoda.Camsoda; import ctbrec.sites.chaturbate.Chaturbate; import ctbrec.sites.fc2live.Fc2Live; +import ctbrec.sites.flirt4free.Flirt4Free; import ctbrec.sites.jasmin.LiveJasmin; import ctbrec.sites.mfc.MyFreeCams; import ctbrec.sites.streamate.Streamate; @@ -14,6 +15,7 @@ import ctbrec.ui.sites.cam4.Cam4SiteUi; import ctbrec.ui.sites.camsoda.CamsodaSiteUi; import ctbrec.ui.sites.chaturbate.ChaturbateSiteUi; import ctbrec.ui.sites.fc2live.Fc2LiveSiteUi; +import ctbrec.ui.sites.flirt4free.Flirt4FreeSiteUi; import ctbrec.ui.sites.jasmin.LiveJasminSiteUi; import ctbrec.ui.sites.myfreecams.MyFreeCamsSiteUi; import ctbrec.ui.sites.streamate.StreamateSiteUi; @@ -25,6 +27,7 @@ public class SiteUiFactory { private static CamsodaSiteUi camsodaSiteUi; private static ChaturbateSiteUi ctbSiteUi; private static Fc2LiveSiteUi fc2SiteUi; + private static Flirt4FreeSiteUi flirt4FreeSiteUi; private static LiveJasminSiteUi jasminSiteUi; private static MyFreeCamsSiteUi mfcSiteUi; private static StreamateSiteUi streamateSiteUi; @@ -55,6 +58,11 @@ public class SiteUiFactory { fc2SiteUi = new Fc2LiveSiteUi((Fc2Live) site); } return fc2SiteUi; + } else if (site instanceof Flirt4Free) { + if (flirt4FreeSiteUi == null) { + flirt4FreeSiteUi = new Flirt4FreeSiteUi((Flirt4Free) site); + } + return flirt4FreeSiteUi; } else if (site instanceof MyFreeCams) { if (mfcSiteUi == null) { mfcSiteUi = new MyFreeCamsSiteUi((MyFreeCams) site); diff --git a/client/src/main/java/ctbrec/ui/sites/flirt4free/Flirt4FreeSiteUi.java b/client/src/main/java/ctbrec/ui/sites/flirt4free/Flirt4FreeSiteUi.java new file mode 100644 index 00000000..fd012a55 --- /dev/null +++ b/client/src/main/java/ctbrec/ui/sites/flirt4free/Flirt4FreeSiteUi.java @@ -0,0 +1,39 @@ +package ctbrec.ui.sites.flirt4free; + +import java.io.IOException; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import ctbrec.sites.ConfigUI; +import ctbrec.sites.flirt4free.Flirt4Free; +import ctbrec.ui.TabProvider; +import ctbrec.ui.sites.AbstractSiteUi; + +public class Flirt4FreeSiteUi extends AbstractSiteUi { + + private static final transient Logger LOG = LoggerFactory.getLogger(Flirt4FreeSiteUi.class); + private Flirt4Free flirt4Free; + private Flirt4FreeTabProvider tabProvider; + + public Flirt4FreeSiteUi(Flirt4Free flirt4Free) { + this.flirt4Free = flirt4Free; + tabProvider = new Flirt4FreeTabProvider(flirt4Free); + //configUi = new LiveJasminConfigUi(liveJasmin); + } + + @Override + public TabProvider getTabProvider() { + return tabProvider; + } + + @Override + public ConfigUI getConfigUI() { + return null; + } + + @Override + public synchronized boolean login() throws IOException { + return false; + } +} diff --git a/client/src/main/java/ctbrec/ui/sites/flirt4free/Flirt4FreeTabProvider.java b/client/src/main/java/ctbrec/ui/sites/flirt4free/Flirt4FreeTabProvider.java new file mode 100644 index 00000000..d057c10f --- /dev/null +++ b/client/src/main/java/ctbrec/ui/sites/flirt4free/Flirt4FreeTabProvider.java @@ -0,0 +1,42 @@ +package ctbrec.ui.sites.flirt4free; + +import java.util.ArrayList; +import java.util.List; + +import ctbrec.sites.flirt4free.Flirt4Free; +import ctbrec.ui.TabProvider; +import ctbrec.ui.ThumbOverviewTab; +import javafx.scene.Scene; +import javafx.scene.control.Tab; +import javafx.util.Duration; + +public class Flirt4FreeTabProvider extends TabProvider { + + private Flirt4Free flirt4Free; + + public Flirt4FreeTabProvider(Flirt4Free flirt4Free) { + this.flirt4Free = flirt4Free; + } + + @Override + public List getTabs(Scene scene) { + List tabs = new ArrayList<>(); + tabs.add(createTab("Girls", flirt4Free.getBaseUrl() + "/live/girls/")); + tabs.add(createTab("Boys", flirt4Free.getBaseUrl() + "/live/guys/")); + tabs.add(createTab("Trans", flirt4Free.getBaseUrl() + "/live/trans/")); + return tabs; + } + + @Override + public Tab getFollowedTab() { + return null; + } + + private ThumbOverviewTab createTab(String title, String url) { + Flirt4FreeUpdateService s = new Flirt4FreeUpdateService(flirt4Free, url); + ThumbOverviewTab tab = new ThumbOverviewTab(title, s, flirt4Free); + tab.setRecorder(flirt4Free.getRecorder()); + s.setPeriod(Duration.seconds(60)); + return tab; + } +} diff --git a/client/src/main/java/ctbrec/ui/sites/flirt4free/Flirt4FreeUpdateService.java b/client/src/main/java/ctbrec/ui/sites/flirt4free/Flirt4FreeUpdateService.java new file mode 100644 index 00000000..267472c4 --- /dev/null +++ b/client/src/main/java/ctbrec/ui/sites/flirt4free/Flirt4FreeUpdateService.java @@ -0,0 +1,97 @@ +package ctbrec.ui.sites.flirt4free; + +import java.io.IOException; +import java.util.ArrayList; +import java.util.List; +import java.util.regex.Matcher; +import java.util.regex.Pattern; +import java.util.stream.Collectors; + +import org.jsoup.nodes.Element; +import org.jsoup.select.Elements; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import ctbrec.Config; +import ctbrec.Model; +import ctbrec.io.HtmlParser; +import ctbrec.io.HttpException; +import ctbrec.sites.flirt4free.Flirt4Free; +import ctbrec.sites.flirt4free.Flirt4FreeModel; +import ctbrec.ui.PaginatedScheduledService; +import javafx.concurrent.Task; +import okhttp3.Request; +import okhttp3.Response; + +public class Flirt4FreeUpdateService extends PaginatedScheduledService { + + private static final transient Logger LOG = LoggerFactory.getLogger(Flirt4FreeUpdateService.class); + private static final int MODELS_PER_PAGE = 40; + private String url; + private Flirt4Free flirt4Free; + + public Flirt4FreeUpdateService(Flirt4Free flirt4Free, String url) { + this.flirt4Free = flirt4Free; + this.url = url; + } + + @Override + protected Task> createTask() { + return new Task>() { + @Override + public List call() throws IOException { + LOG.debug("Fetching page {}", url); + Request request = new Request.Builder() + .url(url) + .addHeader("User-Agent", Config.getInstance().getSettings().httpUserAgent) + .build(); + try (Response response = flirt4Free.getHttpClient().execute(request)) { + if (response.isSuccessful()) { + List models = new ArrayList<>(); + String body = response.body().string(); + Elements tags = HtmlParser.getTags(body, "div#live_models div[class*=modelNumber]"); + for (Element tag : tags) { + tag.setBaseUri(url); + String modelHtml = tag.html(); + Element modelLink = HtmlParser.getTag(modelHtml, "a[class*=modelLink]"); + modelLink.setBaseUri(url); + String href = modelLink.attr("href"); + String name = href.substring(0, href.length()-1); + name = name.substring(name.indexOf('/', 1) + 1); + Flirt4FreeModel model = (Flirt4FreeModel) flirt4Free.createModel(name); + Element img = HtmlParser.getTag(modelHtml, "a[class*=modelLink] img"); + img.setBaseUri(url); + if(img.hasAttr("data-image-url")) { + model.setPreview(img.absUrl("data-image-url")); + } else { + // background-image: url('https://cdn1.vscdns.com/images/models/samples-640x480/3241715.jpg') + Matcher m = Pattern.compile("background-image: url\\('(.*?)'\\)").matcher(img.attr("style")); + if(m.find()) { + model.setPreview(m.group(1)); + } + } + Element link = HtmlParser.getTag(modelHtml, "a.name"); + model.setDisplayName(link.attr("title")); + model.setUrl(modelLink.absUrl("href")); + model.setDescription(""); + String videoHost = modelLink.attr("data-video-host"); + String modelId = modelLink.attr("data-model-id").substring(5); + model.setId(modelId); + String streamUrl = "https://manifest.vscdns.com/manifest.m3u8.m3u8?key=nil&provider=level3&secure=true&host=" + videoHost + "&model_id=" + modelId; + model.setStreamUrl(streamUrl); + model.setOnlineState(ctbrec.Model.State.ONLINE); + model.setOnline(true); + models.add(model); + } + return models.stream() + .skip((page-1) * MODELS_PER_PAGE) + .limit(MODELS_PER_PAGE) + .collect(Collectors.toList()); + } else { + throw new HttpException(response.code(), response.message()); + } + } + } + }; + } +} diff --git a/common/src/main/java/ctbrec/sites/flirt4free/Flirt4Free.java b/common/src/main/java/ctbrec/sites/flirt4free/Flirt4Free.java new file mode 100644 index 00000000..ea12874d --- /dev/null +++ b/common/src/main/java/ctbrec/sites/flirt4free/Flirt4Free.java @@ -0,0 +1,182 @@ +package ctbrec.sites.flirt4free; + +import java.io.IOException; +import java.net.URLEncoder; +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; +import java.util.regex.Matcher; +import java.util.regex.Pattern; + +import org.json.JSONArray; +import org.json.JSONObject; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import ctbrec.Config; +import ctbrec.Model; +import ctbrec.io.HttpClient; +import ctbrec.io.HttpException; +import ctbrec.sites.AbstractSite; +import ctbrec.sites.camsoda.CamsodaModel; +import okhttp3.Request; +import okhttp3.Response; + +public class Flirt4Free extends AbstractSite { + + private static final transient Logger LOG = LoggerFactory.getLogger(Flirt4Free.class); + public static final String BASE_URI = "https://www.flirt4free.com"; + private HttpClient httpClient; + + @Override + public String getName() { + return "Flirt4Free"; + } + + @Override + public String getBaseUrl() { + return BASE_URI; + } + + @Override + public String getAffiliateLink() { + return BASE_URI; + } + + @Override + public String getBuyTokensLink() { + return BASE_URI; + } + + @Override + public Model createModel(String name) { + Flirt4FreeModel model = new Flirt4FreeModel(); + model.setName(name); + model.setUrl(getBaseUrl() + "/rooms/" + name + '/'); + model.setSite(this); + return model; + } + + @Override + public Double getTokenBalance() throws IOException { + return 0d; + // if (!credentialsAvailable()) { + // throw new IOException("Account settings not available"); + // } + // + // String username = Config.getInstance().getSettings().camsodaUsername; + // String url = BASE_URI + "/api/v1/user/" + username; + // Request request = new Request.Builder().url(url).build(); + // try(Response response = getHttpClient().execute(request)) { + // if(response.isSuccessful()) { + // JSONObject json = new JSONObject(response.body().string()); + // if(json.has("user")) { + // JSONObject user = json.getJSONObject("user"); + // if(user.has("tokens")) { + // return (double) user.getInt("tokens"); + // } + // } + // } else { + // throw new HttpException(response.code(), response.message()); + // } + // } + // throw new RuntimeException("Tokens not found in response"); + } + + @Override + public synchronized boolean login() throws IOException { + return credentialsAvailable() && getHttpClient().login(); + } + + @Override + public HttpClient getHttpClient() { + if(httpClient == null) { + httpClient = new Flirt4FreeHttpClient(); + } + 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 List search(String q) throws IOException, InterruptedException { + String url = BASE_URI + "/api/v1/browse/autocomplete?s=" + URLEncoder.encode(q, "utf-8"); + Request req = new Request.Builder() + .url(url) + .addHeader("User-Agent", Config.getInstance().getSettings().httpUserAgent) + .build(); + try(Response response = getHttpClient().execute(req)) { + if(response.isSuccessful()) { + JSONObject json = new JSONObject(response.body().string()); + if(json.optBoolean("status")) { + List models = new ArrayList<>(); + JSONArray results = json.getJSONArray("results"); + for (int i = 0; i < results.length(); i++) { + JSONObject result = results.getJSONObject(i); + CamsodaModel model = (CamsodaModel) createModel(result.getString("username")); + String thumb = result.getString("thumb"); + 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()); + } + } + } + + @Override + public boolean isSiteForModel(Model m) { + return m instanceof Flirt4FreeModel; + } + + @Override + public boolean credentialsAvailable() { + String username = Config.getInstance().getSettings().camsodaUsername; + return username != null && !username.trim().isEmpty(); + } + + @Override + public Model createModelFromUrl(String url) { + Matcher m = Pattern.compile("https?://(?:www\\.)?camsoda.com/([^/]*?)/?").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/flirt4free/Flirt4FreeHttpClient.java b/common/src/main/java/ctbrec/sites/flirt4free/Flirt4FreeHttpClient.java new file mode 100644 index 00000000..46683b1e --- /dev/null +++ b/common/src/main/java/ctbrec/sites/flirt4free/Flirt4FreeHttpClient.java @@ -0,0 +1,103 @@ +package ctbrec.sites.flirt4free; + +import java.io.IOException; +import java.util.Objects; + +import org.json.JSONObject; +import org.jsoup.nodes.Element; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import ctbrec.Config; +import ctbrec.io.HtmlParser; +import ctbrec.io.HttpClient; +import ctbrec.io.HttpException; +import ctbrec.sites.camsoda.Camsoda; +import okhttp3.FormBody; +import okhttp3.Request; +import okhttp3.Response; + +public class Flirt4FreeHttpClient extends HttpClient { + + private static final transient Logger LOG = LoggerFactory.getLogger(Flirt4FreeHttpClient.class); + private String csrfToken = null; + + public Flirt4FreeHttpClient() { + super("flirt4free"); + } + + @Override + public boolean login() throws IOException { + if(loggedIn) { + return true; + } + + // persisted cookies might let us log in + if(checkLoginSuccess()) { + loggedIn = true; + LOG.debug("Logged in with cookies"); + return true; + } + + String url = Flirt4Free.BASE_URI + "/api/v1/auth/login"; + FormBody body = new FormBody.Builder() + .add("username", Config.getInstance().getSettings().camsodaUsername) + .add("password", Config.getInstance().getSettings().camsodaPassword) + .build(); + Request request = new Request.Builder() + .url(url) + .post(body) + .build(); + try (Response response = execute(request)) { + if (response.isSuccessful()) { + JSONObject resp = new JSONObject(response.body().string()); + if (resp.has("error")) { + String error = resp.getString("error"); + if (Objects.equals(error, "Please confirm that you are not a robot.")) { + // return loginWithDialog(); + throw new IOException("CamSoda requested to solve a captcha. Please try again in a while (maybe 15 min)."); + } else { + throw new IOException(resp.getString("error")); + } + } else { + return true; + } + } else { + throw new HttpException(response.code(), response.message()); + } + } + } + + /** + * check, if the login worked + * @throws IOException + */ + public boolean checkLoginSuccess() throws IOException { + String url = Camsoda.BASE_URI + "/api/v1/user/current"; + Request request = new Request.Builder().url(url).build(); + try(Response response = execute(request)) { + if(response.isSuccessful()) { + JSONObject resp = new JSONObject(response.body().string()); + return resp.optBoolean("status"); + } else { + return false; + } + } + } + + protected String getCsrfToken() throws IOException { + if(csrfToken == null) { + String url = Camsoda.BASE_URI; + Request request = new Request.Builder().url(url).build(); + try(Response response = execute(request)) { + if(response.isSuccessful()) { + Element meta = HtmlParser.getTag(response.body().string(), "meta[name=\"_token\"]"); + csrfToken = meta.attr("content"); + } else { + throw new HttpException(response.code(), response.message()); + } + } + } + return csrfToken; + } +} diff --git a/common/src/main/java/ctbrec/sites/flirt4free/Flirt4FreeModel.java b/common/src/main/java/ctbrec/sites/flirt4free/Flirt4FreeModel.java new file mode 100644 index 00000000..1ccedd70 --- /dev/null +++ b/common/src/main/java/ctbrec/sites/flirt4free/Flirt4FreeModel.java @@ -0,0 +1,294 @@ +package ctbrec.sites.flirt4free; + +import java.io.IOException; +import java.io.InputStream; +import java.net.URLEncoder; +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; +import java.util.Objects; +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.squareup.moshi.JsonReader; +import com.squareup.moshi.JsonWriter; + +import ctbrec.AbstractModel; +import ctbrec.Config; +import ctbrec.Model; +import ctbrec.io.HttpException; +import ctbrec.recorder.download.StreamSource; +import okhttp3.Request; +import okhttp3.Response; +import okhttp3.WebSocket; +import okhttp3.WebSocketListener; + +public class Flirt4FreeModel extends AbstractModel { + + private static final transient Logger LOG = LoggerFactory.getLogger(Flirt4FreeModel.class); + private String id; + private String chatHost; + private String chatPort; + private String chatToken; + private String streamHost; + private String streamUrl; + int[] resolution = new int[2]; + private Object monitor = new Object(); + private boolean online = false; + + @Override + public boolean isOnline(boolean ignoreCache) throws IOException, ExecutionException, InterruptedException { + if(ignoreCache) { + String url = "https://ws.vs3.com/rooms/check-model-status.php?model_name=" + getName(); + Request request = new Request.Builder() + .url(url) + .header("Accept", "*/*") + .header("Accept-Language", "en-US,en;q=0.5") + .header("Referer", getUrl()) + .header("User-Agent", Config.getInstance().getSettings().httpUserAgent) + .header("X-Requested-With", "XMLHttpRequest") + .build(); + try(Response response = getSite().getHttpClient().execute(request)) { + if(response.isSuccessful()) { + JSONObject json = new JSONObject(response.body().string()); + online = Objects.equals(json.optString("status"), "online"); + if(online) { + try { + loadStreamUrl(); + } catch(Exception e) { + online = false; + onlineState = Model.State.OFFLINE; + } + } + } else { + throw new HttpException(response.code(), response.message()); + } + } + } + return online; + } + + private void loadModelInfo() throws IOException { + String url = getSite().getBaseUrl() + "/webservices/chat-room-interface.php?a=login_room&model_id=" + id; + LOG.trace("Loading url {}", url); + Request request = new Request.Builder() + .url(url) + .header("Accept", "*/*") + .header("Accept-Language", "en-US,en;q=0.5") + .header("Referer", getUrl()) + .header("User-Agent", Config.getInstance().getSettings().httpUserAgent) + .header("X-Requested-With", "XMLHttpRequest") + .build(); + try(Response response = getSite().getHttpClient().execute(request)) { + if(response.isSuccessful()) { + JSONObject json = new JSONObject(response.body().string()); + if(json.optString("status").equals("success")) { + JSONObject config = json.getJSONObject("config"); + JSONObject performer = config.getJSONObject("performer"); + setName(performer.optString("name_seo", "n/a")); + setDisplayName(performer.optString("name", "n/a")); + setUrl(getSite().getBaseUrl() + "/rooms/" + getName() + '/'); + JSONObject room = config.getJSONObject("room"); + chatHost = room.getString("host"); + chatPort = room.getString("port_to_be"); + chatToken = json.getString("token_enc"); + } else { + LOG.trace("Loading model info failed. Assuming model {} is offline", getName()); + online = false; + onlineState = Model.State.OFFLINE; + } + } else { + throw new HttpException(response.code(), response.message()); + } + } + } + + @Override + public List getStreamSources() throws IOException, ExecutionException, ParseException, PlaylistException { + return getStreamSources(true); + } + + private List getStreamSources(boolean withWebsocket) throws IOException, ExecutionException, ParseException, PlaylistException { + MasterPlaylist masterPlaylist = null; + try { + if(withWebsocket) { + loadStreamUrl(); + } + masterPlaylist = getMasterPlaylist(); + } catch (InterruptedException e) { + throw new ExecutionException(e); + } + 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 = "https://manifest.vscdns.com/" + playlist.getUri(); + LOG.trace("Media playlist {}", src.mediaPlaylistUrl); + sources.add(src); + } + } + return sources; + } + + public MasterPlaylist getMasterPlaylist() throws IOException, ParseException, PlaylistException, InterruptedException { + LOG.trace("Loading master playlist {}", streamUrl); + Request req = new Request.Builder() + .url(streamUrl) + .header("Accept", "*/*") + .header("Accept-Language", "en-US,en;q=0.5") + .header("Referer", getUrl()) + .header("User-Agent", Config.getInstance().getSettings().httpUserAgent) + .header("X-Requested-With", "XMLHttpRequest") + .build(); + try (Response response = getSite().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(); + return master; + } else { + throw new HttpException(response.code(), response.message()); + } + } + } + + private void loadStreamUrl() throws IOException, InterruptedException { + loadModelInfo(); + Objects.requireNonNull(chatHost, "chatHost is null"); + String h = chatHost.replaceAll("chat", "chat-vip"); + String url = "https://" + h + "/chat?token=" + URLEncoder.encode(chatToken, "utf-8") + "&port_to_be=" + chatPort; + LOG.trace("Opening chat websocket {}", url); + Request req = new Request.Builder() + .url(url) + .header("Accept", "*/*") + .header("Accept-Language", "en-US,en;q=0.5") + .header("Referer", getUrl()) + .header("User-Agent", Config.getInstance().getSettings().httpUserAgent) + .header("X-Requested-With", "XMLHttpRequest") + .build(); + + getSite().getHttpClient().newWebSocket(req, new WebSocketListener() { + @Override + public void onOpen(WebSocket webSocket, Response response) { + LOG.trace("Chat websocket for {} opened", getName()); + } + + @Override + public void onMessage(WebSocket webSocket, String text) { + LOG.trace("Chat wbesocket for {}: {}", getName(), text); + JSONObject json = new JSONObject(text); + if (json.optString("command").equals("8011")) { + JSONObject data = json.getJSONObject("data"); + streamHost = data.getString("stream_host"); + online = true; + try { + resolution[0] = Integer.parseInt(data.getString("stream_width")); + resolution[1] = Integer.parseInt(data.getString("stream_height")); + } catch(Exception e) { + LOG.warn("Couldn't determine stream resolution", e); + } + webSocket.close(1000, ""); + } + } + + @Override + public void onFailure(WebSocket webSocket, Throwable t, Response response) { + LOG.error("Chat websocket for {} failed", getName(), t); + synchronized (monitor) { + monitor.notify(); + } + } + + @Override + public void onClosed(WebSocket webSocket, int code, String reason) { + LOG.trace("Chat websocket for {} closed {} {}", getName(), code, reason); + synchronized (monitor) { + monitor.notify(); + } + } + }); + synchronized (monitor) { + monitor.wait(10_000); + if (streamHost == null) { + throw new RuntimeException("Couldn't determine streaming server for model " + getName()); + } else { + streamUrl = "https://manifest.vscdns.com/manifest.m3u8.m3u8?key=nil&provider=level3&secure=true&host=" + streamHost + "&model_id=" + id; + } + } + } + + @Override + public void invalidateCacheEntries() { + } + + @Override + public void receiveTip(Double tokens) throws IOException { + } + + @Override + public int[] getStreamResolution(boolean failFast) throws ExecutionException { + if(failFast) { + return resolution; + } else { + if(streamUrl != null) { + try { + List streamSources = getStreamSources(false); + Collections.sort(streamSources); + StreamSource best = streamSources.get(streamSources.size()-1); + resolution = new int[] {best.width, best.height}; + } catch (IOException | ParseException | PlaylistException e) { + throw new ExecutionException("Couldn't determine stream resolution", e); + } + } + return resolution; + } + } + + @Override + public boolean follow() throws IOException { + return false; + } + + @Override + public boolean unfollow() throws IOException { + return false; + } + + public void setId(String id) { + this.id = id; + } + + @Override + public void readSiteSpecificData(JsonReader reader) throws IOException { + reader.nextName(); + id = reader.nextString(); + } + + @Override + public void writeSiteSpecificData(JsonWriter writer) throws IOException { + writer.name("id").value(id); + } + + public void setStreamUrl(String streamUrl) { + this.streamUrl = streamUrl; + } + + public void setOnline(boolean b) { + online = b; + } +} diff --git a/server/src/main/java/ctbrec/recorder/server/HttpServer.java b/server/src/main/java/ctbrec/recorder/server/HttpServer.java index d8119f08..d5ef63e4 100644 --- a/server/src/main/java/ctbrec/recorder/server/HttpServer.java +++ b/server/src/main/java/ctbrec/recorder/server/HttpServer.java @@ -33,6 +33,7 @@ import ctbrec.sites.cam4.Cam4; import ctbrec.sites.camsoda.Camsoda; import ctbrec.sites.chaturbate.Chaturbate; import ctbrec.sites.fc2live.Fc2Live; +import ctbrec.sites.flirt4free.Flirt4Free; import ctbrec.sites.jasmin.LiveJasmin; import ctbrec.sites.mfc.MyFreeCams; import ctbrec.sites.streamate.Streamate; @@ -85,6 +86,7 @@ public class HttpServer { sites.add(new Camsoda()); sites.add(new Chaturbate()); sites.add(new Fc2Live()); + sites.add(new Flirt4Free()); sites.add(new LiveJasmin()); sites.add(new MyFreeCams()); sites.add(new Streamate());