From 2425a9dc6059bf4d4517d7184add45d6b20e3902 Mon Sep 17 00:00:00 2001 From: 0xboobface <0xboobface@gmail.com> Date: Sat, 22 Dec 2018 19:44:45 +0100 Subject: [PATCH] Add websocket download This is the first version with working downloads for SD and HD. These dowloads only work, if you are logged in. So at the moment you have to set the session ID in the settings to make this work. The session ID can be copied from a valid session in a browser. --- .../ui/sites/jasmin/LiveJasminConfigUi.java | 103 ++++++ .../sites/jasmin/LiveJasminLoginDialog.java | 170 ++++++++++ .../ui/sites/jasmin/LiveJasminSiteUi.java | 93 +++++- .../sites/jasmin/LiveJasminUpdateService.java | 3 +- common/src/main/java/ctbrec/Settings.java | 3 + .../src/main/java/ctbrec/io/HttpClient.java | 22 +- .../java/ctbrec/recorder/LocalRecorder.java | 3 +- .../download/AbstractHlsDownload.java | 22 +- .../recorder/download/MergedHlsDownload.java | 6 - .../java/ctbrec/sites/jasmin/LiveJasmin.java | 3 +- .../sites/jasmin/LiveJasminHttpClient.java | 156 ++++++++- .../jasmin/LiveJasminMergedHlsDownload.java | 48 +++ .../ctbrec/sites/jasmin/LiveJasminModel.java | 110 ++++++- .../jasmin/LiveJasminWebSocketDownload.java | 308 ++++++++++++++++++ .../streamate/StreamateWebsocketClient.java | 7 +- 15 files changed, 1001 insertions(+), 56 deletions(-) create mode 100644 client/src/main/java/ctbrec/ui/sites/jasmin/LiveJasminConfigUi.java create mode 100644 client/src/main/java/ctbrec/ui/sites/jasmin/LiveJasminLoginDialog.java create mode 100644 common/src/main/java/ctbrec/sites/jasmin/LiveJasminMergedHlsDownload.java create mode 100644 common/src/main/java/ctbrec/sites/jasmin/LiveJasminWebSocketDownload.java diff --git a/client/src/main/java/ctbrec/ui/sites/jasmin/LiveJasminConfigUi.java b/client/src/main/java/ctbrec/ui/sites/jasmin/LiveJasminConfigUi.java new file mode 100644 index 00000000..2cf66c02 --- /dev/null +++ b/client/src/main/java/ctbrec/ui/sites/jasmin/LiveJasminConfigUi.java @@ -0,0 +1,103 @@ +package ctbrec.ui.sites.jasmin; + +import ctbrec.Config; +import ctbrec.Settings; +import ctbrec.sites.jasmin.LiveJasmin; +import ctbrec.ui.DesktopIntegration; +import ctbrec.ui.settings.SettingsTab; +import ctbrec.ui.sites.AbstractConfigUI; +import javafx.geometry.Insets; +import javafx.scene.Parent; +import javafx.scene.control.Button; +import javafx.scene.control.CheckBox; +import javafx.scene.control.Label; +import javafx.scene.control.PasswordField; +import javafx.scene.control.TextField; +import javafx.scene.layout.GridPane; +import javafx.scene.layout.Priority; + +public class LiveJasminConfigUi extends AbstractConfigUI { + private LiveJasmin liveJasmin; + + public LiveJasminConfigUi(LiveJasmin liveJasmin) { + this.liveJasmin = liveJasmin; + } + + @Override + public Parent createConfigPanel() { + Settings settings = Config.getInstance().getSettings(); + GridPane layout = SettingsTab.createGridLayout(); + + int row = 0; + Label l = new Label("Active"); + layout.add(l, 0, row); + CheckBox enabled = new CheckBox(); + enabled.setSelected(!settings.disabledSites.contains(liveJasmin.getName())); + enabled.setOnAction((e) -> { + if(enabled.isSelected()) { + settings.disabledSites.remove(liveJasmin.getName()); + } else { + settings.disabledSites.add(liveJasmin.getName()); + } + save(); + }); + GridPane.setMargin(enabled, new Insets(0, 0, 0, SettingsTab.CHECKBOX_MARGIN)); + layout.add(enabled, 1, row++); + + layout.add(new Label("LiveJasmin User"), 0, row); + TextField username = new TextField(Config.getInstance().getSettings().livejasminUsername); + username.textProperty().addListener((ob, o, n) -> { + if(!n.equals(Config.getInstance().getSettings().livejasminUsername)) { + Config.getInstance().getSettings().livejasminUsername = n; + liveJasmin.getHttpClient().logout(); + save(); + } + }); + GridPane.setFillWidth(username, true); + GridPane.setHgrow(username, Priority.ALWAYS); + GridPane.setColumnSpan(username, 2); + layout.add(username, 1, row++); + + layout.add(new Label("LiveJasmin Password"), 0, row); + PasswordField password = new PasswordField(); + password.setText(Config.getInstance().getSettings().livejasminPassword); + password.textProperty().addListener((ob, o, n) -> { + if(!n.equals(Config.getInstance().getSettings().livejasminPassword)) { + Config.getInstance().getSettings().livejasminPassword = n; + liveJasmin.getHttpClient().logout(); + save(); + } + }); + GridPane.setFillWidth(password, true); + GridPane.setHgrow(password, Priority.ALWAYS); + GridPane.setColumnSpan(password, 2); + layout.add(password, 1, row++); + + layout.add(new Label("LiveJasmin Session ID"), 0, row); + TextField sessionId = new TextField(); + sessionId.setText(Config.getInstance().getSettings().livejasminSession); + sessionId.textProperty().addListener((ob, o, n) -> { + if(!n.equals(Config.getInstance().getSettings().livejasminSession)) { + Config.getInstance().getSettings().livejasminSession = n; + save(); + } + }); + GridPane.setFillWidth(sessionId, true); + GridPane.setHgrow(sessionId, Priority.ALWAYS); + GridPane.setColumnSpan(sessionId, 2); + layout.add(sessionId, 1, row++); + + Button createAccount = new Button("Create new Account"); + createAccount.setOnAction((e) -> DesktopIntegration.open(liveJasmin.getAffiliateLink())); + layout.add(createAccount, 1, row++); + GridPane.setColumnSpan(createAccount, 2); + GridPane.setMargin(username, new Insets(0, 0, 0, SettingsTab.CHECKBOX_MARGIN)); + GridPane.setMargin(password, new Insets(0, 0, 0, SettingsTab.CHECKBOX_MARGIN)); + GridPane.setMargin(sessionId, new Insets(0, 0, 0, SettingsTab.CHECKBOX_MARGIN)); + GridPane.setMargin(createAccount, new Insets(0, 0, 0, SettingsTab.CHECKBOX_MARGIN)); + + username.setPrefWidth(300); + + return layout; + } +} diff --git a/client/src/main/java/ctbrec/ui/sites/jasmin/LiveJasminLoginDialog.java b/client/src/main/java/ctbrec/ui/sites/jasmin/LiveJasminLoginDialog.java new file mode 100644 index 00000000..f6071d54 --- /dev/null +++ b/client/src/main/java/ctbrec/ui/sites/jasmin/LiveJasminLoginDialog.java @@ -0,0 +1,170 @@ +package ctbrec.ui.sites.jasmin; + +import java.io.File; +import java.io.IOException; +import java.io.InputStream; +import java.net.CookieHandler; +import java.net.CookieManager; +import java.net.HttpCookie; +import java.net.MalformedURLException; +import java.net.URL; +import java.util.List; +import java.util.Objects; + +import org.json.JSONObject; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import ctbrec.Config; +import ctbrec.OS; +import ctbrec.io.HttpException; +import ctbrec.sites.jasmin.LiveJasmin; +import javafx.concurrent.Worker.State; +import javafx.scene.Scene; +import javafx.scene.control.ProgressIndicator; +import javafx.scene.image.Image; +import javafx.scene.layout.Region; +import javafx.scene.layout.StackPane; +import javafx.scene.web.WebEngine; +import javafx.scene.web.WebView; +import javafx.stage.Stage; +import okhttp3.Request; +import okhttp3.Response; + +public class LiveJasminLoginDialog { + + private static final transient Logger LOG = LoggerFactory.getLogger(LiveJasminLoginDialog.class); + public static final String URL = "https://m.livejasmin.com/en/list"; // #login-modal + private List cookies = null; + private String url; + private Region veil; + private ProgressIndicator p; + private LiveJasmin liveJasmin; + + public LiveJasminLoginDialog(LiveJasmin liveJasmin) throws IOException { + this.liveJasmin = liveJasmin; + Stage stage = new Stage(); + stage.setTitle("LiveJasmin Login"); + InputStream icon = getClass().getResourceAsStream("/icon.png"); + stage.getIcons().add(new Image(icon)); + CookieManager cookieManager = new CookieManager(); + CookieHandler.setDefault(cookieManager); + WebView webView = createWebView(stage); + + veil = new Region(); + veil.setStyle("-fx-background-color: rgba(0, 0, 0, 0.4)"); + p = new ProgressIndicator(); + p.setMaxSize(140, 140); + + StackPane stackPane = new StackPane(); + stackPane.getChildren().addAll(webView, veil, p); + + stage.setScene(new Scene(stackPane, 360, 480)); + stage.showAndWait(); + cookies = cookieManager.getCookieStore().getCookies(); + } + + private WebView createWebView(Stage stage) throws IOException { + + + WebView browser = new WebView(); + WebEngine webEngine = browser.getEngine(); + webEngine.setJavaScriptEnabled(true); + //webEngine.setUserAgent("Mozilla/5.0 (Android 9.0; Mobile; rv:63.0) Gecko/63.0 Firefox/63.0"); + webEngine.setUserAgent("Mozilla/5.0 (Mobile; rv:30.0) Gecko/20100101 Firefox/30.0"); + webEngine.locationProperty().addListener((obs, oldV, newV) -> { + try { + URL _url = new URL(newV); + if (Objects.equals(_url.getPath(), "/")) { + stage.close(); + } + } catch (MalformedURLException e) { + LOG.error("Couldn't parse new url {}", newV, e); + } + url = newV.toString(); + }); + webEngine.getLoadWorker().stateProperty().addListener((observable, oldState, newState) -> { + if (newState == State.SUCCEEDED) { + veil.setVisible(false); + p.setVisible(false); + // try { + // //webEngine.executeScript("$('#eighteen-plus-modal').hide();"); + // //webEngine.executeScript("$('body').html('"+loginForm+"');"); + // //webEngine.executeScript("$('#listpage').append('"+loginForm+"');"); + // // webEngine.executeScript("$('#main-menu-button').click();"); + // // webEngine.executeScript("$('#login-menu').click();"); + // String username = Config.getInstance().getSettings().livejasminUsername; + // if (username != null && !username.trim().isEmpty()) { + // webEngine.executeScript("$('#username').attr('value','" + username + "')"); + // } + // String password = Config.getInstance().getSettings().livejasminPassword; + // if (password != null && !password.trim().isEmpty()) { + // webEngine.executeScript("$('#password').attr('value','" + password + "')"); + // } + // } catch(Exception e) { + // LOG.warn("Couldn't auto fill username and password for LiveJasmin", e); + // } + } else if (newState == State.CANCELLED || newState == State.FAILED) { + veil.setVisible(false); + p.setVisible(false); + } + }); + webEngine.setUserDataDirectory(new File(OS.getConfigDir(), "webengine")); + webEngine.load(URL); + return browser; + } + + private String getLoginForm() throws IOException { + callBaseUrl(); // to get cookies + String url = "https://m.livejasmin.com/en/auth/window/get-login-window?isAjax=1"; + Request request = new Request.Builder() + .url(url) + .addHeader("User-Agent", "Mozilla/5.0 (Android 9.0; Mobile; rv:63.0) Gecko/63.0 Firefox/63.0") + .addHeader("Accept", "application/json, text/javascript, */*") + .addHeader("Accept-Language", "en") + .addHeader("Referer", LiveJasmin.BASE_URL) + .addHeader("X-Requested-With", "XMLHttpRequest") + .build(); + try(Response response = liveJasmin.getHttpClient().execute(request)) { + if(response.isSuccessful()) { + String body = response.body().string(); + JSONObject json = new JSONObject(body); + System.out.println(json.toString(2)); + if(json.optBoolean("success")) { + JSONObject data = json.getJSONObject("data"); + return data.getString("content"); + } else { + throw new IOException("Request was not successful: " + body); + } + } else { + throw new HttpException(response.code(), response.message()); + } + } + } + + private void callBaseUrl() throws IOException { + String url = liveJasmin.getBaseUrl(); + Request request = new Request.Builder() + .url(url) + .header("User-Agent", Config.getInstance().getSettings().httpUserAgent) + .build(); + try(Response response = liveJasmin.getHttpClient().execute(request)) { + if(response.isSuccessful()) { + + } else { + throw new HttpException(response.code(), response.message()); + } + } + } + + public List getCookies() { + for (HttpCookie httpCookie : cookies) { + LOG.debug("Cookie: {}", httpCookie); + } + return cookies; + } + + public String getUrl() { + return url; + } +} diff --git a/client/src/main/java/ctbrec/ui/sites/jasmin/LiveJasminSiteUi.java b/client/src/main/java/ctbrec/ui/sites/jasmin/LiveJasminSiteUi.java index cf09617d..f0115555 100644 --- a/client/src/main/java/ctbrec/ui/sites/jasmin/LiveJasminSiteUi.java +++ b/client/src/main/java/ctbrec/ui/sites/jasmin/LiveJasminSiteUi.java @@ -1,20 +1,39 @@ package ctbrec.ui.sites.jasmin; import java.io.IOException; +import java.net.HttpCookie; +import java.util.ArrayList; +import java.util.List; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; import ctbrec.sites.ConfigUI; import ctbrec.sites.jasmin.LiveJasmin; +import ctbrec.sites.jasmin.LiveJasminHttpClient; import ctbrec.ui.SiteUI; import ctbrec.ui.TabProvider; +import okhttp3.Cookie; +import okhttp3.CookieJar; +import okhttp3.HttpUrl; public class LiveJasminSiteUi implements SiteUI { + private static final transient Logger LOG = LoggerFactory.getLogger(LiveJasminSiteUi.class); private LiveJasmin liveJasmin; private LiveJasminTabProvider tabProvider; + private LiveJasminConfigUi configUi; public LiveJasminSiteUi(LiveJasmin liveJasmin) { this.liveJasmin = liveJasmin; tabProvider = new LiveJasminTabProvider(liveJasmin); + configUi = new LiveJasminConfigUi(liveJasmin); + try { + login(); + } catch (IOException e) { + // TODO Auto-generated catch block + e.printStackTrace(); + } } @Override @@ -24,12 +43,80 @@ public class LiveJasminSiteUi implements SiteUI { @Override public ConfigUI getConfigUI() { - return null; + return configUi; } @Override - public boolean login() throws IOException { - return liveJasmin.login(); + public synchronized boolean login() throws IOException { + boolean automaticLogin = liveJasmin.login(); + return automaticLogin; + // if(automaticLogin) { + // return true; + // } else { + // BlockingQueue queue = new LinkedBlockingQueue<>(); + // + // Runnable showDialog = () -> { + // // login with javafx WebView + // LiveJasminLoginDialog loginDialog; + // try { + // loginDialog = new LiveJasminLoginDialog(liveJasmin); + // // transfer cookies from WebView to OkHttp cookie jar + // transferCookies(loginDialog); + // } catch (IOException e1) { + // LOG.error("Couldn't load login dialog", e1); + // } + // + // try { + // queue.put(true); + // } catch (InterruptedException e) { + // LOG.error("Error while signaling termination", e); + // } + // }; + // + // if(Platform.isFxApplicationThread()) { + // showDialog.run(); + // } else { + // Platform.runLater(showDialog); + // try { + // queue.take(); + // } catch (InterruptedException e) { + // LOG.error("Error while waiting for login dialog to close", e); + // throw new IOException(e); + // } + // } + // + // LiveJasminHttpClient httpClient = (LiveJasminHttpClient)liveJasmin.getHttpClient(); + // boolean loggedIn = httpClient.checkLoginSuccess(); + // if(loggedIn) { + // LOG.info("Logged in."); + // } else { + // LOG.info("Login failed"); + // } + // return loggedIn; + // } } + private void transferCookies(LiveJasminLoginDialog loginDialog) { + LiveJasminHttpClient httpClient = (LiveJasminHttpClient)liveJasmin.getHttpClient(); + CookieJar cookieJar = httpClient.getCookieJar(); + + String[] urls = { + "https://www.livejasmin.com", + "http://www.livejasmin.com", + "https://m.livejasmin.com", + "http://m.livejasmin.com", + "https://livejasmin.com", + "http://livejasmin.com" + }; + + for (String u : urls) { + HttpUrl url = HttpUrl.parse(u); + List cookies = new ArrayList<>(); + for (HttpCookie webViewCookie : loginDialog.getCookies()) { + Cookie cookie = Cookie.parse(url, webViewCookie.toString()); + cookies.add(cookie); + } + cookieJar.saveFromResponse(url, cookies); + } + } } diff --git a/client/src/main/java/ctbrec/ui/sites/jasmin/LiveJasminUpdateService.java b/client/src/main/java/ctbrec/ui/sites/jasmin/LiveJasminUpdateService.java index b4e67a1f..b4d0257f 100644 --- a/client/src/main/java/ctbrec/ui/sites/jasmin/LiveJasminUpdateService.java +++ b/client/src/main/java/ctbrec/ui/sites/jasmin/LiveJasminUpdateService.java @@ -65,11 +65,12 @@ public class LiveJasminUpdateService extends PaginatedScheduledService { model.setId(m.getString("id")); model.setPreview(m.getString("profilePictureUrl")); model.setOnline(true); + model.setOnlineState(ctbrec.Model.State.ONLINE); models.add(model); } } else { LOG.error("Request failed:\n{}", body); - throw new IOException("Response was not successfull"); + throw new IOException("Response was not successful"); } return models; } else { diff --git a/common/src/main/java/ctbrec/Settings.java b/common/src/main/java/ctbrec/Settings.java index c96cf985..4d42f87d 100644 --- a/common/src/main/java/ctbrec/Settings.java +++ b/common/src/main/java/ctbrec/Settings.java @@ -62,6 +62,9 @@ public class Settings { public String camsodaPassword = ""; public String cam4Username = ""; public String cam4Password = ""; + public String livejasminUsername = ""; + public String livejasminPassword = ""; + public String livejasminSession = ""; public String streamateUsername = ""; public String streamatePassword = ""; public String lastDownloadDir = ""; diff --git a/common/src/main/java/ctbrec/io/HttpClient.java b/common/src/main/java/ctbrec/io/HttpClient.java index 5b2d8d9c..5da3d0b9 100644 --- a/common/src/main/java/ctbrec/io/HttpClient.java +++ b/common/src/main/java/ctbrec/io/HttpClient.java @@ -35,10 +35,10 @@ import okhttp3.WebSocketListener; public abstract class HttpClient { private static final transient Logger LOG = LoggerFactory.getLogger(HttpClient.class); - protected OkHttpClient client; + protected OkHttpClient client; protected CookieJarImpl cookieJar = new CookieJarImpl(); - protected boolean loggedIn = false; - protected int loginTries = 0; + protected boolean loggedIn = false; + protected int loginTries = 0; private String name; protected HttpClient(String name) { @@ -93,19 +93,7 @@ public abstract class HttpClient { } } - // public Response execute(Request request) throws IOException { - // Response resp = execute(request, false); - // return resp; - // } - - // public Response execute(Request req, boolean requiresLogin) throws IOException { public Response execute(Request req) throws IOException { - // if(requiresLogin && !loggedIn) { - // loggedIn = login(); - // if(!loggedIn) { - // throw new IOException("403 Unauthorized"); - // } - // } Response resp = client.newCall(req).execute(); return resp; } @@ -222,8 +210,8 @@ public abstract class HttpClient { loggedIn = false; } - public WebSocket newWebSocket(String url, WebSocketListener l) { - Request request = new Request.Builder().url(url).build(); + public WebSocket newWebSocket(Request request, WebSocketListener l) { + //Request request = new Request.Builder().url(url).build(); return client.newWebSocket(request, l); } } diff --git a/common/src/main/java/ctbrec/recorder/LocalRecorder.java b/common/src/main/java/ctbrec/recorder/LocalRecorder.java index 97982b94..3a1e46e9 100644 --- a/common/src/main/java/ctbrec/recorder/LocalRecorder.java +++ b/common/src/main/java/ctbrec/recorder/LocalRecorder.java @@ -193,6 +193,7 @@ public class LocalRecorder implements Recorder { LOG.debug("Starting recording for model {}", model.getName()); Download download = model.createDownload(); + LOG.debug("Downloading with {}", download.getClass().getSimpleName()); recordingProcesses.put(model, download); new Thread() { @Override @@ -461,7 +462,7 @@ public class LocalRecorder implements Recorder { private List listMergedRecordings() { File recordingsDir = new File(config.getSettings().recordingsDir); List possibleRecordings = new LinkedList<>(); - listRecursively(recordingsDir, possibleRecordings, (dir, name) -> name.matches(".*?_\\d{4}-\\d{2}-\\d{2}_\\d{2}-\\d{2}\\.ts")); + listRecursively(recordingsDir, possibleRecordings, (dir, name) -> name.matches(".*?_\\d{4}-\\d{2}-\\d{2}_\\d{2}-\\d{2}\\.(ts|mp4)")); SimpleDateFormat sdf = new SimpleDateFormat(DATE_FORMAT); List recordings = new ArrayList<>(); for (File ts: possibleRecordings) { diff --git a/common/src/main/java/ctbrec/recorder/download/AbstractHlsDownload.java b/common/src/main/java/ctbrec/recorder/download/AbstractHlsDownload.java index b4ab0507..604138c8 100644 --- a/common/src/main/java/ctbrec/recorder/download/AbstractHlsDownload.java +++ b/common/src/main/java/ctbrec/recorder/download/AbstractHlsDownload.java @@ -8,9 +8,12 @@ import java.util.ArrayList; import java.util.Collections; import java.util.Iterator; import java.util.List; +import java.util.concurrent.BlockingQueue; import java.util.concurrent.ExecutionException; import java.util.concurrent.ExecutorService; -import java.util.concurrent.Executors; +import java.util.concurrent.LinkedBlockingQueue; +import java.util.concurrent.ThreadPoolExecutor; +import java.util.concurrent.TimeUnit; import org.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -36,18 +39,19 @@ public abstract class AbstractHlsDownload implements Download { private static final transient Logger LOG = LoggerFactory.getLogger(AbstractHlsDownload.class); - ExecutorService downloadThreadPool = Executors.newFixedThreadPool(5); - HttpClient client; - volatile boolean running = false; - volatile boolean alive = true; - Instant startTime; - Model model; + protected HttpClient client; + protected volatile boolean running = false; + protected volatile boolean alive = true; + protected Instant startTime; + protected Model model; + protected BlockingQueue downloadQueue = new LinkedBlockingQueue<>(50); + protected ExecutorService downloadThreadPool = new ThreadPoolExecutor(5, 5, 2, TimeUnit.MINUTES, downloadQueue); public AbstractHlsDownload(HttpClient client) { this.client = client; } - SegmentPlaylist getNextSegments(String segments) throws IOException, ParseException, PlaylistException { + protected SegmentPlaylist getNextSegments(String segments) throws IOException, ParseException, PlaylistException { URL segmentsUrl = new URL(segments); Request request = new Request.Builder().url(segmentsUrl).addHeader("connection", "keep-alive").build(); try(Response response = client.execute(request)) { @@ -85,7 +89,7 @@ public abstract class AbstractHlsDownload implements Download { } - String getSegmentPlaylistUrl(Model model) throws IOException, ExecutionException, ParseException, PlaylistException { + protected String getSegmentPlaylistUrl(Model model) throws IOException, ExecutionException, ParseException, PlaylistException { LOG.debug("{} stream idx: {}", model.getName(), model.getStreamUrlIndex()); List streamSources = model.getStreamSources(); Collections.sort(streamSources); diff --git a/common/src/main/java/ctbrec/recorder/download/MergedHlsDownload.java b/common/src/main/java/ctbrec/recorder/download/MergedHlsDownload.java index 958fae17..4625cf36 100644 --- a/common/src/main/java/ctbrec/recorder/download/MergedHlsDownload.java +++ b/common/src/main/java/ctbrec/recorder/download/MergedHlsDownload.java @@ -22,13 +22,9 @@ import java.time.ZonedDateTime; import java.util.LinkedList; import java.util.Optional; import java.util.Queue; -import java.util.concurrent.BlockingQueue; import java.util.concurrent.Callable; import java.util.concurrent.ExecutionException; -import java.util.concurrent.ExecutorService; import java.util.concurrent.Future; -import java.util.concurrent.LinkedBlockingQueue; -import java.util.concurrent.ThreadPoolExecutor; import java.util.concurrent.TimeUnit; import org.slf4j.Logger; @@ -63,8 +59,6 @@ public class MergedHlsDownload extends AbstractHlsDownload { private ZonedDateTime splitRecStartTime; private Config config; private File targetFile; - private BlockingQueue downloadQueue = new LinkedBlockingQueue<>(50); - private ExecutorService downloadThreadPool = new ThreadPoolExecutor(5, 5, 2, TimeUnit.MINUTES, downloadQueue); private FileChannel fileChannel = null; private Object downloadFinished = new Object(); diff --git a/common/src/main/java/ctbrec/sites/jasmin/LiveJasmin.java b/common/src/main/java/ctbrec/sites/jasmin/LiveJasmin.java index f9885f4f..fa3e8651 100644 --- a/common/src/main/java/ctbrec/sites/jasmin/LiveJasmin.java +++ b/common/src/main/java/ctbrec/sites/jasmin/LiveJasmin.java @@ -32,6 +32,7 @@ public class LiveJasmin extends AbstractSite { model.setName(name); model.setDescription(""); model.setSite(this); + model.setUrl(getBaseUrl() + "/en/chat/" + name); return model; } @@ -47,7 +48,7 @@ public class LiveJasmin extends AbstractSite { @Override public boolean login() throws IOException { - return false; + return getHttpClient().login(); } @Override diff --git a/common/src/main/java/ctbrec/sites/jasmin/LiveJasminHttpClient.java b/common/src/main/java/ctbrec/sites/jasmin/LiveJasminHttpClient.java index f853fa12..7f09c93b 100644 --- a/common/src/main/java/ctbrec/sites/jasmin/LiveJasminHttpClient.java +++ b/common/src/main/java/ctbrec/sites/jasmin/LiveJasminHttpClient.java @@ -1,18 +1,172 @@ package ctbrec.sites.jasmin; import java.io.IOException; +import java.util.Collections; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import ctbrec.Config; import ctbrec.io.HttpClient; +import okhttp3.Cookie; +import okhttp3.HttpUrl; +import okhttp3.OkHttpClient; +import okhttp3.Request; +import okhttp3.Response; public class LiveJasminHttpClient extends HttpClient { + private static final transient Logger LOG = LoggerFactory.getLogger(LiveJasminHttpClient.class); + protected LiveJasminHttpClient() { super("livejasmin"); } @Override - public boolean login() throws IOException { + public synchronized boolean login() throws IOException { + if (loggedIn) { + return true; + } + + // set session cookie, if session id is available + if(!Config.getInstance().getSettings().livejasminSession.isEmpty()) { + Cookie captchaCookie = new Cookie.Builder() + .domain("livejasmin.com") + .name("session") + .value(Config.getInstance().getSettings().livejasminSession) + .build(); + getCookieJar().saveFromResponse(HttpUrl.parse("https://livejasmin.com"), Collections.singletonList(captchaCookie)); + getCookieJar().saveFromResponse(HttpUrl.parse("https://www.livejasmin.com"), Collections.singletonList(captchaCookie)); + getCookieJar().saveFromResponse(HttpUrl.parse("https://m.livejasmin.com"), Collections.singletonList(captchaCookie)); + } + + + // loadMainPage(); // to get initial cookies + // Cookie captchaCookie = new Cookie.Builder() + // .domain("livejasmin.com") + // .name("captchaRequired") + // .value("0") + // .build(); + // getCookieJar().saveFromResponse(HttpUrl.parse("https://livejasmin.com"), Collections.singletonList(captchaCookie)); + // getCookieJar().saveFromResponse(HttpUrl.parse("https://www.livejasmin.com"), Collections.singletonList(captchaCookie)); + // getCookieJar().saveFromResponse(HttpUrl.parse("https://m.livejasmin.com"), Collections.singletonList(captchaCookie)); + // Map formParams = getLoginFormParameters(); + // getCookieJar().saveFromResponse(HttpUrl.parse("https://livejasmin.com"), Collections.singletonList(captchaCookie)); + // getCookieJar().saveFromResponse(HttpUrl.parse("https://m.livejasmin.com"), Collections.singletonList(captchaCookie)); + // String action = formParams.get("action"); + // formParams.remove("action"); + // Builder formBuilder = new FormBody.Builder(); + // for (Entry param : formParams.entrySet()) { + // formBuilder.add(param.getKey(), param.getValue()); + // } + // formBuilder.add("username", Config.getInstance().getSettings().livejasminUsername); + // formBuilder.add("password", Config.getInstance().getSettings().livejasminPassword); + // FormBody form = formBuilder.build(); + // Buffer b = new Buffer(); + // form.writeTo(b); + // LOG.debug("Form: {}", b.readUtf8()); + // Map> cookies = getCookieJar().getCookies(); + // for (Entry> domain : cookies.entrySet()) { + // LOG.debug("{}", domain.getKey()); + // List cks = domain.getValue(); + // for (Cookie cookie : cks) { + // LOG.debug(" {}", cookie); + // } + // } + // Request request = new Request.Builder() + // .url(LiveJasmin.BASE_URL + action) + // .header("User-Agent", USER_AGENT) + // .header("Accept", "*/*") + // .header("Accept-Language", "en") + // .header("Referer", LiveJasmin.BASE_URL + "/en/girls/") + // .header("X-Requested-With", "XMLHttpRequest") + // .post(form) + // .build(); + // try(Response response = execute(request)) { + // System.out.println("login " + response.code() + " - " + response.message()); + // System.out.println("login " + response.body().string()); + // } + + boolean cookiesWorked = checkLoginSuccess(); + if (cookiesWorked) { + loggedIn = true; + LOG.debug("Logged in with cookies"); + return true; + } + return false; } + // private void loadMainPage() throws IOException { + // Request request = new Request.Builder() + // .url(LiveJasmin.BASE_URL) + // .header("User-Agent", USER_AGENT) + // .build(); + // try(Response response = execute(request)) { + // } + // } + // + // private Map getLoginFormParameters() throws IOException { + // long ts = System.currentTimeMillis(); + // String url = LiveJasmin.BASE_URL + "/en/auth/overlay/get-login-block?_dc="+ts; + // Request request = new Request.Builder() + // .url(url) + // .addHeader("User-Agent", USER_AGENT) + // .addHeader("Accept", "application/json, text/javascript, */*") + // .addHeader("Accept-Language", "en") + // .addHeader("Referer", LiveJasmin.BASE_URL) + // .addHeader("X-Requested-With", "XMLHttpRequest") + // .build(); + // try(Response response = execute(request)) { + // if(response.isSuccessful()) { + // String body = response.body().string(); + // JSONObject json = new JSONObject(body); + // if(json.optBoolean("success")) { + // JSONObject data = json.getJSONObject("data"); + // String content = data.getString("content"); + // Map params = new HashMap<>(); + // Element form = HtmlParser.getTag(content, "form"); + // params.put("action", form.attr("action")); + // Elements hiddenInputs = HtmlParser.getTags(content, "input[type=hidden]"); + // for (Element input : hiddenInputs) { + // String name = input.attr("name"); + // String value = input.attr("value"); + // params.put(name, value); + // } + // params.put("keepmeloggedin", "1"); + // params.put("captcha", ""); + // params.remove("captcha_needed"); + // return params; + // } else { + // throw new IOException("Request was not successful: " + body); + // } + // } else { + // throw new HttpException(response.code(), response.message()); + // } + // } + // } + + public boolean checkLoginSuccess() throws IOException { + OkHttpClient temp = client.newBuilder() + .followRedirects(false) + .followSslRedirects(false) + .build(); + String url = LiveJasmin.BASE_URL + "/en/free/favourite/get-favourite-list"; + Request request = new Request.Builder() + .url(url) + .addHeader("User-Agent", Config.getInstance().getSettings().httpUserAgent) + .addHeader("Accept", "application/json, text/javascript, */*") + .addHeader("Accept-Language", "en") + .addHeader("Referer", LiveJasmin.BASE_URL) + .addHeader("X-Requested-With", "XMLHttpRequest") + .build(); + try(Response response = temp.newCall(request).execute()) { + LOG.debug("Login Check {}: {} - {}", url, response.code(), response.message()); + if(response.isSuccessful()) { + return true; + } else { + return false; + } + } + } } diff --git a/common/src/main/java/ctbrec/sites/jasmin/LiveJasminMergedHlsDownload.java b/common/src/main/java/ctbrec/sites/jasmin/LiveJasminMergedHlsDownload.java new file mode 100644 index 00000000..583a4a31 --- /dev/null +++ b/common/src/main/java/ctbrec/sites/jasmin/LiveJasminMergedHlsDownload.java @@ -0,0 +1,48 @@ +package ctbrec.sites.jasmin; + +import java.io.IOException; +import java.util.concurrent.ExecutionException; +import java.util.concurrent.TimeUnit; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import com.iheartradio.m3u8.ParseException; +import com.iheartradio.m3u8.PlaylistException; + +import ctbrec.io.HttpClient; +import ctbrec.recorder.download.MergedHlsDownload; + +public class LiveJasminMergedHlsDownload extends MergedHlsDownload { + + private static final transient Logger LOG = LoggerFactory.getLogger(LiveJasminMergedHlsDownload.class); + private long lastMasterPlaylistUpdate = 0; + private String segmentUrl; + + public LiveJasminMergedHlsDownload(HttpClient client) { + super(client); + } + + @Override + protected SegmentPlaylist getNextSegments(String segments) throws IOException, ParseException, PlaylistException { + if(this.segmentUrl == null) { + this.segmentUrl = segments; + } + SegmentPlaylist playlist = super.getNextSegments(segmentUrl); + long now = System.currentTimeMillis(); + if( (now - lastMasterPlaylistUpdate) > TimeUnit.SECONDS.toMillis(60)) { + super.downloadThreadPool.submit(this::updatePlaylistUrl); + lastMasterPlaylistUpdate = now; + } + return playlist; + } + + private void updatePlaylistUrl() { + try { + LOG.debug("Updating segment playlist URL for {}", getModel()); + segmentUrl = getSegmentPlaylistUrl(getModel()); + } catch (IOException | ExecutionException | ParseException | PlaylistException e) { + LOG.error("Couldn't update segment playlist url. This might cause a premature download termination", e); + } + } +} diff --git a/common/src/main/java/ctbrec/sites/jasmin/LiveJasminModel.java b/common/src/main/java/ctbrec/sites/jasmin/LiveJasminModel.java index 184eb87a..82eae5b0 100644 --- a/common/src/main/java/ctbrec/sites/jasmin/LiveJasminModel.java +++ b/common/src/main/java/ctbrec/sites/jasmin/LiveJasminModel.java @@ -3,6 +3,7 @@ package ctbrec.sites.jasmin; import java.io.IOException; import java.io.InputStream; import java.util.ArrayList; +import java.util.Arrays; import java.util.List; import java.util.concurrent.ExecutionException; @@ -26,6 +27,8 @@ import com.squareup.moshi.JsonWriter; import ctbrec.AbstractModel; import ctbrec.Config; import ctbrec.io.HttpException; +import ctbrec.recorder.download.Download; +import ctbrec.recorder.download.HlsDownload; import ctbrec.recorder.download.StreamSource; import okhttp3.Request; import okhttp3.Response; @@ -35,20 +38,72 @@ public class LiveJasminModel extends AbstractModel { private static final transient Logger LOG = LoggerFactory.getLogger(LiveJasminModel.class); private String id; private boolean online = false; + private int[] resolution; @Override public boolean isOnline(boolean ignoreCache) throws IOException, ExecutionException, InterruptedException { if(ignoreCache) { - try { - getMasterPlaylistUrl(); - online = true; - } catch (Exception e) { - online = false; - } + loadModelInfo(); } return online; } + protected void loadModelInfo() throws IOException { + String url = "https://m.livejasmin.com/en/chat-html5/" + getName(); + Request req = new Request.Builder().url(url) + .header("User-Agent", "Mozilla/5.0 (iPhone; CPU OS 10_14 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/11.1.1 Mobile/14E304 Safari/605.1.15") + .header("Accept", "application/json,*/*") + .header("Accept-Language", "en") + .header("Referer", getSite().getBaseUrl()) + .header("X-Requested-With", "XMLHttpRequest") + .build(); + try(Response response = getSite().getHttpClient().execute(req)) { + if(response.isSuccessful()) { + String body = response.body().string(); + JSONObject json = new JSONObject(body); + //LOG.debug(json.toString(2)); + if(json.optBoolean("success")) { + JSONObject data = json.getJSONObject("data"); + JSONObject config = data.getJSONObject("config"); + JSONObject chatRoom = config.getJSONObject("chatRoom"); + setId(chatRoom.getString("p_id")); + if(chatRoom.has("profile_picture_url")) { + setPreview(chatRoom.getString("profile_picture_url")); + } + int status = chatRoom.optInt("status", -1); + onlineState = mapStatus(status); + if(chatRoom.optInt("is_on_private", 0) == 1) { + onlineState = State.PRIVATE; + } + resolution = new int[2]; + resolution[0] = config.optInt("streamWidth"); + resolution[1] = config.optInt("streamHeight"); + online = onlineState == State.ONLINE; + LOG.trace("{} - status:{} {} {} {}", getName(), online, onlineState, Arrays.toString(resolution), getUrl()); + } else { + throw new IOException("Response was not successful: " + body); + } + } else { + throw new HttpException(response.code(), response.message()); + } + } + } + + private State mapStatus(int status) { + switch(status) { + case 0: + return State.OFFLINE; + case 1: + return State.ONLINE; + case 2: + case 3: + return State.PRIVATE; + default: + LOG.debug("Unkown state {} {}", status, getUrl()); + return State.UNKNOWN; + } + } + @Override public List getStreamSources() throws IOException, ExecutionException, ParseException, PlaylistException { String masterUrl = getMasterPlaylistUrl(); @@ -86,6 +141,7 @@ public class LiveJasminModel extends AbstractModel { } private String getMasterPlaylistUrl() throws IOException { + loadModelInfo(); String url = site.getBaseUrl() + "/en/stream/hls/free/" + getName(); Request request = new Request.Builder() .url(url) @@ -99,14 +155,12 @@ public class LiveJasminModel extends AbstractModel { if (response.isSuccessful()) { String body = response.body().string(); JSONObject json = new JSONObject(body); - LOG.debug(json.toString(2)); if(json.optBoolean("success")) { JSONObject data = json.getJSONObject("data"); JSONObject hlsStream = data.getJSONObject("hls_stream"); return hlsStream.getString("url"); } else { - LOG.error("Request failed:\n{}", body); - throw new IOException("Response was not successfull"); + throw new IOException("Response was not successful: " + url + "\n" + body); } } else { throw new HttpException(response.code(), response.message()); @@ -124,7 +178,19 @@ public class LiveJasminModel extends AbstractModel { @Override public int[] getStreamResolution(boolean failFast) throws ExecutionException { - return new int[2]; + if(resolution == null) { + if(failFast) { + return new int[2]; + } + try { + loadModelInfo(); + } catch (IOException e) { + throw new ExecutionException(e); + } + return resolution; + } else { + return resolution; + } } @Override @@ -154,12 +220,11 @@ public class LiveJasminModel extends AbstractModel { @Override public void writeSiteSpecificData(JsonWriter writer) throws IOException { if(id == null) { - // TODO make sure the id is set - // try { - // loadModelInfo(); - // } catch (IOException e) { - // LOG.error("Couldn't load model ID for {}. This can cause problems with saving / loading the model", getName()); - // } + try { + loadModelInfo(); + } catch (IOException e) { + LOG.error("Couldn't load model ID for {}. This can cause problems with saving / loading the model", getName()); + } } writer.name("id").value(id); } @@ -167,4 +232,17 @@ public class LiveJasminModel extends AbstractModel { public void setOnline(boolean online) { this.online = online; } + + @Override + public Download createDownload() { + if(Config.getInstance().getSettings().livejasminSession.isEmpty()) { + if(Config.isServerMode()) { + return new HlsDownload(getSite().getHttpClient()); + } else { + return new LiveJasminMergedHlsDownload(getSite().getHttpClient()); + } + } else { + return new LiveJasminWebSocketDownload(getSite().getHttpClient()); + } + } } diff --git a/common/src/main/java/ctbrec/sites/jasmin/LiveJasminWebSocketDownload.java b/common/src/main/java/ctbrec/sites/jasmin/LiveJasminWebSocketDownload.java new file mode 100644 index 00000000..47327987 --- /dev/null +++ b/common/src/main/java/ctbrec/sites/jasmin/LiveJasminWebSocketDownload.java @@ -0,0 +1,308 @@ +package ctbrec.sites.jasmin; + +import java.io.File; +import java.io.FileOutputStream; +import java.io.IOException; +import java.io.UnsupportedEncodingException; +import java.net.URLEncoder; +import java.nio.file.Files; +import java.time.Instant; + +import org.json.JSONArray; +import org.json.JSONObject; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import ctbrec.Config; +import ctbrec.Model; +import ctbrec.io.HttpClient; +import ctbrec.recorder.download.Download; +import okhttp3.Request; +import okhttp3.Response; +import okhttp3.WebSocket; +import okhttp3.WebSocketListener; +import okio.ByteString; + +public class LiveJasminWebSocketDownload implements Download { + private static final transient Logger LOG = LoggerFactory.getLogger(LiveJasminWebSocketDownload.class); + + private String applicationId; + private String sessionId; + private String jsm2SessionId; + private String sb_ip; + private String sb_hash; + private String relayHost; + private String streamHost; + private String clientInstanceId = "01234567890123456789012345678901"; // TODO where to get or generate a random id? + private String streamPath = "streams/clonedLiveStream"; + private WebSocket relay; + private WebSocket stream; + + protected boolean connectionClosed; + private volatile boolean isAlive = true; + + private HttpClient client; + private Model model; + private Instant startTime; + private File targetFile; + + public LiveJasminWebSocketDownload(HttpClient client) { + this.client = client; + } + + @Override + public void start(Model model, Config config) throws IOException { + this.model = model; + startTime = Instant.now(); + File _targetFile = config.getFileForRecording(model); + targetFile = new File(_targetFile.getAbsolutePath().replace(".ts", ".mp4")); + + getPerformerDetails(model.getName()); + LOG.debug("appid: {}", applicationId); + LOG.debug("sessionid: {}",sessionId); + LOG.debug("jsm2sessionid: {}",jsm2SessionId); + LOG.debug("sb_ip: {}",sb_ip); + LOG.debug("sb_hash: {}",sb_hash); + LOG.debug("relay host: {}",relayHost); + LOG.debug("stream host: {}",streamHost); + LOG.debug("clientinstanceid {}",clientInstanceId); + + Request request = new Request.Builder() + .url("https://" + relayHost + "/") + .header("Origin", "https://www.livejasmin.com") + .header("User-Agent", "Mozilla/5.0 (X11; Linux x86_64; rv:63.0) Gecko/20100101 Firefox/63.0") + .header("Accept", "text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8") + .header("Accept-Language", "de,en-US;q=0.7,en;q=0.3") + .build(); + relay = client.newWebSocket(request, new WebSocketListener() { + boolean streamSocketStarted = false; + + @Override + public void onOpen(WebSocket webSocket, Response response) { + LOG.trace("relay open {}", model.getName()); + sendToRelay("{\"event\":\"register\",\"applicationId\":\"" + applicationId + + "\",\"connectionData\":{\"jasmin2App\":true,\"isMobileClient\":false,\"platform\":\"desktop\",\"chatID\":\"freechat\"," + + "\"sessionID\":\"" + sessionId + "\"," + "\"jsm2SessionId\":\"" + jsm2SessionId + "\",\"userType\":\"user\"," + "\"performerId\":\"" + + model + + "\",\"clientRevision\":\"\",\"proxyIP\":\"\",\"playerVer\":\"nanoPlayerVersion: 3.10.3 appCodeName: Mozilla appName: Netscape appVersion: 5.0 (X11) platform: Linux x86_64\",\"livejasminTvmember\":false,\"newApplet\":true,\"livefeedtype\":null,\"gravityCookieId\":\"\",\"passparam\":\"\",\"brandID\":\"jasmin\",\"cobrandId\":\"\",\"subbrand\":\"livejasmin\",\"siteName\":\"LiveJasmin\",\"siteUrl\":\"https://www.livejasmin.com\"," + + "\"clientInstanceId\":\"" + clientInstanceId + "\",\"armaVersion\":\"34.10.0\",\"isPassive\":false}}"); + response.close(); + } + + @Override + public void onMessage(WebSocket webSocket, String text) { + LOG.trace("relay <-- {} T{}", model.getName(), text); + JSONObject event = new JSONObject(text); + if (event.optString("event").equals("accept")) { + new Thread(() -> { + sendToRelay("{\"event\":\"connectSharedObject\",\"name\":\"data/chat_so\"}"); + }).start(); + } else if (event.optString("event").equals("updateSharedObject")) { + // TODO + JSONArray list = event.getJSONArray("list"); + for (int i = 0; i < list.length(); i++) { + JSONObject obj = list.getJSONObject(i); + if (obj.optString("name").equals("streamList")) { + LOG.debug(obj.toString(2)); + streamPath = getStreamPath(obj.getJSONObject("newValue")); + } + } + + if (!streamSocketStarted) { + streamSocketStarted = true; + sendToRelay("{\"event\":\"call\",\"funcName\":\"makeActive\",\"data\":[]}"); + new Thread(() -> { + try { + startStreamSocket(); + } catch (Exception e) { + LOG.error("Couldn't start stream websocket", e); + stop(); + } + }).start(); + } + }else if(event.optString("event").equals("call")) { + String func = event.optString("funcName"); + if(func.equals("closeConnection")) { + connectionClosed = true; + //System.out.println(event.get("data")); + stop(); + } + } + } + + private String getStreamPath(JSONObject obj) { + String streamName = "streams/clonedLiveStream"; + int height = 0; + if(obj.has("streams")) { + JSONArray streams = obj.getJSONArray("streams"); + for (int i = 0; i < streams.length(); i++) { + JSONObject stream = streams.getJSONObject(i); + int h = stream.optInt("height"); + if(h > height) { + height = h; + streamName = stream.getString("streamNameWithFolder"); + streamName = "free/" + stream.getString("name"); + } + } + } + return streamName; + } + + @Override + public void onMessage(WebSocket webSocket, ByteString bytes) { + LOG.trace("relay <-- {} B{}", model.getName(), bytes.toString()); + } + + @Override + public void onClosed(WebSocket webSocket, int code, String reason) { + LOG.trace("relay closed {} {} {}", code, reason, model.getName()); + } + + @Override + public void onFailure(WebSocket webSocket, Throwable t, Response response) { + if(!connectionClosed) { + LOG.trace("relay failure {}", model.getName(), t); + if (response != null) { + response.close(); + } + } + } + }); + } + + private void sendToRelay(String msg) { + LOG.trace("relay --> {} {}", model.getName(), msg); + relay.send(msg); + } + + protected void getPerformerDetails(String name) throws IOException { + String url = "https://m.livejasmin.com/en/chat-html5/" + name; + Request req = new Request.Builder() + .url(url) + .header("User-Agent", "Mozilla/5.0 (iPhone; CPU OS 10_14 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/11.1.1 Mobile/14E304 Safari/605.1.15") + .header("Accept", "application/json,*/*") + .header("Accept-Language", "en") + .header("Referer", "https://www.livejasmin.com") + .header("X-Requested-With", "XMLHttpRequest") + .build(); + try (Response response = client.execute(req)) { + if (response.isSuccessful()) { + String body = response.body().string(); + JSONObject json = new JSONObject(body); + // System.out.println(json.toString(2)); + if (json.optBoolean("success")) { + JSONObject data = json.getJSONObject("data"); + JSONObject config = data.getJSONObject("config"); + JSONObject armageddonConfig = config.getJSONObject("armageddonConfig"); + JSONObject chatRoom = config.getJSONObject("chatRoom"); + sessionId = armageddonConfig.getString("sessionid"); + jsm2SessionId = armageddonConfig.getString("jsm2session"); + sb_hash = chatRoom.getString("sb_hash"); + sb_ip = chatRoom.getString("sb_ip"); + applicationId = "memberChat/jasmin" + name + sb_hash; + relayHost = "dss-relay-" + sb_ip.replace('.', '-') + ".dditscdn.com"; + streamHost = "dss-live-" + sb_ip.replace('.', '-') + ".dditscdn.com"; + } else { + throw new IOException("Response was not successful: " + body); + } + } else { + throw new IOException(response.code() + " - " + response.message()); + } + } + } + + private void startStreamSocket() throws UnsupportedEncodingException { + String rtmpUrl = "rtmp://" + sb_ip + "/" + applicationId + "?sessionId-" + sessionId + "|clientInstanceId-" + clientInstanceId; + String url = "https://" + streamHost + "/stream/?url=" + URLEncoder.encode(rtmpUrl, "utf-8"); + url = url += "&stream=" + URLEncoder.encode(streamPath, "utf-8") + "&cid=863621&pid=49247581854"; + LOG.trace(rtmpUrl); + LOG.trace(url); + + Request request = new Request.Builder().url(url).header("Origin", "https://www.livejasmin.com") + .header("User-Agent", "Mozilla/5.0 (X11; Linux x86_64; rv:63.0) Gecko/20100101 Firefox/63.0") + .header("Accept", "text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8").header("Accept-Language", "de,en-US;q=0.7,en;q=0.3") + .build(); + stream = client.newWebSocket(request, new WebSocketListener() { + FileOutputStream fos; + + @Override + public void onOpen(WebSocket webSocket, Response response) { + LOG.trace("stream open {}", model.getName()); + // webSocket.send("{\"event\":\"ping\"}"); + // webSocket.send(""); + response.close(); + try { + Files.createDirectories(targetFile.getParentFile().toPath()); + fos = new FileOutputStream(targetFile); + } catch (IOException e) { + LOG.error("Couldn't create video file", e); + stop(); + } + } + + @Override + public void onMessage(WebSocket webSocket, String text) { + LOG.trace("stream <-- {} T{}", model.getName(), text); + JSONObject event = new JSONObject(text); + if(event.optString("eventType").equals("onRandomAccessPoint")) { + // send ping + sendToRelay("{\"event\":\"ping\"}"); + } + } + + @Override + public void onMessage(WebSocket webSocket, ByteString bytes) { + //System.out.println("stream <-- B" + bytes.toString()); + try { + fos.write(bytes.toByteArray()); + } catch (IOException e) { + LOG.error("Couldn't write video chunk to file", e); + stop(); + } + } + + @Override + public void onClosed(WebSocket webSocket, int code, String reason) { + LOG.trace("stream closed {} {} {}", code, reason, model.getName()); + } + + @Override + public void onFailure(WebSocket webSocket, Throwable t, Response response) { + if(!connectionClosed) { + LOG.trace("stream failure {}", model.getName(), t); + if (response != null) { + response.close(); + } + } + } + }); + } + + @Override + public void stop() { + connectionClosed = true; + stream.close(1000, ""); + relay.close(1000, ""); + isAlive = false; + } + + @Override + public boolean isAlive() { + return isAlive; + } + + @Override + public File getTarget() { + return targetFile; + } + + @Override + public Model getModel() { + return model; + } + + @Override + public Instant getStartTime() { + return startTime; + } +} diff --git a/common/src/main/java/ctbrec/sites/streamate/StreamateWebsocketClient.java b/common/src/main/java/ctbrec/sites/streamate/StreamateWebsocketClient.java index d13cde56..b7bd9b34 100644 --- a/common/src/main/java/ctbrec/sites/streamate/StreamateWebsocketClient.java +++ b/common/src/main/java/ctbrec/sites/streamate/StreamateWebsocketClient.java @@ -6,7 +6,9 @@ import java.util.regex.Pattern; import org.slf4j.Logger; import org.slf4j.LoggerFactory; +import ctbrec.Config; import ctbrec.io.HttpClient; +import okhttp3.Request; import okhttp3.Response; import okhttp3.WebSocket; import okhttp3.WebSocketListener; @@ -27,7 +29,10 @@ public class StreamateWebsocketClient { public String getRoomId() throws InterruptedException { LOG.debug("Connecting to {}", url); Object monitor = new Object(); - client.newWebSocket(url, new WebSocketListener() { + Request request = new Request.Builder() + .header("User-Agent", Config.getInstance().getSettings().httpUserAgent) + .build(); + client.newWebSocket(request, new WebSocketListener() { @Override public void onOpen(WebSocket webSocket, Response response) { response.close();