From 2ffdbfa71ae1ddfd88aaa053d014266a0d229cb4 Mon Sep 17 00:00:00 2001 From: 0xboobface <0xboobface@gmail.com> Date: Mon, 29 Oct 2018 21:53:41 +0100 Subject: [PATCH] Add followed tab for Cam4 --- src/main/java/ctbrec/sites/cam4/Cam4.java | 58 ++++++++++-- .../ctbrec/sites/cam4/Cam4FollowedTab.java | 75 +++++++++++++++ .../sites/cam4/Cam4FollowedUpdateService.java | 94 +++++++++++++++++++ .../ctbrec/sites/cam4/Cam4HttpClient.java | 43 ++++++++- .../java/ctbrec/sites/cam4/Cam4Model.java | 60 +++++++++++- .../ctbrec/sites/cam4/Cam4TabProvider.java | 6 +- .../ctbrec/sites/cam4/Cam4UpdateService.java | 4 +- 7 files changed, 319 insertions(+), 21 deletions(-) create mode 100644 src/main/java/ctbrec/sites/cam4/Cam4FollowedTab.java create mode 100644 src/main/java/ctbrec/sites/cam4/Cam4FollowedUpdateService.java diff --git a/src/main/java/ctbrec/sites/cam4/Cam4.java b/src/main/java/ctbrec/sites/cam4/Cam4.java index 34c9572b..5d2f1957 100644 --- a/src/main/java/ctbrec/sites/cam4/Cam4.java +++ b/src/main/java/ctbrec/sites/cam4/Cam4.java @@ -2,17 +2,31 @@ package ctbrec.sites.cam4; import java.io.IOException; +import org.slf4j.LoggerFactory; + +import ctbrec.Config; import ctbrec.Model; import ctbrec.io.HttpClient; import ctbrec.recorder.Recorder; import ctbrec.sites.AbstractSite; +import ctbrec.ui.DesktopIntergation; +import ctbrec.ui.SettingsTab; import ctbrec.ui.TabProvider; +import javafx.geometry.Insets; import javafx.scene.Node; +import javafx.scene.control.Button; +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 Cam4 extends AbstractSite { public static final String BASE_URI = "https://www.cam4.com"; + public static final String AFFILIATE_LINK = BASE_URI + "/?referrerId=1514a80d87b5effb456cca02f6743aa1"; + private HttpClient httpClient; private Recorder recorder; @@ -28,7 +42,7 @@ public class Cam4 extends AbstractSite { @Override public String getAffiliateLink() { - return getBaseUrl() + "/?referrerId=1514a80d87b5effb456cca02f6743aa1"; + return AFFILIATE_LINK; } @Override @@ -62,7 +76,10 @@ public class Cam4 extends AbstractSite { @Override public void login() throws IOException { - getHttpClient().login(); + if (credentialsAvailable()) { + boolean success = getHttpClient().login(); + LoggerFactory.getLogger(getClass()).debug("Login success: {}", success); + } } @Override @@ -89,7 +106,7 @@ public class Cam4 extends AbstractSite { @Override public boolean supportsFollow() { - return false; + return true; } @Override @@ -98,13 +115,38 @@ public class Cam4 extends AbstractSite { } @Override - public Node getConfigurationGui() { - return null; + public boolean credentialsAvailable() { + String username = Config.getInstance().getSettings().cam4Username; + return username != null && !username.trim().isEmpty(); } @Override - public boolean credentialsAvailable() { - return false; - } + public Node getConfigurationGui() { + GridPane layout = SettingsTab.createGridLayout(); + layout.add(new Label("Cam4 User"), 0, 0); + TextField username = new TextField(Config.getInstance().getSettings().cam4Username); + username.focusedProperty().addListener((e) -> Config.getInstance().getSettings().cam4Username = username.getText()); + GridPane.setFillWidth(username, true); + GridPane.setHgrow(username, Priority.ALWAYS); + GridPane.setColumnSpan(username, 2); + layout.add(username, 1, 0); + layout.add(new Label("Cam4 Password"), 0, 1); + PasswordField password = new PasswordField(); + password.setText(Config.getInstance().getSettings().cam4Password); + password.focusedProperty().addListener((e) -> Config.getInstance().getSettings().cam4Password = password.getText()); + GridPane.setFillWidth(password, true); + GridPane.setHgrow(password, Priority.ALWAYS); + GridPane.setColumnSpan(password, 2); + layout.add(password, 1, 1); + + Button createAccount = new Button("Create new Account"); + createAccount.setOnAction((e) -> DesktopIntergation.open(Cam4.AFFILIATE_LINK)); + layout.add(createAccount, 1, 2); + 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/src/main/java/ctbrec/sites/cam4/Cam4FollowedTab.java b/src/main/java/ctbrec/sites/cam4/Cam4FollowedTab.java new file mode 100644 index 00000000..bd25fa90 --- /dev/null +++ b/src/main/java/ctbrec/sites/cam4/Cam4FollowedTab.java @@ -0,0 +1,75 @@ +package ctbrec.sites.cam4; + +import ctbrec.ui.FollowedTab; +import ctbrec.ui.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 Cam4FollowedTab extends ThumbOverviewTab implements FollowedTab { + private Label status; + + public Cam4FollowedTab(Cam4 cam4) { + super("Followed", new Cam4FollowedUpdateService(cam4), cam4); + 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) -> { + ((Cam4FollowedUpdateService)updateService).setShowOnline(online.isSelected()); + queue.clear(); + updateService.restart(); + }); + } + + @Override + protected void onSuccess() { + grid.getChildren().remove(status); + super.onSuccess(); + } + + @Override + protected void onFail(WorkerStateEvent event) { + status.setText("Login failed"); + 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()) { + if(event.getCode() == KeyCode.DELETE) { + follow(selectedThumbCells, false); + } + } + }); + } +} diff --git a/src/main/java/ctbrec/sites/cam4/Cam4FollowedUpdateService.java b/src/main/java/ctbrec/sites/cam4/Cam4FollowedUpdateService.java new file mode 100644 index 00000000..c752fb4a --- /dev/null +++ b/src/main/java/ctbrec/sites/cam4/Cam4FollowedUpdateService.java @@ -0,0 +1,94 @@ +package ctbrec.sites.cam4; + +import java.io.IOException; +import java.util.ArrayList; +import java.util.List; +import java.util.concurrent.ExecutionException; +import java.util.concurrent.ExecutorService; +import java.util.concurrent.Executors; +import java.util.concurrent.ThreadFactory; +import java.util.stream.Collectors; + +import org.jsoup.nodes.Element; +import org.jsoup.select.Elements; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import ctbrec.Config; +import ctbrec.Model; +import ctbrec.ui.HtmlParser; +import ctbrec.ui.PaginatedScheduledService; +import javafx.concurrent.Task; +import okhttp3.Request; +import okhttp3.Response; + +public class Cam4FollowedUpdateService extends PaginatedScheduledService { + + private static final transient Logger LOG = LoggerFactory.getLogger(Cam4FollowedUpdateService.class); + private Cam4 site; + private boolean showOnline = true; + + public Cam4FollowedUpdateService(Cam4 site) { + this.site = site; + ExecutorService executor = Executors.newSingleThreadExecutor(new ThreadFactory() { + @Override + public Thread newThread(Runnable r) { + Thread t = new Thread(r); + t.setDaemon(true); + t.setName("ThumbOverviewTab UpdateService"); + return t; + } + }); + setExecutor(executor); + } + + @Override + protected Task> createTask() { + return new Task>() { + @Override + public List call() throws IOException { + List models = new ArrayList<>(); + String username = Config.getInstance().getSettings().cam4Username; + String url = site.getBaseUrl() + '/' + username + "/edit/friends_favorites"; + Request req = new Request.Builder().url(url).build(); + Response response = site.getHttpClient().execute(req, true); + if(response.isSuccessful()) { + String content = response.body().string(); + Elements cells = HtmlParser.getTags(content, "div#favorites div.ff_thumb"); + for (Element cell : cells) { + String cellHtml = cell.html(); + Element link = HtmlParser.getTag(cellHtml, "div.ff_img a"); + String path = link.attr("href"); + String modelName = path.substring(1); + Cam4Model model = (Cam4Model) site.createModel(modelName); + model.setPreview("https://snapshots.xcdnpro.com/thumbnails/"+model.getName()+"?s=" + System.currentTimeMillis()); + model.setOnlineState(parseOnlineState(cellHtml)); + models.add(model); + } + return models.stream() + .filter(m -> { + try { + return m.isOnline() == showOnline; + } catch (IOException | ExecutionException | InterruptedException e) { + LOG.error("Couldn't determine online state", e); + return false; + } + }).collect(Collectors.toList()); + } else { + IOException e = new IOException(response.code() + " " + response.message()); + response.close(); + throw e; + } + } + + private String parseOnlineState(String cellHtml) { + Element state = HtmlParser.getTag(cellHtml, "div.ff_name div"); + return state.attr("class").equals("online") ? "NORMAL" : "OFFLINE"; + } + }; + } + + public void setShowOnline(boolean online) { + this.showOnline = online; + } +} diff --git a/src/main/java/ctbrec/sites/cam4/Cam4HttpClient.java b/src/main/java/ctbrec/sites/cam4/Cam4HttpClient.java index 3649dc9d..0a360b6b 100644 --- a/src/main/java/ctbrec/sites/cam4/Cam4HttpClient.java +++ b/src/main/java/ctbrec/sites/cam4/Cam4HttpClient.java @@ -5,10 +5,15 @@ import java.net.HttpCookie; import java.util.ArrayList; import java.util.List; import java.util.Objects; +import java.util.concurrent.BlockingQueue; +import java.util.concurrent.LinkedBlockingQueue; import org.json.JSONObject; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; import ctbrec.io.HttpClient; +import javafx.application.Platform; import okhttp3.Cookie; import okhttp3.HttpUrl; import okhttp3.Request; @@ -16,15 +21,43 @@ import okhttp3.Response; public class Cam4HttpClient extends HttpClient { + private static final transient Logger LOG = LoggerFactory.getLogger(Cam4HttpClient.class); + @Override public boolean login() throws IOException { - // login with javafx WebView - Cam4LoginDialog loginDialog = new Cam4LoginDialog(); + BlockingQueue queue = new LinkedBlockingQueue<>(); + LOG.debug("Launching dialog"); - // transfer cookies from WebView to OkHttp cookie jar - transferCookies(loginDialog); + Runnable showDialog = () -> { + // login with javafx WebView + Cam4LoginDialog loginDialog = new Cam4LoginDialog(); - return checkLoginSuccess(); + // transfer cookies from WebView to OkHttp cookie jar + transferCookies(loginDialog); + + try { + queue.put(true); + } catch (InterruptedException e) { + LOG.error("Error while signaling termination", e); + } + }; + + if(Platform.isFxApplicationThread()) { + showDialog.run(); + } else { + Platform.runLater(showDialog); + LOG.debug("waiting for queue"); + try { + queue.take(); + } catch (InterruptedException e) { + LOG.error("Error while waiting for login dialog to close", e); + throw new IOException(e); + } + } + + + loggedIn = checkLoginSuccess(); + return loggedIn; } /** diff --git a/src/main/java/ctbrec/sites/cam4/Cam4Model.java b/src/main/java/ctbrec/sites/cam4/Cam4Model.java index 35b96268..6ef57fb3 100644 --- a/src/main/java/ctbrec/sites/cam4/Cam4Model.java +++ b/src/main/java/ctbrec/sites/cam4/Cam4Model.java @@ -9,6 +9,7 @@ import java.util.concurrent.ExecutionException; import org.json.JSONArray; import org.json.JSONObject; +import org.jsoup.nodes.Element; import org.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -22,9 +23,13 @@ import com.iheartradio.m3u8.data.Playlist; import com.iheartradio.m3u8.data.PlaylistData; import ctbrec.AbstractModel; +import ctbrec.Config; import ctbrec.recorder.download.StreamSource; import ctbrec.sites.Site; +import ctbrec.ui.HtmlParser; +import okhttp3.FormBody; import okhttp3.Request; +import okhttp3.RequestBody; import okhttp3.Response; public class Cam4Model extends AbstractModel { @@ -147,14 +152,57 @@ public class Cam4Model extends AbstractModel { @Override public boolean follow() throws IOException { - // TODO Auto-generated method stub - return false; + String url = site.getBaseUrl() + "/profiles/addFriendFavorite?action=addFavorite&object=" + getName() + "&_=" + System.currentTimeMillis(); + Request req = new Request.Builder() + .url(url) + .addHeader("X-Requested-With", "XMLHttpRequest") + .build(); + Response response = site.getHttpClient().execute(req, true); + boolean success = response.isSuccessful(); + response.close(); + return success; } @Override public boolean unfollow() throws IOException { - // TODO Auto-generated method stub - return false; + // get model user id + String url = site.getBaseUrl() + '/' + getName(); + Request req = new Request.Builder().url(url).build(); + Response response = site.getHttpClient().execute(req, true); + String broadCasterId = null; + if(response.isSuccessful()) { + String content = response.body().string(); + try { + Element tag = HtmlParser.getTag(content, "input[name=\"broadcasterId\"]"); + broadCasterId = tag.attr("value"); + } catch(Exception e) { + LOG.debug(content); + throw new IOException(e); + } + + // send unfollow request + String username = Config.getInstance().getSettings().cam4Username; + url = site.getBaseUrl() + '/' + username + "/edit/friends_favorites"; + RequestBody body = new FormBody.Builder() + .add("deleteFavorites", broadCasterId) + .add("simpleresult", "true") + .build(); + req = new Request.Builder() + .url(url) + .post(body) + .addHeader("X-Requested-With", "XMLHttpRequest") + .build(); + response = site.getHttpClient().execute(req, true); + if(response.isSuccessful()) { + return Objects.equals(response.body().string(), "Ok"); + } else { + response.close(); + return false; + } + } else { + response.close(); + return false; + } } @Override @@ -174,4 +222,8 @@ public class Cam4Model extends AbstractModel { public void setPlaylistUrl(String playlistUrl) { this.playlistUrl = playlistUrl; } + + public void setOnlineState(String onlineState) { + this.onlineState = onlineState; + } } diff --git a/src/main/java/ctbrec/sites/cam4/Cam4TabProvider.java b/src/main/java/ctbrec/sites/cam4/Cam4TabProvider.java index b813057d..f38254ca 100644 --- a/src/main/java/ctbrec/sites/cam4/Cam4TabProvider.java +++ b/src/main/java/ctbrec/sites/cam4/Cam4TabProvider.java @@ -25,7 +25,11 @@ public class Cam4TabProvider extends TabProvider { tabs.add(createTab("Female", cam4.getBaseUrl() + "/directoryResults?online=true&gender=female&orderBy=MOST_VIEWERS")); tabs.add(createTab("HD", cam4.getBaseUrl() + "/directoryResults?online=true&hd=true&orderBy=VIDEO_QUALITY")); - + if(cam4.credentialsAvailable()) { + Cam4FollowedTab followed = new Cam4FollowedTab(cam4); + followed.setRecorder(recorder); + tabs.add(followed); + } return tabs; } diff --git a/src/main/java/ctbrec/sites/cam4/Cam4UpdateService.java b/src/main/java/ctbrec/sites/cam4/Cam4UpdateService.java index 5823f285..bcd89205 100644 --- a/src/main/java/ctbrec/sites/cam4/Cam4UpdateService.java +++ b/src/main/java/ctbrec/sites/cam4/Cam4UpdateService.java @@ -52,7 +52,7 @@ public class Cam4UpdateService extends PaginatedScheduledService { return new Task>() { @Override public List call() throws IOException { - if(loginRequired && StringUtil.isBlank(Config.getInstance().getSettings().username)) { // FIXME change to cam4 username + if(loginRequired && StringUtil.isBlank(Config.getInstance().getSettings().cam4Username)) { return Collections.emptyList(); } else { String url = Cam4UpdateService.this.url + "&page=" + page; @@ -72,10 +72,8 @@ public class Cam4UpdateService extends PaginatedScheduledService { Cam4Model model = (Cam4Model) site.createModel(slug); String playlistUrl = profileLink.attr("data-hls-preview-url"); model.setPlaylistUrl(playlistUrl); - //model.setPreview(HtmlParser.getTag(boxHtml, "a img").attr("data-src")); model.setPreview("https://snapshots.xcdnpro.com/thumbnails/"+model.getName()+"?s=" + System.currentTimeMillis()); model.setDescription(parseDesription(boxHtml)); - //model.setOnlineState(parseOnlineState(boxHtml)); models.add(model); } response.close();