forked from j62/ctbrec
1
0
Fork 0

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.
This commit is contained in:
0xboobface 2018-12-22 19:44:45 +01:00
parent 4f3fd8a677
commit 2425a9dc60
15 changed files with 1001 additions and 56 deletions

View File

@ -0,0 +1,103 @@
package ctbrec.ui.sites.jasmin;
import ctbrec.Config;
import ctbrec.Settings;
import ctbrec.sites.jasmin.LiveJasmin;
import ctbrec.ui.DesktopIntegration;
import ctbrec.ui.settings.SettingsTab;
import ctbrec.ui.sites.AbstractConfigUI;
import javafx.geometry.Insets;
import javafx.scene.Parent;
import javafx.scene.control.Button;
import javafx.scene.control.CheckBox;
import javafx.scene.control.Label;
import javafx.scene.control.PasswordField;
import javafx.scene.control.TextField;
import javafx.scene.layout.GridPane;
import javafx.scene.layout.Priority;
public class LiveJasminConfigUi extends AbstractConfigUI {
private LiveJasmin liveJasmin;
public LiveJasminConfigUi(LiveJasmin liveJasmin) {
this.liveJasmin = liveJasmin;
}
@Override
public Parent createConfigPanel() {
Settings settings = Config.getInstance().getSettings();
GridPane layout = SettingsTab.createGridLayout();
int row = 0;
Label l = new Label("Active");
layout.add(l, 0, row);
CheckBox enabled = new CheckBox();
enabled.setSelected(!settings.disabledSites.contains(liveJasmin.getName()));
enabled.setOnAction((e) -> {
if(enabled.isSelected()) {
settings.disabledSites.remove(liveJasmin.getName());
} else {
settings.disabledSites.add(liveJasmin.getName());
}
save();
});
GridPane.setMargin(enabled, new Insets(0, 0, 0, SettingsTab.CHECKBOX_MARGIN));
layout.add(enabled, 1, row++);
layout.add(new Label("LiveJasmin User"), 0, row);
TextField username = new TextField(Config.getInstance().getSettings().livejasminUsername);
username.textProperty().addListener((ob, o, n) -> {
if(!n.equals(Config.getInstance().getSettings().livejasminUsername)) {
Config.getInstance().getSettings().livejasminUsername = n;
liveJasmin.getHttpClient().logout();
save();
}
});
GridPane.setFillWidth(username, true);
GridPane.setHgrow(username, Priority.ALWAYS);
GridPane.setColumnSpan(username, 2);
layout.add(username, 1, row++);
layout.add(new Label("LiveJasmin Password"), 0, row);
PasswordField password = new PasswordField();
password.setText(Config.getInstance().getSettings().livejasminPassword);
password.textProperty().addListener((ob, o, n) -> {
if(!n.equals(Config.getInstance().getSettings().livejasminPassword)) {
Config.getInstance().getSettings().livejasminPassword = n;
liveJasmin.getHttpClient().logout();
save();
}
});
GridPane.setFillWidth(password, true);
GridPane.setHgrow(password, Priority.ALWAYS);
GridPane.setColumnSpan(password, 2);
layout.add(password, 1, row++);
layout.add(new Label("LiveJasmin Session ID"), 0, row);
TextField sessionId = new TextField();
sessionId.setText(Config.getInstance().getSettings().livejasminSession);
sessionId.textProperty().addListener((ob, o, n) -> {
if(!n.equals(Config.getInstance().getSettings().livejasminSession)) {
Config.getInstance().getSettings().livejasminSession = n;
save();
}
});
GridPane.setFillWidth(sessionId, true);
GridPane.setHgrow(sessionId, Priority.ALWAYS);
GridPane.setColumnSpan(sessionId, 2);
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;
}
}

View File

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

View File

@ -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<Boolean> 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<Cookie> cookies = new ArrayList<>();
for (HttpCookie webViewCookie : loginDialog.getCookies()) {
Cookie cookie = Cookie.parse(url, webViewCookie.toString());
cookies.add(cookie);
}
cookieJar.saveFromResponse(url, cookies);
}
}
}

View File

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

View File

@ -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 = "";

View File

@ -35,10 +35,10 @@ import okhttp3.WebSocketListener;
public abstract class HttpClient {
private static final transient Logger LOG = LoggerFactory.getLogger(HttpClient.class);
protected OkHttpClient client;
protected OkHttpClient client;
protected CookieJarImpl cookieJar = new CookieJarImpl();
protected boolean loggedIn = false;
protected int loginTries = 0;
protected boolean loggedIn = false;
protected int loginTries = 0;
private String name;
protected HttpClient(String name) {
@ -93,19 +93,7 @@ public abstract class HttpClient {
}
}
// public Response execute(Request request) throws IOException {
// Response resp = execute(request, false);
// return resp;
// }
// public Response execute(Request req, boolean requiresLogin) throws IOException {
public Response execute(Request req) throws IOException {
// if(requiresLogin && !loggedIn) {
// loggedIn = login();
// if(!loggedIn) {
// throw new IOException("403 Unauthorized");
// }
// }
Response resp = client.newCall(req).execute();
return resp;
}
@ -222,8 +210,8 @@ public abstract class HttpClient {
loggedIn = false;
}
public WebSocket newWebSocket(String url, WebSocketListener l) {
Request request = new Request.Builder().url(url).build();
public WebSocket newWebSocket(Request request, WebSocketListener l) {
//Request request = new Request.Builder().url(url).build();
return client.newWebSocket(request, l);
}
}

View File

@ -193,6 +193,7 @@ public class LocalRecorder implements Recorder {
LOG.debug("Starting recording for model {}", model.getName());
Download download = model.createDownload();
LOG.debug("Downloading with {}", download.getClass().getSimpleName());
recordingProcesses.put(model, download);
new Thread() {
@Override
@ -461,7 +462,7 @@ public class LocalRecorder implements Recorder {
private List<Recording> listMergedRecordings() {
File recordingsDir = new File(config.getSettings().recordingsDir);
List<File> possibleRecordings = new LinkedList<>();
listRecursively(recordingsDir, possibleRecordings, (dir, name) -> name.matches(".*?_\\d{4}-\\d{2}-\\d{2}_\\d{2}-\\d{2}\\.ts"));
listRecursively(recordingsDir, possibleRecordings, (dir, name) -> name.matches(".*?_\\d{4}-\\d{2}-\\d{2}_\\d{2}-\\d{2}\\.(ts|mp4)"));
SimpleDateFormat sdf = new SimpleDateFormat(DATE_FORMAT);
List<Recording> recordings = new ArrayList<>();
for (File ts: possibleRecordings) {

View File

@ -8,9 +8,12 @@ import java.util.ArrayList;
import java.util.Collections;
import java.util.Iterator;
import java.util.List;
import java.util.concurrent.BlockingQueue;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.LinkedBlockingQueue;
import java.util.concurrent.ThreadPoolExecutor;
import java.util.concurrent.TimeUnit;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
@ -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<Runnable> 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<StreamSource> streamSources = model.getStreamSources();
Collections.sort(streamSources);

View File

@ -22,13 +22,9 @@ import java.time.ZonedDateTime;
import java.util.LinkedList;
import java.util.Optional;
import java.util.Queue;
import java.util.concurrent.BlockingQueue;
import java.util.concurrent.Callable;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Future;
import java.util.concurrent.LinkedBlockingQueue;
import java.util.concurrent.ThreadPoolExecutor;
import java.util.concurrent.TimeUnit;
import org.slf4j.Logger;
@ -63,8 +59,6 @@ public class MergedHlsDownload extends AbstractHlsDownload {
private ZonedDateTime splitRecStartTime;
private Config config;
private File targetFile;
private BlockingQueue<Runnable> downloadQueue = new LinkedBlockingQueue<>(50);
private ExecutorService downloadThreadPool = new ThreadPoolExecutor(5, 5, 2, TimeUnit.MINUTES, downloadQueue);
private FileChannel fileChannel = null;
private Object downloadFinished = new Object();

View File

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

View File

@ -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<String, String> 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<String, String> 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<String, List<Cookie>> cookies = getCookieJar().getCookies();
// for (Entry<String, List<Cookie>> domain : cookies.entrySet()) {
// LOG.debug("{}", domain.getKey());
// List<Cookie> 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<String, String> 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<String, String> 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;
}
}
}
}

View File

@ -0,0 +1,48 @@
package ctbrec.sites.jasmin;
import java.io.IOException;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.TimeUnit;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import com.iheartradio.m3u8.ParseException;
import com.iheartradio.m3u8.PlaylistException;
import ctbrec.io.HttpClient;
import ctbrec.recorder.download.MergedHlsDownload;
public class LiveJasminMergedHlsDownload extends MergedHlsDownload {
private static final transient Logger LOG = LoggerFactory.getLogger(LiveJasminMergedHlsDownload.class);
private long lastMasterPlaylistUpdate = 0;
private String segmentUrl;
public LiveJasminMergedHlsDownload(HttpClient client) {
super(client);
}
@Override
protected SegmentPlaylist getNextSegments(String segments) throws IOException, ParseException, PlaylistException {
if(this.segmentUrl == null) {
this.segmentUrl = segments;
}
SegmentPlaylist playlist = super.getNextSegments(segmentUrl);
long now = System.currentTimeMillis();
if( (now - lastMasterPlaylistUpdate) > TimeUnit.SECONDS.toMillis(60)) {
super.downloadThreadPool.submit(this::updatePlaylistUrl);
lastMasterPlaylistUpdate = now;
}
return playlist;
}
private void updatePlaylistUrl() {
try {
LOG.debug("Updating segment playlist URL for {}", getModel());
segmentUrl = getSegmentPlaylistUrl(getModel());
} catch (IOException | ExecutionException | ParseException | PlaylistException e) {
LOG.error("Couldn't update segment playlist url. This might cause a premature download termination", e);
}
}
}

View File

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

View File

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

View File

@ -6,7 +6,9 @@ import java.util.regex.Pattern;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import ctbrec.Config;
import ctbrec.io.HttpClient;
import okhttp3.Request;
import okhttp3.Response;
import okhttp3.WebSocket;
import okhttp3.WebSocketListener;
@ -27,7 +29,10 @@ public class StreamateWebsocketClient {
public String getRoomId() throws InterruptedException {
LOG.debug("Connecting to {}", url);
Object monitor = new Object();
client.newWebSocket(url, new WebSocketListener() {
Request request = new Request.Builder()
.header("User-Agent", Config.getInstance().getSettings().httpUserAgent)
.build();
client.newWebSocket(request, new WebSocketListener() {
@Override
public void onOpen(WebSocket webSocket, Response response) {
response.close();