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