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