From 4f3fd8a677c7340ce137b05e9c3031e560f2b8db Mon Sep 17 00:00:00 2001 From: 0xboobface <0xboobface@gmail.com> Date: Wed, 19 Dec 2018 12:57:44 +0100 Subject: [PATCH 01/38] Add classes for LiveJasmin --- .../java/ctbrec/ui/CamrecApplication.java | 2 + .../main/java/ctbrec/ui/SiteUiFactory.java | 8 + .../ui/sites/jasmin/LiveJasminSiteUi.java | 35 ++++ .../sites/jasmin/LiveJasminTabProvider.java | 36 ++++ .../sites/jasmin/LiveJasminUpdateService.java | 82 +++++++++ .../java/ctbrec/sites/jasmin/LiveJasmin.java | 92 ++++++++++ .../sites/jasmin/LiveJasminHttpClient.java | 18 ++ .../ctbrec/sites/jasmin/LiveJasminModel.java | 170 ++++++++++++++++++ 8 files changed, 443 insertions(+) create mode 100644 client/src/main/java/ctbrec/ui/sites/jasmin/LiveJasminSiteUi.java create mode 100644 client/src/main/java/ctbrec/ui/sites/jasmin/LiveJasminTabProvider.java create mode 100644 client/src/main/java/ctbrec/ui/sites/jasmin/LiveJasminUpdateService.java create mode 100644 common/src/main/java/ctbrec/sites/jasmin/LiveJasmin.java create mode 100644 common/src/main/java/ctbrec/sites/jasmin/LiveJasminHttpClient.java create mode 100644 common/src/main/java/ctbrec/sites/jasmin/LiveJasminModel.java diff --git a/client/src/main/java/ctbrec/ui/CamrecApplication.java b/client/src/main/java/ctbrec/ui/CamrecApplication.java index b2913d98..29ff5a0c 100644 --- a/client/src/main/java/ctbrec/ui/CamrecApplication.java +++ b/client/src/main/java/ctbrec/ui/CamrecApplication.java @@ -37,6 +37,7 @@ import ctbrec.sites.bonga.BongaCams; import ctbrec.sites.cam4.Cam4; import ctbrec.sites.camsoda.Camsoda; import ctbrec.sites.chaturbate.Chaturbate; +import ctbrec.sites.jasmin.LiveJasmin; import ctbrec.sites.mfc.MyFreeCams; import ctbrec.sites.streamate.Streamate; import ctbrec.ui.settings.SettingsTab; @@ -76,6 +77,7 @@ public class CamrecApplication extends Application { sites.add(new Cam4()); sites.add(new Camsoda()); sites.add(new Chaturbate()); + sites.add(new LiveJasmin()); sites.add(new MyFreeCams()); sites.add(new Streamate()); loadConfig(); diff --git a/client/src/main/java/ctbrec/ui/SiteUiFactory.java b/client/src/main/java/ctbrec/ui/SiteUiFactory.java index 7475868c..48728605 100644 --- a/client/src/main/java/ctbrec/ui/SiteUiFactory.java +++ b/client/src/main/java/ctbrec/ui/SiteUiFactory.java @@ -5,12 +5,14 @@ import ctbrec.sites.bonga.BongaCams; import ctbrec.sites.cam4.Cam4; import ctbrec.sites.camsoda.Camsoda; import ctbrec.sites.chaturbate.Chaturbate; +import ctbrec.sites.jasmin.LiveJasmin; 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.jasmin.LiveJasminSiteUi; import ctbrec.ui.sites.myfreecams.MyFreeCamsSiteUi; import ctbrec.ui.sites.streamate.StreamateSiteUi; @@ -20,6 +22,7 @@ public class SiteUiFactory { private static Cam4SiteUi cam4SiteUi; private static CamsodaSiteUi camsodaSiteUi; private static ChaturbateSiteUi ctbSiteUi; + private static LiveJasminSiteUi jasminSiteUi; private static MyFreeCamsSiteUi mfcSiteUi; private static StreamateSiteUi streamateSiteUi; @@ -54,6 +57,11 @@ public class SiteUiFactory { streamateSiteUi = new StreamateSiteUi((Streamate) site); } return streamateSiteUi; + } else if (site instanceof LiveJasmin) { + if (jasminSiteUi == null) { + jasminSiteUi = new LiveJasminSiteUi((LiveJasmin) site); + } + return jasminSiteUi; } throw new RuntimeException("Unknown site " + site.getName()); } diff --git a/client/src/main/java/ctbrec/ui/sites/jasmin/LiveJasminSiteUi.java b/client/src/main/java/ctbrec/ui/sites/jasmin/LiveJasminSiteUi.java new file mode 100644 index 00000000..cf09617d --- /dev/null +++ b/client/src/main/java/ctbrec/ui/sites/jasmin/LiveJasminSiteUi.java @@ -0,0 +1,35 @@ +package ctbrec.ui.sites.jasmin; + +import java.io.IOException; + +import ctbrec.sites.ConfigUI; +import ctbrec.sites.jasmin.LiveJasmin; +import ctbrec.ui.SiteUI; +import ctbrec.ui.TabProvider; + +public class LiveJasminSiteUi implements SiteUI { + + private LiveJasmin liveJasmin; + private LiveJasminTabProvider tabProvider; + + public LiveJasminSiteUi(LiveJasmin liveJasmin) { + this.liveJasmin = liveJasmin; + tabProvider = new LiveJasminTabProvider(liveJasmin); + } + + @Override + public TabProvider getTabProvider() { + return tabProvider; + } + + @Override + public ConfigUI getConfigUI() { + return null; + } + + @Override + public boolean login() throws IOException { + return liveJasmin.login(); + } + +} diff --git a/client/src/main/java/ctbrec/ui/sites/jasmin/LiveJasminTabProvider.java b/client/src/main/java/ctbrec/ui/sites/jasmin/LiveJasminTabProvider.java new file mode 100644 index 00000000..df06011a --- /dev/null +++ b/client/src/main/java/ctbrec/ui/sites/jasmin/LiveJasminTabProvider.java @@ -0,0 +1,36 @@ +package ctbrec.ui.sites.jasmin; + +import java.util.ArrayList; +import java.util.List; + +import ctbrec.sites.jasmin.LiveJasmin; +import ctbrec.ui.TabProvider; +import ctbrec.ui.ThumbOverviewTab; +import javafx.scene.Scene; +import javafx.scene.control.Tab; + +public class LiveJasminTabProvider extends TabProvider { + + private LiveJasmin liveJasmin; + + public LiveJasminTabProvider(LiveJasmin liveJasmin) { + this.liveJasmin = liveJasmin; + } + + @Override + public List getTabs(Scene scene) { + List tabs = new ArrayList<>(); + + long ts = System.currentTimeMillis(); + LiveJasminUpdateService s = new LiveJasminUpdateService(liveJasmin, liveJasmin.getBaseUrl() + "/en/girls/?listPageOrderType=most_popular&_dc=" + ts); + ThumbOverviewTab tab = new ThumbOverviewTab("Girls", s, liveJasmin); + tab.setRecorder(liveJasmin.getRecorder()); + tabs.add(tab); + return tabs; + } + + @Override + public Tab getFollowedTab() { + return null; + } +} diff --git a/client/src/main/java/ctbrec/ui/sites/jasmin/LiveJasminUpdateService.java b/client/src/main/java/ctbrec/ui/sites/jasmin/LiveJasminUpdateService.java new file mode 100644 index 00000000..b4e67a1f --- /dev/null +++ b/client/src/main/java/ctbrec/ui/sites/jasmin/LiveJasminUpdateService.java @@ -0,0 +1,82 @@ +package ctbrec.ui.sites.jasmin; + +import java.io.IOException; +import java.util.ArrayList; +import java.util.List; + +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.HttpException; +import ctbrec.sites.jasmin.LiveJasmin; +import ctbrec.sites.jasmin.LiveJasminModel; +import ctbrec.ui.PaginatedScheduledService; +import javafx.concurrent.Task; +import okhttp3.Request; +import okhttp3.Response; + +public class LiveJasminUpdateService extends PaginatedScheduledService { + + private static final transient Logger LOG = LoggerFactory.getLogger(LiveJasminUpdateService.class); + private String url; + private LiveJasmin liveJasmin; + + public LiveJasminUpdateService(LiveJasmin liveJasmin, String url) { + this.liveJasmin = liveJasmin; + this.url = url; + } + + @Override + protected Task> createTask() { + return new Task>() { + @Override + public List call() throws IOException { + String _url = url + ((page-1) * 36); // TODO find out how to switch pages + LOG.debug("Fetching page {}", url); + 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", liveJasmin.getBaseUrl()) + .addHeader("X-Requested-With", "XMLHttpRequest") + .build(); + try (Response response = liveJasmin.getHttpClient().execute(request)) { + if (response.isSuccessful()) { + String body = response.body().string(); + List models = new ArrayList<>(); + JSONObject json = new JSONObject(body); + //LOG.debug(json.toString(2)); + if(json.optBoolean("success")) { + JSONObject data = json.getJSONObject("data"); + JSONObject content = data.getJSONObject("content"); + JSONArray performers = content.getJSONArray("performers"); + for (int i = 0; i < performers.length(); i++) { + JSONObject m = performers.getJSONObject(i); + String name = m.optString("pid"); + if(name.isEmpty()) { + continue; + } + LiveJasminModel model = (LiveJasminModel) liveJasmin.createModel(name); + model.setId(m.getString("id")); + model.setPreview(m.getString("profilePictureUrl")); + model.setOnline(true); + models.add(model); + } + } else { + LOG.error("Request failed:\n{}", body); + throw new IOException("Response was not successfull"); + } + return models; + } else { + throw new HttpException(response.code(), response.message()); + } + } + } + }; + } +} diff --git a/common/src/main/java/ctbrec/sites/jasmin/LiveJasmin.java b/common/src/main/java/ctbrec/sites/jasmin/LiveJasmin.java new file mode 100644 index 00000000..f9885f4f --- /dev/null +++ b/common/src/main/java/ctbrec/sites/jasmin/LiveJasmin.java @@ -0,0 +1,92 @@ +package ctbrec.sites.jasmin; + +import java.io.IOException; + +import ctbrec.Model; +import ctbrec.io.HttpClient; +import ctbrec.sites.AbstractSite; + +public class LiveJasmin extends AbstractSite { + + public static final String BASE_URL = "https://www.livejasmin.com"; + private HttpClient httpClient; + + @Override + public String getName() { + return "LiveJasmin"; + } + + @Override + public String getBaseUrl() { + return BASE_URL; + } + + @Override + public String getAffiliateLink() { + return "https://awejmp.com/?siteId=jasmin&categoryName=girl&pageName=listpage&performerName=&prm[psid]=0xb00bface&prm[pstool]=205_1&prm[psprogram]=pps&prm[campaign_id]=&subAffId={SUBAFFID}&filters="; + } + + @Override + public Model createModel(String name) { + LiveJasminModel model = new LiveJasminModel(); + model.setName(name); + model.setDescription(""); + model.setSite(this); + return model; + } + + @Override + public Integer getTokenBalance() throws IOException { + return 0; + } + + @Override + public String getBuyTokensLink() { + return getAffiliateLink(); + } + + @Override + public boolean login() throws IOException { + return false; + } + + @Override + public HttpClient getHttpClient() { + if (httpClient == null) { + httpClient = new LiveJasminHttpClient(); + } + 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 isSiteForModel(Model m) { + return m instanceof LiveJasminModel; + } + + @Override + public boolean credentialsAvailable() { + return false; + } + +} diff --git a/common/src/main/java/ctbrec/sites/jasmin/LiveJasminHttpClient.java b/common/src/main/java/ctbrec/sites/jasmin/LiveJasminHttpClient.java new file mode 100644 index 00000000..f853fa12 --- /dev/null +++ b/common/src/main/java/ctbrec/sites/jasmin/LiveJasminHttpClient.java @@ -0,0 +1,18 @@ +package ctbrec.sites.jasmin; + +import java.io.IOException; + +import ctbrec.io.HttpClient; + +public class LiveJasminHttpClient extends HttpClient { + + protected LiveJasminHttpClient() { + super("livejasmin"); + } + + @Override + public boolean login() throws IOException { + return false; + } + +} diff --git a/common/src/main/java/ctbrec/sites/jasmin/LiveJasminModel.java b/common/src/main/java/ctbrec/sites/jasmin/LiveJasminModel.java new file mode 100644 index 00000000..184eb87a --- /dev/null +++ b/common/src/main/java/ctbrec/sites/jasmin/LiveJasminModel.java @@ -0,0 +1,170 @@ +package ctbrec.sites.jasmin; + +import java.io.IOException; +import java.io.InputStream; +import java.util.ArrayList; +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 com.squareup.moshi.JsonReader; +import com.squareup.moshi.JsonWriter; + +import ctbrec.AbstractModel; +import ctbrec.Config; +import ctbrec.io.HttpException; +import ctbrec.recorder.download.StreamSource; +import okhttp3.Request; +import okhttp3.Response; + +public class LiveJasminModel extends AbstractModel { + + private static final transient Logger LOG = LoggerFactory.getLogger(LiveJasminModel.class); + private String id; + private boolean online = false; + + @Override + public boolean isOnline(boolean ignoreCache) throws IOException, ExecutionException, InterruptedException { + if(ignoreCache) { + try { + getMasterPlaylistUrl(); + online = true; + } catch (Exception e) { + online = false; + } + } + return online; + } + + @Override + public List getStreamSources() throws IOException, ExecutionException, ParseException, PlaylistException { + String masterUrl = getMasterPlaylistUrl(); + List streamSources = new ArrayList<>(); + Request req = new Request.Builder().url(masterUrl).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(); + String baseUrl = masterUrl.toString(); + baseUrl = baseUrl.substring(0, baseUrl.lastIndexOf('/') + 1); + streamsource.mediaPlaylistUrl = baseUrl + 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 getMasterPlaylistUrl() throws IOException { + String url = site.getBaseUrl() + "/en/stream/hls/free/" + getName(); + 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", site.getBaseUrl()) + .addHeader("X-Requested-With", "XMLHttpRequest") + .build(); + try (Response response = site.getHttpClient().execute(request)) { + if (response.isSuccessful()) { + String body = response.body().string(); + JSONObject json = new JSONObject(body); + LOG.debug(json.toString(2)); + if(json.optBoolean("success")) { + JSONObject data = json.getJSONObject("data"); + JSONObject hlsStream = data.getJSONObject("hls_stream"); + return hlsStream.getString("url"); + } else { + LOG.error("Request failed:\n{}", body); + throw new IOException("Response was not successfull"); + } + } else { + throw new HttpException(response.code(), response.message()); + } + } + } + + @Override + public void invalidateCacheEntries() { + } + + @Override + public void receiveTip(int tokens) throws IOException { + } + + @Override + public int[] getStreamResolution(boolean failFast) throws ExecutionException { + return new int[2]; + } + + @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; + } + + @Override + public void readSiteSpecificData(JsonReader reader) throws IOException { + reader.nextName(); + id = reader.nextString(); + } + + @Override + public void writeSiteSpecificData(JsonWriter writer) throws IOException { + if(id == null) { + // TODO make sure the id is set + // try { + // loadModelInfo(); + // } catch (IOException e) { + // LOG.error("Couldn't load model ID for {}. This can cause problems with saving / loading the model", getName()); + // } + } + writer.name("id").value(id); + } + + public void setOnline(boolean online) { + this.online = online; + } +} From 2425a9dc6059bf4d4517d7184add45d6b20e3902 Mon Sep 17 00:00:00 2001 From: 0xboobface <0xboobface@gmail.com> Date: Sat, 22 Dec 2018 19:44:45 +0100 Subject: [PATCH 02/38] Add websocket download This is the first version with working downloads for SD and HD. These dowloads only work, if you are logged in. So at the moment you have to set the session ID in the settings to make this work. The session ID can be copied from a valid session in a browser. --- .../ui/sites/jasmin/LiveJasminConfigUi.java | 103 ++++++ .../sites/jasmin/LiveJasminLoginDialog.java | 170 ++++++++++ .../ui/sites/jasmin/LiveJasminSiteUi.java | 93 +++++- .../sites/jasmin/LiveJasminUpdateService.java | 3 +- common/src/main/java/ctbrec/Settings.java | 3 + .../src/main/java/ctbrec/io/HttpClient.java | 22 +- .../java/ctbrec/recorder/LocalRecorder.java | 3 +- .../download/AbstractHlsDownload.java | 22 +- .../recorder/download/MergedHlsDownload.java | 6 - .../java/ctbrec/sites/jasmin/LiveJasmin.java | 3 +- .../sites/jasmin/LiveJasminHttpClient.java | 156 ++++++++- .../jasmin/LiveJasminMergedHlsDownload.java | 48 +++ .../ctbrec/sites/jasmin/LiveJasminModel.java | 110 ++++++- .../jasmin/LiveJasminWebSocketDownload.java | 308 ++++++++++++++++++ .../streamate/StreamateWebsocketClient.java | 7 +- 15 files changed, 1001 insertions(+), 56 deletions(-) create mode 100644 client/src/main/java/ctbrec/ui/sites/jasmin/LiveJasminConfigUi.java create mode 100644 client/src/main/java/ctbrec/ui/sites/jasmin/LiveJasminLoginDialog.java create mode 100644 common/src/main/java/ctbrec/sites/jasmin/LiveJasminMergedHlsDownload.java create mode 100644 common/src/main/java/ctbrec/sites/jasmin/LiveJasminWebSocketDownload.java diff --git a/client/src/main/java/ctbrec/ui/sites/jasmin/LiveJasminConfigUi.java b/client/src/main/java/ctbrec/ui/sites/jasmin/LiveJasminConfigUi.java new file mode 100644 index 00000000..2cf66c02 --- /dev/null +++ b/client/src/main/java/ctbrec/ui/sites/jasmin/LiveJasminConfigUi.java @@ -0,0 +1,103 @@ +package ctbrec.ui.sites.jasmin; + +import ctbrec.Config; +import ctbrec.Settings; +import ctbrec.sites.jasmin.LiveJasmin; +import ctbrec.ui.DesktopIntegration; +import ctbrec.ui.settings.SettingsTab; +import ctbrec.ui.sites.AbstractConfigUI; +import javafx.geometry.Insets; +import javafx.scene.Parent; +import javafx.scene.control.Button; +import javafx.scene.control.CheckBox; +import javafx.scene.control.Label; +import javafx.scene.control.PasswordField; +import javafx.scene.control.TextField; +import javafx.scene.layout.GridPane; +import javafx.scene.layout.Priority; + +public class LiveJasminConfigUi extends AbstractConfigUI { + private LiveJasmin liveJasmin; + + public LiveJasminConfigUi(LiveJasmin liveJasmin) { + this.liveJasmin = liveJasmin; + } + + @Override + public Parent createConfigPanel() { + Settings settings = Config.getInstance().getSettings(); + GridPane layout = SettingsTab.createGridLayout(); + + int row = 0; + Label l = new Label("Active"); + layout.add(l, 0, row); + CheckBox enabled = new CheckBox(); + enabled.setSelected(!settings.disabledSites.contains(liveJasmin.getName())); + enabled.setOnAction((e) -> { + if(enabled.isSelected()) { + settings.disabledSites.remove(liveJasmin.getName()); + } else { + settings.disabledSites.add(liveJasmin.getName()); + } + save(); + }); + GridPane.setMargin(enabled, new Insets(0, 0, 0, SettingsTab.CHECKBOX_MARGIN)); + layout.add(enabled, 1, row++); + + layout.add(new Label("LiveJasmin User"), 0, row); + TextField username = new TextField(Config.getInstance().getSettings().livejasminUsername); + username.textProperty().addListener((ob, o, n) -> { + if(!n.equals(Config.getInstance().getSettings().livejasminUsername)) { + Config.getInstance().getSettings().livejasminUsername = n; + liveJasmin.getHttpClient().logout(); + save(); + } + }); + GridPane.setFillWidth(username, true); + GridPane.setHgrow(username, Priority.ALWAYS); + GridPane.setColumnSpan(username, 2); + layout.add(username, 1, row++); + + layout.add(new Label("LiveJasmin Password"), 0, row); + PasswordField password = new PasswordField(); + password.setText(Config.getInstance().getSettings().livejasminPassword); + password.textProperty().addListener((ob, o, n) -> { + if(!n.equals(Config.getInstance().getSettings().livejasminPassword)) { + Config.getInstance().getSettings().livejasminPassword = n; + liveJasmin.getHttpClient().logout(); + save(); + } + }); + GridPane.setFillWidth(password, true); + GridPane.setHgrow(password, Priority.ALWAYS); + GridPane.setColumnSpan(password, 2); + layout.add(password, 1, row++); + + layout.add(new Label("LiveJasmin Session ID"), 0, row); + TextField sessionId = new TextField(); + sessionId.setText(Config.getInstance().getSettings().livejasminSession); + sessionId.textProperty().addListener((ob, o, n) -> { + if(!n.equals(Config.getInstance().getSettings().livejasminSession)) { + Config.getInstance().getSettings().livejasminSession = n; + save(); + } + }); + GridPane.setFillWidth(sessionId, true); + GridPane.setHgrow(sessionId, Priority.ALWAYS); + GridPane.setColumnSpan(sessionId, 2); + layout.add(sessionId, 1, row++); + + Button createAccount = new Button("Create new Account"); + createAccount.setOnAction((e) -> DesktopIntegration.open(liveJasmin.getAffiliateLink())); + layout.add(createAccount, 1, row++); + GridPane.setColumnSpan(createAccount, 2); + GridPane.setMargin(username, new Insets(0, 0, 0, SettingsTab.CHECKBOX_MARGIN)); + GridPane.setMargin(password, new Insets(0, 0, 0, SettingsTab.CHECKBOX_MARGIN)); + GridPane.setMargin(sessionId, new Insets(0, 0, 0, SettingsTab.CHECKBOX_MARGIN)); + GridPane.setMargin(createAccount, new Insets(0, 0, 0, SettingsTab.CHECKBOX_MARGIN)); + + username.setPrefWidth(300); + + return layout; + } +} diff --git a/client/src/main/java/ctbrec/ui/sites/jasmin/LiveJasminLoginDialog.java b/client/src/main/java/ctbrec/ui/sites/jasmin/LiveJasminLoginDialog.java new file mode 100644 index 00000000..f6071d54 --- /dev/null +++ b/client/src/main/java/ctbrec/ui/sites/jasmin/LiveJasminLoginDialog.java @@ -0,0 +1,170 @@ +package ctbrec.ui.sites.jasmin; + +import java.io.File; +import java.io.IOException; +import java.io.InputStream; +import java.net.CookieHandler; +import java.net.CookieManager; +import java.net.HttpCookie; +import java.net.MalformedURLException; +import java.net.URL; +import java.util.List; +import java.util.Objects; + +import org.json.JSONObject; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import ctbrec.Config; +import ctbrec.OS; +import ctbrec.io.HttpException; +import ctbrec.sites.jasmin.LiveJasmin; +import javafx.concurrent.Worker.State; +import javafx.scene.Scene; +import javafx.scene.control.ProgressIndicator; +import javafx.scene.image.Image; +import javafx.scene.layout.Region; +import javafx.scene.layout.StackPane; +import javafx.scene.web.WebEngine; +import javafx.scene.web.WebView; +import javafx.stage.Stage; +import okhttp3.Request; +import okhttp3.Response; + +public class LiveJasminLoginDialog { + + private static final transient Logger LOG = LoggerFactory.getLogger(LiveJasminLoginDialog.class); + public static final String URL = "https://m.livejasmin.com/en/list"; // #login-modal + private List cookies = null; + private String url; + private Region veil; + private ProgressIndicator p; + private LiveJasmin liveJasmin; + + public LiveJasminLoginDialog(LiveJasmin liveJasmin) throws IOException { + this.liveJasmin = liveJasmin; + Stage stage = new Stage(); + stage.setTitle("LiveJasmin Login"); + InputStream icon = getClass().getResourceAsStream("/icon.png"); + stage.getIcons().add(new Image(icon)); + CookieManager cookieManager = new CookieManager(); + CookieHandler.setDefault(cookieManager); + WebView webView = createWebView(stage); + + veil = new Region(); + veil.setStyle("-fx-background-color: rgba(0, 0, 0, 0.4)"); + p = new ProgressIndicator(); + p.setMaxSize(140, 140); + + StackPane stackPane = new StackPane(); + stackPane.getChildren().addAll(webView, veil, p); + + stage.setScene(new Scene(stackPane, 360, 480)); + stage.showAndWait(); + cookies = cookieManager.getCookieStore().getCookies(); + } + + private WebView createWebView(Stage stage) throws IOException { + + + WebView browser = new WebView(); + WebEngine webEngine = browser.getEngine(); + webEngine.setJavaScriptEnabled(true); + //webEngine.setUserAgent("Mozilla/5.0 (Android 9.0; Mobile; rv:63.0) Gecko/63.0 Firefox/63.0"); + webEngine.setUserAgent("Mozilla/5.0 (Mobile; rv:30.0) Gecko/20100101 Firefox/30.0"); + webEngine.locationProperty().addListener((obs, oldV, newV) -> { + try { + URL _url = new URL(newV); + if (Objects.equals(_url.getPath(), "/")) { + stage.close(); + } + } catch (MalformedURLException e) { + LOG.error("Couldn't parse new url {}", newV, e); + } + url = newV.toString(); + }); + webEngine.getLoadWorker().stateProperty().addListener((observable, oldState, newState) -> { + if (newState == State.SUCCEEDED) { + veil.setVisible(false); + p.setVisible(false); + // try { + // //webEngine.executeScript("$('#eighteen-plus-modal').hide();"); + // //webEngine.executeScript("$('body').html('"+loginForm+"');"); + // //webEngine.executeScript("$('#listpage').append('"+loginForm+"');"); + // // webEngine.executeScript("$('#main-menu-button').click();"); + // // webEngine.executeScript("$('#login-menu').click();"); + // String username = Config.getInstance().getSettings().livejasminUsername; + // if (username != null && !username.trim().isEmpty()) { + // webEngine.executeScript("$('#username').attr('value','" + username + "')"); + // } + // String password = Config.getInstance().getSettings().livejasminPassword; + // if (password != null && !password.trim().isEmpty()) { + // webEngine.executeScript("$('#password').attr('value','" + password + "')"); + // } + // } catch(Exception e) { + // LOG.warn("Couldn't auto fill username and password for LiveJasmin", e); + // } + } else if (newState == State.CANCELLED || newState == State.FAILED) { + veil.setVisible(false); + p.setVisible(false); + } + }); + webEngine.setUserDataDirectory(new File(OS.getConfigDir(), "webengine")); + webEngine.load(URL); + return browser; + } + + private String getLoginForm() throws IOException { + callBaseUrl(); // to get cookies + String url = "https://m.livejasmin.com/en/auth/window/get-login-window?isAjax=1"; + Request request = new Request.Builder() + .url(url) + .addHeader("User-Agent", "Mozilla/5.0 (Android 9.0; Mobile; rv:63.0) Gecko/63.0 Firefox/63.0") + .addHeader("Accept", "application/json, text/javascript, */*") + .addHeader("Accept-Language", "en") + .addHeader("Referer", LiveJasmin.BASE_URL) + .addHeader("X-Requested-With", "XMLHttpRequest") + .build(); + try(Response response = liveJasmin.getHttpClient().execute(request)) { + if(response.isSuccessful()) { + String body = response.body().string(); + JSONObject json = new JSONObject(body); + System.out.println(json.toString(2)); + if(json.optBoolean("success")) { + JSONObject data = json.getJSONObject("data"); + return data.getString("content"); + } else { + throw new IOException("Request was not successful: " + body); + } + } else { + throw new HttpException(response.code(), response.message()); + } + } + } + + private void callBaseUrl() throws IOException { + String url = liveJasmin.getBaseUrl(); + Request request = new Request.Builder() + .url(url) + .header("User-Agent", Config.getInstance().getSettings().httpUserAgent) + .build(); + try(Response response = liveJasmin.getHttpClient().execute(request)) { + if(response.isSuccessful()) { + + } else { + throw new HttpException(response.code(), response.message()); + } + } + } + + public List getCookies() { + for (HttpCookie httpCookie : cookies) { + LOG.debug("Cookie: {}", httpCookie); + } + return cookies; + } + + public String getUrl() { + return url; + } +} diff --git a/client/src/main/java/ctbrec/ui/sites/jasmin/LiveJasminSiteUi.java b/client/src/main/java/ctbrec/ui/sites/jasmin/LiveJasminSiteUi.java index cf09617d..f0115555 100644 --- a/client/src/main/java/ctbrec/ui/sites/jasmin/LiveJasminSiteUi.java +++ b/client/src/main/java/ctbrec/ui/sites/jasmin/LiveJasminSiteUi.java @@ -1,20 +1,39 @@ package ctbrec.ui.sites.jasmin; import java.io.IOException; +import java.net.HttpCookie; +import java.util.ArrayList; +import java.util.List; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; import ctbrec.sites.ConfigUI; import ctbrec.sites.jasmin.LiveJasmin; +import ctbrec.sites.jasmin.LiveJasminHttpClient; import ctbrec.ui.SiteUI; import ctbrec.ui.TabProvider; +import okhttp3.Cookie; +import okhttp3.CookieJar; +import okhttp3.HttpUrl; public class LiveJasminSiteUi implements SiteUI { + private static final transient Logger LOG = LoggerFactory.getLogger(LiveJasminSiteUi.class); private LiveJasmin liveJasmin; private LiveJasminTabProvider tabProvider; + private LiveJasminConfigUi configUi; public LiveJasminSiteUi(LiveJasmin liveJasmin) { this.liveJasmin = liveJasmin; tabProvider = new LiveJasminTabProvider(liveJasmin); + configUi = new LiveJasminConfigUi(liveJasmin); + try { + login(); + } catch (IOException e) { + // TODO Auto-generated catch block + e.printStackTrace(); + } } @Override @@ -24,12 +43,80 @@ public class LiveJasminSiteUi implements SiteUI { @Override public ConfigUI getConfigUI() { - return null; + return configUi; } @Override - public boolean login() throws IOException { - return liveJasmin.login(); + public synchronized boolean login() throws IOException { + boolean automaticLogin = liveJasmin.login(); + return automaticLogin; + // if(automaticLogin) { + // return true; + // } else { + // BlockingQueue queue = new LinkedBlockingQueue<>(); + // + // Runnable showDialog = () -> { + // // login with javafx WebView + // LiveJasminLoginDialog loginDialog; + // try { + // loginDialog = new LiveJasminLoginDialog(liveJasmin); + // // transfer cookies from WebView to OkHttp cookie jar + // transferCookies(loginDialog); + // } catch (IOException e1) { + // LOG.error("Couldn't load login dialog", e1); + // } + // + // try { + // queue.put(true); + // } catch (InterruptedException e) { + // LOG.error("Error while signaling termination", e); + // } + // }; + // + // if(Platform.isFxApplicationThread()) { + // showDialog.run(); + // } else { + // Platform.runLater(showDialog); + // try { + // queue.take(); + // } catch (InterruptedException e) { + // LOG.error("Error while waiting for login dialog to close", e); + // throw new IOException(e); + // } + // } + // + // LiveJasminHttpClient httpClient = (LiveJasminHttpClient)liveJasmin.getHttpClient(); + // boolean loggedIn = httpClient.checkLoginSuccess(); + // if(loggedIn) { + // LOG.info("Logged in."); + // } else { + // LOG.info("Login failed"); + // } + // return loggedIn; + // } } + private void transferCookies(LiveJasminLoginDialog loginDialog) { + LiveJasminHttpClient httpClient = (LiveJasminHttpClient)liveJasmin.getHttpClient(); + CookieJar cookieJar = httpClient.getCookieJar(); + + String[] urls = { + "https://www.livejasmin.com", + "http://www.livejasmin.com", + "https://m.livejasmin.com", + "http://m.livejasmin.com", + "https://livejasmin.com", + "http://livejasmin.com" + }; + + for (String u : urls) { + HttpUrl url = HttpUrl.parse(u); + List cookies = new ArrayList<>(); + for (HttpCookie webViewCookie : loginDialog.getCookies()) { + Cookie cookie = Cookie.parse(url, webViewCookie.toString()); + cookies.add(cookie); + } + cookieJar.saveFromResponse(url, cookies); + } + } } diff --git a/client/src/main/java/ctbrec/ui/sites/jasmin/LiveJasminUpdateService.java b/client/src/main/java/ctbrec/ui/sites/jasmin/LiveJasminUpdateService.java index b4e67a1f..b4d0257f 100644 --- a/client/src/main/java/ctbrec/ui/sites/jasmin/LiveJasminUpdateService.java +++ b/client/src/main/java/ctbrec/ui/sites/jasmin/LiveJasminUpdateService.java @@ -65,11 +65,12 @@ public class LiveJasminUpdateService extends PaginatedScheduledService { model.setId(m.getString("id")); model.setPreview(m.getString("profilePictureUrl")); model.setOnline(true); + model.setOnlineState(ctbrec.Model.State.ONLINE); models.add(model); } } else { LOG.error("Request failed:\n{}", body); - throw new IOException("Response was not successfull"); + throw new IOException("Response was not successful"); } return models; } else { diff --git a/common/src/main/java/ctbrec/Settings.java b/common/src/main/java/ctbrec/Settings.java index c96cf985..4d42f87d 100644 --- a/common/src/main/java/ctbrec/Settings.java +++ b/common/src/main/java/ctbrec/Settings.java @@ -62,6 +62,9 @@ public class Settings { public String camsodaPassword = ""; public String cam4Username = ""; public String cam4Password = ""; + public String livejasminUsername = ""; + public String livejasminPassword = ""; + public String livejasminSession = ""; public String streamateUsername = ""; public String streamatePassword = ""; public String lastDownloadDir = ""; diff --git a/common/src/main/java/ctbrec/io/HttpClient.java b/common/src/main/java/ctbrec/io/HttpClient.java index 5b2d8d9c..5da3d0b9 100644 --- a/common/src/main/java/ctbrec/io/HttpClient.java +++ b/common/src/main/java/ctbrec/io/HttpClient.java @@ -35,10 +35,10 @@ import okhttp3.WebSocketListener; public abstract class HttpClient { private static final transient Logger LOG = LoggerFactory.getLogger(HttpClient.class); - protected OkHttpClient client; + protected OkHttpClient client; protected CookieJarImpl cookieJar = new CookieJarImpl(); - protected boolean loggedIn = false; - protected int loginTries = 0; + protected boolean loggedIn = false; + protected int loginTries = 0; private String name; protected HttpClient(String name) { @@ -93,19 +93,7 @@ public abstract class HttpClient { } } - // public Response execute(Request request) throws IOException { - // Response resp = execute(request, false); - // return resp; - // } - - // public Response execute(Request req, boolean requiresLogin) throws IOException { public Response execute(Request req) throws IOException { - // if(requiresLogin && !loggedIn) { - // loggedIn = login(); - // if(!loggedIn) { - // throw new IOException("403 Unauthorized"); - // } - // } Response resp = client.newCall(req).execute(); return resp; } @@ -222,8 +210,8 @@ public abstract class HttpClient { loggedIn = false; } - public WebSocket newWebSocket(String url, WebSocketListener l) { - Request request = new Request.Builder().url(url).build(); + public WebSocket newWebSocket(Request request, WebSocketListener l) { + //Request request = new Request.Builder().url(url).build(); return client.newWebSocket(request, l); } } diff --git a/common/src/main/java/ctbrec/recorder/LocalRecorder.java b/common/src/main/java/ctbrec/recorder/LocalRecorder.java index 97982b94..3a1e46e9 100644 --- a/common/src/main/java/ctbrec/recorder/LocalRecorder.java +++ b/common/src/main/java/ctbrec/recorder/LocalRecorder.java @@ -193,6 +193,7 @@ public class LocalRecorder implements Recorder { LOG.debug("Starting recording for model {}", model.getName()); Download download = model.createDownload(); + LOG.debug("Downloading with {}", download.getClass().getSimpleName()); recordingProcesses.put(model, download); new Thread() { @Override @@ -461,7 +462,7 @@ public class LocalRecorder implements Recorder { private List listMergedRecordings() { File recordingsDir = new File(config.getSettings().recordingsDir); List possibleRecordings = new LinkedList<>(); - listRecursively(recordingsDir, possibleRecordings, (dir, name) -> name.matches(".*?_\\d{4}-\\d{2}-\\d{2}_\\d{2}-\\d{2}\\.ts")); + listRecursively(recordingsDir, possibleRecordings, (dir, name) -> name.matches(".*?_\\d{4}-\\d{2}-\\d{2}_\\d{2}-\\d{2}\\.(ts|mp4)")); SimpleDateFormat sdf = new SimpleDateFormat(DATE_FORMAT); List recordings = new ArrayList<>(); for (File ts: possibleRecordings) { diff --git a/common/src/main/java/ctbrec/recorder/download/AbstractHlsDownload.java b/common/src/main/java/ctbrec/recorder/download/AbstractHlsDownload.java index b4ab0507..604138c8 100644 --- a/common/src/main/java/ctbrec/recorder/download/AbstractHlsDownload.java +++ b/common/src/main/java/ctbrec/recorder/download/AbstractHlsDownload.java @@ -8,9 +8,12 @@ import java.util.ArrayList; import java.util.Collections; import java.util.Iterator; import java.util.List; +import java.util.concurrent.BlockingQueue; import java.util.concurrent.ExecutionException; import java.util.concurrent.ExecutorService; -import java.util.concurrent.Executors; +import java.util.concurrent.LinkedBlockingQueue; +import java.util.concurrent.ThreadPoolExecutor; +import java.util.concurrent.TimeUnit; import org.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -36,18 +39,19 @@ public abstract class AbstractHlsDownload implements Download { private static final transient Logger LOG = LoggerFactory.getLogger(AbstractHlsDownload.class); - ExecutorService downloadThreadPool = Executors.newFixedThreadPool(5); - HttpClient client; - volatile boolean running = false; - volatile boolean alive = true; - Instant startTime; - Model model; + protected HttpClient client; + protected volatile boolean running = false; + protected volatile boolean alive = true; + protected Instant startTime; + protected Model model; + protected BlockingQueue downloadQueue = new LinkedBlockingQueue<>(50); + protected ExecutorService downloadThreadPool = new ThreadPoolExecutor(5, 5, 2, TimeUnit.MINUTES, downloadQueue); public AbstractHlsDownload(HttpClient client) { this.client = client; } - SegmentPlaylist getNextSegments(String segments) throws IOException, ParseException, PlaylistException { + protected SegmentPlaylist getNextSegments(String segments) throws IOException, ParseException, PlaylistException { URL segmentsUrl = new URL(segments); Request request = new Request.Builder().url(segmentsUrl).addHeader("connection", "keep-alive").build(); try(Response response = client.execute(request)) { @@ -85,7 +89,7 @@ public abstract class AbstractHlsDownload implements Download { } - String getSegmentPlaylistUrl(Model model) throws IOException, ExecutionException, ParseException, PlaylistException { + protected String getSegmentPlaylistUrl(Model model) throws IOException, ExecutionException, ParseException, PlaylistException { LOG.debug("{} stream idx: {}", model.getName(), model.getStreamUrlIndex()); List streamSources = model.getStreamSources(); Collections.sort(streamSources); diff --git a/common/src/main/java/ctbrec/recorder/download/MergedHlsDownload.java b/common/src/main/java/ctbrec/recorder/download/MergedHlsDownload.java index 958fae17..4625cf36 100644 --- a/common/src/main/java/ctbrec/recorder/download/MergedHlsDownload.java +++ b/common/src/main/java/ctbrec/recorder/download/MergedHlsDownload.java @@ -22,13 +22,9 @@ import java.time.ZonedDateTime; import java.util.LinkedList; import java.util.Optional; import java.util.Queue; -import java.util.concurrent.BlockingQueue; import java.util.concurrent.Callable; import java.util.concurrent.ExecutionException; -import java.util.concurrent.ExecutorService; import java.util.concurrent.Future; -import java.util.concurrent.LinkedBlockingQueue; -import java.util.concurrent.ThreadPoolExecutor; import java.util.concurrent.TimeUnit; import org.slf4j.Logger; @@ -63,8 +59,6 @@ public class MergedHlsDownload extends AbstractHlsDownload { private ZonedDateTime splitRecStartTime; private Config config; private File targetFile; - private BlockingQueue downloadQueue = new LinkedBlockingQueue<>(50); - private ExecutorService downloadThreadPool = new ThreadPoolExecutor(5, 5, 2, TimeUnit.MINUTES, downloadQueue); private FileChannel fileChannel = null; private Object downloadFinished = new Object(); diff --git a/common/src/main/java/ctbrec/sites/jasmin/LiveJasmin.java b/common/src/main/java/ctbrec/sites/jasmin/LiveJasmin.java index f9885f4f..fa3e8651 100644 --- a/common/src/main/java/ctbrec/sites/jasmin/LiveJasmin.java +++ b/common/src/main/java/ctbrec/sites/jasmin/LiveJasmin.java @@ -32,6 +32,7 @@ public class LiveJasmin extends AbstractSite { model.setName(name); model.setDescription(""); model.setSite(this); + model.setUrl(getBaseUrl() + "/en/chat/" + name); return model; } @@ -47,7 +48,7 @@ public class LiveJasmin extends AbstractSite { @Override public boolean login() throws IOException { - return false; + return getHttpClient().login(); } @Override diff --git a/common/src/main/java/ctbrec/sites/jasmin/LiveJasminHttpClient.java b/common/src/main/java/ctbrec/sites/jasmin/LiveJasminHttpClient.java index f853fa12..7f09c93b 100644 --- a/common/src/main/java/ctbrec/sites/jasmin/LiveJasminHttpClient.java +++ b/common/src/main/java/ctbrec/sites/jasmin/LiveJasminHttpClient.java @@ -1,18 +1,172 @@ package ctbrec.sites.jasmin; import java.io.IOException; +import java.util.Collections; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import ctbrec.Config; import ctbrec.io.HttpClient; +import okhttp3.Cookie; +import okhttp3.HttpUrl; +import okhttp3.OkHttpClient; +import okhttp3.Request; +import okhttp3.Response; public class LiveJasminHttpClient extends HttpClient { + private static final transient Logger LOG = LoggerFactory.getLogger(LiveJasminHttpClient.class); + protected LiveJasminHttpClient() { super("livejasmin"); } @Override - public boolean login() throws IOException { + public synchronized boolean login() throws IOException { + if (loggedIn) { + return true; + } + + // set session cookie, if session id is available + if(!Config.getInstance().getSettings().livejasminSession.isEmpty()) { + Cookie captchaCookie = new Cookie.Builder() + .domain("livejasmin.com") + .name("session") + .value(Config.getInstance().getSettings().livejasminSession) + .build(); + getCookieJar().saveFromResponse(HttpUrl.parse("https://livejasmin.com"), Collections.singletonList(captchaCookie)); + getCookieJar().saveFromResponse(HttpUrl.parse("https://www.livejasmin.com"), Collections.singletonList(captchaCookie)); + getCookieJar().saveFromResponse(HttpUrl.parse("https://m.livejasmin.com"), Collections.singletonList(captchaCookie)); + } + + + // loadMainPage(); // to get initial cookies + // Cookie captchaCookie = new Cookie.Builder() + // .domain("livejasmin.com") + // .name("captchaRequired") + // .value("0") + // .build(); + // getCookieJar().saveFromResponse(HttpUrl.parse("https://livejasmin.com"), Collections.singletonList(captchaCookie)); + // getCookieJar().saveFromResponse(HttpUrl.parse("https://www.livejasmin.com"), Collections.singletonList(captchaCookie)); + // getCookieJar().saveFromResponse(HttpUrl.parse("https://m.livejasmin.com"), Collections.singletonList(captchaCookie)); + // Map formParams = getLoginFormParameters(); + // getCookieJar().saveFromResponse(HttpUrl.parse("https://livejasmin.com"), Collections.singletonList(captchaCookie)); + // getCookieJar().saveFromResponse(HttpUrl.parse("https://m.livejasmin.com"), Collections.singletonList(captchaCookie)); + // String action = formParams.get("action"); + // formParams.remove("action"); + // Builder formBuilder = new FormBody.Builder(); + // for (Entry param : formParams.entrySet()) { + // formBuilder.add(param.getKey(), param.getValue()); + // } + // formBuilder.add("username", Config.getInstance().getSettings().livejasminUsername); + // formBuilder.add("password", Config.getInstance().getSettings().livejasminPassword); + // FormBody form = formBuilder.build(); + // Buffer b = new Buffer(); + // form.writeTo(b); + // LOG.debug("Form: {}", b.readUtf8()); + // Map> cookies = getCookieJar().getCookies(); + // for (Entry> domain : cookies.entrySet()) { + // LOG.debug("{}", domain.getKey()); + // List cks = domain.getValue(); + // for (Cookie cookie : cks) { + // LOG.debug(" {}", cookie); + // } + // } + // Request request = new Request.Builder() + // .url(LiveJasmin.BASE_URL + action) + // .header("User-Agent", USER_AGENT) + // .header("Accept", "*/*") + // .header("Accept-Language", "en") + // .header("Referer", LiveJasmin.BASE_URL + "/en/girls/") + // .header("X-Requested-With", "XMLHttpRequest") + // .post(form) + // .build(); + // try(Response response = execute(request)) { + // System.out.println("login " + response.code() + " - " + response.message()); + // System.out.println("login " + response.body().string()); + // } + + boolean cookiesWorked = checkLoginSuccess(); + if (cookiesWorked) { + loggedIn = true; + LOG.debug("Logged in with cookies"); + return true; + } + return false; } + // private void loadMainPage() throws IOException { + // Request request = new Request.Builder() + // .url(LiveJasmin.BASE_URL) + // .header("User-Agent", USER_AGENT) + // .build(); + // try(Response response = execute(request)) { + // } + // } + // + // private Map getLoginFormParameters() throws IOException { + // long ts = System.currentTimeMillis(); + // String url = LiveJasmin.BASE_URL + "/en/auth/overlay/get-login-block?_dc="+ts; + // Request request = new Request.Builder() + // .url(url) + // .addHeader("User-Agent", USER_AGENT) + // .addHeader("Accept", "application/json, text/javascript, */*") + // .addHeader("Accept-Language", "en") + // .addHeader("Referer", LiveJasmin.BASE_URL) + // .addHeader("X-Requested-With", "XMLHttpRequest") + // .build(); + // try(Response response = execute(request)) { + // if(response.isSuccessful()) { + // String body = response.body().string(); + // JSONObject json = new JSONObject(body); + // if(json.optBoolean("success")) { + // JSONObject data = json.getJSONObject("data"); + // String content = data.getString("content"); + // Map params = new HashMap<>(); + // Element form = HtmlParser.getTag(content, "form"); + // params.put("action", form.attr("action")); + // Elements hiddenInputs = HtmlParser.getTags(content, "input[type=hidden]"); + // for (Element input : hiddenInputs) { + // String name = input.attr("name"); + // String value = input.attr("value"); + // params.put(name, value); + // } + // params.put("keepmeloggedin", "1"); + // params.put("captcha", ""); + // params.remove("captcha_needed"); + // return params; + // } else { + // throw new IOException("Request was not successful: " + body); + // } + // } else { + // throw new HttpException(response.code(), response.message()); + // } + // } + // } + + public boolean checkLoginSuccess() throws IOException { + OkHttpClient temp = client.newBuilder() + .followRedirects(false) + .followSslRedirects(false) + .build(); + String url = LiveJasmin.BASE_URL + "/en/free/favourite/get-favourite-list"; + 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", LiveJasmin.BASE_URL) + .addHeader("X-Requested-With", "XMLHttpRequest") + .build(); + try(Response response = temp.newCall(request).execute()) { + LOG.debug("Login Check {}: {} - {}", url, response.code(), response.message()); + if(response.isSuccessful()) { + return true; + } else { + return false; + } + } + } } diff --git a/common/src/main/java/ctbrec/sites/jasmin/LiveJasminMergedHlsDownload.java b/common/src/main/java/ctbrec/sites/jasmin/LiveJasminMergedHlsDownload.java new file mode 100644 index 00000000..583a4a31 --- /dev/null +++ b/common/src/main/java/ctbrec/sites/jasmin/LiveJasminMergedHlsDownload.java @@ -0,0 +1,48 @@ +package ctbrec.sites.jasmin; + +import java.io.IOException; +import java.util.concurrent.ExecutionException; +import java.util.concurrent.TimeUnit; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import com.iheartradio.m3u8.ParseException; +import com.iheartradio.m3u8.PlaylistException; + +import ctbrec.io.HttpClient; +import ctbrec.recorder.download.MergedHlsDownload; + +public class LiveJasminMergedHlsDownload extends MergedHlsDownload { + + private static final transient Logger LOG = LoggerFactory.getLogger(LiveJasminMergedHlsDownload.class); + private long lastMasterPlaylistUpdate = 0; + private String segmentUrl; + + public LiveJasminMergedHlsDownload(HttpClient client) { + super(client); + } + + @Override + protected SegmentPlaylist getNextSegments(String segments) throws IOException, ParseException, PlaylistException { + if(this.segmentUrl == null) { + this.segmentUrl = segments; + } + SegmentPlaylist playlist = super.getNextSegments(segmentUrl); + long now = System.currentTimeMillis(); + if( (now - lastMasterPlaylistUpdate) > TimeUnit.SECONDS.toMillis(60)) { + super.downloadThreadPool.submit(this::updatePlaylistUrl); + lastMasterPlaylistUpdate = now; + } + return playlist; + } + + private void updatePlaylistUrl() { + try { + LOG.debug("Updating segment playlist URL for {}", getModel()); + segmentUrl = getSegmentPlaylistUrl(getModel()); + } catch (IOException | ExecutionException | ParseException | PlaylistException e) { + LOG.error("Couldn't update segment playlist url. This might cause a premature download termination", e); + } + } +} diff --git a/common/src/main/java/ctbrec/sites/jasmin/LiveJasminModel.java b/common/src/main/java/ctbrec/sites/jasmin/LiveJasminModel.java index 184eb87a..82eae5b0 100644 --- a/common/src/main/java/ctbrec/sites/jasmin/LiveJasminModel.java +++ b/common/src/main/java/ctbrec/sites/jasmin/LiveJasminModel.java @@ -3,6 +3,7 @@ package ctbrec.sites.jasmin; import java.io.IOException; import java.io.InputStream; import java.util.ArrayList; +import java.util.Arrays; import java.util.List; import java.util.concurrent.ExecutionException; @@ -26,6 +27,8 @@ import com.squareup.moshi.JsonWriter; import ctbrec.AbstractModel; import ctbrec.Config; import ctbrec.io.HttpException; +import ctbrec.recorder.download.Download; +import ctbrec.recorder.download.HlsDownload; import ctbrec.recorder.download.StreamSource; import okhttp3.Request; import okhttp3.Response; @@ -35,20 +38,72 @@ public class LiveJasminModel extends AbstractModel { private static final transient Logger LOG = LoggerFactory.getLogger(LiveJasminModel.class); private String id; private boolean online = false; + private int[] resolution; @Override public boolean isOnline(boolean ignoreCache) throws IOException, ExecutionException, InterruptedException { if(ignoreCache) { - try { - getMasterPlaylistUrl(); - online = true; - } catch (Exception e) { - online = false; - } + loadModelInfo(); } return online; } + protected void loadModelInfo() throws IOException { + String url = "https://m.livejasmin.com/en/chat-html5/" + getName(); + Request req = new Request.Builder().url(url) + .header("User-Agent", "Mozilla/5.0 (iPhone; CPU OS 10_14 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/11.1.1 Mobile/14E304 Safari/605.1.15") + .header("Accept", "application/json,*/*") + .header("Accept-Language", "en") + .header("Referer", getSite().getBaseUrl()) + .header("X-Requested-With", "XMLHttpRequest") + .build(); + try(Response response = getSite().getHttpClient().execute(req)) { + if(response.isSuccessful()) { + String body = response.body().string(); + JSONObject json = new JSONObject(body); + //LOG.debug(json.toString(2)); + if(json.optBoolean("success")) { + JSONObject data = json.getJSONObject("data"); + JSONObject config = data.getJSONObject("config"); + JSONObject chatRoom = config.getJSONObject("chatRoom"); + setId(chatRoom.getString("p_id")); + if(chatRoom.has("profile_picture_url")) { + setPreview(chatRoom.getString("profile_picture_url")); + } + int status = chatRoom.optInt("status", -1); + onlineState = mapStatus(status); + if(chatRoom.optInt("is_on_private", 0) == 1) { + onlineState = State.PRIVATE; + } + resolution = new int[2]; + resolution[0] = config.optInt("streamWidth"); + resolution[1] = config.optInt("streamHeight"); + online = onlineState == State.ONLINE; + LOG.trace("{} - status:{} {} {} {}", getName(), online, onlineState, Arrays.toString(resolution), getUrl()); + } else { + throw new IOException("Response was not successful: " + body); + } + } else { + throw new HttpException(response.code(), response.message()); + } + } + } + + private State mapStatus(int status) { + switch(status) { + case 0: + return State.OFFLINE; + case 1: + return State.ONLINE; + case 2: + case 3: + return State.PRIVATE; + default: + LOG.debug("Unkown state {} {}", status, getUrl()); + return State.UNKNOWN; + } + } + @Override public List getStreamSources() throws IOException, ExecutionException, ParseException, PlaylistException { String masterUrl = getMasterPlaylistUrl(); @@ -86,6 +141,7 @@ public class LiveJasminModel extends AbstractModel { } private String getMasterPlaylistUrl() throws IOException { + loadModelInfo(); String url = site.getBaseUrl() + "/en/stream/hls/free/" + getName(); Request request = new Request.Builder() .url(url) @@ -99,14 +155,12 @@ public class LiveJasminModel extends AbstractModel { if (response.isSuccessful()) { String body = response.body().string(); JSONObject json = new JSONObject(body); - LOG.debug(json.toString(2)); if(json.optBoolean("success")) { JSONObject data = json.getJSONObject("data"); JSONObject hlsStream = data.getJSONObject("hls_stream"); return hlsStream.getString("url"); } else { - LOG.error("Request failed:\n{}", body); - throw new IOException("Response was not successfull"); + throw new IOException("Response was not successful: " + url + "\n" + body); } } else { throw new HttpException(response.code(), response.message()); @@ -124,7 +178,19 @@ public class LiveJasminModel extends AbstractModel { @Override public int[] getStreamResolution(boolean failFast) throws ExecutionException { - return new int[2]; + if(resolution == null) { + if(failFast) { + return new int[2]; + } + try { + loadModelInfo(); + } catch (IOException e) { + throw new ExecutionException(e); + } + return resolution; + } else { + return resolution; + } } @Override @@ -154,12 +220,11 @@ public class LiveJasminModel extends AbstractModel { @Override public void writeSiteSpecificData(JsonWriter writer) throws IOException { if(id == null) { - // TODO make sure the id is set - // try { - // loadModelInfo(); - // } catch (IOException e) { - // LOG.error("Couldn't load model ID for {}. This can cause problems with saving / loading the model", getName()); - // } + try { + loadModelInfo(); + } catch (IOException e) { + LOG.error("Couldn't load model ID for {}. This can cause problems with saving / loading the model", getName()); + } } writer.name("id").value(id); } @@ -167,4 +232,17 @@ public class LiveJasminModel extends AbstractModel { public void setOnline(boolean online) { this.online = online; } + + @Override + public Download createDownload() { + if(Config.getInstance().getSettings().livejasminSession.isEmpty()) { + if(Config.isServerMode()) { + return new HlsDownload(getSite().getHttpClient()); + } else { + return new LiveJasminMergedHlsDownload(getSite().getHttpClient()); + } + } else { + return new LiveJasminWebSocketDownload(getSite().getHttpClient()); + } + } } diff --git a/common/src/main/java/ctbrec/sites/jasmin/LiveJasminWebSocketDownload.java b/common/src/main/java/ctbrec/sites/jasmin/LiveJasminWebSocketDownload.java new file mode 100644 index 00000000..47327987 --- /dev/null +++ b/common/src/main/java/ctbrec/sites/jasmin/LiveJasminWebSocketDownload.java @@ -0,0 +1,308 @@ +package ctbrec.sites.jasmin; + +import java.io.File; +import java.io.FileOutputStream; +import java.io.IOException; +import java.io.UnsupportedEncodingException; +import java.net.URLEncoder; +import java.nio.file.Files; +import java.time.Instant; + +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.recorder.download.Download; +import okhttp3.Request; +import okhttp3.Response; +import okhttp3.WebSocket; +import okhttp3.WebSocketListener; +import okio.ByteString; + +public class LiveJasminWebSocketDownload implements Download { + private static final transient Logger LOG = LoggerFactory.getLogger(LiveJasminWebSocketDownload.class); + + private String applicationId; + private String sessionId; + private String jsm2SessionId; + private String sb_ip; + private String sb_hash; + private String relayHost; + private String streamHost; + private String clientInstanceId = "01234567890123456789012345678901"; // TODO where to get or generate a random id? + private String streamPath = "streams/clonedLiveStream"; + private WebSocket relay; + private WebSocket stream; + + protected boolean connectionClosed; + private volatile boolean isAlive = true; + + private HttpClient client; + private Model model; + private Instant startTime; + private File targetFile; + + public LiveJasminWebSocketDownload(HttpClient client) { + this.client = client; + } + + @Override + public void start(Model model, Config config) throws IOException { + this.model = model; + startTime = Instant.now(); + File _targetFile = config.getFileForRecording(model); + targetFile = new File(_targetFile.getAbsolutePath().replace(".ts", ".mp4")); + + getPerformerDetails(model.getName()); + LOG.debug("appid: {}", applicationId); + LOG.debug("sessionid: {}",sessionId); + LOG.debug("jsm2sessionid: {}",jsm2SessionId); + LOG.debug("sb_ip: {}",sb_ip); + LOG.debug("sb_hash: {}",sb_hash); + LOG.debug("relay host: {}",relayHost); + LOG.debug("stream host: {}",streamHost); + LOG.debug("clientinstanceid {}",clientInstanceId); + + Request request = new Request.Builder() + .url("https://" + relayHost + "/") + .header("Origin", "https://www.livejasmin.com") + .header("User-Agent", "Mozilla/5.0 (X11; Linux x86_64; rv:63.0) Gecko/20100101 Firefox/63.0") + .header("Accept", "text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8") + .header("Accept-Language", "de,en-US;q=0.7,en;q=0.3") + .build(); + relay = client.newWebSocket(request, new WebSocketListener() { + boolean streamSocketStarted = false; + + @Override + public void onOpen(WebSocket webSocket, Response response) { + LOG.trace("relay open {}", model.getName()); + sendToRelay("{\"event\":\"register\",\"applicationId\":\"" + applicationId + + "\",\"connectionData\":{\"jasmin2App\":true,\"isMobileClient\":false,\"platform\":\"desktop\",\"chatID\":\"freechat\"," + + "\"sessionID\":\"" + sessionId + "\"," + "\"jsm2SessionId\":\"" + jsm2SessionId + "\",\"userType\":\"user\"," + "\"performerId\":\"" + + model + + "\",\"clientRevision\":\"\",\"proxyIP\":\"\",\"playerVer\":\"nanoPlayerVersion: 3.10.3 appCodeName: Mozilla appName: Netscape appVersion: 5.0 (X11) platform: Linux x86_64\",\"livejasminTvmember\":false,\"newApplet\":true,\"livefeedtype\":null,\"gravityCookieId\":\"\",\"passparam\":\"\",\"brandID\":\"jasmin\",\"cobrandId\":\"\",\"subbrand\":\"livejasmin\",\"siteName\":\"LiveJasmin\",\"siteUrl\":\"https://www.livejasmin.com\"," + + "\"clientInstanceId\":\"" + clientInstanceId + "\",\"armaVersion\":\"34.10.0\",\"isPassive\":false}}"); + response.close(); + } + + @Override + public void onMessage(WebSocket webSocket, String text) { + LOG.trace("relay <-- {} T{}", model.getName(), text); + JSONObject event = new JSONObject(text); + if (event.optString("event").equals("accept")) { + new Thread(() -> { + sendToRelay("{\"event\":\"connectSharedObject\",\"name\":\"data/chat_so\"}"); + }).start(); + } else if (event.optString("event").equals("updateSharedObject")) { + // TODO + JSONArray list = event.getJSONArray("list"); + for (int i = 0; i < list.length(); i++) { + JSONObject obj = list.getJSONObject(i); + if (obj.optString("name").equals("streamList")) { + LOG.debug(obj.toString(2)); + streamPath = getStreamPath(obj.getJSONObject("newValue")); + } + } + + if (!streamSocketStarted) { + streamSocketStarted = true; + sendToRelay("{\"event\":\"call\",\"funcName\":\"makeActive\",\"data\":[]}"); + new Thread(() -> { + try { + startStreamSocket(); + } catch (Exception e) { + LOG.error("Couldn't start stream websocket", e); + stop(); + } + }).start(); + } + }else if(event.optString("event").equals("call")) { + String func = event.optString("funcName"); + if(func.equals("closeConnection")) { + connectionClosed = true; + //System.out.println(event.get("data")); + stop(); + } + } + } + + private String getStreamPath(JSONObject obj) { + String streamName = "streams/clonedLiveStream"; + int height = 0; + if(obj.has("streams")) { + JSONArray streams = obj.getJSONArray("streams"); + for (int i = 0; i < streams.length(); i++) { + JSONObject stream = streams.getJSONObject(i); + int h = stream.optInt("height"); + if(h > height) { + height = h; + streamName = stream.getString("streamNameWithFolder"); + streamName = "free/" + stream.getString("name"); + } + } + } + return streamName; + } + + @Override + public void onMessage(WebSocket webSocket, ByteString bytes) { + LOG.trace("relay <-- {} B{}", model.getName(), bytes.toString()); + } + + @Override + public void onClosed(WebSocket webSocket, int code, String reason) { + LOG.trace("relay closed {} {} {}", code, reason, model.getName()); + } + + @Override + public void onFailure(WebSocket webSocket, Throwable t, Response response) { + if(!connectionClosed) { + LOG.trace("relay failure {}", model.getName(), t); + if (response != null) { + response.close(); + } + } + } + }); + } + + private void sendToRelay(String msg) { + LOG.trace("relay --> {} {}", model.getName(), msg); + relay.send(msg); + } + + protected void getPerformerDetails(String name) throws IOException { + String url = "https://m.livejasmin.com/en/chat-html5/" + name; + Request req = new Request.Builder() + .url(url) + .header("User-Agent", "Mozilla/5.0 (iPhone; CPU OS 10_14 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/11.1.1 Mobile/14E304 Safari/605.1.15") + .header("Accept", "application/json,*/*") + .header("Accept-Language", "en") + .header("Referer", "https://www.livejasmin.com") + .header("X-Requested-With", "XMLHttpRequest") + .build(); + try (Response response = client.execute(req)) { + if (response.isSuccessful()) { + String body = response.body().string(); + JSONObject json = new JSONObject(body); + // System.out.println(json.toString(2)); + if (json.optBoolean("success")) { + JSONObject data = json.getJSONObject("data"); + JSONObject config = data.getJSONObject("config"); + JSONObject armageddonConfig = config.getJSONObject("armageddonConfig"); + JSONObject chatRoom = config.getJSONObject("chatRoom"); + sessionId = armageddonConfig.getString("sessionid"); + jsm2SessionId = armageddonConfig.getString("jsm2session"); + sb_hash = chatRoom.getString("sb_hash"); + sb_ip = chatRoom.getString("sb_ip"); + applicationId = "memberChat/jasmin" + name + sb_hash; + relayHost = "dss-relay-" + sb_ip.replace('.', '-') + ".dditscdn.com"; + streamHost = "dss-live-" + sb_ip.replace('.', '-') + ".dditscdn.com"; + } else { + throw new IOException("Response was not successful: " + body); + } + } else { + throw new IOException(response.code() + " - " + response.message()); + } + } + } + + private void startStreamSocket() throws UnsupportedEncodingException { + String rtmpUrl = "rtmp://" + sb_ip + "/" + applicationId + "?sessionId-" + sessionId + "|clientInstanceId-" + clientInstanceId; + String url = "https://" + streamHost + "/stream/?url=" + URLEncoder.encode(rtmpUrl, "utf-8"); + url = url += "&stream=" + URLEncoder.encode(streamPath, "utf-8") + "&cid=863621&pid=49247581854"; + LOG.trace(rtmpUrl); + LOG.trace(url); + + Request request = new Request.Builder().url(url).header("Origin", "https://www.livejasmin.com") + .header("User-Agent", "Mozilla/5.0 (X11; Linux x86_64; rv:63.0) Gecko/20100101 Firefox/63.0") + .header("Accept", "text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8").header("Accept-Language", "de,en-US;q=0.7,en;q=0.3") + .build(); + stream = client.newWebSocket(request, new WebSocketListener() { + FileOutputStream fos; + + @Override + public void onOpen(WebSocket webSocket, Response response) { + LOG.trace("stream open {}", model.getName()); + // webSocket.send("{\"event\":\"ping\"}"); + // webSocket.send(""); + response.close(); + try { + Files.createDirectories(targetFile.getParentFile().toPath()); + fos = new FileOutputStream(targetFile); + } catch (IOException e) { + LOG.error("Couldn't create video file", e); + stop(); + } + } + + @Override + public void onMessage(WebSocket webSocket, String text) { + LOG.trace("stream <-- {} T{}", model.getName(), text); + JSONObject event = new JSONObject(text); + if(event.optString("eventType").equals("onRandomAccessPoint")) { + // send ping + sendToRelay("{\"event\":\"ping\"}"); + } + } + + @Override + public void onMessage(WebSocket webSocket, ByteString bytes) { + //System.out.println("stream <-- B" + bytes.toString()); + try { + fos.write(bytes.toByteArray()); + } catch (IOException e) { + LOG.error("Couldn't write video chunk to file", e); + stop(); + } + } + + @Override + public void onClosed(WebSocket webSocket, int code, String reason) { + LOG.trace("stream closed {} {} {}", code, reason, model.getName()); + } + + @Override + public void onFailure(WebSocket webSocket, Throwable t, Response response) { + if(!connectionClosed) { + LOG.trace("stream failure {}", model.getName(), t); + if (response != null) { + response.close(); + } + } + } + }); + } + + @Override + public void stop() { + connectionClosed = true; + stream.close(1000, ""); + relay.close(1000, ""); + isAlive = false; + } + + @Override + public boolean isAlive() { + return isAlive; + } + + @Override + public File getTarget() { + return targetFile; + } + + @Override + public Model getModel() { + return model; + } + + @Override + public Instant getStartTime() { + return startTime; + } +} diff --git a/common/src/main/java/ctbrec/sites/streamate/StreamateWebsocketClient.java b/common/src/main/java/ctbrec/sites/streamate/StreamateWebsocketClient.java index d13cde56..b7bd9b34 100644 --- a/common/src/main/java/ctbrec/sites/streamate/StreamateWebsocketClient.java +++ b/common/src/main/java/ctbrec/sites/streamate/StreamateWebsocketClient.java @@ -6,7 +6,9 @@ import java.util.regex.Pattern; import org.slf4j.Logger; import org.slf4j.LoggerFactory; +import ctbrec.Config; import ctbrec.io.HttpClient; +import okhttp3.Request; import okhttp3.Response; import okhttp3.WebSocket; import okhttp3.WebSocketListener; @@ -27,7 +29,10 @@ public class StreamateWebsocketClient { public String getRoomId() throws InterruptedException { LOG.debug("Connecting to {}", url); Object monitor = new Object(); - client.newWebSocket(url, new WebSocketListener() { + Request request = new Request.Builder() + .header("User-Agent", Config.getInstance().getSettings().httpUserAgent) + .build(); + client.newWebSocket(request, new WebSocketListener() { @Override public void onOpen(WebSocket webSocket, Response response) { response.close(); From c364250440fbc3c413b6e4490d614bb55cfc3274 Mon Sep 17 00:00:00 2001 From: 0xboobface <0xboobface@gmail.com> Date: Sat, 22 Dec 2018 20:53:41 +0100 Subject: [PATCH 03/38] Add follow / unfollow for livejasmin --- .../sites/jasmin/LiveJasminFollowedTab.java | 76 ++++++++++++++++ .../LiveJasminFollowedUpdateService.java | 91 +++++++++++++++++++ .../sites/jasmin/LiveJasminTabProvider.java | 8 +- .../sites/jasmin/LiveJasminUpdateService.java | 5 +- .../java/ctbrec/sites/jasmin/LiveJasmin.java | 5 +- .../sites/jasmin/LiveJasminHttpClient.java | 10 ++ .../ctbrec/sites/jasmin/LiveJasminModel.java | 45 ++++++++- 7 files changed, 230 insertions(+), 10 deletions(-) create mode 100644 client/src/main/java/ctbrec/ui/sites/jasmin/LiveJasminFollowedTab.java create mode 100644 client/src/main/java/ctbrec/ui/sites/jasmin/LiveJasminFollowedUpdateService.java diff --git a/client/src/main/java/ctbrec/ui/sites/jasmin/LiveJasminFollowedTab.java b/client/src/main/java/ctbrec/ui/sites/jasmin/LiveJasminFollowedTab.java new file mode 100644 index 00000000..b04ac265 --- /dev/null +++ b/client/src/main/java/ctbrec/ui/sites/jasmin/LiveJasminFollowedTab.java @@ -0,0 +1,76 @@ +package ctbrec.ui.sites.jasmin; + +import ctbrec.sites.jasmin.LiveJasmin; +import ctbrec.ui.FollowedTab; +import ctbrec.ui.ThumbOverviewTab; +import javafx.concurrent.WorkerStateEvent; +import javafx.geometry.Insets; +import javafx.scene.Scene; +import javafx.scene.control.Label; +import javafx.scene.control.RadioButton; +import javafx.scene.control.ToggleGroup; +import javafx.scene.input.KeyCode; +import javafx.scene.input.KeyEvent; +import javafx.scene.layout.HBox; + +public class LiveJasminFollowedTab extends ThumbOverviewTab implements FollowedTab { + private Label status; + + public LiveJasminFollowedTab(LiveJasmin liveJasmin) { + super("Followed", new LiveJasminFollowedUpdateService(liveJasmin), liveJasmin); + status = new Label("Logging in..."); + grid.getChildren().add(status); + } + + @Override + protected void createGui() { + super.createGui(); + addOnlineOfflineSelector(); + } + + private void addOnlineOfflineSelector() { + ToggleGroup group = new ToggleGroup(); + RadioButton online = new RadioButton("online"); + online.setToggleGroup(group); + RadioButton offline = new RadioButton("offline"); + offline.setToggleGroup(group); + pagination.getChildren().add(online); + pagination.getChildren().add(offline); + HBox.setMargin(online, new Insets(5,5,5,40)); + HBox.setMargin(offline, new Insets(5,5,5,5)); + online.setSelected(true); + group.selectedToggleProperty().addListener((e) -> { + ((LiveJasminFollowedUpdateService)updateService).setShowOnline(online.isSelected()); + queue.clear(); + updateService.restart(); + }); + } + + @Override + protected void onSuccess() { + grid.getChildren().remove(status); + super.onSuccess(); + } + + @Override + protected void onFail(WorkerStateEvent event) { + status.setText("Login failed"); + super.onFail(event); + } + + @Override + public void selected() { + status.setText("Logging in..."); + super.selected(); + } + + public void setScene(Scene scene) { + scene.addEventFilter(KeyEvent.KEY_PRESSED, event -> { + if(this.isSelected()) { + if(event.getCode() == KeyCode.DELETE) { + follow(selectedThumbCells, false); + } + } + }); + } +} diff --git a/client/src/main/java/ctbrec/ui/sites/jasmin/LiveJasminFollowedUpdateService.java b/client/src/main/java/ctbrec/ui/sites/jasmin/LiveJasminFollowedUpdateService.java new file mode 100644 index 00000000..90ada272 --- /dev/null +++ b/client/src/main/java/ctbrec/ui/sites/jasmin/LiveJasminFollowedUpdateService.java @@ -0,0 +1,91 @@ +package ctbrec.ui.sites.jasmin; + +import java.io.IOException; +import java.util.ArrayList; +import java.util.List; + +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.HttpException; +import ctbrec.sites.jasmin.LiveJasmin; +import ctbrec.sites.jasmin.LiveJasminModel; +import ctbrec.ui.PaginatedScheduledService; +import javafx.concurrent.Task; +import okhttp3.Request; +import okhttp3.Response; + +public class LiveJasminFollowedUpdateService extends PaginatedScheduledService { + + private static final transient Logger LOG = LoggerFactory.getLogger(LiveJasminFollowedUpdateService.class); + private LiveJasmin liveJasmin; + private String url; + private boolean showOnline = true; + + public LiveJasminFollowedUpdateService(LiveJasmin liveJasmin) { + this.liveJasmin = liveJasmin; + long ts = System.currentTimeMillis(); + this.url = liveJasmin.getBaseUrl() + "/en/free/favourite/get-favourite-list?_dc=" + ts; + } + + @Override + protected Task> createTask() { + return new Task>() { + @Override + public List call() throws IOException { + //String _url = url + ((page-1) * 36); // TODO find out how to switch pages + //LOG.debug("Fetching page {}", url); + Request request = new Request.Builder() + .url(url) + .header("User-Agent", Config.getInstance().getSettings().httpUserAgent) + .header("Accept", "*/*") + .header("Accept-Language", "en") + .header("Referer", liveJasmin.getBaseUrl() + "/en/free/favorite") + .header("X-Requested-With", "XMLHttpRequest") + .build(); + try (Response response = liveJasmin.getHttpClient().execute(request)) { + if (response.isSuccessful()) { + String body = response.body().string(); + List models = new ArrayList<>(); + JSONObject json = new JSONObject(body); + LOG.debug(json.toString(2)); + if(json.has("success")) { + JSONObject data = json.getJSONObject("data"); + JSONArray performers = data.getJSONArray("performers"); + for (int i = 0; i < performers.length(); i++) { + JSONObject m = performers.getJSONObject(i); + String name = m.optString("pid"); + if(name.isEmpty()) { + continue; + } + LiveJasminModel model = (LiveJasminModel) liveJasmin.createModel(name); + model.setId(m.getString("id")); + model.setPreview(m.getString("profilePictureUrl")); + Model.State onlineState = LiveJasminModel.mapStatus(m.getInt("status")); + boolean online = onlineState == Model.State.ONLINE; + model.setOnlineState(onlineState); + if(online == showOnline) { + models.add(model); + } + } + } else { + LOG.error("Request failed:\n{}", body); + throw new IOException("Response was not successful"); + } + return models; + } else { + throw new HttpException(response.code(), response.message()); + } + } + } + }; + } + + public void setShowOnline(boolean showOnline) { + this.showOnline = showOnline; + } +} diff --git a/client/src/main/java/ctbrec/ui/sites/jasmin/LiveJasminTabProvider.java b/client/src/main/java/ctbrec/ui/sites/jasmin/LiveJasminTabProvider.java index df06011a..f58f976a 100644 --- a/client/src/main/java/ctbrec/ui/sites/jasmin/LiveJasminTabProvider.java +++ b/client/src/main/java/ctbrec/ui/sites/jasmin/LiveJasminTabProvider.java @@ -12,6 +12,7 @@ import javafx.scene.control.Tab; public class LiveJasminTabProvider extends TabProvider { private LiveJasmin liveJasmin; + private LiveJasminFollowedTab followedTab; public LiveJasminTabProvider(LiveJasmin liveJasmin) { this.liveJasmin = liveJasmin; @@ -26,11 +27,16 @@ public class LiveJasminTabProvider extends TabProvider { ThumbOverviewTab tab = new ThumbOverviewTab("Girls", s, liveJasmin); tab.setRecorder(liveJasmin.getRecorder()); tabs.add(tab); + + followedTab = new LiveJasminFollowedTab(liveJasmin); + followedTab.setRecorder(liveJasmin.getRecorder()); + tabs.add(followedTab); + return tabs; } @Override public Tab getFollowedTab() { - return null; + return followedTab; } } diff --git a/client/src/main/java/ctbrec/ui/sites/jasmin/LiveJasminUpdateService.java b/client/src/main/java/ctbrec/ui/sites/jasmin/LiveJasminUpdateService.java index b4d0257f..6714cce3 100644 --- a/client/src/main/java/ctbrec/ui/sites/jasmin/LiveJasminUpdateService.java +++ b/client/src/main/java/ctbrec/ui/sites/jasmin/LiveJasminUpdateService.java @@ -35,7 +35,7 @@ public class LiveJasminUpdateService extends PaginatedScheduledService { return new Task>() { @Override public List call() throws IOException { - String _url = url + ((page-1) * 36); // TODO find out how to switch pages + //String _url = url + ((page-1) * 36); // TODO find out how to switch pages LOG.debug("Fetching page {}", url); Request request = new Request.Builder() .url(url) @@ -64,8 +64,7 @@ public class LiveJasminUpdateService extends PaginatedScheduledService { LiveJasminModel model = (LiveJasminModel) liveJasmin.createModel(name); model.setId(m.getString("id")); model.setPreview(m.getString("profilePictureUrl")); - model.setOnline(true); - model.setOnlineState(ctbrec.Model.State.ONLINE); + model.setOnlineState(LiveJasminModel.mapStatus(m.optInt("status"))); models.add(model); } } else { diff --git a/common/src/main/java/ctbrec/sites/jasmin/LiveJasmin.java b/common/src/main/java/ctbrec/sites/jasmin/LiveJasmin.java index fa3e8651..4f0dd311 100644 --- a/common/src/main/java/ctbrec/sites/jasmin/LiveJasmin.java +++ b/common/src/main/java/ctbrec/sites/jasmin/LiveJasmin.java @@ -2,6 +2,7 @@ package ctbrec.sites.jasmin; import java.io.IOException; +import ctbrec.Config; import ctbrec.Model; import ctbrec.io.HttpClient; import ctbrec.sites.AbstractSite; @@ -77,7 +78,7 @@ public class LiveJasmin extends AbstractSite { @Override public boolean supportsFollow() { - return false; + return true; } @Override @@ -87,7 +88,7 @@ public class LiveJasmin extends AbstractSite { @Override public boolean credentialsAvailable() { - return false; + return !Config.getInstance().getSettings().livejasminSession.isEmpty(); } } diff --git a/common/src/main/java/ctbrec/sites/jasmin/LiveJasminHttpClient.java b/common/src/main/java/ctbrec/sites/jasmin/LiveJasminHttpClient.java index 7f09c93b..b979ee53 100644 --- a/common/src/main/java/ctbrec/sites/jasmin/LiveJasminHttpClient.java +++ b/common/src/main/java/ctbrec/sites/jasmin/LiveJasminHttpClient.java @@ -2,6 +2,7 @@ package ctbrec.sites.jasmin; import java.io.IOException; import java.util.Collections; +import java.util.NoSuchElementException; import org.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -169,4 +170,13 @@ public class LiveJasminHttpClient extends HttpClient { } } } + + public String getSessionId() { + Cookie sessionCookie = getCookieJar().getCookie(HttpUrl.parse("https://www.livejasmin.com"), "session"); + if(sessionCookie != null) { + return sessionCookie.value(); + } else { + throw new NoSuchElementException("session cookie not found"); + } + } } diff --git a/common/src/main/java/ctbrec/sites/jasmin/LiveJasminModel.java b/common/src/main/java/ctbrec/sites/jasmin/LiveJasminModel.java index 82eae5b0..64008189 100644 --- a/common/src/main/java/ctbrec/sites/jasmin/LiveJasminModel.java +++ b/common/src/main/java/ctbrec/sites/jasmin/LiveJasminModel.java @@ -89,7 +89,7 @@ public class LiveJasminModel extends AbstractModel { } } - private State mapStatus(int status) { + public static State mapStatus(int status) { switch(status) { case 0: return State.OFFLINE; @@ -99,11 +99,17 @@ public class LiveJasminModel extends AbstractModel { case 3: return State.PRIVATE; default: - LOG.debug("Unkown state {} {}", status, getUrl()); + LOG.debug("Unkown state {}", status); return State.UNKNOWN; } } + @Override + public void setOnlineState(State status) { + super.setOnlineState(status); + online = status == State.ONLINE; + } + @Override public List getStreamSources() throws IOException, ExecutionException, ParseException, PlaylistException { String masterUrl = getMasterPlaylistUrl(); @@ -195,12 +201,43 @@ public class LiveJasminModel extends AbstractModel { @Override public boolean follow() throws IOException { - return false; + return follow(true); } @Override public boolean unfollow() throws IOException { - return false; + return follow(false); + } + + private boolean follow(boolean follow) throws IOException { + if (id == null) { + loadModelInfo(); + } + + String sessionId = ((LiveJasminHttpClient) site.getHttpClient()).getSessionId(); + String url; + if (follow) { + url = site.getBaseUrl() + "/en/free/favourite/add-favourite?session=" + sessionId + "&performerId=" + id; + } else { + url = site.getBaseUrl() + "/en/free/favourite/delete-favourite?session=" + sessionId + "&performerId=" + id; + } + Request request = new Request.Builder() + .url(url) + .addHeader("User-Agent", Config.getInstance().getSettings().httpUserAgent) + .addHeader("Accept", "*/*") + .addHeader("Accept-Language", "en") + .addHeader("Referer", getUrl()) + .addHeader("X-Requested-With", "XMLHttpRequest") + .build(); + try (Response response = site.getHttpClient().execute(request)) { + if (response.isSuccessful()) { + String body = response.body().string(); + JSONObject json = new JSONObject(body); + return json.optString("status").equalsIgnoreCase("ok"); + } else { + throw new HttpException(response.code(), response.message()); + } + } } public String getId() { From b8d9f4bc19c63ff0cfbec13be07661335cc553c1 Mon Sep 17 00:00:00 2001 From: 0xboobface <0xboobface@gmail.com> Date: Sat, 22 Dec 2018 22:33:01 +0100 Subject: [PATCH 04/38] Implement search for livejasmin --- .../java/ctbrec/sites/jasmin/LiveJasmin.java | 62 +++++++++++++++++++ 1 file changed, 62 insertions(+) diff --git a/common/src/main/java/ctbrec/sites/jasmin/LiveJasmin.java b/common/src/main/java/ctbrec/sites/jasmin/LiveJasmin.java index 4f0dd311..cbbb13aa 100644 --- a/common/src/main/java/ctbrec/sites/jasmin/LiveJasmin.java +++ b/common/src/main/java/ctbrec/sites/jasmin/LiveJasmin.java @@ -1,11 +1,24 @@ package ctbrec.sites.jasmin; import java.io.IOException; +import java.net.URLEncoder; +import java.util.ArrayList; +import java.util.List; +import java.util.regex.Matcher; +import java.util.regex.Pattern; + +import org.json.JSONObject; +import org.jsoup.nodes.Element; +import org.jsoup.select.Elements; import ctbrec.Config; import ctbrec.Model; +import ctbrec.io.HtmlParser; import ctbrec.io.HttpClient; +import ctbrec.io.HttpException; import ctbrec.sites.AbstractSite; +import okhttp3.Request; +import okhttp3.Response; public class LiveJasmin extends AbstractSite { @@ -81,6 +94,55 @@ public class LiveJasmin extends AbstractSite { return true; } + @Override + public boolean supportsSearch() { + return true; + } + + @Override + public List search(String q) throws IOException, InterruptedException { + String query = URLEncoder.encode(q, "utf-8"); + long ts = System.currentTimeMillis(); + String url = getBaseUrl() + "/en/auto-suggest-search/auto-suggest?category=girls&searchText=" + query + "&_dc=" + ts + "&appletType=html5"; + Request request = new Request.Builder() + .url(url) + .addHeader("User-Agent", Config.getInstance().getSettings().httpUserAgent) + .addHeader("Accept", "*/*") + .addHeader("Accept-Language", "en") + .addHeader("Referer", getBaseUrl()) + .addHeader("X-Requested-With", "XMLHttpRequest") + .build(); + try (Response response = getHttpClient().execute(request)) { + if (response.isSuccessful()) { + String body = response.body().string(); + JSONObject json = new JSONObject(body); + if(json.optBoolean("success")) { + List models = new ArrayList<>(); + JSONObject data = json.getJSONObject("data"); + String html = data.getString("content"); + Elements items = HtmlParser.getTags(html, "li.name"); + for (Element item : items) { + String itemHtml = item.html(); + Element link = HtmlParser.getTag(itemHtml, "a"); + LiveJasminModel model = (LiveJasminModel) createModel(link.attr("title")); + Element pic = HtmlParser.getTag(itemHtml, "span.pic i"); + String style = pic.attr("style"); + Matcher m = Pattern.compile("url\\('(.*?)'\\)").matcher(style); + if(m.find()) { + model.setPreview(m.group(1)); + } + models.add(model); + } + return models; + } else { + throw new IOException("Response was not successful: " + url + "\n" + body); + } + } else { + throw new HttpException(response.code(), response.message()); + } + } + } + @Override public boolean isSiteForModel(Model m) { return m instanceof LiveJasminModel; From 32099eec4625e9e40027f086ef0b049ba24b257b Mon Sep 17 00:00:00 2001 From: 0xboobface <0xboobface@gmail.com> Date: Sat, 22 Dec 2018 22:33:16 +0100 Subject: [PATCH 05/38] Add a few more pages --- .../ui/sites/jasmin/LiveJasminTabProvider.java | 16 +++++++++++----- 1 file changed, 11 insertions(+), 5 deletions(-) diff --git a/client/src/main/java/ctbrec/ui/sites/jasmin/LiveJasminTabProvider.java b/client/src/main/java/ctbrec/ui/sites/jasmin/LiveJasminTabProvider.java index f58f976a..998c2ba7 100644 --- a/client/src/main/java/ctbrec/ui/sites/jasmin/LiveJasminTabProvider.java +++ b/client/src/main/java/ctbrec/ui/sites/jasmin/LiveJasminTabProvider.java @@ -22,11 +22,10 @@ public class LiveJasminTabProvider extends TabProvider { public List getTabs(Scene scene) { List tabs = new ArrayList<>(); - long ts = System.currentTimeMillis(); - LiveJasminUpdateService s = new LiveJasminUpdateService(liveJasmin, liveJasmin.getBaseUrl() + "/en/girls/?listPageOrderType=most_popular&_dc=" + ts); - ThumbOverviewTab tab = new ThumbOverviewTab("Girls", s, liveJasmin); - tab.setRecorder(liveJasmin.getRecorder()); - tabs.add(tab); + tabs.add(createTab("Girls", liveJasmin.getBaseUrl() + "/en/girls/?listPageOrderType=most_popular")); + tabs.add(createTab("Girls HD", liveJasmin.getBaseUrl() + "/en/girls/hd/?listPageOrderType=most_popular")); + tabs.add(createTab("Boys", liveJasmin.getBaseUrl() + "/en/boys/?listPageOrderType=most_popular")); + tabs.add(createTab("Boys HD", liveJasmin.getBaseUrl() + "/en/boys/hd/?listPageOrderType=most_popular")); followedTab = new LiveJasminFollowedTab(liveJasmin); followedTab.setRecorder(liveJasmin.getRecorder()); @@ -39,4 +38,11 @@ public class LiveJasminTabProvider extends TabProvider { public Tab getFollowedTab() { return followedTab; } + + private ThumbOverviewTab createTab(String title, String url) { + LiveJasminUpdateService s = new LiveJasminUpdateService(liveJasmin, url); + ThumbOverviewTab tab = new ThumbOverviewTab(title, s, liveJasmin); + tab.setRecorder(liveJasmin.getRecorder()); + return tab; + } } From 9b764ec8edb7b1c7766766385e222d267cbf253a Mon Sep 17 00:00:00 2001 From: 0xboobface <0xboobface@gmail.com> Date: Sat, 22 Dec 2018 22:33:24 +0100 Subject: [PATCH 06/38] Sort by popularity --- .../ui/sites/jasmin/LiveJasminUpdateService.java | 14 ++++++++++++++ 1 file changed, 14 insertions(+) diff --git a/client/src/main/java/ctbrec/ui/sites/jasmin/LiveJasminUpdateService.java b/client/src/main/java/ctbrec/ui/sites/jasmin/LiveJasminUpdateService.java index 6714cce3..e810fcab 100644 --- a/client/src/main/java/ctbrec/ui/sites/jasmin/LiveJasminUpdateService.java +++ b/client/src/main/java/ctbrec/ui/sites/jasmin/LiveJasminUpdateService.java @@ -2,6 +2,7 @@ package ctbrec.ui.sites.jasmin; import java.io.IOException; import java.util.ArrayList; +import java.util.Collections; import java.util.List; import org.json.JSONArray; @@ -11,11 +12,14 @@ import org.slf4j.LoggerFactory; import ctbrec.Config; import ctbrec.Model; +import ctbrec.io.CookieJarImpl; import ctbrec.io.HttpException; import ctbrec.sites.jasmin.LiveJasmin; import ctbrec.sites.jasmin.LiveJasminModel; import ctbrec.ui.PaginatedScheduledService; import javafx.concurrent.Task; +import okhttp3.Cookie; +import okhttp3.HttpUrl; import okhttp3.Request; import okhttp3.Response; @@ -36,6 +40,16 @@ public class LiveJasminUpdateService extends PaginatedScheduledService { @Override public List call() throws IOException { //String _url = url + ((page-1) * 36); // TODO find out how to switch pages + + // sort by popularity + CookieJarImpl cookieJar = liveJasmin.getHttpClient().getCookieJar(); + Cookie sortCookie = new Cookie.Builder() + .domain("livejasmin.com") + .name("listPageOrderType") + .value("most_popular") + .build(); + cookieJar.saveFromResponse(HttpUrl.parse("https://www.livejasmin.com"), Collections.singletonList(sortCookie)); + LOG.debug("Fetching page {}", url); Request request = new Request.Builder() .url(url) From e66b75848f84403f357554d1afd9d28cee1b9a4e Mon Sep 17 00:00:00 2001 From: 0xboobface <0xboobface@gmail.com> Date: Sun, 23 Dec 2018 13:57:17 +0100 Subject: [PATCH 07/38] Add download for the chunked http mp4 stream --- .../jasmin/LiveJasminChunkedHttpDownload.java | 293 ++++++++++++++++++ .../ctbrec/sites/jasmin/LiveJasminModel.java | 20 +- 2 files changed, 303 insertions(+), 10 deletions(-) create mode 100644 common/src/main/java/ctbrec/sites/jasmin/LiveJasminChunkedHttpDownload.java diff --git a/common/src/main/java/ctbrec/sites/jasmin/LiveJasminChunkedHttpDownload.java b/common/src/main/java/ctbrec/sites/jasmin/LiveJasminChunkedHttpDownload.java new file mode 100644 index 00000000..30cb3c81 --- /dev/null +++ b/common/src/main/java/ctbrec/sites/jasmin/LiveJasminChunkedHttpDownload.java @@ -0,0 +1,293 @@ +package ctbrec.sites.jasmin; + +import java.io.File; +import java.io.FileOutputStream; +import java.io.IOException; +import java.io.InputStream; +import java.net.URLEncoder; +import java.nio.file.Files; +import java.time.Instant; +import java.util.Random; + +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.recorder.download.Download; +import okhttp3.Request; +import okhttp3.Response; +import okhttp3.WebSocket; +import okhttp3.WebSocketListener; +import okio.ByteString; + +public class LiveJasminChunkedHttpDownload implements Download { + + private static final transient Logger LOG = LoggerFactory.getLogger(LiveJasminChunkedHttpDownload.class); + private static final transient String USER_AGENT = "Mozilla/5.0 (iPhone; CPU OS 10_14 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/11.1.1 Mobile/14E304 Safari/605.1.15"; + + private HttpClient client; + private Model model; + private Instant startTime; + private File targetFile; + + private String applicationId; + private String sessionId; + private String jsm2SessionId; + private String sb_ip; + private String sb_hash; + private String relayHost; + private String hlsHost; + private String clientInstanceId = newClientInstanceId(); // generate a 32 digit random number + private String streamPath = "streams/clonedLiveStream"; + private boolean isAlive = true; + + public LiveJasminChunkedHttpDownload(HttpClient client) { + this.client = client; + } + + private String newClientInstanceId() { + return new java.math.BigInteger(256, new Random()).toString().substring(0, 32); + } + + @Override + public void start(Model model, Config config) throws IOException { + this.model = model; + startTime = Instant.now(); + File _targetFile = config.getFileForRecording(model); + targetFile = new File(_targetFile.getAbsolutePath().replace(".ts", ".mp4")); + + getPerformerDetails(model.getName()); + try { + getStreamPath(); + } catch (InterruptedException e) { + throw new IOException("Couldn't determine stream path", e); + } + + LOG.debug("appid: {}", applicationId); + LOG.debug("sessionid: {}", sessionId); + LOG.debug("jsm2sessionid: {}", jsm2SessionId); + LOG.debug("sb_ip: {}", sb_ip); + LOG.debug("sb_hash: {}", sb_hash); + LOG.debug("hls host: {}", hlsHost); + LOG.debug("clientinstanceid {}", clientInstanceId); + LOG.debug("stream path {}", streamPath); + + String rtmpUrl = "rtmp://" + sb_ip + "/" + applicationId + "?sessionId-" + sessionId + "|clientInstanceId-" + clientInstanceId; + + String m3u8 = "https://" + hlsHost + "/h5live/http/playlist.m3u8?url=" + URLEncoder.encode(rtmpUrl, "utf-8"); + m3u8 = m3u8 += "&stream=" + URLEncoder.encode(streamPath, "utf-8"); + + Request req = new Request.Builder() + .url(m3u8) + .header("User-Agent", USER_AGENT) + .header("Accept", "application/json,*/*") + .header("Accept-Language", "en") + .header("Referer", model.getUrl()) + .header("X-Requested-With", "XMLHttpRequest") + .build(); + try (Response response = client.execute(req)) { + if (response.isSuccessful()) { + System.out.println(response.body().string()); + } else { + throw new IOException(response.code() + " - " + response.message()); + } + } + + String url = "https://" + hlsHost + "/h5live/http/stream.mp4?url=" + URLEncoder.encode(rtmpUrl, "utf-8"); + url = url += "&stream=" + URLEncoder.encode(streamPath, "utf-8"); + + LOG.debug("Downloading {}", url); + req = new Request.Builder() + .url(url) + .header("User-Agent", USER_AGENT) + .header("Accept", "application/json,*/*") + .header("Accept-Language", "en") + .header("Referer", model.getUrl()) + .header("X-Requested-With", "XMLHttpRequest") + .build(); + try (Response response = client.execute(req)) { + if (response.isSuccessful()) { + FileOutputStream fos = null; + try { + Files.createDirectories(targetFile.getParentFile().toPath()); + fos = new FileOutputStream(targetFile); + + InputStream in = response.body().byteStream(); + byte[] b = new byte[10240]; + int len = -1; + while (isAlive && (len = in.read(b)) >= 0) { + fos.write(b, 0, len); + } + } catch (IOException e) { + LOG.error("Couldn't create video file", e); + } finally { + isAlive = false; + if(fos != null) { + fos.close(); + } + } + } else { + throw new IOException(response.code() + " - " + response.message()); + } + } + } + + private void getStreamPath() throws InterruptedException { + Object lock = new Object(); + + Request request = new Request.Builder() + .url("https://" + relayHost + "/?random=" + newClientInstanceId()) + .header("Origin", "https://www.livejasmin.com") + .header("User-Agent", "Mozilla/5.0 (X11; Linux x86_64; rv:63.0) Gecko/20100101 Firefox/63.0") + .header("Accept", "text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8") + .header("Accept-Language", "de,en-US;q=0.7,en;q=0.3") + .build(); + client.newWebSocket(request, new WebSocketListener() { + @Override + public void onOpen(WebSocket webSocket, Response response) { + LOG.debug("relay open {}", model.getName()); + webSocket.send("{\"event\":\"register\",\"applicationId\":\"" + applicationId + + "\",\"connectionData\":{\"jasmin2App\":true,\"isMobileClient\":false,\"platform\":\"desktop\",\"chatID\":\"freechat\"," + + "\"sessionID\":\"" + sessionId + "\"," + "\"jsm2SessionId\":\"" + jsm2SessionId + "\",\"userType\":\"user\"," + "\"performerId\":\"" + + model + + "\",\"clientRevision\":\"\",\"proxyIP\":\"\",\"playerVer\":\"nanoPlayerVersion: 3.10.3 appCodeName: Mozilla appName: Netscape appVersion: 5.0 (X11) platform: Linux x86_64\",\"livejasminTvmember\":false,\"newApplet\":true,\"livefeedtype\":null,\"gravityCookieId\":\"\",\"passparam\":\"\",\"brandID\":\"jasmin\",\"cobrandId\":\"\",\"subbrand\":\"livejasmin\",\"siteName\":\"LiveJasmin\",\"siteUrl\":\"https://www.livejasmin.com\"," + + "\"clientInstanceId\":\"" + clientInstanceId + "\",\"armaVersion\":\"34.10.0\",\"isPassive\":false}}"); + response.close(); + } + + @Override + public void onMessage(WebSocket webSocket, String text) { + LOG.debug("relay <-- {} T{}", model.getName(), text); + JSONObject event = new JSONObject(text); + if (event.optString("event").equals("accept")) { + webSocket.send("{\"event\":\"connectSharedObject\",\"name\":\"data/chat_so\"}"); + } else if (event.optString("event").equals("updateSharedObject")) { + JSONArray list = event.getJSONArray("list"); + for (int i = 0; i < list.length(); i++) { + JSONObject obj = list.getJSONObject(i); + if (obj.optString("name").equals("streamList")) { + LOG.debug(obj.toString(2)); + streamPath = getStreamPath(obj.getJSONObject("newValue")); + LOG.debug("Stream Path: {}", streamPath); + webSocket.send("{\"event\":\"call\",\"funcName\":\"makeActive\",\"data\":[]}"); + webSocket.close(1000, ""); + synchronized (lock) { + lock.notify(); + } + } + } + }else if(event.optString("event").equals("call")) { + String func = event.optString("funcName"); + if(func.equals("closeConnection")) { + stop(); + } + } + } + + private String getStreamPath(JSONObject obj) { + String streamName = "streams/clonedLiveStream"; + int height = 0; + if(obj.has("streams")) { + JSONArray streams = obj.getJSONArray("streams"); + for (int i = 0; i < streams.length(); i++) { + JSONObject stream = streams.getJSONObject(i); + int h = stream.optInt("height"); + if(h > height) { + height = h; + streamName = stream.getString("streamNameWithFolder"); + streamName = "free/" + stream.getString("name"); + } + } + } + return streamName; + } + + @Override + public void onMessage(WebSocket webSocket, ByteString bytes) { + LOG.debug("relay <-- {} B{}", model.getName(), bytes.toString()); + } + + @Override + public void onClosed(WebSocket webSocket, int code, String reason) { + LOG.debug("relay closed {} {} {}", code, reason, model.getName()); + } + + @Override + public void onFailure(WebSocket webSocket, Throwable t, Response response) { + LOG.debug("relay failure {}", model.getName(), t); + if (response != null) { + response.close(); + } + } + }); + + synchronized (lock) { + lock.wait(); + } + } + + protected void getPerformerDetails(String name) throws IOException { + String url = "https://m.livejasmin.com/en/chat-html5/" + name; + Request req = new Request.Builder() + .url(url) + .header("User-Agent", USER_AGENT) + .header("Accept", "application/json,*/*") + .header("Accept-Language", "en") + .header("Referer", "https://www.livejasmin.com") + .header("X-Requested-With", "XMLHttpRequest") + .build(); + try (Response response = client.execute(req)) { + if (response.isSuccessful()) { + String body = response.body().string(); + JSONObject json = new JSONObject(body); + // System.out.println(json.toString(2)); + if (json.optBoolean("success")) { + JSONObject data = json.getJSONObject("data"); + JSONObject config = data.getJSONObject("config"); + JSONObject armageddonConfig = config.getJSONObject("armageddonConfig"); + JSONObject chatRoom = config.getJSONObject("chatRoom"); + sessionId = armageddonConfig.getString("sessionid"); + jsm2SessionId = armageddonConfig.getString("jsm2session"); + sb_hash = chatRoom.getString("sb_hash"); + sb_ip = chatRoom.getString("sb_ip"); + applicationId = "memberChat/jasmin" + name + sb_hash; + hlsHost = "dss-hls-" + sb_ip.replace('.', '-') + ".dditscdn.com"; + relayHost = "dss-relay-" + sb_ip.replace('.', '-') + ".dditscdn.com"; + } else { + throw new IOException("Response was not successful: " + body); + } + } else { + throw new IOException(response.code() + " - " + response.message()); + } + } + } + + @Override + public void stop() { + isAlive = false; + } + + @Override + public boolean isAlive() { + return isAlive ; + } + + @Override + public File getTarget() { + return targetFile; + } + + @Override + public Model getModel() { + return model; + } + + @Override + public Instant getStartTime() { + return startTime; + } +} diff --git a/common/src/main/java/ctbrec/sites/jasmin/LiveJasminModel.java b/common/src/main/java/ctbrec/sites/jasmin/LiveJasminModel.java index 64008189..24204da7 100644 --- a/common/src/main/java/ctbrec/sites/jasmin/LiveJasminModel.java +++ b/common/src/main/java/ctbrec/sites/jasmin/LiveJasminModel.java @@ -28,7 +28,6 @@ import ctbrec.AbstractModel; import ctbrec.Config; import ctbrec.io.HttpException; import ctbrec.recorder.download.Download; -import ctbrec.recorder.download.HlsDownload; import ctbrec.recorder.download.StreamSource; import okhttp3.Request; import okhttp3.Response; @@ -272,14 +271,15 @@ public class LiveJasminModel extends AbstractModel { @Override public Download createDownload() { - if(Config.getInstance().getSettings().livejasminSession.isEmpty()) { - if(Config.isServerMode()) { - return new HlsDownload(getSite().getHttpClient()); - } else { - return new LiveJasminMergedHlsDownload(getSite().getHttpClient()); - } - } else { - return new LiveJasminWebSocketDownload(getSite().getHttpClient()); - } + // if(Config.getInstance().getSettings().livejasminSession.isEmpty()) { + // if(Config.isServerMode()) { + // return new HlsDownload(getSite().getHttpClient()); + // } else { + // return new LiveJasminMergedHlsDownload(getSite().getHttpClient()); + // } + // } else { + // return new LiveJasminWebSocketDownload(getSite().getHttpClient()); + // } + return new LiveJasminChunkedHttpDownload(getSite().getHttpClient()); } } From 154a78660f7264cff8fd08202113847dc1595bcd Mon Sep 17 00:00:00 2001 From: 0xboobface <0xboobface@gmail.com> Date: Sun, 23 Dec 2018 13:57:44 +0100 Subject: [PATCH 08/38] Add comment with URL for token balance --- common/src/main/java/ctbrec/sites/jasmin/LiveJasmin.java | 1 + 1 file changed, 1 insertion(+) diff --git a/common/src/main/java/ctbrec/sites/jasmin/LiveJasmin.java b/common/src/main/java/ctbrec/sites/jasmin/LiveJasmin.java index cbbb13aa..cc53d667 100644 --- a/common/src/main/java/ctbrec/sites/jasmin/LiveJasmin.java +++ b/common/src/main/java/ctbrec/sites/jasmin/LiveJasmin.java @@ -52,6 +52,7 @@ public class LiveJasmin extends AbstractSite { @Override public Integer getTokenBalance() throws IOException { + // https://www.livejasmin.com/en/offline-surprise/get-member-balance?session=m15f0175aa97d2d3c64df8f9bf96e3d8e return 0; } From d29672bb214fa3b9ed70210b48e0a7948b476027 Mon Sep 17 00:00:00 2001 From: 0xboobface <0xboobface@gmail.com> Date: Sun, 23 Dec 2018 13:58:08 +0100 Subject: [PATCH 09/38] Remove debug output --- .../ctbrec/ui/sites/jasmin/LiveJasminFollowedUpdateService.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/client/src/main/java/ctbrec/ui/sites/jasmin/LiveJasminFollowedUpdateService.java b/client/src/main/java/ctbrec/ui/sites/jasmin/LiveJasminFollowedUpdateService.java index 90ada272..b0cf8089 100644 --- a/client/src/main/java/ctbrec/ui/sites/jasmin/LiveJasminFollowedUpdateService.java +++ b/client/src/main/java/ctbrec/ui/sites/jasmin/LiveJasminFollowedUpdateService.java @@ -52,7 +52,7 @@ public class LiveJasminFollowedUpdateService extends PaginatedScheduledService { String body = response.body().string(); List models = new ArrayList<>(); JSONObject json = new JSONObject(body); - LOG.debug(json.toString(2)); + //LOG.debug(json.toString(2)); if(json.has("success")) { JSONObject data = json.getJSONObject("data"); JSONArray performers = data.getJSONArray("performers"); From 3ab4ef785bda4c8a6b23f0fd2eeb7ada3ae6c7d4 Mon Sep 17 00:00:00 2001 From: 0xboobface <0xboobface@gmail.com> Date: Tue, 25 Dec 2018 18:05:00 +0100 Subject: [PATCH 10/38] Remove liveJasminSession --- common/src/main/java/ctbrec/Settings.java | 1 - 1 file changed, 1 deletion(-) diff --git a/common/src/main/java/ctbrec/Settings.java b/common/src/main/java/ctbrec/Settings.java index 4d42f87d..649fb393 100644 --- a/common/src/main/java/ctbrec/Settings.java +++ b/common/src/main/java/ctbrec/Settings.java @@ -64,7 +64,6 @@ public class Settings { public String cam4Password = ""; public String livejasminUsername = ""; public String livejasminPassword = ""; - public String livejasminSession = ""; public String streamateUsername = ""; public String streamatePassword = ""; public String lastDownloadDir = ""; From da4b0ec20ce65aa4616db4e155ef3f8e6f3282d8 Mon Sep 17 00:00:00 2001 From: bounty1342 Date: Tue, 1 Jan 2019 22:32:59 +0100 Subject: [PATCH 11/38] DockerFile to build the server The following commande help build the server with the required version : docker build --build-arg versionM3u8=0.2.7-CTBREC --build-arg version=1.16.0 --build-arg memory=-Xmx192m -t bounty1342/ctbrec . --no-cache Next update could select the branch or commit to ease testing. --- server/Dockerfile.txt | 33 +++++++++++++++++++++++++++++++++ 1 file changed, 33 insertions(+) create mode 100644 server/Dockerfile.txt diff --git a/server/Dockerfile.txt b/server/Dockerfile.txt new file mode 100644 index 00000000..ca6e8f2e --- /dev/null +++ b/server/Dockerfile.txt @@ -0,0 +1,33 @@ +FROM alpine/git as open-m3u8Git +WORKDIR /app +RUN git clone https://github.com/0xboobface/open-m3u8.git + +FROM gradle:4.10-jdk10 as open-m3u8Build +WORKDIR /app/open-m3u8 +COPY --from=open-m3u8Git --chown=gradle:gradle /app /app +RUN gradle install + +FROM alpine/git as ctbrecGit +WORKDIR /app +RUN git clone https://github.com/0xboobface/ctbrec.git + +FROM maven:3-jdk-11-slim as ctbrecBuild +ARG ctbrec +ARG versionM3u8 +WORKDIR /app/master +COPY --from=ctbrecGit /app/ctbrec /app +COPY --from=open-m3u8Build /app/open-m3u8/build/libs/ /app/common/libs/ +RUN mvn clean install:install-file -Dfile=/app/common/libs/open-m3u8-${versionM3u8}.jar -DgroupId=com.iheartradio.m3u8 -DartifactId=open-m3u8 -Dversion=${versionM3u8} -Dpackaging=jar -DgeneratePom=true +RUN mvn clean +RUN mvn install + +FROM openjdk:12-alpine +WORKDIR /app +ARG memory +ARG version +ENV artifact ctbrec-server-${version}-final.jar +ENV path /app/server/target/${artifact} +COPY --from=ctbrecBuild ${path} ./${artifact} +EXPOSE 8080 +CMD java ${memory} -cp ${artifact} -Dctbrec.config=/server.json ctbrec.recorder.server.HttpServer + From 114098d16c62c52d5e824f223fc77df8a6b0de45 Mon Sep 17 00:00:00 2001 From: bounty1342 Date: Tue, 1 Jan 2019 22:38:07 +0100 Subject: [PATCH 12/38] docker-compose to start the container Could also be run with docker run -d -p 8080:8080 -v /ctb/app/config:/root/.config/ctbrec/ -v /ctb/video:/root/ctbrec 0xboobface/ctbrec --- server/docker-compose.yml | 9 +++++++++ 1 file changed, 9 insertions(+) create mode 100644 server/docker-compose.yml diff --git a/server/docker-compose.yml b/server/docker-compose.yml new file mode 100644 index 00000000..f9d5bc72 --- /dev/null +++ b/server/docker-compose.yml @@ -0,0 +1,9 @@ + version: 3 + services: + ctbrec: + ports: + - '8080:8080' + volumes: + - 'ctbrec/config:/root/.config/ctbrec/' + - 'ctbrec/video:/root/ctbrec' + image: bounty1342/ctbrec \ No newline at end of file From 17fcdb405afcc3dae24d698983a9d572519b5b36 Mon Sep 17 00:00:00 2001 From: bounty1342 Date: Tue, 1 Jan 2019 22:43:13 +0100 Subject: [PATCH 13/38] Add info about docker --- server/README.md | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/server/README.md b/server/README.md index 3d227d0b..1d9e724c 100644 --- a/server/README.md +++ b/server/README.md @@ -31,5 +31,13 @@ This is the server part, which is only needed, if you want to run ctbrec in clie ## Docker There is a docker image, created by Github user [1461748123](https://github.com/1461748123), which you can find on [Docker Hub](https://hub.docker.com/r/1461748123/ctbrec/) +You can also build your own image with the Dockerfile included. + +To run them, execute the following command : +``docker run -d -p 8080:8080 -v /ctb/app/config:/root/.config/ctbrec/ -v /ctb/video:/root/ctbrec 0xboobface/ctbrec`` + +You can also use the docker-compose with command : +``docker-compose up`` + ## License CTB Recorder is licensed under the GPLv3. See [LICENSE.txt](https://raw.githubusercontent.com/0xboobface/ctbrec/master/LICENSE.txt). From 77753bd3778f692740909e0d761148f7eff7076d Mon Sep 17 00:00:00 2001 From: 0xboobface <0xboobface@gmail.com> Date: Sat, 5 Jan 2019 16:38:48 +0100 Subject: [PATCH 14/38] Use TextArea and Okhttp to show the chnagelog Instead of using a WebView use TextArea and Okhttp, so that we can get rid of javafx-web --- client/src/main/java/ctbrec/ui/UpdateTab.java | 40 +++++++++++-------- 1 file changed, 24 insertions(+), 16 deletions(-) diff --git a/client/src/main/java/ctbrec/ui/UpdateTab.java b/client/src/main/java/ctbrec/ui/UpdateTab.java index 43eb3e61..f6c8fbfb 100644 --- a/client/src/main/java/ctbrec/ui/UpdateTab.java +++ b/client/src/main/java/ctbrec/ui/UpdateTab.java @@ -3,23 +3,25 @@ package ctbrec.ui; import org.slf4j.Logger; import org.slf4j.LoggerFactory; -import ctbrec.Config; +import ctbrec.io.HttpException; import ctbrec.ui.CamrecApplication.Release; +import ctbrec.ui.controls.Dialogs; import javafx.geometry.Insets; import javafx.geometry.Pos; import javafx.scene.control.Button; import javafx.scene.control.Label; import javafx.scene.control.Tab; +import javafx.scene.control.TextArea; import javafx.scene.layout.Priority; import javafx.scene.layout.VBox; -import javafx.scene.web.WebEngine; -import javafx.scene.web.WebView; +import okhttp3.Request; +import okhttp3.Response; public class UpdateTab extends Tab { private static final transient Logger LOG = LoggerFactory.getLogger(UpdateTab.class); - private WebView browser; + private TextArea changelog; public UpdateTab(Release latest) { setText("Update Available"); @@ -32,18 +34,24 @@ public class UpdateTab extends Tab { vbox.getChildren().add(button); VBox.setMargin(button, new Insets(0, 0, 10, 0)); vbox.setAlignment(Pos.CENTER); - - browser = new WebView(); - try { - WebEngine webEngine = browser.getEngine(); - webEngine.load("https://raw.githubusercontent.com/0xboobface/ctbrec/master/CHANGELOG.md"); - webEngine.setUserDataDirectory(Config.getInstance().getConfigDir()); - vbox.getChildren().add(browser); - VBox.setVgrow(browser, Priority.ALWAYS); - } catch (Exception e) { - LOG.error("Couldn't load changelog", e); - } - + changelog = new TextArea(); + changelog.setEditable(false); + vbox.getChildren().add(changelog); + VBox.setVgrow(changelog, Priority.ALWAYS); setContent(vbox); + + new Thread(() -> { + Request req = new Request.Builder().url("https://raw.githubusercontent.com/0xboobface/ctbrec/master/CHANGELOG.md").build(); + try(Response resp = CamrecApplication.httpClient.execute(req)) { + if(resp.isSuccessful()) { + changelog.setText(resp.body().string()); + } else { + throw new HttpException(resp.code(), resp.message()); + } + } catch (Exception e1) { + LOG.error("Couldn't download the changelog", e1); + Dialogs.showError("Communication error", "Couldn't download the changelog", e1); + } + }).start(); } } From 3f0ecf48057b6f0a6afb21ee4ac2a8738fc626a2 Mon Sep 17 00:00:00 2001 From: 0xboobface <0xboobface@gmail.com> Date: Tue, 8 Jan 2019 14:17:26 +0100 Subject: [PATCH 15/38] Fix Streamate favorites tab --- .../ctbrec/ui/sites/streamate/StreamateFollowedService.java | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/client/src/main/java/ctbrec/ui/sites/streamate/StreamateFollowedService.java b/client/src/main/java/ctbrec/ui/sites/streamate/StreamateFollowedService.java index d78b04c1..2c78e1d7 100644 --- a/client/src/main/java/ctbrec/ui/sites/streamate/StreamateFollowedService.java +++ b/client/src/main/java/ctbrec/ui/sites/streamate/StreamateFollowedService.java @@ -47,7 +47,8 @@ public class StreamateFollowedService extends PaginatedScheduledService { public List call() throws IOException, SAXException, ParserConfigurationException, XPathExpressionException { httpClient.login(); String saKey = httpClient.getSaKey(); - String _url = url + "&page_number=" + page + "&results_per_page=" + MODELS_PER_PAGE + "&sakey=" + saKey; + Long userId = httpClient.getUserId(); + String _url = url + "&page_number=" + page + "&results_per_page=" + MODELS_PER_PAGE + "&sakey=" + saKey + "&userid=" + userId; LOG.debug("Fetching page {}", _url); Request request = new Request.Builder() .url(_url) From a6709bd6db5c8d5c59cd542d65737841d478a3b9 Mon Sep 17 00:00:00 2001 From: 0xboobface <0xboobface@gmail.com> Date: Tue, 8 Jan 2019 14:26:22 +0100 Subject: [PATCH 16/38] Introduce external browser for logins Since the JavaFX integrated browser does not work reliably for recaptcha and in general does not behave like standard browsers, I now use an external browser for the logins. The dependency to javafx-web has been removed. The external browser is based on electron, which uses chromium as internal browser. The implementation can be found at https://github.com/0xboobface/ctbrec-minimal-browser The browser is a minimal browser, which only shows the web page content without any other controls. It is launched by ctbrec in a new process and remote controlled over a socket connection. I first tried to control it via stdin/stdout, but it turns out, that stdin is not supported by electron on windows. --- client/.gitignore | 1 + client/pom.xml | 4 +- client/src/assembly/linux-jre.xml | 8 + client/src/assembly/linux.xml | 10 ++ client/src/assembly/macos-jre.xml | 8 + client/src/assembly/macos.xml | 10 ++ client/src/assembly/win64-jre.xml | 8 + client/src/assembly/win64.xml | 10 ++ .../java/ctbrec/ui/CamrecApplication.java | 4 + .../main/java/ctbrec/ui/DonateTabHtml.java | 33 ---- .../main/java/ctbrec/ui/ExternalBrowser.java | 141 +++++++++++++++ .../main/java/ctbrec/ui/WebbrowserTab.java | 31 ---- .../bonga/BongaCamsElectronLoginDialog.java | 124 +++++++++++++ .../ui/sites/bonga/BongaCamsLoginDialog.java | 120 ------------- .../ui/sites/bonga/BongaCamsSiteUi.java | 71 ++------ .../sites/cam4/Cam4ElectronLoginDialog.java | 127 +++++++++++++ .../ctbrec/ui/sites/cam4/Cam4LoginDialog.java | 112 ------------ .../java/ctbrec/ui/sites/cam4/Cam4SiteUi.java | 62 ++----- .../ui/sites/camsoda/CamsodaLoginDialog.java | 110 ------------ .../ui/sites/camsoda/CamsodaSiteUi.java | 115 ++++++------ .../ui/sites/jasmin/LiveJasminConfigUi.java | 28 +-- .../jasmin/LiveJasminElectronLoginDialog.java | 97 ++++++++++ .../LiveJasminFollowedUpdateService.java | 5 + .../sites/jasmin/LiveJasminLoginDialog.java | 170 ------------------ .../ui/sites/jasmin/LiveJasminSiteUi.java | 110 ++++-------- .../sites/jasmin/LiveJasminTabProvider.java | 2 + .../sites/jasmin/LiveJasminUpdateService.java | 4 +- common/pom.xml | 2 +- common/src/main/java/ctbrec/OS.java | 43 +++++ common/src/main/java/ctbrec/Settings.java | 1 + .../main/java/ctbrec/io/CookieJarImpl.java | 17 +- .../java/ctbrec/sites/jasmin/LiveJasmin.java | 2 +- .../sites/jasmin/LiveJasminHttpClient.java | 32 ++-- .../ctbrec/sites/jasmin/LiveJasminModel.java | 3 +- master/pom.xml | 5 + 35 files changed, 767 insertions(+), 863 deletions(-) delete mode 100644 client/src/main/java/ctbrec/ui/DonateTabHtml.java create mode 100644 client/src/main/java/ctbrec/ui/ExternalBrowser.java delete mode 100644 client/src/main/java/ctbrec/ui/WebbrowserTab.java create mode 100644 client/src/main/java/ctbrec/ui/sites/bonga/BongaCamsElectronLoginDialog.java delete mode 100644 client/src/main/java/ctbrec/ui/sites/bonga/BongaCamsLoginDialog.java create mode 100644 client/src/main/java/ctbrec/ui/sites/cam4/Cam4ElectronLoginDialog.java delete mode 100644 client/src/main/java/ctbrec/ui/sites/cam4/Cam4LoginDialog.java delete mode 100644 client/src/main/java/ctbrec/ui/sites/camsoda/CamsodaLoginDialog.java create mode 100644 client/src/main/java/ctbrec/ui/sites/jasmin/LiveJasminElectronLoginDialog.java delete mode 100644 client/src/main/java/ctbrec/ui/sites/jasmin/LiveJasminLoginDialog.java diff --git a/client/.gitignore b/client/.gitignore index 2c4e8e27..222879c9 100644 --- a/client/.gitignore +++ b/client/.gitignore @@ -6,3 +6,4 @@ /ctbrec-tunnel.sh /jre/ /server-local.sh +/browser/ diff --git a/client/pom.xml b/client/pom.xml index 435967b8..c21fedba 100644 --- a/client/pom.xml +++ b/client/pom.xml @@ -4,7 +4,7 @@ 4.0.0 client - + ctbrec master @@ -80,7 +80,7 @@ org.openjfx - javafx-web + javafx-media diff --git a/client/src/assembly/linux-jre.xml b/client/src/assembly/linux-jre.xml index 4cf3b8ba..182be496 100644 --- a/client/src/assembly/linux-jre.xml +++ b/client/src/assembly/linux-jre.xml @@ -38,5 +38,13 @@ ctbrec/jre false + + browser/ctbrec-minimal-browser-linux-x64 + + **/* + + ctbrec/browser + false + diff --git a/client/src/assembly/linux.xml b/client/src/assembly/linux.xml index 96e803e9..a692495d 100644 --- a/client/src/assembly/linux.xml +++ b/client/src/assembly/linux.xml @@ -29,4 +29,14 @@ ctbrec + + + browser/ctbrec-minimal-browser-linux-x64 + + **/* + + ctbrec/browser + false + + diff --git a/client/src/assembly/macos-jre.xml b/client/src/assembly/macos-jre.xml index ec549040..31f2ff40 100644 --- a/client/src/assembly/macos-jre.xml +++ b/client/src/assembly/macos-jre.xml @@ -38,5 +38,13 @@ ctbrec/jre false + + browser/ctbrec-minimal-browser-darwin-x64 + + **/* + + ctbrec/browser + false + diff --git a/client/src/assembly/macos.xml b/client/src/assembly/macos.xml index fb7620ab..f9c9fcca 100644 --- a/client/src/assembly/macos.xml +++ b/client/src/assembly/macos.xml @@ -29,4 +29,14 @@ ctbrec + + + browser/ctbrec-minimal-browser-darwin-x64 + + **/* + + ctbrec/browser + false + + diff --git a/client/src/assembly/win64-jre.xml b/client/src/assembly/win64-jre.xml index f994af4c..06a6cb8d 100644 --- a/client/src/assembly/win64-jre.xml +++ b/client/src/assembly/win64-jre.xml @@ -40,5 +40,13 @@ ctbrec/jre false + + browser/ctbrec-minimal-browser-win32-x64 + + **/* + + ctbrec/browser + false + diff --git a/client/src/assembly/win64.xml b/client/src/assembly/win64.xml index fd31b626..1f1cf1e7 100644 --- a/client/src/assembly/win64.xml +++ b/client/src/assembly/win64.xml @@ -31,4 +31,14 @@ ctbrec + + + browser/ctbrec-minimal-browser-win32-x64 + + **/* + + ctbrec/browser + false + + diff --git a/client/src/main/java/ctbrec/ui/CamrecApplication.java b/client/src/main/java/ctbrec/ui/CamrecApplication.java index 29ff5a0c..0e8cf692 100644 --- a/client/src/main/java/ctbrec/ui/CamrecApplication.java +++ b/client/src/main/java/ctbrec/ui/CamrecApplication.java @@ -196,6 +196,10 @@ public class CamrecApplication extends Application { System.exit(1); }); } + try { + ExternalBrowser.getInstance().close(); + } catch (IOException e) { + } } }.start(); }); diff --git a/client/src/main/java/ctbrec/ui/DonateTabHtml.java b/client/src/main/java/ctbrec/ui/DonateTabHtml.java deleted file mode 100644 index de1fc23e..00000000 --- a/client/src/main/java/ctbrec/ui/DonateTabHtml.java +++ /dev/null @@ -1,33 +0,0 @@ -package ctbrec.ui; - -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; - -import javafx.scene.control.Tab; -import javafx.scene.web.WebEngine; -import javafx.scene.web.WebView; - -public class DonateTabHtml extends Tab { - - private static final transient Logger LOG = LoggerFactory.getLogger(DonateTabHtml.class); - - private WebView browser; - - public DonateTabHtml() { - setClosable(false); - setText("Donate"); - - browser = new WebView(); - try { - WebEngine webEngine = browser.getEngine(); - webEngine.load("https://0xboobface.github.io/ctbrec/#donate"); - webEngine.setJavaScriptEnabled(true); - webEngine.setOnAlert((e) -> { - System.out.println(e.getData()); - }); - setContent(browser); - } catch (Exception e) { - LOG.error("Couldn't load donate.html", e); - } - } -} diff --git a/client/src/main/java/ctbrec/ui/ExternalBrowser.java b/client/src/main/java/ctbrec/ui/ExternalBrowser.java new file mode 100644 index 00000000..648014d7 --- /dev/null +++ b/client/src/main/java/ctbrec/ui/ExternalBrowser.java @@ -0,0 +1,141 @@ +package ctbrec.ui; + +import java.io.BufferedReader; +import java.io.IOException; +import java.io.InputStream; +import java.io.InputStreamReader; +import java.io.OutputStream; +import java.net.Socket; +import java.util.concurrent.locks.Lock; +import java.util.concurrent.locks.ReentrantLock; +import java.util.function.Consumer; + +import org.json.JSONObject; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import ctbrec.OS; +import ctbrec.io.StreamRedirectThread; + +public class ExternalBrowser implements AutoCloseable { + private static final transient Logger LOG = LoggerFactory.getLogger(ExternalBrowser.class); + private static final ExternalBrowser INSTANCE = new ExternalBrowser(); + private Lock lock = new ReentrantLock(); + + private Process p; + private Consumer messageListener; + private InputStream in; + private OutputStream out; + private Socket socket; + private Thread reader; + private volatile boolean stopped = true; + + public static ExternalBrowser getInstance() { + return INSTANCE; + } + + public void run(String jsonConfig, Consumer messageListener) throws InterruptedException, IOException { + lock.lock(); + stopped = false; + this.messageListener = messageListener; + + p = new ProcessBuilder(OS.getBrowserCommand()).start(); + new StreamRedirectThread(p.getInputStream(), System.err); + new StreamRedirectThread(p.getErrorStream(), System.err); + LOG.debug("Browser started"); + + connectToRemoteControlSocket(); + LOG.debug("Connected to remote control server. Sending config {}", jsonConfig); + + out.write(jsonConfig.getBytes("utf-8")); + out.write('\n'); + out.flush(); + + LOG.debug("Waiting for browser to terminate"); + p.waitFor(); + int exitValue = p.exitValue(); + p = null; + LOG.debug("Browser Process terminated with {}", exitValue); + } + + private void connectToRemoteControlSocket() { + for (int i = 0; i < 20; i++) { + try { + socket = new Socket("localhost", 3202); + in = socket.getInputStream(); + out = socket.getOutputStream(); + reader = new Thread(this::readBrowserOutput); + reader.start(); + return; + } catch (IOException e) { + if(i == 19) { + LOG.error("Connection to remote control socket failed", e); + return; + } + } + + // wait a bit, so that the remote server socket can be opened by electron + try { + Thread.sleep(100); + } catch (InterruptedException e) { + } + } + } + + public void executeJavaScript(String javaScript) throws IOException { + //LOG.debug("Executing JS {}", javaScript); + JSONObject script = new JSONObject(); + script.put("execute", javaScript); + out.write(script.toString().getBytes("utf-8")); + out.write('\n'); + out.flush(); + if(javaScript.equals("quit")) { + stopped = true; + } + } + + @Override + public void close() throws IOException { + if(stopped) { + return; + } + stopped = true; + executeJavaScript("quit"); + if(socket != null) { + socket.close(); + socket = null; + } + messageListener = null; + reader = null; + in = null; + out = null; + if(p != null) { + p.destroy(); + } + } + + private void readBrowserOutput() { + LOG.debug("Browser output reader started"); + try { + BufferedReader br = new BufferedReader(new InputStreamReader(in)); + String line; + while( !Thread.interrupted() && (line = br.readLine()) != null ) { + if(!line.startsWith("{")) { + System.err.println(line); + } else { + if(messageListener != null) { + messageListener.accept(line); + } + } + } + } catch (IOException e) { + if(!stopped) { + LOG.error("Couldn't read browser output", e); + } + } + } + + public void release() { + lock.unlock(); + } +} diff --git a/client/src/main/java/ctbrec/ui/WebbrowserTab.java b/client/src/main/java/ctbrec/ui/WebbrowserTab.java deleted file mode 100644 index cf904a3e..00000000 --- a/client/src/main/java/ctbrec/ui/WebbrowserTab.java +++ /dev/null @@ -1,31 +0,0 @@ -package ctbrec.ui; - -import java.io.File; - -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; - -import ctbrec.OS; -import ctbrec.ui.controls.Dialogs; -import javafx.scene.control.Tab; -import javafx.scene.web.WebEngine; -import javafx.scene.web.WebView; - -public class WebbrowserTab extends Tab { - - private static final transient Logger LOG = LoggerFactory.getLogger(WebbrowserTab.class); - - public WebbrowserTab(String uri) { - WebView browser = new WebView(); - WebEngine webEngine = browser.getEngine(); - webEngine.setUserDataDirectory(new File(OS.getConfigDir(), "webengine")); - webEngine.setJavaScriptEnabled(true); - webEngine.load(uri); - setContent(browser); - - webEngine.setOnError(evt -> { - LOG.error("Couldn't load {}", uri, evt.getException()); - Dialogs.showError("Error", "Couldn't load " + uri, evt.getException()); - }); - } -} diff --git a/client/src/main/java/ctbrec/ui/sites/bonga/BongaCamsElectronLoginDialog.java b/client/src/main/java/ctbrec/ui/sites/bonga/BongaCamsElectronLoginDialog.java new file mode 100644 index 00000000..82df391b --- /dev/null +++ b/client/src/main/java/ctbrec/ui/sites/bonga/BongaCamsElectronLoginDialog.java @@ -0,0 +1,124 @@ +package ctbrec.ui.sites.bonga; + +import java.io.IOException; +import java.net.MalformedURLException; +import java.net.URL; +import java.util.Collections; +import java.util.Objects; +import java.util.function.Consumer; + +import org.json.JSONArray; +import org.json.JSONObject; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import ctbrec.Config; +import ctbrec.sites.bonga.BongaCams; +import ctbrec.ui.ExternalBrowser; +import okhttp3.Cookie; +import okhttp3.Cookie.Builder; +import okhttp3.CookieJar; +import okhttp3.HttpUrl; + +public class BongaCamsElectronLoginDialog { + + private static final transient Logger LOG = LoggerFactory.getLogger(BongaCamsElectronLoginDialog.class); + public static final String DOMAIN = "bongacams.com"; + public static final String URL = BongaCams.BASE_URL + "/login"; + private CookieJar cookieJar; + private ExternalBrowser browser; + + public BongaCamsElectronLoginDialog(CookieJar cookieJar) throws IOException { + this.cookieJar = cookieJar; + browser = ExternalBrowser.getInstance(); + try { + JSONObject config = new JSONObject(); + config.put("url", URL); + config.put("w", 640); + config.put("h", 480); + JSONObject msg = new JSONObject(); + msg.put("config", config); + browser.run(msg.toString(), msgHandler); + } catch (InterruptedException e) { + throw new IOException("Couldn't wait for login dialog", e); + } finally { + browser.close(); + browser.release(); + } + } + + private Consumer msgHandler = (line) -> { + if(!line.startsWith("{")) { + System.err.println(line); + } else { + JSONObject json = new JSONObject(line); + //LOG.debug("Browser: {}", json.toString(2)); + if(json.has("url")) { + String url = json.getString("url"); + if(url.endsWith("/login")) { + try { + Thread.sleep(500); + String username = Config.getInstance().getSettings().bongaUsername; + if (username != null && !username.trim().isEmpty()) { + browser.executeJavaScript("$('input[name=\"log_in[username]\"]').attr('value','" + username + "')"); + } + String password = Config.getInstance().getSettings().bongaPassword; + if (password != null && !password.trim().isEmpty()) { + browser.executeJavaScript("$('input[name=\"log_in[password]\"]').attr('value','" + password + "')"); + } + String[] simplify = new String[] { + "$('div#header').css('display','none');", + "$('div.footer').css('display','none');", + "$('div.footer_copy').css('display','none')", + "$('div[class~=\"banner_top_index\"]').css('display','none');", + "$('td.menu_container').css('display','none');", + "$('div[class~=\"fancybox-overlay\"]').css('display','none');" + }; + for (String js : simplify) { + browser.executeJavaScript(js); + } + } catch(Exception e) { + LOG.warn("Couldn't auto fill username and password for BongaCams", e); + } + } + + if(json.has("cookies")) { + JSONArray _cookies = json.getJSONArray("cookies"); + for (int i = 0; i < _cookies.length(); i++) { + JSONObject cookie = _cookies.getJSONObject(i); + if(cookie.getString("domain").contains(DOMAIN)) { + Builder b = new Cookie.Builder() + .path(cookie.getString("path")) + .domain(DOMAIN) + .name(cookie.getString("name")) + .value(cookie.getString("value")) + .expiresAt(Double.valueOf(cookie.optDouble("expirationDate")).longValue()); + if(cookie.optBoolean("hostOnly")) { + b.hostOnlyDomain(DOMAIN); + } + if(cookie.optBoolean("httpOnly")) { + b.httpOnly(); + } + if(cookie.optBoolean("secure")) { + b.secure(); + } + Cookie c = b.build(); + cookieJar.saveFromResponse(HttpUrl.parse(BongaCams.BASE_URL), Collections.singletonList(c)); + } + } + } + + try { + URL _url = new URL(url); + if (Objects.equals(_url.getPath(), "/")) { + browser.close(); + } + } catch (MalformedURLException e) { + LOG.error("Couldn't parse new url {}", url, e); + } catch (IOException e) { + LOG.error("Couldn't send shutdown request to external browser", e); + } + } + } + }; +} diff --git a/client/src/main/java/ctbrec/ui/sites/bonga/BongaCamsLoginDialog.java b/client/src/main/java/ctbrec/ui/sites/bonga/BongaCamsLoginDialog.java deleted file mode 100644 index 099d0c7c..00000000 --- a/client/src/main/java/ctbrec/ui/sites/bonga/BongaCamsLoginDialog.java +++ /dev/null @@ -1,120 +0,0 @@ -package ctbrec.ui.sites.bonga; - -import java.io.File; -import java.io.InputStream; -import java.net.CookieHandler; -import java.net.CookieManager; -import java.net.HttpCookie; -import java.net.MalformedURLException; -import java.net.URL; -import java.util.List; -import java.util.Objects; - -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; - -import ctbrec.Config; -import ctbrec.OS; -import ctbrec.sites.bonga.BongaCams; -import javafx.concurrent.Worker.State; -import javafx.scene.Scene; -import javafx.scene.control.ProgressIndicator; -import javafx.scene.image.Image; -import javafx.scene.layout.Region; -import javafx.scene.layout.StackPane; -import javafx.scene.web.WebEngine; -import javafx.scene.web.WebView; -import javafx.stage.Stage; - -public class BongaCamsLoginDialog { - - private static final transient Logger LOG = LoggerFactory.getLogger(BongaCamsLoginDialog.class); - public static final String URL = BongaCams.BASE_URL + "/login"; - private List cookies = null; - private String url; - private Region veil; - private ProgressIndicator p; - - public BongaCamsLoginDialog() { - Stage stage = new Stage(); - stage.setTitle("BongaCams Login"); - InputStream icon = getClass().getResourceAsStream("/icon.png"); - stage.getIcons().add(new Image(icon)); - CookieManager cookieManager = new CookieManager(); - CookieHandler.setDefault(cookieManager); - WebView webView = createWebView(stage); - - veil = new Region(); - veil.setStyle("-fx-background-color: rgba(0, 0, 0, 0.4)"); - p = new ProgressIndicator(); - p.setMaxSize(140, 140); - - StackPane stackPane = new StackPane(); - stackPane.getChildren().addAll(webView, veil, p); - - stage.setScene(new Scene(stackPane, 640, 480)); - stage.showAndWait(); - cookies = cookieManager.getCookieStore().getCookies(); - } - - private WebView createWebView(Stage stage) { - WebView browser = new WebView(); - WebEngine webEngine = browser.getEngine(); - webEngine.setJavaScriptEnabled(true); - webEngine.setUserAgent(Config.getInstance().getSettings().httpUserAgent); - webEngine.locationProperty().addListener((obs, oldV, newV) -> { - try { - URL _url = new URL(newV); - if (Objects.equals(_url.getPath(), "/")) { - stage.close(); - } - } catch (MalformedURLException e) { - LOG.error("Couldn't parse new url {}", newV, e); - } - url = newV.toString(); - }); - webEngine.getLoadWorker().stateProperty().addListener((observable, oldState, newState) -> { - if (newState == State.SUCCEEDED) { - veil.setVisible(false); - p.setVisible(false); - //System.out.println("############# " + webEngine.getLocation()); - //System.out.println(webEngine.getDocument().getDocumentElement().getTextContent()); - try { - String username = Config.getInstance().getSettings().bongaUsername; - if (username != null && !username.trim().isEmpty()) { - webEngine.executeScript("$('input[name=\"log_in[username]\"]').attr('value','" + username + "')"); - } - String password = Config.getInstance().getSettings().bongaPassword; - if (password != null && !password.trim().isEmpty()) { - webEngine.executeScript("$('input[name=\"log_in[password]\"]').attr('value','" + password + "')"); - } - webEngine.executeScript("$('div[class~=\"fancybox-overlay\"]').css('display','none')"); - webEngine.executeScript("$('div#header').css('display','none')"); - webEngine.executeScript("$('div.footer').css('display','none')"); - webEngine.executeScript("$('div.footer_copy').css('display','none')"); - webEngine.executeScript("$('div[class~=\"banner_top_index\"]').css('display','none')"); - webEngine.executeScript("$('td.menu_container').css('display','none')"); - } catch(Exception e) { - LOG.warn("Couldn't auto fill username and password for BongaCams", e); - } - } else if (newState == State.CANCELLED || newState == State.FAILED) { - veil.setVisible(false); - p.setVisible(false); - } - }); - webEngine.setUserDataDirectory(new File(OS.getConfigDir(), "webengine")); - webEngine.load(URL); - return browser; - } - - public List getCookies() { - // for (HttpCookie httpCookie : cookies) { - // LOG.debug("Cookie: {}", httpCookie); - // } - return cookies; - } - - public String getUrl() { - return url; - } -} diff --git a/client/src/main/java/ctbrec/ui/sites/bonga/BongaCamsSiteUi.java b/client/src/main/java/ctbrec/ui/sites/bonga/BongaCamsSiteUi.java index 8e123696..46a6b1f3 100644 --- a/client/src/main/java/ctbrec/ui/sites/bonga/BongaCamsSiteUi.java +++ b/client/src/main/java/ctbrec/ui/sites/bonga/BongaCamsSiteUi.java @@ -1,9 +1,6 @@ package ctbrec.ui.sites.bonga; import java.io.IOException; -import java.net.HttpCookie; -import java.util.ArrayList; -import java.util.List; import java.util.concurrent.BlockingQueue; import java.util.concurrent.LinkedBlockingQueue; @@ -15,10 +12,6 @@ import ctbrec.sites.bonga.BongaCams; import ctbrec.sites.bonga.BongaCamsHttpClient; import ctbrec.ui.SiteUI; import ctbrec.ui.TabProvider; -import javafx.application.Platform; -import okhttp3.Cookie; -import okhttp3.CookieJar; -import okhttp3.HttpUrl; public class BongaCamsSiteUi implements SiteUI { @@ -50,31 +43,25 @@ public class BongaCamsSiteUi implements SiteUI { return true; } else { BlockingQueue queue = new LinkedBlockingQueue<>(); + try { + new Thread(() -> { + // login with external browser window + try { + new BongaCamsElectronLoginDialog(bongaCams.getHttpClient().getCookieJar()); + } catch (Exception e1) { + LOG.error("Error logging in with external browser", e1); + } - Runnable showDialog = () -> { - // login with javafx WebView - BongaCamsLoginDialog loginDialog = new BongaCamsLoginDialog(); - - // transfer cookies from WebView to OkHttp cookie jar - transferCookies(loginDialog); - - try { - queue.put(true); - } catch (InterruptedException e) { - LOG.error("Error while signaling termination", e); - } - }; - - if(Platform.isFxApplicationThread()) { - showDialog.run(); - } else { - Platform.runLater(showDialog); - try { - queue.take(); - } catch (InterruptedException e) { - LOG.error("Error while waiting for login dialog to close", e); - throw new IOException(e); - } + try { + queue.put(true); + } catch (InterruptedException e) { + LOG.error("Error while signaling termination", e); + } + }).start(); + queue.take(); + } catch (InterruptedException e) { + LOG.error("Error while waiting for login dialog to close", e); + throw new IOException(e); } BongaCamsHttpClient httpClient = (BongaCamsHttpClient)bongaCams.getHttpClient(); @@ -87,26 +74,4 @@ public class BongaCamsSiteUi implements SiteUI { return loggedIn; } } - - - private void transferCookies(BongaCamsLoginDialog loginDialog) { - BongaCamsHttpClient httpClient = (BongaCamsHttpClient)bongaCams.getHttpClient(); - CookieJar cookieJar = httpClient.getCookieJar(); - - HttpUrl redirectedUrl = HttpUrl.parse(loginDialog.getUrl()); - List cookies = new ArrayList<>(); - for (HttpCookie webViewCookie : loginDialog.getCookies()) { - Cookie cookie = Cookie.parse(redirectedUrl, webViewCookie.toString()); - cookies.add(cookie); - } - cookieJar.saveFromResponse(redirectedUrl, cookies); - - HttpUrl origUrl = HttpUrl.parse(BongaCamsLoginDialog.URL); - cookies = new ArrayList<>(); - for (HttpCookie webViewCookie : loginDialog.getCookies()) { - Cookie cookie = Cookie.parse(origUrl, webViewCookie.toString()); - cookies.add(cookie); - } - cookieJar.saveFromResponse(origUrl, cookies); - } } diff --git a/client/src/main/java/ctbrec/ui/sites/cam4/Cam4ElectronLoginDialog.java b/client/src/main/java/ctbrec/ui/sites/cam4/Cam4ElectronLoginDialog.java new file mode 100644 index 00000000..117849d0 --- /dev/null +++ b/client/src/main/java/ctbrec/ui/sites/cam4/Cam4ElectronLoginDialog.java @@ -0,0 +1,127 @@ +package ctbrec.ui.sites.cam4; + +import java.io.IOException; +import java.net.MalformedURLException; +import java.net.URL; +import java.util.Collections; +import java.util.Objects; +import java.util.function.Consumer; + +import org.json.JSONArray; +import org.json.JSONObject; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import ctbrec.Config; +import ctbrec.sites.cam4.Cam4; +import ctbrec.ui.ExternalBrowser; +import okhttp3.Cookie; +import okhttp3.Cookie.Builder; +import okhttp3.CookieJar; +import okhttp3.HttpUrl; + +public class Cam4ElectronLoginDialog { + + private static final transient Logger LOG = LoggerFactory.getLogger(Cam4ElectronLoginDialog.class); + public static final String DOMAIN = "cam4.com"; + public static final String URL = Cam4.BASE_URI + "/login"; + private CookieJar cookieJar; + private ExternalBrowser browser; + + public Cam4ElectronLoginDialog(CookieJar cookieJar) throws IOException { + this.cookieJar = cookieJar; + browser = ExternalBrowser.getInstance(); + try { + JSONObject config = new JSONObject(); + config.put("url", URL); + config.put("w", 480); + config.put("h", 640); + JSONObject msg = new JSONObject(); + msg.put("config", config); + browser.run(msg.toString(), msgHandler); + } catch (InterruptedException e) { + throw new IOException("Couldn't wait for login dialog", e); + } finally { + browser.close(); + browser.release(); + } + } + + private Consumer msgHandler = (line) -> { + if(!line.startsWith("{")) { + System.err.println(line); + } else { + JSONObject json = new JSONObject(line); + if(json.has("url")) { + String url = json.getString("url"); + + if(url.endsWith("/login")) { + try { + String username = Config.getInstance().getSettings().cam4Username; + if (username != null && !username.trim().isEmpty()) { + browser.executeJavaScript("document.querySelector('#loginPageForm input[name=\"username\"]').value = '" + username + "';"); + } + String password = Config.getInstance().getSettings().cam4Password; + if (password != null && !password.trim().isEmpty()) { + browser.executeJavaScript("document.querySelector('#loginPageForm input[name=\"password\"]').value = '" + password + "';"); + } + browser.executeJavaScript("document.getElementById('footer').setAttribute('style', 'display:none');"); + browser.executeJavaScript("document.getElementById('promptArea').setAttribute('style', 'display:none');"); + browser.executeJavaScript("document.getElementById('content').setAttribute('style', 'padding: 0');"); + browser.executeJavaScript("document.querySelector('div[class~=\"navbar\"]').setAttribute('style', 'display:none');"); + } catch(Exception e) { + LOG.warn("Couldn't auto fill username and password for Cam4", e); + } + } + + if(json.has("cookies")) { + JSONArray _cookies = json.getJSONArray("cookies"); + try { + URL _url = new URL(url); + for (int i = 0; i < _cookies.length(); i++) { + JSONObject cookie = _cookies.getJSONObject(i); + if(cookie.getString("domain").contains("cam4")) { + String domain = cookie.getString("domain"); + if(domain.startsWith(".")) { + domain = domain.substring(1); + } + Cookie c = createCookie(domain, cookie); + cookieJar.saveFromResponse(HttpUrl.parse(url), Collections.singletonList(c)); + c = createCookie("cam4.com", cookie); + cookieJar.saveFromResponse(HttpUrl.parse(Cam4.BASE_URI), Collections.singletonList(c)); + } + } + if (Objects.equals(_url.getPath(), "/")) { + try { + browser.close(); + } catch(IOException e) { + LOG.error("Couldn't send close request to browser", e); + } + } + } catch (MalformedURLException e) { + LOG.error("Couldn't parse new url {}", url, e); + } + } + } + } + }; + + private Cookie createCookie(String domain, JSONObject cookie) { + Builder b = new Cookie.Builder() + .path(cookie.getString("path")) + .domain(domain) + .name(cookie.getString("name")) + .value(cookie.getString("value")) + .expiresAt(Double.valueOf(cookie.optDouble("expirationDate")).longValue()); + if(cookie.optBoolean("hostOnly")) { + b.hostOnlyDomain(domain); + } + if(cookie.optBoolean("httpOnly")) { + b.httpOnly(); + } + if(cookie.optBoolean("secure")) { + b.secure(); + } + return b.build(); + } +} diff --git a/client/src/main/java/ctbrec/ui/sites/cam4/Cam4LoginDialog.java b/client/src/main/java/ctbrec/ui/sites/cam4/Cam4LoginDialog.java deleted file mode 100644 index 65c13019..00000000 --- a/client/src/main/java/ctbrec/ui/sites/cam4/Cam4LoginDialog.java +++ /dev/null @@ -1,112 +0,0 @@ -package ctbrec.ui.sites.cam4; - -import java.io.File; -import java.io.InputStream; -import java.net.CookieHandler; -import java.net.CookieManager; -import java.net.HttpCookie; -import java.net.MalformedURLException; -import java.net.URL; -import java.util.List; -import java.util.Objects; - -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; - -import ctbrec.Config; -import ctbrec.OS; -import ctbrec.sites.cam4.Cam4; -import javafx.concurrent.Worker.State; -import javafx.scene.Scene; -import javafx.scene.control.ProgressIndicator; -import javafx.scene.image.Image; -import javafx.scene.layout.Region; -import javafx.scene.layout.StackPane; -import javafx.scene.web.WebEngine; -import javafx.scene.web.WebView; -import javafx.stage.Stage; - -public class Cam4LoginDialog { - - private static final transient Logger LOG = LoggerFactory.getLogger(Cam4LoginDialog.class); - public static final String URL = Cam4.BASE_URI + "/login"; - private List cookies = null; - private String url; - private Region veil; - private ProgressIndicator p; - - public Cam4LoginDialog() { - Stage stage = new Stage(); - stage.setTitle("Cam4 Login"); - InputStream icon = getClass().getResourceAsStream("/icon.png"); - stage.getIcons().add(new Image(icon)); - CookieManager cookieManager = new CookieManager(); - CookieHandler.setDefault(cookieManager); - WebView webView = createWebView(stage); - - veil = new Region(); - veil.setStyle("-fx-background-color: rgba(0, 0, 0, 0.4)"); - p = new ProgressIndicator(); - p.setMaxSize(140, 140); - - StackPane stackPane = new StackPane(); - stackPane.getChildren().addAll(webView, veil, p); - - stage.setScene(new Scene(stackPane, 480, 854)); - stage.showAndWait(); - cookies = cookieManager.getCookieStore().getCookies(); - } - - private WebView createWebView(Stage stage) { - WebView browser = new WebView(); - WebEngine webEngine = browser.getEngine(); - webEngine.setJavaScriptEnabled(true); - webEngine.setUserAgent(Config.getInstance().getSettings().httpUserAgent); - webEngine.locationProperty().addListener((obs, oldV, newV) -> { - try { - URL _url = new URL(newV); - if (Objects.equals(_url.getPath(), "/")) { - stage.close(); - } - } catch (MalformedURLException e) { - LOG.error("Couldn't parse new url {}", newV, e); - } - url = newV.toString(); - }); - webEngine.getLoadWorker().stateProperty().addListener((observable, oldState, newState) -> { - if (newState == State.SUCCEEDED) { - veil.setVisible(false); - p.setVisible(false); - try { - String username = Config.getInstance().getSettings().cam4Username; - if (username != null && !username.trim().isEmpty()) { - webEngine.executeScript("$('input[name=username]').attr('value','" + username + "')"); - } - String password = Config.getInstance().getSettings().cam4Password; - if (password != null && !password.trim().isEmpty()) { - webEngine.executeScript("$('input[name=password]').attr('value','" + password + "')"); - } - webEngine.executeScript("$('div[class~=navbar]').css('display','none')"); - webEngine.executeScript("$('div#footer').css('display','none')"); - webEngine.executeScript("$('div#content').css('padding','0')"); - } catch(Exception e) { - LOG.warn("Couldn't auto fill username and password for Cam4", e); - } - } else if (newState == State.CANCELLED || newState == State.FAILED) { - veil.setVisible(false); - p.setVisible(false); - } - }); - webEngine.setUserDataDirectory(new File(OS.getConfigDir(), "webengine")); - webEngine.load(URL); - return browser; - } - - public List getCookies() { - return cookies; - } - - public String getUrl() { - return url; - } -} diff --git a/client/src/main/java/ctbrec/ui/sites/cam4/Cam4SiteUi.java b/client/src/main/java/ctbrec/ui/sites/cam4/Cam4SiteUi.java index 8e61b411..fbc18c81 100644 --- a/client/src/main/java/ctbrec/ui/sites/cam4/Cam4SiteUi.java +++ b/client/src/main/java/ctbrec/ui/sites/cam4/Cam4SiteUi.java @@ -1,9 +1,6 @@ package ctbrec.ui.sites.cam4; import java.io.IOException; -import java.net.HttpCookie; -import java.util.ArrayList; -import java.util.List; import java.util.concurrent.BlockingQueue; import java.util.concurrent.LinkedBlockingQueue; @@ -16,9 +13,6 @@ import ctbrec.sites.cam4.Cam4HttpClient; import ctbrec.ui.SiteUI; import ctbrec.ui.TabProvider; import javafx.application.Platform; -import okhttp3.Cookie; -import okhttp3.CookieJar; -import okhttp3.HttpUrl; public class Cam4SiteUi implements SiteUI { private static final transient Logger LOG = LoggerFactory.getLogger(Cam4SiteUi.class); @@ -46,18 +40,19 @@ public class Cam4SiteUi implements SiteUI { @Override public synchronized boolean login() throws IOException { boolean automaticLogin = cam4.login(); - if(automaticLogin) { + if (automaticLogin) { return true; } else { BlockingQueue queue = new LinkedBlockingQueue<>(); Runnable showDialog = () -> { - // login with javafx WebView - Cam4LoginDialog loginDialog = new Cam4LoginDialog(); - - // transfer cookies from WebView to OkHttp cookie jar - transferCookies(loginDialog); + // login with external browser + try { + new Cam4ElectronLoginDialog(cam4.getHttpClient().getCookieJar()); + } catch (Exception e1) { + LOG.error("Error logging in with external browser", e1); + } try { queue.put(true); @@ -66,16 +61,12 @@ public class Cam4SiteUi implements SiteUI { } }; - if(Platform.isFxApplicationThread()) { - showDialog.run(); - } else { - Platform.runLater(showDialog); - try { - queue.take(); - } catch (InterruptedException e) { - LOG.error("Error while waiting for login dialog to close", e); - throw new IOException(e); - } + Platform.runLater(showDialog); + try { + queue.take(); + } catch (InterruptedException e) { + LOG.error("Error while waiting for login dialog to close", e); + throw new IOException(e); } Cam4HttpClient httpClient = (Cam4HttpClient) cam4.getHttpClient(); @@ -83,31 +74,4 @@ public class Cam4SiteUi implements SiteUI { return loggedIn; } } - - - private void transferCookies(Cam4LoginDialog loginDialog) { - Cam4HttpClient httpClient = (Cam4HttpClient) cam4.getHttpClient(); - CookieJar cookieJar = httpClient.getCookieJar(); - - HttpUrl redirectedUrl = HttpUrl.parse(loginDialog.getUrl()); - List cookies = new ArrayList<>(); - for (HttpCookie webViewCookie : loginDialog.getCookies()) { - if(webViewCookie.getDomain().contains("cam4")) { - Cookie cookie = Cookie.parse(redirectedUrl, webViewCookie.toString()); - LOG.debug("{} {} {}", webViewCookie.getDomain(), webViewCookie.getName(), webViewCookie.getValue()); - cookies.add(cookie); - } - } - cookieJar.saveFromResponse(redirectedUrl, cookies); - - HttpUrl origUrl = HttpUrl.parse(Cam4LoginDialog.URL); - cookies = new ArrayList<>(); - for (HttpCookie webViewCookie : loginDialog.getCookies()) { - if(webViewCookie.getDomain().contains("cam4")) { - Cookie cookie = Cookie.parse(origUrl, webViewCookie.toString()); - cookies.add(cookie); - } - } - cookieJar.saveFromResponse(origUrl, cookies); - } } diff --git a/client/src/main/java/ctbrec/ui/sites/camsoda/CamsodaLoginDialog.java b/client/src/main/java/ctbrec/ui/sites/camsoda/CamsodaLoginDialog.java deleted file mode 100644 index e08731ea..00000000 --- a/client/src/main/java/ctbrec/ui/sites/camsoda/CamsodaLoginDialog.java +++ /dev/null @@ -1,110 +0,0 @@ -package ctbrec.ui.sites.camsoda; - -import java.io.File; -import java.io.InputStream; -import java.net.CookieHandler; -import java.net.CookieManager; -import java.net.HttpCookie; -import java.util.Base64; -import java.util.List; - -import ctbrec.OS; -import ctbrec.sites.camsoda.Camsoda; -import javafx.concurrent.Worker.State; -import javafx.scene.Scene; -import javafx.scene.control.ProgressIndicator; -import javafx.scene.image.Image; -import javafx.scene.layout.Region; -import javafx.scene.layout.StackPane; -import javafx.scene.web.WebEngine; -import javafx.scene.web.WebView; -import javafx.stage.Stage; - -// FIXME this dialog does not help, because google's recaptcha does not work -// with WebView even though it does work in Cam4LoginDialog -public class CamsodaLoginDialog { - - public static final String URL = Camsoda.BASE_URI; - private List cookies = null; - private String url; - private Region veil; - private ProgressIndicator p; - - public CamsodaLoginDialog() { - Stage stage = new Stage(); - stage.setTitle("CamSoda Login"); - InputStream icon = getClass().getResourceAsStream("/icon.png"); - stage.getIcons().add(new Image(icon)); - CookieManager cookieManager = new CookieManager(); - CookieHandler.setDefault(cookieManager); - WebView webView = createWebView(stage); - - veil = new Region(); - veil.setStyle("-fx-background-color: rgba(1, 1, 1)"); - p = new ProgressIndicator(); - p.setMaxSize(140, 140); - - p.setVisible(true); - veil.visibleProperty().bind(p.visibleProperty()); - - StackPane stackPane = new StackPane(); - stackPane.getChildren().addAll(webView, veil, p); - - stage.setScene(new Scene(stackPane, 400, 358)); - stage.showAndWait(); - cookies = cookieManager.getCookieStore().getCookies(); - } - - private WebView createWebView(Stage stage) { - WebView browser = new WebView(); - WebEngine webEngine = browser.getEngine(); - webEngine.setJavaScriptEnabled(true); - webEngine.locationProperty().addListener((obs, oldV, newV) -> { - // try { - // URL _url = new URL(newV); - // if (Objects.equals(_url.getPath(), "/")) { - // stage.close(); - // } - // } catch (MalformedURLException e) { - // LOG.error("Couldn't parse new url {}", newV, e); - // } - url = newV.toString(); - System.out.println(newV.toString()); - }); - webEngine.getLoadWorker().stateProperty().addListener((observable, oldState, newState) -> { - if (newState == State.SUCCEEDED) { - webEngine.executeScript("document.querySelector('a[ng-click=\"signin();\"]').click()"); - p.setVisible(false); - - // TODO make this work - // String username = Config.getInstance().getSettings().camsodaUsername; - // if (username != null && !username.trim().isEmpty()) { - // webEngine.executeScript("document.querySelector('input[name=\"loginUsername\"]').value = '" + username + "'"); - // } - // String password = Config.getInstance().getSettings().camsodaPassword; - // if (password != null && !password.trim().isEmpty()) { - // webEngine.executeScript("document.querySelector('input[name=\"loginPassword\"]').value = '" + password + "'"); - // } - } else if (newState == State.CANCELLED || newState == State.FAILED) { - p.setVisible(false); - } - }); - - webEngine.setUserStyleSheetLocation("data:text/css;base64," + Base64.getEncoder().encodeToString(CUSTOM_STYLE.getBytes())); - webEngine.setUserDataDirectory(new File(OS.getConfigDir(), "webengine")); - webEngine.load(URL); - return browser; - } - - public List getCookies() { - return cookies; - } - - public String getUrl() { - return url; - } - - private static final String CUSTOM_STYLE = "" - + ".ngdialog.ngdialog-theme-custom { padding: 0 !important }" - + ".ngdialog-overlay { background: black !important; }"; -} diff --git a/client/src/main/java/ctbrec/ui/sites/camsoda/CamsodaSiteUi.java b/client/src/main/java/ctbrec/ui/sites/camsoda/CamsodaSiteUi.java index af1be6ab..2b2a2eb7 100644 --- a/client/src/main/java/ctbrec/ui/sites/camsoda/CamsodaSiteUi.java +++ b/client/src/main/java/ctbrec/ui/sites/camsoda/CamsodaSiteUi.java @@ -1,24 +1,14 @@ package ctbrec.ui.sites.camsoda; import java.io.IOException; -import java.net.HttpCookie; -import java.util.ArrayList; -import java.util.List; -import java.util.concurrent.BlockingQueue; -import java.util.concurrent.LinkedBlockingQueue; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import ctbrec.sites.ConfigUI; import ctbrec.sites.camsoda.Camsoda; -import ctbrec.sites.camsoda.CamsodaHttpClient; import ctbrec.ui.SiteUI; import ctbrec.ui.TabProvider; -import ctbrec.ui.sites.cam4.Cam4LoginDialog; -import javafx.application.Platform; -import okhttp3.Cookie; -import okhttp3.HttpUrl; public class CamsodaSiteUi implements SiteUI { @@ -50,58 +40,57 @@ public class CamsodaSiteUi implements SiteUI { return automaticLogin; } - - @SuppressWarnings("unused") - private boolean loginWithDialog() throws IOException { - BlockingQueue queue = new LinkedBlockingQueue<>(); - - Runnable showDialog = () -> { - // login with javafx WebView - CamsodaLoginDialog loginDialog = new CamsodaLoginDialog(); - - // transfer cookies from WebView to OkHttp cookie jar - transferCookies(loginDialog); - - try { - queue.put(true); - } catch (InterruptedException e) { - LOG.error("Error while signaling termination", e); - } - }; - - if(Platform.isFxApplicationThread()) { - showDialog.run(); - } else { - Platform.runLater(showDialog); - try { - queue.take(); - } catch (InterruptedException e) { - LOG.error("Error while waiting for login dialog to close", e); - throw new IOException(e); - } - } - - CamsodaHttpClient httpClient = (CamsodaHttpClient)camsoda.getHttpClient(); - boolean loggedIn = httpClient.checkLoginSuccess(); - return loggedIn; - } - - private void transferCookies(CamsodaLoginDialog loginDialog) { - HttpUrl redirectedUrl = HttpUrl.parse(loginDialog.getUrl()); - List cookies = new ArrayList<>(); - for (HttpCookie webViewCookie : loginDialog.getCookies()) { - Cookie cookie = Cookie.parse(redirectedUrl, webViewCookie.toString()); - cookies.add(cookie); - } - camsoda.getHttpClient().getCookieJar().saveFromResponse(redirectedUrl, cookies); - - HttpUrl origUrl = HttpUrl.parse(Cam4LoginDialog.URL); - cookies = new ArrayList<>(); - for (HttpCookie webViewCookie : loginDialog.getCookies()) { - Cookie cookie = Cookie.parse(origUrl, webViewCookie.toString()); - cookies.add(cookie); - } - camsoda.getHttpClient().getCookieJar().saveFromResponse(origUrl, cookies); - } + // @SuppressWarnings("unused") + // private boolean loginWithDialog() throws IOException { + // BlockingQueue queue = new LinkedBlockingQueue<>(); + // + // Runnable showDialog = () -> { + // // login with external browser + // CamsodaLoginDialog loginDialog = new CamsodaLoginDialog(); + // + // // transfer cookies from WebView to OkHttp cookie jar + // transferCookies(loginDialog); + // + // try { + // queue.put(true); + // } catch (InterruptedException e) { + // LOG.error("Error while signaling termination", e); + // } + // }; + // + // if(Platform.isFxApplicationThread()) { + // showDialog.run(); + // } else { + // Platform.runLater(showDialog); + // try { + // queue.take(); + // } catch (InterruptedException e) { + // LOG.error("Error while waiting for login dialog to close", e); + // throw new IOException(e); + // } + // } + // + // CamsodaHttpClient httpClient = (CamsodaHttpClient)camsoda.getHttpClient(); + // boolean loggedIn = httpClient.checkLoginSuccess(); + // return loggedIn; + // } + // + // private void transferCookies(CamsodaLoginDialog loginDialog) { + // HttpUrl redirectedUrl = HttpUrl.parse(loginDialog.getUrl()); + // List cookies = new ArrayList<>(); + // for (HttpCookie webViewCookie : loginDialog.getCookies()) { + // Cookie cookie = Cookie.parse(redirectedUrl, webViewCookie.toString()); + // cookies.add(cookie); + // } + // camsoda.getHttpClient().getCookieJar().saveFromResponse(redirectedUrl, cookies); + // + // HttpUrl origUrl = HttpUrl.parse(Camsoda.BASE_URI); + // cookies = new ArrayList<>(); + // for (HttpCookie webViewCookie : loginDialog.getCookies()) { + // Cookie cookie = Cookie.parse(origUrl, webViewCookie.toString()); + // cookies.add(cookie); + // } + // camsoda.getHttpClient().getCookieJar().saveFromResponse(origUrl, cookies); + // } } diff --git a/client/src/main/java/ctbrec/ui/sites/jasmin/LiveJasminConfigUi.java b/client/src/main/java/ctbrec/ui/sites/jasmin/LiveJasminConfigUi.java index 2cf66c02..f1c8c8db 100644 --- a/client/src/main/java/ctbrec/ui/sites/jasmin/LiveJasminConfigUi.java +++ b/client/src/main/java/ctbrec/ui/sites/jasmin/LiveJasminConfigUi.java @@ -73,19 +73,20 @@ public class LiveJasminConfigUi extends AbstractConfigUI { GridPane.setColumnSpan(password, 2); layout.add(password, 1, row++); - layout.add(new Label("LiveJasmin Session ID"), 0, row); - TextField sessionId = new TextField(); - sessionId.setText(Config.getInstance().getSettings().livejasminSession); - sessionId.textProperty().addListener((ob, o, n) -> { - if(!n.equals(Config.getInstance().getSettings().livejasminSession)) { - Config.getInstance().getSettings().livejasminSession = n; - save(); - } - }); - GridPane.setFillWidth(sessionId, true); - GridPane.setHgrow(sessionId, Priority.ALWAYS); - GridPane.setColumnSpan(sessionId, 2); - layout.add(sessionId, 1, row++); + // layout.add(new Label("LiveJasmin Session ID"), 0, row); + // TextField sessionId = new TextField(); + // sessionId.setText(Config.getInstance().getSettings().livejasminSession); + // sessionId.textProperty().addListener((ob, o, n) -> { + // if(!n.equals(Config.getInstance().getSettings().livejasminSession)) { + // Config.getInstance().getSettings().livejasminSession = n; + // save(); + // } + // }); + // GridPane.setFillWidth(sessionId, true); + // GridPane.setHgrow(sessionId, Priority.ALWAYS); + // GridPane.setColumnSpan(sessionId, 2); + // GridPane.setMargin(sessionId, new Insets(0, 0, 0, SettingsTab.CHECKBOX_MARGIN)); + // layout.add(sessionId, 1, row++); Button createAccount = new Button("Create new Account"); createAccount.setOnAction((e) -> DesktopIntegration.open(liveJasmin.getAffiliateLink())); @@ -93,7 +94,6 @@ public class LiveJasminConfigUi extends AbstractConfigUI { GridPane.setColumnSpan(createAccount, 2); GridPane.setMargin(username, new Insets(0, 0, 0, SettingsTab.CHECKBOX_MARGIN)); GridPane.setMargin(password, new Insets(0, 0, 0, SettingsTab.CHECKBOX_MARGIN)); - GridPane.setMargin(sessionId, new Insets(0, 0, 0, SettingsTab.CHECKBOX_MARGIN)); GridPane.setMargin(createAccount, new Insets(0, 0, 0, SettingsTab.CHECKBOX_MARGIN)); username.setPrefWidth(300); diff --git a/client/src/main/java/ctbrec/ui/sites/jasmin/LiveJasminElectronLoginDialog.java b/client/src/main/java/ctbrec/ui/sites/jasmin/LiveJasminElectronLoginDialog.java new file mode 100644 index 00000000..86879d9e --- /dev/null +++ b/client/src/main/java/ctbrec/ui/sites/jasmin/LiveJasminElectronLoginDialog.java @@ -0,0 +1,97 @@ +package ctbrec.ui.sites.jasmin; + +import java.io.IOException; +import java.util.Collections; +import java.util.function.Consumer; + +import org.json.JSONArray; +import org.json.JSONObject; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import ctbrec.Config; +import ctbrec.sites.jasmin.LiveJasmin; +import ctbrec.ui.ExternalBrowser; +import okhttp3.Cookie; +import okhttp3.Cookie.Builder; +import okhttp3.CookieJar; +import okhttp3.HttpUrl; + +public class LiveJasminElectronLoginDialog { + + private static final transient Logger LOG = LoggerFactory.getLogger(LiveJasminElectronLoginDialog.class); + public static final String URL = LiveJasmin.BASE_URL + "/en/auth/login"; + private CookieJar cookieJar; + private ExternalBrowser browser; + + public LiveJasminElectronLoginDialog(CookieJar cookieJar) throws IOException { + this.cookieJar = cookieJar; + browser = ExternalBrowser.getInstance(); + try { + JSONObject config = new JSONObject(); + config.put("url", URL); + config.put("w", 640); + config.put("h", 720); + JSONObject msg = new JSONObject(); + msg.put("config", config); + browser.run(msg.toString(), msgHandler); + } catch (InterruptedException e) { + throw new IOException("Couldn't wait for login dialog", e); + } finally { + browser.close(); + browser.release(); + } + } + + private Consumer msgHandler = (line) -> { + //LOG.debug("Browser: {}", line); + if(!line.startsWith("{")) { + System.err.println(line); + } else { + JSONObject json = new JSONObject(line); + if(json.has("url")) { + String url = json.getString("url"); + if(url.endsWith("/auth/login")) { + try { + String username = Config.getInstance().getSettings().livejasminUsername; + if (username != null && !username.trim().isEmpty()) { + browser.executeJavaScript("document.querySelector('#login_form input[name=\"username\"]').value = '" + username + "';"); + } + String password = Config.getInstance().getSettings().livejasminPassword; + if (password != null && !password.trim().isEmpty()) { + browser.executeJavaScript("document.querySelector('#login_form input[name=\"password\"]').value = '" + password + "';"); + } + browser.executeJavaScript("document.getElementById('header_container').setAttribute('style', 'display:none');"); + browser.executeJavaScript("document.getElementById('footer').setAttribute('style', 'display:none');"); + browser.executeJavaScript("document.getElementById('react-container').setAttribute('style', 'display:none');"); + browser.executeJavaScript("document.getElementById('inner_container').setAttribute('style', 'padding: 0; margin: 1em');"); + browser.executeJavaScript("document.querySelector('div[class~=\"content_box\"]').setAttribute('style', 'margin: 1em');"); + } catch(Exception e) { + LOG.warn("Couldn't auto fill username and password", e); + } + } + if(json.has("cookies")) { + JSONArray _cookies = json.getJSONArray("cookies"); + for (int i = 0; i < _cookies.length(); i++) { + JSONObject cookie = _cookies.getJSONObject(i); + Builder b = new Cookie.Builder() + .path("/") + .domain("livejasmin.com") + .name(cookie.getString("name")) + .value(cookie.getString("value")) + .expiresAt(0); + Cookie c = b.build(); + cookieJar.saveFromResponse(HttpUrl.parse(LiveJasmin.BASE_URL), Collections.singletonList(c)); + } + } + if(url.contains("/member/")) { + try { + browser.close(); + } catch(IOException e) { + LOG.error("Couldn't send close request to browser", e); + } + } + } + } + }; +} diff --git a/client/src/main/java/ctbrec/ui/sites/jasmin/LiveJasminFollowedUpdateService.java b/client/src/main/java/ctbrec/ui/sites/jasmin/LiveJasminFollowedUpdateService.java index b0cf8089..a660344c 100644 --- a/client/src/main/java/ctbrec/ui/sites/jasmin/LiveJasminFollowedUpdateService.java +++ b/client/src/main/java/ctbrec/ui/sites/jasmin/LiveJasminFollowedUpdateService.java @@ -15,6 +15,7 @@ import ctbrec.io.HttpException; import ctbrec.sites.jasmin.LiveJasmin; import ctbrec.sites.jasmin.LiveJasminModel; import ctbrec.ui.PaginatedScheduledService; +import ctbrec.ui.SiteUiFactory; import javafx.concurrent.Task; import okhttp3.Request; import okhttp3.Response; @@ -37,6 +38,10 @@ public class LiveJasminFollowedUpdateService extends PaginatedScheduledService { return new Task>() { @Override public List call() throws IOException { + boolean loggedIn = SiteUiFactory.getUi(liveJasmin).login(); + if(!loggedIn) { + throw new RuntimeException("Couldn't login on livejasmin.com"); + } //String _url = url + ((page-1) * 36); // TODO find out how to switch pages //LOG.debug("Fetching page {}", url); Request request = new Request.Builder() diff --git a/client/src/main/java/ctbrec/ui/sites/jasmin/LiveJasminLoginDialog.java b/client/src/main/java/ctbrec/ui/sites/jasmin/LiveJasminLoginDialog.java deleted file mode 100644 index f6071d54..00000000 --- a/client/src/main/java/ctbrec/ui/sites/jasmin/LiveJasminLoginDialog.java +++ /dev/null @@ -1,170 +0,0 @@ -package ctbrec.ui.sites.jasmin; - -import java.io.File; -import java.io.IOException; -import java.io.InputStream; -import java.net.CookieHandler; -import java.net.CookieManager; -import java.net.HttpCookie; -import java.net.MalformedURLException; -import java.net.URL; -import java.util.List; -import java.util.Objects; - -import org.json.JSONObject; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; - -import ctbrec.Config; -import ctbrec.OS; -import ctbrec.io.HttpException; -import ctbrec.sites.jasmin.LiveJasmin; -import javafx.concurrent.Worker.State; -import javafx.scene.Scene; -import javafx.scene.control.ProgressIndicator; -import javafx.scene.image.Image; -import javafx.scene.layout.Region; -import javafx.scene.layout.StackPane; -import javafx.scene.web.WebEngine; -import javafx.scene.web.WebView; -import javafx.stage.Stage; -import okhttp3.Request; -import okhttp3.Response; - -public class LiveJasminLoginDialog { - - private static final transient Logger LOG = LoggerFactory.getLogger(LiveJasminLoginDialog.class); - public static final String URL = "https://m.livejasmin.com/en/list"; // #login-modal - private List cookies = null; - private String url; - private Region veil; - private ProgressIndicator p; - private LiveJasmin liveJasmin; - - public LiveJasminLoginDialog(LiveJasmin liveJasmin) throws IOException { - this.liveJasmin = liveJasmin; - Stage stage = new Stage(); - stage.setTitle("LiveJasmin Login"); - InputStream icon = getClass().getResourceAsStream("/icon.png"); - stage.getIcons().add(new Image(icon)); - CookieManager cookieManager = new CookieManager(); - CookieHandler.setDefault(cookieManager); - WebView webView = createWebView(stage); - - veil = new Region(); - veil.setStyle("-fx-background-color: rgba(0, 0, 0, 0.4)"); - p = new ProgressIndicator(); - p.setMaxSize(140, 140); - - StackPane stackPane = new StackPane(); - stackPane.getChildren().addAll(webView, veil, p); - - stage.setScene(new Scene(stackPane, 360, 480)); - stage.showAndWait(); - cookies = cookieManager.getCookieStore().getCookies(); - } - - private WebView createWebView(Stage stage) throws IOException { - - - WebView browser = new WebView(); - WebEngine webEngine = browser.getEngine(); - webEngine.setJavaScriptEnabled(true); - //webEngine.setUserAgent("Mozilla/5.0 (Android 9.0; Mobile; rv:63.0) Gecko/63.0 Firefox/63.0"); - webEngine.setUserAgent("Mozilla/5.0 (Mobile; rv:30.0) Gecko/20100101 Firefox/30.0"); - webEngine.locationProperty().addListener((obs, oldV, newV) -> { - try { - URL _url = new URL(newV); - if (Objects.equals(_url.getPath(), "/")) { - stage.close(); - } - } catch (MalformedURLException e) { - LOG.error("Couldn't parse new url {}", newV, e); - } - url = newV.toString(); - }); - webEngine.getLoadWorker().stateProperty().addListener((observable, oldState, newState) -> { - if (newState == State.SUCCEEDED) { - veil.setVisible(false); - p.setVisible(false); - // try { - // //webEngine.executeScript("$('#eighteen-plus-modal').hide();"); - // //webEngine.executeScript("$('body').html('"+loginForm+"');"); - // //webEngine.executeScript("$('#listpage').append('"+loginForm+"');"); - // // webEngine.executeScript("$('#main-menu-button').click();"); - // // webEngine.executeScript("$('#login-menu').click();"); - // String username = Config.getInstance().getSettings().livejasminUsername; - // if (username != null && !username.trim().isEmpty()) { - // webEngine.executeScript("$('#username').attr('value','" + username + "')"); - // } - // String password = Config.getInstance().getSettings().livejasminPassword; - // if (password != null && !password.trim().isEmpty()) { - // webEngine.executeScript("$('#password').attr('value','" + password + "')"); - // } - // } catch(Exception e) { - // LOG.warn("Couldn't auto fill username and password for LiveJasmin", e); - // } - } else if (newState == State.CANCELLED || newState == State.FAILED) { - veil.setVisible(false); - p.setVisible(false); - } - }); - webEngine.setUserDataDirectory(new File(OS.getConfigDir(), "webengine")); - webEngine.load(URL); - return browser; - } - - private String getLoginForm() throws IOException { - callBaseUrl(); // to get cookies - String url = "https://m.livejasmin.com/en/auth/window/get-login-window?isAjax=1"; - Request request = new Request.Builder() - .url(url) - .addHeader("User-Agent", "Mozilla/5.0 (Android 9.0; Mobile; rv:63.0) Gecko/63.0 Firefox/63.0") - .addHeader("Accept", "application/json, text/javascript, */*") - .addHeader("Accept-Language", "en") - .addHeader("Referer", LiveJasmin.BASE_URL) - .addHeader("X-Requested-With", "XMLHttpRequest") - .build(); - try(Response response = liveJasmin.getHttpClient().execute(request)) { - if(response.isSuccessful()) { - String body = response.body().string(); - JSONObject json = new JSONObject(body); - System.out.println(json.toString(2)); - if(json.optBoolean("success")) { - JSONObject data = json.getJSONObject("data"); - return data.getString("content"); - } else { - throw new IOException("Request was not successful: " + body); - } - } else { - throw new HttpException(response.code(), response.message()); - } - } - } - - private void callBaseUrl() throws IOException { - String url = liveJasmin.getBaseUrl(); - Request request = new Request.Builder() - .url(url) - .header("User-Agent", Config.getInstance().getSettings().httpUserAgent) - .build(); - try(Response response = liveJasmin.getHttpClient().execute(request)) { - if(response.isSuccessful()) { - - } else { - throw new HttpException(response.code(), response.message()); - } - } - } - - public List getCookies() { - for (HttpCookie httpCookie : cookies) { - LOG.debug("Cookie: {}", httpCookie); - } - return cookies; - } - - public String getUrl() { - return url; - } -} diff --git a/client/src/main/java/ctbrec/ui/sites/jasmin/LiveJasminSiteUi.java b/client/src/main/java/ctbrec/ui/sites/jasmin/LiveJasminSiteUi.java index f0115555..10f20ab8 100644 --- a/client/src/main/java/ctbrec/ui/sites/jasmin/LiveJasminSiteUi.java +++ b/client/src/main/java/ctbrec/ui/sites/jasmin/LiveJasminSiteUi.java @@ -1,9 +1,8 @@ package ctbrec.ui.sites.jasmin; import java.io.IOException; -import java.net.HttpCookie; -import java.util.ArrayList; -import java.util.List; +import java.util.concurrent.BlockingQueue; +import java.util.concurrent.LinkedBlockingQueue; import org.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -13,9 +12,6 @@ import ctbrec.sites.jasmin.LiveJasmin; import ctbrec.sites.jasmin.LiveJasminHttpClient; import ctbrec.ui.SiteUI; import ctbrec.ui.TabProvider; -import okhttp3.Cookie; -import okhttp3.CookieJar; -import okhttp3.HttpUrl; public class LiveJasminSiteUi implements SiteUI { @@ -28,12 +24,6 @@ public class LiveJasminSiteUi implements SiteUI { this.liveJasmin = liveJasmin; tabProvider = new LiveJasminTabProvider(liveJasmin); configUi = new LiveJasminConfigUi(liveJasmin); - try { - login(); - } catch (IOException e) { - // TODO Auto-generated catch block - e.printStackTrace(); - } } @Override @@ -49,74 +39,42 @@ public class LiveJasminSiteUi implements SiteUI { @Override public synchronized boolean login() throws IOException { boolean automaticLogin = liveJasmin.login(); - return automaticLogin; - // if(automaticLogin) { - // return true; - // } else { - // BlockingQueue queue = new LinkedBlockingQueue<>(); - // - // Runnable showDialog = () -> { - // // login with javafx WebView - // LiveJasminLoginDialog loginDialog; - // try { - // loginDialog = new LiveJasminLoginDialog(liveJasmin); - // // transfer cookies from WebView to OkHttp cookie jar - // transferCookies(loginDialog); - // } catch (IOException e1) { - // LOG.error("Couldn't load login dialog", e1); - // } - // - // try { - // queue.put(true); - // } catch (InterruptedException e) { - // LOG.error("Error while signaling termination", e); - // } - // }; - // - // if(Platform.isFxApplicationThread()) { - // showDialog.run(); - // } else { - // Platform.runLater(showDialog); - // try { - // queue.take(); - // } catch (InterruptedException e) { - // LOG.error("Error while waiting for login dialog to close", e); - // throw new IOException(e); - // } - // } - // - // LiveJasminHttpClient httpClient = (LiveJasminHttpClient)liveJasmin.getHttpClient(); - // boolean loggedIn = httpClient.checkLoginSuccess(); - // if(loggedIn) { - // LOG.info("Logged in."); - // } else { - // LOG.info("Login failed"); - // } - // return loggedIn; - // } - } + if(automaticLogin) { + return true; + } else { + BlockingQueue queue = new LinkedBlockingQueue<>(); - private void transferCookies(LiveJasminLoginDialog loginDialog) { - LiveJasminHttpClient httpClient = (LiveJasminHttpClient)liveJasmin.getHttpClient(); - CookieJar cookieJar = httpClient.getCookieJar(); + new Thread (() -> { + // login with external browser window + try { + //LiveJasminElectronLoginDialog dialog = + new LiveJasminElectronLoginDialog(liveJasmin.getHttpClient().getCookieJar()); + } catch (IOException e1) { + LOG.error("Error logging in with external browser", e1); + } - String[] urls = { - "https://www.livejasmin.com", - "http://www.livejasmin.com", - "https://m.livejasmin.com", - "http://m.livejasmin.com", - "https://livejasmin.com", - "http://livejasmin.com" - }; + try { + queue.put(true); + } catch (InterruptedException e) { + LOG.error("Error while signaling termination", e); + } + }).start(); - for (String u : urls) { - HttpUrl url = HttpUrl.parse(u); - List cookies = new ArrayList<>(); - for (HttpCookie webViewCookie : loginDialog.getCookies()) { - Cookie cookie = Cookie.parse(url, webViewCookie.toString()); - cookies.add(cookie); + try { + queue.take(); + } catch (InterruptedException e) { + LOG.error("Error while waiting for login dialog to close", e); + throw new IOException(e); } - cookieJar.saveFromResponse(url, cookies); + + LiveJasminHttpClient httpClient = (LiveJasminHttpClient)liveJasmin.getHttpClient(); + boolean loggedIn = httpClient.checkLoginSuccess(); + if(loggedIn) { + LOG.info("Logged in"); + } else { + LOG.info("Login failed"); + } + return loggedIn; } } } diff --git a/client/src/main/java/ctbrec/ui/sites/jasmin/LiveJasminTabProvider.java b/client/src/main/java/ctbrec/ui/sites/jasmin/LiveJasminTabProvider.java index 998c2ba7..ee135d5d 100644 --- a/client/src/main/java/ctbrec/ui/sites/jasmin/LiveJasminTabProvider.java +++ b/client/src/main/java/ctbrec/ui/sites/jasmin/LiveJasminTabProvider.java @@ -8,6 +8,7 @@ import ctbrec.ui.TabProvider; import ctbrec.ui.ThumbOverviewTab; import javafx.scene.Scene; import javafx.scene.control.Tab; +import javafx.util.Duration; public class LiveJasminTabProvider extends TabProvider { @@ -43,6 +44,7 @@ public class LiveJasminTabProvider extends TabProvider { LiveJasminUpdateService s = new LiveJasminUpdateService(liveJasmin, url); ThumbOverviewTab tab = new ThumbOverviewTab(title, s, liveJasmin); tab.setRecorder(liveJasmin.getRecorder()); + s.setPeriod(Duration.seconds(60)); return tab; } } diff --git a/client/src/main/java/ctbrec/ui/sites/jasmin/LiveJasminUpdateService.java b/client/src/main/java/ctbrec/ui/sites/jasmin/LiveJasminUpdateService.java index e810fcab..e19a3b3d 100644 --- a/client/src/main/java/ctbrec/ui/sites/jasmin/LiveJasminUpdateService.java +++ b/client/src/main/java/ctbrec/ui/sites/jasmin/LiveJasminUpdateService.java @@ -48,7 +48,7 @@ public class LiveJasminUpdateService extends PaginatedScheduledService { .name("listPageOrderType") .value("most_popular") .build(); - cookieJar.saveFromResponse(HttpUrl.parse("https://www.livejasmin.com"), Collections.singletonList(sortCookie)); + cookieJar.saveFromResponse(HttpUrl.parse("https://livejasmin.com"), Collections.singletonList(sortCookie)); LOG.debug("Fetching page {}", url); Request request = new Request.Builder() @@ -56,7 +56,7 @@ public class LiveJasminUpdateService extends PaginatedScheduledService { .addHeader("User-Agent", Config.getInstance().getSettings().httpUserAgent) .addHeader("Accept", "application/json, text/javascript, */*") .addHeader("Accept-Language", "en") - .addHeader("Referer", liveJasmin.getBaseUrl()) + .addHeader("Referer", liveJasmin.getBaseUrl() + "/en/girls/") .addHeader("X-Requested-With", "XMLHttpRequest") .build(); try (Response response = liveJasmin.getHttpClient().execute(request)) { diff --git a/common/pom.xml b/common/pom.xml index 4308d633..8242591a 100644 --- a/common/pom.xml +++ b/common/pom.xml @@ -57,7 +57,7 @@ org.openjfx - javafx-web + javafx-media provided diff --git a/common/src/main/java/ctbrec/OS.java b/common/src/main/java/ctbrec/OS.java index e86842e4..268e8cbf 100644 --- a/common/src/main/java/ctbrec/OS.java +++ b/common/src/main/java/ctbrec/OS.java @@ -8,8 +8,11 @@ import java.awt.TrayIcon; import java.awt.TrayIcon.MessageType; import java.io.File; import java.io.IOException; +import java.net.URI; +import java.net.URISyntaxException; import java.nio.file.Path; import java.nio.file.Paths; +import java.util.Arrays; import java.util.Map.Entry; import org.slf4j.Logger; @@ -61,6 +64,46 @@ public class OS { return configDir; } + public static String[] getBrowserCommand(String...args) { + if(System.getenv("CTBREC_BROWSER") != null) { + String cmd[] = new String[args.length + 1]; + cmd[0] = System.getenv("CTBREC_BROWSER"); + System.arraycopy(args, 0, cmd, 1, args.length); + return cmd; + } + + try { + URI uri = OS.class.getProtectionDomain().getCodeSource().getLocation().toURI(); + File jar = new File(uri.getPath()); + File browserDir = new File(jar.getParentFile(), "browser"); + String[] cmd; + switch (getOsType()) { + case LINUX: + cmd = new String[args.length + 1]; + cmd[0] = new File(browserDir, "ctbrec-minimal-browser").getAbsolutePath(); + System.arraycopy(args, 0, cmd, 1, args.length); + break; + case WINDOWS: + cmd = new String[args.length + 1]; + cmd[0] = new File(browserDir, "ctbrec-minimal-browser.exe").getAbsolutePath(); + System.arraycopy(args, 0, cmd, 1, args.length); + break; + case MAC: + cmd = new String[args.length + 2]; + cmd[0] = "open"; + cmd[1] = new File(browserDir, "ctbrec-minimal-browser.app").getAbsolutePath(); + System.arraycopy(args, 0, cmd, 2, args.length); + break; + default: + throw new RuntimeException("Unsupported operating system " + System.getProperty("os.name")); + } + LOG.debug("Browser command: {}", Arrays.toString(cmd)); + return cmd; + } catch (URISyntaxException e) { + throw new RuntimeException(e); + } + } + public static Settings getDefaultSettings() { Settings settings = new Settings(); if(getOsType() == TYPE.WINDOWS) { diff --git a/common/src/main/java/ctbrec/Settings.java b/common/src/main/java/ctbrec/Settings.java index 649fb393..1e6612cf 100644 --- a/common/src/main/java/ctbrec/Settings.java +++ b/common/src/main/java/ctbrec/Settings.java @@ -36,6 +36,7 @@ public class Settings { public boolean localRecording = true; public int httpPort = 8080; public int httpTimeout = 10000; + public String httpUserAgentMobile = "Mozilla/5.0 (Android 9.0; Mobile; rv:63.0) Gecko/63.0 Firefox/63.0"; public String httpUserAgent = "Mozilla/5.0 Gecko/20100101 Firefox/62.0"; public String httpServer = "localhost"; public String recordingsDir = System.getProperty("user.home") + File.separator + "ctbrec"; diff --git a/common/src/main/java/ctbrec/io/CookieJarImpl.java b/common/src/main/java/ctbrec/io/CookieJarImpl.java index 69192ecb..324fb93a 100644 --- a/common/src/main/java/ctbrec/io/CookieJarImpl.java +++ b/common/src/main/java/ctbrec/io/CookieJarImpl.java @@ -25,7 +25,7 @@ public class CookieJarImpl implements CookieJar { @Override public void saveFromResponse(HttpUrl url, List cookies) { - String host = getHost(url); + String host = getDomain(url); List cookiesForUrl = cookieStore.get(host); if (cookiesForUrl != null) { cookiesForUrl = new ArrayList(cookiesForUrl); //unmodifiable @@ -52,7 +52,7 @@ public class CookieJarImpl implements CookieJar { @Override public List loadForRequest(HttpUrl url) { - String host = getHost(url); + String host = getDomain(url); List cookies = cookieStore.get(host); LOG.debug("Cookies for {}", url); Optional.ofNullable(cookies).ifPresent(cookiez -> cookiez.forEach(c -> { @@ -72,12 +72,13 @@ public class CookieJarImpl implements CookieJar { throw new NoSuchElementException("No cookie named " + name + " for " + url.host() + " available"); } - private String getHost(HttpUrl url) { - String host = url.host(); - if (host.startsWith("www.")) { - host = host.substring(4); - } - return host; + private String getDomain(HttpUrl url) { + // String host = url.host(); + // if (host.startsWith("www.")) { + // host = host.substring(4); + // } + // return host; + return url.topPrivateDomain(); } @Override diff --git a/common/src/main/java/ctbrec/sites/jasmin/LiveJasmin.java b/common/src/main/java/ctbrec/sites/jasmin/LiveJasmin.java index cc53d667..1b3fd206 100644 --- a/common/src/main/java/ctbrec/sites/jasmin/LiveJasmin.java +++ b/common/src/main/java/ctbrec/sites/jasmin/LiveJasmin.java @@ -151,7 +151,7 @@ public class LiveJasmin extends AbstractSite { @Override public boolean credentialsAvailable() { - return !Config.getInstance().getSettings().livejasminSession.isEmpty(); + return !Config.getInstance().getSettings().livejasminUsername.isEmpty(); } } diff --git a/common/src/main/java/ctbrec/sites/jasmin/LiveJasminHttpClient.java b/common/src/main/java/ctbrec/sites/jasmin/LiveJasminHttpClient.java index b979ee53..1d495599 100644 --- a/common/src/main/java/ctbrec/sites/jasmin/LiveJasminHttpClient.java +++ b/common/src/main/java/ctbrec/sites/jasmin/LiveJasminHttpClient.java @@ -1,7 +1,6 @@ package ctbrec.sites.jasmin; import java.io.IOException; -import java.util.Collections; import java.util.NoSuchElementException; import org.slf4j.Logger; @@ -29,19 +28,6 @@ public class LiveJasminHttpClient extends HttpClient { return true; } - // set session cookie, if session id is available - if(!Config.getInstance().getSettings().livejasminSession.isEmpty()) { - Cookie captchaCookie = new Cookie.Builder() - .domain("livejasmin.com") - .name("session") - .value(Config.getInstance().getSettings().livejasminSession) - .build(); - getCookieJar().saveFromResponse(HttpUrl.parse("https://livejasmin.com"), Collections.singletonList(captchaCookie)); - getCookieJar().saveFromResponse(HttpUrl.parse("https://www.livejasmin.com"), Collections.singletonList(captchaCookie)); - getCookieJar().saveFromResponse(HttpUrl.parse("https://m.livejasmin.com"), Collections.singletonList(captchaCookie)); - } - - // loadMainPage(); // to get initial cookies // Cookie captchaCookie = new Cookie.Builder() // .domain("livejasmin.com") @@ -152,10 +138,17 @@ public class LiveJasminHttpClient extends HttpClient { .followRedirects(false) .followSslRedirects(false) .build(); - String url = LiveJasmin.BASE_URL + "/en/free/favourite/get-favourite-list"; + + // getCookieJar().getCookies().entrySet().forEach((e) -> { + // e.getValue().forEach(c -> { + // LOG.debug("LOGIN CHECK: Cookie {} {}={}", e.getKey(), c.name(), c.value()); + // }); + // }); + + String url = "https://m.livejasmin.com/en/member/favourite/get-favourite-list?ajax=1"; Request request = new Request.Builder() .url(url) - .addHeader("User-Agent", Config.getInstance().getSettings().httpUserAgent) + .addHeader("User-Agent", Config.getInstance().getSettings().httpUserAgentMobile) .addHeader("Accept", "application/json, text/javascript, */*") .addHeader("Accept-Language", "en") .addHeader("Referer", LiveJasmin.BASE_URL) @@ -166,13 +159,18 @@ public class LiveJasminHttpClient extends HttpClient { if(response.isSuccessful()) { return true; } else { + // if(response.code() >= 300 && response.code() < 400) { + // for (String name : response.headers().names()) { + // LOG.debug("{}: {}", name, response.header(name)); + // } + // } return false; } } } public String getSessionId() { - Cookie sessionCookie = getCookieJar().getCookie(HttpUrl.parse("https://www.livejasmin.com"), "session"); + Cookie sessionCookie = getCookieJar().getCookie(HttpUrl.parse(LiveJasmin.BASE_URL), "session"); if(sessionCookie != null) { return sessionCookie.value(); } else { diff --git a/common/src/main/java/ctbrec/sites/jasmin/LiveJasminModel.java b/common/src/main/java/ctbrec/sites/jasmin/LiveJasminModel.java index 24204da7..8722c6f4 100644 --- a/common/src/main/java/ctbrec/sites/jasmin/LiveJasminModel.java +++ b/common/src/main/java/ctbrec/sites/jasmin/LiveJasminModel.java @@ -280,6 +280,7 @@ public class LiveJasminModel extends AbstractModel { // } else { // return new LiveJasminWebSocketDownload(getSite().getHttpClient()); // } - return new LiveJasminChunkedHttpDownload(getSite().getHttpClient()); + //return new LiveJasminChunkedHttpDownload(getSite().getHttpClient()); + return new LiveJasminWebSocketDownload(getSite().getHttpClient()); } } diff --git a/master/pom.xml b/master/pom.xml index bc994b4a..34ff1e30 100644 --- a/master/pom.xml +++ b/master/pom.xml @@ -85,6 +85,11 @@ javafx-web 11 + + org.openjfx + javafx-media + 11 + com.google.guava guava From 4516b8d787c382833fc8e56a8a2a9c916aa93045 Mon Sep 17 00:00:00 2001 From: 0xboobface <0xboobface@gmail.com> Date: Wed, 9 Jan 2019 15:11:18 +0100 Subject: [PATCH 17/38] Fix parsing of model name for recordings --- common/src/main/java/ctbrec/recorder/LocalRecorder.java | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/common/src/main/java/ctbrec/recorder/LocalRecorder.java b/common/src/main/java/ctbrec/recorder/LocalRecorder.java index 3a1e46e9..dfd501ec 100644 --- a/common/src/main/java/ctbrec/recorder/LocalRecorder.java +++ b/common/src/main/java/ctbrec/recorder/LocalRecorder.java @@ -468,10 +468,11 @@ public class LocalRecorder implements Recorder { for (File ts: possibleRecordings) { try { String filename = ts.getName(); - String dateString = filename.substring(filename.length() - 3 - DATE_FORMAT.length(), filename.length() - 3); + int extLength = filename.length() - filename.lastIndexOf('.'); + String dateString = filename.substring(filename.length() - extLength - DATE_FORMAT.length(), filename.length() - extLength); Date startDate = sdf.parse(dateString); Recording recording = new Recording(); - recording.setModelName(filename.substring(0, filename.length() - 4 - DATE_FORMAT.length())); + recording.setModelName(filename.substring(0, filename.length() - extLength - 1 - DATE_FORMAT.length())); recording.setStartDate(Instant.ofEpochMilli(startDate.getTime())); String path = ts.getAbsolutePath().replace(config.getSettings().recordingsDir, ""); if(!path.startsWith("/")) { @@ -775,6 +776,10 @@ public class LocalRecorder implements Recorder { try { LOG.debug("Determining video length for {}", download.getTarget()); File target = download.getTarget(); + if(!target.exists()) { + return true; + } + double duration = 0; if(target.isDirectory()) { File playlist = new File(target, "playlist.m3u8"); From f2ac9e3657597291fe4e06aa317b2a213ec75fdf Mon Sep 17 00:00:00 2001 From: 0xboobface <0xboobface@gmail.com> Date: Wed, 9 Jan 2019 15:12:21 +0100 Subject: [PATCH 18/38] Show error dialog if login fails --- .../src/main/java/ctbrec/ui/sites/bonga/BongaCamsSiteUi.java | 2 ++ client/src/main/java/ctbrec/ui/sites/cam4/Cam4SiteUi.java | 2 ++ .../main/java/ctbrec/ui/sites/jasmin/LiveJasminSiteUi.java | 4 +++- 3 files changed, 7 insertions(+), 1 deletion(-) diff --git a/client/src/main/java/ctbrec/ui/sites/bonga/BongaCamsSiteUi.java b/client/src/main/java/ctbrec/ui/sites/bonga/BongaCamsSiteUi.java index 46a6b1f3..d5453670 100644 --- a/client/src/main/java/ctbrec/ui/sites/bonga/BongaCamsSiteUi.java +++ b/client/src/main/java/ctbrec/ui/sites/bonga/BongaCamsSiteUi.java @@ -12,6 +12,7 @@ import ctbrec.sites.bonga.BongaCams; import ctbrec.sites.bonga.BongaCamsHttpClient; import ctbrec.ui.SiteUI; import ctbrec.ui.TabProvider; +import ctbrec.ui.controls.Dialogs; public class BongaCamsSiteUi implements SiteUI { @@ -50,6 +51,7 @@ public class BongaCamsSiteUi implements SiteUI { new BongaCamsElectronLoginDialog(bongaCams.getHttpClient().getCookieJar()); } catch (Exception e1) { LOG.error("Error logging in with external browser", e1); + Dialogs.showError("Login error", "Couldn't login to " + bongaCams.getName(), e1); } try { diff --git a/client/src/main/java/ctbrec/ui/sites/cam4/Cam4SiteUi.java b/client/src/main/java/ctbrec/ui/sites/cam4/Cam4SiteUi.java index fbc18c81..3d56eec3 100644 --- a/client/src/main/java/ctbrec/ui/sites/cam4/Cam4SiteUi.java +++ b/client/src/main/java/ctbrec/ui/sites/cam4/Cam4SiteUi.java @@ -12,6 +12,7 @@ import ctbrec.sites.cam4.Cam4; import ctbrec.sites.cam4.Cam4HttpClient; import ctbrec.ui.SiteUI; import ctbrec.ui.TabProvider; +import ctbrec.ui.controls.Dialogs; import javafx.application.Platform; public class Cam4SiteUi implements SiteUI { @@ -52,6 +53,7 @@ public class Cam4SiteUi implements SiteUI { new Cam4ElectronLoginDialog(cam4.getHttpClient().getCookieJar()); } catch (Exception e1) { LOG.error("Error logging in with external browser", e1); + Dialogs.showError("Login error", "Couldn't login to " + cam4.getName(), e1); } try { diff --git a/client/src/main/java/ctbrec/ui/sites/jasmin/LiveJasminSiteUi.java b/client/src/main/java/ctbrec/ui/sites/jasmin/LiveJasminSiteUi.java index 10f20ab8..94634972 100644 --- a/client/src/main/java/ctbrec/ui/sites/jasmin/LiveJasminSiteUi.java +++ b/client/src/main/java/ctbrec/ui/sites/jasmin/LiveJasminSiteUi.java @@ -12,6 +12,7 @@ import ctbrec.sites.jasmin.LiveJasmin; import ctbrec.sites.jasmin.LiveJasminHttpClient; import ctbrec.ui.SiteUI; import ctbrec.ui.TabProvider; +import ctbrec.ui.controls.Dialogs; public class LiveJasminSiteUi implements SiteUI { @@ -49,8 +50,9 @@ public class LiveJasminSiteUi implements SiteUI { try { //LiveJasminElectronLoginDialog dialog = new LiveJasminElectronLoginDialog(liveJasmin.getHttpClient().getCookieJar()); - } catch (IOException e1) { + } catch (Exception e1) { LOG.error("Error logging in with external browser", e1); + Dialogs.showError("Login error", "Couldn't login to " + liveJasmin.getName(), e1); } try { From 45df31eb150e9e426957a8de95ee8369d19399a9 Mon Sep 17 00:00:00 2001 From: 0xboobface <0xboobface@gmail.com> Date: Wed, 9 Jan 2019 20:34:25 +0100 Subject: [PATCH 19/38] Properly end websocket download End the websocket download if a model changes state from online. Also properly close both sockets and set isAlive to false, if the download is closed or fails --- .../jasmin/LiveJasminWebSocketDownload.java | 63 ++++++++++++++++--- 1 file changed, 56 insertions(+), 7 deletions(-) diff --git a/common/src/main/java/ctbrec/sites/jasmin/LiveJasminWebSocketDownload.java b/common/src/main/java/ctbrec/sites/jasmin/LiveJasminWebSocketDownload.java index 47327987..f28c7252 100644 --- a/common/src/main/java/ctbrec/sites/jasmin/LiveJasminWebSocketDownload.java +++ b/common/src/main/java/ctbrec/sites/jasmin/LiveJasminWebSocketDownload.java @@ -13,8 +13,13 @@ import org.json.JSONObject; import org.slf4j.Logger; import org.slf4j.LoggerFactory; +import com.google.common.eventbus.Subscribe; + import ctbrec.Config; import ctbrec.Model; +import ctbrec.event.Event; +import ctbrec.event.EventBusHolder; +import ctbrec.event.ModelStateChangedEvent; import ctbrec.io.HttpClient; import ctbrec.recorder.download.Download; import okhttp3.Request; @@ -67,6 +72,8 @@ public class LiveJasminWebSocketDownload implements Download { LOG.debug("stream host: {}",streamHost); LOG.debug("clientinstanceid {}",clientInstanceId); + EventBusHolder.BUS.register(this); + Request request = new Request.Builder() .url("https://" + relayHost + "/") .header("Origin", "https://www.livejasmin.com") @@ -98,13 +105,26 @@ public class LiveJasminWebSocketDownload implements Download { sendToRelay("{\"event\":\"connectSharedObject\",\"name\":\"data/chat_so\"}"); }).start(); } else if (event.optString("event").equals("updateSharedObject")) { - // TODO JSONArray list = event.getJSONArray("list"); for (int i = 0; i < list.length(); i++) { JSONObject obj = list.getJSONObject(i); if (obj.optString("name").equals("streamList")) { - LOG.debug(obj.toString(2)); + //LOG.debug(obj.toString(2)); streamPath = getStreamPath(obj.getJSONObject("newValue")); + } else if(obj.optString("name").equals("isPrivate") + || obj.optString("name").equals("onPrivate") + || obj.optString("name").equals("onPrivateAll") + || obj.optString("name").equals("onPrivateLJ")) + { + if(obj.optBoolean("newValue")) { + // model went private, stop recording + LOG.debug("Model {} state changed to private -> stopping download", model.getName()); + stop(); + } + } else if(obj.optString("name").equals("recommendedBandwidth") || obj.optString("name").equals("realQualityData")) { + // stream quality related -> do nothing + } else { + LOG.debug("{} -{}", model.getName(), obj.toString()); } } @@ -120,13 +140,22 @@ public class LiveJasminWebSocketDownload implements Download { } }).start(); } - }else if(event.optString("event").equals("call")) { + } else if(event.optString("event").equals("call")) { String func = event.optString("funcName"); - if(func.equals("closeConnection")) { + if (func.equals("closeConnection")) { connectionClosed = true; - //System.out.println(event.get("data")); + // System.out.println(event.get("data")); stop(); + } else if (func.equals("addLine")) { + // chat message -> ignore + } else if (func.equals("receiveInvitation")) { + // invitation to private show -> ignore + } else { + LOG.debug("{} -{}", model.getName(), event.toString()); } + } else { + if(!event.optString("event").equals("pong")) + LOG.debug("{} -{}", model.getName(), event.toString()); } } @@ -156,12 +185,14 @@ public class LiveJasminWebSocketDownload implements Download { @Override public void onClosed(WebSocket webSocket, int code, String reason) { LOG.trace("relay closed {} {} {}", code, reason, model.getName()); + stop(); } @Override public void onFailure(WebSocket webSocket, Throwable t, Response response) { if(!connectionClosed) { LOG.trace("relay failure {}", model.getName(), t); + stop(); if (response != null) { response.close(); } @@ -170,6 +201,17 @@ public class LiveJasminWebSocketDownload implements Download { }); } + @Subscribe + public void handleEvent(Event evt) { + if(evt.getType() == Event.Type.MODEL_STATUS_CHANGED) { + ModelStateChangedEvent me = (ModelStateChangedEvent) evt; + if(me.getModel().equals(model) && me.getOldState() == Model.State.ONLINE) { + LOG.debug("Model {} state changed to {} -> stopping download", me.getNewState(), model.getName()); + stop(); + } + } + } + private void sendToRelay(String msg) { LOG.trace("relay --> {} {}", model.getName(), msg); relay.send(msg); @@ -264,12 +306,14 @@ public class LiveJasminWebSocketDownload implements Download { @Override public void onClosed(WebSocket webSocket, int code, String reason) { LOG.trace("stream closed {} {} {}", code, reason, model.getName()); + stop(); } @Override public void onFailure(WebSocket webSocket, Throwable t, Response response) { if(!connectionClosed) { LOG.trace("stream failure {}", model.getName(), t); + stop(); if (response != null) { response.close(); } @@ -281,9 +325,14 @@ public class LiveJasminWebSocketDownload implements Download { @Override public void stop() { connectionClosed = true; - stream.close(1000, ""); - relay.close(1000, ""); + EventBusHolder.BUS.unregister(this); isAlive = false; + if (stream != null) { + stream.close(1000, ""); + } + if (relay != null) { + relay.close(1000, ""); + } } @Override From 196b82dfa3dc5382b696ec8db2aad6d630d838bb Mon Sep 17 00:00:00 2001 From: 0xboobface <0xboobface@gmail.com> Date: Wed, 9 Jan 2019 20:34:52 +0100 Subject: [PATCH 20/38] Consider empty recordings as too short --- common/src/main/java/ctbrec/recorder/LocalRecorder.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/common/src/main/java/ctbrec/recorder/LocalRecorder.java b/common/src/main/java/ctbrec/recorder/LocalRecorder.java index dfd501ec..d03b1b63 100644 --- a/common/src/main/java/ctbrec/recorder/LocalRecorder.java +++ b/common/src/main/java/ctbrec/recorder/LocalRecorder.java @@ -776,7 +776,7 @@ public class LocalRecorder implements Recorder { try { LOG.debug("Determining video length for {}", download.getTarget()); File target = download.getTarget(); - if(!target.exists()) { + if(!target.exists() || target.length() == 0) { return true; } From bf6b7156428e79a650851f93a453624ae432a6f1 Mon Sep 17 00:00:00 2001 From: 0xboobface <0xboobface@gmail.com> Date: Wed, 9 Jan 2019 20:35:31 +0100 Subject: [PATCH 21/38] Login every 30 min to LiveJasmin so that the session does not expire --- .../jasmin/LiveJasminElectronLoginDialog.java | 4 +- .../ui/sites/jasmin/LiveJasminSiteUi.java | 13 ++- .../sites/jasmin/LiveJasminUpdateService.java | 16 ++- .../ui/sites/jasmin/NotLoggedInExcetion.java | 5 + .../sites/jasmin/LiveJasminHttpClient.java | 106 ------------------ 5 files changed, 34 insertions(+), 110 deletions(-) create mode 100644 client/src/main/java/ctbrec/ui/sites/jasmin/NotLoggedInExcetion.java diff --git a/client/src/main/java/ctbrec/ui/sites/jasmin/LiveJasminElectronLoginDialog.java b/client/src/main/java/ctbrec/ui/sites/jasmin/LiveJasminElectronLoginDialog.java index 86879d9e..4efd2d23 100644 --- a/client/src/main/java/ctbrec/ui/sites/jasmin/LiveJasminElectronLoginDialog.java +++ b/client/src/main/java/ctbrec/ui/sites/jasmin/LiveJasminElectronLoginDialog.java @@ -24,7 +24,7 @@ public class LiveJasminElectronLoginDialog { private CookieJar cookieJar; private ExternalBrowser browser; - public LiveJasminElectronLoginDialog(CookieJar cookieJar) throws IOException { + public LiveJasminElectronLoginDialog(CookieJar cookieJar) throws Exception { this.cookieJar = cookieJar; browser = ExternalBrowser.getInstance(); try { @@ -37,6 +37,8 @@ public class LiveJasminElectronLoginDialog { browser.run(msg.toString(), msgHandler); } catch (InterruptedException e) { throw new IOException("Couldn't wait for login dialog", e); + } catch (IOException e) { + LOG.debug("Error while starting the browser or communication to it", e); } finally { browser.close(); browser.release(); diff --git a/client/src/main/java/ctbrec/ui/sites/jasmin/LiveJasminSiteUi.java b/client/src/main/java/ctbrec/ui/sites/jasmin/LiveJasminSiteUi.java index 94634972..e017e4aa 100644 --- a/client/src/main/java/ctbrec/ui/sites/jasmin/LiveJasminSiteUi.java +++ b/client/src/main/java/ctbrec/ui/sites/jasmin/LiveJasminSiteUi.java @@ -3,6 +3,7 @@ package ctbrec.ui.sites.jasmin; import java.io.IOException; import java.util.concurrent.BlockingQueue; import java.util.concurrent.LinkedBlockingQueue; +import java.util.concurrent.TimeUnit; import org.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -20,6 +21,7 @@ public class LiveJasminSiteUi implements SiteUI { private LiveJasmin liveJasmin; private LiveJasminTabProvider tabProvider; private LiveJasminConfigUi configUi; + private long lastLoginTime = 0; public LiveJasminSiteUi(LiveJasmin liveJasmin) { this.liveJasmin = liveJasmin; @@ -39,16 +41,23 @@ public class LiveJasminSiteUi implements SiteUI { @Override public synchronized boolean login() throws IOException { + // renew login every 30 min + long now = System.currentTimeMillis(); + boolean renew = false; + if((now - lastLoginTime) > TimeUnit.MINUTES.toMillis(30)) { + renew = true; + } + boolean automaticLogin = liveJasmin.login(); - if(automaticLogin) { + if(automaticLogin && !renew) { return true; } else { + lastLoginTime = System.currentTimeMillis(); BlockingQueue queue = new LinkedBlockingQueue<>(); new Thread (() -> { // login with external browser window try { - //LiveJasminElectronLoginDialog dialog = new LiveJasminElectronLoginDialog(liveJasmin.getHttpClient().getCookieJar()); } catch (Exception e1) { LOG.error("Error logging in with external browser", e1); diff --git a/client/src/main/java/ctbrec/ui/sites/jasmin/LiveJasminUpdateService.java b/client/src/main/java/ctbrec/ui/sites/jasmin/LiveJasminUpdateService.java index e19a3b3d..69017169 100644 --- a/client/src/main/java/ctbrec/ui/sites/jasmin/LiveJasminUpdateService.java +++ b/client/src/main/java/ctbrec/ui/sites/jasmin/LiveJasminUpdateService.java @@ -17,6 +17,8 @@ import ctbrec.io.HttpException; import ctbrec.sites.jasmin.LiveJasmin; import ctbrec.sites.jasmin.LiveJasminModel; import ctbrec.ui.PaginatedScheduledService; +import ctbrec.ui.SiteUI; +import ctbrec.ui.SiteUiFactory; import javafx.concurrent.Task; import okhttp3.Cookie; import okhttp3.HttpUrl; @@ -38,8 +40,11 @@ public class LiveJasminUpdateService extends PaginatedScheduledService { protected Task> createTask() { return new Task>() { @Override - public List call() throws IOException { + public List call() throws IOException, NotLoggedInExcetion { //String _url = url + ((page-1) * 36); // TODO find out how to switch pages + if(!SiteUiFactory.getUi(liveJasmin).login()) { + throw new NotLoggedInExcetion(); + } // sort by popularity CookieJarImpl cookieJar = liveJasmin.getHttpClient().getCookieJar(); @@ -60,6 +65,7 @@ public class LiveJasminUpdateService extends PaginatedScheduledService { .addHeader("X-Requested-With", "XMLHttpRequest") .build(); try (Response response = liveJasmin.getHttpClient().execute(request)) { + LOG.debug("Response {} {}", response.code(), response.message()); if (response.isSuccessful()) { String body = response.body().string(); List models = new ArrayList<>(); @@ -81,6 +87,14 @@ public class LiveJasminUpdateService extends PaginatedScheduledService { model.setOnlineState(LiveJasminModel.mapStatus(m.optInt("status"))); models.add(model); } + } else if(json.optString("error").equals("Please login.")) { + SiteUI siteUI = SiteUiFactory.getUi(liveJasmin); + if(siteUI.login()) { + return call(); + } else { + LOG.error("Request failed:\n{}", body); + throw new IOException("Response was not successful"); + } } else { LOG.error("Request failed:\n{}", body); throw new IOException("Response was not successful"); diff --git a/client/src/main/java/ctbrec/ui/sites/jasmin/NotLoggedInExcetion.java b/client/src/main/java/ctbrec/ui/sites/jasmin/NotLoggedInExcetion.java new file mode 100644 index 00000000..57b528b7 --- /dev/null +++ b/client/src/main/java/ctbrec/ui/sites/jasmin/NotLoggedInExcetion.java @@ -0,0 +1,5 @@ +package ctbrec.ui.sites.jasmin; + +public class NotLoggedInExcetion extends Exception { + +} diff --git a/common/src/main/java/ctbrec/sites/jasmin/LiveJasminHttpClient.java b/common/src/main/java/ctbrec/sites/jasmin/LiveJasminHttpClient.java index 1d495599..3991c3d8 100644 --- a/common/src/main/java/ctbrec/sites/jasmin/LiveJasminHttpClient.java +++ b/common/src/main/java/ctbrec/sites/jasmin/LiveJasminHttpClient.java @@ -28,52 +28,6 @@ public class LiveJasminHttpClient extends HttpClient { return true; } - // loadMainPage(); // to get initial cookies - // Cookie captchaCookie = new Cookie.Builder() - // .domain("livejasmin.com") - // .name("captchaRequired") - // .value("0") - // .build(); - // getCookieJar().saveFromResponse(HttpUrl.parse("https://livejasmin.com"), Collections.singletonList(captchaCookie)); - // getCookieJar().saveFromResponse(HttpUrl.parse("https://www.livejasmin.com"), Collections.singletonList(captchaCookie)); - // getCookieJar().saveFromResponse(HttpUrl.parse("https://m.livejasmin.com"), Collections.singletonList(captchaCookie)); - // Map formParams = getLoginFormParameters(); - // getCookieJar().saveFromResponse(HttpUrl.parse("https://livejasmin.com"), Collections.singletonList(captchaCookie)); - // getCookieJar().saveFromResponse(HttpUrl.parse("https://m.livejasmin.com"), Collections.singletonList(captchaCookie)); - // String action = formParams.get("action"); - // formParams.remove("action"); - // Builder formBuilder = new FormBody.Builder(); - // for (Entry param : formParams.entrySet()) { - // formBuilder.add(param.getKey(), param.getValue()); - // } - // formBuilder.add("username", Config.getInstance().getSettings().livejasminUsername); - // formBuilder.add("password", Config.getInstance().getSettings().livejasminPassword); - // FormBody form = formBuilder.build(); - // Buffer b = new Buffer(); - // form.writeTo(b); - // LOG.debug("Form: {}", b.readUtf8()); - // Map> cookies = getCookieJar().getCookies(); - // for (Entry> domain : cookies.entrySet()) { - // LOG.debug("{}", domain.getKey()); - // List cks = domain.getValue(); - // for (Cookie cookie : cks) { - // LOG.debug(" {}", cookie); - // } - // } - // Request request = new Request.Builder() - // .url(LiveJasmin.BASE_URL + action) - // .header("User-Agent", USER_AGENT) - // .header("Accept", "*/*") - // .header("Accept-Language", "en") - // .header("Referer", LiveJasmin.BASE_URL + "/en/girls/") - // .header("X-Requested-With", "XMLHttpRequest") - // .post(form) - // .build(); - // try(Response response = execute(request)) { - // System.out.println("login " + response.code() + " - " + response.message()); - // System.out.println("login " + response.body().string()); - // } - boolean cookiesWorked = checkLoginSuccess(); if (cookiesWorked) { loggedIn = true; @@ -84,67 +38,12 @@ public class LiveJasminHttpClient extends HttpClient { return false; } - // private void loadMainPage() throws IOException { - // Request request = new Request.Builder() - // .url(LiveJasmin.BASE_URL) - // .header("User-Agent", USER_AGENT) - // .build(); - // try(Response response = execute(request)) { - // } - // } - // - // private Map getLoginFormParameters() throws IOException { - // long ts = System.currentTimeMillis(); - // String url = LiveJasmin.BASE_URL + "/en/auth/overlay/get-login-block?_dc="+ts; - // Request request = new Request.Builder() - // .url(url) - // .addHeader("User-Agent", USER_AGENT) - // .addHeader("Accept", "application/json, text/javascript, */*") - // .addHeader("Accept-Language", "en") - // .addHeader("Referer", LiveJasmin.BASE_URL) - // .addHeader("X-Requested-With", "XMLHttpRequest") - // .build(); - // try(Response response = execute(request)) { - // if(response.isSuccessful()) { - // String body = response.body().string(); - // JSONObject json = new JSONObject(body); - // if(json.optBoolean("success")) { - // JSONObject data = json.getJSONObject("data"); - // String content = data.getString("content"); - // Map params = new HashMap<>(); - // Element form = HtmlParser.getTag(content, "form"); - // params.put("action", form.attr("action")); - // Elements hiddenInputs = HtmlParser.getTags(content, "input[type=hidden]"); - // for (Element input : hiddenInputs) { - // String name = input.attr("name"); - // String value = input.attr("value"); - // params.put(name, value); - // } - // params.put("keepmeloggedin", "1"); - // params.put("captcha", ""); - // params.remove("captcha_needed"); - // return params; - // } else { - // throw new IOException("Request was not successful: " + body); - // } - // } else { - // throw new HttpException(response.code(), response.message()); - // } - // } - // } - public boolean checkLoginSuccess() throws IOException { OkHttpClient temp = client.newBuilder() .followRedirects(false) .followSslRedirects(false) .build(); - // getCookieJar().getCookies().entrySet().forEach((e) -> { - // e.getValue().forEach(c -> { - // LOG.debug("LOGIN CHECK: Cookie {} {}={}", e.getKey(), c.name(), c.value()); - // }); - // }); - String url = "https://m.livejasmin.com/en/member/favourite/get-favourite-list?ajax=1"; Request request = new Request.Builder() .url(url) @@ -159,11 +58,6 @@ public class LiveJasminHttpClient extends HttpClient { if(response.isSuccessful()) { return true; } else { - // if(response.code() >= 300 && response.code() < 400) { - // for (String name : response.headers().names()) { - // LOG.debug("{}: {}", name, response.header(name)); - // } - // } return false; } } From f24959b76e5e529b8551efdeb78747dd1141477e Mon Sep 17 00:00:00 2001 From: 0xboobface <0xboobface@gmail.com> Date: Thu, 10 Jan 2019 14:22:26 +0100 Subject: [PATCH 22/38] Switch token data type to double LiveJasmin and maybe other sites use a more currency like token system --- .../src/main/java/ctbrec/ui/JavaFxModel.java | 2 +- .../main/java/ctbrec/ui/ThumbOverviewTab.java | 28 +++++++------- client/src/main/java/ctbrec/ui/TipDialog.java | 12 +++--- .../src/main/java/ctbrec/ui/TokenLabel.java | 23 +++++++----- common/src/main/java/ctbrec/Model.java | 2 +- .../java/ctbrec/recorder/RemoteRecorder.java | 6 +-- common/src/main/java/ctbrec/sites/Site.java | 2 +- .../java/ctbrec/sites/bonga/BongaCams.java | 4 +- .../ctbrec/sites/bonga/BongaCamsModel.java | 4 +- .../src/main/java/ctbrec/sites/cam4/Cam4.java | 2 +- .../ctbrec/sites/cam4/Cam4HttpClient.java | 2 +- .../java/ctbrec/sites/cam4/Cam4Model.java | 2 +- .../java/ctbrec/sites/camsoda/Camsoda.java | 4 +- .../ctbrec/sites/camsoda/CamsodaModel.java | 4 +- .../ctbrec/sites/chaturbate/Chaturbate.java | 4 +- .../sites/chaturbate/ChaturbateModel.java | 4 +- .../java/ctbrec/sites/jasmin/LiveJasmin.java | 37 +++++++++++++++++-- .../ctbrec/sites/jasmin/LiveJasminModel.java | 2 +- .../java/ctbrec/sites/mfc/MyFreeCams.java | 4 +- .../ctbrec/sites/mfc/MyFreeCamsModel.java | 4 +- .../ctbrec/sites/streamate/Streamate.java | 4 +- .../sites/streamate/StreamateModel.java | 24 ++++++------ 22 files changed, 107 insertions(+), 73 deletions(-) diff --git a/client/src/main/java/ctbrec/ui/JavaFxModel.java b/client/src/main/java/ctbrec/ui/JavaFxModel.java index f60ce99d..eafe3847 100644 --- a/client/src/main/java/ctbrec/ui/JavaFxModel.java +++ b/client/src/main/java/ctbrec/ui/JavaFxModel.java @@ -126,7 +126,7 @@ public class JavaFxModel implements Model { } @Override - public void receiveTip(int tokens) throws IOException { + public void receiveTip(Double tokens) throws IOException { SiteUiFactory.getUi(getSite()).login(); delegate.receiveTip(tokens); } diff --git a/client/src/main/java/ctbrec/ui/ThumbOverviewTab.java b/client/src/main/java/ctbrec/ui/ThumbOverviewTab.java index e07c6755..b727b66b 100644 --- a/client/src/main/java/ctbrec/ui/ThumbOverviewTab.java +++ b/client/src/main/java/ctbrec/ui/ThumbOverviewTab.java @@ -4,6 +4,7 @@ import static ctbrec.ui.controls.Dialogs.*; import java.io.IOException; import java.net.SocketTimeoutException; +import java.text.DecimalFormat; import java.util.ArrayList; import java.util.Collections; import java.util.Comparator; @@ -470,20 +471,19 @@ public class ThumbOverviewTab extends Tab implements TabSelectionListener { tipDialog.showAndWait(); String tipText = tipDialog.getResult(); if(tipText != null) { - if(tipText.matches("[1-9]\\d*")) { - int tokens = Integer.parseInt(tipText); - try { - SiteUiFactory.getUi(site).login(); - cell.getModel().receiveTip(tokens); - Map event = new HashMap<>(); - event.put("event", "tokens.sent"); - event.put("amount", tokens); - EventBusHolder.BUS.post(event); - } catch (Exception e1) { - LOG.error("An error occured while sending tip", e1); - showError("Couldn't send tip", "An error occured while sending tip:", e1); - } - } else { + DecimalFormat df = new DecimalFormat("0.##"); + try { + Number tokens = df.parse(tipText); + SiteUiFactory.getUi(site).login(); + cell.getModel().receiveTip(tokens.doubleValue()); + Map event = new HashMap<>(); + event.put("event", "tokens.sent"); + event.put("amount", tokens.doubleValue()); + EventBusHolder.BUS.post(event); + } catch (IOException ex) { + LOG.error("An error occured while sending tip", ex); + showError("Couldn't send tip", "An error occured while sending tip:", ex); + } catch (Exception ex) { showError("Couldn't send tip", "You entered an invalid amount of tokens", null); } } diff --git a/client/src/main/java/ctbrec/ui/TipDialog.java b/client/src/main/java/ctbrec/ui/TipDialog.java index 8aaefb93..d231e2f8 100644 --- a/client/src/main/java/ctbrec/ui/TipDialog.java +++ b/client/src/main/java/ctbrec/ui/TipDialog.java @@ -1,5 +1,6 @@ package ctbrec.ui; +import java.text.DecimalFormat; import java.util.Objects; import java.util.concurrent.ExecutionException; @@ -30,21 +31,21 @@ public class TipDialog extends TextInputDialog { } private void loadTokenBalance() { - Task task = new Task() { + Task task = new Task() { @Override - protected Integer call() throws Exception { + protected Double call() throws Exception { if (!Objects.equals(System.getenv("CTBREC_DEV"), "1")) { SiteUiFactory.getUi(site).login(); return site.getTokenBalance(); } else { - return 1_000_000; + return 1_000_000d; } } @Override protected void done() { try { - int tokens = get(); + double tokens = get(); Platform.runLater(() -> { if (tokens <= 0) { String msg = "Do you want to buy tokens now?\n\nIf you agree, "+site.getName()+" will open in a browser. " @@ -59,7 +60,8 @@ public class TipDialog extends TextInputDialog { } } else { getEditor().setDisable(false); - setHeaderText("Current token balance: " + tokens); + DecimalFormat df = new DecimalFormat("0.##"); + setHeaderText("Current token balance: " + df.format(tokens)); } }); } catch (InterruptedException | ExecutionException e) { diff --git a/client/src/main/java/ctbrec/ui/TokenLabel.java b/client/src/main/java/ctbrec/ui/TokenLabel.java index 2117c444..917dad12 100644 --- a/client/src/main/java/ctbrec/ui/TokenLabel.java +++ b/client/src/main/java/ctbrec/ui/TokenLabel.java @@ -1,5 +1,6 @@ package ctbrec.ui; +import java.text.DecimalFormat; import java.util.Map; import java.util.Objects; import java.util.concurrent.ExecutionException; @@ -19,7 +20,7 @@ import javafx.scene.control.Tooltip; public class TokenLabel extends Label { private static final transient Logger LOG = LoggerFactory.getLogger(TokenLabel.class); - private int tokens = -1; + private double tokens = -1; private Site site; public TokenLabel(Site site) { @@ -29,11 +30,10 @@ public class TokenLabel extends Label { @Subscribe public void tokensUpdates(Map e) { if (Objects.equals("tokens", e.get("event"))) { - tokens = (int) e.get("amount"); + tokens = (double) e.get("amount"); updateText(); } else if (Objects.equals("tokens.sent", e.get("event"))) { - int _tokens = (int) e.get("amount"); - tokens -= _tokens; + tokens -= (double) e.get("amount"); updateText(); } } @@ -45,31 +45,34 @@ public class TokenLabel extends Label { updateText(); } - public void update(int tokens) { + public void update(double tokens) { this.tokens = tokens; updateText(); } private void updateText() { - Platform.runLater(() -> setText("Tokens: " + tokens)); + Platform.runLater(() -> { + DecimalFormat df = new DecimalFormat("0.##"); + setText("Tokens: " + df.format(tokens)); + }); } public void loadBalance() { - Task task = new Task() { + Task task = new Task() { @Override - protected Integer call() throws Exception { + protected Double call() throws Exception { if (!Objects.equals(System.getenv("CTBREC_DEV"), "1")) { SiteUiFactory.getUi(site).login(); return site.getTokenBalance(); } else { - return 1_000_000; + return 1_000_000d; } } @Override protected void done() { try { - int tokens = get(); + double tokens = get(); update(tokens); } catch (InterruptedException | ExecutionException e) { LOG.error("Couldn't retrieve account balance", e); diff --git a/common/src/main/java/ctbrec/Model.java b/common/src/main/java/ctbrec/Model.java index 70ddee51..1569fff8 100644 --- a/common/src/main/java/ctbrec/Model.java +++ b/common/src/main/java/ctbrec/Model.java @@ -72,7 +72,7 @@ public interface Model extends Comparable { public void invalidateCacheEntries(); - public void receiveTip(int tokens) throws IOException; + public void receiveTip(Double tokens) throws IOException; /** * Determines the stream resolution for this model diff --git a/common/src/main/java/ctbrec/recorder/RemoteRecorder.java b/common/src/main/java/ctbrec/recorder/RemoteRecorder.java index 38d27d99..c8629a7f 100644 --- a/common/src/main/java/ctbrec/recorder/RemoteRecorder.java +++ b/common/src/main/java/ctbrec/recorder/RemoteRecorder.java @@ -453,7 +453,7 @@ public class RemoteRecorder implements Recorder { } @Override - public void receiveTip(int tokens) throws IOException { + public void receiveTip(Double tokens) throws IOException { } @Override @@ -532,8 +532,8 @@ public class RemoteRecorder implements Recorder { } @Override - public Integer getTokenBalance() throws IOException { - return 0; + public Double getTokenBalance() throws IOException { + return 0d; } @Override diff --git a/common/src/main/java/ctbrec/sites/Site.java b/common/src/main/java/ctbrec/sites/Site.java index 9225b52c..92a58087 100644 --- a/common/src/main/java/ctbrec/sites/Site.java +++ b/common/src/main/java/ctbrec/sites/Site.java @@ -14,7 +14,7 @@ public interface Site { public void setRecorder(Recorder recorder); public Recorder getRecorder(); public Model createModel(String name); - public Integer getTokenBalance() throws IOException; + public Double getTokenBalance() throws IOException; public String getBuyTokensLink(); public boolean login() throws IOException; public HttpClient getHttpClient(); diff --git a/common/src/main/java/ctbrec/sites/bonga/BongaCams.java b/common/src/main/java/ctbrec/sites/bonga/BongaCams.java index 52b2d084..ae3d9e0c 100644 --- a/common/src/main/java/ctbrec/sites/bonga/BongaCams.java +++ b/common/src/main/java/ctbrec/sites/bonga/BongaCams.java @@ -57,7 +57,7 @@ public class BongaCams extends AbstractSite { } @Override - public Integer getTokenBalance() throws IOException { + public Double getTokenBalance() throws IOException { int userId = ((BongaCamsHttpClient)getHttpClient()).getUserId(); String url = BongaCams.BASE_URL + "/tools/amf.php"; RequestBody body = new FormBody.Builder() @@ -78,7 +78,7 @@ public class BongaCams extends AbstractSite { JSONObject json = new JSONObject(response.body().string()); if(json.optString("status").equals("online")) { JSONObject userData = json.getJSONObject("userData"); - return userData.getInt("balance"); + return (double) userData.getInt("balance"); } else { throw new IOException("Request was not successful: " + json.toString(2)); } diff --git a/common/src/main/java/ctbrec/sites/bonga/BongaCamsModel.java b/common/src/main/java/ctbrec/sites/bonga/BongaCamsModel.java index bd891eba..a0eb8245 100644 --- a/common/src/main/java/ctbrec/sites/bonga/BongaCamsModel.java +++ b/common/src/main/java/ctbrec/sites/bonga/BongaCamsModel.java @@ -154,13 +154,13 @@ public class BongaCamsModel extends AbstractModel { } @Override - public void receiveTip(int tokens) throws IOException { + public void receiveTip(Double tokens) throws IOException { String url = BongaCams.BASE_URL + "/chat-ajax-amf-service?" + System.currentTimeMillis(); int userId = ((BongaCamsHttpClient)site.getHttpClient()).getUserId(); RequestBody body = new FormBody.Builder() .add("method", "tipModel") .add("args[]", getName()) - .add("args[]", Integer.toString(tokens)) + .add("args[]", Integer.toString(tokens.intValue())) .add("args[]", Integer.toString(userId)) .add("args[3]", "") .build(); diff --git a/common/src/main/java/ctbrec/sites/cam4/Cam4.java b/common/src/main/java/ctbrec/sites/cam4/Cam4.java index 62a1cea2..016f2cad 100644 --- a/common/src/main/java/ctbrec/sites/cam4/Cam4.java +++ b/common/src/main/java/ctbrec/sites/cam4/Cam4.java @@ -51,7 +51,7 @@ public class Cam4 extends AbstractSite { } @Override - public Integer getTokenBalance() throws IOException { + public Double getTokenBalance() throws IOException { if (!credentialsAvailable()) { throw new IOException("Not logged in"); } diff --git a/common/src/main/java/ctbrec/sites/cam4/Cam4HttpClient.java b/common/src/main/java/ctbrec/sites/cam4/Cam4HttpClient.java index a9d8abd7..90f9eb00 100644 --- a/common/src/main/java/ctbrec/sites/cam4/Cam4HttpClient.java +++ b/common/src/main/java/ctbrec/sites/cam4/Cam4HttpClient.java @@ -55,7 +55,7 @@ public class Cam4HttpClient extends HttpClient { } } - protected int getTokenBalance() throws IOException { + protected double getTokenBalance() throws IOException { if(!loggedIn) { login(); } diff --git a/common/src/main/java/ctbrec/sites/cam4/Cam4Model.java b/common/src/main/java/ctbrec/sites/cam4/Cam4Model.java index 0c40a424..580b287d 100644 --- a/common/src/main/java/ctbrec/sites/cam4/Cam4Model.java +++ b/common/src/main/java/ctbrec/sites/cam4/Cam4Model.java @@ -176,7 +176,7 @@ public class Cam4Model extends AbstractModel { } @Override - public void receiveTip(int tokens) throws IOException { + public void receiveTip(Double tokens) throws IOException { throw new RuntimeException("Not implemented for Cam4, yet"); } diff --git a/common/src/main/java/ctbrec/sites/camsoda/Camsoda.java b/common/src/main/java/ctbrec/sites/camsoda/Camsoda.java index 10a12117..316a9323 100644 --- a/common/src/main/java/ctbrec/sites/camsoda/Camsoda.java +++ b/common/src/main/java/ctbrec/sites/camsoda/Camsoda.java @@ -57,7 +57,7 @@ public class Camsoda extends AbstractSite { } @Override - public Integer getTokenBalance() throws IOException { + public Double getTokenBalance() throws IOException { if (!credentialsAvailable()) { throw new IOException("Account settings not available"); } @@ -71,7 +71,7 @@ public class Camsoda extends AbstractSite { if(json.has("user")) { JSONObject user = json.getJSONObject("user"); if(user.has("tokens")) { - return user.getInt("tokens"); + return (double) user.getInt("tokens"); } } } else { diff --git a/common/src/main/java/ctbrec/sites/camsoda/CamsodaModel.java b/common/src/main/java/ctbrec/sites/camsoda/CamsodaModel.java index 7c567fc6..71678751 100644 --- a/common/src/main/java/ctbrec/sites/camsoda/CamsodaModel.java +++ b/common/src/main/java/ctbrec/sites/camsoda/CamsodaModel.java @@ -179,13 +179,13 @@ public class CamsodaModel extends AbstractModel { } @Override - public void receiveTip(int tokens) throws IOException { + public void receiveTip(Double tokens) throws IOException { String csrfToken = ((CamsodaHttpClient)site.getHttpClient()).getCsrfToken(); String url = site.getBaseUrl() + "/api/v1/tip/" + getName(); if (!Objects.equals(System.getenv("CTBREC_DEV"), "1")) { LOG.debug("Sending tip {}", url); RequestBody body = new FormBody.Builder() - .add("amount", Integer.toString(tokens)) + .add("amount", Integer.toString(tokens.intValue())) .add("comment", "") .build(); Request request = new Request.Builder() diff --git a/common/src/main/java/ctbrec/sites/chaturbate/Chaturbate.java b/common/src/main/java/ctbrec/sites/chaturbate/Chaturbate.java index d31b7983..708def45 100644 --- a/common/src/main/java/ctbrec/sites/chaturbate/Chaturbate.java +++ b/common/src/main/java/ctbrec/sites/chaturbate/Chaturbate.java @@ -80,7 +80,7 @@ public class Chaturbate extends AbstractSite { } @Override - public Integer getTokenBalance() throws IOException { + public Double getTokenBalance() throws IOException { String username = Config.getInstance().getSettings().username; if (username == null || username.trim().isEmpty()) { throw new IOException("Not logged in"); @@ -93,7 +93,7 @@ public class Chaturbate extends AbstractSite { String profilePage = resp.body().string(); String tokenText = HtmlParser.getText(profilePage, "span.tokencount"); int tokens = Integer.parseInt(tokenText); - return tokens; + return (double) tokens; } else { throw new IOException("HTTP response: " + resp.code() + " - " + resp.message()); } diff --git a/common/src/main/java/ctbrec/sites/chaturbate/ChaturbateModel.java b/common/src/main/java/ctbrec/sites/chaturbate/ChaturbateModel.java index e8322b8c..1428c3fd 100644 --- a/common/src/main/java/ctbrec/sites/chaturbate/ChaturbateModel.java +++ b/common/src/main/java/ctbrec/sites/chaturbate/ChaturbateModel.java @@ -127,8 +127,8 @@ public class ChaturbateModel extends AbstractModel { } @Override - public void receiveTip(int tokens) throws IOException { - getChaturbate().sendTip(getName(), tokens); + public void receiveTip(Double tokens) throws IOException { + getChaturbate().sendTip(getName(), tokens.intValue()); } @Override diff --git a/common/src/main/java/ctbrec/sites/jasmin/LiveJasmin.java b/common/src/main/java/ctbrec/sites/jasmin/LiveJasmin.java index 1b3fd206..c37cd368 100644 --- a/common/src/main/java/ctbrec/sites/jasmin/LiveJasmin.java +++ b/common/src/main/java/ctbrec/sites/jasmin/LiveJasmin.java @@ -13,6 +13,7 @@ import org.jsoup.select.Elements; import ctbrec.Config; import ctbrec.Model; +import ctbrec.NotLoggedInExcetion; import ctbrec.io.HtmlParser; import ctbrec.io.HttpClient; import ctbrec.io.HttpException; @@ -51,9 +52,34 @@ public class LiveJasmin extends AbstractSite { } @Override - public Integer getTokenBalance() throws IOException { - // https://www.livejasmin.com/en/offline-surprise/get-member-balance?session=m15f0175aa97d2d3c64df8f9bf96e3d8e - return 0; + public Double getTokenBalance() throws IOException { + if(getLiveJasminHttpClient().login()) { + String sessionId = getLiveJasminHttpClient().getSessionId(); + String url = getBaseUrl() + "/en/offline-surprise/get-member-balance?session=" + sessionId; + Request request = new Request.Builder() + .url(url) + .addHeader("User-Agent", Config.getInstance().getSettings().httpUserAgent) + .addHeader("Accept", "*/*") + .addHeader("Accept-Language", "en") + .addHeader("Referer", getBaseUrl()) + .addHeader("X-Requested-With", "XMLHttpRequest") + .build(); + try (Response response = getHttpClient().execute(request)) { + if(response.isSuccessful()) { + String body = response.body().string(); + JSONObject json = new JSONObject(body); + if(json.optBoolean("success")) { + return json.optDouble("result"); + } else { + throw new IOException("Response was not successful: " + url + "\n" + body); + } + } else { + throw new HttpException(response.code(), response.message()); + } + } + } else { + throw new IOException(new NotLoggedInExcetion()); + } } @Override @@ -87,7 +113,7 @@ public class LiveJasmin extends AbstractSite { @Override public boolean supportsTips() { - return false; + return true; } @Override @@ -154,4 +180,7 @@ public class LiveJasmin extends AbstractSite { return !Config.getInstance().getSettings().livejasminUsername.isEmpty(); } + private LiveJasminHttpClient getLiveJasminHttpClient() { + return (LiveJasminHttpClient) httpClient; + } } diff --git a/common/src/main/java/ctbrec/sites/jasmin/LiveJasminModel.java b/common/src/main/java/ctbrec/sites/jasmin/LiveJasminModel.java index 8722c6f4..513a7a92 100644 --- a/common/src/main/java/ctbrec/sites/jasmin/LiveJasminModel.java +++ b/common/src/main/java/ctbrec/sites/jasmin/LiveJasminModel.java @@ -178,7 +178,7 @@ public class LiveJasminModel extends AbstractModel { } @Override - public void receiveTip(int tokens) throws IOException { + public void receiveTip(Double tokens) throws IOException { } @Override diff --git a/common/src/main/java/ctbrec/sites/mfc/MyFreeCams.java b/common/src/main/java/ctbrec/sites/mfc/MyFreeCams.java index 787fac8e..0bdd2926 100644 --- a/common/src/main/java/ctbrec/sites/mfc/MyFreeCams.java +++ b/common/src/main/java/ctbrec/sites/mfc/MyFreeCams.java @@ -59,14 +59,14 @@ public class MyFreeCams extends AbstractSite { } @Override - public Integer getTokenBalance() throws IOException { + public Double getTokenBalance() throws IOException { Request req = new Request.Builder().url(baseUrl + "/php/account.php?request=status").build(); try(Response response = getHttpClient().execute(req)) { if(response.isSuccessful()) { String content = response.body().string(); Elements tags = HtmlParser.getTags(content, "div.content > p > b"); String tokens = tags.get(2).text(); - return Integer.parseInt(tokens); + return Double.parseDouble(tokens); } else { throw new HttpException(response.code(), response.message()); } diff --git a/common/src/main/java/ctbrec/sites/mfc/MyFreeCamsModel.java b/common/src/main/java/ctbrec/sites/mfc/MyFreeCamsModel.java index 768ef5df..ae305785 100644 --- a/common/src/main/java/ctbrec/sites/mfc/MyFreeCamsModel.java +++ b/common/src/main/java/ctbrec/sites/mfc/MyFreeCamsModel.java @@ -160,7 +160,7 @@ public class MyFreeCamsModel extends AbstractModel { } @Override - public void receiveTip(int tokens) throws IOException { + public void receiveTip(Double tokens) throws IOException { String tipUrl = MyFreeCams.baseUrl + "/php/tip.php"; String initUrl = tipUrl + "?request=tip&username="+getName()+"&broadcaster_id="+getUid(); Request req = new Request.Builder().url(initUrl).build(); @@ -173,7 +173,7 @@ public class MyFreeCamsModel extends AbstractModel { RequestBody body = new FormBody.Builder() .add("token", token) .add("broadcaster_id", Integer.toString(uid)) - .add("tip_value", Integer.toString(tokens)) + .add("tip_value", Integer.toString(tokens.intValue())) .add("submit_tip", "1") .add("anonymous", "") .add("public", "1") diff --git a/common/src/main/java/ctbrec/sites/streamate/Streamate.java b/common/src/main/java/ctbrec/sites/streamate/Streamate.java index a86eb91b..7e77e786 100644 --- a/common/src/main/java/ctbrec/sites/streamate/Streamate.java +++ b/common/src/main/java/ctbrec/sites/streamate/Streamate.java @@ -57,7 +57,7 @@ public class Streamate extends AbstractSite { } @Override - public Integer getTokenBalance() throws IOException { + public Double getTokenBalance() throws IOException { // int userId = ((StreamateHttpClient)getHttpClient()).getUserId(); // String url = Streamate.BASE_URL + "/tools/amf.php"; // RequestBody body = new FormBody.Builder() @@ -86,7 +86,7 @@ public class Streamate extends AbstractSite { // throw new HttpException(response.code(), response.message()); // } // } - return 0; + return 0d; } @Override diff --git a/common/src/main/java/ctbrec/sites/streamate/StreamateModel.java b/common/src/main/java/ctbrec/sites/streamate/StreamateModel.java index 519bce2c..68c41911 100644 --- a/common/src/main/java/ctbrec/sites/streamate/StreamateModel.java +++ b/common/src/main/java/ctbrec/sites/streamate/StreamateModel.java @@ -132,7 +132,7 @@ public class StreamateModel extends AbstractModel { } @Override - public void receiveTip(int tokens) throws IOException { + public void receiveTip(Double tokens) throws IOException { /* Mt._giveGoldAjax = function(e, t) { var n = _t.getState(), @@ -180,17 +180,17 @@ public class StreamateModel extends AbstractModel { String url = "https://hybridclient.naiadsystems.com/api/v1/givegold/"; // this returns 404 at the moment. not sure if it's the wrong server, or if this is not used anymore RequestBody body = new FormBody.Builder() - .add("amt", Integer.toString(tokens)) // amount - .add("isprepopulated", "1") // ? - .add("modelname", getName()) // model's name - .add("nickname", nickname) // user's nickname - .add("performernickname", getName()) // model's name - .add("sakey", saKey) // sakey from login - .add("session", "") // is related to gold an private shows, for normal tips keep it empty - .add("smid", Long.toString(getId())) // model id - .add("streamid", getStreamId()) // id of the current stream - .add("userid", Long.toString(userId)) // user's id - .add("username", nickname) // user's nickname + .add("amt", Integer.toString(tokens.intValue())) // amount + .add("isprepopulated", "1") // ? + .add("modelname", getName()) // model's name + .add("nickname", nickname) // user's nickname + .add("performernickname", getName()) // model's name + .add("sakey", saKey) // sakey from login + .add("session", "") // is related to gold an private shows, for normal tips keep it empty + .add("smid", Long.toString(getId())) // model id + .add("streamid", getStreamId()) // id of the current stream + .add("userid", Long.toString(userId)) // user's id + .add("username", nickname) // user's nickname .build(); Buffer b = new Buffer(); body.writeTo(b); From 07e0eb005b6898cb38771df68fb63ba20ded0d68 Mon Sep 17 00:00:00 2001 From: 0xboobface <0xboobface@gmail.com> Date: Fri, 11 Jan 2019 15:35:01 +0100 Subject: [PATCH 23/38] Create recordings dir, if it does not exist in getRecordingsFileStore --- common/src/main/java/ctbrec/recorder/LocalRecorder.java | 3 +++ 1 file changed, 3 insertions(+) diff --git a/common/src/main/java/ctbrec/recorder/LocalRecorder.java b/common/src/main/java/ctbrec/recorder/LocalRecorder.java index d03b1b63..7c12e4be 100644 --- a/common/src/main/java/ctbrec/recorder/LocalRecorder.java +++ b/common/src/main/java/ctbrec/recorder/LocalRecorder.java @@ -733,6 +733,9 @@ public class LocalRecorder implements Recorder { private FileStore getRecordingsFileStore() throws IOException { File recordingsDir = new File(config.getSettings().recordingsDir); + if(!recordingsDir.exists()) { + Files.createDirectories(recordingsDir.toPath()); + } FileStore store = Files.getFileStore(recordingsDir.toPath()); return store; } From 9ceec96195421908cbdd659bb93dec145175d0a7 Mon Sep 17 00:00:00 2001 From: 0xboobface <0xboobface@gmail.com> Date: Sat, 12 Jan 2019 17:25:13 +0100 Subject: [PATCH 24/38] Make sure, the external browser lock gets released --- .../main/java/ctbrec/ui/ExternalBrowser.java | 46 ++++++++++--------- .../bonga/BongaCamsElectronLoginDialog.java | 1 - .../sites/cam4/Cam4ElectronLoginDialog.java | 1 - .../jasmin/LiveJasminElectronLoginDialog.java | 1 - 4 files changed, 24 insertions(+), 25 deletions(-) diff --git a/client/src/main/java/ctbrec/ui/ExternalBrowser.java b/client/src/main/java/ctbrec/ui/ExternalBrowser.java index 648014d7..b31fc313 100644 --- a/client/src/main/java/ctbrec/ui/ExternalBrowser.java +++ b/client/src/main/java/ctbrec/ui/ExternalBrowser.java @@ -17,6 +17,7 @@ import org.slf4j.LoggerFactory; import ctbrec.OS; import ctbrec.io.StreamRedirectThread; +// TODO implement proxy support public class ExternalBrowser implements AutoCloseable { private static final transient Logger LOG = LoggerFactory.getLogger(ExternalBrowser.class); private static final ExternalBrowser INSTANCE = new ExternalBrowser(); @@ -35,30 +36,35 @@ public class ExternalBrowser implements AutoCloseable { } public void run(String jsonConfig, Consumer messageListener) throws InterruptedException, IOException { + LOG.debug("Running browser with config {}", jsonConfig); lock.lock(); - stopped = false; - this.messageListener = messageListener; + try { + stopped = false; + this.messageListener = messageListener; - p = new ProcessBuilder(OS.getBrowserCommand()).start(); - new StreamRedirectThread(p.getInputStream(), System.err); - new StreamRedirectThread(p.getErrorStream(), System.err); - LOG.debug("Browser started"); + p = new ProcessBuilder(OS.getBrowserCommand()).start(); + new StreamRedirectThread(p.getInputStream(), System.err); + new StreamRedirectThread(p.getErrorStream(), System.err); + LOG.debug("Browser started"); - connectToRemoteControlSocket(); - LOG.debug("Connected to remote control server. Sending config {}", jsonConfig); + connectToRemoteControlSocket(); + LOG.debug("Connected to remote control server. Sending config {}", jsonConfig); - out.write(jsonConfig.getBytes("utf-8")); - out.write('\n'); - out.flush(); + out.write(jsonConfig.getBytes("utf-8")); + out.write('\n'); + out.flush(); - LOG.debug("Waiting for browser to terminate"); - p.waitFor(); - int exitValue = p.exitValue(); - p = null; - LOG.debug("Browser Process terminated with {}", exitValue); + LOG.debug("Waiting for browser to terminate"); + p.waitFor(); + int exitValue = p.exitValue(); + p = null; + LOG.debug("Browser Process terminated with {}", exitValue); + } finally { + lock.unlock(); + } } - private void connectToRemoteControlSocket() { + private void connectToRemoteControlSocket() throws IOException { for (int i = 0; i < 20; i++) { try { socket = new Socket("localhost", 3202); @@ -70,7 +76,7 @@ public class ExternalBrowser implements AutoCloseable { } catch (IOException e) { if(i == 19) { LOG.error("Connection to remote control socket failed", e); - return; + throw e; } } @@ -134,8 +140,4 @@ public class ExternalBrowser implements AutoCloseable { } } } - - public void release() { - lock.unlock(); - } } diff --git a/client/src/main/java/ctbrec/ui/sites/bonga/BongaCamsElectronLoginDialog.java b/client/src/main/java/ctbrec/ui/sites/bonga/BongaCamsElectronLoginDialog.java index 82df391b..73146f5f 100644 --- a/client/src/main/java/ctbrec/ui/sites/bonga/BongaCamsElectronLoginDialog.java +++ b/client/src/main/java/ctbrec/ui/sites/bonga/BongaCamsElectronLoginDialog.java @@ -43,7 +43,6 @@ public class BongaCamsElectronLoginDialog { throw new IOException("Couldn't wait for login dialog", e); } finally { browser.close(); - browser.release(); } } diff --git a/client/src/main/java/ctbrec/ui/sites/cam4/Cam4ElectronLoginDialog.java b/client/src/main/java/ctbrec/ui/sites/cam4/Cam4ElectronLoginDialog.java index 117849d0..5424e2ff 100644 --- a/client/src/main/java/ctbrec/ui/sites/cam4/Cam4ElectronLoginDialog.java +++ b/client/src/main/java/ctbrec/ui/sites/cam4/Cam4ElectronLoginDialog.java @@ -43,7 +43,6 @@ public class Cam4ElectronLoginDialog { throw new IOException("Couldn't wait for login dialog", e); } finally { browser.close(); - browser.release(); } } diff --git a/client/src/main/java/ctbrec/ui/sites/jasmin/LiveJasminElectronLoginDialog.java b/client/src/main/java/ctbrec/ui/sites/jasmin/LiveJasminElectronLoginDialog.java index 4efd2d23..b5191dd8 100644 --- a/client/src/main/java/ctbrec/ui/sites/jasmin/LiveJasminElectronLoginDialog.java +++ b/client/src/main/java/ctbrec/ui/sites/jasmin/LiveJasminElectronLoginDialog.java @@ -41,7 +41,6 @@ public class LiveJasminElectronLoginDialog { LOG.debug("Error while starting the browser or communication to it", e); } finally { browser.close(); - browser.release(); } } From d46e4be4509e8418bf9fdd28a2d7b42c062594a2 Mon Sep 17 00:00:00 2001 From: 0xboobface <0xboobface@gmail.com> Date: Sat, 12 Jan 2019 17:25:53 +0100 Subject: [PATCH 25/38] Introduced tab for LiveJasmin to force logins --- .../ctbrec/ui/sites/jasmin/LiveJasminTab.java | 54 +++++++++++++++++++ .../sites/jasmin/LiveJasminTabProvider.java | 2 +- 2 files changed, 55 insertions(+), 1 deletion(-) create mode 100644 client/src/main/java/ctbrec/ui/sites/jasmin/LiveJasminTab.java diff --git a/client/src/main/java/ctbrec/ui/sites/jasmin/LiveJasminTab.java b/client/src/main/java/ctbrec/ui/sites/jasmin/LiveJasminTab.java new file mode 100644 index 00000000..ddde8414 --- /dev/null +++ b/client/src/main/java/ctbrec/ui/sites/jasmin/LiveJasminTab.java @@ -0,0 +1,54 @@ +package ctbrec.ui.sites.jasmin; + +import ctbrec.sites.Site; +import ctbrec.ui.FollowedTab; +import ctbrec.ui.PaginatedScheduledService; +import ctbrec.ui.ThumbOverviewTab; +import javafx.concurrent.WorkerStateEvent; +import javafx.scene.Scene; +import javafx.scene.control.Label; +import javafx.scene.input.KeyCode; +import javafx.scene.input.KeyEvent; + +public class LiveJasminTab extends ThumbOverviewTab implements FollowedTab { + private Label status; + + public LiveJasminTab(String title, PaginatedScheduledService updateService, Site site) { + super(title, updateService, site); + status = new Label("Logging in..."); + grid.getChildren().add(status); + } + + @Override + protected void createGui() { + super.createGui(); + } + + @Override + protected void onSuccess() { + grid.getChildren().remove(status); + super.onSuccess(); + } + + @Override + protected void onFail(WorkerStateEvent event) { + status.setText("Login failed"); + super.onFail(event); + } + + @Override + public void selected() { + status.setText("Logging in..."); + super.selected(); + } + + public void setScene(Scene scene) { + scene.addEventFilter(KeyEvent.KEY_PRESSED, event -> { + if(this.isSelected()) { + if(event.getCode() == KeyCode.DELETE) { + follow(selectedThumbCells, false); + } + } + }); + } +} diff --git a/client/src/main/java/ctbrec/ui/sites/jasmin/LiveJasminTabProvider.java b/client/src/main/java/ctbrec/ui/sites/jasmin/LiveJasminTabProvider.java index ee135d5d..66fd05e8 100644 --- a/client/src/main/java/ctbrec/ui/sites/jasmin/LiveJasminTabProvider.java +++ b/client/src/main/java/ctbrec/ui/sites/jasmin/LiveJasminTabProvider.java @@ -42,7 +42,7 @@ public class LiveJasminTabProvider extends TabProvider { private ThumbOverviewTab createTab(String title, String url) { LiveJasminUpdateService s = new LiveJasminUpdateService(liveJasmin, url); - ThumbOverviewTab tab = new ThumbOverviewTab(title, s, liveJasmin); + LiveJasminTab tab = new LiveJasminTab(title, s, liveJasmin); tab.setRecorder(liveJasmin.getRecorder()); s.setPeriod(Duration.seconds(60)); return tab; From ffcdb847144d8e6ee56e4ee74e31572400dabe8a Mon Sep 17 00:00:00 2001 From: 0xboobface <0xboobface@gmail.com> Date: Tue, 15 Jan 2019 17:09:35 +0100 Subject: [PATCH 26/38] Disable tipping for the time being --- common/src/main/java/ctbrec/NotLoggedInExcetion.java | 5 +++++ common/src/main/java/ctbrec/sites/jasmin/LiveJasmin.java | 2 +- .../src/main/java/ctbrec/sites/jasmin/LiveJasminModel.java | 4 ++++ 3 files changed, 10 insertions(+), 1 deletion(-) create mode 100644 common/src/main/java/ctbrec/NotLoggedInExcetion.java diff --git a/common/src/main/java/ctbrec/NotLoggedInExcetion.java b/common/src/main/java/ctbrec/NotLoggedInExcetion.java new file mode 100644 index 00000000..f3f293b0 --- /dev/null +++ b/common/src/main/java/ctbrec/NotLoggedInExcetion.java @@ -0,0 +1,5 @@ +package ctbrec; + +public class NotLoggedInExcetion extends Exception { + +} diff --git a/common/src/main/java/ctbrec/sites/jasmin/LiveJasmin.java b/common/src/main/java/ctbrec/sites/jasmin/LiveJasmin.java index c37cd368..99d4a721 100644 --- a/common/src/main/java/ctbrec/sites/jasmin/LiveJasmin.java +++ b/common/src/main/java/ctbrec/sites/jasmin/LiveJasmin.java @@ -113,7 +113,7 @@ public class LiveJasmin extends AbstractSite { @Override public boolean supportsTips() { - return true; + return false; } @Override diff --git a/common/src/main/java/ctbrec/sites/jasmin/LiveJasminModel.java b/common/src/main/java/ctbrec/sites/jasmin/LiveJasminModel.java index 513a7a92..10846ee2 100644 --- a/common/src/main/java/ctbrec/sites/jasmin/LiveJasminModel.java +++ b/common/src/main/java/ctbrec/sites/jasmin/LiveJasminModel.java @@ -179,6 +179,10 @@ public class LiveJasminModel extends AbstractModel { @Override public void receiveTip(Double tokens) throws IOException { + // tips are send over the relay websocket, e.g: + // {"event":"call","funcName":"sendSurprise","data":[1,"SurpriseGirlFlower"]} + // response: + // {"event":"call","funcName":"startSurprise","userId":"xyz_hash_gibberish","data":[{"memberid":"userxyz","amount":1,"tipName":"SurpriseGirlFlower","err_desc":"OK","err_text":"OK"}]} } @Override From dd2b8041d700251bf9917227d171cbe91d0c077a Mon Sep 17 00:00:00 2001 From: 0xboobface <0xboobface@gmail.com> Date: Tue, 15 Jan 2019 17:10:56 +0100 Subject: [PATCH 27/38] Add some text to explain, that LiveJasmin is not fully functional --- .../sites/jasmin/LiveJasminFollowedTab.java | 8 +-- .../ctbrec/ui/sites/jasmin/LiveJasminTab.java | 56 ++++++++++++++++--- common/src/main/java/ctbrec/Settings.java | 1 + 3 files changed, 51 insertions(+), 14 deletions(-) diff --git a/client/src/main/java/ctbrec/ui/sites/jasmin/LiveJasminFollowedTab.java b/client/src/main/java/ctbrec/ui/sites/jasmin/LiveJasminFollowedTab.java index b04ac265..151be60e 100644 --- a/client/src/main/java/ctbrec/ui/sites/jasmin/LiveJasminFollowedTab.java +++ b/client/src/main/java/ctbrec/ui/sites/jasmin/LiveJasminFollowedTab.java @@ -2,24 +2,19 @@ package ctbrec.ui.sites.jasmin; import ctbrec.sites.jasmin.LiveJasmin; import ctbrec.ui.FollowedTab; -import ctbrec.ui.ThumbOverviewTab; import javafx.concurrent.WorkerStateEvent; import javafx.geometry.Insets; import javafx.scene.Scene; -import javafx.scene.control.Label; import javafx.scene.control.RadioButton; import javafx.scene.control.ToggleGroup; import javafx.scene.input.KeyCode; import javafx.scene.input.KeyEvent; import javafx.scene.layout.HBox; -public class LiveJasminFollowedTab extends ThumbOverviewTab implements FollowedTab { - private Label status; +public class LiveJasminFollowedTab extends LiveJasminTab implements FollowedTab { public LiveJasminFollowedTab(LiveJasmin liveJasmin) { super("Followed", new LiveJasminFollowedUpdateService(liveJasmin), liveJasmin); - status = new Label("Logging in..."); - grid.getChildren().add(status); } @Override @@ -64,6 +59,7 @@ public class LiveJasminFollowedTab extends ThumbOverviewTab implements FollowedT super.selected(); } + @Override public void setScene(Scene scene) { scene.addEventFilter(KeyEvent.KEY_PRESSED, event -> { if(this.isSelected()) { diff --git a/client/src/main/java/ctbrec/ui/sites/jasmin/LiveJasminTab.java b/client/src/main/java/ctbrec/ui/sites/jasmin/LiveJasminTab.java index ddde8414..04922b0b 100644 --- a/client/src/main/java/ctbrec/ui/sites/jasmin/LiveJasminTab.java +++ b/client/src/main/java/ctbrec/ui/sites/jasmin/LiveJasminTab.java @@ -1,22 +1,59 @@ package ctbrec.ui.sites.jasmin; +import java.io.IOException; + +import ctbrec.Config; import ctbrec.sites.Site; -import ctbrec.ui.FollowedTab; +import ctbrec.ui.DesktopIntegration; import ctbrec.ui.PaginatedScheduledService; import ctbrec.ui.ThumbOverviewTab; import javafx.concurrent.WorkerStateEvent; import javafx.scene.Scene; +import javafx.scene.control.Button; import javafx.scene.control.Label; import javafx.scene.input.KeyCode; import javafx.scene.input.KeyEvent; -public class LiveJasminTab extends ThumbOverviewTab implements FollowedTab { - private Label status; +public class LiveJasminTab extends ThumbOverviewTab { + protected Label status; + protected Button acknowledge = new Button("That's alright"); + private Button createAccount = new Button("Create Account"); + private boolean betaAcknowledged = Config.getInstance().getSettings().livejasminBetaAcknowledged; public LiveJasminTab(String title, PaginatedScheduledService updateService, Site site) { super(title, updateService, site); - status = new Label("Logging in..."); - grid.getChildren().add(status); + if(!betaAcknowledged) { + status = new Label("LiveJasmin is not fully functional. Live previews do not work.\n" + + "Starting the stream in the external player also only works for streams with SD quality.\n" + + "Recording works only in standalone mode. The server does not support it, because you have " + + "to be logged in.\nSo make sure, that you have an account and that you have entered your credentials " + + "on the settings tab."); + grid.getChildren().add(status); + grid.getChildren().add(acknowledge); + grid.getChildren().add(createAccount); + } else { + status = new Label("Logging in..."); + grid.getChildren().add(status); + } + + acknowledge.setOnAction(e -> { + betaAcknowledged = true; + Config.getInstance().getSettings().livejasminBetaAcknowledged = true; + try { + Config.getInstance().save(); + } catch (IOException e1) { + } + status.setText("Logging in..."); + grid.getChildren().remove(acknowledge); + grid.getChildren().remove(createAccount); + if(updateService != null) { + updateService.cancel(); + updateService.reset(); + updateService.restart(); + } + }); + + createAccount.setOnAction(e -> DesktopIntegration.open(site.getAffiliateLink())); } @Override @@ -26,8 +63,12 @@ public class LiveJasminTab extends ThumbOverviewTab implements FollowedTab { @Override protected void onSuccess() { - grid.getChildren().remove(status); - super.onSuccess(); + if(Config.getInstance().getSettings().livejasminBetaAcknowledged) { + grid.getChildren().remove(status); + grid.getChildren().remove(acknowledge); + grid.getChildren().remove(createAccount); + super.onSuccess(); + } } @Override @@ -38,7 +79,6 @@ public class LiveJasminTab extends ThumbOverviewTab implements FollowedTab { @Override public void selected() { - status.setText("Logging in..."); super.selected(); } diff --git a/common/src/main/java/ctbrec/Settings.java b/common/src/main/java/ctbrec/Settings.java index d849ff1c..c35eb421 100644 --- a/common/src/main/java/ctbrec/Settings.java +++ b/common/src/main/java/ctbrec/Settings.java @@ -65,6 +65,7 @@ public class Settings { public String cam4Password = ""; public String livejasminUsername = ""; public String livejasminPassword = ""; + public boolean livejasminBetaAcknowledged = false; public String streamateUsername = ""; public String streamatePassword = ""; public String lastDownloadDir = ""; From 3e4483aabdd3d69e60ac2bb6f1cd214b69fbd7bb Mon Sep 17 00:00:00 2001 From: 0xboobface <0xboobface@gmail.com> Date: Thu, 17 Jan 2019 10:53:37 +0100 Subject: [PATCH 28/38] Print out master playlist URL for LiveJasmin --- common/src/main/java/ctbrec/sites/jasmin/LiveJasminModel.java | 1 + 1 file changed, 1 insertion(+) diff --git a/common/src/main/java/ctbrec/sites/jasmin/LiveJasminModel.java b/common/src/main/java/ctbrec/sites/jasmin/LiveJasminModel.java index 10846ee2..44ccdd00 100644 --- a/common/src/main/java/ctbrec/sites/jasmin/LiveJasminModel.java +++ b/common/src/main/java/ctbrec/sites/jasmin/LiveJasminModel.java @@ -112,6 +112,7 @@ public class LiveJasminModel extends AbstractModel { @Override public List getStreamSources() throws IOException, ExecutionException, ParseException, PlaylistException { String masterUrl = getMasterPlaylistUrl(); + LOG.debug("Master playlist: {}", masterUrl); List streamSources = new ArrayList<>(); Request req = new Request.Builder().url(masterUrl).build(); try(Response response = site.getHttpClient().execute(req)) { From ceb65f0ff27907d72ec9c9ed10cd364ba68f6cfb Mon Sep 17 00:00:00 2001 From: 0xboobface <0xboobface@gmail.com> Date: Thu, 17 Jan 2019 12:20:29 +0100 Subject: [PATCH 29/38] Logging --- client/src/main/java/ctbrec/ui/controls/StreamPreview.java | 5 ----- 1 file changed, 5 deletions(-) diff --git a/client/src/main/java/ctbrec/ui/controls/StreamPreview.java b/client/src/main/java/ctbrec/ui/controls/StreamPreview.java index a7b5911f..122c4f58 100644 --- a/client/src/main/java/ctbrec/ui/controls/StreamPreview.java +++ b/client/src/main/java/ctbrec/ui/controls/StreamPreview.java @@ -3,7 +3,6 @@ package ctbrec.ui.controls; import java.io.InterruptedIOException; import java.util.Collections; import java.util.List; -import java.util.Optional; import java.util.concurrent.ExecutionException; import java.util.concurrent.ExecutorService; import java.util.concurrent.Executors; @@ -162,10 +161,6 @@ public class StreamPreview extends StackPane { private void onError(MediaPlayer videoPlayer) { LOG.error("Error while starting preview stream", videoPlayer.getError()); - Optional cause = Optional.ofNullable(videoPlayer).map(v -> v.getError()).map(e -> e.getCause()); - if(cause.isPresent()) { - LOG.error("Error while starting preview stream root cause:", cause.get()); - } showTestImage(); } From 8e0e8855d078ed4e04241b8442439fa771112ad3 Mon Sep 17 00:00:00 2001 From: 0xboobface <0xboobface@gmail.com> Date: Fri, 18 Jan 2019 15:29:14 +0100 Subject: [PATCH 30/38] Enable LiveJasmin for the server Since the HLS downloads suddenly work for LiveJasmin, we can enable it for the server, too. --- .../main/java/ctbrec/recorder/server/HttpServer.java | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/server/src/main/java/ctbrec/recorder/server/HttpServer.java b/server/src/main/java/ctbrec/recorder/server/HttpServer.java index 4262c6ff..8cc3b289 100644 --- a/server/src/main/java/ctbrec/recorder/server/HttpServer.java +++ b/server/src/main/java/ctbrec/recorder/server/HttpServer.java @@ -32,6 +32,7 @@ import ctbrec.sites.bonga.BongaCams; import ctbrec.sites.cam4.Cam4; import ctbrec.sites.camsoda.Camsoda; import ctbrec.sites.chaturbate.Chaturbate; +import ctbrec.sites.jasmin.LiveJasmin; import ctbrec.sites.mfc.MyFreeCams; import ctbrec.sites.streamate.Streamate; @@ -78,11 +79,12 @@ public class HttpServer { } private void createSites() { - sites.add(new Chaturbate()); - sites.add(new MyFreeCams()); - sites.add(new Camsoda()); - sites.add(new Cam4()); sites.add(new BongaCams()); + sites.add(new Cam4()); + sites.add(new Camsoda()); + sites.add(new Chaturbate()); + sites.add(new LiveJasmin()); + sites.add(new MyFreeCams()); sites.add(new Streamate()); } From f7ca2a1eab7bd97cedd6ae554b60737d5ef1d076 Mon Sep 17 00:00:00 2001 From: 0xboobface <0xboobface@gmail.com> Date: Fri, 18 Jan 2019 16:30:16 +0100 Subject: [PATCH 31/38] Changed LiveJasmin beta warning a bit Since the HLS downloads suddenly work the warning now only states, that you have to log in and that the previews don't work --- .../sites/jasmin/LiveJasminFollowedTab.java | 19 ------------------- .../ctbrec/ui/sites/jasmin/LiveJasminTab.java | 6 ++---- .../sites/jasmin/LiveJasminTabProvider.java | 2 +- 3 files changed, 3 insertions(+), 24 deletions(-) diff --git a/client/src/main/java/ctbrec/ui/sites/jasmin/LiveJasminFollowedTab.java b/client/src/main/java/ctbrec/ui/sites/jasmin/LiveJasminFollowedTab.java index 151be60e..d5c9cdc0 100644 --- a/client/src/main/java/ctbrec/ui/sites/jasmin/LiveJasminFollowedTab.java +++ b/client/src/main/java/ctbrec/ui/sites/jasmin/LiveJasminFollowedTab.java @@ -2,7 +2,6 @@ package ctbrec.ui.sites.jasmin; import ctbrec.sites.jasmin.LiveJasmin; import ctbrec.ui.FollowedTab; -import javafx.concurrent.WorkerStateEvent; import javafx.geometry.Insets; import javafx.scene.Scene; import javafx.scene.control.RadioButton; @@ -41,24 +40,6 @@ public class LiveJasminFollowedTab extends LiveJasminTab implements FollowedTab }); } - @Override - protected void onSuccess() { - grid.getChildren().remove(status); - super.onSuccess(); - } - - @Override - protected void onFail(WorkerStateEvent event) { - status.setText("Login failed"); - super.onFail(event); - } - - @Override - public void selected() { - status.setText("Logging in..."); - super.selected(); - } - @Override public void setScene(Scene scene) { scene.addEventFilter(KeyEvent.KEY_PRESSED, event -> { diff --git a/client/src/main/java/ctbrec/ui/sites/jasmin/LiveJasminTab.java b/client/src/main/java/ctbrec/ui/sites/jasmin/LiveJasminTab.java index 04922b0b..5957be34 100644 --- a/client/src/main/java/ctbrec/ui/sites/jasmin/LiveJasminTab.java +++ b/client/src/main/java/ctbrec/ui/sites/jasmin/LiveJasminTab.java @@ -24,10 +24,8 @@ public class LiveJasminTab extends ThumbOverviewTab { super(title, updateService, site); if(!betaAcknowledged) { status = new Label("LiveJasmin is not fully functional. Live previews do not work.\n" - + "Starting the stream in the external player also only works for streams with SD quality.\n" - + "Recording works only in standalone mode. The server does not support it, because you have " - + "to be logged in.\nSo make sure, that you have an account and that you have entered your credentials " - + "on the settings tab."); + + "Also make sure, that you have an account and that you have entered your credentials.\n" + + "Otherwise you might get errors."); grid.getChildren().add(status); grid.getChildren().add(acknowledge); grid.getChildren().add(createAccount); diff --git a/client/src/main/java/ctbrec/ui/sites/jasmin/LiveJasminTabProvider.java b/client/src/main/java/ctbrec/ui/sites/jasmin/LiveJasminTabProvider.java index 66fd05e8..a1e8e877 100644 --- a/client/src/main/java/ctbrec/ui/sites/jasmin/LiveJasminTabProvider.java +++ b/client/src/main/java/ctbrec/ui/sites/jasmin/LiveJasminTabProvider.java @@ -42,7 +42,7 @@ public class LiveJasminTabProvider extends TabProvider { private ThumbOverviewTab createTab(String title, String url) { LiveJasminUpdateService s = new LiveJasminUpdateService(liveJasmin, url); - LiveJasminTab tab = new LiveJasminTab(title, s, liveJasmin); + ThumbOverviewTab tab = new LiveJasminTab(title, s, liveJasmin); tab.setRecorder(liveJasmin.getRecorder()); s.setPeriod(Duration.seconds(60)); return tab; From df47f4ba91c73a06012627ee2ec76b0107b30ea6 Mon Sep 17 00:00:00 2001 From: 0xboobface <0xboobface@gmail.com> Date: Fri, 18 Jan 2019 16:31:32 +0100 Subject: [PATCH 32/38] Add specialized HLS server download for LiveJasmin The server-side HLS download has to refresh the master playlist URL like the merged HLS donwload --- .../sites/jasmin/LiveJasminHlsDownload.java | 48 +++++++++++++++++++ 1 file changed, 48 insertions(+) create mode 100644 common/src/main/java/ctbrec/sites/jasmin/LiveJasminHlsDownload.java diff --git a/common/src/main/java/ctbrec/sites/jasmin/LiveJasminHlsDownload.java b/common/src/main/java/ctbrec/sites/jasmin/LiveJasminHlsDownload.java new file mode 100644 index 00000000..31775753 --- /dev/null +++ b/common/src/main/java/ctbrec/sites/jasmin/LiveJasminHlsDownload.java @@ -0,0 +1,48 @@ +package ctbrec.sites.jasmin; + +import java.io.IOException; +import java.util.concurrent.ExecutionException; +import java.util.concurrent.TimeUnit; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import com.iheartradio.m3u8.ParseException; +import com.iheartradio.m3u8.PlaylistException; + +import ctbrec.io.HttpClient; +import ctbrec.recorder.download.HlsDownload; + +public class LiveJasminHlsDownload extends HlsDownload { + + private static final transient Logger LOG = LoggerFactory.getLogger(LiveJasminHlsDownload.class); + private long lastMasterPlaylistUpdate = 0; + private String segmentUrl; + + public LiveJasminHlsDownload(HttpClient client) { + super(client); + } + + @Override + protected SegmentPlaylist getNextSegments(String segments) throws IOException, ParseException, PlaylistException { + if(this.segmentUrl == null) { + this.segmentUrl = segments; + } + SegmentPlaylist playlist = super.getNextSegments(segmentUrl); + long now = System.currentTimeMillis(); + if( (now - lastMasterPlaylistUpdate) > TimeUnit.SECONDS.toMillis(60)) { + super.downloadThreadPool.submit(this::updatePlaylistUrl); + lastMasterPlaylistUpdate = now; + } + return playlist; + } + + private void updatePlaylistUrl() { + try { + LOG.debug("Updating segment playlist URL for {}", getModel()); + segmentUrl = getSegmentPlaylistUrl(getModel()); + } catch (IOException | ExecutionException | ParseException | PlaylistException e) { + LOG.error("Couldn't update segment playlist url. This might cause a premature download termination", e); + } + } +} From 86f086eb20d1732cbbe734f909e1fd688c68035c Mon Sep 17 00:00:00 2001 From: 0xboobface <0xboobface@gmail.com> Date: Fri, 18 Jan 2019 16:32:47 +0100 Subject: [PATCH 33/38] Create specialized HLS downloads for server and client --- .../ctbrec/sites/jasmin/LiveJasminModel.java | 48 ++++++++----------- 1 file changed, 21 insertions(+), 27 deletions(-) diff --git a/common/src/main/java/ctbrec/sites/jasmin/LiveJasminModel.java b/common/src/main/java/ctbrec/sites/jasmin/LiveJasminModel.java index 44ccdd00..4171e8c0 100644 --- a/common/src/main/java/ctbrec/sites/jasmin/LiveJasminModel.java +++ b/common/src/main/java/ctbrec/sites/jasmin/LiveJasminModel.java @@ -41,7 +41,7 @@ public class LiveJasminModel extends AbstractModel { @Override public boolean isOnline(boolean ignoreCache) throws IOException, ExecutionException, InterruptedException { - if(ignoreCache) { + if (ignoreCache) { loadModelInfo(); } return online; @@ -49,29 +49,29 @@ public class LiveJasminModel extends AbstractModel { protected void loadModelInfo() throws IOException { String url = "https://m.livejasmin.com/en/chat-html5/" + getName(); - Request req = new Request.Builder().url(url) - .header("User-Agent", "Mozilla/5.0 (iPhone; CPU OS 10_14 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/11.1.1 Mobile/14E304 Safari/605.1.15") + Request req = new Request.Builder().url(url).header("User-Agent", + "Mozilla/5.0 (iPhone; CPU OS 10_14 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/11.1.1 Mobile/14E304 Safari/605.1.15") .header("Accept", "application/json,*/*") .header("Accept-Language", "en") .header("Referer", getSite().getBaseUrl()) .header("X-Requested-With", "XMLHttpRequest") .build(); - try(Response response = getSite().getHttpClient().execute(req)) { - if(response.isSuccessful()) { + try (Response response = getSite().getHttpClient().execute(req)) { + if (response.isSuccessful()) { String body = response.body().string(); JSONObject json = new JSONObject(body); - //LOG.debug(json.toString(2)); - if(json.optBoolean("success")) { + // LOG.debug(json.toString(2)); + if (json.optBoolean("success")) { JSONObject data = json.getJSONObject("data"); JSONObject config = data.getJSONObject("config"); JSONObject chatRoom = config.getJSONObject("chatRoom"); setId(chatRoom.getString("p_id")); - if(chatRoom.has("profile_picture_url")) { + if (chatRoom.has("profile_picture_url")) { setPreview(chatRoom.getString("profile_picture_url")); } int status = chatRoom.optInt("status", -1); onlineState = mapStatus(status); - if(chatRoom.optInt("is_on_private", 0) == 1) { + if (chatRoom.optInt("is_on_private", 0) == 1) { onlineState = State.PRIVATE; } resolution = new int[2]; @@ -89,7 +89,7 @@ public class LiveJasminModel extends AbstractModel { } public static State mapStatus(int status) { - switch(status) { + switch (status) { case 0: return State.OFFLINE; case 1: @@ -115,8 +115,8 @@ public class LiveJasminModel extends AbstractModel { LOG.debug("Master playlist: {}", masterUrl); List streamSources = new ArrayList<>(); Request req = new Request.Builder().url(masterUrl).build(); - try(Response response = site.getHttpClient().execute(req)) { - if(response.isSuccessful()) { + 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(); @@ -161,7 +161,7 @@ public class LiveJasminModel extends AbstractModel { if (response.isSuccessful()) { String body = response.body().string(); JSONObject json = new JSONObject(body); - if(json.optBoolean("success")) { + if (json.optBoolean("success")) { JSONObject data = json.getJSONObject("data"); JSONObject hlsStream = data.getJSONObject("hls_stream"); return hlsStream.getString("url"); @@ -188,8 +188,8 @@ public class LiveJasminModel extends AbstractModel { @Override public int[] getStreamResolution(boolean failFast) throws ExecutionException { - if(resolution == null) { - if(failFast) { + if (resolution == null) { + if (failFast) { return new int[2]; } try { @@ -260,7 +260,7 @@ public class LiveJasminModel extends AbstractModel { @Override public void writeSiteSpecificData(JsonWriter writer) throws IOException { - if(id == null) { + if (id == null) { try { loadModelInfo(); } catch (IOException e) { @@ -276,16 +276,10 @@ public class LiveJasminModel extends AbstractModel { @Override public Download createDownload() { - // if(Config.getInstance().getSettings().livejasminSession.isEmpty()) { - // if(Config.isServerMode()) { - // return new HlsDownload(getSite().getHttpClient()); - // } else { - // return new LiveJasminMergedHlsDownload(getSite().getHttpClient()); - // } - // } else { - // return new LiveJasminWebSocketDownload(getSite().getHttpClient()); - // } - //return new LiveJasminChunkedHttpDownload(getSite().getHttpClient()); - return new LiveJasminWebSocketDownload(getSite().getHttpClient()); + if(Config.isServerMode()) { + return new LiveJasminHlsDownload(getSite().getHttpClient()); + } else { + return new LiveJasminMergedHlsDownload(getSite().getHttpClient()); + } } } From ce839ee222c2ffbd6e032cbc6e332157e59662b0 Mon Sep 17 00:00:00 2001 From: 0xboobface <0xboobface@gmail.com> Date: Fri, 18 Jan 2019 18:57:16 +0100 Subject: [PATCH 34/38] Implemented tipping for LiveJasmin Add new class, which opens a chat websocket (relay server) and sends a tip message --- .../java/ctbrec/sites/jasmin/LiveJasmin.java | 2 +- .../ctbrec/sites/jasmin/LiveJasminModel.java | 6 + .../jasmin/LiveJasminTippingWebSocket.java | 173 ++++++++++++++++++ 3 files changed, 180 insertions(+), 1 deletion(-) create mode 100644 common/src/main/java/ctbrec/sites/jasmin/LiveJasminTippingWebSocket.java diff --git a/common/src/main/java/ctbrec/sites/jasmin/LiveJasmin.java b/common/src/main/java/ctbrec/sites/jasmin/LiveJasmin.java index 99d4a721..c37cd368 100644 --- a/common/src/main/java/ctbrec/sites/jasmin/LiveJasmin.java +++ b/common/src/main/java/ctbrec/sites/jasmin/LiveJasmin.java @@ -113,7 +113,7 @@ public class LiveJasmin extends AbstractSite { @Override public boolean supportsTips() { - return false; + return true; } @Override diff --git a/common/src/main/java/ctbrec/sites/jasmin/LiveJasminModel.java b/common/src/main/java/ctbrec/sites/jasmin/LiveJasminModel.java index 4171e8c0..7c09cc58 100644 --- a/common/src/main/java/ctbrec/sites/jasmin/LiveJasminModel.java +++ b/common/src/main/java/ctbrec/sites/jasmin/LiveJasminModel.java @@ -184,6 +184,12 @@ public class LiveJasminModel extends AbstractModel { // {"event":"call","funcName":"sendSurprise","data":[1,"SurpriseGirlFlower"]} // response: // {"event":"call","funcName":"startSurprise","userId":"xyz_hash_gibberish","data":[{"memberid":"userxyz","amount":1,"tipName":"SurpriseGirlFlower","err_desc":"OK","err_text":"OK"}]} + LiveJasminTippingWebSocket tippingSocket = new LiveJasminTippingWebSocket(site.getHttpClient()); + try { + tippingSocket.sendTip(this, Config.getInstance(), tokens); + } catch (InterruptedException e) { + throw new IOException(e); + } } @Override diff --git a/common/src/main/java/ctbrec/sites/jasmin/LiveJasminTippingWebSocket.java b/common/src/main/java/ctbrec/sites/jasmin/LiveJasminTippingWebSocket.java new file mode 100644 index 00000000..305ebdfa --- /dev/null +++ b/common/src/main/java/ctbrec/sites/jasmin/LiveJasminTippingWebSocket.java @@ -0,0 +1,173 @@ +package ctbrec.sites.jasmin; + +import java.io.IOException; + +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 okhttp3.Request; +import okhttp3.Response; +import okhttp3.WebSocket; +import okhttp3.WebSocketListener; +import okio.ByteString; + +public class LiveJasminTippingWebSocket { + private static final transient Logger LOG = LoggerFactory.getLogger(LiveJasminTippingWebSocket.class); + + private String applicationId; + private String sessionId; + private String jsm2SessionId; + private String sb_ip; + private String sb_hash; + private String relayHost; + private String streamHost; + private String clientInstanceId = "01234567890123456789012345678901"; // TODO where to get or generate a random id? + private WebSocket relay; + private Throwable exception; + + private HttpClient client; + private Model model; + + public LiveJasminTippingWebSocket(HttpClient client) { + this.client = client; + } + + public void sendTip(Model model, Config config, double amount) throws IOException, InterruptedException { + this.model = model; + getPerformerDetails(model.getName()); + LOG.debug("appid: {}", applicationId); + LOG.debug("sessionid: {}",sessionId); + LOG.debug("jsm2sessionid: {}",jsm2SessionId); + LOG.debug("sb_ip: {}",sb_ip); + LOG.debug("sb_hash: {}",sb_hash); + LOG.debug("relay host: {}",relayHost); + LOG.debug("stream host: {}",streamHost); + LOG.debug("clientinstanceid {}",clientInstanceId); + + Request request = new Request.Builder() + .url("https://" + relayHost + "/") + .header("Origin", "https://www.livejasmin.com") + .header("User-Agent", "Mozilla/5.0 (X11; Linux x86_64; rv:63.0) Gecko/20100101 Firefox/63.0") + .header("Accept", "text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8") + .header("Accept-Language", "de,en-US;q=0.7,en;q=0.3") + .build(); + Object monitor = new Object(); + relay = client.newWebSocket(request, new WebSocketListener() { + @Override + public void onOpen(WebSocket webSocket, Response response) { + LOG.trace("relay open {}", model.getName()); + sendToRelay("{\"event\":\"register\",\"applicationId\":\"" + applicationId + + "\",\"connectionData\":{\"jasmin2App\":true,\"isMobileClient\":false,\"platform\":\"desktop\",\"chatID\":\"freechat\"," + + "\"sessionID\":\"" + sessionId + "\"," + "\"jsm2SessionId\":\"" + jsm2SessionId + "\",\"userType\":\"user\"," + "\"performerId\":\"" + + model + + "\",\"clientRevision\":\"\",\"proxyIP\":\"\",\"playerVer\":\"nanoPlayerVersion: 3.10.3 appCodeName: Mozilla appName: Netscape appVersion: 5.0 (X11) platform: Linux x86_64\",\"livejasminTvmember\":false,\"newApplet\":true,\"livefeedtype\":null,\"gravityCookieId\":\"\",\"passparam\":\"\",\"brandID\":\"jasmin\",\"cobrandId\":\"\",\"subbrand\":\"livejasmin\",\"siteName\":\"LiveJasmin\",\"siteUrl\":\"https://www.livejasmin.com\"," + + "\"clientInstanceId\":\"" + clientInstanceId + "\",\"armaVersion\":\"34.10.0\",\"isPassive\":false}}"); + response.close(); + } + + @Override + public void onMessage(WebSocket webSocket, String text) { + LOG.trace("relay <-- {} T{}", model.getName(), text); + JSONObject event = new JSONObject(text); + if (event.optString("event").equals("accept")) { + new Thread(() -> { + sendToRelay("{\"event\":\"connectSharedObject\",\"name\":\"data/chat_so\"}"); + }).start(); + } else if(event.optString("event").equals("call")) { + String func = event.optString("funcName"); + if (func.equals("setName")) { + LOG.debug("Entered chat -> Sending tip of {}", amount); + sendToRelay("{\"event\":\"call\",\"funcName\":\"sendSurprise\",\"data\":["+amount+",\"SurpriseGirlFlower\"]}"); + } else if (func.equals("startSurprise")) { + // {"event":"call","funcName":"startSurprise","userId":"xyz_hash_gibberish","data":[{"memberid":"userxyz","amount":1,"tipName":"SurpriseGirlFlower","err_desc":"OK","err_text":"OK"}]} + JSONArray dataArray = event.getJSONArray("data"); + JSONObject data = dataArray.getJSONObject(0); + String errText = data.optString("err_text"); + String errDesc = data.optString("err_desc"); + LOG.debug("Tip response {} - {}", errText, errDesc); + if(!errText.equalsIgnoreCase("OK")) { + exception = new IOException(errText + " - " + errDesc); + } + synchronized (monitor) { + monitor.notify(); + } + } + } + } + + @Override + public void onMessage(WebSocket webSocket, ByteString bytes) { + LOG.trace("relay <-- {} B{}", model.getName(), bytes.toString()); + } + + @Override + public void onClosed(WebSocket webSocket, int code, String reason) { + LOG.trace("relay closed {} {} {}", code, reason, model.getName()); + exception = new IOException("Socket closed by server - " + code + " " + reason); + synchronized (monitor) { + monitor.notify(); + } + } + + @Override + public void onFailure(WebSocket webSocket, Throwable t, Response response) { + exception = t; + synchronized (monitor) { + monitor.notify(); + } + } + }); + synchronized (monitor) { + monitor.wait(); + } + if(exception != null) { + throw new IOException(exception); + } + } + + private void sendToRelay(String msg) { + LOG.trace("relay --> {} {}", model.getName(), msg); + relay.send(msg); + } + + protected void getPerformerDetails(String name) throws IOException { + String url = "https://m.livejasmin.com/en/chat-html5/" + name; + Request req = new Request.Builder() + .url(url) + .header("User-Agent", "Mozilla/5.0 (iPhone; CPU OS 10_14 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/11.1.1 Mobile/14E304 Safari/605.1.15") + .header("Accept", "application/json,*/*") + .header("Accept-Language", "en") + .header("Referer", "https://www.livejasmin.com") + .header("X-Requested-With", "XMLHttpRequest") + .build(); + try (Response response = client.execute(req)) { + if (response.isSuccessful()) { + String body = response.body().string(); + JSONObject json = new JSONObject(body); + // System.out.println(json.toString(2)); + if (json.optBoolean("success")) { + JSONObject data = json.getJSONObject("data"); + JSONObject config = data.getJSONObject("config"); + JSONObject armageddonConfig = config.getJSONObject("armageddonConfig"); + JSONObject chatRoom = config.getJSONObject("chatRoom"); + sessionId = armageddonConfig.getString("sessionid"); + jsm2SessionId = armageddonConfig.getString("jsm2session"); + sb_hash = chatRoom.getString("sb_hash"); + sb_ip = chatRoom.getString("sb_ip"); + applicationId = "memberChat/jasmin" + name + sb_hash; + relayHost = "dss-relay-" + sb_ip.replace('.', '-') + ".dditscdn.com"; + streamHost = "dss-live-" + sb_ip.replace('.', '-') + ".dditscdn.com"; + } else { + throw new IOException("Response was not successful: " + body); + } + } else { + throw new IOException(response.code() + " - " + response.message()); + } + } + } +} From e2d3ef264eaa1cbf067055a973ca273bed0cf416 Mon Sep 17 00:00:00 2001 From: 0xboobface <0xboobface@gmail.com> Date: Sat, 19 Jan 2019 15:30:57 +0100 Subject: [PATCH 35/38] Add proxy support for the external browser --- .../main/java/ctbrec/ui/ExternalBrowser.java | 55 +++++++++++++++++-- .../bonga/BongaCamsElectronLoginDialog.java | 2 +- .../sites/cam4/Cam4ElectronLoginDialog.java | 2 +- .../jasmin/LiveJasminElectronLoginDialog.java | 2 +- 4 files changed, 53 insertions(+), 8 deletions(-) diff --git a/client/src/main/java/ctbrec/ui/ExternalBrowser.java b/client/src/main/java/ctbrec/ui/ExternalBrowser.java index b31fc313..79d84997 100644 --- a/client/src/main/java/ctbrec/ui/ExternalBrowser.java +++ b/client/src/main/java/ctbrec/ui/ExternalBrowser.java @@ -14,10 +14,11 @@ import org.json.JSONObject; import org.slf4j.Logger; import org.slf4j.LoggerFactory; +import ctbrec.Config; import ctbrec.OS; +import ctbrec.Settings.ProxyType; import ctbrec.io.StreamRedirectThread; -// TODO implement proxy support public class ExternalBrowser implements AutoCloseable { private static final transient Logger LOG = LoggerFactory.getLogger(ExternalBrowser.class); private static final ExternalBrowser INSTANCE = new ExternalBrowser(); @@ -35,22 +36,27 @@ public class ExternalBrowser implements AutoCloseable { return INSTANCE; } - public void run(String jsonConfig, Consumer messageListener) throws InterruptedException, IOException { + public void run(JSONObject jsonConfig, Consumer messageListener) throws InterruptedException, IOException { LOG.debug("Running browser with config {}", jsonConfig); lock.lock(); try { stopped = false; this.messageListener = messageListener; + addProxyConfig(jsonConfig.getJSONObject("config")); + p = new ProcessBuilder(OS.getBrowserCommand()).start(); new StreamRedirectThread(p.getInputStream(), System.err); new StreamRedirectThread(p.getErrorStream(), System.err); LOG.debug("Browser started"); connectToRemoteControlSocket(); - LOG.debug("Connected to remote control server. Sending config {}", jsonConfig); - - out.write(jsonConfig.getBytes("utf-8")); + if(LOG.isTraceEnabled()) { + LOG.debug("Connected to remote control server. Sending config {}", jsonConfig); + } else { + LOG.debug("Connected to remote control server. Sending config"); + } + out.write(jsonConfig.toString().getBytes("utf-8")); out.write('\n'); out.flush(); @@ -140,4 +146,43 @@ public class ExternalBrowser implements AutoCloseable { } } } + + private void addProxyConfig(JSONObject jsonConfig) { + ProxyType proxyType = Config.getInstance().getSettings().proxyType; + switch (proxyType) { + case HTTP: + JSONObject proxy = new JSONObject(); + proxy.put("address", + "http=" + Config.getInstance().getSettings().proxyHost + ':' + Config.getInstance().getSettings().proxyPort + + ";https=" + Config.getInstance().getSettings().proxyHost + ':' + Config.getInstance().getSettings().proxyPort); + if(Config.getInstance().getSettings().proxyUser != null && !Config.getInstance().getSettings().proxyUser.isEmpty()) { + String username = Config.getInstance().getSettings().proxyUser; + String password = Config.getInstance().getSettings().proxyPassword; + proxy.put("user", username); + proxy.put("password", password); + } + jsonConfig.put("proxy", proxy); + break; + case SOCKS4: + proxy = new JSONObject(); + proxy.put("address", "socks4://" + Config.getInstance().getSettings().proxyHost + ':' + Config.getInstance().getSettings().proxyPort); + jsonConfig.put("proxy", proxy); + break; + case SOCKS5: + proxy = new JSONObject(); + proxy.put("address", "socks5://" + Config.getInstance().getSettings().proxyHost + ':' + Config.getInstance().getSettings().proxyPort); + if(Config.getInstance().getSettings().proxyUser != null && !Config.getInstance().getSettings().proxyUser.isEmpty()) { + String username = Config.getInstance().getSettings().proxyUser; + String password = Config.getInstance().getSettings().proxyPassword; + proxy.put("user", username); + proxy.put("password", password); + } + jsonConfig.put("proxy", proxy); + break; + case DIRECT: + default: + // nothing to do here + break; + } + } } diff --git a/client/src/main/java/ctbrec/ui/sites/bonga/BongaCamsElectronLoginDialog.java b/client/src/main/java/ctbrec/ui/sites/bonga/BongaCamsElectronLoginDialog.java index 73146f5f..2ed49de3 100644 --- a/client/src/main/java/ctbrec/ui/sites/bonga/BongaCamsElectronLoginDialog.java +++ b/client/src/main/java/ctbrec/ui/sites/bonga/BongaCamsElectronLoginDialog.java @@ -38,7 +38,7 @@ public class BongaCamsElectronLoginDialog { config.put("h", 480); JSONObject msg = new JSONObject(); msg.put("config", config); - browser.run(msg.toString(), msgHandler); + browser.run(msg, msgHandler); } catch (InterruptedException e) { throw new IOException("Couldn't wait for login dialog", e); } finally { diff --git a/client/src/main/java/ctbrec/ui/sites/cam4/Cam4ElectronLoginDialog.java b/client/src/main/java/ctbrec/ui/sites/cam4/Cam4ElectronLoginDialog.java index 5424e2ff..6adc8f0d 100644 --- a/client/src/main/java/ctbrec/ui/sites/cam4/Cam4ElectronLoginDialog.java +++ b/client/src/main/java/ctbrec/ui/sites/cam4/Cam4ElectronLoginDialog.java @@ -38,7 +38,7 @@ public class Cam4ElectronLoginDialog { config.put("h", 640); JSONObject msg = new JSONObject(); msg.put("config", config); - browser.run(msg.toString(), msgHandler); + browser.run(msg, msgHandler); } catch (InterruptedException e) { throw new IOException("Couldn't wait for login dialog", e); } finally { diff --git a/client/src/main/java/ctbrec/ui/sites/jasmin/LiveJasminElectronLoginDialog.java b/client/src/main/java/ctbrec/ui/sites/jasmin/LiveJasminElectronLoginDialog.java index b5191dd8..9f260eb0 100644 --- a/client/src/main/java/ctbrec/ui/sites/jasmin/LiveJasminElectronLoginDialog.java +++ b/client/src/main/java/ctbrec/ui/sites/jasmin/LiveJasminElectronLoginDialog.java @@ -34,7 +34,7 @@ public class LiveJasminElectronLoginDialog { config.put("h", 720); JSONObject msg = new JSONObject(); msg.put("config", config); - browser.run(msg.toString(), msgHandler); + browser.run(msg, msgHandler); } catch (InterruptedException e) { throw new IOException("Couldn't wait for login dialog", e); } catch (IOException e) { From 1de0af5350b6b56735ced4f140afa2505abb2f0f Mon Sep 17 00:00:00 2001 From: 0xboobface <0xboobface@gmail.com> Date: Sat, 19 Jan 2019 15:59:31 +0100 Subject: [PATCH 36/38] Update changelog --- CHANGELOG.md | 16 ++++++++++++++++ 1 file changed, 16 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 773b637e..936a05e6 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,19 @@ +1.17.0 +======================== +* Added LiveJasmin + There are some issues, though: + * live previews don't work + * it's best to have an account and to be logged in, otherwise you might get + errors after some time + * the pagination and sorting of the models is random, because the + pagination LiveJasmin uses is quite obscure +* Added an electron based external browser component, which makes logins, which are + secured by Google's recaptcha, more reliable. This should also fix the login problems + with BongaCams (#58) +* Added a docker file for the server (thanks to bounty1342) +* Fixed Streamate favorites tab +* Added a setting for the thumbnail overview update interval + 1.16.0 ======================== * Thumbnails can show a live preview. Can be switched on in the settings. From 11f2a3ef8d047a5a1897dd9b0995519dd1621f06 Mon Sep 17 00:00:00 2001 From: 0xboobface <0xboobface@gmail.com> Date: Sat, 19 Jan 2019 16:06:50 +0100 Subject: [PATCH 37/38] Increase version to 1.17.0 --- client/pom.xml | 2 +- common/pom.xml | 2 +- master/pom.xml | 2 +- server/pom.xml | 2 +- 4 files changed, 4 insertions(+), 4 deletions(-) diff --git a/client/pom.xml b/client/pom.xml index c21fedba..0c1f7ca8 100644 --- a/client/pom.xml +++ b/client/pom.xml @@ -8,7 +8,7 @@ ctbrec master - 1.16.0 + 1.17.0 ../master diff --git a/common/pom.xml b/common/pom.xml index 8242591a..73f11d3b 100644 --- a/common/pom.xml +++ b/common/pom.xml @@ -8,7 +8,7 @@ ctbrec master - 1.16.0 + 1.17.0 ../master diff --git a/master/pom.xml b/master/pom.xml index 34ff1e30..5b7f7464 100644 --- a/master/pom.xml +++ b/master/pom.xml @@ -6,7 +6,7 @@ ctbrec master pom - 1.16.0 + 1.17.0 ../common diff --git a/server/pom.xml b/server/pom.xml index 4b71406f..6a6473ec 100644 --- a/server/pom.xml +++ b/server/pom.xml @@ -8,7 +8,7 @@ ctbrec master - 1.16.0 + 1.17.0 ../master From 43ef2bdf67e207aa6b6893bf28bc63029cadefa5 Mon Sep 17 00:00:00 2001 From: 0xboobface <0xboobface@gmail.com> Date: Sun, 20 Jan 2019 13:33:16 +0100 Subject: [PATCH 38/38] Add setting to disable playlist generation on the server Implementing #142. This has feature has the disadvantage, that the recording is always detected as still recording, because the finished state is assumed by the existence of the playlist file. --- common/src/main/java/ctbrec/Settings.java | 1 + common/src/main/java/ctbrec/recorder/LocalRecorder.java | 4 ++++ 2 files changed, 5 insertions(+) diff --git a/common/src/main/java/ctbrec/Settings.java b/common/src/main/java/ctbrec/Settings.java index c35eb421..9e155327 100644 --- a/common/src/main/java/ctbrec/Settings.java +++ b/common/src/main/java/ctbrec/Settings.java @@ -103,4 +103,5 @@ public class Settings { public String recordingsSortColumn = ""; public String recordingsSortType = ""; public double[] recordingsColumnWidths = new double[0]; + public boolean generatePlaylist = true; } diff --git a/common/src/main/java/ctbrec/recorder/LocalRecorder.java b/common/src/main/java/ctbrec/recorder/LocalRecorder.java index 7c12e4be..52b49b6f 100644 --- a/common/src/main/java/ctbrec/recorder/LocalRecorder.java +++ b/common/src/main/java/ctbrec/recorder/LocalRecorder.java @@ -398,6 +398,10 @@ public class LocalRecorder implements Recorder { } private void generatePlaylist(File recDir) { + if(!config.getSettings().generatePlaylist) { + return; + } + PlaylistGenerator playlistGenerator = new PlaylistGenerator(); playlistGenerators.put(recDir, playlistGenerator); try {