forked from j62/ctbrec
1
0
Fork 0

Merge branch 'dev' into fc2

# Conflicts:
#	client/src/main/java/ctbrec/ui/CamrecApplication.java
#	client/src/main/java/ctbrec/ui/SiteUiFactory.java
This commit is contained in:
0xboobface 2019-01-20 13:51:04 +01:00
commit 55fc6729f8
81 changed files with 3057 additions and 718 deletions

View File

@ -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.

1
client/.gitignore vendored
View File

@ -6,3 +6,4 @@
/ctbrec-tunnel.sh
/jre/
/server-local.sh
/browser/

View File

@ -4,11 +4,11 @@
<modelVersion>4.0.0</modelVersion>
<artifactId>client</artifactId>
<parent>
<groupId>ctbrec</groupId>
<artifactId>master</artifactId>
<version>1.16.0</version>
<version>1.17.0</version>
<relativePath>../master</relativePath>
</parent>
@ -80,7 +80,7 @@
</dependency>
<dependency>
<groupId>org.openjfx</groupId>
<artifactId>javafx-web</artifactId>
<artifactId>javafx-media</artifactId>
</dependency>
</dependencies>

View File

@ -38,5 +38,13 @@
<outputDirectory>ctbrec/jre</outputDirectory>
<filtered>false</filtered>
</fileSet>
<fileSet>
<directory>browser/ctbrec-minimal-browser-linux-x64</directory>
<includes>
<include>**/*</include>
</includes>
<outputDirectory>ctbrec/browser</outputDirectory>
<filtered>false</filtered>
</fileSet>
</fileSets>
</assembly>

View File

@ -29,4 +29,14 @@
<outputDirectory>ctbrec</outputDirectory>
</file>
</files>
<fileSets>
<fileSet>
<directory>browser/ctbrec-minimal-browser-linux-x64</directory>
<includes>
<include>**/*</include>
</includes>
<outputDirectory>ctbrec/browser</outputDirectory>
<filtered>false</filtered>
</fileSet>
</fileSets>
</assembly>

View File

@ -38,5 +38,13 @@
<outputDirectory>ctbrec/jre</outputDirectory>
<filtered>false</filtered>
</fileSet>
<fileSet>
<directory>browser/ctbrec-minimal-browser-darwin-x64</directory>
<includes>
<include>**/*</include>
</includes>
<outputDirectory>ctbrec/browser</outputDirectory>
<filtered>false</filtered>
</fileSet>
</fileSets>
</assembly>

View File

@ -29,4 +29,14 @@
<outputDirectory>ctbrec</outputDirectory>
</file>
</files>
<fileSets>
<fileSet>
<directory>browser/ctbrec-minimal-browser-darwin-x64</directory>
<includes>
<include>**/*</include>
</includes>
<outputDirectory>ctbrec/browser</outputDirectory>
<filtered>false</filtered>
</fileSet>
</fileSets>
</assembly>

View File

@ -40,5 +40,13 @@
<outputDirectory>ctbrec/jre</outputDirectory>
<filtered>false</filtered>
</fileSet>
<fileSet>
<directory>browser/ctbrec-minimal-browser-win32-x64</directory>
<includes>
<include>**/*</include>
</includes>
<outputDirectory>ctbrec/browser</outputDirectory>
<filtered>false</filtered>
</fileSet>
</fileSets>
</assembly>

View File

@ -31,4 +31,14 @@
<outputDirectory>ctbrec</outputDirectory>
</file>
</files>
<fileSets>
<fileSet>
<directory>browser/ctbrec-minimal-browser-win32-x64</directory>
<includes>
<include>**/*</include>
</includes>
<outputDirectory>ctbrec/browser</outputDirectory>
<filtered>false</filtered>
</fileSet>
</fileSets>
</assembly>

View File

@ -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();
});

View File

@ -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);
}
}
}

View File

@ -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<String> 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<String> 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;
}
}
}

View File

@ -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);
}

View File

@ -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());
}

View File

@ -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<String, Object> 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<String, Object> 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);
}
}

View File

@ -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<Integer> task = new Task<Integer>() {
Task<Double> task = new Task<Double>() {
@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) {

View File

@ -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<String, Object> 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<Integer> task = new Task<Integer>() {
Task<Double> task = new Task<Double>() {
@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);

View File

@ -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();
}
}

View File

@ -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());
});
}
}

View File

@ -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<Throwable> 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();
}

View File

@ -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<String> 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);
}
}
}
};
}

View File

@ -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<HttpCookie> 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<HttpCookie> getCookies() {
// for (HttpCookie httpCookie : cookies) {
// LOG.debug("Cookie: {}", httpCookie);
// }
return cookies;
}
public String getUrl() {
return url;
}
}

View File

@ -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<Boolean> 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<Cookie> 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);
}
}

View File

@ -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<String> 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();
}
}

View File

@ -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<HttpCookie> 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<HttpCookie> getCookies() {
return cookies;
}
public String getUrl() {
return url;
}
}

View File

@ -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<Boolean> 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<Cookie> 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);
}
}

View File

@ -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<HttpCookie> 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<HttpCookie> 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; }";
}

View File

@ -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<Boolean> 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<Cookie> 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<Boolean> 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<Cookie> 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);
// }
}

View File

@ -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;
}
}

View File

@ -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<String> 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);
}
}
}
}
};
}

View File

@ -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);
}
}
});
}
}

View File

@ -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<List<Model>> createTask() {
return new Task<List<Model>>() {
@Override
public List<Model> 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<Model> 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;
}
}

View File

@ -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<Boolean> 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;
}
}
}

View File

@ -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);
}
}
});
}
}

View File

@ -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<Tab> getTabs(Scene scene) {
List<Tab> 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;
}
}

View File

@ -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<List<Model>> createTask() {
return new Task<List<Model>>() {
@Override
public List<Model> 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<Model> 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());
}
}
}
};
}
}

View File

@ -0,0 +1,5 @@
package ctbrec.ui.sites.jasmin;
public class NotLoggedInExcetion extends Exception {
}

View File

@ -47,7 +47,8 @@ public class StreamateFollowedService extends PaginatedScheduledService {
public List<Model> 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)

View File

@ -8,7 +8,7 @@
<parent>
<groupId>ctbrec</groupId>
<artifactId>master</artifactId>
<version>1.16.0</version>
<version>1.17.0</version>
<relativePath>../master</relativePath>
</parent>
@ -57,7 +57,7 @@
</dependency>
<dependency>
<groupId>org.openjfx</groupId>
<artifactId>javafx-web</artifactId>
<artifactId>javafx-media</artifactId>
<scope>provided</scope>
</dependency>
<dependency>

View File

@ -72,7 +72,7 @@ public interface Model extends Comparable<Model> {
public void invalidateCacheEntries();
public void receiveTip(int tokens) throws IOException;
public void receiveTip(Double tokens) throws IOException;
/**
* Determines the stream resolution for this model

View File

@ -0,0 +1,5 @@
package ctbrec;
public class NotLoggedInExcetion extends Exception {
}

View File

@ -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) {

View File

@ -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;
}

View File

@ -25,7 +25,7 @@ public class CookieJarImpl implements CookieJar {
@Override
public void saveFromResponse(HttpUrl url, List<Cookie> cookies) {
String host = getHost(url);
String host = getDomain(url);
List<Cookie> cookiesForUrl = cookieStore.get(host);
if (cookiesForUrl != null) {
cookiesForUrl = new ArrayList<Cookie>(cookiesForUrl); //unmodifiable
@ -52,7 +52,7 @@ public class CookieJarImpl implements CookieJar {
@Override
public List<Cookie> loadForRequest(HttpUrl url) {
String host = getHost(url);
String host = getDomain(url);
List<Cookie> 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

View File

@ -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);
}
}

View File

@ -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<Recording> listMergedRecordings() {
File recordingsDir = new File(config.getSettings().recordingsDir);
List<File> 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<Recording> 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");

View File

@ -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

View File

@ -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<Runnable> downloadQueue = new LinkedBlockingQueue<>(50);
protected ExecutorService downloadThreadPool = new ThreadPoolExecutor(5, 5, 2, TimeUnit.MINUTES, downloadQueue);
public AbstractHlsDownload(HttpClient client) {
this.client = client;

View File

@ -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<Runnable> downloadQueue = new LinkedBlockingQueue<>(50);
private ExecutorService downloadThreadPool = new ThreadPoolExecutor(5, 5, 2, TimeUnit.MINUTES, downloadQueue);
private FileChannel fileChannel = null;
private Object downloadFinished = new Object();

View File

@ -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();

View File

@ -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));
}

View File

@ -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();

View File

@ -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");
}

View File

@ -55,7 +55,7 @@ public class Cam4HttpClient extends HttpClient {
}
}
protected int getTokenBalance() throws IOException {
protected double getTokenBalance() throws IOException {
if(!loggedIn) {
login();
}

View File

@ -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");
}

View File

@ -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 {

View File

@ -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()

View File

@ -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());
}

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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();

View File

@ -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<Model> 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<Model> 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;
}
}

View File

@ -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;
}
}

View File

@ -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);
}
}
}

View File

@ -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");
}
}
}

View File

@ -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);
}
}
}

View File

@ -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<StreamSource> getStreamSources() throws IOException, ExecutionException, ParseException, PlaylistException {
String masterUrl = getMasterPlaylistUrl();
LOG.debug("Master playlist: {}", masterUrl);
List<StreamSource> 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());
}
}
}

View File

@ -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());
}
}
}
}

View File

@ -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;
}
}

View File

@ -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());
}

View File

@ -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")

View File

@ -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

View File

@ -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);

View File

@ -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();

View File

@ -6,7 +6,7 @@
<groupId>ctbrec</groupId>
<artifactId>master</artifactId>
<packaging>pom</packaging>
<version>1.16.0</version>
<version>1.17.0</version>
<modules>
<module>../common</module>
@ -85,6 +85,11 @@
<artifactId>javafx-web</artifactId>
<version>11</version>
</dependency>
<dependency>
<groupId>org.openjfx</groupId>
<artifactId>javafx-media</artifactId>
<version>11</version>
</dependency>
<dependency>
<groupId>com.google.guava</groupId>
<artifactId>guava</artifactId>

33
server/Dockerfile.txt Normal file
View File

@ -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

View File

@ -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).

View File

@ -0,0 +1,9 @@
version: 3
services:
ctbrec:
ports:
- '8080:8080'
volumes:
- 'ctbrec/config:/root/.config/ctbrec/'
- 'ctbrec/video:/root/ctbrec'
image: bounty1342/ctbrec

View File

@ -8,7 +8,7 @@
<parent>
<groupId>ctbrec</groupId>
<artifactId>master</artifactId>
<version>1.16.0</version>
<version>1.17.0</version>
<relativePath>../master</relativePath>
</parent>

View File

@ -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());
}