diff --git a/client/src/main/java/ctbrec/ui/sites/showup/ShowupConfigUI.java b/client/src/main/java/ctbrec/ui/sites/showup/ShowupConfigUI.java new file mode 100644 index 00000000..7be1aed8 --- /dev/null +++ b/client/src/main/java/ctbrec/ui/sites/showup/ShowupConfigUI.java @@ -0,0 +1,86 @@ +package ctbrec.ui.sites.showup; + +import ctbrec.Config; +import ctbrec.Settings; +import ctbrec.sites.showup.Showup; +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 ShowupConfigUI extends AbstractConfigUI { + private Showup site; + + public ShowupConfigUI(Showup site) { + this.site = site; + } + + @Override + public Parent createConfigPanel() { + GridPane layout = SettingsTab.createGridLayout(); + Settings settings = Config.getInstance().getSettings(); + + int row = 0; + Label l = new Label("Active"); + layout.add(l, 0, row); + CheckBox enabled = new CheckBox(); + enabled.setSelected(!settings.disabledSites.contains(site.getName())); + enabled.setOnAction(e -> { + if(enabled.isSelected()) { + settings.disabledSites.remove(site.getName()); + } else { + settings.disabledSites.add(site.getName()); + } + save(); + }); + GridPane.setMargin(enabled, new Insets(0, 0, 0, SettingsTab.CHECKBOX_MARGIN)); + layout.add(enabled, 1, row++); + + layout.add(new Label("Showup User"), 0, row); + TextField username = new TextField(Config.getInstance().getSettings().showupUsername); + username.textProperty().addListener((ob, o, n) -> { + if(!n.equals(Config.getInstance().getSettings().showupUsername)) { + Config.getInstance().getSettings().showupUsername = username.getText(); + site.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("Showup Password"), 0, row); + PasswordField password = new PasswordField(); + password.setText(Config.getInstance().getSettings().showupPassword); + password.textProperty().addListener((ob, o, n) -> { + if(!n.equals(Config.getInstance().getSettings().showupPassword)) { + Config.getInstance().getSettings().showupPassword = password.getText(); + site.getHttpClient().logout(); + save(); + } + }); + GridPane.setFillWidth(password, true); + GridPane.setHgrow(password, Priority.ALWAYS); + GridPane.setColumnSpan(password, 2); + layout.add(password, 1, row++); + + Button createAccount = new Button("Create new Account"); + createAccount.setOnAction(e -> DesktopIntegration.open(site.getAffiliateLink())); + layout.add(createAccount, 1, row); + GridPane.setColumnSpan(createAccount, 2); + GridPane.setMargin(username, new Insets(0, 0, 0, SettingsTab.CHECKBOX_MARGIN)); + GridPane.setMargin(password, new Insets(0, 0, 0, SettingsTab.CHECKBOX_MARGIN)); + GridPane.setMargin(createAccount, new Insets(0, 0, 0, SettingsTab.CHECKBOX_MARGIN)); + return layout; + } + +} diff --git a/client/src/main/java/ctbrec/ui/sites/showup/ShowupElectronLoginDialog.java b/client/src/main/java/ctbrec/ui/sites/showup/ShowupElectronLoginDialog.java new file mode 100644 index 00000000..64a56c18 --- /dev/null +++ b/client/src/main/java/ctbrec/ui/sites/showup/ShowupElectronLoginDialog.java @@ -0,0 +1,133 @@ +package ctbrec.ui.sites.showup; + +import java.io.IOException; +import java.net.MalformedURLException; +import java.net.URL; +import java.util.Collections; +import java.util.Objects; +import java.util.function.Consumer; + +import org.json.JSONArray; +import org.json.JSONObject; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import ctbrec.Config; +import ctbrec.sites.showup.Showup; +import ctbrec.ui.ExternalBrowser; +import okhttp3.Cookie; +import okhttp3.Cookie.Builder; +import okhttp3.CookieJar; +import okhttp3.HttpUrl; + +public class ShowupElectronLoginDialog { + + private static final Logger LOG = LoggerFactory.getLogger(ShowupElectronLoginDialog.class); + public static final String DOMAIN = "showup.tv"; + public static final String URL = Showup.BASE_URL; + private CookieJar cookieJar; + private ExternalBrowser browser; + + public ShowupElectronLoginDialog(CookieJar cookieJar) throws IOException { + this.cookieJar = cookieJar; + browser = ExternalBrowser.getInstance(); + try { + JSONObject config = new JSONObject(); + config.put("url", URL); + config.put("w", 640); + config.put("h", 480); + JSONObject msg = new JSONObject(); + msg.put("config", config); + browser.run(msg, msgHandler); + } catch (InterruptedException e) { + Thread.currentThread().interrupt(); + throw new IOException("Couldn't wait for login dialog", e); + } finally { + browser.close(); + } + } + + private Consumer msgHandler = line -> { + if(!line.startsWith("{")) { + System.err.println(line); // NOSONAR + } else { + JSONObject json = new JSONObject(line); + //LOG.debug("Browser: {}", json.toString(2)); + if(json.has("url")) { + String url = json.getString("url"); + if(url.contains("/site/accept_rules")) { + try { + Thread.sleep(500); + String[] simplify = new String[] { + "document.getElementById(\"acceptrules\").submit();" + }; + for (String js : simplify) { + browser.executeJavaScript(js); + } + } catch(Exception e) { + LOG.warn("Couldn't auto fill username and password for Showup", e); + } + } else if(url.equals(URL + '/')) { + try { + Thread.sleep(500); + String[] simplify = new String[] { + //"document.ready(function() {$('a[class~=\"login-btn\"]').click()});", + "console.log('Hello CTBREC');", + "user.openLoginPopUp();" + }; + for (String js : simplify) { + browser.executeJavaScript(js); + } + String username = Config.getInstance().getSettings().showupUsername; + if (username != null && !username.trim().isEmpty()) { + browser.executeJavaScript("$('input[name=\"email\"]').attr('value','" + username + "')"); + } + String password = Config.getInstance().getSettings().showupPassword; + if (password != null && !password.trim().isEmpty()) { + browser.executeJavaScript("$('input[name=\"password\"]').attr('value','" + password + "')"); + } + browser.executeJavaScript("$('input[name=\"remember\"]').attr('value','true')"); + } catch(Exception e) { + LOG.warn("Couldn't auto fill username and password for Showup", e); + } + } + + if(json.has("cookies")) { + JSONArray _cookies = json.getJSONArray("cookies"); + for (int i = 0; i < _cookies.length(); i++) { + JSONObject cookie = _cookies.getJSONObject(i); + if(cookie.getString("domain").contains(DOMAIN)) { + Builder b = new Cookie.Builder() + .path(cookie.getString("path")) + .domain(DOMAIN) + .name(cookie.getString("name")) + .value(cookie.getString("value")) + .expiresAt(Double.valueOf(cookie.optDouble("expirationDate")).longValue()); // NOSONAR + if(cookie.optBoolean("hostOnly")) { + b.hostOnlyDomain(DOMAIN); + } + if(cookie.optBoolean("httpOnly")) { + b.httpOnly(); + } + if(cookie.optBoolean("secure")) { + b.secure(); + } + Cookie c = b.build(); + cookieJar.saveFromResponse(HttpUrl.parse(Showup.BASE_URL), Collections.singletonList(c)); + } + } + } + + try { + if (Objects.equals(new URL(url).getPath(), "/site/log_in")) { + browser.close(); + } + } catch (MalformedURLException e) { + LOG.error("Couldn't parse new url {}", url, e); + } catch (IOException e) { + LOG.error("Couldn't send shutdown request to external browser", e); + } + } + } + }; +} diff --git a/client/src/main/java/ctbrec/ui/sites/showup/ShowupFollowedTab.java b/client/src/main/java/ctbrec/ui/sites/showup/ShowupFollowedTab.java new file mode 100644 index 00000000..f4b42b4e --- /dev/null +++ b/client/src/main/java/ctbrec/ui/sites/showup/ShowupFollowedTab.java @@ -0,0 +1,79 @@ +package ctbrec.ui.sites.showup; + +import ctbrec.sites.showup.Showup; +import ctbrec.ui.tabs.FollowedTab; +import ctbrec.ui.tabs.ThumbOverviewTab; +import javafx.concurrent.WorkerStateEvent; +import javafx.geometry.Insets; +import javafx.scene.Scene; +import javafx.scene.control.Label; +import javafx.scene.control.RadioButton; +import javafx.scene.control.ToggleGroup; +import javafx.scene.input.KeyCode; +import javafx.scene.input.KeyEvent; +import javafx.scene.layout.HBox; + +public class ShowupFollowedTab extends ThumbOverviewTab implements FollowedTab { + private Label status; + boolean showOnline = true; + + public ShowupFollowedTab(String title, Showup showup) { + super(title, new ShowupFollowedUpdateService(showup), showup); + status = new Label("Logging in..."); + grid.getChildren().add(status); + } + + @Override + protected void createGui() { + super.createGui(); + addOnlineOfflineSelector(); + } + + private void addOnlineOfflineSelector() { + ToggleGroup group = new ToggleGroup(); + RadioButton online = new RadioButton("online"); + online.setToggleGroup(group); + RadioButton offline = new RadioButton("offline"); + offline.setToggleGroup(group); + pagination.getChildren().add(online); + pagination.getChildren().add(offline); + HBox.setMargin(online, new Insets(5, 5, 5, 40)); + HBox.setMargin(offline, new Insets(5, 5, 5, 5)); + online.setSelected(true); + group.selectedToggleProperty().addListener(e -> { + queue.clear(); + ((ShowupFollowedUpdateService)updateService).showOnline(online.isSelected()); + updateService.restart(); + }); + } + + @Override + protected void onSuccess() { + grid.getChildren().remove(status); + super.onSuccess(); + } + + @Override + protected void onFail(WorkerStateEvent event) { + String msg = ""; + if (event.getSource().getException() != null) { + msg = ": " + event.getSource().getException().getMessage(); + } + status.setText("Login failed" + msg); + super.onFail(event); + } + + @Override + public void selected() { + status.setText("Logging in..."); + super.selected(); + } + + public void setScene(Scene scene) { + scene.addEventFilter(KeyEvent.KEY_PRESSED, event -> { + if (this.isSelected() && event.getCode() == KeyCode.DELETE) { + follow(selectedThumbCells, false); + } + }); + } +} diff --git a/client/src/main/java/ctbrec/ui/sites/showup/ShowupFollowedUpdateService.java b/client/src/main/java/ctbrec/ui/sites/showup/ShowupFollowedUpdateService.java new file mode 100644 index 00000000..838c3d28 --- /dev/null +++ b/client/src/main/java/ctbrec/ui/sites/showup/ShowupFollowedUpdateService.java @@ -0,0 +1,97 @@ +package ctbrec.ui.sites.showup; + +import static ctbrec.io.HttpConstants.*; + +import java.io.IOException; +import java.util.ArrayList; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.stream.Collectors; + +import org.json.JSONArray; +import org.json.JSONObject; + +import ctbrec.Config; +import ctbrec.Model; +import ctbrec.io.HttpException; +import ctbrec.sites.showup.Showup; +import ctbrec.sites.showup.ShowupModel; +import ctbrec.ui.SiteUiFactory; +import ctbrec.ui.tabs.PaginatedScheduledService; +import javafx.concurrent.Task; +import okhttp3.Request; +import okhttp3.Response; + +public class ShowupFollowedUpdateService extends PaginatedScheduledService { + + private Showup site; + private boolean showOnline = true; + + public ShowupFollowedUpdateService(Showup site) { + this.site = site; + } + + @Override + protected Task> createTask() { + return new Task>() { + @Override + public List call() throws IOException { + SiteUiFactory.getUi(site).login(); + + Request request = new Request.Builder() + .url(site.getBaseUrl() + "/site/favorites") + .header(ACCEPT, MIMETYPE_APPLICATION_JSON) + .header(USER_AGENT, Config.getInstance().getSettings().httpUserAgent) + .header(X_REQUESTED_WITH, XML_HTTP_REQUEST) + .build(); + try (Response response = site.getHttpClient().execute(request)) { + if (response.isSuccessful()) { + String body = response.body().string(); + JSONObject json = new JSONObject(body); + if (json.optString("status").equalsIgnoreCase("success")) { + Map onlineModels = parseOnlineModels(json); + return parseFavorites(json).stream() + .filter(m -> onlineModels.containsKey(m.getUid()) == showOnline) + .collect(Collectors.toList()); + } else { + throw new RuntimeException("Request was not successful: " + body); + } + } else { + throw new HttpException(response.code(), response.message()); + } + } + } + + private List parseFavorites(JSONObject json) { + var favorites = new ArrayList(); + JSONArray list = json.getJSONArray("list"); + for (int i = 0; i < list.length(); i++) { + JSONObject m = list.getJSONObject(i); + ShowupModel model = new ShowupModel(); + model.setSite(site); + model.setUid(m.optLong("fav_uid")); + model.setName(m.optString("username")); + model.setUrl(site.getBaseUrl() + '/' + model.getName()); + + } + return favorites; + } + + private Map parseOnlineModels(JSONObject json) { + var onlineModels = new HashMap(); + JSONArray online = json.getJSONArray("online"); + for (int i = 0; i < online.length(); i++) { + JSONObject m = online.getJSONObject(i); + String preview = site.getBaseUrl() + "/files/" + m.optString("big_img") + ".jpg"; + onlineModels.put(m.optLong("uid"), preview); + } + return onlineModels; + } + }; + } + + void showOnline(boolean online) { + this.showOnline = online; + } +} diff --git a/client/src/main/java/ctbrec/ui/sites/showup/ShowupSiteUi.java b/client/src/main/java/ctbrec/ui/sites/showup/ShowupSiteUi.java index bd35d731..54d14085 100644 --- a/client/src/main/java/ctbrec/ui/sites/showup/ShowupSiteUi.java +++ b/client/src/main/java/ctbrec/ui/sites/showup/ShowupSiteUi.java @@ -1,33 +1,82 @@ package ctbrec.ui.sites.showup; import java.io.IOException; +import java.util.concurrent.BlockingQueue; +import java.util.concurrent.LinkedBlockingQueue; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; import ctbrec.sites.ConfigUI; import ctbrec.sites.showup.Showup; +import ctbrec.ui.controls.Dialogs; import ctbrec.ui.sites.AbstractSiteUi; import ctbrec.ui.tabs.TabProvider; public class ShowupSiteUi extends AbstractSiteUi { + private static final Logger LOG = LoggerFactory.getLogger(ShowupSiteUi.class); + private Showup site; + private ConfigUI configUi; + private TabProvider tabProvider; public ShowupSiteUi(Showup site) { this.site = site; + configUi = new ShowupConfigUI(site); + tabProvider = new ShowupTabProvider(site); } @Override public TabProvider getTabProvider() { - return new ShowupTabProvider(site); + return tabProvider; } @Override public ConfigUI getConfigUI() { - return null; + return configUi; } @Override public boolean login() throws IOException { - return false; + boolean automaticLogin = site.login(); + if(automaticLogin) { + return true; + } else { + BlockingQueue queue = new LinkedBlockingQueue<>(); + try { + new Thread(() -> { + // login with external browser window + try { + new ShowupElectronLoginDialog(site.getHttpClient().getCookieJar()); + } catch (Exception e1) { + LOG.error("Error logging in with external browser", e1); + Dialogs.showError("Login error", "Couldn't login to " + site.getName(), e1); + } + + try { + queue.put(true); + } catch (InterruptedException e) { + LOG.error("Error while signaling termination", e); + Thread.currentThread().interrupt(); + } + }).start(); + queue.take(); + } catch (InterruptedException e) { + Thread.currentThread().interrupt(); + throw new IOException(e); + } + + // ShowupHttpClient httpClient = (ShowupHttpClient)site.getHttpClient(); + // boolean loggedIn = httpClient.checkLoginSuccess(); + // if(loggedIn) { + // LOG.info("Logged in. User ID is {}", httpClient.getUserId()); + // } else { + // LOG.info("Login failed"); + // } + // return loggedIn; + return true; + } } } diff --git a/client/src/main/java/ctbrec/ui/sites/showup/ShowupTabProvider.java b/client/src/main/java/ctbrec/ui/sites/showup/ShowupTabProvider.java index 5831216c..d195a49c 100644 --- a/client/src/main/java/ctbrec/ui/sites/showup/ShowupTabProvider.java +++ b/client/src/main/java/ctbrec/ui/sites/showup/ShowupTabProvider.java @@ -23,6 +23,7 @@ public class ShowupTabProvider extends TabProvider { tabs.add(createTab("Women", "female")); tabs.add(createTab("Men", "male")); tabs.add(createTab("All", "all")); + //tabs.add(new ShowupFollowedTab("Favorites", site)); return tabs; } diff --git a/common/src/main/java/ctbrec/Settings.java b/common/src/main/java/ctbrec/Settings.java index d9dd8706..906313a9 100644 --- a/common/src/main/java/ctbrec/Settings.java +++ b/common/src/main/java/ctbrec/Settings.java @@ -110,6 +110,8 @@ public class Settings { public boolean requireAuthentication = false; public String servletContext = ""; public boolean showPlayerStarting = false; + public String showupUsername = ""; + public String showupPassword = ""; public boolean singlePlayer = true; public int splitRecordings = 0; public String startTab = "Settings"; diff --git a/common/src/main/java/ctbrec/sites/showup/Showup.java b/common/src/main/java/ctbrec/sites/showup/Showup.java index 36ad387c..159d921b 100644 --- a/common/src/main/java/ctbrec/sites/showup/Showup.java +++ b/common/src/main/java/ctbrec/sites/showup/Showup.java @@ -28,7 +28,9 @@ import okhttp3.Response; public class Showup extends AbstractSite { - private static final transient Logger LOG = LoggerFactory.getLogger(Showup.class); + private static final Logger LOG = LoggerFactory.getLogger(Showup.class); + public static final String BASE_URL = "https://showup.tv"; + private ShowupHttpClient httpClient; @Override @@ -38,7 +40,7 @@ public class Showup extends AbstractSite { @Override public String getBaseUrl() { - return "https://showup.tv"; + return BASE_URL; } @Override @@ -113,7 +115,7 @@ public class Showup extends AbstractSite { @Override public boolean login() throws IOException { - return false; + return getHttpClient().login(); } @Override diff --git a/common/src/main/java/ctbrec/sites/showup/ShowupHttpClient.java b/common/src/main/java/ctbrec/sites/showup/ShowupHttpClient.java index a2b9ced0..c99b5a8f 100644 --- a/common/src/main/java/ctbrec/sites/showup/ShowupHttpClient.java +++ b/common/src/main/java/ctbrec/sites/showup/ShowupHttpClient.java @@ -1,18 +1,35 @@ package ctbrec.sites.showup; +import static ctbrec.io.HttpConstants.*; + import java.io.IOException; import java.util.ArrayList; import java.util.Iterator; import java.util.List; import java.util.Map; +import java.util.regex.Matcher; +import java.util.regex.Pattern; + +import org.json.JSONObject; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; import com.google.common.base.Objects; +import ctbrec.Config; +import ctbrec.Settings; import ctbrec.io.HttpClient; +import ctbrec.io.HttpException; import okhttp3.Cookie; +import okhttp3.FormBody; +import okhttp3.Request; +import okhttp3.Response; public class ShowupHttpClient extends HttpClient { + private static final Logger LOG = LoggerFactory.getLogger(ShowupHttpClient.class); + private String csrfToken; + protected ShowupHttpClient() { super("showup"); setCookie("accept_rules", "true"); @@ -20,7 +37,11 @@ public class ShowupHttpClient extends HttpClient { } public void setCookie(String name, String value) { - Cookie cookie = new Cookie.Builder().domain("showup.tv").name(name).value(value).build(); + Cookie cookie = new Cookie.Builder() + .domain("showup.tv") + .name(name) + .value(value) + .build(); Map> cookies = cookieJar.getCookies(); List cookiesForDomain = cookies.computeIfAbsent(cookie.domain(), k -> new ArrayList()); @@ -33,9 +54,63 @@ public class ShowupHttpClient extends HttpClient { cookiesForDomain.add(cookie); } - @Override - public boolean login() throws IOException { - return false; + public String getCsrfToken() throws IOException { + if (csrfToken == null) { + Request req = new Request.Builder() + .url(Showup.BASE_URL + "/site/log_in") + .header(USER_AGENT, Config.getInstance().getSettings().httpUserAgent) + .build(); + + try (Response response = execute(req)) { + if (response.isSuccessful()) { + String body = response.body().string(); + Matcher m = Pattern.compile("var csrf = '(.*?)';").matcher(body); + if (m.matches()) { + csrfToken = m.group(1); + } else { + throw new RuntimeException("CSRF Token not found"); // NOSONAR + } + } else { + throw new HttpException(response.code(), response.message()); + } + } + } + return csrfToken; } + @Override + public boolean login() throws IOException { + Settings settings = Config.getInstance().getSettings(); + FormBody body = new FormBody.Builder() + .add("is_ajax", "1") + .add("email", settings.showupUsername) + .add("password", settings.showupPassword) + .add("remember", "1") + .build(); + Request req = new Request.Builder() + .url(Showup.BASE_URL) + .header(USER_AGENT, Config.getInstance().getSettings().httpUserAgent) + .header(X_REQUESTED_WITH, XML_HTTP_REQUEST) + .header(REFERER, Showup.BASE_URL + '/') + .header(ORIGIN, Showup.BASE_URL) + .header(ACCEPT, MIMETYPE_APPLICATION_JSON) + .post(body) + .build(); + + try (Response response = execute(req)) { + if (response.isSuccessful()) { + String responseBody = response.body().string(); + if (responseBody.startsWith("{")) { + JSONObject json = new JSONObject(responseBody); + return json.optString("status").equalsIgnoreCase("success"); + } else { + String msg = "Login was not successful"; + LOG.warn("{}\n{}", msg, responseBody); + return false; + } + } else { + throw new HttpException(response.code(), response.message()); + } + } + } }