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.
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..0c1f7ca8 100644
--- a/client/pom.xml
+++ b/client/pom.xml
@@ -4,11 +4,11 @@
4.0.0
client
-
+
ctbrec
master
- 1.16.0
+ 1.17.0
../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 93b26cef..3d632a33 100644
--- a/client/src/main/java/ctbrec/ui/CamrecApplication.java
+++ b/client/src/main/java/ctbrec/ui/CamrecApplication.java
@@ -38,6 +38,7 @@ import ctbrec.sites.cam4.Cam4;
import ctbrec.sites.camsoda.Camsoda;
import ctbrec.sites.chaturbate.Chaturbate;
import ctbrec.sites.fc2live.Fc2Live;
+import ctbrec.sites.jasmin.LiveJasmin;
import ctbrec.sites.mfc.MyFreeCams;
import ctbrec.sites.streamate.Streamate;
import ctbrec.ui.settings.SettingsTab;
@@ -78,6 +79,7 @@ public class CamrecApplication extends Application {
sites.add(new Camsoda());
sites.add(new Chaturbate());
sites.add(new Fc2Live());
+ sites.add(new LiveJasmin());
sites.add(new MyFreeCams());
sites.add(new Streamate());
loadConfig();
@@ -196,6 +198,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..79d84997
--- /dev/null
+++ b/client/src/main/java/ctbrec/ui/ExternalBrowser.java
@@ -0,0 +1,188 @@
+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.Config;
+import ctbrec.OS;
+import ctbrec.Settings.ProxyType;
+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(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();
+ 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();
+
+ 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() throws IOException {
+ 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);
+ throw e;
+ }
+ }
+
+ // 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);
+ }
+ }
+ }
+
+ 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/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/SiteUiFactory.java b/client/src/main/java/ctbrec/ui/SiteUiFactory.java
index 1e0c01e6..aa2b5edd 100644
--- a/client/src/main/java/ctbrec/ui/SiteUiFactory.java
+++ b/client/src/main/java/ctbrec/ui/SiteUiFactory.java
@@ -6,6 +6,7 @@ import ctbrec.sites.cam4.Cam4;
import ctbrec.sites.camsoda.Camsoda;
import ctbrec.sites.chaturbate.Chaturbate;
import ctbrec.sites.fc2live.Fc2Live;
+import ctbrec.sites.jasmin.LiveJasmin;
import ctbrec.sites.mfc.MyFreeCams;
import ctbrec.sites.streamate.Streamate;
import ctbrec.ui.sites.bonga.BongaCamsSiteUi;
@@ -13,6 +14,7 @@ import ctbrec.ui.sites.cam4.Cam4SiteUi;
import ctbrec.ui.sites.camsoda.CamsodaSiteUi;
import ctbrec.ui.sites.chaturbate.ChaturbateSiteUi;
import ctbrec.ui.sites.fc2live.Fc2LiveUi;
+import ctbrec.ui.sites.jasmin.LiveJasminSiteUi;
import ctbrec.ui.sites.myfreecams.MyFreeCamsSiteUi;
import ctbrec.ui.sites.streamate.StreamateSiteUi;
@@ -23,6 +25,7 @@ public class SiteUiFactory {
private static CamsodaSiteUi camsodaSiteUi;
private static ChaturbateSiteUi ctbSiteUi;
private static Fc2LiveUi fc2SiteUi;
+ private static LiveJasminSiteUi jasminSiteUi;
private static MyFreeCamsSiteUi mfcSiteUi;
private static StreamateSiteUi streamateSiteUi;
@@ -62,6 +65,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/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/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();
}
}
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/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();
}
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..2ed49de3
--- /dev/null
+++ b/client/src/main/java/ctbrec/ui/sites/bonga/BongaCamsElectronLoginDialog.java
@@ -0,0 +1,123 @@
+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, msgHandler);
+ } catch (InterruptedException e) {
+ throw new IOException("Couldn't wait for login dialog", e);
+ } finally {
+ browser.close();
+ }
+ }
+
+ 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..d5453670 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,7 @@ 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;
+import ctbrec.ui.controls.Dialogs;
public class BongaCamsSiteUi implements SiteUI {
@@ -50,31 +44,26 @@ 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);
+ Dialogs.showError("Login error", "Couldn't login to " + bongaCams.getName(), 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 +76,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..6adc8f0d
--- /dev/null
+++ b/client/src/main/java/ctbrec/ui/sites/cam4/Cam4ElectronLoginDialog.java
@@ -0,0 +1,126 @@
+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, msgHandler);
+ } catch (InterruptedException e) {
+ throw new IOException("Couldn't wait for login dialog", e);
+ } finally {
+ browser.close();
+ }
+ }
+
+ 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..3d56eec3 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;
@@ -15,10 +12,8 @@ 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;
-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 +41,20 @@ 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);
+ Dialogs.showError("Login error", "Couldn't login to " + cam4.getName(), e1);
+ }
try {
queue.put(true);
@@ -66,16 +63,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 +76,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
new file mode 100644
index 00000000..f1c8c8db
--- /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);
+ // 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()));
+ 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(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/LiveJasminElectronLoginDialog.java b/client/src/main/java/ctbrec/ui/sites/jasmin/LiveJasminElectronLoginDialog.java
new file mode 100644
index 00000000..9f260eb0
--- /dev/null
+++ b/client/src/main/java/ctbrec/ui/sites/jasmin/LiveJasminElectronLoginDialog.java
@@ -0,0 +1,98 @@
+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 Exception {
+ 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, 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();
+ }
+ }
+
+ 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/LiveJasminFollowedTab.java b/client/src/main/java/ctbrec/ui/sites/jasmin/LiveJasminFollowedTab.java
new file mode 100644
index 00000000..d5c9cdc0
--- /dev/null
+++ b/client/src/main/java/ctbrec/ui/sites/jasmin/LiveJasminFollowedTab.java
@@ -0,0 +1,53 @@
+package ctbrec.ui.sites.jasmin;
+
+import ctbrec.sites.jasmin.LiveJasmin;
+import ctbrec.ui.FollowedTab;
+import javafx.geometry.Insets;
+import javafx.scene.Scene;
+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 LiveJasminTab implements FollowedTab {
+
+ public LiveJasminFollowedTab(LiveJasmin liveJasmin) {
+ super("Followed", new LiveJasminFollowedUpdateService(liveJasmin), liveJasmin);
+ }
+
+ @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
+ 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..a660344c
--- /dev/null
+++ b/client/src/main/java/ctbrec/ui/sites/jasmin/LiveJasminFollowedUpdateService.java
@@ -0,0 +1,96 @@
+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 ctbrec.ui.SiteUiFactory;
+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 {
+ 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()
+ .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/LiveJasminSiteUi.java b/client/src/main/java/ctbrec/ui/sites/jasmin/LiveJasminSiteUi.java
new file mode 100644
index 00000000..e017e4aa
--- /dev/null
+++ b/client/src/main/java/ctbrec/ui/sites/jasmin/LiveJasminSiteUi.java
@@ -0,0 +1,91 @@
+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;
+
+import ctbrec.sites.ConfigUI;
+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 {
+
+ private static final transient Logger LOG = LoggerFactory.getLogger(LiveJasminSiteUi.class);
+ private LiveJasmin liveJasmin;
+ private LiveJasminTabProvider tabProvider;
+ private LiveJasminConfigUi configUi;
+ private long lastLoginTime = 0;
+
+ public LiveJasminSiteUi(LiveJasmin liveJasmin) {
+ this.liveJasmin = liveJasmin;
+ tabProvider = new LiveJasminTabProvider(liveJasmin);
+ configUi = new LiveJasminConfigUi(liveJasmin);
+ }
+
+ @Override
+ public TabProvider getTabProvider() {
+ return tabProvider;
+ }
+
+ @Override
+ public ConfigUI getConfigUI() {
+ return configUi;
+ }
+
+ @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 && !renew) {
+ return true;
+ } else {
+ lastLoginTime = System.currentTimeMillis();
+ BlockingQueue queue = new LinkedBlockingQueue<>();
+
+ new Thread (() -> {
+ // login with external browser window
+ try {
+ new LiveJasminElectronLoginDialog(liveJasmin.getHttpClient().getCookieJar());
+ } catch (Exception e1) {
+ LOG.error("Error logging in with external browser", e1);
+ Dialogs.showError("Login error", "Couldn't login to " + liveJasmin.getName(), e1);
+ }
+
+ try {
+ queue.put(true);
+ } catch (InterruptedException e) {
+ LOG.error("Error while signaling termination", e);
+ }
+ }).start();
+
+ 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;
+ }
+ }
+}
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..5957be34
--- /dev/null
+++ b/client/src/main/java/ctbrec/ui/sites/jasmin/LiveJasminTab.java
@@ -0,0 +1,92 @@
+package ctbrec.ui.sites.jasmin;
+
+import java.io.IOException;
+
+import ctbrec.Config;
+import ctbrec.sites.Site;
+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 {
+ 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);
+ if(!betaAcknowledged) {
+ status = new Label("LiveJasmin is not fully functional. Live previews do not work.\n"
+ + "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);
+ } 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
+ protected void createGui() {
+ super.createGui();
+ }
+
+ @Override
+ protected void onSuccess() {
+ if(Config.getInstance().getSettings().livejasminBetaAcknowledged) {
+ grid.getChildren().remove(status);
+ grid.getChildren().remove(acknowledge);
+ grid.getChildren().remove(createAccount);
+ super.onSuccess();
+ }
+ }
+
+ @Override
+ protected void onFail(WorkerStateEvent event) {
+ status.setText("Login failed");
+ super.onFail(event);
+ }
+
+ @Override
+ public void selected() {
+ 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
new file mode 100644
index 00000000..a1e8e877
--- /dev/null
+++ b/client/src/main/java/ctbrec/ui/sites/jasmin/LiveJasminTabProvider.java
@@ -0,0 +1,50 @@
+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;
+import javafx.util.Duration;
+
+public class LiveJasminTabProvider extends TabProvider {
+
+ private LiveJasmin liveJasmin;
+ private LiveJasminFollowedTab followedTab;
+
+ public LiveJasminTabProvider(LiveJasmin liveJasmin) {
+ this.liveJasmin = liveJasmin;
+ }
+
+ @Override
+ public List getTabs(Scene scene) {
+ List tabs = new ArrayList<>();
+
+ 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());
+ tabs.add(followedTab);
+
+ return tabs;
+ }
+
+ @Override
+ public Tab getFollowedTab() {
+ return followedTab;
+ }
+
+ private ThumbOverviewTab createTab(String title, String url) {
+ LiveJasminUpdateService s = new LiveJasminUpdateService(liveJasmin, url);
+ ThumbOverviewTab tab = new LiveJasminTab(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
new file mode 100644
index 00000000..69017169
--- /dev/null
+++ b/client/src/main/java/ctbrec/ui/sites/jasmin/LiveJasminUpdateService.java
@@ -0,0 +1,110 @@
+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;
+import org.json.JSONObject;
+import org.slf4j.Logger;
+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 ctbrec.ui.SiteUI;
+import ctbrec.ui.SiteUiFactory;
+import javafx.concurrent.Task;
+import okhttp3.Cookie;
+import okhttp3.HttpUrl;
+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, 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();
+ Cookie sortCookie = new Cookie.Builder()
+ .domain("livejasmin.com")
+ .name("listPageOrderType")
+ .value("most_popular")
+ .build();
+ cookieJar.saveFromResponse(HttpUrl.parse("https://livejasmin.com"), Collections.singletonList(sortCookie));
+
+ 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() + "/en/girls/")
+ .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<>();
+ 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.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");
+ }
+ return models;
+ } else {
+ throw new HttpException(response.code(), response.message());
+ }
+ }
+ }
+ };
+ }
+}
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/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)
diff --git a/common/pom.xml b/common/pom.xml
index 4308d633..73f11d3b 100644
--- a/common/pom.xml
+++ b/common/pom.xml
@@ -8,7 +8,7 @@
ctbrec
master
- 1.16.0
+ 1.17.0
../master
@@ -57,7 +57,7 @@
org.openjfx
- javafx-web
+ javafx-media
provided
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/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/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 5aedbdb0..9e155327 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";
@@ -62,6 +63,9 @@ public class Settings {
public String camsodaPassword = "";
public String cam4Username = "";
public String cam4Password = "";
+ public String livejasminUsername = "";
+ public String livejasminPassword = "";
+ public boolean livejasminBetaAcknowledged = false;
public String streamateUsername = "";
public String streamatePassword = "";
public String lastDownloadDir = "";
@@ -99,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/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/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..52b49b6f 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
@@ -397,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 {
@@ -461,16 +466,17 @@ 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) {
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("/")) {
@@ -731,6 +737,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;
}
@@ -774,6 +783,10 @@ public class LocalRecorder implements Recorder {
try {
LOG.debug("Determining video length for {}", download.getTarget());
File target = download.getTarget();
+ if(!target.exists() || target.length() == 0) {
+ return true;
+ }
+
double duration = 0;
if(target.isDirectory()) {
File playlist = new File(target, "playlist.m3u8");
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/recorder/download/AbstractHlsDownload.java b/common/src/main/java/ctbrec/recorder/download/AbstractHlsDownload.java
index 20ab1f95..375a700f 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;
@@ -37,12 +40,13 @@ 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;
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/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/fc2live/Fc2Live.java b/common/src/main/java/ctbrec/sites/fc2live/Fc2Live.java
index 787d0801..a795b00e 100644
--- a/common/src/main/java/ctbrec/sites/fc2live/Fc2Live.java
+++ b/common/src/main/java/ctbrec/sites/fc2live/Fc2Live.java
@@ -35,8 +35,8 @@ public class Fc2Live extends AbstractSite {
}
@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/fc2live/Fc2Model.java b/common/src/main/java/ctbrec/sites/fc2live/Fc2Model.java
index f6a6975b..b5b106da 100644
--- a/common/src/main/java/ctbrec/sites/fc2live/Fc2Model.java
+++ b/common/src/main/java/ctbrec/sites/fc2live/Fc2Model.java
@@ -188,7 +188,7 @@ public class Fc2Model 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/fc2live/Fc2WebSocketClient.java b/common/src/main/java/ctbrec/sites/fc2live/Fc2WebSocketClient.java
index 90fc073e..7f332e9b 100644
--- a/common/src/main/java/ctbrec/sites/fc2live/Fc2WebSocketClient.java
+++ b/common/src/main/java/ctbrec/sites/fc2live/Fc2WebSocketClient.java
@@ -5,7 +5,9 @@ import org.json.JSONObject;
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;
@@ -26,7 +28,14 @@ public class Fc2WebSocketClient {
public String getPlaylistUrl() throws InterruptedException {
LOG.debug("Connecting to {}", url);
Object monitor = new Object();
- client.newWebSocket(url, new WebSocketListener() {
+
+ Request request = new Request.Builder()
+ .url(url)
+ .header("User-Agent", Config.getInstance().getSettings().httpUserAgent)
+ .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) {
response.close();
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..c37cd368
--- /dev/null
+++ b/common/src/main/java/ctbrec/sites/jasmin/LiveJasmin.java
@@ -0,0 +1,186 @@
+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.NotLoggedInExcetion;
+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 {
+
+ 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);
+ model.setUrl(getBaseUrl() + "/en/chat/" + name);
+ return model;
+ }
+
+ @Override
+ 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
+ public String getBuyTokensLink() {
+ return getAffiliateLink();
+ }
+
+ @Override
+ public boolean login() throws IOException {
+ return getHttpClient().login();
+ }
+
+ @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 true;
+ }
+
+ @Override
+ public boolean supportsFollow() {
+ 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;
+ }
+
+ @Override
+ public boolean credentialsAvailable() {
+ return !Config.getInstance().getSettings().livejasminUsername.isEmpty();
+ }
+
+ private LiveJasminHttpClient getLiveJasminHttpClient() {
+ return (LiveJasminHttpClient) httpClient;
+ }
+}
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/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);
+ }
+ }
+}
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..3991c3d8
--- /dev/null
+++ b/common/src/main/java/ctbrec/sites/jasmin/LiveJasminHttpClient.java
@@ -0,0 +1,74 @@
+package ctbrec.sites.jasmin;
+
+import java.io.IOException;
+import java.util.NoSuchElementException;
+
+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 synchronized boolean login() throws IOException {
+ if (loggedIn) {
+ return true;
+ }
+
+ boolean cookiesWorked = checkLoginSuccess();
+ if (cookiesWorked) {
+ loggedIn = true;
+ LOG.debug("Logged in with cookies");
+ return true;
+ }
+
+ return false;
+ }
+
+ public boolean checkLoginSuccess() throws IOException {
+ OkHttpClient temp = client.newBuilder()
+ .followRedirects(false)
+ .followSslRedirects(false)
+ .build();
+
+ 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().httpUserAgentMobile)
+ .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;
+ }
+ }
+ }
+
+ public String getSessionId() {
+ Cookie sessionCookie = getCookieJar().getCookie(HttpUrl.parse(LiveJasmin.BASE_URL), "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/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
new file mode 100644
index 00000000..7c09cc58
--- /dev/null
+++ b/common/src/main/java/ctbrec/sites/jasmin/LiveJasminModel.java
@@ -0,0 +1,291 @@
+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;
+
+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.Download;
+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;
+ private int[] resolution;
+
+ @Override
+ public boolean isOnline(boolean ignoreCache) throws IOException, ExecutionException, InterruptedException {
+ if (ignoreCache) {
+ 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());
+ }
+ }
+ }
+
+ public static 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);
+ 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();
+ 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()) {
+ 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 {
+ loadModelInfo();
+ 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);
+ if (json.optBoolean("success")) {
+ JSONObject data = json.getJSONObject("data");
+ JSONObject hlsStream = data.getJSONObject("hls_stream");
+ return hlsStream.getString("url");
+ } else {
+ throw new IOException("Response was not successful: " + url + "\n" + body);
+ }
+ } else {
+ throw new HttpException(response.code(), response.message());
+ }
+ }
+ }
+
+ @Override
+ public void invalidateCacheEntries() {
+ }
+
+ @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"}]}
+ LiveJasminTippingWebSocket tippingSocket = new LiveJasminTippingWebSocket(site.getHttpClient());
+ try {
+ tippingSocket.sendTip(this, Config.getInstance(), tokens);
+ } catch (InterruptedException e) {
+ throw new IOException(e);
+ }
+ }
+
+ @Override
+ public int[] getStreamResolution(boolean failFast) throws ExecutionException {
+ if (resolution == null) {
+ if (failFast) {
+ return new int[2];
+ }
+ try {
+ loadModelInfo();
+ } catch (IOException e) {
+ throw new ExecutionException(e);
+ }
+ return resolution;
+ } else {
+ return resolution;
+ }
+ }
+
+ @Override
+ public boolean follow() throws IOException {
+ return follow(true);
+ }
+
+ @Override
+ public boolean unfollow() throws IOException {
+ return follow(false);
+ }
+
+ private boolean follow(boolean follow) throws IOException {
+ 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() {
+ 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) {
+ 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;
+ }
+
+ @Override
+ public Download createDownload() {
+ if(Config.isServerMode()) {
+ return new LiveJasminHlsDownload(getSite().getHttpClient());
+ } else {
+ return new LiveJasminMergedHlsDownload(getSite().getHttpClient());
+ }
+ }
+}
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());
+ }
+ }
+ }
+}
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..f28c7252
--- /dev/null
+++ b/common/src/main/java/ctbrec/sites/jasmin/LiveJasminWebSocketDownload.java
@@ -0,0 +1,357 @@
+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 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;
+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);
+
+ EventBusHolder.BUS.register(this);
+
+ 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")) {
+ 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"));
+ } 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());
+ }
+ }
+
+ 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();
+ } 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());
+ }
+ }
+
+ 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());
+ 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();
+ }
+ }
+ }
+ });
+ }
+
+ @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);
+ }
+
+ 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());
+ 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();
+ }
+ }
+ }
+ });
+ }
+
+ @Override
+ public void stop() {
+ connectionClosed = true;
+ EventBusHolder.BUS.unregister(this);
+ isAlive = false;
+ if (stream != null) {
+ stream.close(1000, "");
+ }
+ if (relay != null) {
+ relay.close(1000, "");
+ }
+ }
+
+ @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/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);
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();
diff --git a/master/pom.xml b/master/pom.xml
index bc994b4a..5b7f7464 100644
--- a/master/pom.xml
+++ b/master/pom.xml
@@ -6,7 +6,7 @@
ctbrec
master
pom
- 1.16.0
+ 1.17.0
../common
@@ -85,6 +85,11 @@
javafx-web
11
+
+ org.openjfx
+ javafx-media
+ 11
+
com.google.guava
guava
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
+
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).
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
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
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());
}